欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

C++中的Primer拷貝、賦值與銷毀詳解

 更新時間:2025年03月19日 09:20:09   作者:c-c-developer  
這篇文章主要介紹了C++中的Primer拷貝、賦值與銷毀方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教

拷貝控制和資源管理

在本章中,我們將學(xué)到,類可以定義構(gòu)造函數(shù),用來控制在創(chuàng)建此類型對象時做什么。在本章中,我們還將學(xué)習(xí)類如何控制該類型對象拷貝、賦值、移動或銷毀時做什么。類通過一些特殊的成員函數(shù)控制這些操作,包括:拷貝構(gòu)造函數(shù)、移動構(gòu)造函數(shù)、拷貝賦值運算符、移動賦值運算符以及析構(gòu)函數(shù)。

當(dāng)定義一個類時,我們顯式地或隱式地指定在此類型的對象拷貝、移動、賦值和銷毀時做什么。一個類通過定義五種特殊的成員函數(shù)來控制這些操作,包括:拷貝構(gòu)造函數(shù)(copy constructor)、拷貝賦值運算符(copy-assignment operator)、移動構(gòu)造函數(shù)(move constructor)、移動賦值運算符(move-assignment operator)和析構(gòu)函數(shù)(destructor)??截惡鸵苿訕?gòu)造函數(shù)定義了當(dāng)用同類型的另一個對象初始化本對象時做什么??截惡鸵苿淤x值運算符定義了將一個對象賦予同類型的另一個對象時做什么。析構(gòu)函數(shù)定義了當(dāng)此類型對象銷毀時做什么。我們稱這些操作為拷貝控制操作(copy control)。

如果一個類沒有定義所有這些拷貝控制成員,編譯器會自動為它定義缺失的操作。因此,很多類會忽略這些拷貝控制操作。但是,對一些類來說,依賴這些操作的默認(rèn)定義會導(dǎo)致災(zāi)難。通常,實現(xiàn)拷貝控制操作最困難的地方是首先認(rèn)識到什么時候需要定義這些操作。

在定義任何C++類時,拷貝控制操作都是必要部分.對初學(xué)C++的程序員來說,必須定義對象拷貝、移動、賦值炎銷毀時做什么,這常常令他們感到困擾。這種困擾很復(fù)雜,因為如果我們不顯式定義這些操作,編譯器也會為我們定義,但編譯器定義的版本的行為可能并非我們所想。

拷貝、賦值與銷毀

我們將以最基本的操作一一拷貝構(gòu)造函數(shù)、拷貝賦值運算符和析構(gòu)函數(shù)作為開始。

拷貝構(gòu)造函數(shù)

如果一個構(gòu)造函數(shù)的第一個參數(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ù)的第一個參數(shù)必須是一個引用類型,原因我們稍后解釋。雖然我們可以定義一個接受非const引用的拷貝構(gòu)造函數(shù),但此參數(shù)幾乎總是一個const的引用。拷貝構(gòu)造函數(shù)在幾種情況下都會被隱式地使用。因此,拷貝構(gòu)造函數(shù)通常不應(yīng)該是explicit的。

合成拷貝構(gòu)造函數(shù)

如果我們沒有為一個類定義拷貝構(gòu)造函數(shù),編譯器會為我們定義一個。與合成默認(rèn)構(gòu)造函數(shù)不同,即使我們定義了其他構(gòu)造函數(shù),編譯器也會為我們合成一個拷貝構(gòu)造函數(shù)。

對某些類來說,合成拷貝構(gòu)造函數(shù)(synthesized copy constructor)用來阻止我們拷貝該類類型的對象。而一般情況,合成的拷貝構(gòu)造函數(shù)會將其參數(shù)的成員逐個拷貝到正在創(chuàng)建的對象中。編譯器從給定對象中依次將每個非static成員拷貝到正在創(chuàng)建的對象中。

