一、析構函數的定義
析構函數為成員函數的一種,名字與類名相同,在前面加‘~’沒有參數和返回值在C++中“~”是位取反運算符。
一個類最多只能有一個析構函數。析構函數不返回任何值,沒有函數類型,也沒有函數參數,因此它不能被重載。
構造函數可能有多個,但析構函數只能有一個,就像人來到人世間,可能出生的環境家庭不同(重載構造函數),但最終都會死亡(析構函數)。
class C { public: ~C ( ) { } ~C (int i) { } // error C2524: “C”: 析構函數 必須有“void”參數列表 // warning C4523: “C”: 指定了多個析構函數 };
析構函數對象消亡時即自動被調用。可以定義析構函數來在對象消亡前做善后工作,比如釋放分配的空間等。
如果定義類時沒寫析構函數,則編譯器生成缺省析構函數。缺省析構函數什么也不做。如果定義了析構函數,則編譯器不生成缺省析構函數。
析構函數的作用並不是刪除對象,而是在撤銷對象占用的內存之前完成一些清理工作。
例:
class A { private : char * p; public: A ( ) { p = new char[10]; } ~ A ( ) { delete [] p; } };
若A類沒寫析構函數,則在生成A對象后,new出來的內存空間未清除,可能造成內存泄露。
在創建一類的對象數組時,對於每一個數組元素,都會執行缺省的構造函數。同樣,對象數組生命期結束時,對象數組的每個元素的析構函數都會被調用。
#include<iostream> using namespace std; unsigned count = 0; class A { public: A ( ) { i = ++count; cout << "Creating A " << i <<endl; } ~A ( ) { cout << "A Destructor called " << i <<endl; } private : int i; }; int main( ) { A ar[3]; // 對象數組 return 0; }
程序執行結果為:
Creating A 1
Creating A 2
Creating A 3
A Destructor called 3
A Destructor called 2
A Destructor called 1
類似於棧的后進先出
二、析構函數的調用
如果出現以下幾種情況,程序就會執行析構函數:
(1)如果在一個函數中定義了一個對象(它是自動局部對象),當這個函數被調用結束時,對象應該釋放,在對象釋放前自動執行析構函數。
(2)static局部對象在函數調用結束時對象並不釋放,因此也不調用析構函數,只在main函數結束或調用exit函數結束程序時,才調用static局部對象的析構函數。
(3)如果定義了一個全局對象,則在程序的流程離開其作用域時(如main函數結束或調用exit函數) 時,調用該全局對象的析構函數。
(4)如果用new運算符動態地建立了一個對象,當用delete運算符釋放該對象時,先調用該對象的析構函數。
(5)調用復制構造函數后。
例:
例4.35 #include <iostream> using namespace std; class CMyclass { public: ~CMyclass( ) { cout << "destructor" << endl; } }; CMyclass obj; CMyclass fun(CMyclass sobj ) { return sobj; //函數調用返回時生成臨時對象返回 } void main( ) { obj = fun(obj); //函數調用的返回值(臨時對象)被用過后,該臨時對象析構函數被調用 }
程序執行結果為:
destructor // 形參和實參結合,會調用復制構造函數,臨時對象析構
destructor // return sobj函數調用返回,會調用復制構造函數,臨時對象析構
destructor // obj對象析構
總之,在臨時對象生成的時候會有構造函數被調用,臨時對象消亡導致析構函數調用。
三、構造函數和析構函數的調用情況
構造函數用於對對象中的變量賦初值,析構函數用於釋放所定義的對象的所有內存空間。構造函數和析構函數都不需要用戶調用的,構造函數在定義對象時自動調用,析構函數當對象的生存期結束的時候會自動的調用。一般來說,析構函數的調用順序與構造函數相反。但對象存儲類型可以改變析構函數的調用順序。
全局范圍中定義的對象的構造函數在文件中的任何其他函數(包括main)執行之前調用(但不同文件之間全局對象構造函數的執行順序是不確定的)。全局變量是需要在進入main()函數前即初始化的,所以在一個程序中一個全局變量的構造函數應該是第一個被調用的,比main()函數都要早。同時全局對象又必須在main()函數返回后才被銷毀,當main終止或調用exit函數時調用相應的析構函數,所以它的析構函數是最后才被調用。
當程序執行到對象定義時,調用自動局部對象的構造函數。該對象的析構函數在對象離開范圍時調用(即離開定義對象的塊時)。自動對象的構造函數與析構函數在每次對象進人和離開范圍時調用。
static局部對象的構造函數只在程序執行首次到達對象定義時調用一次,對應的析構函數在main終止或調用exit函數時調用。
#include <iostream> using namespace std; class A { public: A( int value ) { i = value; cout << "Object "<<i<<" constructor"; } ~A() // destructor { cout <<"Object "<<i<<" destructor"<< endl; } private: int i; }; A first(1); // global object全局變量 void func() { A fifth(5); cout <<" (local automatic in create)"<< endl; static A sixth( 6 ); cout <<" (local static in create)"<< endl; A seventh(7); cout <<" (local automatic in create)"<< endl; } int main() { cout <<" (global created before main)" <<endl; A second(2); cout <<" (local automatic in main)"<< endl; static A third(3); // local object cout <<" (local static in main)"<< endl; func(); // call function to create objects A fourth(4); // local object cout <<" (local automatic in main)"<< endl; return 0; }
程序執行結果為:
Object 1 constructor (global creasted before main)
Object 2 constructor (local automatic in main)
Object 3 constructor (local static in main)
Object 5 constructor (local automatic in create)
Object 6 constructor (local static in create)
Object 7 constructor (local automatic in create)
Object 7 destructor
Object 5 destructor
Object 4 constructor (local automatic in main}
Object 4 destructor
Object 2 destructor
Object 6 destructor
Object 3 destructor
Object 1 destructor
上例中,main函數中聲明3個對象,對象second和fourth是局部自動對象,對象third是static局部對象。這些對象的構造函數在程序執行到對象定義時調用。對象fourth和second的析構函數在到達main函數結尾時候依次調用。由於對象third是static局部對象,因此到程序結束時才退出,在程序終止時候刪除所有其他對象之后和調用first的析構函數之前調用對象third的析構函數。函數func聲明3個對象。對象fifth和seventh是局部自動對象,對象sixth是static局部對象。對象seventh和fifth的析構函數在到func結束時候依次調用。由於對象sixth是static局部對象,因此到程序結束時才退出。sixth的析構函數在程序終止時刪除所有其他對象之后和調用third和first的析構函數之前調用。
若函數參數是類類型,調用函數時要調用復制構造函數,用實際參數初始化形式參數。當函數返回類類型時,也要通過復制構造函數建立臨時對象。
例:
#include <iostream> using namespace std; class A { public: A() { cout<<"A constructor"<<endl; } ~A() { cout<<"A destructor"<<endl; } A(A &) { cout <<"A copy constructor"<<endl; } }; class B { public: B() { cout<<"B constructor"<<endl; } ~B() { cout<<"B destructor"<<endl; } B(B &) { cout<<"B copy constructor"<<endl; } }; A a; B b; void func1(A obj) {} void func2(B &obj) {} int main() { func1(a); func2(b); return 0; }
程序執行結果為:
A constructor // a構造
B constructor // b構造
A copy constructor // func1函數參數調用A的復制構造函數
A destructor // func1函數參數析構
B destructor // b析構
A destructor // a析構
上例中函數func1的參數是A的值傳遞方式,實參初始化形參時需要調用復制構造函數,函數調用結束后,棧上的形參消亡時要調用A的析構函數。函數func2的參數是B的引用傳遞方式,形參只是實參的一個別名,並沒有創建新的對象,因此不會調用復制構造函數和析構函數。
這里再附上一道面試題:
#include <iostream> using namespace std; void hello( ) { cout << " Hello, world!\n"; } int main( ) { hello( ); return 0; }
試修改上面的程序,使其輸出變成:
Begin
Hello, world!
End
限制:(1)不能對main()進行任何修改;(2)不能修改hello()函數。
這里用到了構造函數和析構函數的調用了。
#include <iostream> using namespace std; class A { public: A ( ) { cout << "Begin\n"; } ~A ( ) { cout << "End\n"; } }; void hello( ) {cout << " Hello, world!\n"; } A a; // a是一個全局對象 int main( ) { hello( ); return 0; }
下面總結下不同存儲類型構造函數和析構函數的調用。
構造函數的調用:
(1)全局變量:程序運行前;
(2)函數中靜態變量:函數開始前;
(3)函數參數:函數開始前;
(4)函數返回值:函數返回前;
析構函數的調用:
(1)全局變量:程序結束前;
(2)main中變量:main結束前;
(3)函數中靜態變量:程序結束前;
(4)函數參數:函數結束前;
(5)函數中變量:函數結束前;
(6)函數返回值:函數返回值被使用后;
對於相同作用域和存儲類別的對象,調用析構函數的次序正好與調用構造函數的次序相反
四、私有析構函數
有時你想這樣管理某些對象,要讓某種類型的對象能夠自我銷毀,也就是能夠“delete this”。很明顯這種管理方式需要此類型對象被分配在堆中。若我們必須在堆中創建對象,為了執行這種限制,你必須找到一種方法禁止以調用“new”以外的其它手段建立對象。這很容易做到,非堆對象(non-heap object),即棧對象在定義它的地方被自動構造,在生存時間結束時自動被釋放,所以只要禁止使用隱式的析構函數,就可以實現這種限制。
把這些調用變得不合法的一種最直接的方法是把析構函數聲明為private,把構造函數聲明為public。你可以增加一個專用的偽析構函數,用來訪問真正的析構函數,客戶端調用偽析構函數釋放他們建立的對象。
#include <iostream> using namespace std; class A { public: A() { cout<<"A"<<endl; } void destroy() const { cout<<"delete A"<<endl; delete this; } private: ~A() {} }; int main( ) { A *pa = new A; pa->destroy(); return 0; }
程序執行結果為:
A
delete A
若A類對象是在棧上創建:
A a; // error C2248: “A::~A”: 無法訪問 private 成員(在“A”類中聲明)
編譯時會提示不能訪問私有成員。因為在棧上生成對象時,類對象在離開作用域時會調用析構函數釋放空間,此時無法調用私有的析構函數。C++在編譯過程中,所有的非虛函數調用都必須分析完成。即使是虛函數,也需檢查可訪問性。因此,當在棧上生成對象時,對象會自動析構,也就說析構函數必須可以訪問。而堆上生成對象,由於析構時機由程序員控制,所以不一定需要析構函數。
被聲明為私有析構函數的類對象只能在堆上創建,並且該類不能被繼承。
class A { private: ~A() {} }; class B : public A { // error C2248: “A::~A”: 無法訪問 private 成員(在“A”類中聲明) } // warning C4624: “B”: 未能生成析構函數,因為基類析構函數不可訪問