寫在前面:
關於C++的賦值運算符重載函數(operator=),網絡以及各種教材上都有很多介紹,但可惜的是,內容大多雷同且不全面。面對這一局面,在下在整合各種資源及融入個人理解的基礎上,整理出一篇較為全面/詳盡的文章,以饗讀者。
正文:
Ⅰ.舉例
例1
#include<iostream> #include<string> using namespace std; class MyStr { private: char *name; int id; public: MyStr() {} MyStr(int _id, char *_name) //constructor { cout << "constructor" << endl; id = _id; name = new char[strlen(_name) + 1]; strcpy_s(name, strlen(_name) + 1, _name); } MyStr(const MyStr& str) { cout << "copy constructor" << endl; id = str.id; if (name != NULL) delete[] name; name = new char[strlen(str.name) + 1]; strcpy_s(name, strlen(str.name) + 1, str.name); } MyStr& operator =(const MyStr& str)//賦值運算符 { cout << "operator =" << endl; if (this != &str) { if (name != NULL) delete[] name; this->id = str.id; int len = strlen(str.name); name = new char[len + 1]; strcpy_s(name, strlen(str.name) + 1, str.name); } return *this; } ~MyStr() { delete[] name; } }; int main() { MyStr str1(1, "hhxx"); cout << "====================" << endl; MyStr str2; str2 = str1; cout << "====================" << endl; MyStr str3 = str2; return 0; }
結果:
Ⅱ.參數
一般地,賦值運算符重載函數的參數是函數所在類的const類型的引用(如上面例1),加const是因為:
①我們不希望在這個函數中對用來進行賦值的“原版”做任何修改。
②加上const,對於const的和非const的實參,函數就能接受;如果不加,就只能接受非const的實參。
用引用是因為:
這樣可以避免在函數調用時對實參的一次拷貝,提高了效率。
注意:
上面的規定都不是強制的,可以不加const,也可以沒有引用,甚至參數可以不是函數所在的對象,正如后面例2中的那樣。
Ⅲ.返回值
一般地,返回值是被賦值者的引用,即*this(如上面例1),原因是
①這樣在函數返回時避免一次拷貝,提高了效率。
②更重要的,這樣可以實現連續賦值,即類似a=b=c這樣。如果不是返回引用而是返回值類型,那么,執行a=b時,調用賦值運算符重載函數,在函數返回時,由於返回的是值類型,所以要對return后邊的“東西”進行一次拷貝,得到一個未命名的副本(有些資料上稱之為“匿名對象”),然后將這個副本返回,而這個副本是右值,所以,執行a=b后,得到的是一個右值,再執行=c就會出錯。
注意:
這也不是強制的,我們可以將函數返回值聲明為void,然后什么也不返回,只不過這樣就不能夠連續賦值了。
Ⅳ.調用時機
當為一個類對象賦值(注意:可以用本類對象為其賦值(如上面例1),也可以用其它類型(如內置類型)的值為其賦值,關於這一點,見后面的例2)時,會由該對象調用該類的賦值運算符重載函數。
如上邊代碼中
str2 = str1;
一句,用str1為str2賦值,會由str2調用MyStr類的賦值運算符重載函數。
需要注意的是,
MyStr str2;
str2 = str1;
和
MyStr str3 = str2;
在調用函數上是有區別的。正如我們在上面結果中看到的那樣。
前者MyStr str2;一句是str2的聲明加定義,調用無參構造函數,所以str2 = str1;一句是在str2已經存在的情況下,用str1來為str2賦值,調用的是拷貝賦值運算符重載函數;而后者,是用str2來初始化str3,調用的是拷貝構造函數。
Ⅴ.提供默認賦值運算符重載函數的時機
當程序沒有顯式地提供一個以本類或本類的引用為參數的賦值運算符重載函數時,編譯器會自動生成這樣一個賦值運算符重載函數。注意我們的限定條件,不是說只要程序中有了顯式的賦值運算符重載函數,編譯器就一定不再提供默認的版本,而是說只有程序顯式提供了以本類或本類的引用為參數的賦值運算符重載函數時,編譯器才不會提供默認的版本。可見,所謂默認,就是“以本類或本類的引用為參數”的意思。
見下面的例2
#include<iostream> #include<string> using namespace std; class Data { private: int data; public: Data() {}; Data(int _data) :data(_data) { cout << "constructor" << endl; } Data& operator=(const int _data) { cout << "operator=(int _data)" << endl; data = _data; return *this; } }; int main() { Data data1(1); Data data2,data3; cout << "=====================" << endl; data2 = 1; cout << "=====================" << endl; data3 = data2; return 0; }
結果:
上面的例子中,我們提供了一個帶int型參數的賦值運算符重載函數,data2 = 1;一句調用了該函數,如果編譯器不再提供默認的賦值運算符重載函數,那么,data3 = data2;一句將不會編譯通過,但我們看到事實並非如此。所以,這個例子有力地證明了我們的結論。
Ⅵ.構造函數還是賦值運算符重載函數
如果我們將上面例子中的賦值運算符重載函數注釋掉,main函數中的代碼依然可以編譯通過。只不過結論變成了
可見,當用一個非類A的值(如上面的int型值)為類A的對象賦值時
①如果匹配的構造函數和賦值運算符重載函數同時存在(如例2),會調用賦值運算符重載函數。
②如果只有匹配的構造函數存在,就會調用這個構造函數。
Ⅶ.顯式提供賦值運算符重載函數的時機
①用非類A類型的值為類A的對象賦值時(當然,從Ⅵ中可以看出,這種情況下我們可以不提供相應的賦值運算符重載函數而只提供相應的構造函數來完成任務)。
②當用類A類型的值為類A的對象賦值且類A的成員變量中含有指針時,為避免淺拷貝(關於淺拷貝和深拷貝,下面會講到),必須顯式提供賦值運算符重載函數(如例1)。
Ⅷ.淺拷貝和深拷貝
拷貝構造函數和賦值運算符重載函數都會涉及到這個問題。
所謂淺拷貝,就是說編譯器提供的默認的拷貝構造函數和賦值運算符重載函數,僅僅是將對象a中各個數據成員的值拷貝給對象b中對應的數據成員(這里假設a、b為同一個類的兩個對象,且用a拷貝出b或用a來給b賦值),而不做其它任何事。
假設我們將例1中顯式提供的拷貝構造函數注釋掉,然后同樣執行MyStr str3 = str2;語句,此時調用默認的拷貝構造函數,它只是將str2的id值和nane值拷貝到str3,這樣,str2和str3中的name值是相同的,即它們指向內存中的同一區域(在例1中,是字符串”hhxx”)。如下圖
這樣,會有兩個致命的錯誤
①當我們通過str2修改它的name時,str3的name也會被修改!
②當執行str2和str3的析構函數時,會導致同一內存區域釋放兩次,程序崩潰!
這是萬萬不可行的,所以我們必須通過顯式提供拷貝構造函數以避免這樣的問題。就像我們在例1中做的那樣,先判斷被拷貝者的name是否為空,若否,delete[] name(后面會解釋為什么要這么做),然后,為name重新申請空間,再將拷貝者name中的數據拷貝到被拷貝者的name中。執行后,如圖
這樣,str2.name和str3.name各自獨立,避免了上面兩個致命錯誤。
我們是以拷貝構造函數為例說明的,賦值運算符重載函數也是同樣的道理。
Ⅸ.賦值運算符重載函數只能是類的非靜態的成員函數
C++規定,賦值運算符重載函數只能是類的非靜態的成員函數,不能是靜態成員函數,也不能是友元函數。關於原因,有人說,賦值運算符重載函數往往要返回*this,而無論是靜態成員函數還是友元函數都沒有this指針。這乍看起來很有道理,但仔細一想,我們完全可以寫出這樣的代碼
static friend MyStr& operator=(const MyStr str1,const MyStr str2) { …… return str1; }
可見,這種說法並不能揭露C++這么規定的原因。
其實,之所以不是靜態成員函數,是因為靜態成員函數只能操作類的靜態成員,不能操作非靜態成員。如果我們將賦值運算符重載函數定義為靜態成員函數,那么,該函數將無法操作類的非靜態成員,這顯然是不可行的。
在前面的講述中我們說過,當程序沒有顯式地提供一個以本類或本類的引用為參數的賦值運算符重載函數時,編譯器會自動提供一個。現在,假設C++允許將賦值運算符重載函數定義為友元函數並且我們也確實這么做了,而且以類的引用為參數。與此同時,我們在類內卻沒有顯式提供一個以本類或本類的引用為參數的賦值運算符重載函數。由於友元函數並不屬於這個類,所以,此時編譯器一看,類內並沒有一個以本類或本類的引用為參數的賦值運算符重載函數,所以會自動提供一個。此時,我們再執行類似於str2=str1這樣的代碼,那么,編譯器是該執行它提供的默認版本呢,還是執行我們定義的友元函數版本呢?
為了避免這樣的二義性,C++強制規定,賦值運算符重載函數只能定義為類的成員函數,這樣,編譯器就能夠判定是否要提供默認版本了,也不會再出現二義性。
Ⅹ. 賦值運算符重載函數不能被繼承
見下面的例3
#include<iostream> #include<string> using namespace std; class A { public: int X; A() {} A& operator =(const int x) { X = x; return *this; } }; class B :public A { public: B(void) :A() {} }; int main() { A a; B b; a = 45; //b = 67; (A)b = 67; return 0; }
注釋掉的一句無法編譯通過。報錯提示:沒有與這些操作數匹配的”=”運算符。對於b = 67;一句,首先,沒有可供調用的構造函數(前面說過,在沒有匹配的賦值運算符重載函數時,類似於該句的代碼可以調用匹配的構造函數),此時,代碼不能編譯通過,說明父類的operator =函數並沒有被子類繼承。
為什么賦值運算符重載函數不能被繼承呢?
因為相較於基類,派生類往往要添加一些自己的數據成員和成員函數,如果允許派生類繼承基類的賦值運算符重載函數,那么,在派生類不提供自己的賦值運算符重載函數時,就只能調用基類的,但基類版本只能處理基類的數據成員,在這種情況下,派生類自己的數據成員怎么辦?
所以,C++規定,賦值運算符重載函數不能被繼承。
上面代碼中, (A)b = 67; 一句可以編譯通過,原因是我們將B類對象b強制轉換成了A類對象。
Ⅺ.賦值運算符重載函數要避免自賦值
對於賦值運算符重載函數,我們要避免自賦值情況(即自己給自己賦值)的發生,一般地,我們通過比較賦值者與被賦值者的地址是否相同來判斷兩者是否是同一對象(正如例1中的if (this != &str)一句)。
為什么要避免自賦值呢?
①為了效率。顯然,自己給自己賦值完全是毫無意義的無用功,特別地,對於基類數據成員間的賦值,還會調用基類的賦值運算符重載函數,開銷是很大的。如果我們一旦判定是自賦值,就立即return *this,會避免對其它函數的調用。
②如果類的數據成員中含有指針,自賦值有時會導致災難性的后果。對於指針間的賦值(注意這里指的是指針所指內容間的賦值,這里假設用_p給p賦值),先要將p所指向的空間delete掉(為什么要這么做呢?因為指針p所指的空間通常是new來的,如果在為p重新分配空間前沒有將p原來的空間delete掉,會造成內存泄露),然后再為p重新分配空間,將_p所指的內容拷貝到p所指的空間。如果是自賦值,那么p和_p是同一指針,在賦值操作前對p的delete操作,將導致p所指的數據同時被銷毀。那么重新賦值時,拿什么來賦?
所以,對於賦值運算符重載函數,一定要先檢查是否是自賦值,如果是,直接return *this。