寫在前面:ios
關於C++的賦值運算符重載函數(operator=),網絡以及各類教材上都有不少介紹,但惋惜的是,內容大多雷同且不全面。面對這一局面,在下在整合各類資源及融入我的理解的基礎上,整理出一篇較爲全面/詳盡的文章,以饗讀者。網絡
正文:函數
Ⅰ.舉例this
例1spa
#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; }
結果:指針
Ⅱ.參數code
通常地,賦值運算符重載函數的參數是函數所在類的const類型的引用(如上面例1),加const是由於:對象
①咱們不但願在這個函數中對用來進行賦值的「原版」作任何修改。blog
②加上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。
結束語:
至此,本文的全部內容都介紹完了。因爲在下才疏學淺,錯誤紕漏之處在所不免,若是您在閱讀的過程當中發現了在下的錯誤和不足,請您務必指出。您的批評指正就是在下前進的不竭動力!