C++ Primer 筆記-第13章 拷貝控制

第13章 拷貝控制
拷貝構(gòu)造函數(shù)(copy constructor)?和?移動構(gòu)造函數(shù)(move constructor)?定義了當用同類型的另一個對象初始化本對象時做什么。
拷貝賦值運算符(copy-assignment operator)?和?移動賦值運算符(move-assignment operator)?定義了將一個對象賦予同類型的另一個對象時做什么。
析構(gòu)函數(shù)(destructor)?定義了當此類型對象銷毀時做什么。
如果一個類沒有定義這些拷貝控制成員,編譯器會自動為它定義缺失的操作。

13.1 拷貝、賦值與銷毀
13.1.1 拷貝構(gòu)造函數(shù)
如果一個構(gòu)造函數(shù)的第一個參數(shù)是自身類類型的引用,且任何額外參數(shù)都有默認值,則此構(gòu)造函數(shù)是拷貝構(gòu)造函數(shù):

class Foo(){
public: ? ?
????Foo(); ? ? ? ? ? ????????// 默認構(gòu)造函數(shù) ? ?
????Foo(const Foo&); // 拷貝構(gòu)造函數(shù)
? ?// ...
}

如果我們沒有為一個類定義拷貝構(gòu)造函數(shù),即使我們定義了其他構(gòu)造函數(shù),編譯器也會為我們合成一個拷貝構(gòu)造函數(shù)。從給定對象中依次將每個非?
static
?成員拷貝到正在創(chuàng)建的對象中。每個成員的類型決定了它如何拷貝:對類類型的成員會使用其拷貝構(gòu)造函數(shù)來拷貝,內(nèi)置類型的成員則直接拷貝。

class Sales_data{
public:
? ?Sales_data(const Sales_data&);
? ?// ... ...
private:
? ?std::string bookNo;
? ?int units_sold = 0;
? ?double revenue = 0.0;
}
// 自己定義,或讓編譯器合成:
Sales_data::Sales_data(const Sales_data& orig)
? ?: bookNo(orig.bookNo), ? ? ? ? ?// 使用string的拷貝構(gòu)造函數(shù)
? ?units_sold(orig.units_sold), ? ?// 直接拷貝
? ?revenue(orig.revenue) ? ? ? ? ? // 直接拷貝
? ?{ ? } ?

當使用?直接初始化?時,實際上是要求編譯器使用?普通?的函數(shù)匹配來選擇與我們提供的參數(shù)最匹配的構(gòu)造函數(shù)。
當我們使用?拷貝初始化?時,要求編譯器使用?拷貝構(gòu)造函數(shù)?將右側(cè)運算對象拷貝到正在創(chuàng)建的對象中,如果需要的話還要進行類型轉(zhuǎn)換。

// 直接初始化
string dots(10, ‘.’);
string s(dots);
// 拷貝初始化
string s2 = dots;
string null_book = "9-9-9";
string nines = string(10, '9');

拷貝初始化不僅在用?
=
?定義變量時會發(fā)生,在下列情況下也會發(fā)生:(1)將一個對象作為實參傳遞給一個非引用類型的形參
(2)從一個返回類型為非引用類型的函數(shù)返回一個對象
(3)用花括號列表初始化一個數(shù)組中的元素或一個聚合類中的成員。如果?拷貝構(gòu)造函數(shù)的參數(shù)?不是?引用類型(而是值類型),則調(diào)用永遠也不會成功——為了調(diào)用拷貝構(gòu)造函數(shù),我們必須拷貝它的實參,但為了拷貝實參,我們又需要調(diào)用拷貝構(gòu)造函數(shù),如此無限循環(huán)。
13.1.2 拷貝賦值運算符
重載運算符(overloaded operator)的本質(zhì)是函數(shù),其名字由?
operator
?關(guān)鍵字后接表示要定義的運算符的符號組成 (比如operatro=
)。如果一個運算符是一個成員函數(shù),其左側(cè)運算對象就綁定到隱式的?
this
?參數(shù)。對于一個二元運算符,其右側(cè)運算對象作為顯示參數(shù)傳遞。為了與內(nèi)置類型的賦值保持一直,賦值運算符通常應(yīng)該返回一個指向其左側(cè)運算對象的引用(連續(xù)拷貝時需要:
a = b = c
)。標準庫通常要求保存在容器中的類型要具有賦值運算符,且其返回值是左側(cè)運算對象的引用。

