日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

虚析构函数? vptr? 指针偏移?多态数组? delete 基类指针 内存泄漏?崩溃?...

發布時間:2024/4/13 编程问答 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 虚析构函数? vptr? 指针偏移?多态数组? delete 基类指针 内存泄漏?崩溃?... 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

五條基本規則:

1、如果基類已經插入了vptr, 則派生類將繼承和重用該vptr。vptr(一般在對象內存模型的頂部)必須隨著對象類型的變化而不斷地改變它的指向,以保證其值和當前對象的實際類型是一致的。

2、在遇到通過基類指針或引用調用虛函數的語句時,首先根據指針或引用的靜態類型來判斷所調函數是否屬于該class或者它的某個public 基類,如果

屬于再進行調用語句的改寫:

C++ Code?
1
? (*(p->_vptr[slotNum]))(p,?arg-list);

其中p是基類指針,vptr是p指向的對象的隱含指針,而slotNum 就是調用的虛函數指針在vtable 的編號,這個數組元素的索引號在編譯時就確定下來,

并且不會隨著派生層的增加而改變。如果不屬于,則直接調用指針或引用的靜態類型對應的函數,如果此函數不存在,則編譯出錯。

3、C++標準規定對對象取地址將始終為對應類型的首地址,這樣的話如果試圖取基類類型的地址,將取到的則是基類部分的首地址。我們常用的編譯器,如vc++、g++等都是用的尾部追加成員的方式實現的繼承(前置基類的實現方式),在最好的情況下可以做到指針不偏移;另一些編譯器(比如適用于某些嵌入式設備的編譯器)是采用后置基類的實現方式,取基類指針一定是偏移的。

4、delete[] ?的實現包含指針的算術運算,并且需要依次調用每個指針指向的元素的析構函數,然后釋放整個數組元素的內存。

?

?在類繼承機制中,構造函數和析構函數具有一種特別機制叫?“層鏈式調用通知” 《?C++編程思想 》

如下面的例子:

C++ Code?
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 偏離了呢?問題就嚴重了,直接崩潰,看下面的例子分析。

現在來看下面這個問題:

C++ Code?
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。

?

最后來看一個所謂的“多態數組” 問題

C++ Code?
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 基類指針(在指針沒有偏離的情況下) 會不會造成內存泄漏的問題,上面說到如果此時基類析構函數為虛函數,那么是不會內存泄漏的,如果不是則行為未定義。

如下所示:

C++ Code?
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++的數組不支持多態”?

?

?

先是在微博上看到了個微博和云風的評論,然后我回了“樓主對C的內存管理不了解”。

?

?

后來引發了很多人的討論,大量的人又借機來黑C++,比如:

?

//@Baidu-ThursdayWang:這不就c++弱爆了的地方嗎,需要記憶太多東西

//@編程浪子張發財:這個跟C關系真不大。不過我得驗證一下,感覺真的不應該是這樣的。如果基類的析構這種情況不能 調用,就太弱了。

//@程序元:現在看來,當初由于毅力不夠而沒有深入糾纏c++語言特性的各種犄角旮旯的坑爹細枝末節,實是幸事。為現在還沉浸于這些詭異特性并樂此不疲的同志們感到憂傷。

?

然后,也出現了一些亂七八糟的理解:

?

?

?

//@BA5BO: 數組是基于拷貝的,而多態是基于指針的,派生類賦值給基類數組只是拷貝復制了一個基類新對象,當然不需要派生類析構函數

//@編程浪子張發財:我突然理解是怎么回事了,這種情況下數組中各元素都是等長結構體,類型必須一致,的確沒法多態。這跟C#和java不同。后兩者對于引用類型存放的是對象指針。

?

等等,看來我必需要寫一篇博客以正視聽了。

?

因為沒有看到上下文,我就猜測討論的可能會是下面這兩種情況之一:

?

