c++拷貝建構函式、賦值運算子=過載、深拷貝與淺拷貝
摘要:
在面向物件程式設計中,物件間的相互拷貝和賦值是經常進行的操作。
如果物件在宣告的同時馬上進行的初始化操作,則稱之為拷貝運算。例如:
class1 A("af"); class1 B(A);//classB=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(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.而拷貝函式不需要如此,因為物件此時還沒有分配堆空間。
不要按值向函式傳遞物件。如果物件有內部指標指向動態分配的堆記憶體,絲毫不要考慮把物件按值傳遞給函式,要按引用傳遞。並記住:若函式不能改變引數物件的狀態和目標物件的狀態,則要使用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 過載賦值運算子返回結果為類物件引用的執行結果
拷貝建構函式的幾個細節
1. 拷貝建構函式裡能呼叫private成員變數嗎?
解答:這個問題是在網上見的,當時一下子有點暈。其時從名子我們就知道拷貝建構函式其時就是一個特殊的建構函式,操作的還是自己類的成員變數,所以不受private的限制。
2. 以下函式哪個是拷貝建構函式,為什麼?
[c-sharp] view plaincopyprint?- X::X(const X&);
- X::X(X);
- X::X(X&, int a=1);
- X::X(X&,