class Sales_data{
public:
? ?// ... ... ? Sales_data& operator=(const Sales_data& rhs);
private:
? ?std::string bookNo;
? ?int units_sold = 0;
? ?double revenue = 0.0;
}
Sales_data& Sales_data::operator=(const Sales_data& rhs){
? ?bookNo = rhs.bookNo; ? ? ? ? ? ?// 調(diào)用string::operator=
? ?units_sold = rhs.units_sold; ? ?// 使用內(nèi)置類型的賦值
? ?revenue = rhs.revenue;
? ?return *this; ? ? ? ? ? ? ? ? ? // 返回左側(cè)對象的引用
}

13.1.3 析構(gòu)函數(shù)
析構(gòu)函數(shù)釋放對象使用的資源,并銷毀對象的非?
static
?數(shù)據(jù)成員。由于析構(gòu)函數(shù)不接受參數(shù),因此不能被重載。對于一個給定類,只會有唯一一個析構(gòu)函數(shù)。
析構(gòu)過程中首先執(zhí)行函數(shù)體,然后銷毀成員。成員按初始化順序的逆序銷毀。
析構(gòu)函數(shù)體自身并不直接銷毀成員,成員是在析構(gòu)函數(shù)體之后隱含的析構(gòu)階段中被銷毀的。
在析構(gòu)函數(shù)中,不存在類似構(gòu)造函數(shù)中初始化列表的東西來控制成員如何銷毀,析構(gòu)部分是隱式的。
銷毀類類型的成員需要執(zhí)行成員自己的析構(gòu)函數(shù)。內(nèi)置類型沒有析構(gòu)函數(shù),因此銷毀內(nèi)置類型成員什么也不需要做。
隱式銷毀一個內(nèi)置指針類型的成員不會?
delete
?它所指向的對象。(智能指針是類類型,所以在析構(gòu)階段被自動銷毀時會根據(jù)情況delete
所指的對象。)當指向一個對象的引用或指針離開作用域時,析構(gòu)函數(shù)不會執(zhí)行。
13.1.4 三/五法則
如果一個類需要自定義析構(gòu)函數(shù),幾乎可以肯定它也需要自定義拷貝賦值運算符和拷貝構(gòu)造函數(shù)。(例如動態(tài)分配對象的銷毀帶來的析構(gòu)和拷貝問題)
如果一個類需要一個拷貝構(gòu)造函數(shù),幾乎可以肯定它也需要一個拷貝賦值運算符,反之亦然。
然而無論是需要拷貝構(gòu)造函數(shù)還是需要拷貝賦值運算符都不必然意味著也需要析構(gòu)函數(shù)。
13.1.5 使用?=default
通過將拷貝控制成員定義為?
=default
?來顯示地要求編譯器生成合成的版本。在類內(nèi)用?
=default
?修飾成員的聲明時,合成的函數(shù)將隱式地聲明為內(nèi)聯(lián)的。如果不希望合成的成員是內(nèi)聯(lián)函數(shù),應(yīng)該只對成員的類外定義使用?
=default
。只能對具有合成版本的成員函數(shù)使用?
=default
?(即默認構(gòu)造函數(shù)或拷貝構(gòu)造函數(shù))。

class Sales_data{
public:
? ?Sales_data() = default; ? ? ? ? // 內(nèi)聯(lián)
? ?Sales_data(const Sales_data&); ?// 外聯(lián)
}
Sales_data::Sales_data(const Sales_data&) = default;

13.1.6 阻止拷貝
新標準下,可以通過將拷貝構(gòu)造函數(shù)和拷貝賦值運算符定義為?刪除的函數(shù)(deleted function)?來阻止拷貝。
與?
=default
?不同,=delete
?必須出現(xiàn)在函數(shù)第一次聲明的時候。編譯器需要知道一個函數(shù)是刪除的,以便禁止試圖使用它的操作。一個默認的成員只影響為這個成員而生成的代碼,因此?
=default
?直到編譯器生成代碼時才需要。與?
=default
?的另一個不同之處是,我們可以對任何函數(shù)指定?=delete
?,相對的我們只能對編譯器可以合成的默認構(gòu)造函數(shù)或拷貝控制成員使用?=default
。