每個成員的類型決定了它如何拷貝:對類類型的成員,會使用其拷貝構(gòu)造函數(shù)來拷貝;內(nèi)置類型的成員則直接拷貝。雖然我們不能直接拷貝一個數(shù)組,但合成拷貝構(gòu)造函數(shù)會逐元素地拷貝一個數(shù)組類型的成員。如果數(shù)組元素是類類型,則使用元素的拷貝構(gòu)造函數(shù)來進行拷貝。

作為一個例子,我們的sales_data類的合成拷貝構(gòu)造函數(shù)等價于:

class Sales_data
public:
//其他成員和構(gòu)造函數(shù)的定義,如前
//與合成的拷貝構(gòu)造函數(shù)等價的指貝構(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ù)等價
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ù)最匹配的構(gòu)造函數(shù)。當(dāng)我們使用拷貝初始化(copy initialization)時,我們要求編譯器將右側(cè)運算對象拷貝到正在創(chuàng)建的對象中,如果需要的話還要進行類型轉(zhuǎn)換。

拷貝初始化通常使用拷貝構(gòu)造函數(shù)來完成。但是,如果一個類有一個移動構(gòu)造函數(shù),則拷貝初始化有時會使用移動構(gòu)造函數(shù)而非拷貝構(gòu)造函數(shù)來完成。但現(xiàn)在,我們只需了解拷貝初始化何時發(fā)生,以及拷貝初始化是依靠拷貝構(gòu)造函數(shù)或移動構(gòu)造函數(shù)來完成的就可以了。

拷貝初始化不僅在我們用=定義變量時會發(fā)生,在下列情況下也會發(fā)生

  • 將一個對象作為實參傳遞給一個非引用類型的形參
  • 從一個返回類型為非引用類型的函數(shù)返回一個對象
  • 用花括號列表初始化一個數(shù)組中的元素或一個聚合類中的成員

某些類類型還會對他們所分配的對象使用拷貝初始化。例如,當(dāng)我們初始化標(biāo)準(zhǔn)庫容器或是調(diào)用其insert或push成員時,容器會對其元素進行拷貝初始化。與之相對,用emplace成員創(chuàng)建的元素都進行直接初始化。

拷貝初始化的限制

如前所述,如果我們使用初始化值要求通過一個explicit的構(gòu)造函數(shù)來進行類型轉(zhuǎn)換,那么使用拷貝初始化是直接初始化就不是無關(guān)緊要的了:

vector<int>v1(10);//正確,直接初始化
vector<int>v2=10;//錯誤:接受大小參數(shù)的構(gòu)造函數(shù)是explicit的
void(vector<int>);//的參數(shù)進行指貝初始化
f(10);//錯誤:不能用一個explicit的構(gòu)造函數(shù)拷貝一個實參
f(vector<int>(10));//正確:從一個int直接構(gòu)造一個臨時vector

直接初始化v1是合法的,但看起來與之等價的拷貝初始化v2則是錯誤的,因為vector的接受單一大小參數(shù)的構(gòu)造函數(shù)是explicit的。出于同樣的原因,當(dāng)傳遞一個實參或從函數(shù)返回一個值時,我們不能隱式使用一個explicit構(gòu)造函數(shù)。如果我們希望使用一個explicit構(gòu)造函數(shù),就必須顯式地使用,像此代碼中最后一行那樣。編譯器可以繞過拷貝構(gòu)造函數(shù)

在拷貝初始化過程中,編譯器可以(但不是必須)跳過拷貝/移動構(gòu)造函數(shù),直接創(chuàng)建對象。即,編譯器被允許將下面的代碼

string null_book="9-999-99999-9";//拷貝初始化

改寫為

string null_book("9-999-99999-9");//編譯器略過了拷貝構(gòu)造函數(shù)

但是,即使編譯器略過了拷貝/移動構(gòu)造函數(shù),但在這個程序點上,拷貝/移動構(gòu)造函數(shù)必須是存在且可訪問的(例如,不能是private的)。

拷貝賦值運算符