1) 一個Base*[]的指針數組中,存放了一堆派生類的指針,這樣,你delete [] pBase; 只是把指針數組給刪除了,并沒有刪除指針所指向的對象。這個是最基礎的C的問題。你先得for這個指針數組,把數據里的對象都delete掉,然后再刪除數組。很明顯,這和C++沒有什么關系。

?

2)第二種可能是:Base *pBase = new Derived[n] 這樣的情況。這種情況下,delete[] pBase 明顯不會調用虛析構函數(當然,這并不一定,我后面會說) ,這就是上面云風回的微博。對此,我覺得如果是這個樣子,這個程序員完全沒有搞懂C語言中的指針和數組是怎么一回事,也沒有搞清楚, 什么是對象,什么是對象的指針和引用,這完全就是C語言沒有學好。

?

后來,在看到了?@GeniusVczh?的原文 《如何設計一門語言(一)——什么是坑(a)》最后時,才知道了說的是第二種情況。也就是下面的這個示例(我加了虛的析構函數這樣方便編譯):

?

class Base { ??public: ????virtual ~B(){ cout <<"B::~B()"<<endl; } }; class Derived : public Base { ??public: ????virtual ~D() { cout <<"D::D~()"<<endl; } }; Base* pBase = new Derived[10]; delete[] pBase;

?

C語言補課

?

我先不說這段C++的程序在什么情況下能正確調用派生類的析構函數,我還是先來說說C語言,這樣我在后面說這段代碼時你就明白了。

?

對于上面的:

?

Base* pBase = new Derived[10];

?

這個語言和下面的有什么不同嗎?

?

Derived d[10]; Base* pBase = d;

?

一個是堆內存動態分配,一個是棧內存靜態分配。只是內存的位置和類型不一樣,在語法和使用上沒有什么不一樣的。(如果你把Base 和 Derived想成struct,把new想成malloc() ,你還覺得這和C++有什么關系嗎?)

?

那么,你覺得pBase這個指針是指向對象的,是對象的引用,還是指向一個數組的,是數組的引用?

?

于是乎,你可以想像一下下面的場景:

?

int *pInt; char* pChar; pInt = (int*)malloc(10*sizeof(int)); pChar = (char*)pInt;

?

對上面的pInt和pChar指針來說,pInt[3]和pChar[3]所指向的內容是否一樣呢?當然不一樣,因為int是4個字節,char是1個字節,步長不一樣,所以當然不一樣。

?

那么再回到那個把Derived[]數組的指針轉成Base類型的指針pBase,那么pBase[3]是否會指向正確的Derrived[3]呢?

?

我們來看個純C語言的例程,下面有兩個結構體,就像繼承一樣,我還別有用心地加了一個void *vptr,好像虛函數表一樣:

?

struct A { ????void *vptr; ????int i; }; struct B{ ????void *vptr; ????int i; ????char c; ????int j; }b[2] ={ ????{(void*)0x01, 100, 'a', -1}, ????{(void*)0x02, 200, 'A', -2} };

?

注意:我用的是G++編譯的,在64bits平臺上編譯的,其中的sizeof(void*)的值是8。

?

我們看一下棧上內存分配:

?

struct A *pa1 = (struct A*)(b);

?

用gdb我們可以看到下面的情況:(pa1[1]的成員的值完全亂掉了)

?

(gdb) p b $7 = {{vptr = 0x1, i = 100, c = 97 'a', j = -1}, {vptr = 0x2, i = 200, c = 65 'A', j = -2}} (gdb) p pa1[0] $8 = {vptr = 0x1, i = 100} (gdb) p pa1[1] $9 = {vptr = 0x7fffffffffff, i = 2}

?

我們再來看一下堆上的情況:(我們動態了struct B [2],然后轉成struct A *,然后對其成員操作)

?

struct A *pa = (struct A*)malloc(2*sizeof(struct B)); struct B *pb = (struct B*)pa; pa[0].vptr = (void*) 0x01; pa[1].vptr = (void*) 0x02; pa[0].i = 100; pa[1].i = 200;

