1. 程式人生 > 其它 >7.C++拷貝建構函式

7.C++拷貝建構函式

拷貝建構函式

我們經常會用一個變數去初始化一個同類型的變數,那麼對於自定義的型別也應該有類似的操作,那麼建立物件時如何使用一個已經存在的物件去建立另一個與之相同的物件呢?

建構函式:只有單個形參,該形參是對本類型別物件的引用(一般常用const修飾),在用已存在的類型別物件建立新物件時由編譯器自動呼叫

拷貝建構函式是建構函式的一個過載,因此顯式的定義了拷貝構造,那麼編譯器也不再預設生成建構函式。

特徵

拷貝構造也是一個特殊的成員函式。

特徵如下:

  • 拷貝構造是建構函式的一個過載;
  • 拷貝構造的引數只有一個並且型別必須是該類的引用,而不是使用傳值呼叫,否則會無限遞迴;
  • 若沒有顯式定義拷貝建構函式,編譯器會自己生成一個預設拷貝構造,預設的拷貝建構函式物件按記憶體儲存和位元組序完成拷貝,也叫淺拷貝;
class Date
{
public:
	Date(int year, int month, int day)
		:
		_year(year),
		_month(month),
		_day(day)
	{}
	void Display()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2001, 7, 28);
	Date d2(d1);
	d1.Display();
	d2.Display();
	return 0;
}

輸出:

2001-7-28

2001-7-28

  • 那麼對於那些直接管理著記憶體資源的類(含有指標變數),那麼簡單的值拷貝還頂得住嗎?顯然頂不住啊。

通過圖示說明:

兩個string類的物件指向了同一塊空間,這不就亂套了嗎,如果其中一個物件通過指標改變了指向記憶體的資料,那麼另一個物件也會受到影響,這是我們不願發生的,我們希望每個物件都能獨立運作。

下面這個程式會崩潰。

class String
{
public:
	String(const char* str = "songxin")
	{
		cout << "String(const char* str = \"songxin\")" << endl;
		_str = (char*)malloc(strlen(str) + 1);
		strcpy(_str, str);
	}
	~String()
	{
		cout << "~String()" << endl;
		free(_str);
		_str = nullptr;
	}
private:
	char* _str;
};
int main()
{
	String s1;
	String s2(s1);
	return 0;
}

原因是兩個string類的成員指標都指向一塊記憶體,而它們又分別呼叫了一次解構函式,相當於對同一塊記憶體空間釋放了兩次,程式崩潰。

因此對於這種情況的物件,我們就不能再使用編譯器生成的預設拷貝構造了,而只能自己去顯式的定義拷貝構造並且要實現深拷貝。

編譯器生成的拷貝構造

編譯器預設生成的拷貝構造會做些什麼呢?

  • 對於內建型別成員

​ 完成值拷貝;

  • 對於自定義型別成員

    呼叫成員的拷貝構造;

class Time
{
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		:
		_hour(hour),
		_minute(minute),
		_second(second)
	{}
	Time(Time& t)
	{
		_hour = t._hour;
		_minute = t._minute;
		_second = t._second;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
Time top(0, 1, 1);
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1, Time& t = top)
		:
		_year(year),
		_month(month),
		_day(day),
		_t(t)
	{}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};
int main()
{
	Time t(1, 1, 1);
	Date d1(2001, 7, 28,t);
	Date d2(d1);
	return 0;
}

如果預設生成的拷貝構造沒有呼叫Time類成員的拷貝構造,那麼d2的_t的值應該是(_hour = 0, _minute = 0, _second = 0),而這裡最終的結果是d2中的_t和d2中的_t值相同。

這裡Date類中自動生成的拷貝建構函式的內建型別會進行位元組序拷貝,而對於自定義型別_t呼叫了Time的拷貝建構函式。

拷貝構造的初始化列表

拷貝構造是建構函式的一個過載,因此拷貝建構函式也是有初始化列表的,所以也建議在初始化列表階段完成對物件的初始化,養成良好習慣。

可以不顯式定義拷貝建構函式的情況

  • 成員變數沒有指標;
  • 成員有指標,但並沒有管理記憶體資源;

顯式定義拷貝構造的誤區

之前一直存在這個誤區:

我們都知道,編譯器生成的建構函式在初始化列表會呼叫成員的建構函式,而我們顯式去定義建構函式時,即使我們不寫也會在初始化列表去呼叫自定義型別成員的建構函式。

通過類比,我就犯了一個低階錯誤:

就是既然編譯器生成的拷貝構造可以在初始化列表自動呼叫自定義成員的拷貝構造,那麼我們顯式定義的拷貝構造即使不寫,也會在初始化列表自動去呼叫自定義成員的拷貝構造。

於是我寫出瞭如下程式碼:

