1. 程式人生 > 其它 >拷貝建構函式與運算子過載(c++詳解)

拷貝建構函式與運算子過載(c++詳解)

技術標籤:Cppc++堆疊引用傳遞

拷貝建構函式與運算子過載

拷貝建構函式引出

先給出日期類的定義:

class Date
{
public:
	

	Date(int year=1900,int month=1, int day=1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
    void  print()
	{
		cout << _year << " " << _month <<
" " << _day << endl; } private: int _year; int _month; int _day; };

有一種場景,我們需要另一個物件和之前物件所初始化的值一模一樣。

第一種方法:每次建立物件寫成一樣的值,但是如果d1的值改變了呢,其他在跟著修改,資料量很大時很不方便


Date d1(2020,2,1);
Date d2(2020,2,1);

第二種方法:採用拷貝建構函式

拷貝建構函式

拷貝建構函式:只有單個形參(不算那個隱含的this指標),該形參是對本類型別的一個引用(一般用const修飾),再用已存在的類型別物件建立新物件時由編譯器自動呼叫。

特點:

  1. 拷貝建構函式是建構函式的一個過載。
  2. 拷貝建構函式的引數只有一個且必須使用引用傳參,使用傳值會引發無窮的遞迴呼叫。
    在這裡插入圖片描述
    傳值的話編譯不通過。
    分析一下不通過的原因:
    假如我們要給d2物件拷貝d1的值 Date d2(d1);,根據以前的知識,傳值傳參需要發生拷貝把d1傳給Date(const Date d)中的d,就是傳值傳參,所以,d1拷貝給d就又要呼叫拷貝建構函式Date d(d1)`,在呼叫。引發無窮遞迴。
    所以需要傳引用
Date(const Date& d)
{
_year = d.year;
_month = d.month;
_day = d.day;
}

在和傳值對比一下:

傳引用時不需要拷貝,呼叫函式Date d2(d1); 然後看函式Date(const Date& d)的形參d,d是d1的一個別名。就完成了對 物件d1內成員的全部拷貝,但預設的拷貝建構函式只拷貝值,所以也被我們成為值拷貝,或者淺拷貝。
只拷貝值的含義是什麼呢,在這個例子可能理解的不是很清楚,我們看到這兩個的值雖然是完全一樣的,但是另一種場景呢?

class stack
{
public:
	stack(int capacity=4)
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		_capacity = capacity;
		_size = 0;

	}
	~stack
	{
	free(-a);
	_capacity=0;
	_size=0;
	}
	
private:
	int* _a;
	int _size;
	int _capacity;

};
int main()
{
	stack s1;
	stack s2(s1);
	return 0;
}

這裡我們想幹的是在建一個物件s2,這個物件也想s1一樣有一樣的一個一樣大小的陣列,但是我們發現
在這裡插入圖片描述
其他一樣是一樣了,但是s2的指標,和s1的指標竟然指向了同一塊空間,這就是淺拷貝。比如說,我看見了舍友有一個很好用的手機,我想要一個跟他記憶體一樣大的手機,而不是和他共用一個手機。

而且這個程式最後會崩潰,建立s2,s2後進系統上的棧,先出所以s2物件生命週期先結束,會呼叫我們寫的解構函式(自己生成的解構函式不釋放)釋放對上的空間,而s1此時是淺拷貝,和他共用一個空間,生命週期結束,再次呼叫解構函式,又free了一次,完蛋。

由於日期類裡面沒有指標什麼的,所以預設生成的就可以用,因為它只需要淺拷貝所有位元組, 而棧裡,則是必須我們要自己寫一個拷貝構造實現深拷貝(要一塊跟你一樣大的空間,讓指標指向自己的),因為不寫,就出現了上述舉例的情況

運算子過載引出

C++為了增加程式碼的可讀性引入了運算子過載。自定義型別就可以像內建型別一樣去用運算子。需要我們自己定義運算子過載

Date d; 
d+1;//日期加一天

運算子過載

運算子過載是具有特殊函式名的函式,也具有返回值型別,函式名字,及引數列表,其返回值型別與引數列表與普通函式類似。
函式名字:關鍵字operator後面接需要過載的運算子符號。
函式原型: 返回值型別 operator 後面接需要過載的運算子符號
注意:

  • 不能通過連線其他符號創造新的操作符
  • 衝在操作符必須有一個類型別或者列舉型別的運算元
  • 用於內建型別的操作符其含義不能改變,例如內建的整形+,不能改變其含義
  • 作為類成員函式,其形參看起來比運算元數目少一,因為成員函式有個預設的形參this指向呼叫物件,限定為第一個形參。
  • .* :: sizeof ?: . 以上五個運算子不能過載(面試選擇題經常考)
  • 想用什麼操作符去自己過載

第一次寫了這樣,忘記了作為成員函式的時候,第一個引數已經有了就是隱含的this指標,指向呼叫物件。
在這裡插入圖片描述
不需要改變所以加上const,傳值的話外面會呼叫拷貝建構函式,傳引用就不呼叫了。

bool operator==(const Date& d2)
	{
		return _year == d2._year && _month == d2._month && _day == d2._day;
	}

開始的時候運算子函式形參寫的是這樣 const Date d2,雖然也可以,但是傳遞過去會呼叫拷貝建構函式。
但是腦子一混,把operator==函式當成了拷貝建構函式,還在想拷貝建構函式的形參是const Date d2的話不就無窮呼叫了嗎。(zzz,你不重新寫拷貝建構函式,人家預設生成的的引數肯定是引用型別的,不用你操心)

賦值運算子過載

把d2的值賦值給d1,需要注意的是它與拷貝構造是不一樣的。賦值說明的是,兩個物件已經定義出來了。

Date d1;
Date d2(2021,2,6);
d1=d2;

而拷貝構造則是將d1的值直接初始化給d2。兩者是不一樣的

Date d1;
Date d2(d1);
//賦值過載
	void operator=(const Date& d)
	{
	//自己賦值自己沒有意義
	//開始寫成這樣,讓物件比較。。還要寫一個運算子過載函式比較 if(*this!=d)
	if(this!=&d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	          }
	}

但是在我們的內建型別中是支援連續賦值的
比如說:

int i,j,k;
i=j=k=10;

也就是說k=10,有一個返回值k,讓j=k,返回j,又讓i=j。
所以給我們的函式加上返回值,型別為類名,返回this所指向的物件

//賦值過載
	Date operator=(const Date& d)
	{
	//自己賦值自己沒有意義
	if(this!=&d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	          }
	return *this;
	}	

當返回值為Date型別,*this傳遞給外面需要兩次拷貝構造(先傳給臨時變數(Date型別),臨時變數再給外面用於接收的變數)

當返回值為Date&,臨時變數是返回值的別名,減少一次拷貝構造。形參做引用(總共一次)也減少一次拷貝構造。
所以下面這樣寫一共減少了兩次拷貝構造。

   //最終版賦值過載
	Date& operator=(const Date& d)
	{
		//自己賦值自己沒有意義
	if(this!=&d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	          }
	   return *this;
	}
	

注意:返回值做引用有一個條件就是看你返回值的生命週期,假如this所指向的物件生命週期在本函式體內,那麼this傳給中間變數,此時this銷燬,中間變數是個引用也跟著銷燬。外面的變數接收不到它。但是在這裡*this是d1物件,生命週期在main函式裡,所以不用擔心

當我們不寫,他也會自動生成,不過預設生成的他也是隻進行淺拷貝,只拷貝值,像之前寫拷貝建構函式裡的Static,裡面的指標變數。當d1=d2時,兩個物件裡面的兩個指標指向同一塊空間,析構的時候釋放兩次,程式崩潰。