前段時(shí)間瘋狂的在調(diào)試著數(shù)據(jù)結(jié)構(gòu)中的各個(gè)程序。但是有兩個(gè)東西我一直是當(dāng)注釋放在那的,這就是鏈接結(jié)構(gòu)中的copy構(gòu)造函數(shù)和賦值重載函數(shù);這也是我學(xué)習(xí)中的一個(gè)毛病——對(duì)于不懂的,只想著能拖就拖。不過(guò),由于作業(yè)的要求,只能硬著頭皮把書(shū)啃了兩個(gè)小時(shí)。下面就是我對(duì)這兩個(gè)東西的理解。
在數(shù)據(jù)結(jié)構(gòu)中,對(duì)于程序的實(shí)現(xiàn)一般有兩種方式:順序?qū)崿F(xiàn)和鏈接實(shí)現(xiàn),即一個(gè)是由數(shù)組作為基本的數(shù)據(jù)成員,而另一個(gè)則是由節(jié)點(diǎn)Node作為基本的數(shù)據(jù)成員。其中,鏈接實(shí)現(xiàn)時(shí),如果用戶(hù)的不經(jīng)意,就會(huì)造成很多不必要的garbage,而正是出于這種考慮,在C++中有了三個(gè)保護(hù)函數(shù):析構(gòu)函數(shù)、copy構(gòu)造函數(shù)和賦值重載函數(shù)。
析構(gòu)函數(shù)類(lèi)似于構(gòu)造函數(shù)的“反函數(shù)”,目的是:“在所聲明的對(duì)象在銷(xiāo)毀時(shí),釋放其所占用的內(nèi)存空間?!比缦旅媸莝tack的析構(gòu)函數(shù):
而對(duì)于copy構(gòu)造函數(shù)和賦值重載函數(shù),都是為了避免用戶(hù)在使用時(shí),輕易對(duì)兩個(gè)對(duì)象之間賦值所設(shè)立的保護(hù)函數(shù)。他們的區(qū)別可以簡(jiǎn)單通過(guò)下面這個(gè)例子看出來(lái):
" 在學(xué)習(xí)這一章內(nèi)容前我們已經(jīng)學(xué)習(xí)過(guò)了類(lèi)的構(gòu)造函數(shù)和析構(gòu)函數(shù)的相關(guān)知識(shí),對(duì)于普通類(lèi)型的對(duì)象來(lái)說(shuō),他們之間的復(fù)制是很簡(jiǎn)單的,例如:
int a = 10;
int b =a;
自己定義的類(lèi)的對(duì)象同樣是對(duì)象,誰(shuí)也不能阻止我們用以下的方式進(jìn)行復(fù)制,例如:
#include <iostream>
using namespace std;
class Test
{
public:
Test(int temp)
{
p1=temp;
}
protected:
int p1;
};
void main()
{
Test a(99);
Test b=a;
}
普通對(duì)象和類(lèi)對(duì)象同為對(duì)象,他們之間的特性有相似之處也有不同之處,類(lèi)對(duì)象內(nèi)部存在成員變量,而普通對(duì)象是沒(méi)有的,當(dāng)同樣的復(fù)制方法發(fā)生在不同的對(duì)象上的時(shí)候,那么系統(tǒng)對(duì)他們進(jìn)行的操作也是不一樣的,就類(lèi)對(duì)象而言,相同類(lèi)型的類(lèi)對(duì)象是通過(guò)拷貝構(gòu)造函數(shù)來(lái)完成整個(gè)復(fù)制過(guò)程的,在上面的代碼中,我們并沒(méi)有看到拷貝構(gòu)造函數(shù),同樣完成了復(fù)制工作,這又是為什么呢?因?yàn)楫?dāng)一個(gè)類(lèi)沒(méi)有自定義的拷貝構(gòu)造函數(shù)的時(shí)候系統(tǒng)會(huì)自動(dòng)提供一個(gè)默認(rèn)的拷貝構(gòu)造函數(shù),來(lái)完成復(fù)制工作。
下面,我們?yōu)榱苏f(shuō)明情況,就普通情況而言(以上面的代碼為例),我們來(lái)自己定義一個(gè)與系統(tǒng)默認(rèn)拷貝構(gòu)造函數(shù)一樣的拷貝構(gòu)造函數(shù),看看它的內(nèi)部是如何工作的!
代碼如下:
#include <iostream>
using namespace std;
class Test
{
public:
Test(int temp)
{
p1=temp;
}
Test(Test &c_t)//這里就是自定義的拷貝構(gòu)造函數(shù)
{
cout<<"進(jìn)入copy構(gòu)造函數(shù)"<<endl;
p1=c_t.p1;//這句如果去掉就不能完成復(fù)制工作了,此句復(fù)制過(guò)程的核心語(yǔ)句
}
public:
int p1;
};
void main()
{
Test a(99);
Test b=a;
cout<<b.p1;
cin.get();
}
上面代碼中的Test(Test &c_t)就是我們自定義的拷貝構(gòu)造函數(shù),拷貝構(gòu)造函數(shù)的名稱(chēng)必須與類(lèi)名稱(chēng)一致,函數(shù)的形式參數(shù)是本類(lèi)型的一個(gè)引用變量,且必須是引用。
當(dāng)用一個(gè)已經(jīng)初始化過(guò)了的自定義類(lèi)類(lèi)型對(duì)象去初始化另一個(gè)新構(gòu)造的對(duì)象的時(shí)候,拷貝構(gòu)造函數(shù)就會(huì)被自動(dòng)調(diào)用,如果你沒(méi)有自定義拷貝構(gòu)造函數(shù)的時(shí)候系統(tǒng)將會(huì)提供給一個(gè)默認(rèn)的拷貝構(gòu)造函數(shù)來(lái)完成這個(gè)過(guò)程,上面代碼的復(fù)制核心語(yǔ)句就是通過(guò)Test(Test &c_t)拷貝構(gòu)造函數(shù)內(nèi)的p1=c_t.p1;語(yǔ)句完成的。如果取掉這句代碼,那么b對(duì)象的p1屬性將得到一個(gè)未知的隨機(jī)值;
下面我們來(lái)討論一下關(guān)于淺拷貝和深拷貝的問(wèn)題。
就上面的代碼情況而言,很多人會(huì)問(wèn)到,既然系統(tǒng)會(huì)自動(dòng)提供一個(gè)默認(rèn)的拷貝構(gòu)造函數(shù)來(lái)處理復(fù)制,那么我們沒(méi)有意義要去自定義拷貝構(gòu)造函數(shù)呀,對(duì),就普通情況而言這的確是沒(méi)有必要的,但在某寫(xiě)狀況下,類(lèi)體內(nèi)的成員是需要開(kāi)辟動(dòng)態(tài)開(kāi)辟堆內(nèi)存的,如果我們不自定義拷貝構(gòu)造函數(shù)而讓系統(tǒng)自己處理,那么就會(huì)導(dǎo)致堆內(nèi)存的所屬權(quán)產(chǎn)生混亂,試想一下,已經(jīng)開(kāi)辟的一端堆地址原來(lái)是屬于對(duì)象a的,由于復(fù)制過(guò)程發(fā)生,b對(duì)象取得是a已經(jīng)開(kāi)辟的堆地址,一旦程序產(chǎn)生析構(gòu),釋放堆的時(shí)候,計(jì)算機(jī)是不可能清楚這段地址是真正屬于誰(shuí)的,當(dāng)連續(xù)發(fā)生兩次析構(gòu)的時(shí)候就出現(xiàn)了運(yùn)行錯(cuò)誤。
為了更詳細(xì)的說(shuō)明問(wèn)題,請(qǐng)看如下的代碼。
#include <iostream>
using namespace std;
class Internet
{
public:
Internet(char *name,char *address)
{
cout<<"載入構(gòu)造函數(shù)"<<endl;
strcpy(Internet::name,name);
strcpy(Internet::address,address);
cname=new char[strlen(name)+1];
if(cname!=NULL)
{
strcpy(Internet::cname,name);
}
}
Internet(Internet &temp)
{
cout<<"載入COPY構(gòu)造函數(shù)"<<endl;
strcpy(Internet::name,temp.name);
strcpy(Internet::address,temp.address);
cname=new char[strlen(name)+1];//這里注意,深拷貝的體現(xiàn)!
if(cname!=NULL)
{
strcpy(Internet::cname,name);
}
}
~Internet()
{
cout<<"載入析構(gòu)函數(shù)!";
delete[] cname;
cin.get();
}
void show();
protected:
char name[20];
char address[30];
char *cname;
};
void Internet::show()
{
cout<<name<<":"<<address<<cname<<endl;
}
void test(Internet ts)
{
cout<<"載入test函數(shù)"<<endl;
}
void main()
{
Internet a("中國(guó)軟件開(kāi)發(fā)實(shí)驗(yàn)室","www.cndev-lab.com");
Internet b = a;
b.show();
test(b);
}
上面代碼就演示了深拷貝的問(wèn)題,對(duì)對(duì)象b的cname屬性采取了新開(kāi)辟內(nèi)存的方式避免了內(nèi)存歸屬不清所導(dǎo)致析構(gòu)釋放空間時(shí)候的錯(cuò)誤,最后我必須提一下,對(duì)于上面的程序我的解釋并不多,就是希望讀者本身運(yùn)行程序觀察變化,進(jìn)而深刻理解。
拷貝和淺拷貝的定義可以簡(jiǎn)單理解成:如果一個(gè)類(lèi)擁有資源(堆,或者是其它系統(tǒng)資源),當(dāng)這個(gè)類(lèi)的對(duì)象發(fā)生復(fù)制過(guò)程的時(shí)候,這個(gè)過(guò)程就可以叫做深拷貝,反之對(duì)象存在資源但復(fù)制過(guò)程并未復(fù)制資源的情況視為淺拷貝。
淺拷貝資源后在釋放資源的時(shí)候會(huì)產(chǎn)生資源歸屬不清的情況導(dǎo)致程序運(yùn)行出錯(cuò),這點(diǎn)尤其需要注意!
另一篇:
關(guān)于拷貝構(gòu)造函數(shù)和賦值運(yùn)算符
作者:馮明德
重點(diǎn):包含動(dòng)態(tài)分配成員的類(lèi) 應(yīng)提供拷貝構(gòu)造函數(shù),并重載"="賦值操作符。
以下討論中將用到的例子:
class CExample
{
public:
CExample(){pBuffer=NULL; nSize=0;}
~CExample(){delete pBuffer;}
void Init(int n){ pBuffer=new char[n]; nSize=n;}
private:
char *pBuffer; //類(lèi)的對(duì)象中包含指針,指向動(dòng)態(tài)分配的內(nèi)存資源
int nSize;
};
這個(gè)類(lèi)的主要特點(diǎn)是包含指向其他資源的指針。
pBuffer指向堆中分配的一段內(nèi)存空間。
一、拷貝構(gòu)造函數(shù)
int main(int argc, char* argv[])
{
CExample theObjone;
theObjone.Init40);
//現(xiàn)在需要另一個(gè)對(duì)象,需要將他初始化稱(chēng)對(duì)象一的狀態(tài)
CExample theObjtwo=theObjone;
...
}
語(yǔ)句"CExample theObjtwo=theObjone;"用theObjone初始化theObjtwo。
其完成方式是內(nèi)存拷貝,復(fù)制所有成員的值。
完成后,theObjtwo.pBuffer==theObjone.pBuffer。
即它們將指向同樣的地方,指針雖然復(fù)制了,但所指向的空間并沒(méi)有復(fù)制,而是由兩個(gè)對(duì)象共用了。這樣不符合要求,對(duì)象之間不獨(dú)立了,并為空間的刪除帶來(lái)隱患。
所以需要采用必要的手段來(lái)避免此類(lèi)情況。
回顧以下此語(yǔ)句的具體過(guò)程:首先建立對(duì)象theObjtwo,并調(diào)用其構(gòu)造函數(shù),然后成員被拷貝。
可以在構(gòu)造函數(shù)中添加操作來(lái)解決指針成員的問(wèn)題。
所以C++語(yǔ)法中除了提供缺省形式的構(gòu)造函數(shù)外,還規(guī)范了另一種特殊的構(gòu)造函數(shù):拷貝構(gòu)造函數(shù),上面的語(yǔ)句中,如果類(lèi)中定義了拷貝構(gòu)造函數(shù),這對(duì)象建立時(shí),調(diào)用的將是拷貝構(gòu)造函數(shù),在拷貝構(gòu)造函數(shù)中,可以根據(jù)傳入的變量,復(fù)制指針?biāo)赶虻馁Y源。
拷貝構(gòu)造函數(shù)的格式為:構(gòu)造函數(shù)名(對(duì)象的引用)
提供了拷貝構(gòu)造函數(shù)后的CExample類(lèi)定義為:
class CExample
{
public:
CExample(){pBuffer=NULL; nSize=0;}
~CExample(){delete pBuffer;}
CExample(const CExample&); //拷貝構(gòu)造函數(shù)
void Init(int n){ pBuffer=new char[n]; nSize=n;}
private:
char *pBuffer; //類(lèi)的對(duì)象中包含指針,指向動(dòng)態(tài)分配的內(nèi)存資源
int nSize;
};
CExample::CExample(const CExample& RightSides) //拷貝構(gòu)造函數(shù)的定義
{
nSize=RightSides.nSize; //復(fù)制常規(guī)成員
pBuffer=new char[nSize]; //復(fù)制指針指向的內(nèi)容
memcpy(pBuffer,RightSides.pBuffer,nSize*sizeof(char));
}
這樣,定義新對(duì)象,并用已有對(duì)象初始化新對(duì)象時(shí),CExample(const CExample& RightSides)將被調(diào)用,而已有對(duì)象用別名RightSides傳給構(gòu)造函數(shù),以用來(lái)作復(fù)制。
原則上,應(yīng)該為所有包含動(dòng)態(tài)分配成員的類(lèi)都提供拷貝構(gòu)造函數(shù)。
拷貝構(gòu)造函數(shù)的另一種調(diào)用。
當(dāng)對(duì)象直接作為參數(shù)傳給函數(shù)時(shí),函數(shù)將建立對(duì)象的臨時(shí)拷貝,這個(gè)拷貝過(guò)程也將調(diào)同拷貝構(gòu)造函數(shù)。
例如
BOOL testfunc(CExample obj);
testfunc(theObjone); //對(duì)象直接作為參數(shù)。
BOOL testfunc(CExample obj)
{
//針對(duì)obj的操作實(shí)際上是針對(duì)復(fù)制后的臨時(shí)拷貝進(jìn)行的
}
還有一種情況,也是與臨時(shí)對(duì)象有關(guān)的
當(dāng)函數(shù)中的局部對(duì)象被被返回給函數(shù)調(diào)者時(shí),也將建立此局部對(duì)象的一個(gè)臨時(shí)拷貝,拷貝構(gòu)造函數(shù)也將被調(diào)用
CTest func()
{
CTest theTest;
return theTest
}
二、賦值符的重載
下面的代碼與上例相似
int main(int argc, char* argv[])
{
CExample theObjone;
theObjone.Init(40);
CExample theObjthree;
theObjthree.Init(60);
//現(xiàn)在需要一個(gè)對(duì)象賦值操作,被賦值對(duì)象的原內(nèi)容被清除,并用右邊對(duì)象的內(nèi)容填充。
theObjthree=theObjone;
return 0;
}
也用到了"="號(hào),但與"一、"中的例子并不同,"一、"的例子中,"="在對(duì)象聲明語(yǔ)句中,表示初始化。更多時(shí)候,這種初始化也可用括號(hào)表示。
例如 CExample theObjone(theObjtwo);
而本例子中,"="表示賦值操作。將對(duì)象theObjone的內(nèi)容復(fù)制到對(duì)象theObjthree;,這其中涉及到對(duì)象theObjthree原有內(nèi)容的丟棄,新內(nèi)容的復(fù)制。
但"="的缺省操作只是將成員變量的值相應(yīng)復(fù)制。舊的值被自然丟棄。
由于對(duì)象內(nèi)包含指針,將造成不良后果:指針的值被丟棄了,但指針指向的內(nèi)容并未釋放。指針的值被復(fù)制了,但指針?biāo)竷?nèi)容并未復(fù)制。
因此,包含動(dòng)態(tài)分配成員的類(lèi)除提供拷貝構(gòu)造函數(shù)外,還應(yīng)該考慮重載"="賦值操作符號(hào)。
類(lèi)定義變?yōu)?
class CExample
{
...
CExample(const CExample&); //拷貝構(gòu)造函數(shù)
CExample& operator = (const CExample&); //賦值符重載
...
};
//賦值操作符重載
CExample & CExample::operator = (const CExample& RightSides)
{
nSize=RightSides.nSize; //復(fù)制常規(guī)成員
char *temp=new char[nSize]; //復(fù)制指針指向的內(nèi)容
memcpy(temp,RightSides.pBuffer,nSize*sizeof(char));
delete []pBuffer; //刪除原指針指向內(nèi)容 (將刪除操作放在后面,避免X=X特殊情況下,內(nèi)容的丟失)
pBuffer=temp; //建立新指向
return *this
}
三、拷貝構(gòu)造函數(shù)使用賦值運(yùn)算符重載的代碼。
CExample::CExample(const CExample& RightSides)
{
pBuffer=NULL;
*this=RightSides //調(diào)用重載后的"="
}
"