class Time
{
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		:
		_hour(hour),
		_minute(minute),
		_second(second)
	{}
	Time(Time& t)
	{
		_hour = t._hour;
		_minute = t._minute;
		_second = t._second;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
Time top(2, 2, 2);
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1, Time& t = top)
		:
		_year(year),
		_month(month),
		_day(day),
		_t(t)
	{}
	Date(Date& d)//顯式定義了拷貝構造
	{

	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};
int main()
{
	Time t(1, 1, 1);
	Date d1(2001, 7, 28,t);
	Date d2(d1);
	return 0;
}

通過監視視窗檢視d2呼叫拷貝構造後的值:

並沒有拷貝成功。

我只顧著類比它們的功能,可我恰恰忽略了拷貝構造也是一種建構函式啊,那麼自然初始化列表也是和普通構造一樣,會去呼叫自定義類的建構函式,不處理內建型別。只不過編譯器生成的是經過處理的建構函式達到了拷貝的效果。(太傻逼了這錯誤)

結論

拷貝建構函式是建構函式的一種,它也有初始化列表,如果是編譯器生成的拷貝構造,它會對內建型別做位元組序拷貝對自定義型別成員會呼叫自定義成員的拷貝構造

可如果是我們顯式定義出的拷貝構造,它也是有初始化列表的,但是它的初始化列表可不會去呼叫成員的拷貝構造奧,而是和普通建構函式一樣,對於內建型別成員不去初始化值,對於自定義型別成員呼叫自定義成員的建構函式而不是拷貝建構函式。

編譯器關於拷貝構造的優化

下面這段程式碼共呼叫了多少次拷貝構造?

class Widget
{
public:
	Widget()
		:
		_n()
	{
		cout << "Widget()" << endl;
	}
	Widget(const Widget& d)
		:
		_n(d._n)
	{
		cout << "Widget(Widget& d)" << endl;
	}
private:
	int _n;
};

Widget f(Widget u)
{
	Widget v(u);
	Widget w = v;
	return w;
}

int main() 
{
	Widget x;
	Widget y = f(f(x));
}

我們一個一個數。

首先執行f(x),那麼會傳參拷貝(第一次)

函式體Widget v(u);(第二次)

Widget w = v;(第三次)

return w;用w拷貝構造一個的臨時物件tmp(第四次),臨時物件tmp作為實參傳值拷貝給形參u(第五次)

函式體Widget v(u);(第六次)

Widget w = v;(第七次)

return w;用w拷貝構造一個的臨時物件tmp(第八次),使用臨時物件tmp拷貝構造y(第九次)

以上是我們的分析結果,但事實如此嗎?

輸出結果:

Widget()
Widget(Widget& d)
Widget(Widget& d)
Widget(Widget& d)
Widget(Widget& d)
Widget(Widget& d)
Widget(Widget& d)
Widget(Widget& d)

這裡呼叫了七次拷貝構造,和我們分析出的不太一樣。

我們知道不同編譯器是有差異的,它們和C++標準是有一些出入的,在一些比較新的編譯器通常會做下列優化,讓我們的程式更快更節約資源。

在一個表示式中,連續構造會被優化:

  • 構造+拷貝構造
class Widget
{
public:
	Widget(int n = 0)
		:
		_n(n)
	{
		cout << "Widget()" << endl;
	}
	Widget(const Widget& d)
		:
		_n(d._n)
	{
		cout << "Widget(Widget& d)" << endl;
	}
private:
	int _n;
};
int main() 
{
	Widget x = 1;
}

輸出:

Widget()

按照以前所講的,第29行這裡會發生隱式型別轉換,即會用1去呼叫Widget的建構函式,並且1會作為建構函式的第一個形參,構造出一個Widget臨時物件再使用這個臨時物件去呼叫x的拷貝構造,簡言之就是先構造再拷貝構造

但是這裡通過輸出我們知道Widget x = 1;只調用了一次建構函式,也就是說這種先構造再拷貝構造會被編譯器優化為直接使用1去構造x,而不是用1構造一個臨時物件,再使用臨時物件去拷貝構造x。

  • 拷貝構造+拷貝構造
class Widget
{
public:
	Widget(int n = 0)
		:
		_n(n)
	{
		cout << "Widget()" << endl;
	}
	Widget(const Widget& d)
		:
		_n(d._n)
	{
		cout << "Widget(Widget& d)" << endl;
	}
private:
	int _n;
};

Widget f(Widget u)
{
	Widget v(u);
	Widget w = v;
	return w;
}

int main()
{
	Widget x;
	Widget y = f(x);
	return 0;
}

輸出:

Widget()
Widget(Widget& d)
Widget(Widget& d)
Widget(Widget& d)
Widget(Widget& d)

呼叫了四次拷貝構造。

倘若我們將30行改為f(x);,再來看看輸出結果:

Widget()
Widget(Widget& d)
Widget(Widget& d)
Widget(Widget& d)
Widget(Widget& d)

這兩次居然是一樣的,可明明Widget y = f(x);應該比f(x);要多一次拷貝構造啊???畢竟多了一步y的拷貝構造。

說明其中有兩次拷貝構造被合併為一次拷貝構造了,傳參過程和函式體內中是一定不會存在拷貝構造的優化的,只有return w;,會先使用w拷貝構造一個臨時物件,再用臨時物件去拷貝構造y這段過程會被優化,這兩次拷貝構造被優化為直接使用w去拷貝構造y,因此這兩段程式碼呼叫拷貝構造的次數是相同的。

結論:

  1. 構造 + 拷貝構造會被編譯器優化為一次構造;
  2. 連續的拷貝構造會被編譯器優化為一次拷貝構造;

因此為何第一段程式碼我們也就理解了,第四次和第五次會合併為一次,第八次和第九次會合併為一次,總共呼叫了七次拷貝構造。

關於這裡的優化不是C++標準所規定的,瞭解即可。