1. 程式人生 > >類和物件02 四個預設的函式 類的生存週期

類和物件02 四個預設的函式 類的生存週期

在c++中有六個預設函式 他們分別是:

一.建構函式

二.解構函式

三.拷貝建構函式

四.賦值運算子的過載函式

五.取地址操作符的過載函式

六.const修飾的取地址操作符的過載函式

在這裡我們只討論前四個函式

 

  1. 建構函式

我們知道面向物件的程式設計語言傾向於物件 而物件一定要經過初始化後使用起來才安全 因此引入了建構函式的概念 物件在建立時都會呼叫建構函式來進行初始化  建構函式可用於為某些成員變數設定初始值。

建構函式作用:初始化物件的記憶體空間(對成員變數賦資源)

 

首先物件的生成分為兩步:

  1. 給物件開闢記憶體空間
  2. 給物件的記憶體呼叫建構函式來做初始化

     需要注意的是:建構函式執行時 物件的記憶體空間已經分配好了 建構函式的作用只是初始化這片空間  物件在生成時 一定會自動呼叫某個建構函式來進行初始化

 

系統預設的函式:1.公有的 2.內聯的

 

建構函式是一種特殊的成員函式 如果使用者沒有自己寫建構函式 編譯器會自動生成一個無參的建構函式 也叫預設函式;如果使用者編寫了建構函式 那麼編譯器也就不會自動生成預設構造函數了

 

建構函式名稱和類名字相同 並且不寫返回值型別(void)也不寫;

系統提供的預設建構函式如下:

Student()

{

}          //預設建構函式

 

總結一下建構函式:

(1)建構函式可以過載

(2)不能手動呼叫

(3)順序構造

 

2.解構函式

解構函式於建構函式相對應,建構函式是物件建立的時候自動呼叫的,而解構函式就是物件在銷燬的時候自動呼叫的

解構函式的函式名是在類名前加上~ 不返回任何值,沒有返回型別,也沒有函式引數。由於沒有函式引數,因此它不能被過載 永遠只能寫一個。換言之,一個類可以有多個建構函式,但是隻能有一個解構函式(也可以這樣理解:生而不同死了都一樣==)

~Student()

{

}

 

一個類只能有且有一個解構函式,如果沒有顯式的定義,系統會生成一個預設的解構函式(合成解構函式)。每有一次建構函式的呼叫就會有一次解構函式的呼叫;

物件的銷燬:

  1. 釋放物件所佔的其他資源
  2. 釋放物件所佔的記憶體空間

 

當在主函式中用new建立物件時 僅僅是呼叫建構函式建立了物件,分配了記憶體空間。但是沒有呼叫解構函式,因為此時指定的物件的記憶體是由new來建立分配的,此時分配空間是分配在堆上的,只有主動析構或程式結束,才會釋放空間 編譯器不能夠自動呼叫解構函式將其刪除 所以必須要呼叫delete手動釋放才可以。

 

總結一下解構函式:

(1)解構函式不可以過載

(2)可以手動呼叫

(3)先構造 後析構

 

3.拷貝建構函式

原型:類名(const 類名& rhs)

拷貝建構函式是一種特殊的建構函式,它能夠用一個已知的物件初始化一個被建立的同類新物件。該函式的形式引數必須是本類物件的常引用,因此與普通建構函式在形式引數上有非常明顯的區別。

拷貝建構函式的引數若不是引用而是值傳遞則會導致拷貝建構函式無限制的遞迴下去從而導致棧溢位。因此拷貝建構函式的引數必須為引用。

 

深拷貝與淺拷貝

 

淺拷貝:在物件複製時 只對物件中的資料成員進行簡單的賦值 預設拷貝建構函式執行的也是淺拷貝

來舉一個例子:

class Rect

{

 public:

      Rect( )  //建構函式 ;p指向堆中的空間

      {

        p=new int(100);

}

~ Rect( )  //解構函式 ;釋放動態分配的空間

      {

        delete p;

}

 private:

       int width;

       int height;

       int* p;  //指標成員

};

 

int main()

{

    Rect rect1;

    Rect rect2(rect1);//複製物件

return 0;

}

 

分析:在執行定義rect1物件後 由於在建構函式中有一個動態分配 於是為:

在使用rect1複製rect2時 由於執行的是淺拷貝 只是將成員的值進行賦值 這時ret1.p=rect2.p 即這兩個指標指向了堆裡的同一個空間 如下圖:

 

這並不是我們期待的結果 因為在銷燬物件時 兩個物件的解構函式將對同一個記憶體空間釋放兩次於是就會出錯 此時我們就需要深拷貝:

class Rect

{

 public:

      Rect( )  //建構函式 ;p指向堆中的空間

      {

        p=new int(100);

}

Rect(const Rect& r)//拷貝建構函式

{

  width = r.width;

  height = r.height;

  p = new int;

  *p = *(r.p);

}

~ Rect( )  //解構函式 ;釋放動態分配的空間

      {

        delete p;

}

 private:

       int width;

       int height;

       int* p;  //指標成員

};

 

int main()

{

    Rect rect1;

    Rect rect2(rect1);//複製物件

return 0;

}

此時 在完成物件的複製後為下圖:

此時rect1的p和rect2的p各自指向一段記憶體空間 但它們之指向的空間具有相同的內容 這就是深拷貝 因此當成員有指標成員時 一定要考慮深拷貝!

 

4.賦值運算子的過載函式

形式: 類名& operator =(const 類名& rhs)  返回一個類物件的引用(如果賦值運算子返回的是類物件本身則會呼叫拷貝建構函式 如果賦值運算子返回的是物件引用,那麼其不會呼叫類的拷貝建構函式,這是返回物件和返回引用的主要區別,返回引用的效率明顯較高)

預設淺拷貝 要注意有指標成員時考慮深拷貝

 

若賦值建構函式如果引數為值傳遞 僅僅是多了一次拷貝 並不會無限遞迴。賦值建構函式引數既可以為引用,也可以為值傳遞,值傳遞會多一次拷貝。因此建議賦值建構函式建議也寫為引用型別 減少拷貝次數能提高賦值效率

 

作用:把已存在的物件賦值給相同型別的物件(不生成新物件)

步驟:

  1. 防止自賦值
  2. 釋放舊資源(防止指向新資源 舊資源記憶體洩漏)
  3. 開闢新資源
  4. 賦值

程式碼如下:

class CGoods

{

public:

CGoods& operator=(const CGoods& rhs)

{

if (this != &rhs)

{

delete[] mname;

mname = new char[strlen(rhs.mname) + 1]();

 

strcpy(mname, rhs.mname);

mprice = rhs.mprice;

mamount = rhs.mamount;

}

return *this;

}

private:

char* mname;

float mprice;

int mamount;

};

int main()

{

CGoods good1;

CGoods good3("good3", 10.1, 20);

good1 = good3;//此時this指標在good1裡

//good1.operator=(good3)

 return 0;

}

 

C++ 規定,在非靜態成員函式內部可以直接使用 this 關鍵字,this 就代表指向該函式所作用的物件的指標。

因為靜態成員函式並不作用於某個物件,所以在其內部不能使用 this 指標;

 

臨時物件:

int main()

{

                  CGoods good1= (“good1”,”10.1””20”);(類例項化物件)

                  good1 = 30;

            (CGoods類)(int類)

}

首先賦值運算子左右運算元的型別要匹配 然而現在左運算元為CGoods類 右運算元為Int類 此時會先找帶有int型別引數的建構函式 來生成一個臨時物件 此時臨時物件的型別為CGoods 型別就匹配了(臨時物件只在建構函式中生成)

 

臨時物件:

  1. 隱式生成臨時物件 : 編譯器推演需要的物件型別
  2. 顯式生成臨時物件 : 程式指明要生成的物件型別

優化:如果臨時物件生成的目的是為了生成新物件 那麼就會以生成臨時物件的方式生成新物件

生存週期:表示式結束 臨時物件銷燬 good1 = 30;(遇到;就銷燬了)一般表示式結束標誌為, ; ?

引用會提升臨時物件的生存週期 使臨時物件變得和引用物件一樣的生存週期

類型別的返回值 都是由臨時物件帶出來

 

臨時量(包括臨時物件):

     內建型別==> 常量

     自定義型別==> 變數(記憶體中)

     隱式生成的臨時量==>常量

形參const:1.防止實參被修改 2.接收隱式生成的臨時量(常量 加const為常引用)

explicit作用:禁止隱式生成臨時物件(臨時物件只在建構函式中生成)