struct NoCopy{
? ?NoCopy() = default; ? ? ? ? ? ?// 使用合成的默認構(gòu)造函數(shù)
? ?NoCopy(const NoCopy&) = delete; ? ? ? ? ? ? // 阻止拷貝
? ?NoCopy &operator=(const NoCopy&) = delete; ?// 阻止賦值
? ?~NoCopy() = default; ? ? ? ? ? ? ? // 使用合成的析構(gòu)函數(shù)
? ?// 其他成員... ...
};

析構(gòu)函數(shù)不能是刪除的成員,否則無法銷毀類型的對象了。
對于刪除了析構(gòu)函數(shù)的類型,雖然不能直接定義這種類型的變量和成員,但可以動態(tài)分配這種類型的對象。但是不能釋放這些對象。
對于析構(gòu)函數(shù)已刪除的類型,不能定義該類型的變量或釋放指向該類型動態(tài)分配對象的指針。

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

本質(zhì)上,當不可能拷貝、賦值或銷毀類的成員時,類的合成拷貝控制成員就被定義為刪除的。
希望阻止拷貝的類應(yīng)該使用?
=delete
?來定義它們自己的拷貝構(gòu)造函數(shù)和拷貝賦值運算符,而不應(yīng)該將它們聲明為?private
的(舊標準時使用的方法)。

13.2 拷貝控制和資源管理
類的行為像一個值,當拷貝一個像值的對象時,副本和原對象是完全獨立的。改變副本不會對原對像有任何影響。
行為像指針的類則共享狀態(tài)。當拷貝時,副本和原對象使用相同的底層數(shù)據(jù)。改變副本也會改變原對象。
13.2.1 行為像值的類
對于一個賦值運算符來說,即使是將一個對象賦予它自身,也要能正確工作。一個好的方法是在銷毀左側(cè)運算對象資源之前拷貝右側(cè)運算對象。

class HasPtr{
public: ? ?// 對ps指向的string,每個HasPtr對象都有自己的拷貝
? ?HasPtr(const HasPtr& p)
? ? ? ?: ps(new std::string(*p.ps)), i(p.i) {}
? ?HasPtr& operator=(const HasPtr&);
? ?~HasPtr() { delete ps; }
private:
? ?std::string *ps; ? ?// 動態(tài)分配內(nèi)存
? ?int i;
}
HasPtr& HasPtr::operator=(const HasPtr& rhs){
? ?// 錯誤:如果rhs和*this是同一個對象
? ?// 我們就將從已釋放的內(nèi)存中拷貝數(shù)據(jù)!
? ?delete ps; ?// 釋放對象指向的string
? ?ps = new string(*rhs.ps);
? ?// 正確:
? ?auto newp = new string(*rhs.ps); ? ?// 拷貝底層string
? ?delete ps; ? ? ? ? ? ? ? ? ? ? ? ? ?// 釋放舊內(nèi)存
? ?i = rhs.i;
? ?return *this;
}

13.2.2 定義行為像指針的類
類比?
shared_ptr
,我們?yōu)榱酥苯庸芾碣Y源,需要自己設(shè)計引用計數(shù)。將計數(shù)器保存在動態(tài)內(nèi)存中。

class HasPtr{
public:
? ?// 拷貝構(gòu)造函數(shù)拷貝所有三個數(shù)據(jù)成員,并遞增計數(shù)器
? ?HasPtr(const HasPtr& p)
? ? ? ?: ps(p.ps), i(p.i), use(p.use) { ++*use; }
? ?HasPtr& Operator=(const HasPtr&);
? ?~HasPtr();
private:
? ?std::string *ps;
? ?int i;
? ?std::size_t *use; ? // 用來記錄有多少個對象共享*ps的成員
}
HasPtr::~HasPtr(){
? ?// 如果引用計數(shù)變?yōu)?,釋放成員的動態(tài)內(nèi)存
? ?if(--*use = 0)?{
? ? ? ?delete ps;
? ? ? ?delete use;
? ?}
}
HasPtr& HasPtr::operator=(const HasPtr& rhs){
? ?// 先遞增rhs中的計數(shù)然后再遞減左側(cè)運算對象中的計數(shù),避免自賦值
? ?++*rhs.use;
? ?if(--*use == 0)?{
? ? ? ?delete ps;
? ? ? ?delete use;
? ?}
? ?// 逐成員拷貝
? ?ps = rhs.ps;
? ?i = rhs.i;
? ?use = rhs.use;
? ?return *this; ? // 返回本(左側(cè))對象
}


