C++建構函式詳解及顯示呼叫建構函式
c++類的建構函式詳解
一、 建構函式是幹什麼的
class Counter
{
public:
// 類Counter的建構函式
// 特點:以類名作為函式名,無返回型別
Counter()
{
m_value = 0;
}
private:
// 資料成員
int m_value;
}
該類物件被建立時,編譯系統物件分配記憶體空間,並自動呼叫該建構函式->由建構函式完成成員的初始化工作
eg: Counter c1;
編譯系統為物件c1的每個資料成員(m_value)分配記憶體空間,並呼叫建構函式Counter( )自動地初始化物件c1的m_value值設定為0
故:
建構函式的作用:初始化物件的資料成員。
二、 建構函式的種類
class Complex
{
private :
double m_real;
double m_imag;
public:
// 無引數建構函式
// 如果建立一個類你沒有寫任何建構函式,則系統會自動生成預設的無參建構函式,函式為空,什麼都不做
// 只要你寫了一個下面的某一種建構函式,系統就不會再自動生成這樣一個預設的建構函式,如果希望有一個這樣的無參建構函式,則需要自己顯示地寫出來
Complex(void)
{
m_real = 0.0;
m_imag = 0.0;
}
// 一般建構函式(也稱過載建構函式)
// 一般建構函式可以有各種引數形式,一個類可以有多個一般建構函式,前提是引數的個數或者型別不同(基於c++的過載函式原理)
// 例如:你還可以寫一個 Complex( int num)的構造函數出來
// 建立物件時根據傳入的引數不同調用不同的建構函式
Complex(double real, double imag)
{
m_real = real;
m_imag = imag;
}
// 複製建構函式(也稱為拷貝建構函式)
// 複製建構函式引數為類物件本身的引用,用於根據一個已存在的物件複製出一個新的該類的物件,一般在函式中會將已存在物件的資料成員的值複製一份到新建立的物件中
// 若沒有顯示的寫複製建構函式,則系統會預設建立一個複製建構函式,但當類中有指標成員時,由系統預設建立該複製建構函式會存在風險,具體原因請查詢 有關 “淺拷貝” 、“深拷貝”的文章論述
Complex(const Complex & c)
{
// 將物件c中的資料成員值複製過來
m_real = c.m_real;
m_imag = c.m_imag;
}
// 型別轉換建構函式,根據一個指定的型別的物件建立一個本類的物件,
//需要注意的一點是,這個其實就是一般的建構函式,但是對於出現這種單引數的建構函式,C++會預設將引數對應的型別轉換為該類型別,有時候這種隱私的轉換是我們所不想要的,所以需要使用explicit來限制這種轉換。
// 例如:下面將根據一個double型別的物件建立了一個Complex物件
Complex(double r)
{
m_real = r;
m_imag = 0.0;
}
// 等號運算子過載(也叫賦值建構函式)
// 注意,這個類似複製建構函式,將=右邊的本類物件的值複製給等號左邊的物件,它不屬於建構函式,等號左右兩邊的物件必須已經被建立
// 若沒有顯示的寫=運算子過載,則系統也會建立一個預設的=運算子過載,只做一些基本的拷貝工作
Complex &operator=( const Complex &rhs )
{
// 首先檢測等號右邊的是否就是左邊的物件本身,若是本物件本身,則直接返回
if ( this == &rhs )
{
return *this;
}
// 複製等號右邊的成員到左邊的物件中
this->m_real = rhs.m_real;
this->m_imag = rhs.m_imag;
// 把等號左邊的物件再次傳出
// 目的是為了支援連等 eg: a=b=c 系統首先執行 b=c
// 然後執行 a= ( b=c的返回值,這裡應該是複製c值後的b物件)
return *this;
}
};
下面使用上面定義的類物件來說明各個建構函式的用法:
int main()
{
// 呼叫了無參建構函式,資料成員初值被賦為0.0
Complex c1,c2;
// 呼叫一般建構函式,資料成員初值被賦為指定值
Complex c3(1.0,2.5);
// 也可以使用下面的形式
Complex c3 = Complex(1.0,2.5);
// 把c3的資料成員的值賦值給c1
// 由於c1已經事先被建立,故此處不會呼叫任何建構函式
// 只會呼叫 = 號運算子過載函式
c1 = c3;
// 呼叫型別轉換建構函式
// 系統首先呼叫型別轉換建構函式,將5.2建立為一個本類的臨時物件,然後呼叫等號運算子過載,將該臨時物件賦值給c1
c2 = 5.2;
// 呼叫拷貝建構函式( 有下面兩種呼叫方式)
Complex c5(c2);
Complex c4 = c2; // 注意和 = 運算子過載區分,這裡等號左邊的物件不是事先已經建立,故需要呼叫拷貝建構函式,引數為c2
//這一點特別重要,這兒是初始化,不是賦值。其實這兒就涉及了C++中的兩種初始化的方式:複製初始化和賦值初始化。其中c5採用的是複製初始化,而c4採用的是賦值初始化,這兩種方式都是要呼叫拷貝建構函式的。
}
三、思考與測驗
1. 仔細觀察複製建構函式
Complex(const Complex & c)
{
// 將物件c中的資料成員值複製過來
m_real = c.m_real;
m_img = c.m_img;
}
為什麼函式中可以直接訪問物件c的私有成員?
答:(網上)因為拷貝建構函式是放在本身這個類裡的,而類中的函式可以訪問這個類的物件的所有成員,當然包括私有成員了。
2. 挑戰題,瞭解引用與傳值的區別
Complex test1(const Complex& c)
{
return c;
}
Complex test2(const Complex c)
{
return c;
}
Complex test3()
{
static Complex c(1.0,5.0);
return c;
}
Complex& test4()
{
static Complex c(1.0,5.0);
return c;
}
void main()
{
Complex a,b;
// 下面函式執行過程中各會呼叫幾次建構函式,呼叫的是什麼建構函式?
test1(a);
test2(a);
b = test3();
b = test4();
test2(1.2);
// 下面這條語句會出錯嗎?
test1(1.2); //test1( Complex(1.2 )) 呢?
}
答:
為了便於看建構函式的呼叫效果,我將類重新改一下,新增一些輸出資訊,程式碼如下:
View Code
下面是程式執行結果:第一次執行結果
第2次執行結果第3次執行結果
四、附錄(淺拷貝與深拷貝)
上面提到,如果沒有自定義複製建構函式,則系統會建立預設的複製建構函式,但系統建立的預設複製建構函式只會執行“淺拷貝”,即將被拷貝物件的資料成員的 值一一賦值給新建立的物件,若該類的資料成員中有指標成員,則會使得新的物件的指標所指向的地址與被拷貝物件的指標所指向的地址相同,delete該指標 時則會導致兩次重複delete而出錯。下面是示例:
【淺拷貝與深拷貝】
#include <iostream.h>
#include <string.h>
class Person
{
public :
// 建構函式
Person(char * pN)
{
cout << "一般建構函式被呼叫 !\n";
m_pName = new char[strlen(pN) + 1];
//在堆中開闢一個記憶體塊存放pN所指的字串
if(m_pName != NULL)
{
//如果m_pName不是空指標,則把形參指標pN所指的字串複製給它
strcpy(m_pName ,pN);
}
}
// 系統建立的預設複製建構函式,只做位模式拷貝
Person(Person & p)
{
//使兩個字串指標指向同一地址位置
m_pName = p.m_pName;
}
~Person( )
{
delete m_pName;
}
private :
char * m_pName;
};
void main( )
{
Person man("lujun");
Person woman(man);
// 結果導致 man 和 woman 的指標都指向了同一個地址
// 函式結束析構時
// 同一個地址被delete兩次
}
// 下面自己設計複製建構函式,實現“深拷貝”,即不讓指標指向同一地址,而是重新申請一塊記憶體給新的物件的指標資料成員
Person(Person & chs);
{
// 用運算子new為新物件的指標資料成員分配空間
m_pName=new char[strlen(p.m_pName)+ 1];
if(m_pName)
{
// 複製內容
strcpy(m_pName ,chs.m_pName);
}
// 則新建立的物件的m_pName與原物件chs的m_pName不再指向同一地址了
}
參考地址:http://ticktick.blog.51cto.com/823160/194307
下面討論一個重要問題是:建構函式的顯式呼叫
大家看看下面這段程式碼的輸出結果是什麼?這段程式碼有問題麼?
#include <iostream>
class CTest
{
public:
CTest()
{
m_a = 1;
}
CTest(int b)
{
m_b = b;
CTest();
}
~CTest()
{}
void show
{
std::cout << m_a << std::endl;
std::cout << m_b << std::endl;
}
private:
int m_a;
int m_b;
};
void main()
{
CTest myTest(2);
myTest.show();
}
-----------------------------------------------------------
【分析】
-----------------------------------------------------------
輸出結果中,m_a是一個不確定的值,因為沒有被賦初值,m_b 為2
注意下面這段程式碼
CTest(int b)
{
m_b = b;
CTest();
}
在呼叫CTest()函式時,實際上是建立了一個匿名的臨時CTest類物件,CTest()中賦值 m_a = 1 也是對該匿名物件賦值,故我們定義的myTest的m_a其實沒有被賦值。說白了,其實建構函式並不像普通函式那樣進行一段處理,而是建立了一個物件,並 且對該物件賦初值,所以顯式呼叫建構函式無法實現給私有成員賦值的目的。
這個例子告訴我們以後程式碼中千萬不要出現使用一個建構函式顯式呼叫另外一個建構函式,這樣會出現不確定性。其實一些初始化的程式碼可以寫在一個單獨的init函式中,然後每一個建構函式都呼叫一下該初始化函式就行了。
在此,順便再提出另外一個問題以供思考:
CTest *p = NULL;
void func()
{
p = new CTest();
}
程式碼右邊顯示呼叫CTest(),是否依然會產生一個匿名的臨時物件a,然後將該匿名的臨時物件a的地址賦給指標p? 如果是這樣的話,出了func函式後,臨時物件a是否會被析構? 那指標p不成為了野指標了?你能解釋這個問題麼?
答:我實驗的結果是不會產生臨時物件a,直接將產生的物件指標賦給了p