與類控制其對象如何初始化一樣,類也可以控制其對象如何賦值:

Sales_data trans,accum;
trans=accum;//使用Sales_data的拷貝賦值運算符

與拷貝構(gòu)造函數(shù)一樣,如果類未定義自己的拷貝賦值運算符,編譯器會為它合成一個。

重載賦值運算符

在介紹合成賦值運算符之前,我們需要了解一點兒有關(guān)重載運算符(overloaded operator)的知識。

重載運算符本質(zhì)上是函數(shù),其名字由operator關(guān)鍵字后接表示要定義的運算符的符號組成。因此,賦值運算符就是一個名為operator=的函數(shù)。類似于任何其他函數(shù),
運算符函數(shù)也有一個返回類型和一個參數(shù)列表。

重載運算符的參數(shù)表示運算符的運算對象。某些運算符,包括賦值運算符,必須定義為成員函數(shù)。如果一個運算符是一個成員函數(shù),其左側(cè)運算對象就綁定到隱式的this參數(shù)。對于一個二元運算符,例如賦值運算符,其右側(cè)運算對象作為顯式參數(shù)傳遞。

拷貝賦值運算符接受一個與其所在類相同類型的參數(shù):

class Foo{
public:
    Foo& operator=(const Foo&);//賦值運算符
};

為了與內(nèi)置類型的賦值保持一致,賦值運算符通常返回一個指向其左側(cè)運算對象的引用。另外值得注意的是,標(biāo)準(zhǔn)庫通常要求保存在容器中的類型要具有賦值運算符,且其返回值是左側(cè)運算對象的引用。

賦值運算符通常應(yīng)該返回一個指向其左側(cè)運算對豫的引用。

合成拷貝賦值運算符

與處理拷貝構(gòu)造函數(shù)一樣,如果一個類未定義自己的拷貝賦值運算符,編譯器會為它生成一個合成拷貝賦值運算符(synthesized copy-assignment operator)。類似拷貝構(gòu)造函數(shù),對于某些類,合成拷貝賦值運算符用來禁止該類型對象的賦值。如果拷貝賦值運算符并非出于此目的,它會將右側(cè)運算對象的每個非static成員賦予左側(cè)運算對象的對應(yīng)成員,這一工作是通過成員類型的拷貝賦值運算符來完成的。對于數(shù)組類型的成員,逐個賦值數(shù)組元素。合成拷貝賦值運算符返名一個指向其左側(cè)運算對象的引用。

作為一個例子,下面的代碼等價于Sales_data的合成拷貝賦值運算符:

//等價于合成指貝賦值運算符
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òu)函數(shù)

析構(gòu)函數(shù)執(zhí)行與構(gòu)造函數(shù)相反的操作:構(gòu)造函數(shù)初始化對象的非static數(shù)據(jù)成員,還可能做一些其他工作;析構(gòu)函數(shù)釋放對象使用的資源,并銷毀對象的非static數(shù)據(jù)成員。

析構(gòu)函數(shù)是類的一個成員函數(shù),名字由波浪號接類名構(gòu)成。它沒有返回值,也不接受參數(shù)

class Foo{
public:
~Foo();//析構(gòu)函數(shù)
}

由于析構(gòu)函數(shù)不接受參數(shù),因此它不能被重載。對一個給定類,只會有唯一一個析構(gòu)函數(shù)。

析構(gòu)函數(shù)完成什么工作

