構造函數和析構函數的一些問題

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的時候,其實做了兩件事,一是:調用malloc分配所需內存,二是:調用構造函數。
delete的時候,也是做了兩件事,一是:調用析造函數,二是:調用free釋放內存。

所以推測構造函數也是可以顯式調用的。做了個實現:
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 的用法 。
placement new的作用就是:創建對象(調用該類的構造函數)但是不分配內存,而是在已有的內存塊上面創建對象。用於需要反覆創建並刪除的對象上,可以降低分配釋放內存的性能消耗。請查閱placement new相關資料。
顯示調用構造函數有什麼用? 
有時候,你可能由於效率考慮要用到malloc去給類對象分配內存,因爲malloc是不調用構造函數的,所以這個時候會派上用場了。
另外下面也是可以的,雖然內置類型沒有構造函數。
int* i = (int*)malloc(sizeof(int));
new (i) int();


3、拷貝構造函數爲什麼不能用值傳遞 
當你嘗試着把拷貝構造函數寫成值傳遞的時候,會發現編譯都通不過,錯誤信息如下:
error: invalid constructor; you probably meant 'S (const S&)' (大致意思是:無效的構造函數,你應該寫成。。。)
當編譯錯誤的時候你就開始糾結了,爲什麼拷貝構造函數一定要使用引用傳遞呢,我上網查找了許多資料,大家的意思基本上都是說如果用值傳遞的話可能會產生死循環。編譯器可能基於這樣的原因不允許出現值傳遞的拷貝構造函數,也有可能是C++標準是這樣規定的。

如果真是產生死循環這個原因的話,應該是這樣子的:
#include<iostream>
using namespace std;
class S
{
  int a;
  public:
  S(int x):a(x){}
  S(const S st){this->a=st.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,所以auto_ptr不能用於指向對象數組的指針。當複製 auto_ptr 對象或者將它的值賦給其他 auto_ptr 對象的時候,將基礎對象的所有權從原來的 auto_ptr 對象轉給副本,原來的 auto_ptr 對象重置爲未綁定狀態。因此,不能將 auto_ptrs 存儲在標準庫容器類型中。如果要將智能指針作爲STL容器的元素,可以使用Boost庫裏的shared_ptr。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章