一文說盡C++賦值運算符重載函數(operator=)

http://www.cnblogs.com/zpcdbky/p/5027481.html

在前面:

????? 關于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是否為空,若否,dalete 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。

結束語:

?? ? ?至此,本文的所有內容都介紹完了。由于在下才疏學淺,錯誤紕漏之處在所難免,如果您在閱讀的過程中發現了在下的錯誤和不足,請您務必指出。您的批評指正就是在下前進的不竭動力!?


本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/383712.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/383712.shtml
英文地址,請注明出處:http://en.pswp.cn/news/383712.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Python a和a[:]的區別

簡單來講a[:]是深復制&#xff0c;a是淺復制&#xff0c;相當于賦值a的話是賦值了指針&#xff0c;賦值a[:]相當于復制了a對應的那段空間 例如&#xff1a; a [1,1,1,1,1,1]for x in a:if x1:a.remove(x)print(a)運行結果&#xff1a; remove操作是移除序列中第一個x元素。…

約瑟夫環(c語言程序完整版)

https://blog.csdn.net/m_hahahaha1994/article/details/51742453約瑟夫環&#xff08;約瑟夫問題&#xff09;是一個數學的應用問題&#xff1a;已知n個人&#xff08;以編號1&#xff0c;2&#xff0c;3…n分別表示&#xff09;圍坐在一張圓桌周圍。從編號為k的人開始報數&am…

Linux系統【二】exec族函數及應用

文件描述符 文件描述符表是一個指針數組&#xff0c;文件描述符是一個整數。 文件描述符表對應的指針是一個結構體&#xff0c;名字為file_struct&#xff0c;里面保存的是已經打開文件的信息 需要注意的是父子進程之間讀時共享&#xff0c;寫時復制的原則是針對物理地址而言…

白話C++系列(27) -- RTTI:運行時類型識別

http://www.cnblogs.com/kkdd-2013/p/5601783.htmlRTTI—運行時類型識別 RTTI&#xff1a;Run-Time Type Identification。 那么RTTI如何來體現呢&#xff1f;這就要涉及到typeid和dynamic_cast這兩個知識點了。為了更好的去理解&#xff0c;那么我們就通過一個例子來說明。這個…

使用頭文件的原因和規范

原因 通過頭文件來調用庫功能。在很多場合&#xff0c;源代碼不便&#xff08;或不準&#xff09;向用戶公布&#xff0c;只 要向用戶提供頭文件和二進制的庫即可。用戶只需要按照頭文件中的接口聲明來調用庫 功能&#xff0c;而不必關心接口怎么實現的。編譯器會從庫中提取相應…

轉圈踢人問題

https://www.cnblogs.com/lanxuezaipiao/p/3339603.html 有N個人圍一圈依次報數&#xff0c;數到3的倍數的人出列&#xff0c;問當只剩一個人時他原來的位子在哪里&#xff1f; 解答&#xff1a;經典的轉圈踢人問題&#xff0c;好吧專業一點&#xff0c;約瑟夫環問題&#xff0…

Linux系統【三】回收子進程

孤兒進程 父進程先于子進程結束&#xff0c;則子進程成為孤兒進程&#xff0c;子進程的父進程成為init進程&#xff0c;則稱init進程領養孤兒進程。現在好像是用戶進程中的system進程。 僵尸進程 進程終止&#xff0c;父進程不進行回收&#xff0c;自己成殘留資源(PCB)存放在…

string類的基本實現

https://blog.csdn.net/qq_29503203/article/details/52265829在面試中面試官常常會讓你寫出string類的基本操作&#xff0c;比如&#xff1a;構造函數&#xff0c;析構函數&#xff0c;拷貝構造等等.下面是除此之外的一些操作&#xff0c;希望可以幫助你更好的理解string以便以…

Python3常用數據結構

Python3中有三種組合數據類型&#xff0c;分別為&#xff1a; 序列類型&#xff1a;字符串&#xff08;str&#xff09;、元組&#xff08;tuple&#xff09;、列表&#xff08;list&#xff09;集合類型&#xff1a;集合&#xff08;set&#xff09;映射類型&#xff1a;字典…