如同構(gòu)造函數(shù)有一個初始化部分和一個函數(shù)體,析構(gòu)函數(shù)也有一個函數(shù)體和一個析構(gòu)部分。在一個構(gòu)造函數(shù)中,成員的初始化是在函數(shù)體執(zhí)行之前完成的,且按照它們在類中出現(xiàn)的順序進行初始化。在一個析構(gòu)函數(shù)中,首先執(zhí)行函數(shù)體,然后銷毀成員。成員按初始化順序的逆序銷毀。在對象最后一次使用之后,析構(gòu)函數(shù)的函數(shù)體可執(zhí)行類設(shè)計者希望執(zhí)行的任何收尾工作。通常,析構(gòu)函數(shù)釋放對象在生存期分配的所有資源。在一個析構(gòu)函數(shù)中,不存在類似構(gòu)造函數(shù)中初始化列表的東西來控制成員如何銷毀,析構(gòu)部分是隱式的。成員銷毀時發(fā)生什么完全依賴于成員的類型。銷毀類類型的成員需要執(zhí)行成員自己的析構(gòu)函數(shù)。內(nèi)置類型沒有析構(gòu)函數(shù),因此銷毀內(nèi)置類型成員什么也不需要做。

隱式銷毀一個內(nèi)置指針類型的成員不會delete它所指向的對象。

與普通指針不同,智能指針是類類型,所以具有析構(gòu)函數(shù)。因此,與普通指針不同,智能指針成員在析構(gòu)階段會被自動銷毀。什么時候會調(diào)用析構(gòu)函數(shù)

無論何時一個對象被銷毀,就會自動調(diào)用其析構(gòu)函數(shù):

  • 變量在離開其作用域時被銷毀。
  • 當(dāng)一個對象被銷毀時,其成員被銷毀。
  • 容器(無論是標(biāo)準(zhǔn)庫容器還是數(shù)組)被銷毀時,其元素被銷毀。
  • 對于動態(tài)分配的對象,當(dāng)對指向它的指針應(yīng)用delete運算符時被銷毀。
  • 對于臨時對象,當(dāng)創(chuàng)建它的完整表達式結(jié)束時被銷毀。

由于析構(gòu)函數(shù)自動運行,我們的程序可以按需要分配資源,而(通常)無須擔(dān)心何時釋放這些資源。

例如,下面代碼片段定義了四個sales_data對象:

{//新作用域
//p和p2指向動態(tài)分配的對象
Sales_data *p=new Sales_data;//p是一個內(nèi)置指針
auto p2=make_shared<Sales_data>();//p2是一個shared_ptr
Sales_data item(*p);//指貝構(gòu)造函數(shù)將*p拷貝到item中
vector<Sales_data>vec;//局部對象
vec.push_back(*p2);//拷貝p2指向的對象
delete p;//對p指向的對象執(zhí)行析構(gòu)函數(shù)

}//退出局部作用域;對item、p2和vec調(diào)用析構(gòu)函數(shù)
//銷毀p2會途減其引用計數(shù);如果引用計數(shù)變?yōu)?,對象被釋放
//銷毀vec會銷鱷它的元素

每個Sales_data對象都包含一個string成員,它分配動態(tài)內(nèi)存婁保存bookNo成員中的字符。但是,我們的代碼唯一需要直接管理的內(nèi)存就是我們直接分配的Sales_data對象。我們的代碼只需直接釋放綁定到p的動態(tài)分配對象。

其他Sales_data對象會在離開作用域時被自動銷毀。當(dāng)程序塊結(jié)束時,vec、p2和item都離開了作用域,意味著在這些對象上分別會執(zhí)行vector、shared_ptr和Sales_data的析構(gòu)函數(shù)。vector的析構(gòu)函數(shù)會銷毀我們添加到vec的元素。shared_ptr的析構(gòu)函數(shù)會遞減p2指向的對象的引用計數(shù)。在本例中,引用計數(shù)會變?yōu)?,因此shared_ptz的析構(gòu)函數(shù)會delete p2分配的Sales_data對象。

在所有情況下,Sales_data的析構(gòu)函數(shù)都會隱式地銷毀bookNo成員.銷毀bookNo會調(diào)用string的析構(gòu)函數(shù),它會釋放用來保存ISBN的內(nèi)存。

當(dāng)指向一個對象的引用或指針離開作用域時,析構(gòu)函數(shù)不會執(zhí)行。

合成析構(gòu)函數(shù)