13.3 交換操作
編寫?
HasPtr
?類自己的?swap
?函數(shù)(通過交換成員變量指針來優(yōu)化代碼)。與拷貝控制成員不同,
swap
?并不是必要的。但是,對于分配了資源的類,定義?swap
?可能是一種很重要的優(yōu)化手段。

class HasPtr?{
? ?// 為了訪問private成員,將函數(shù)定義為friend
? ?friend void swap(HasPtr&, HasPtr&);?
? ?// 其他成員...
};
// 為了優(yōu)化代碼將其聲明為內(nèi)聯(lián)函數(shù)
inline void swap(HasPtr& lhs, HasPtr& rhs)?{
? ?using std::swap;
? ?swap(lhs.ps, rhs.ps); ? ?// 交換指針,而不是string數(shù)據(jù)
? ?swap(lhs.i, rhs.i); ? ? ?// 交換int成員
}
// 假定有一個Foo類,它有一個類型為HasPtr的成員h
// 為Foo編寫一個swap函數(shù)
void swap(Foo& lhs, Foo& rhs)?{
? ?// 錯誤:這個函數(shù)使用了標準庫版本的swap,而不是HasPtr版本
? ?std::swap(lhs.h, rhs.h);
? ?// 正確:使用HasPtr版本的swap
? ?swap(lhs.h, rhs.h);
? ?// 交換類型Foo的其他成員
}

定義?
swap
?的類通常用?swap
?來定義它們的賦值運算符。使用一種名為?拷貝并交換(copy and swap)?的技術(shù),將左側(cè)運算對象與?右側(cè)運算對象的一個副本?進行交換。使用拷貝和交換的賦值運算符自動就是異常安全的,且能正確處理自賦值。

// rhs是值傳遞
HasPtr& HasPtr::operator=(HasPtr rhs)?{
? ?// 交換左側(cè)運算對象和局部變量rhs的內(nèi)容
? ?swap(*this, rhs); ? // rhs現(xiàn)在指向本對象曾經(jīng)使用的內(nèi)存
? ?return *this; ? ? ? // rhs被銷毀,從而delete了rhs中的指針
}


13.4 拷貝控制示例
雖然通常來說分配資源的類更需要拷貝控制,但資源管理并不是一個類需要定義自己的拷貝控制成員的唯一原因。一些類也需要拷貝控制成員的幫助來進行薄記工作或其他操作。
拷貝賦值運算符?通常執(zhí)行?拷貝構(gòu)造函數(shù)?和?析構(gòu)函數(shù)?中也要做的工作。這種情況下,公共的工作應(yīng)該放在?
private
?的工具函數(shù)中完成。

13.5 動態(tài)內(nèi)存管理類
某些類需要在運行時分配可變大小的內(nèi)存空間,這種類通??梢允褂脴藴蕩烊萜鱽肀4嫠鼈兊臄?shù)據(jù)。但是這一策略并不是對每個類都適用,某些類需要自己進行內(nèi)存分配,這些類一般來說必須定義自己的拷貝控制成員來管理所分配的內(nèi)存。
類?
strVec
?將模仿?vector<string>
?的功能。我們將使用一個?allocator
?來獲取原始內(nèi)存(未構(gòu)造)。在需要添加新元素時用?allocator
?的?construct
?成員在原始內(nèi)存中創(chuàng)建對象。在需要刪除一個元素時,我們將使用?destroy
?成員來銷毀元素。使用?
string
?的?移動構(gòu)造函數(shù)?來避免在重新分配內(nèi)存空間時因拷貝?string
?所帶來的分配和釋放的額外開銷,strVec
?的性能會好得多。

