C++中的Primer拷貝、賦值與銷毀詳解
拷貝控制和資源管理
在本章中,我們將學(xué)到,類可以定義構(gòu)造函數(shù),用來控制在創(chuàng)建此類型對(duì)象時(shí)做什么。在本章中,我們還將學(xué)習(xí)類如何控制該類型對(duì)象拷貝、賦值、移動(dòng)或銷毀時(shí)做什么。類通過一些特殊的成員函數(shù)控制這些操作,包括:拷貝構(gòu)造函數(shù)、移動(dòng)構(gòu)造函數(shù)、拷貝賦值運(yùn)算符、移動(dòng)賦值運(yùn)算符以及析構(gòu)函數(shù)。
當(dāng)定義一個(gè)類時(shí),我們顯式地或隱式地指定在此類型的對(duì)象拷貝、移動(dòng)、賦值和銷毀時(shí)做什么。一個(gè)類通過定義五種特殊的成員函數(shù)來控制這些操作,包括:拷貝構(gòu)造函數(shù)(copy constructor)、拷貝賦值運(yùn)算符(copy-assignment operator)、移動(dòng)構(gòu)造函數(shù)(move constructor)、移動(dòng)賦值運(yùn)算符(move-assignment operator)和析構(gòu)函數(shù)(destructor)。拷貝和移動(dòng)構(gòu)造函數(shù)定義了當(dāng)用同類型的另一個(gè)對(duì)象初始化本對(duì)象時(shí)做什么??截惡鸵苿?dòng)賦值運(yùn)算符定義了將一個(gè)對(duì)象賦予同類型的另一個(gè)對(duì)象時(shí)做什么。析構(gòu)函數(shù)定義了當(dāng)此類型對(duì)象銷毀時(shí)做什么。我們稱這些操作為拷貝控制操作(copy control)。
如果一個(gè)類沒有定義所有這些拷貝控制成員,編譯器會(huì)自動(dòng)為它定義缺失的操作。因此,很多類會(huì)忽略這些拷貝控制操作。但是,對(duì)一些類來說,依賴這些操作的默認(rèn)定義會(huì)導(dǎo)致災(zāi)難。通常,實(shí)現(xiàn)拷貝控制操作最困難的地方是首先認(rèn)識(shí)到什么時(shí)候需要定義這些操作。
在定義任何C++類時(shí),拷貝控制操作都是必要部分.對(duì)初學(xué)C++的程序員來說,必須定義對(duì)象拷貝、移動(dòng)、賦值炎銷毀時(shí)做什么,這常常令他們感到困擾。這種困擾很復(fù)雜,因?yàn)槿绻覀儾伙@式定義這些操作,編譯器也會(huì)為我們定義,但編譯器定義的版本的行為可能并非我們所想。
拷貝、賦值與銷毀
我們將以最基本的操作一一拷貝構(gòu)造函數(shù)、拷貝賦值運(yùn)算符和析構(gòu)函數(shù)作為開始。
拷貝構(gòu)造函數(shù)
如果一個(gè)構(gòu)造函數(shù)的第一個(gè)參數(shù)是自身類類型的引用,且任何額外參數(shù)都有默認(rèn)值,則此構(gòu)造函數(shù)是拷貝構(gòu)造函數(shù)。
class Foo{
public:
Foo(); //默認(rèn)構(gòu)造函數(shù)
Foo(const Foo&); // 拷貝構(gòu)造函數(shù)
}拷貝構(gòu)造函數(shù)的第一個(gè)參數(shù)必須是一個(gè)引用類型,原因我們稍后解釋。雖然我們可以定義一個(gè)接受非const引用的拷貝構(gòu)造函數(shù),但此參數(shù)幾乎總是一個(gè)const的引用。拷貝構(gòu)造函數(shù)在幾種情況下都會(huì)被隱式地使用。因此,拷貝構(gòu)造函數(shù)通常不應(yīng)該是explicit的。
合成拷貝構(gòu)造函數(shù)
如果我們沒有為一個(gè)類定義拷貝構(gòu)造函數(shù),編譯器會(huì)為我們定義一個(gè)。與合成默認(rèn)構(gòu)造函數(shù)不同,即使我們定義了其他構(gòu)造函數(shù),編譯器也會(huì)為我們合成一個(gè)拷貝構(gòu)造函數(shù)。
對(duì)某些類來說,合成拷貝構(gòu)造函數(shù)(synthesized copy constructor)用來阻止我們拷貝該類類型的對(duì)象。而一般情況,合成的拷貝構(gòu)造函數(shù)會(huì)將其參數(shù)的成員逐個(gè)拷貝到正在創(chuàng)建的對(duì)象中。編譯器從給定對(duì)象中依次將每個(gè)非static成員拷貝到正在創(chuàng)建的對(duì)象中。
每個(gè)成員的類型決定了它如何拷貝:對(duì)類類型的成員,會(huì)使用其拷貝構(gòu)造函數(shù)來拷貝;內(nèi)置類型的成員則直接拷貝。雖然我們不能直接拷貝一個(gè)數(shù)組,但合成拷貝構(gòu)造函數(shù)會(huì)逐元素地拷貝一個(gè)數(shù)組類型的成員。如果數(shù)組元素是類類型,則使用元素的拷貝構(gòu)造函數(shù)來進(jìn)行拷貝。
作為一個(gè)例子,我們的sales_data類的合成拷貝構(gòu)造函數(shù)等價(jià)于:
class Sales_data
public:
//其他成員和構(gòu)造函數(shù)的定義,如前
//與合成的拷貝構(gòu)造函數(shù)等價(jià)的指貝構(gòu)造函數(shù)的聲明
Sales_data(const Sales_data&);
private:
std::string bookKNo;
int unit_sold = 0;
double revenue=0.0;
//與Sales_data的合成的指貝構(gòu)造函數(shù)等價(jià)
Sales_data::Sales_data(const Sales_data&orig):
bookNo(orig.bookNo),//使用stritng的指貝構(gòu)造函數(shù)
units_sold(orig.units_sold),//指貝orig.units_sold
revenue(orig.revenue)//拷貝orig.revenue
{}//空函數(shù)體拷貝初始化
現(xiàn)在,我們可以完全理解直接初始化和拷貝初始化之間的差異了:
string dots(10,'.');//直接初始化 string s(dots)}//直接初始化 string s2=dots;//拷貝初始化 string null_book="9-~999-99999-9";//拷貝初始化 string nines=string(100,'9'); //拷貝初始化
當(dāng)使用直接初始化時(shí),我們實(shí)際上是要求編譯器使用普通的函數(shù)匹配來選擇與我們提供的參數(shù)最匹配的構(gòu)造函數(shù)。當(dāng)我們使用拷貝初始化(copy initialization)時(shí),我們要求編譯器將右側(cè)運(yùn)算對(duì)象拷貝到正在創(chuàng)建的對(duì)象中,如果需要的話還要進(jìn)行類型轉(zhuǎn)換。
拷貝初始化通常使用拷貝構(gòu)造函數(shù)來完成。但是,如果一個(gè)類有一個(gè)移動(dòng)構(gòu)造函數(shù),則拷貝初始化有時(shí)會(huì)使用移動(dòng)構(gòu)造函數(shù)而非拷貝構(gòu)造函數(shù)來完成。但現(xiàn)在,我們只需了解拷貝初始化何時(shí)發(fā)生,以及拷貝初始化是依靠拷貝構(gòu)造函數(shù)或移動(dòng)構(gòu)造函數(shù)來完成的就可以了。
拷貝初始化不僅在我們用=定義變量時(shí)會(huì)發(fā)生,在下列情況下也會(huì)發(fā)生
- 將一個(gè)對(duì)象作為實(shí)參傳遞給一個(gè)非引用類型的形參
- 從一個(gè)返回類型為非引用類型的函數(shù)返回一個(gè)對(duì)象
- 用花括號(hào)列表初始化一個(gè)數(shù)組中的元素或一個(gè)聚合類中的成員
某些類類型還會(huì)對(duì)他們所分配的對(duì)象使用拷貝初始化。例如,當(dāng)我們初始化標(biāo)準(zhǔn)庫容器或是調(diào)用其insert或push成員時(shí),容器會(huì)對(duì)其元素進(jìn)行拷貝初始化。與之相對(duì),用emplace成員創(chuàng)建的元素都進(jìn)行直接初始化。
拷貝初始化的限制
如前所述,如果我們使用初始化值要求通過一個(gè)explicit的構(gòu)造函數(shù)來進(jìn)行類型轉(zhuǎn)換,那么使用拷貝初始化是直接初始化就不是無關(guān)緊要的了:
vector<int>v1(10);//正確,直接初始化 vector<int>v2=10;//錯(cuò)誤:接受大小參數(shù)的構(gòu)造函數(shù)是explicit的 void(vector<int>);//的參數(shù)進(jìn)行指貝初始化 f(10);//錯(cuò)誤:不能用一個(gè)explicit的構(gòu)造函數(shù)拷貝一個(gè)實(shí)參 f(vector<int>(10));//正確:從一個(gè)int直接構(gòu)造一個(gè)臨時(shí)vector
直接初始化v1是合法的,但看起來與之等價(jià)的拷貝初始化v2則是錯(cuò)誤的,因?yàn)関ector的接受單一大小參數(shù)的構(gòu)造函數(shù)是explicit的。出于同樣的原因,當(dāng)傳遞一個(gè)實(shí)參或從函數(shù)返回一個(gè)值時(shí),我們不能隱式使用一個(gè)explicit構(gòu)造函數(shù)。如果我們希望使用一個(gè)explicit構(gòu)造函數(shù),就必須顯式地使用,像此代碼中最后一行那樣。編譯器可以繞過拷貝構(gòu)造函數(shù)
在拷貝初始化過程中,編譯器可以(但不是必須)跳過拷貝/移動(dòng)構(gòu)造函數(shù),直接創(chuàng)建對(duì)象。即,編譯器被允許將下面的代碼
string null_book="9-999-99999-9";//拷貝初始化
改寫為
string null_book("9-999-99999-9");//編譯器略過了拷貝構(gòu)造函數(shù)但是,即使編譯器略過了拷貝/移動(dòng)構(gòu)造函數(shù),但在這個(gè)程序點(diǎn)上,拷貝/移動(dòng)構(gòu)造函數(shù)必須是存在且可訪問的(例如,不能是private的)。
拷貝賦值運(yùn)算符
與類控制其對(duì)象如何初始化一樣,類也可以控制其對(duì)象如何賦值:
Sales_data trans,accum; trans=accum;//使用Sales_data的拷貝賦值運(yùn)算符
與拷貝構(gòu)造函數(shù)一樣,如果類未定義自己的拷貝賦值運(yùn)算符,編譯器會(huì)為它合成一個(gè)。
重載賦值運(yùn)算符
在介紹合成賦值運(yùn)算符之前,我們需要了解一點(diǎn)兒有關(guān)重載運(yùn)算符(overloaded operator)的知識(shí)。
重載運(yùn)算符本質(zhì)上是函數(shù),其名字由operator關(guān)鍵字后接表示要定義的運(yùn)算符的符號(hào)組成。因此,賦值運(yùn)算符就是一個(gè)名為operator=的函數(shù)。類似于任何其他函數(shù),
運(yùn)算符函數(shù)也有一個(gè)返回類型和一個(gè)參數(shù)列表。
重載運(yùn)算符的參數(shù)表示運(yùn)算符的運(yùn)算對(duì)象。某些運(yùn)算符,包括賦值運(yùn)算符,必須定義為成員函數(shù)。如果一個(gè)運(yùn)算符是一個(gè)成員函數(shù),其左側(cè)運(yùn)算對(duì)象就綁定到隱式的this參數(shù)。對(duì)于一個(gè)二元運(yùn)算符,例如賦值運(yùn)算符,其右側(cè)運(yùn)算對(duì)象作為顯式參數(shù)傳遞。
拷貝賦值運(yùn)算符接受一個(gè)與其所在類相同類型的參數(shù):
class Foo{
public:
Foo& operator=(const Foo&);//賦值運(yùn)算符
};為了與內(nèi)置類型的賦值保持一致,賦值運(yùn)算符通常返回一個(gè)指向其左側(cè)運(yùn)算對(duì)象的引用。另外值得注意的是,標(biāo)準(zhǔn)庫通常要求保存在容器中的類型要具有賦值運(yùn)算符,且其返回值是左側(cè)運(yùn)算對(duì)象的引用。
賦值運(yùn)算符通常應(yīng)該返回一個(gè)指向其左側(cè)運(yùn)算對(duì)豫的引用。
合成拷貝賦值運(yùn)算符
與處理拷貝構(gòu)造函數(shù)一樣,如果一個(gè)類未定義自己的拷貝賦值運(yùn)算符,編譯器會(huì)為它生成一個(gè)合成拷貝賦值運(yùn)算符(synthesized copy-assignment operator)。類似拷貝構(gòu)造函數(shù),對(duì)于某些類,合成拷貝賦值運(yùn)算符用來禁止該類型對(duì)象的賦值。如果拷貝賦值運(yùn)算符并非出于此目的,它會(huì)將右側(cè)運(yùn)算對(duì)象的每個(gè)非static成員賦予左側(cè)運(yùn)算對(duì)象的對(duì)應(yīng)成員,這一工作是通過成員類型的拷貝賦值運(yùn)算符來完成的。對(duì)于數(shù)組類型的成員,逐個(gè)賦值數(shù)組元素。合成拷貝賦值運(yùn)算符返名一個(gè)指向其左側(cè)運(yùn)算對(duì)象的引用。
作為一個(gè)例子,下面的代碼等價(jià)于Sales_data的合成拷貝賦值運(yùn)算符:
//等價(jià)于合成指貝賦值運(yùn)算符
Sales_data&
Sales_data::operator=(const Sales_data&rhs)
{
bookkNo=rhs.bookNo//調(diào)用string::operator=
units_sold=rhs.units_sold;//使用內(nèi)置的int賦值
revenue=rhs.revenue;//使用內(nèi)置的double賦值
return*this;//返回一個(gè)此對(duì)象的引用
}析構(gòu)函數(shù)
析構(gòu)函數(shù)執(zhí)行與構(gòu)造函數(shù)相反的操作:構(gòu)造函數(shù)初始化對(duì)象的非static數(shù)據(jù)成員,還可能做一些其他工作;析構(gòu)函數(shù)釋放對(duì)象使用的資源,并銷毀對(duì)象的非static數(shù)據(jù)成員。
析構(gòu)函數(shù)是類的一個(gè)成員函數(shù),名字由波浪號(hào)接類名構(gòu)成。它沒有返回值,也不接受參數(shù)
class Foo{
public:
~Foo();//析構(gòu)函數(shù)
}由于析構(gòu)函數(shù)不接受參數(shù),因此它不能被重載。對(duì)一個(gè)給定類,只會(huì)有唯一一個(gè)析構(gòu)函數(shù)。
析構(gòu)函數(shù)完成什么工作
如同構(gòu)造函數(shù)有一個(gè)初始化部分和一個(gè)函數(shù)體,析構(gòu)函數(shù)也有一個(gè)函數(shù)體和一個(gè)析構(gòu)部分。在一個(gè)構(gòu)造函數(shù)中,成員的初始化是在函數(shù)體執(zhí)行之前完成的,且按照它們在類中出現(xiàn)的順序進(jìn)行初始化。在一個(gè)析構(gòu)函數(shù)中,首先執(zhí)行函數(shù)體,然后銷毀成員。成員按初始化順序的逆序銷毀。在對(duì)象最后一次使用之后,析構(gòu)函數(shù)的函數(shù)體可執(zhí)行類設(shè)計(jì)者希望執(zhí)行的任何收尾工作。通常,析構(gòu)函數(shù)釋放對(duì)象在生存期分配的所有資源。在一個(gè)析構(gòu)函數(shù)中,不存在類似構(gòu)造函數(shù)中初始化列表的東西來控制成員如何銷毀,析構(gòu)部分是隱式的。成員銷毀時(shí)發(fā)生什么完全依賴于成員的類型。銷毀類類型的成員需要執(zhí)行成員自己的析構(gòu)函數(shù)。內(nèi)置類型沒有析構(gòu)函數(shù),因此銷毀內(nèi)置類型成員什么也不需要做。
隱式銷毀一個(gè)內(nèi)置指針類型的成員不會(huì)delete它所指向的對(duì)象。
與普通指針不同,智能指針是類類型,所以具有析構(gòu)函數(shù)。因此,與普通指針不同,智能指針成員在析構(gòu)階段會(huì)被自動(dòng)銷毀。什么時(shí)候會(huì)調(diào)用析構(gòu)函數(shù)
無論何時(shí)一個(gè)對(duì)象被銷毀,就會(huì)自動(dòng)調(diào)用其析構(gòu)函數(shù):
- 變量在離開其作用域時(shí)被銷毀。
- 當(dāng)一個(gè)對(duì)象被銷毀時(shí),其成員被銷毀。
- 容器(無論是標(biāo)準(zhǔn)庫容器還是數(shù)組)被銷毀時(shí),其元素被銷毀。
- 對(duì)于動(dòng)態(tài)分配的對(duì)象,當(dāng)對(duì)指向它的指針應(yīng)用delete運(yùn)算符時(shí)被銷毀。
- 對(duì)于臨時(shí)對(duì)象,當(dāng)創(chuàng)建它的完整表達(dá)式結(jié)束時(shí)被銷毀。
由于析構(gòu)函數(shù)自動(dòng)運(yùn)行,我們的程序可以按需要分配資源,而(通常)無須擔(dān)心何時(shí)釋放這些資源。
例如,下面代碼片段定義了四個(gè)sales_data對(duì)象:
{//新作用域
//p和p2指向動(dòng)態(tài)分配的對(duì)象
Sales_data *p=new Sales_data;//p是一個(gè)內(nèi)置指針
auto p2=make_shared<Sales_data>();//p2是一個(gè)shared_ptr
Sales_data item(*p);//指貝構(gòu)造函數(shù)將*p拷貝到item中
vector<Sales_data>vec;//局部對(duì)象
vec.push_back(*p2);//拷貝p2指向的對(duì)象
delete p;//對(duì)p指向的對(duì)象執(zhí)行析構(gòu)函數(shù)
}//退出局部作用域;對(duì)item、p2和vec調(diào)用析構(gòu)函數(shù)
//銷毀p2會(huì)途減其引用計(jì)數(shù);如果引用計(jì)數(shù)變?yōu)?,對(duì)象被釋放
//銷毀vec會(huì)銷鱷它的元素每個(gè)Sales_data對(duì)象都包含一個(gè)string成員,它分配動(dòng)態(tài)內(nèi)存婁保存bookNo成員中的字符。但是,我們的代碼唯一需要直接管理的內(nèi)存就是我們直接分配的Sales_data對(duì)象。我們的代碼只需直接釋放綁定到p的動(dòng)態(tài)分配對(duì)象。
其他Sales_data對(duì)象會(huì)在離開作用域時(shí)被自動(dòng)銷毀。當(dāng)程序塊結(jié)束時(shí),vec、p2和item都離開了作用域,意味著在這些對(duì)象上分別會(huì)執(zhí)行vector、shared_ptr和Sales_data的析構(gòu)函數(shù)。vector的析構(gòu)函數(shù)會(huì)銷毀我們添加到vec的元素。shared_ptr的析構(gòu)函數(shù)會(huì)遞減p2指向的對(duì)象的引用計(jì)數(shù)。在本例中,引用計(jì)數(shù)會(huì)變?yōu)?,因此shared_ptz的析構(gòu)函數(shù)會(huì)delete p2分配的Sales_data對(duì)象。
在所有情況下,Sales_data的析構(gòu)函數(shù)都會(huì)隱式地銷毀bookNo成員.銷毀bookNo會(huì)調(diào)用string的析構(gòu)函數(shù),它會(huì)釋放用來保存ISBN的內(nèi)存。
當(dāng)指向一個(gè)對(duì)象的引用或指針離開作用域時(shí),析構(gòu)函數(shù)不會(huì)執(zhí)行。
合成析構(gòu)函數(shù)
當(dāng)一個(gè)類未定義自己的析構(gòu)函數(shù)時(shí),編詳器會(huì)為它定義一個(gè)合成析構(gòu)函數(shù)(synthesized destructor)。類似拷貝構(gòu)造函數(shù)和拷貝賦值運(yùn)算符,對(duì)于樹些類,合成析構(gòu)函數(shù)被用來阻止該類型的對(duì)象被銷毀。如果不是這種情況,合成析構(gòu)函數(shù)的函數(shù)體就為空。
例如,下面的代碼片段等價(jià)于Sales_data的合成析構(gòu)函數(shù):
class Sales_data{
public:
//成員會(huì)被自動(dòng)銷毀,除此之外不需要做其他事情
~Sales_data()《
//其他成員的定義,如前
}在(空)析構(gòu)函數(shù)體執(zhí)行完畢后,成員會(huì)被自動(dòng)銷毀。特別的,string的析構(gòu)函數(shù)會(huì)被調(diào)用,它將釋放bookNo成員所用的內(nèi)存。
認(rèn)識(shí)到析構(gòu)函數(shù)體自身并不直接銷毀成員是非常重要的。成員是在析構(gòu)函數(shù)體之后隱含的析構(gòu)階段中被銷毀的。在整個(gè)對(duì)象銷毀過程中,析構(gòu)函數(shù)體是作為成員銷毀步驟之外的另一部分而進(jìn)行的。
三/五法則
如前所述,右三個(gè)基本操作可以控制類的拷貝操作:拷貝構(gòu)造函數(shù),拷貝賦值運(yùn)算符和析構(gòu)函數(shù)。而且,在新標(biāo)準(zhǔn)下,一個(gè)類還可以定義一個(gè)移動(dòng)構(gòu)造函數(shù)和一個(gè)移動(dòng)賦值運(yùn)算符。
C++語言并不要求我們定義所有這些操作:可以只定義其中一個(gè)或兩個(gè),而不必定義所有。但是,這些操作通常應(yīng)該被看做一個(gè)整體。通常,只需要其中一個(gè)操作,而不需要定義所有操作的情況是很少見的。
需要析構(gòu)函數(shù)的類也需要拷貝和賦值操作
當(dāng)我們決定一個(gè)類是否要定義它自己版本的拷貝控制成員時(shí),一個(gè)基本原則是首先確定這個(gè)惡類是否需要一個(gè)析構(gòu)函數(shù)。通常,對(duì)析構(gòu)函數(shù)的需求要比對(duì)拷貝構(gòu)造函數(shù)或賦值運(yùn)算符的需求更為明顯。如果這個(gè)類需要一個(gè)析構(gòu)函數(shù),我們幾乎可以肯定它也需要一個(gè)拷貝構(gòu)造函數(shù)和一個(gè)拷貝賦值運(yùn)算符。
我們在練習(xí)中用過的HasPtr類是一個(gè)好例子。這個(gè)類在構(gòu)造函數(shù)中分配動(dòng)態(tài)內(nèi)存。合成析構(gòu)函數(shù)不會(huì)delete一個(gè)指針數(shù)據(jù)成員。因此,此類需要定義一個(gè)析構(gòu)函數(shù)來釋放構(gòu)造函數(shù)分配的內(nèi)存。
應(yīng)該怎么做可能還有點(diǎn)兒不清晰,但基本原則告訴我們,HasPtr也需要一個(gè)拷貝構(gòu)造函數(shù)和一個(gè)拷貝賦值運(yùn)算符。
如果我們?yōu)镠asPtr定義一個(gè)析構(gòu)函數(shù),但使用合成版本的拷貝構(gòu)造函數(shù)和拷貝賦值運(yùn)算符,考慮會(huì)發(fā)生什么:
class HasPtr{
public:
HasPtr(const std:;string&s=std::string()):
ps(new std::string(s)),i(0){}
~HasPtr(){deleteps;
//錯(cuò)誤:HasPtr需要一個(gè)拷貝構(gòu)造函數(shù)和一個(gè)拷貝賦值運(yùn)算符
//其他成員的定義,如前
};
在這個(gè)版本的類定義中,構(gòu)造函數(shù)中分配的內(nèi)存將在HasPtr對(duì)象銷毀時(shí)被釋放。但不幸的是,我們引入了一個(gè)嚴(yán)重的錯(cuò)誤!這個(gè)版本的類使用了合成的拷貝構(gòu)造函數(shù)和拷貝賦值運(yùn)算符。這些函數(shù)簡單拷貝指針成員,這意味著多個(gè)HasPtr對(duì)象可能指向相同的內(nèi)存:
HasPtr f(HasPtr hp)// HasPtr是傳值參數(shù),所以將被指貝
{
HasPtr ret=hp;//拷貝給定的HasPt
//處理ret
return ret;//ret和hp被銷毀
}
當(dāng)f返回時(shí),hp和ret都被銷毀,在兩個(gè)對(duì)象上都會(huì)調(diào)用HasPtr的析構(gòu)函數(shù)。此析構(gòu)函數(shù)會(huì)delete ret和hp中的指針成員。但這兩個(gè)對(duì)象包含相同的指針值。此代碼會(huì)導(dǎo)致此指針被delete兩次,這顯然是一個(gè)錯(cuò)誤。將要發(fā)生什么是未定義的。
此外,f的調(diào)用者還會(huì)使用傳遞給f的對(duì)象:
```cpp
HasPtr("some values");
f(p);//當(dāng)王結(jié)束時(shí),p指向的肉存被釋放
HasPtr(p);//現(xiàn)在p和q都指向無效內(nèi)存!p以及q指向的內(nèi)存不再有效,在hp(或ret!)銷毀時(shí)它就被歸還給系統(tǒng)了。
需要拷貝操作的類也需要賦值操作,反之亦然
雖然很多類需要定義所有(或是不需要定義任何)拷貝控制成員,但某些類所要完成的工作,只需要拷貝或賦值操作,不需要析構(gòu)函數(shù)。作為一個(gè)例子,考慮一個(gè)類為每個(gè)對(duì)象分配一個(gè)獨(dú)有的、唯一的序號(hào)。這個(gè)類需要一個(gè)拷貝構(gòu)造函數(shù)為每個(gè)新創(chuàng)建的對(duì)象生成一個(gè)新的、獨(dú)一無二的序號(hào)。除此之外,這個(gè)拷貝構(gòu)造函數(shù)從給定對(duì)象拷貝所有其他數(shù)據(jù)成員。這個(gè)類還需要自定義拷貝賦值運(yùn)算符來邀
免將序號(hào)賦予目的對(duì)象。但是,這個(gè)類不需要自定義析構(gòu)函數(shù)。
這個(gè)例子引出了第二個(gè)基本原則:如果一個(gè)類需要一個(gè)拷貝構(gòu)造函數(shù),幾乎可以肯定它也需要一個(gè)拷貝賦值運(yùn)算符。反之亦然一一如果一個(gè)類需要一個(gè)拷貝賦值運(yùn)算符,幾乎可以肯定它也需要一個(gè)拷貝構(gòu)造函數(shù)。然而,無論是需要拷貝構(gòu)造函數(shù)還是需要拷貝賦值運(yùn)算符都不必然意味著也需要析構(gòu)函數(shù)。
使用=default
我們可以通過將拷貝控制成員定義為=default來顯式地要求編譯器生成合成的版本
class Sales_data{
public:
//拷貝控制成員;使用default
Sales_data()=default;
Sales_data(const Sales_data&)=default;
Sales_data& operator=(const Sales_data&);
~Sales_data()=default;
//其他成員的定義,如前
Sales_data &Sales_data::operator=(const Sales_data&)=default;當(dāng)我們在類內(nèi)用=default修飾成員的聲明時(shí),合成的函數(shù)將隱式地聲明為內(nèi)聯(lián)的(就像任何其他類內(nèi)聲明的成員函數(shù)一樣)。如果我們不希望合成的成員是內(nèi)聯(lián)函數(shù),應(yīng)該只對(duì)成員的類外定義使用=default,就像對(duì)拷貝賦值運(yùn)算符所做的那樣。
阻止拷貝
雖然大多數(shù)類應(yīng)該定義(而且通常也的確定義了)拷貝構(gòu)造函數(shù)和拷貝賦值運(yùn)算符,但對(duì)某些類來說,這些操作沒有合理的意義。在此情況下,定義類時(shí)必須采用某種機(jī)制阻止拷貝或賦值。例如,iostream類阻止了拷貝,以避免多個(gè)對(duì)象寫入或讀取相同的IO緩沖。為了阻止拷貝,看起來可能應(yīng)該不定義拷貝控制成員。但是,這種策略是無效的:如果我們的類未定義這些操作,編譯器為它生成合成的版本。
定義刪除的函數(shù)
在新標(biāo)準(zhǔn)下,我們可以通過將拷貝構(gòu)造函數(shù)和拷貝賦值運(yùn)算符定義為刪除的函數(shù)(deleted function)來阻止拷貝。刪除的函數(shù)是這樣一種函數(shù):我們雖然聲明了它們,但不能以任何方式使用它們。在函數(shù)的參數(shù)列表后面加上=delete來指出我們希望將它定義為刪除的:
struct NoCopy{
NoCopy()=default;//使用合成的默認(rèn)構(gòu)造出數(shù)
NoCopy(const NoCopy&)=delete;//阻止拷貝
NoCopy&operator=(const NoCopy&)=delete;//阻止賦值
~NoCopy() = default; // 使用合成的析構(gòu)函數(shù)
// 其他成員一個(gè)刪除了析構(gòu)函數(shù)的類型,編譯器將不允許定義該類型的變量或創(chuàng)建該類的臨時(shí)對(duì)象。而且,如果一個(gè)類有某個(gè)成員的類型刪除了析構(gòu)函數(shù),我們也不能定義該類的變量或臨時(shí)對(duì)象。因?yàn)槿绻粋€(gè)成員的析構(gòu)函數(shù)是刪除的,則該成員無法被銷毀。而如果一個(gè)成員無法被銷毀,則對(duì)象整體也就無法被銷毀了。
對(duì)于刪除了析構(gòu)函數(shù)的類型,雖然我們不能定義這種類型的變量或成員,但可以動(dòng)態(tài)分配這種類型的對(duì)象。但是,不能釋放這些對(duì)象:
struct NoDtor{
NoDtor()=default;//使用合成默認(rèn)構(gòu)造函數(shù)
~NoDtor()=delete;//我們不能銷毀NoDtor類型的對(duì)象
NoDtor nd;//錯(cuò)誤:NoDtor的析構(gòu)函數(shù)是剛除的
NoDtor*p=new NoDptor();//正確:但我們不能delete p
delete p;//錯(cuò)誤:NoDtor的析構(gòu)函數(shù)是剔除的
}合成的拷貝控制成員可能是刪除的
如前所述,如果我們未定義拷貝控制成員,編譯器會(huì)為我們定義合成的版本。類似的,如果一個(gè)類未定義構(gòu)造函數(shù),編譯器會(huì)為其合成一個(gè)默認(rèn)構(gòu)造函數(shù)。對(duì)某些類來說,編譯器將這些合成的成員定義為刪除的函數(shù):
- 如果類的某個(gè)成員的析構(gòu)函數(shù)是刪除的或不可訪問的(例如,是private的),則類的合成析構(gòu)函數(shù)被定義為刪除的。
- 如果類的某個(gè)成員的拷貝構(gòu)造函數(shù)是刪除的或不可訪問的,則類的合成拷貝構(gòu)造函數(shù)被定義為刪除的。
- 如果類的某個(gè)成員的析構(gòu)函數(shù)是刪除的或不可訪問的,則類合成的拷貝構(gòu)造函數(shù)也被定義為刪除的。
- 如果類的樹個(gè)成員的拷貝賦值運(yùn)算符是刪除的或不可訪問的,或是類有一個(gè)const的或引用成員,則類的合成拷貝賦值運(yùn)算符被定義為刪除的。
- 如果類的某個(gè)成員的析構(gòu)函數(shù)是刪除的或不可訪問的,或是類有一個(gè)引用成員,它沒有類內(nèi)初始化器,或是類有一個(gè)const成員,它沒有類內(nèi)初始化器且其類型未顯式定義默認(rèn)構(gòu)造函數(shù),則該類的默認(rèn)構(gòu)造函數(shù)被定義為刪除的。
本質(zhì)上,這些規(guī)則的含義是:如果一個(gè)類有數(shù)據(jù)成員不能默認(rèn)構(gòu)造、拷貝、復(fù)制或銷毀,則對(duì)應(yīng)的成員函數(shù)將被定義為刪除的。
一個(gè)成員有刪除的或不可訪問的析構(gòu)函數(shù)會(huì)導(dǎo)致合成的默認(rèn)和拷貝構(gòu)造函數(shù)被定義為刪除的,這看起來可能有些奇怪。其原因是,如果沒有這條規(guī)則,我們可能會(huì)創(chuàng)建出無法銷毀的對(duì)象。
對(duì)于具有引用成員或無法默認(rèn)構(gòu)造的const成員的類,編詳器不會(huì)為其合成默認(rèn)構(gòu)造函數(shù),這應(yīng)該不奇怪。同樣不出人意料的規(guī)則是:如果一個(gè)類有const成員,則它不能使用合成的拷貝賦值運(yùn)算符。畢竟,此運(yùn)算符試圖賦值所有成員,而將一個(gè)新值賦予一個(gè)const對(duì)象是不可能的。
雖然我們可以將一個(gè)新值賦予一個(gè)引用成員,但這樣做改變的是引用指向的對(duì)象的值,而不是引用本身。如果為這樣的類合成拷貝賦值運(yùn)算符,則賦值后,左側(cè)運(yùn)算對(duì)象仍然指向與賦值前一樣的對(duì)象,而不會(huì)與右側(cè)運(yùn)算對(duì)象指向相同的對(duì)象。由于這種行為看起來并不是我們所期望的,因此對(duì)于有引用成員的類,合成拷貝賦值運(yùn)算符被定義為刪除的。
本質(zhì)上,當(dāng)不可能拷貝、賦值或銷毀類的成員時(shí),類的合成拷貝控制成員就被定義為刪除的。
private拷貝控制
在新標(biāo)準(zhǔn)發(fā)布之前,類是通過將其拷貝構(gòu)造函數(shù)和拷貝賦值運(yùn)算符聲明為private的來阻止拷貝:
class PrivateCopy{
//無訪問說明符;接下來的成員默認(rèn)為private的;
//拷貝控制成員是private的,因此普通用戶代碼無法訪問
PrivateCopy(const PrivateCopy&);
PrivateCopy&operator=(const PrivateCopy&);
//其他成員
public:
PrivateCopy()=default;//使用合成的默認(rèn)構(gòu)造出數(shù)
~PrivateCopy();//用戶可以定義此類型的對(duì)象,但無法拷貝它們
}由于析構(gòu)函數(shù)是public的,用戶可以定義PrivateCopy類型的對(duì)象。但是,由于拷貝構(gòu)造函數(shù)和拷貝賦值運(yùn)算符是private的,用戶代碼將不能拷貝這個(gè)類型的對(duì)象。但是,友元和成員函數(shù)仍舊可以拷貝對(duì)象。為了阻止友元和成員函數(shù)進(jìn)行拷貝,我們將這些拷貝控制成員聲明為private的,但并不定義它們。
聲明但不定義一個(gè)成員函數(shù)是合法的,對(duì)此只有一個(gè)例外。試圖訪問一個(gè)未定義的成員將導(dǎo)致一個(gè)鏈接時(shí)錯(cuò)誤。通過聲明(但不定義)private的拷貝構(gòu)造函數(shù),我們可以預(yù)先阻止任何拷貝該類型對(duì)象的企圖:試圖拷貝對(duì)象的用戶代碼將在編譯階段被標(biāo)記為錯(cuò)誤;成員函數(shù)或友元函數(shù)中的拷貝操作將會(huì)導(dǎo)致鏈接時(shí)錯(cuò)誤。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
vscode cmake compilers配置路徑的實(shí)現(xiàn)
本文主要介紹了vscode cmake compilers配置路徑的實(shí)現(xiàn),文中通過圖文介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-03-03
c++實(shí)現(xiàn)十進(jìn)制轉(zhuǎn)換成16進(jìn)制示例
這篇文章主要介紹了c++實(shí)現(xiàn)十進(jìn)制轉(zhuǎn)換成16進(jìn)制示例,需要的朋友可以參考下2014-05-05
基于c++11的event-driven library的理解
這篇文章主要介紹了基于c++11的event-driven library的理解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-02-02
C++利用鏈表實(shí)現(xiàn)圖書信息管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C++利用鏈表實(shí)現(xiàn)圖書信息管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-11-11