當(dāng)一個類未定義自己的析構(gòu)函數(shù)時,編詳器會為它定義一個合成析構(gòu)函數(shù)(synthesized destructor)。類似拷貝構(gòu)造函數(shù)和拷貝賦值運算符,對于樹些類,合成析構(gòu)函數(shù)被用來阻止該類型的對象被銷毀。如果不是這種情況,合成析構(gòu)函數(shù)的函數(shù)體就為空。

例如,下面的代碼片段等價于Sales_data的合成析構(gòu)函數(shù):

class Sales_data{
public:
    //成員會被自動銷毀,除此之外不需要做其他事情
    ~Sales_data()《
    //其他成員的定義,如前
}

在(空)析構(gòu)函數(shù)體執(zhí)行完畢后,成員會被自動銷毀。特別的,string的析構(gòu)函數(shù)會被調(diào)用,它將釋放bookNo成員所用的內(nèi)存。

認(rèn)識到析構(gòu)函數(shù)體自身并不直接銷毀成員是非常重要的。成員是在析構(gòu)函數(shù)體之后隱含的析構(gòu)階段中被銷毀的。在整個對象銷毀過程中,析構(gòu)函數(shù)體是作為成員銷毀步驟之外的另一部分而進行的。

三/五法則

如前所述,右三個基本操作可以控制類的拷貝操作:拷貝構(gòu)造函數(shù),拷貝賦值運算符和析構(gòu)函數(shù)。而且,在新標(biāo)準(zhǔn)下,一個類還可以定義一個移動構(gòu)造函數(shù)和一個移動賦值運算符。

C++語言并不要求我們定義所有這些操作:可以只定義其中一個或兩個,而不必定義所有。但是,這些操作通常應(yīng)該被看做一個整體。通常,只需要其中一個操作,而不需要定義所有操作的情況是很少見的。

需要析構(gòu)函數(shù)的類也需要拷貝和賦值操作

當(dāng)我們決定一個類是否要定義它自己版本的拷貝控制成員時,一個基本原則是首先確定這個惡類是否需要一個析構(gòu)函數(shù)。通常,對析構(gòu)函數(shù)的需求要比對拷貝構(gòu)造函數(shù)或賦值運算符的需求更為明顯。如果這個類需要一個析構(gòu)函數(shù),我們幾乎可以肯定它也需要一個拷貝構(gòu)造函數(shù)和一個拷貝賦值運算符。

我們在練習(xí)中用過的HasPtr類是一個好例子。這個類在構(gòu)造函數(shù)中分配動態(tài)內(nèi)存。合成析構(gòu)函數(shù)不會delete一個指針數(shù)據(jù)成員。因此,此類需要定義一個析構(gòu)函數(shù)來釋放構(gòu)造函數(shù)分配的內(nèi)存。

應(yīng)該怎么做可能還有點兒不清晰,但基本原則告訴我們,HasPtr也需要一個拷貝構(gòu)造函數(shù)和一個拷貝賦值運算符。

如果我們?yōu)镠asPtr定義一個析構(gòu)函數(shù),但使用合成版本的拷貝構(gòu)造函數(shù)和拷貝賦值運算符,考慮會發(fā)生什么:

class HasPtr{
public:
HasPtr(const std:;string&s=std::string()):
ps(new std::string(s)),i(0){}
~HasPtr(){deleteps;
//錯誤:HasPtr需要一個拷貝構(gòu)造函數(shù)和一個拷貝賦值運算符
//其他成員的定義,如前
};

在這個版本的類定義中,構(gòu)造函數(shù)中分配的內(nèi)存將在HasPtr對象銷毀時被釋放。但不幸的是,我們引入了一個嚴(yán)重的錯誤!這個版本的類使用了合成的拷貝構(gòu)造函數(shù)和拷貝賦值運算符。這些函數(shù)簡單拷貝指針成員,這意味著多個HasPtr對象可能指向相同的內(nèi)存:

HasPtr f(HasPtr hp)// HasPtr是傳值參數(shù),所以將被指貝
{
    HasPtr ret=hp;//拷貝給定的HasPt
    //處理ret
    return ret;//ret和hp被銷毀
}

當(dāng)f返回時,hp和ret都被銷毀,在兩個對象上都會調(diào)用HasPtr的析構(gòu)函數(shù)。此析構(gòu)函數(shù)會delete ret和hp中的指針成員。但這兩個對象包含相同的指針值。此代碼會導(dǎo)致此指針被delete兩次,這顯然是一個錯誤。將要發(fā)生什么是未定義的。

此外,f的調(diào)用者還會使用傳遞給f的對象:

```cpp
HasPtr("some values");
f(p);//當(dāng)王結(jié)束時,p指向的肉存被釋放
HasPtr(p);//現(xiàn)在p和q都指向無效內(nèi)存!

p以及q指向的內(nèi)存不再有效,在hp(或ret!)銷毀時它就被歸還給系統(tǒng)了。

需要拷貝操作的類也需要賦值操作,反之亦然

雖然很多類需要定義所有(或是不需要定義任何)拷貝控制成員,但某些類所要完成的工作,只需要拷貝或賦值操作,不需要析構(gòu)函數(shù)。作為一個例子,考慮一個類為每個對象分配一個獨有的、唯一的序號。這個類需要一個拷貝構(gòu)造函數(shù)為每個新創(chuàng)建的對象生成一個新的、獨一無二的序號。除此之外,這個拷貝構(gòu)造函數(shù)從給定對象拷貝所有其他數(shù)據(jù)成員。這個類還需要自定義拷貝賦值運算符來邀

免將序號賦予目的對象。但是,這個類不需要自定義析構(gòu)函數(shù)。

這個例子引出了第二個基本原則:如果一個類需要一個拷貝構(gòu)造函數(shù),幾乎可以肯定它也需要一個拷貝賦值運算符。反之亦然一一如果一個類需要一個拷貝賦值運算符,幾乎可以肯定它也需要一個拷貝構(gòu)造函數(shù)。然而,無論是需要拷貝構(gòu)造函數(shù)還是需要拷貝賦值運算符都不必然意味著也需要析構(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ù)將隱式地聲明為內(nèi)聯(lián)的(就像任何其他類內(nèi)聲明的成員函數(shù)一樣)。如果我們不希望合成的成員是內(nèi)聯(lián)函數(shù),應(yīng)該只對成員的類外定義使用=default,就像對拷貝賦值運算符所做的那樣。

阻止拷貝

雖然大多數(shù)類應(yīng)該定義(而且通常也的確定義了)拷貝構(gòu)造函數(shù)和拷貝賦值運算符,但對某些類來說,這些操作沒有合理的意義。在此情況下,定義類時必須采用某種機制阻止拷貝或賦值。例如,iostream類阻止了拷貝,以避免多個對象寫入或讀取相同的IO緩沖。為了阻止拷貝,看起來可能應(yīng)該不定義拷貝控制成員。但是,這種策略是無效的:如果我們的類未定義這些操作,編譯器為它生成合成的版本。

定義刪除的函數(shù)

在新標(biāo)準(zhǔn)下,我們可以通過將拷貝構(gòu)造函數(shù)和拷貝賦值運算符定義為刪除的函數(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òu)函數(shù)的類型,編譯器將不允許定義該類型的變量或創(chuàng)建該類的臨時對象。而且,如果一個類有某個成員的類型刪除了析構(gòu)函數(shù),我們也不能定義該類的變量或臨時對象。因為如果一個成員的析構(gòu)函數(shù)是刪除的,則該成員無法被銷毀。而如果一個成員無法被銷毀,則對象整體也就無法被銷毀了。

對于刪除了析構(gòu)函數(shù)的類型,雖然我們不能定義這種類型的變量或成員,但可以動態(tài)分配這種類型的對象。但是,不能釋放這些對象:

struct NoDtor{
NoDtor()=default;//使用合成默認(rèn)構(gòu)造函數(shù)
~NoDtor()=delete;//我們不能銷毀NoDtor類型的對象
NoDtor nd;//錯誤:NoDtor的析構(gòu)函數(shù)是剛除的
NoDtor*p=new NoDptor();//正確:但我們不能delete p
delete p;//錯誤:NoDtor的析構(gòu)函數(shù)是剔除的
}

合成的拷貝控制成員可能是刪除的

如前所述,如果我們未定義拷貝控制成員,編譯器會為我們定義合成的版本。類似的,如果一個類未定義構(gòu)造函數(shù),編譯器會為其合成一個默認(rèn)構(gòu)造函數(shù)。對某些類來說,編譯器將這些合成的成員定義為刪除的函數(shù):

  • 如果類的某個成員的析構(gòu)函數(shù)是刪除的或不可訪問的(例如,是private的),則類的合成析構(gòu)函數(shù)被定義為刪除的。
  • 如果類的某個成員的拷貝構(gòu)造函數(shù)是刪除的或不可訪問的,則類的合成拷貝構(gòu)造函數(shù)被定義為刪除的。
  • 如果類的某個成員的析構(gòu)函數(shù)是刪除的或不可訪問的,則類合成的拷貝構(gòu)造函數(shù)也被定義為刪除的。
  • 如果類的樹個成員的拷貝賦值運算符是刪除的或不可訪問的,或是類有一個const的或引用成員,則類的合成拷貝賦值運算符被定義為刪除的。
  • 如果類的某個成員的析構(gòu)函數(shù)是刪除的或不可訪問的,或是類有一個引用成員,它沒有類內(nèi)初始化器,或是類有一個const成員,它沒有類內(nèi)初始化器且其類型未顯式定義默認(rèn)構(gòu)造函數(shù),則該類的默認(rèn)構(gòu)造函數(shù)被定義為刪除的。

本質(zhì)上,這些規(guī)則的含義是:如果一個類有數(shù)據(jù)成員不能默認(rèn)構(gòu)造、拷貝、復(fù)制或銷毀,則對應(yīng)的成員函數(shù)將被定義為刪除的。

一個成員有刪除的或不可訪問的析構(gòu)函數(shù)會導(dǎo)致合成的默認(rèn)和拷貝構(gòu)造函數(shù)被定義為刪除的,這看起來可能有些奇怪。其原因是,如果沒有這條規(guī)則,我們可能會創(chuàng)建出無法銷毀的對象。

對于具有引用成員或無法默認(rèn)構(gòu)造的const成員的類,編詳器不會為其合成默認(rèn)構(gòu)造函數(shù),這應(yīng)該不奇怪。同樣不出人意料的規(guī)則是:如果一個類有const成員,則它不能使用合成的拷貝賦值運算符。畢竟,此運算符試圖賦值所有成員,而將一個新值賦予一個const對象是不可能的。

雖然我們可以將一個新值賦予一個引用成員,但這樣做改變的是引用指向的對象的值,而不是引用本身。如果為這樣的類合成拷貝賦值運算符,則賦值后,左側(cè)運算對象仍然指向與賦值前一樣的對象,而不會與右側(cè)運算對象指向相同的對象。由于這種行為看起來并不是我們所期望的,因此對于有引用成員的類,合成拷貝賦值運算符被定義為刪除的。

本質(zhì)上,當(dāng)不可能拷貝、賦值或銷毀類的成員時,類的合成拷貝控制成員就被定義為刪除的。

private拷貝控制

在新標(biāo)準(zhǔn)發(fā)布之前,類是通過將其拷貝構(gòu)造函數(shù)和拷貝賦值運算符聲明為private的來阻止拷貝:

class PrivateCopy{
//無訪問說明符;接下來的成員默認(rèn)為private的;
//拷貝控制成員是private的,因此普通用戶代碼無法訪問
PrivateCopy(const PrivateCopy&);
PrivateCopy&operator=(const PrivateCopy&);
//其他成員
public:
PrivateCopy()=default;//使用合成的默認(rèn)構(gòu)造出數(shù)
~PrivateCopy();//用戶可以定義此類型的對象,但無法拷貝它們
}

由于析構(gòu)函數(shù)是public的,用戶可以定義PrivateCopy類型的對象。但是,由于拷貝構(gòu)造函數(shù)和拷貝賦值運算符是private的,用戶代碼將不能拷貝這個類型的對象。但是,友元和成員函數(shù)仍舊可以拷貝對象。為了阻止友元和成員函數(shù)進行拷貝,我們將這些拷貝控制成員聲明為private的,但并不定義它們。

聲明但不定義一個成員函數(shù)是合法的,對此只有一個例外。試圖訪問一個未定義的成員將導(dǎo)致一個鏈接時錯誤。通過聲明(但不定義)private的拷貝構(gòu)造函數(shù),我們可以預(yù)先阻止任何拷貝該類型對象的企圖:試圖拷貝對象的用戶代碼將在編譯階段被標(biāo)記為錯誤;成員函數(shù)或友元函數(shù)中的拷貝操作將會導(dǎo)致鏈接時錯誤。

總結(jié)

以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • VC對自定義資源加密解密(AES)的詳解

    VC對自定義資源加密解密(AES)的詳解

    本篇文章是對VC對自定義資源加密解密(AES)進行了詳細的分析介紹,需要的朋友參考下
    2013-06-06
  • vscode cmake compilers配置路徑的實現(xiàn)

    vscode cmake compilers配置路徑的實現(xiàn)

    本文主要介紹了vscode cmake compilers配置路徑的實現(xiàn),文中通過圖文介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2024-03-03
  • c++實現(xiàn)十進制轉(zhuǎn)換成16進制示例

    c++實現(xiàn)十進制轉(zhuǎn)換成16進制示例

    這篇文章主要介紹了c++實現(xiàn)十進制轉(zhuǎn)換成16進制示例,需要的朋友可以參考下
    2014-05-05
  • 基于c++11的event-driven library的理解

    基于c++11的event-driven library的理解

    這篇文章主要介紹了基于c++11的event-driven library的理解,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-02-02
  • C++利用鏈表實現(xiàn)圖書信息管理系統(tǒng)

    C++利用鏈表實現(xiàn)圖書信息管理系統(tǒng)

    這篇文章主要為大家詳細介紹了C++利用鏈表實現(xiàn)圖書信息管理系統(tǒng),文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-11-11
  • C語言實現(xiàn)簡易訂餐系統(tǒng)

    C語言實現(xiàn)簡易訂餐系統(tǒng)

    這篇文章主要為大家詳細介紹了C語言實現(xiàn)簡易訂餐系統(tǒng),文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-06-06
  • C++回溯算法廣度優(yōu)先搜索舉例分析

    C++回溯算法廣度優(yōu)先搜索舉例分析

    回溯在迷宮搜索中使用很常見,就是這條路走不通,然后返回前一個路口,繼續(xù)下一條路?;厮菟惴ㄕf白了就是窮舉法,下面讓我們一起來看看吧
    2022-03-03
  • C++虛函數(shù)注意事項

    C++虛函數(shù)注意事項

    這篇文章主要給大家分享了EC++虛函數(shù)注意事項,
    2022-01-01
  • C語言中的運算符和結(jié)合性問題

    C語言中的運算符和結(jié)合性問題

    這篇文章主要介紹了C語言中的運算符和結(jié)合性問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-03-03
  • 詳解C語言中return與exit的區(qū)別

    詳解C語言中return與exit的區(qū)別

    這篇文章主要介紹了詳解C語言中return與exit的區(qū)別的相關(guān)資料,希望通過本文能幫助到大家,讓大家理解這部分內(nèi)容,需要的朋友可以參考下
    2017-10-10

最新評論