13.6 對象移動
在重新分配內(nèi)存的過程中,從舊內(nèi)存將元素拷貝到新內(nèi)存是不必要的,更好的方式是移動元素。
使用移動而非拷貝的另一個原因源于IO類或?
unique_ptr
?這樣的類。這些類都包含不能被共享的資源(如指針或IO緩沖)。因此這些類型的對象不能拷貝但可以移動。在舊C++標準中,沒有直接的方法移動對象。因此,即使不必拷貝對象的情況下,也不得不拷貝。
標準庫容器、
string
?和?shared_ptr
?類既支持移動也支持拷貝。IO類和?unique_ptr
?類可以移動但是不能拷貝。我理解的?對象移動的本質(zhì)?其實就是對象內(nèi)存地址所有權(quán)的轉(zhuǎn)移。之所以需要分出左/右值,就是由于左值對象的所有權(quán)不唯一,如果移動的是左值對象,接下來再使用左值對象的值時會出錯。而右值對象的所有權(quán)唯一,當移動的是右值對象時,就應(yīng)該確保接下來不會有任何地方再次調(diào)用這個右值對象(這也就是為什么要遵守即使使用?
move
?強行將左值轉(zhuǎn)成了右值也不能再調(diào)用移后源對象的規(guī)定)。
13.6.1 右值引用
為了支持移動操作,新標準引入了右值引用(rvalue reference)——必須綁定到右值的引用。通過 && 而不是 & 來獲得右值引用。
我們不能將左值引用綁定到要求轉(zhuǎn)換的表達式、字面常量或返回右值的表達式。右值引用則相反,我們可以將其綁定在這類表達式上。但是不能將一個右值引用直接綁定到一個左值上:

int i = 42;
int& r = i; ? ? ? ? ? ? // 正確:r左值引用i
int&& rr = i; ? ? ? ? ? // 錯誤:不能將一個右值引用綁定到一個左值上
int& r2 = i * 42; ? ? ? // 錯誤:i*42是一個右值
const int& r3 = i * 42; // 正確:可以將一個const的左值引用綁定到一個右值上
int&& rr2 = i * 42; ? ? // 正確:將右值引用綁定到右值上

返回左值引用的函數(shù),連同賦值、下標、解引用和前置遞增/遞減運算符,都是返回左值的表達式的例子。
返回非引用類型的函數(shù),連同算數(shù)、關(guān)系、位以及后置遞增/遞減運算符,都生成右值。
右值引用要么是字面值常量,要么是在表達式求值過程中創(chuàng)建的將要被銷毀的臨時對象。因此,右值引用的代碼可以自由地接管所引用的對象的資源。
變量表達式都是左值,我們不能將一個右值引用綁定到一個右值引用類型的變量上:

int&& rr1 = 42; ? ? // 正確:字面常量42是右值,rr1是右值引用類型
int&& rr2 = rr1; ? ?// 錯誤:雖然rr1是右值引用類型,但是rr1是左值
// 變量(rr1)是左值,因此不能將一個右值引用(rr2)直接綁定到一個變量(rr1)上,
// 即使這個變量(rr1)是右值引用類型也不行。

雖然不能將一個右值引用直接綁定到一個左值上,但是可以通過調(diào)用?
move
?標準庫函數(shù)顯示地將一個左值轉(zhuǎn)換為對應(yīng)的右值引用類型。

int&& rr3 = std::move(rr1); // 正確
// 調(diào)用move就意味著對于rr1:可以銷毀這個移后源對象,也可以賦予它新值,
// 但是不能使用這個移后源對象的值。

使用?
move
?的代碼應(yīng)該使用?std::move
?而不是?move
。這樣做可以避免潛在的名字沖突。
13.6.2 移動構(gòu)造函數(shù)和移動賦值運算符
為了讓類支持移動操作,需要為其定義移動構(gòu)造函數(shù)和移動賦值運算符。
除了完成資源移動,移動構(gòu)造函數(shù)還必須確保移后源對象能夠被銷毀。移后源對象必須不再指向被移動的資源-此時這些資源的所有權(quán)已經(jīng)歸屬新創(chuàng)建的對象。

