1. 程式人生 > >C++本質:類的賦值運算符=的重載,以及深拷貝和淺拷貝

C++本質:類的賦值運算符=的重載,以及深拷貝和淺拷貝

fin 過程 種類 解決 對象的引用 執行 面向 鏈式 alt

關鍵詞:構造函數,淺拷貝,深拷貝,堆棧(stack),堆heap,賦值運算符
摘要:
在面向對象程序設計中,對象間的相互拷貝和賦值是經常進行的操作。
如果對象在申明的同時馬上進行的初始化操作,則稱之為拷貝運算。例如:
class1 A("af"); class1 B=A;
此時其實際調用的是B(A)這樣的淺拷貝操作。
如果對象在申明之後,在進行的賦值運算,我們稱之為賦值運算。例如:
class1 A("af"); class1 B;
B=A;
此時實際調用的類的缺省賦值函數B.operator=(A);
不管是淺拷貝還是賦值運算,其都有缺省的定義。也就是說,即使我們不overload這兩種operation,仍然可以運行。
那麽,我們到底需不需要overload這兩種operation 呢?
答案就是:一般,我們我們需要手動編寫析構函數的類,都需要overload 拷貝函數和賦值運算符。


下面介紹類的賦值運算符

1.C++中對象的內存分配方式
在C++中,對象的實例在編譯的時候,就需要為其分配內存大小,因此,系統都是在stack上為其分配內存的。這一點和C#完全不同!千萬記住:在C#中,所有類都是reference type,要創建類的實體,必須通過new在heap上為其分配空間,同時返回在stack上指向其地址的reference.
因此,在C++中,只要申明該實例,在程序編譯後,就要為其分配相應的內存空間,至於實體內的各個域的值,就由其構造函數決定了。
例如:
技術分享圖片class A
技術分享圖片{
技術分享圖片public:
技術分享圖片 A()
技術分享圖片 {
技術分享圖片 }
技術分享圖片 A(int id,char *t_name)
技術分享圖片
{
技術分享圖片 _id=id;
技術分享圖片 name=new char[strlen(t_name)+1];
技術分享圖片 strcpy(name,t_name);
技術分享圖片 }
private:
char *username;
int _id;
技術分享圖片}
技術分享圖片
技術分享圖片int main()
技術分享圖片{
技術分享圖片A a(1,"herengang");
技術分享圖片A b;
技術分享圖片}

在程序編譯之後,a和b在stack上都被分配相應的內存大小。只不過對象a的域都被初始化,而b則都為隨機值。
其內存分配如下:
技術分享圖片

2. 缺省情況下的賦值運算符
如果我們執行以下:
b=a;
則其執行的是缺省定義的缺省的賦值運算。所謂缺省的賦值運算,是指對象中的所有位於stack中的域,進行相應的復制。但是,如果對象有位於heap上的域的話,其不會為拷貝對象分配heap上的空間,而只是指向相同的heap上的同一個地址。
執行b=a這樣的缺省的賦值運算後,其內存分配如下:
技術分享圖片

因此,對於缺省的賦值運算,如果對象域內沒有heap上的空間,其不會產生任何問題。但是,如果對象域內需要申請heap上的空間,那麽在析構對象的時候,就會連續兩次釋放heap上的同一塊內存區域,從而導致異常。
技術分享圖片 ~A()
技術分享圖片 {
技術分享圖片 delete name;
技術分享圖片 }

3.解決辦法--重載(overload)賦值運算符
因此,對於對象的域在heap上分配內存的情況,我們必須重載賦值運算符。當對象間進行拷貝的時候,我們必須讓不同對象的成員域指向其不同的heap地址--如果成員域屬於heap的話。
因此,重載賦值運算符後的代碼如下: 技術分享圖片class A
技術分享圖片{
技術分享圖片public:
技術分享圖片
技術分享圖片 A()
技術分享圖片 {
技術分享圖片 }
技術分享圖片 A(int id,char *t_name)
技術分享圖片 {
技術分享圖片 _id=id;
技術分享圖片 name=new char[strlen(t_name)+1];
技術分享圖片 strcpy(name,t_name);
技術分享圖片 }
技術分享圖片
技術分享圖片 A& operator =(A& a)
//註意:此處一定要返回對象的引用,否則返回後其值立即消失!
技術分享圖片 {
if(name!=NULL)
delete name;
技術分享圖片 this->_id=a._id;
技術分享圖片 int len=strlen(a.name);
技術分享圖片 name=new char[len+1];
技術分享圖片 strcpy(name,a.name);
技術分享圖片 return *this;
技術分享圖片 }
技術分享圖片
技術分享圖片 ~A()
技術分享圖片 {
技術分享圖片 cout<<"~destructor"<<endl;
技術分享圖片 delete name;
技術分享圖片 }
技術分享圖片
技術分享圖片 int _id;
技術分享圖片 char *name;
技術分享圖片};

