(一):構造函數、析構函數、賦值與初始化、explicit關鍵字
構造函數是爲了保證對象的每個數據成員都被正確初始化
通常情況下構造函數應聲明爲公有函數,一般被隱式地調用。
構造函數被聲明爲私有有特殊的用途,比如單例模式,以後詳談。
構造函數可以有任意類型和任意個數的參數,一個類可以有多個構造
默認構造函數
不帶參數的構造函數
如果程序中未聲明,則系統自動產生出一個默認構造函數,是空函數
如果程序實現任何一個構造函數(包括拷貝構造函數),那麼編譯器將不再提供默認構造函數
棧上的對象生存期到了會自動調用析構函數;而new operator做了兩件事,一個是創建了對象內存,一個是調用構造函數;堆上的內存需要delete釋放,做了兩件事,一是調用析構函數,二是釋放內存
//我們不能調用一個構造函數但沒有提供參數(實例化對象),如
A a();
//因爲是有歧義的,我們也可以看成是聲明瞭一個沒有參數的函數a,返回值是類型A的一個對象
但在函數傳參的時候往往可以這樣寫: A() //即定義一個無名對象。
還有一個注意點,全局對象的構造先於main函數執行,在return 0時全局變量的生存期也到了,故也會自動調用
析構函數
沒有參數
析構函數不能被重載,如果沒有定義析構函數,編譯器會自動生成一個默認析構函數,其格式如下:
類名::~默認析構函數名( )
{
}
默認析構函數是一個空函數
實際上,構造函數和析構函數都是可以被顯式調用的,只是很少這樣做,
new的時候,其實做了兩件事,
一是:調用malloc分配所需內存(實際上是調用operator new),二是:調用構造函數。
delete的時候,也是做了兩件事,
一是:調用析造函數,二是:調用free釋放內存(實際上是調用operator delete)。
這裏只是爲了演示,正常情況下析構函數只會被調用一次,如果被調用兩次,而析構函數內有delete的操作,會導致內存釋放兩次的錯誤
placement new的作用就是:創建對象(調用該類的構造函數)但是不分配內存,而是在已有的內存塊上面創建對象。用於需要反覆創建並刪除的對象上,可以降低分配釋放內存的性能消耗。
轉換構造函數
單個參數的構造函數不一定是轉換構造函數
將其它類型轉換爲類類型
類的構造函數只有一個參數是非常危險的,因爲編譯器可以使用這種構造函數把參數的類型隱式轉換爲類類型
賦值與初始化的區別
在初始化語句中的等號不是運算符。
#include "Test.h"
int main(void)
{
Test t = 10; // 等價於Test t(10); 這裏的=不是運算符,表示初始化。
t = 20; // 賦值操作
Test t2;
t = t2; // 賦值操作 t.operator=(t2);
return 0;
}
explicit 關鍵字
只提供給類的構造函數使用的關鍵字。
編譯器不會把聲明爲explicit的構造函數用於隱式轉換,它只能在程序代碼中顯示創建對象
(二):初始化列表(const和引用成員)、拷貝構造函數
一、構造函數初始化列表
推薦在構造函數初始化列表中進行初始化
構造函數的執行分爲兩個階段
初始化段
普通計算段
一是構造對象之前,必須先構造對象的成員;二是對象成員構造的順序與定義時的順序有關,跟初始化列表順序無關;三是構造的順序和析構的順序相反;四是如果對象成員對應的類沒有默認構造函數,那對象成員也只能在初始化列表進行初始化。再提一點,如果類是繼承而來,基類沒有默認構造函數的時候,基類的構造函數要在派生類構造函數初始化列表中調用
(二)、const成員、引用成員的初始化
//const成員的初始化只能在構造函數初始化列表中進行
// 引用成員的初始化也只能在構造函數初始化列表中進行
// 對象成員(對象成員所對應的類沒有默認構造函數)的初始化,也只能在構造函數初始化列表中進行
因爲const變量或者引用都得在定義的時候初始化,所以const成員和引用成員必須在初始化列表中初始化。另外,可以使用定義枚舉類型來得到類作用域共有的常量。
如果類中沒有定義拷貝構造函數,則系統自動生成一個缺省複製構造函數,作爲該類的公有成員,所做的事情也是簡單的成員複製
(二)、拷貝構造函數調用的幾種情況
當函數的形參是類的對象,調用函數時,進行形參與實參結合時使用。這時要在內存新建立一個局部對象,並把實參拷貝到新的對象中。理所當然也調
用拷貝構造函數。還有一點,爲什麼拷貝構造函數的參數需要是引用? 這是因爲如果拷貝構造函數中的參數不是一個引用,即形如CClass(const
CClass c_class),那麼就相當於採用了傳值的方式(pass-by-value),而傳值的方式會調用該類的拷貝構造函數,從而造成無窮遞歸地調用拷貝構造函
數。
當函數的返回值是類對象,函數執行完成返回調用者時使用。也是要建立一個臨時對象,再返回調用者。爲什麼不直接用要返回的局部對象呢?
因爲局部對象在離開建立它的函數時就消亡了,不可能在返回調用函數後繼續生存,所以在處理這種情況時,編譯系統會在調用函數的表達式中創建一
個無名臨時對象,該臨時對象的生存週期只在函數調用處的表達式中。所謂return對象,實際上是調用拷貝構造函數把該對象的值拷入臨時對象。如果
返回的是變量,處理過程類似,只是不調用構造函數
(三):深拷貝與淺拷貝、空類與空數組
說得簡單點,假設一個類有指針成員,如果在拷貝的時候順帶連指針指向的內存也分配了,就稱爲深拷貝
如果只是分配指針本身的內存,那就是淺拷貝
淺拷貝造成的問題是有兩個指針指向同塊內存,delete其中一個指針,那麼剩下的指針將成爲野指針。編譯器合成的默認拷貝構造函數和賦值運算符是淺拷貝的,如果只是普通成員的賦值,淺拷貝也是可以的。
此外,如果我們想讓對象是獨一無二的,需要禁止拷貝,只需要將拷貝構造函數和等號運算符聲明爲私有,並且不提供它們的實現
注意:在編寫派生類的賦值函數時,不要忘記對基類的數據成員重新賦值,可以通過調用基類的賦值函數來實現
二、空類與空數組
空類默認產生的成員:
class Empty {};
Empty(); // 默認構造函數
Empty( const Empty& ); // 默認拷貝構造函數
~Empty(); // 默認析構函數
Empty& operator=( const Empty& ); // 默認賦值運算符
Empty* operator&(); // 取址運算符
const Empty* operator&() const; // 取址運算符 const
空類的大小爲1個字節
#include<iostream>
using namespace std;
int main()
{
int a[0];
class B {};
struct C
{
int m;
int n;
char buffer[];
};
class D
{
int s[0];
};
cout << "sizeof(a)=" << sizeof(a) << endl; //0
cout << "B{}=" << sizeof(B) << endl; //1
cout << "C=" << sizeof(C) << endl; //8
cout << "D=" << sizeof(D) << endl; //0
return 0;
}