StrVec::StrVec(StrVec&& s) noexcept
? ?// 成員初始化器接管s中的資源
? ?:elements(s.elements), first_free(s.first_free), cap(s.cap)
{
? ?// 令s進入這樣的狀態(tài)-對其運行析構(gòu)函數(shù)是安全的。
? ?s.elements = s.first_free = s.cap = nullptr;
? ?// StrVec的析構(gòu)函數(shù)在first_free上調(diào)用deallocate。
? ?// 如果忘記了改變s.first_free,
? ?// 則銷毀移后源對象就會釋放掉我們剛剛移動的內(nèi)存。
}
// noexcept 通知標準庫此構(gòu)造函數(shù)不拋出任何異常。

移動賦值運算符執(zhí)行析構(gòu)函數(shù)和移動構(gòu)造函數(shù)相同的工作,不拋出異常,且要正確處理自賦值情況
移動賦值運算符去檢查自賦值情況有些奇怪,因為移動賦值運算符需要右側(cè)運算符對象的一個右值。之所以檢查的原因是此右值可能是?
move
?調(diào)用的返回結(jié)果。在移動操作之后,移后源對象必須保持有效的、可析構(gòu)的狀態(tài),但用戶不能對其值進行任何假設(shè)。
只有當一個類沒有定義任何自己版本的拷貝控制成員,且類的每個非?
static
?數(shù)據(jù)成員都能移動構(gòu)造或移動賦值時,編譯器才會為它合成移動構(gòu)造函數(shù)或移動賦值運算符。定義了一個移動構(gòu)造函數(shù)或移動賦值運算符的類必須也定義自己的拷貝操作。否則,這些成員默認地被定義為刪除的。
更新“三/五法則”:一般來說,如果一個類定義了任何一個拷貝操作,它就應(yīng)該定義所有的五個操作。某些類必須定義拷貝構(gòu)造函數(shù)、拷貝賦值運算符和析構(gòu)函數(shù)才能正確工作。這些類通常擁有一個資源,而拷貝成員必須拷貝此資源??截愐粋€資源會導(dǎo)致一些額外的開銷。在這種拷貝并非必要的情況下,定義了移動構(gòu)造函數(shù)和移動賦值運算符的類就避免此問題。
一個移動迭代器通過改變給定迭代器的解引用運算符的行為來適配此迭代器,移動迭代器的解引用運算符生成一個右值引用。
通過調(diào)用標準庫的?
make_move_iterator
?函數(shù)將一個普通迭代器轉(zhuǎn)換為一個移動迭代器。不要隨意使用移動操作,由于一個移后源對象具有不確定的狀態(tài),對其調(diào)用?
std::move
?是危險的,當我們調(diào)用它時,必須絕對確認移后源對象沒有其他用戶。在移動構(gòu)造函數(shù)和移動賦值運算符這些類實現(xiàn)代碼之外的地方,只有當你確信需要進行移動操作且移動操作是安全的,才可以使用?
std::move
。
13.6.3 右值引用和成員函數(shù)
區(qū)分移動和拷貝的重載函數(shù)通常有一個版本接受一個?
const T&
,而另一個版本接受一個?T&&
。

class StrVec {
public:
? ?void push_back(const std::string&); // 拷貝:綁定到任意類型的T
? ?void push_back(std::string&&); ? ? ?// 移動:只能綁定到類型T的可修改的右值
? ?// ...
}
StrVec vec;
string s = "some string...";
vec.push_back(s); ? ? ? ? ? ? ? // 拷貝
vec.push_back(std::move(s)); ? ?// 移動
vec.push_back("done"); ? ? ? ? ?// 移動

引用限定符:用來指出一個非?
static
?成員函數(shù)可以用于左值或右值的符號。限定符?&
?和?&&
?應(yīng)該放在參數(shù)列表之前或者?const
?限定符之后(如果有的話)。被?&
?限定的函數(shù)只能用于左值;被?&&
?限定的函數(shù)只能用于右值。

class string?{
? ?void func1() &;
? ?void func2() &&;
? ?void func3() const &;
};
void string::func1() & {}
void string::func2() && {}
void string::func3() const & {}
string a = "a" , b = "b";
a.func1(); ? ? ? ? ? ? ? ? ??????????// 左值調(diào)用成員函數(shù)
auto n = (s1 + s2).func2(); // 右值調(diào)用成員函數(shù)

綜合?
const
?和引用限定符可以區(qū)分重載版本。如果一個成員函數(shù)有引用限定符,則具有相同參數(shù)列表的所有版本都必須有引用限定符。

