虛解構函式? vptr? 指標偏移?多型陣列? delete 基類指標 記憶體洩漏?崩潰?
五條基本規則:
1、如果基類已經插入了vptr, 則派生類將繼承和重用該vptr。vptr(一般在物件記憶體模型的頂部)必須隨著物件型別的變化而不斷地改變它的指向,以保證其值和當前物件的實際型別是一致的。
2、在遇到通過基類指標或引用呼叫虛擬函式的語句時,首先根據指標或引用的靜態型別來判斷所調函式是否屬於該class或者它的某個public 基類,如果
屬於再進行呼叫語句的改寫:
1
|
(*(p->_vptr[slotNum]))(p, arg-list);
|
其中p是基類指標,vptr是p指向的物件的隱含指標,而slotNum 就是呼叫的虛擬函式指標在vtable 的編號,這個陣列元素的索引號在編譯時就確定下來,
並且不會隨著派生層的增加而改變。如果不屬於,則直接呼叫指標或引用的靜態型別對應的函式,如果此函式不存在,則編譯出錯。
3、C++標準規定對物件取地址將始終為對應型別的首地址,這樣的話如果試圖取基類型別的地址,將取到的則是基類部分的首地址。我們常用的編譯器,如vc++、g++等都是用的尾部追加成員的方式實現的繼承(前置基類的實現方式),在最好的情況下可以做到指標不偏移;另一些編譯器(比如適用於某些嵌入式裝置的編譯器)是採用後置基類的實現方式,取基類指標一定是偏移的。
4、delete[] 的實現包含指標的算術運算,並且需要依次呼叫每個指標指向的元素的解構函式,然後釋放整個陣列元素的記憶體。
5、 在類繼承機制中,建構函式和解構函式具有一種特別機制叫 “層鏈式呼叫通知” 《 C++程式設計思想 》
C++標準規定:基類的解構函式必須宣告為virtual, 如果你不宣告,那麼"層鏈式呼叫通知"這樣的機制是沒法構建起來.從而就導致了基類的解構函式被呼叫了,而派生類的解構函式沒有呼叫這個問題發生.
如下面的例子:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#include<iostream>
using namespace std; class IRectangle { public: virtual ~IRectangle() {} virtual void Draw() = 0; }; class Rectangle: public IRectangle { public: virtual ~Rectangle() {} virtual void Draw(int scale) { cout << "Rectangle::Draw(int)" << endl; } virtual void Draw() { cout << "Rectangle::Draw()" << endl; } }; int main(void) { IRectangle *pI = new Rectangle; pI->Draw(); pI->Draw(200); delete pI; return 0; } |
按照上面的規則2,pI->Draw(200); 會編譯出錯,因為在基類並沒有定義Draw(int) 的虛擬函式,於是查詢基類是否定義了Draw(int),還是沒有,就出錯了,從出錯提示也可以看出來:“IRectangle::Draw”: 函式不接受 1 個引數。
此外,上述小例子還隱含另一個知識點,我們把出錯的語句遮蔽掉,看輸出:
Rectangle::Draw()
~Rectangle()
~IRectangle()
即派生類和基類的解構函式都會被呼叫,這是因為我們將基類的解構函式宣告為虛擬函式的原因,在pI 指向派生類首地址的前提下,如果~IRectangle()
是虛擬函式,那麼會找到實際的函式~Rectangle() 執行,而~Rectangle() 會進一步呼叫~IRectangle()(規則5)。如果沒有這樣做的話,只會輸出基類的
解構函式,這種輸出情況通過比對規則2也可以理解,pI 現在雖然指向派生類物件首地址,但執行pI->~IRectangle() 時 發現不是虛擬函式,故直接呼叫,
假如在派生類解構函式內有釋放記憶體資源的操作,那麼將造成記憶體洩漏。更甚者,問題遠遠沒那麼簡單,我們知道delete pI ; 會先呼叫解構函式,再釋
放記憶體(operator delete),上面的例子因為派生類和基類現在的大小都是4個位元組即一個vptr,故不存在釋放記憶體崩潰的情況,即pI 現在就指向派生
類物件的首地址。如果pI 偏離了呢?問題就嚴重了,直接崩潰,看下面的例子分析。
現在來看下面這個問題:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
#include <iostream>
using namespace std; class Base { public: ~Base() { cout << "~Base()" << endl; } void fun() { cout << "Base::fun()" << endl; } }; class Derived : public Base { public: ~Derived() { cout << "~Derived()" << endl; } virtual void fun() { cout << "Derived::fun()" << endl; } }; int main() { Derived *dp = new Derived; Base *p = dp; p->fun(); cout << sizeof(Base) << endl; cout << sizeof(Derived) << endl; cout << (void *)dp << endl; cout << (void *)p << endl; delete p; p = NULL; return 0; } |
由於基類的fun不是虛擬函式,故p->fun() 呼叫的是Base::fun()(規則2),而且delete p 還會崩潰,為什麼呢?因為此時基類是空類1個位元組,派生類有虛擬函式故有vptr 4個位元組,基類“繼承”的1個位元組附在vptr下面,現在的p 實際上是指向了附屬1位元組,即operator delete(void*) 傳遞的指標值已經不是new 出來時候的指標值,故造成程式崩潰。 將基類解構函式改成虛擬函式,fun() 最好也改成虛擬函式,只要有一個虛擬函式,基類大小就為一個vptr ,此時基類和派生類大小都是4個位元組,p也指向派生類的首地址,問題解決,參考規則3。
最後來看一個所謂的“多型陣列” 問題
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#include<iostream>
using namespace std; class B { int b; public: virtual ~B() { cout << "B::~B()" << endl; } }; class D: public B { int i; public: virtual ~D() { cout << "D::~D()" << endl; } }; int main(void) { cout << "sizeB:" << sizeof(B) << " sizeD:" << sizeof(D) << endl; B *pb = new D[2]; delete [] pb; return 0; } |
由於sizeB != sizeD,參照規則4,pb[1] 按照B的大小去跨越,指向的根本不是一個真正的B物件,當然也不是一個D物件,因為找到的D[1] 虛擬函式表位置是錯的,故呼叫解構函式出錯。程式在g++ 下是segment fault 的,但在vs 中卻可以正確執行,在C++的標準中,這樣的用法是undefined 的,只能說每個編譯器實現不同,但我們最好不要寫出這樣的程式碼,免得庸人自擾。
delete-expression:
::opt delete cast-expression
::opt delete [ ] cast-expression
In the first alternative (delete object), if the static type of the operand is different from its dynamic type, the static type shall be a base class of the
operand’s dynamic type 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.
第二點也就是上面所提到的問題。關於第一點。也是論壇上經常討論的,也就是說delete 基類指標(在指標沒有偏離的情況下) 會不會造成記憶體洩漏的問題,上面說到如果此時基類解構函式為虛擬函式,那麼是不會記憶體洩漏的,如果不是則行為未定義。
如下所示:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#include<iostream>
using namespace std; class B { int b; public: virtual ~B() { cout << "B::~B()" << endl; } }; class D: public B { int i; public: virtual ~D() { cout << "D::~D()" << endl; } }; int main(void) { cout << "sizeB:" << sizeof(B) << " sizeD:" << sizeof(D) << endl; D *pd = new D; B *pb = pd; cout << (void *)pb << endl; cout << (void *)pd << endl; delete pb; return 0; } |
現在B與D大小不一致,delete pb; 此時pb 沒有偏移,在linux g++ 下通過valgrind (valgrind --leak-check=full ./test )檢測,並沒有記憶體洩漏,基類和派生類的解構函式也正常被呼叫。
如果將B 的解構函式virtual 關鍵字去掉,那麼B與D大小不一致,而且此時pb 已經偏移,delete pb; 先呼叫~B(),然後free 出錯,如
*** glibc detected *** ./test: free(): invalid pointer: 0x09d0000c *** ,參照前面講過的例子。
如果將B和D 的virtual 都去掉,B與D大小不一致,此時pb 沒有偏移,delete pb; 只呼叫~B(),但用varlgrind 檢測也沒有記憶體洩漏,實際上如上所說,這種情況是未定義的,但可以肯定的是沒有呼叫~D(),如果在~D() 內有釋放記憶體資源的操作,那麼一定是存在記憶體洩漏的。
參考:
《高質量程式設計指南C++/C語言》
http://coolshell.cn/articles/9543.html
http://blog.csdn.net/unituniverse2/article/details/12302139
http://bbs.csdn.net/topics/370098480
相關文章
- 從預設解構函式學習c++,new,delete,記憶體洩漏,野指標函式C++delete記憶體指標
- 基類指標、虛純虛擬函式、多型性、虛析構指標函式多型
- 基類指標,子類指標,虛擬函式,override與final指標函式IDE
- 陣列,函式與指標 詳解陣列函式指標
- 陣列指標,指標陣列陣列指標
- 指標陣列與陣列指標指標陣列
- Go 陣列指標(指向陣列的指標)Go陣列指標
- 指標函式 和 函式指標指標函式
- 指標陣列和陣列指標與二維陣列指標陣列
- 「程式設計師面試」一文搞懂野指標、懸空指標、空指標和記憶體洩漏,附程式碼示例!程式設計師面試指標記憶體
- 陣列指標陣列指標
- 函式指標基礎函式指標
- C語言指標(三):陣列指標和字串指標C語言指標陣列字串
- 函式指標函式指標
- 函式指標、回撥函式、動態記憶體分配、檔案操作函式指標記憶體
- Golang 學習——陣列指標和指標陣列的區別Golang陣列指標
- [C++] 成員函式指標和函式指標C++函式指標
- c語言野指標與結構體指標動態記憶體分配小解C語言指標結構體記憶體
- C陣列和指標陣列指標
- 【不在混淆的C】指標函式、函式指標、回撥函式指標函式
- c++ 類的函式引用 指標C++函式指標
- 多型體驗,和探索爺爺類指標的多型性多型指標
- typedef void (*Fun) (void) 的理解——函式指標——typedef函式指標函式指標
- 二級指標,二維陣列函式引數傳遞指標陣列函式
- 泛型程式設計(模板函式,模板類的套用) Myvector 具體案例 實現可存放int 陣列 char陣列 類物件陣列 以及一組指標泛型程式設計函式陣列物件指標
- 【原創】淺談指標(十三)指向陣列的指標指標陣列
- 【原創】淺談指標(九)二維陣列和多級指標相關指標陣列
- C語言重點——指標篇(一文讓你完全搞懂指標)| 從記憶體理解指標 | 指標完全解析C語言指標記憶體
- 透過指標引用陣列指標陣列
- 二維陣列與指標陣列指標
- C++(函式指標)C++函式指標
- 關於函式指標函式指標
- c++ 函式指標C++函式指標
- 第 10 節:複合型別-5. 指標 -- 指標與指標變數 -8. 多級指標型別指標變數
- 函式指標&回撥函式Callback函式指標
- 指標陣列練習排列字串指標陣列字串
- C語言 指標與陣列C語言指標陣列
- 指標:存放記憶體地址的變數指標記憶體變數
- C++ 指標動態記憶體分配C++指標記憶體