如果拷貝建構函式中的參數不是一個引用,即形如CClass(const CClass c_class),那麼就相當於採用了傳值的方式(pass-by-value),而傳值的方式會調用該類的拷貝建構函式,從而造成無窮遞迴地調用拷貝構 造函數。因此拷貝建構函式的參數必須是一個引用
在C++中, 建構函式,拷貝建構函式,解構函式和賦值函數(賦值運算子多載)是最基本不過的需要掌握的知識。 但是如果我問你“拷貝建構函式的參數為什麼必須使用參考型別?”這個問題, 你會怎麼回答? 或許你會回答為了減少一次記憶體拷貝? 很慚愧的是,我的第一感覺也是這麼回答。不過還好,我思索一下以後,發現這個答案是不對的。
原因:
如 果拷貝建構函式中的參數不是一個引用,即形如CClass(const CClass c_class),那麼就相當於採用了傳值的方式(pass-by-value),而傳值的方式會調用該類的拷貝建構函式,從而造成無窮遞迴地調用拷貝構 造函數。因此拷貝建構函式的參數必須是一個引用。
需要澄清的是,傳指標其實也是傳值,如果上面的拷貝建構函式寫成CClass(const CClass* c_class),也是不行的。事實上,只有傳引用不是傳值外,其他所有的傳遞方式都是傳值。
先從一個小例子開始:(自己測試一下自己看看這個程式的輸出是什嗎?)
複製代碼 代碼如下:
#include<iostream>
using namespace std;
class CExample
{
private:
int m_nTest;
public:
CExample(int x) : m_nTest(x) //帶參數建構函式
{
cout << "constructor with argument"<<endl;
}
// 拷貝建構函式,參數中的const不是嚴格必須的,但引用符號是必須的
CExample(const CExample & ex) //拷貝建構函式
{
m_nTest = ex.m_nTest;
cout << "copy constructor"<<endl;
}
CExample& operator = (const CExample &ex) //賦值函數(賦值運算子多載)
{
cout << "assignment operator"<<endl;
m_nTest = ex.m_nTest;
return *this;
}
void myTestFunc(CExample ex)
{
}
};
int main(void)
{
CExample aaa(2);
CExample bbb(3);
bbb = aaa;
CExample ccc = aaa;
bbb.myTestFunc(aaa);
return 0;
}
果你能一眼看出就是這個結果的話, 恭喜你,可以站起來扭扭屁股,不用再往下看了。
如果你的結果和輸出結果有誤差, 那拜託你謙虛的看完。
第一個輸出: constructor with argument // CExample aaa(2);
如果你不理解的話, 找個人把你拖出去痛打一頓,然後嘴裡還喊著“我是二師兄,我是二師兄.......”
第二個輸出:constructor with argument // CExample bbb(3);
分析同第一個
第三個輸出: assignment operator // bbb = aaa;
第四個輸出: copy constructor // CExample ccc = aaa;
這 兩個得放到一塊說。 肯定會有人問為什麼兩個不一致。原因是, bbb對象已經執行個體化了,不需要構造,此時只是將aaa賦值給bbb,只會調用賦值函數,就這麼簡單,還不懂的話,撞牆去! 但是ccc還沒有執行個體化,因此調用的是拷貝建構函式,構造出ccc,而不是賦值函數,還不懂的話,我撞牆去!!
第五個輸出: copy constructor // bbb.myTestFunc(aaa);
實 際上是aaa作為參數傳遞給bbb.myTestFunc(CExample ex), 即CExample ex = aaa;和第四個一致的, 所以還是拷貝建構函式,而不是賦值函數, 如果仍然不懂, 我的頭剛才已經流血了,不要再讓我撞了,你就自己使勁的再裝一次吧。
通過這個例子, 我們來分析一下為什麼拷貝建構函式的參數只能使用參考型別。
看第四個輸出: copy constructor // CExample ccc = aaa;
構 造ccc,實質上是ccc.CExample(aaa); 我們假如拷貝建構函式參數不是參考型別的話, 那麼將使得 ccc.CExample(aaa)變成aaa傳值給ccc.CExample(CExample ex),即CExample ex = aaa,因為 ex 沒有被初始化, 所以 CExample ex = aaa 繼續調用拷貝建構函式,接下來的是構造ex,也就是 ex.CExample(aaa),必然又會有aaa傳給CExample(CExample ex), 即 CExample ex = aaa;那麼又會觸發拷貝建構函式,就這下永遠的遞迴下去。
所以繞了那麼大的彎子,就是想說明拷貝建構函式的參數使用參考型別不是為了減少一次記憶體拷貝, 而是避免拷貝建構函式無限制的遞迴下去。
附帶說明,在下面幾種情況下會調用拷貝建構函式:
a、顯式或隱式地用同類型的一個對象來初始化另外一個對象。如上例中,用對象c初始化d;
b、作為實參(argument)傳遞給一個函數。如CClass(const CClass c_class)中,就會調用CClass的拷貝建構函式;
c、在函數體內返回一個對象時,也會調用傳回值類型的拷貝建構函式;
d、初始化序列容器中的元素時。比如 vector<string> svec(5),string的預設建構函式和拷貝建構函式都會被調用;
e、用列表的方式初始化數組元素時。string a[] = {string(“hello”), string(“world”)}; 會調用string的拷貝建構函式。
如 果在沒有顯式聲明建構函式的情況下,編譯器都會為一個類合成一個預設的建構函式。如果在一個類中聲明了一個建構函式,那麼就會阻止編譯器為該類合成預設的 建構函式。和建構函式不同的是,即便定義了其他建構函式(但沒有定義拷貝建構函式),編譯器總是會為我們合成一個拷貝建構函式。
另外函數的傳回值是不是引用也有很大的區別,返回的不是引用的時候,只是一個簡單的對象,此時需要調用拷貝建構函式,否則,如果是引用的話就不需要調用拷貝建構函式。
複製代碼 代碼如下:
#include<iostream>
using namespace std;
class A
{
private:
int m_nTest;
public:
A()
{
}
A(const A& other) //建構函式重載
{
m_nTest = other.m_nTest;
cout << "copy constructor"<<endl;
}
A & operator =(const A& other)
{
if(this != &other)
{
m_nTest = other.m_nTest;
cout<<"Copy Assign"<<endl;
}
return *this;
}
};
A fun(A &x)
{
return x; //返回的不是引用的時候,需要調用拷貝建構函式
}
int main(void)
{
A test;
fun(test);
system("pause");
return 0;
}
分享一道筆試題目,編譯運行中的C++代碼,結果是什嗎?(A)編譯錯誤;(B)編譯成功,運行時程式崩潰;(C)編譯運行正常,輸出10。請選擇正確答案並分析原因。
複製代碼 代碼如下:
class A
{
private:
int value;
public:
A(int n)
{
value = n;
}
A(A other)
{
value = other.value;
}
void Print()
{
cout<<value<<endl;
}
};
int main(void)
{
A a = 10;
A b = a;
b.Print();
return 0;
}
答案:編譯錯誤。在 複製建構函式中傳入的參數是A的一個執行個體。由於是傳值,把形參拷貝到實參會調用複製建構函式。因此如果允許複製建構函式傳值,那麼會形成永無休止的遞迴並 造成棧溢出。因此C++的標準不允許複製建構函式傳值參數,而必須是傳引用或者常量引用。在Visual Studio和GCC中,都將編譯出錯。