拷貝構造函數的參數類型必須是引用(轉)

在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;
copy constructor                 //  bbb.myTestFunc(aaa);

第一個輸出: constructorwith argument      // CExample aaa(2);

第二個輸出:constructorwith argument     // CExample bbb(3);

 前兩個是比較好理解的。

第三個輸出: assignmentoperator               // bbb = aaa;

第四個輸出: copyconstructor                     // CExample ccc = aaa;

這兩個得放到一塊說。 肯定會有人問爲什麼兩個不一致。原因是, bbb對象已經實例化了,不需要構造,此時只是將aaa賦值給bbb,只會調用賦值函數,就這麼簡單,還不懂的話,撞牆去!但是ccc還沒有實例化,因此調用的是拷貝構造函數,構造出ccc,而不是賦值函數。

第五個輸出: copyconstructor                     //  bbb.myTestFunc(aaa);

實際上是aaa作爲參數傳遞給bbb.myTestFunc(CExample ex), 即CExample ex = aaa;和第四個一致的, 所以還是拷貝構造函數,而不是賦值函數。

通過這個例子, 我們來分析一下爲什麼拷貝構造函數的參數只能使用引用類型。

看第四個輸出: copyconstructor                     // CExample ccc = aaa;

構造ccc,實質上是ccc.CExample(aaa); 我們假如拷貝構造函數參數不是引用類型的話, 那麼將使得 ccc.CExample(aaa)變成aaa傳值給ccc.CExample(CExampleex),即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++代碼,結果是什麼?

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中,都將編譯出錯。


發佈了27 篇原創文章 · 獲贊 6 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章