int main()
{
A a(1,"herengang");
A b;
b=a;

}

其內存分配如下:
技術分享圖片
這樣,在對象a,b退出相應的作用域,其調用相應的析構函數,然後釋放分別屬於不同heap空間的內存,程序正常結束。


references:
類的深拷貝函數的重載
public class A
{
public:
...
A(A &a);//重載拷貝函數
A& operator=(A &b);//重載賦值函數
//或者 我們也可以這樣重載賦值運算符 void operator=(A &a);即不返回任何值。如果這樣的話,他將不支持客戶代買中的鏈式賦值 ,例如a=b=c will be prohibited!
private:
int _id;
char *username;
}

A::A(A &a)
{
_id=a._id;
username=new char[strlen(a.username)+1];
if(username!=NULL)
strcpy(username,a.usernam);
}

A& A::operaton=(A &a)
{
if(this==&a)// 問:什麽需要判斷這個條件?(不是必須,只是優化而已)。答案:提示:考慮a=a這樣的操作。
return *this;
if(username!=NULL)
delete username;
_id=a._id;
username=new char[strlen(a.username)+1];
if(username!=NULL)
strcpy(username,a.usernam);
return *this;
}
//另外一種寫法:
void A::operation=(A &a)
{
if(username!=NULL)
delete username;
_id=a._id;
username=new char[strlen(a.username)+1];
if(username!=NULL)
strcpy(username,a.usernam);
}

其實,從上可以看出,賦值運算符和拷貝函數很相似。只不過賦值函數最好有返回值(進行鏈式賦值),返回也最好是對象的引用(為什麽不是對象本身呢?note2有講解), 而拷貝函數不需要返回任何。同時,賦值函數首先要釋放掉對象自身的堆空間(如果需要的話),然後進行其他的operation.而拷貝函數不需要如此,因為對象此時還沒有分配堆空間。

note1:
不要按值向函數傳遞對象。如果對象有內部指針指向動態分配的堆內存,絲毫不要考慮把對象按值傳遞給函數,要按引用傳遞。並記住:若函數不能改變參數對象的狀態和目標對象的狀態,則要使用const修飾符

note2:問題:
對於類的成員需要動態申請堆空間的類的對象,大家都知道,我們都最好要overload其賦值函數和拷貝函數。拷貝構造函數是沒有任何返回類型的,這點毋庸置疑。 而賦值函數可以返回多種類型,例如以上講的void,類本身class1,以及類的引用 class &? 問,這幾種賦值函數的返回各有什麽異同?
答:1 如果賦值函數返回的是void ,我們知道,其唯一一點需要註意的是,其不支持鏈式賦值運算,即a=b=c這樣是不允許的!
2 對於返回的是類對象本身,還是類對象的引用,其有著本質的區別!
第一:如果其返回的是類對象本身。
A operator =(A& a)
{
if(name!=NULL)
delete name;
this->_id=a._id;
int len=strlen(a.name);
name=new char[len+1];
strcpy(name,a.name);
return *this;
}
其過程是這樣的:
class1 A("herengnag");
class1 B;
B=A;
看似簡單的賦值操作,其所有的過程如下:
1 釋放對象原來的堆資源
2 重新申請堆空間
3 拷貝源的值到對象的堆空間的值
4 創建臨時對象(調用臨時對象拷貝構造函數),將臨時對象返回
5. 臨時對象結束,調用臨時對象析構函數,釋放臨時對象堆內存
my god,還真復雜!!
但是,在這些步驟裏面,如果第4步,我們沒有overload 拷貝函數,也就是沒有進行深拷貝。那麽在進行第5步釋放臨時對象的heap 空間時,將釋放掉的是和目標對象同一塊的heap空間。這樣當目標對象B作用域結束調用析構函數時,就會產生錯誤!!
因此,如果賦值運算符返回的是類對象本身,那麽一定要overload 類的拷貝函數(進行深拷貝)!
第二:如果賦值運算符返回的是對象的引用,
A& operator =(A& a)
{
if(name!=NULL)
delete name;
this->_id=a._id;
int len=strlen(a.name);
name=new char[len+1];
strcpy(name,a.name);
return *this;
}
那麽其過程如下:
1 釋放掉原來對象所占有的堆空間
1.申請一塊新的堆內存
2 將源對象的堆內存的值copy給新的堆內存
3 返回源對象的引用
4 結束。
因此,如果賦值運算符返回的是對象引用,那麽其不會調用類的拷貝構造函數,這是問題的關鍵所在!!

完整代碼如下:

技術分享圖片 技術分享圖片// virtual.cpp : Defines the entry point for the console application.
技術分享圖片//
技術分享圖片
技術分享圖片#include "stdafx.h"
技術分享圖片#include "string.h"
技術分享圖片#include "stdlib.h"
技術分享圖片#include "assert.h"
技術分享圖片
技術分享圖片class complex
技術分享圖片{
技術分享圖片public:
技術分享圖片 int real;
技術分享圖片 int virt;
技術分享圖片public:
技術分享圖片 complex(){real=virt=0;}
技術分享圖片 complex(int treal,int tvirt){real=treal;virt=tvirt;}
技術分享圖片 complex operator+(const complex &x)
技術分享圖片 {
技術分享圖片 real+=x.real;
技術分享圖片 virt+=x.virt;
技術分享圖片 return *this;
技術分享圖片 }
技術分享圖片 complex operator=(const complex &x)
技術分享圖片 {
技術分享圖片 return complex(x.real,x.virt);
技術分享圖片 }
技術分享圖片};
技術分享圖片
技術分享圖片
技術分享圖片class A
技術分享圖片{
技術分享圖片public:
技術分享圖片 A(){m_username=NULL;printf("null constructor");}
技術分享圖片 A(char *username)
技術分享圖片 {
技術分享圖片 int len;
技術分享圖片 len=strlen(username);
技術分享圖片 m_username=new char[len+1];//(char*)malloc(sizeof(len+1));
技術分享圖片 strcpy(m_username,username);
技術分享圖片 printf("\nUsername is %s\n",m_username);
技術分享圖片 }
技術分享圖片
技術分享圖片 A(A &a);
技術分享圖片 A operator=(A &b);
技術分享圖片 int test(const int &x)
技術分享圖片 {
技術分享圖片 return x;
技術分享圖片 }
技術分享圖片
技術分享圖片 virtual ~A()
技術分享圖片 {
技術分享圖片 // if(m_username)
技術分享圖片 {
技術分享圖片 delete m_username;
技術分享圖片 printf("\nA is destructed\n");
技術分享圖片 }
技術分享圖片 }
技術分享圖片
技術分享圖片
技術分享圖片
技術分享圖片protected:
技術分享圖片 char *m_username;
技術分享圖片
技術分享圖片};
技術分享圖片
技術分享圖片
技術分享圖片
技術分享圖片A::A(A &a)
技術分享圖片{
技術分享圖片
技術分享圖片 int len=strlen(a.m_username);
技術分享圖片 this->m_username=new char[len+2];
技術分享圖片 strcpy(m_username,a.m_username);
技術分享圖片 strcat(m_username,"f");
技術分享圖片 printf("\ndeep copy function");
技術分享圖片}
技術分享圖片
技術分享圖片
技術分享圖片A A::operator=(A &b)
技術分享圖片{
技術分享圖片 if(m_username)
技術分享圖片 delete m_username;
技術分享圖片
技術分享圖片 int len=strlen(b.m_username);
技術分享圖片 this->m_username=new char[len+1];
技術分享圖片 strcpy(m_username,b.m_username);
技術分享圖片// printf("copied successfully!");
技術分享圖片 return *this;
技術分享圖片}
技術分享圖片
技術分享圖片
技術分享圖片
技術分享圖片class B:public A
技術分享圖片{
技術分享圖片public:
技術分享圖片 B(char *username,char *password):A(username)
技術分享圖片 {
技術分享圖片 int len=strlen(password)+1;
技術分享圖片 m_password=new char[len];//(char *)malloc(sizeof(len));
技術分享圖片 strcpy(m_password,password);
技術分享圖片 printf("username:%s,password:%s\n",m_username,m_password);
技術分享圖片 }
技術分享圖片 ~B()
技術分享圖片 {
技術分享圖片 delete m_password;
技術分享圖片 printf("B is destructed\n");
技術分享圖片 }
技術分享圖片protected:
技術分享圖片 char *m_password;
技術分享圖片};
技術分享圖片
技術分享圖片int main(int argc, char* argv[])
技術分享圖片{
技術分享圖片// B b("herengang","982135");
技術分享圖片// A *a=&b;
技術分享圖片// delete a;
技術分享圖片 A a("haha");
技術分享圖片 A b;
技術分享圖片
技術分享圖片 printf("\nbegin to invoke copy function");
技術分享圖片 b=a;
技術分享圖片
技術分享圖片// printf("%d",b.test(2));
技術分享圖片 //complex x(1,3),y(1,4);
技術分享圖片 //x=(x+y);
技術分享圖片 //printf("%d,%d",x.real,x.virt);
技術分享圖片 return 0;
技術分享圖片
技術分享圖片
技術分享圖片}
技術分享圖片
技術分享圖片
技術分享圖片

1 重載賦值運算符返回結果為類對象的運行結果

技術分享圖片
明顯, 運算符最後調用了拷貝構造函數

2 重載賦值運算符返回結果為類對象引用的運行結果
技術分享圖片
很明顯,沒有調用拷貝構造函數

C++本質:類的賦值運算符=的重載,以及深拷貝和淺拷貝