Linux C++ 回射服務器

http://blog.csdn.net/qq_25425023/article/details/53914820回射服務器就是服務端將客戶端的數據發送回去。我實現的回射服務器返回增加了時間。服務端代碼&#xff0c;可以很容易看懂&#xff1a;[cpp] view plaincopy#include <sys/socket.h> #include <stdio.h&g…

TCP第四次揮手為什么要等待2MSL

當客戶端進入TIME-WAIT狀態的時候(也就是第四次揮手的時候)&#xff0c;必須經過時間計數器設置的時間2MSL(最長報文段壽命)后&#xff0c;才能進入關閉狀態&#xff0c;這時為什么呢&#xff1f;&#xff1f;&#xff1f; 這最主要是因為兩個理由&#xff1a; 1、為了保證客戶…

計算機網絡【一】概述+OSI參考模型

網絡概述 局域網:覆蓋范圍小(100m以內)&#xff0c;自己花錢買設備&#xff0c;帶寬固定(10M,100M,1000M)&#xff0c;自己維護&#xff08;接入層交換機直接連接電腦、匯聚層交換機直接連接接入層交換機&#xff09; 廣域網:距離遠&#xff0c;花錢買服務&#xff0c;租帶寬&…

單鏈表逆序的多種方式

https://www.cnblogs.com/eniac12/p/4860642.htmltemplate<class T> void List<T>::Inverse() {if(first NULL) return;LinkNode<T> *p, *prev, *latter; p first->link;   // 當前結點prev NULL;   // 前一結點l…

Linux系統【四】進程間通信-管道

進程間通信&#xff08;IPC Interprocess Communication&#xff09; 進程和進程之間的通信只能通過內核&#xff0c;在內核中提供一塊緩沖區進行通信。內核提供的這種機制叫做IPC 在進程間完成數據傳輸需要借助操作系統提供的特殊方法&#xff0c;如&#xff1a;文件&#xf…

單鏈表各種操作詳解

#include "stdio.h" #include "stdlib.h"#define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0#define MAXSIZE 20 /* 存儲空間初始分配量 */typedef int Status;/* Status是函數的類型,其值是函數結果狀態代碼&#xff0c;如OK等 */ typedef int…

Linux系統【五】進程間通信-共享內存mmap

mmap函數 #include <sys/mman.h> void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);參數&#xff1a; void *addr建立映射區的首地址&#xff0c;由Linux內核指定&#xff0c;所以我們直接傳遞NULL。也就是說雖然這是一個參宿但是并不…

socket編程 -- epoll模型服務端/客戶端通信的實現

https://blog.csdn.net/y396397735/article/details/50680359 本例實現如下功能&#xff1a; 支持多客戶端與一個服務端進行通信&#xff0c;客戶端給服務端發送字符串數據&#xff0c;服務端將字符串中小寫轉為大寫后發送回客戶端&#xff0c;客戶端打印輸出經轉換后的字符串。…

Python3 面向對象程序設計

類的定義 Python使用class關鍵字來定義類 class Car:def infor(self):print("This is a car") car Car() car.infor()內置方法isinstance()來測試一個對象是否為某個類的實例 self參數 類的 所有實例方法都有一個默認的self參數&#xff0c;并且必須是方法的第一…

計算機網絡【二】物理層基礎知識

計算機網絡的性能 速率&#xff1a;連接在計算機網絡上的主機在數字信道上傳送數據位數的速率&#xff0c;也成為data rate 或bit rate&#xff0c;單位是b/s,kb/s,Mb/s,Gb/s。 我們平時所講的寬帶的速度是以字為單位的&#xff0c;但是實際中應用一般顯示的是字節 &#xff0…

Linux網絡編程——tcp并發服務器(多進程)

https://blog.csdn.net/lianghe_work/article/details/46503895一、tcp并發服務器概述一個好的服務器,一般都是并發服務器&#xff08;同一時刻可以響應多個客戶端的請求&#xff09;。并發服務器設計技術一般有&#xff1a;多進程服務器、多線程服務器、I/O復用服務器等。二、…