很多 C++ 方面的書籍都說明了虛析構的作用:
- 保證派生類的解構式被呼叫,并且使析構順序與建構式相反
- 保證資源能夠被正確釋放
很久一段時間以來,我一直認為第 2 點僅僅指的是:當派生類使用 RAII 手法時,如果派生類的析構沒有被呼叫,就會產生資源泄露,就像下面的代碼:
#include <iostream>
struct A
{
A() {
std::cout << "A::A" << std::endl;
}
~A() {
std::cout << "A::~A" << std::endl;
}
};
struct B : A
{
B() {
x = new int;
std::cout << "B::B" << std::endl;
}
~B() {
delete x;
std::cout << "B::~B" << std::endl;
}
int* x;
};
int main()
{
A* a = new B;
delete a;
}
這段代碼結果輸出:
A::A
B::B
A::~A
B 的解構式沒被呼叫,a->x 沒有被正確釋放,產生了記憶體泄漏,
后來發現在多重繼承情況下,情況可能更加嚴重,例如以下代碼:
#include <iostream>
struct A1
{
A1() : a1(0) {}
~A1() {
std::cout << "A1::~A1" << std::endl;
}
int a1;
};
struct A2
{
A2() : a2(0) {}
~A2() {
std::cout << "A2::~A2" << std::endl;
}
int a2;
};
struct B : A1, A2
{
B() : b(0) {}
~B() {
std::cout << "B::~B" << std::endl;
}
int b;
};
int main()
{
B* b = new B;
A1* a1 = b;
A2* a2 = b;
printf("%p %p %p\n", b, a1, a2);
delete a2;
}
輸出:
0x5cbeb0 0x5cbeb0 0x5cbeb4
A2::~A2
free(): invalid pointer
已放棄 (核心已轉儲)
B* 隱式轉型成 A2*,C++ 派生類指標(參考)轉型為基類指標(參考)被稱為 upcast,upcast 在單一繼承的情況下,指標沒有進行偏移,但是在多重繼承下,會進行指標偏移,可以看到在多重繼承下,第 2 個基類指標與派生類指標不同,再看 delete b 生成的匯編代碼:
movq -40(%rbp), %rbx # %rbx = a2
testq %rbx, %rbx # a2 == 0 ?
je .L8
movq %rbx, %rdi # A2's this ptr = a2
call A2::~A2() [complete object destructor]
movl $4, %esi
movq %rbx, %rdi
call operator delete(void*, unsigned long) # call operator delete(a2, 4)
可以看到先呼叫了 A2::~A2(),再呼叫了 operator delete(a2, 12), 傳給底層 free() 函式的指標是 a2(0x5cbeb4),正確的指標應該是 b(0x5cbeb0),而且第2個引數傳遞的是 4,是 A2 的大小,不是 B 的大小,free() 檢測到這個是非法的指標,直接終止行程,給 A1 和 A2 的解構式都加上 virtual,執行結果為:
0x1eb2eb0 0x1eb2eb0 0x1eb2ec0
B::~B
A2::~A2
A1::~A1
執行結果是正常的,再看此時生成的匯編代碼:
movq -40(%rbp), %rax # %rax = a2
testq %rax, %rax # a2 == 0 ?
je .L13
movq (%rax), %rdx # %rdx = vptr
addq $8, %rdx # %rdx = vptr + 8
movq (%rdx), %rdx # %rdx = vptr[1] or %rdx = *(vptr + 8)
movq %rax, %rdi # %rax = vptr[1]
call *%rdx # call vptr[1]
這段代碼使用了虛函式,找到 B 的虛表:
vtable for B:
.quad 0
.quad typeinfo for B
.quad B::~B() [complete object destructor] # vptr B inherit A1
.quad B::~B() [deleting destructor]
.quad -16
.quad typeinfo for B
.quad non-virtual thunk to B::~B() [complete object destructor] # vptr B inherit A2
.quad non-virtual thunk to B::~B() [deleting destructor]
a2 的虛指標指向 non-virtual thunk to B::~B() [complete object destructor],會執行這個代碼段:
non-virtual thunk to B::~B() [deleting destructor]:
subq $16, %rdi # this = a2 - 16 or this = b, a2 downcast to b
jmp .LTHUNK1
由于 a2 != b,a2 要進行 downcast 變成 b,于是使用 thunk 技術進行指標偏移,再呼叫B::~B() [deleting destructor],B::~B() [deleting destructor]再呼叫 B::~B(b),和 operator delete(b, 32)
.set .LTHUNK1,B::~B() [deleting destructor]
B::~B() [deleting destructor]:
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
movq %rdi, -8(%rbp) # store this to stack
movq -8(%rbp), %rax # %rax = this
movq %rax, %rdi
call B::~B() [complete object destructor] # call B::~B(b)
movq -8(%rbp), %rax
movl $32, %esi
movq %rax, %rdi
call operator delete(void*, unsigned long) # call operator delete(b, 32)
leave
ret
可以看到傳遞給 operator delete 的指標和大小是正確的,A2::~A2() 和 A1::~A1() 在 B::~B() [complete object destructor] 中被呼叫,不需要繼續深入觀察,
虛析構完美解決了這兩個問題:
- 派生類的解構式沒有被呼叫
- 傳遞給底層
free()函式的指標是錯誤的
在 ISO/IEC 14882:2011 5.3.3 也有對不使用虛析構的描述
In the first alternative (delete object), if the static type of the object to be deleted is different from its
dynamic type, the static type shall be a base class of the dynamic type of the object to be deleted and the
static type shall have a virtual destructor or the behavior is undefined. In the second alternative (delete
array) if the dynamic type of the object to be deleted differs from its static type, the behavior is undefined.
本文來自博客園,作者:mkckr0,轉載請注明原文鏈接:https://www.cnblogs.com/mkckr0/p/16211554.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/468716.html
標籤:C++
上一篇:SAS來自單變數的新變數陣列
下一篇:數字輸入、輸出、排序輸出和去重
