拷貝建構函式與運算子過載(c++詳解)
拷貝建構函式與運算子過載
拷貝建構函式引出
先給出日期類的定義:
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修飾),再用已存在的類型別物件建立新物件時由編譯器自動呼叫。
- 拷貝建構函式是建構函式的一個過載。
- 拷貝建構函式的引數只有一個且必須使用引用傳參,使用傳值會引發無窮的遞迴呼叫。
傳值的話編譯不通過。
分析一下不通過的原因:
假如我們要給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時,兩個物件裡面的兩個指標指向同一塊空間,析構的時候釋放兩次,程式崩潰。