?

用gdb來查看一下變量,我們可以看到下面的情況:(pa沒問題,但是pb[1]的內存亂掉了)

?

(gdb) p pa[0] $1 = {vptr = 0x1, i = 100} (gdb) p pa[1] $2 = {vptr = 0x2, i = 200} (gdb) p pb[0] $3 = {vptr = 0x1, i = 100, c = 0 '\000', j = 2} (gdb) p pb[1] $4 = {vptr = 0xc8, i = 0, c = 0 '\000', j = 0}

?

可見,這完全就是C語言里亂轉型造成了內存的混亂,這和C++一點關系都沒有。而且,C++的任何一本書都說過,父類對象和子類對象的轉型會帶來嚴重的內存問題。

?

但是,如果在64bits平臺下,如果把我們的structB改一下,改成如下(把struct B中的int j給注釋掉):

?

struct A { ????void *vptr; ????int i; }; struct B{ ????void *vptr; ????int i; ????char c; ????//int j; <---注釋掉int j }b[2] ={ ????{(void*)0x01, 100, 'a'}, ????{(void*)0x02, 200, 'A'} };

?

你就會發現,上面的內存混亂的問題都沒有了,因為struct A和struct B的size是一樣的:

?

(gdb) p sizeof(struct A) $6 = 16 (gdb) p sizeof(struct B) $7 = 16

?

注:如果不注釋int j,那么sizeof(struct B)的值是24。

?

這就是C語言中的內存對齊,內存對齊的原因就是為了更快的存取內存(詳見《深入理解C語言》)

?

如果內存對齊了,而且struct A中的成員的順序在struct B中是一樣的而且在最前面話,那么就沒有問題。

?

再來看C++的程序

?

如果你看過我5年前寫的《C++虛函數表解析》以及《C++內存對象布局 上篇、下篇》,你就知道C++的標準會把虛函數表的指針放在類實例的最前面,你也就知道為什么我別有用心地在struct A和struct B前加了一個 void *vptr。C++之所以要加在最前面就是為了轉型后,不會找不到虛表了。

?

好了,到這里,我們再來看C++,看下面的代碼:

?

#include 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; }

?

上面的代碼可以正確執行,包括調用子類的虛函數!因為內存對齊了。在我的64bits的CentOS上——sizeof(B):16 ,sizeof(D):16

?

但是,如果你在class D中再加一個int成員的問題,這個程序就Segmentation fault了。因為—— sizeof(B):16 ,sizeof(D):24。pb[1]的虛表找到了一個錯誤的內存上,內存亂掉了。

?

再注:我在Visual Studio 2010上做了一下測試,對于 struct 來說,其表現和gcc的是一樣的,但對于class的代碼來說,其可以“正確調用到虛函數”無論父類和子類有沒有一樣的size。

?

然而,在C++的標準中,下面這樣的用法是undefined! 你可以看看StackOverflow上的相關問題討論:《Why is it undefined behavior to delete[] an array of derived objects via a base pointer?》(同樣,你也可以看看《More Effective C++》中的條款三)

?

Base* pBase = new Derived[10]; delete[] pBase;

?

所以,微軟C++編程譯器define這個事讓我非常不解,對微軟的C++編譯器再度失望,看似默默地把其編譯對了很漂亮,實則誤導了好多人把這種undefined的東西當成defined來用,還贊揚做得好,真是令人無語。就像微博上的這個貼一樣,說VC多么牛,還說這是OO的特性。我勒個去!

?

?

現在,你終于知道Base* pBase = new Derived[10];這個問題是C語言的轉型的問題,你也應該知道用于數組的指針是怎么回事了吧?這是一個很奇葩的代碼!請你不要像那些人一樣在微博上和這里的評論里高呼并和我理論到:“微軟的C++編譯器支持這個事!”。

?

最后,我越來越發現,很多說C++難用的人,其實是不懂C語言。

?

?

?

?

?

關于通過不含虛析構函數的基類類型的指針刪除派生類對象的問題

?

如題。問這問題時先基于一個前提條件:析構函數不含釋放其他資源的代碼,甚至可以是空函數,甚至甚至都可以干脆的不寫。這種情況下是否仍有任何問題。
  這個問題的結論是 會導致未定義的行為(但不是內存泄漏那么簡單)。具體如何就看編譯器的實現了。
  我們常用的編譯器,如vc、gcc等都是用的尾部追加成員的方式實現的繼承(前置基類的實現方式)。這樣的話在最好的情況下,可以做到對于同一個對象,整個類 和 其中的基類部分 共享一個內存起始地址(比如單繼承且類和其所有基類均無任何虛函數。而這個條件實際上經常無法滿足)。也就是說取對象地址,然后轉換為void *或者size_t類型再輸出,同用基類指針指向這個對象,然后轉換指針為void *或者size_t類型再輸出,將會發現這兩個地址在數值上是相等的。此時如果用delete通過基類指針刪除這個對象,可以認為是直接的調用了staitc void operator delete (void *);這個操作符(因為析構函數沒做刪除其他資源的操作)。所以不會有任何問題。當條件不滿足的時候,面臨的情況則和下面一種類似。
  另一些編譯器(比如適用于某些嵌入式設備的編譯器)卻使用了先行添加本類成員的方式實現繼承(或者說是尾部追加基類視圖的方式)。這樣即使是單繼承也存在類指針數值上的改變(C++標準規定對對象取地址將始終為對應類型的首地址,這樣的話如果試圖取基類類型的地址,將取到的則是基類部分的首地址。而因為基類被追加到對象末端,所以就會通過在數值上增加地址來跳過派生部分)。此時如果對基類指針做delete操作,會導致很嚴重的后果。因為編譯器從基類指針并不知道派生類是什么,所以刪除操作僅能試圖刪除自己和自己擁有的所有基類部分。但是這個delete所使用的staitc void operator delete (void *);所傳入的void*指針并不是原先new所生成的地址。這樣將會導致堆內存損壞(不光是內存泄漏了)。
  而如果基類中已經提供了虛析構函數(哪怕只是個空函數)就不會導致錯誤了,因為通過基類指針調用delete刪除派生類對象的時候,delete將通過虛函數定位機制(我這里不說虛表,因為不同的編譯器實現不同,有的可能根本沒有虛表這種方式)找到整個對象的首地址而不僅僅是基類部分的首地址。注意即使是這種情況下,前面說的問題仍然存在,即通過一個基類指針仍然不可能知道派生類的存在,從而不可能通過形式上的類型推導直接修正指針到最終派生對象的地址。所以這種推導一定發生在運行時(運行時無法推導類型(注意這里不關RTTI的事),而是通過推導邏輯實現)。說的簡單些,當一個類的析構函數為虛函數時,通過這種類型的指針刪除任一個此類的派生類對象的時候,邏輯上將等同于直接通過最末端派生類的指針刪除這個對象(實現的時候多了一個指針重定位動作。但 運行時 開銷極小)

