1、構造函數和析構函數為什么沒有返回值?
構造函數和析構函數是兩個非常特殊的函數:它們沒有返回值。這與返回值為void的函數顯然不同,后者雖然也不返回任何值,但還可以讓它做點別的事情,而構造函數和析構函數則不允許。在程序中創建和消除一個對象的行為非常特殊,就像出生和死亡,而且總是由編譯器來調用這些函數以確保它們被執行。如果它們有返回值,要么編譯器必須知道如何處理返回值,要么就只能由客戶程序員自己來顯式的調用構造函數與析構函數,這樣一來,安全性就被人破壞了。另外,析構函數不帶任何參數,因為析構不需任何選項。
如果允許構造函數有返回值,在某此情況下,會引起歧義。如下兩個例子
class C { public: C(): x(0) { } C(int i): x(i) { } private: int x; };
如果C的構造函數可以有返回值,比如int:int C():x(0) { return 1; } //1表示構造成功,0表示失敗
那么下列代碼會發生什么事呢?
C c = C(); //此時c.x == 1!!!
很明顯,C()調用了C的無參數構造函數。該構造函數返回int值1。恰好C有一個但參數構造函數C(int i)。於是,混亂來了。按照C++的規定,C c = C();是用默認構造函數創建一個臨時對象,並用這個臨時對象初始化c。此時,c.x的值應該是0。但是,如果C::C()有返回值,並且返回了1(為了表示成功),則C++會用1去初始化c,即調用但參數構造函數C::C(int i)。得到的c.x便會是1。於是,語義產生了歧義。使得C++原本已經非常復雜的語法,進一步混亂不堪。
構造函數的調用之所以不設返回值,是因為構造函數的特殊性決定的。從基本語義角度來講,構造函數返回的應當是所構造的對象。否則,我們將無法使用臨時對象:
void f(int a) {...} //(1)
void f(const C& a) {...} //(2)
f(C()); //(3),究竟調用誰?
對於(3),我們希望調用的是(2),但如果C::C()有int類型的返回值,那么究竟是調(1)好呢,還是調用(2)好呢。於是,我們的重載體系,乃至整個的語法體系都會崩潰。
這里的核心是表達式的類型。目前,表達式C()的類型是類C。但如果C::C()有返回類型R,那么表達式C()的類型應當是R,而不是C,於是便會引發上述的類型問題。
2、顯式調用構造函數和析構函數
#include <iostream> using namespace std; class MyClass { public: MyClass() { cout << "Constructors" << endl; } ~MyClass() { cout << "Destructors" << endl; } }; int main() { MyClass* pMyClass = new MyClass; pMyClass->~MyClass(); delete pMyClass; return 0; }
結果:
Constructors
Destructors //這個是顯示調用的析構函數
Destructors //這個是delete調用的析構函數
這有什么用?有時候,在對象的生命周期結束前,想先結束這個對象的時候就會派上用場了。直接調用析構函數並不釋放對象所在的內存。
由此想到的:
new的時候,其實做了三件事,一是:調用::operator new分配所需內存。二是:調用構造函數。三是:返回指向新分配並構造的對象的指針。
delete的時候,做了兩件事,一是:調用析構函數,二是:調用::operator delete釋放內存。
所以推測構造函數也是可以顯式調用的。做個實驗:
int main()
{
MyClass* pMyClass = (MyClass*)malloc(sizeof(MyClass));
pMyClass->MyClass();
// …
}
編譯pMyClass->MyClass()出錯:
error C2273: 'function-style cast' : illegal as right side of '->'operator
它以為MyClass是這個類型。
解決辦法有兩個:
第一:pMyClass->MyClass::MyClass();
第二:new(pMyClass) MyClass();
第二種用法涉及C++ placement new 的用法。參考:http://www.cnblogs.com/luxiaoxun/archive/2012/08/10/2631812.html
顯示調用構造函數有什么用?
有時候,你可能由於效率考慮要用到malloc去給類對象分配內存,因為malloc是不調用構造函數的,所以這個時候會派上用場了。
另外下面也是可以的,雖然內置類型沒有構造函數。
int* i = (int*)malloc(sizeof(int));
new (i) int();
3、拷貝(復制)構造函數為什么不能用值傳遞
當你嘗試着把拷貝構造函數寫成值傳遞的時候,會發現編譯都通不過,錯誤信息如下:
error: invalid constructor; you probably meant 'S (const S&)' (大致意思是:無效的構造函數,你應該寫成。。。)
當編譯錯誤的時候你就開始糾結了,為什么拷貝構造函數一定要使用引用傳遞呢,我上網查找了許多資料,大家的意思基本上都是說如果用值傳遞的話可能會產生死循環。編譯器可能基於這樣的原因不允許出現值傳遞的拷貝構造函數,也有可能是C++標准是這樣規定的。
如果真是產生死循環這個原因的話,應該是這樣子的:
class S { public: S(int x):a(x){ } S(const S st) //拷貝構造函數 { a = st.a; } private: int a; }; int main() { S s1(2); S s2(s1); return 0; }
當給s2初始化的時候調用了s2的拷貝構造函數,由於是值傳遞,系統會給形參st重新申請一段空間,然后調用自身的拷貝構造函數把s1的數據成員的值傳給st。當調用自身的拷貝構造函數的時候又因為是值傳遞,所以...
也就是說,只要調用拷貝構造函數,就會重新申請一段空間,只要重新申請一段空間,就會調用拷貝構造函數,這樣一直下去就形成了一個死循環。所以拷貝構造函數一定不能是值傳遞。
4、構造函數/析構函數拋出異常的問題
構造函數拋出異常:
1.不建議在構造函數中拋出異常;
2.構造函數拋出異常時,析構函數將不會被執行;
C++僅僅能刪除被完全構造的對象(fully contructed objects),只有一個對象的構造函數完全運行完畢,這個對象才能被完全地構造。對象中的每個數據成員應該清理自己,如果構造函數拋出異常,對象的析構函數將不會運行。如果你的對象需要撤銷一些已經做了的動作(如分配了內存,打開了一個文件,或者鎖定了某個信號量),這些需要被撤銷的動作必須被對象內部的一個數據成員記住處理。
析構函數拋出異常:
在有兩種情況下會調用析構函數。第一種是在正常情況下刪除一個對象,例如對象超出了作用域或被顯式地delete。第二種是異常傳遞的堆棧輾轉開解(stack-unwinding)過程中,由異常處理系統刪除一個對象。
在上述兩種情況下,調用析構函數時異常可能處於激活狀態也可能沒有處於激活狀態。遺憾的是沒有辦法在析構函數內部區分出這兩種情況。因此在寫析構函數時你必須保守地假設有異常被激活,因為如果在一個異常被激活的同時,析構函數也拋出異常,並導致程序控制權轉移到析構函數外,C++將調用terminate函數。這個函數的作用正如其名字所表示的:它終止你程序的運行,而且是立即終止,甚至連局部對象都沒有被釋放。
概括如下:
1.析構函數不應該拋出異常;
2.當析構函數中會有一些可能發生異常時,那么就必須要把這種可能發生的異常完全封裝在析構函數內部,決不能讓它拋出函數之外;
3.當處理另一個異常過程中,不要從析構函數拋出異常;
在構造函數和析構函數中防止資源泄漏的好方法就是使用smart point(智能指針),C++ STL提供了類模板auto_ptr,用auto_ptr對象代替原始指針,你將不再為堆對象不能被刪除而擔心,即使在拋出異常時,對象也能被及時刪除。因為auto_ptr的析構函數使用的是單對象形式的delete,而不是delete [],所以auto_ptr不能用於指向對象數組的指針。當復制 auto_ptr 對象或者將它的值賦給其他 auto_ptr 對象的時候,將基礎對象的所有權從原來的 auto_ptr 對象轉給副本,原來的 auto_ptr 對象重置為未綁定狀態。因此,不能將 auto_ptrs 存儲在標准庫容器類型中。如果要將智能指針作為STL容器的元素,可以使用Boost庫里的shared_ptr。