  即使在繼承樹中的各類視圖基址不共享的情況下,一般的類型轉換(只要你不是將void *指針強制轉換成類指針)卻并不成問題(但是比前置型多出一定的運行時開銷,包括按偏移量移動指針和對NULL地址的特殊處理)。對于p到pBase的隱式指針轉換,編譯器完全可以偷偷的將地址直接換掉(因為編譯器完整的知道源類型和目標類型)。對于將pBase強制轉換為p,編譯器則通過開發者提供的目標類型獲取轉換所需的步驟。這種轉換和delete刪除操作是不一樣的,因為僅一句delete pBase中并不包含任何p指向的對象類型的信息。

?

  始終需要強調的仍然是:不要寫出依賴于編譯器實現的代碼。絕不能依賴于未定義的行為

?

  最后再說說題外話,為什么有編譯器要設計成基類后置型的?因為一些小系統對指針的位寬比較敏感(比如可以參考8086匯編,里面的跳轉,不同位寬的偏移量尋址指令速度差異巨大)。讓基類視圖后置可以做到本類數據成員更靠前從而地址相對于類型基址的偏移量較小,從而加快訪問速度。還有些機器的偏移量尋址寄存器的位寬設計本身就比較小,不支持直接跨越較大的地址范圍。

?

?

?

?

?

[C++][經典探討]類繼承中,通過基類指針delete釋放,是否會造成內存泄漏

?

[序言]
很久不寫C/C++技術貼了,算一下自己用C++也有7~8年了,雖然現在用Delphi比較多,但是對C++還是有一份熱情.前段時間在CSDN看到一個帖子,?很多人都沒有引用權威文獻來針對這個問題進行討論,如果沒有全文文獻的引用,那么討論將會是一個持久戰.要結束這種情況,還是以書為準。如果大家都喜歡探討技術,可以加入我的QQ:643439947一起學習

[建議]
C++是一門非常重要語言且博大精深.沒有10年的使用時間和大量C++的書籍閱讀,最好不要輕易去探討C++某些特性,不然真的是那著石頭砸自己的腳.就因為這些原因本人也很少在CSDN解答C++的問題,因為C++實在太多細節要注意了,知道得越多,越覺得自己是C++菜鳥.我很害怕的回答是錯誤的.

[感謝]
曾半仙,?簡約而不簡單?這些熱心網友提出建議性

[適用范圍]
本問題所涉及的知識點太多和范圍太廣,我特定歸類為windows桌面系統.?如果突然有人牽扯到嵌入式系統以及嵌入式編譯器,那就真的沒完沒了.下面是一個牛人看了文件給的思路和范圍,可想而知太多不可預測的因素了."你考慮一下嵌入開發環境,?雖然語法上支持,?但是庫并沒有實現new和delete,?這樣就引發了不確定因素,?特別是程序員喜歡模版,?喜歡優化,?想使用內存池的情況?"

[原則]
本人是中立人士,不針對任何人,只針對問題.在分析這個問題我又復習了一邊C++.這個問題牽涉到?析構函數?虛函數?構造函數?派生?new/delete?5個主要問題.本著學術認真的態度,我翻閱了如下C++書籍
1>?C++?Primer?Plus
2>?C++編程思想?2卷合訂本?新版
3>?Effective?C++
4>?Imperfect?C++

[引發問題的CSDN鏈接]
http://topic.csdn.net/u/20110715/15/7ca1e66b-8a04-4c90-80f0-6265ff0269af.html?91968

[還原問題]
class?A
{
public:
????A(){}?;
????~A()?{}?;?//?Ooops?must?use?virtual?~A()
}?;


class?B?:?public?A
{
public:
????B(){}?;
????~B()?{}?;
}?;

int?main()
{
????A?*pclass_A?=?new?B?;?//?創建一個B對象指針?隱性轉換為?A*
??????????????????????????//??這里我們需要注意這個轉換涉及到一個概念叫:?Upcast?中文翻譯叫:向上類型轉換
????delete?pclass_A?;
????pclass_A=?NULL?;
????return?0;
}

[分析結論]
就這段代碼本身而言我看了4本書也沒有很明確的說到這樣的寫法就會有泄漏.但可以確定這樣的寫法是一個隱性錯誤,已違反C++的繼承規則和違背繼承的實現原理機制.
詳細請看:http://www.parashift.com/c++-faq-lite/virtual-functions.html#faq-20.7

"....不把析構函數設為虛函數是一個隱性的錯誤,因為它常常不會對程序有直接影響。但要注意它不知不覺得引入存儲器泄漏(關閉程序是內存為釋放)。同樣,這樣的析構操作還有可能掩蓋發生的問題...."(摘自:?C++編程思想?2卷合訂本?第387頁)。這句話雖然很短,但是解答了我們很多疑問.

1>?“如果你不使用虛析構函數,不會對程序有直接影響”.這里的“不會對程序有直接影響”,我們可以認為delete一個基類指針(基類是沒有析構函數),不會照成內存泄漏(僅針對上面的代碼而言,如果在派生類中有分配堆,那么肯定會有內存泄漏).
這里為什么我們可以認為delete一個基類指針(基類是沒有析構函數),不會照成內存泄漏呢?這就是C++的new?和?delete?的特有機制和職責了.下面看這句話:
"....當在堆棧里主動創建對象時,對象的大小和它們的聲明周期被準確地內置在生成的代碼里,這是因為編譯器知道確切的類型,數量和范圍....."(摘自:?C++編程思想?2卷合訂本?第318頁的)這里非常明確的告訴我們,會知道確切的"類型,數量和范圍",注意這里有"范圍",因此可以推斷通過基類指針進行delete,是不會對“不會對程序有直接影響”(備注:請諒解,我沒敢直接說不會有內存泄漏,因為我沒有能跟編譯器廠商求證,但我認為是"應該"不會造成內存泄漏).

2>"但要注意它不知不覺地引入存儲器泄漏"這句話又針對前句話做了補充,特別強調了"不知不覺地"+"引入"+"存儲器泄漏".很明顯的說明了,如果會發生泄漏,那就是外部人為造成的,比如的B類內部中使用了new操作,比如申請10個字節char?*char_A?=?new?char[10],那么根據“C++的繼承規則和繼承的實現原理機制”如果你不把基類的析構函數聲明并定義為virtual,那么B類在釋放的時候,沒法做尾場清理的.比如前面的?new?char[10]不能被釋放.

額外討論:?在類繼承機制中,構造函數和析構函數具有一種特別機制叫?“層鏈式調用通知”,這個機制原理是建立在?“vpointer”?“VPTR”?“VTABLE”這種東西(摘自:?C++編程思想?2卷合訂本?第369頁)(備注:層鏈式調用通知是我個人理解并總結的詞匯.大家可以通過閱讀?C++編程思想?2卷合訂本?第385頁).
流程是這樣:在構造一個有類繼承機制的類,比如上面的類B,那么會先調用A類的構造,A構造完成之后在調用B類的構造函數,達到"由里向外"通知調用的效果.那么釋放一個有類繼承機制的類,那么會調用B類的析構函數,?再調用A類的析構函數,達到"由外向里"通知通知的效果,那么為了達到這個這種“層鏈式調用通知”的效果,C++標準規定:基類的析構函數必須聲明為virtual,?如果你不聲明,那么"層鏈式調用通知"這樣的機制是沒法構建起來.從而就導致了基類的析構函數被調用了,而派生類的析構函數沒有調用這個問題發生.但這里要特別注意:這種特殊情況下派生類的析構函數沒有被調用,有2中情況發生:
1>如果你的派生類內部沒有分配任何堆,而只是單一的局部變量,那么根據局部變量和類的生命周期理論,他們是會被釋放的,“不會對程序有直接影響”(備注:請諒解,我沒敢直接說不會有內存泄漏,因為我沒有能跟編譯器廠商求證,但我因為是"應該"不會造成內存泄漏),比如本文頂部列舉的代碼片段.
2>如果你的派生類內部有分配堆,那么派生類就沒法通過自身的析構函數進行尾場清理了,比如?delete?[]a?;

[結尾]
寫這個文章花費了我1個小時,但在寫之前,花費了我2個小時去翻閱4本C++書籍重新去消化這個經典問題.

[查閱資料]
1>?C++?Primer?Plus?里面的?第13章?類繼承
2>?C++編程思想?2卷合訂本?新版?里面的?第13章?動態對象創建?第14章?繼承和組合?第15章?多態性和虛函數

總結

以上是生活随笔為你收集整理的虚析构函数? vptr? 指针偏移?多态数组? delete 基类指针 内存泄漏?崩溃?...的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。