c++基础:之封装
什么是類
?
C++是什么?C++設計之初就是class with c,所以簡單點說,C++就是帶類的C,那么什么是類?
?
類,簡單點說就是類型,在C++中我們一開始所接觸的類型有如下幾種:
?
//+-------------------
?
char,
short,
int,
long,
long long,
float,
double
……
//+------------------
?
這些類型屬于語言本身的一部分,我們稱之為基本類型,基本類型不可被更改,也不可被創(chuàng)造,更不可
被消滅,任何一個程序都是有基本類型搭建起來的,比如,我們想要用一個類型來表示一個學生,那么
我們可以char*,來表示他的名字,用unsigned int來表示他的學號,用double來表示他的成績等等,而
這個表示學生信息的類型是由我們自定義而來,所以我們稱之為自定義類型,在C語言里面,如果我們
想要自定義一個類型出來,那么我們只能用關鍵字struct或者union,常使用的是struct,而union僅僅用
于某些特殊的場合,所以我們可以按如下的放下來定義一個自定義類型:
?
?
//+-----------------
?
typedef struct UserType{
? ? int a;
? ? double b;
? ? long long c;
}* __LPUSERTYPE;
?
//+----------------
?
這個自定義類型由三個數據段組成,當然如果你要問我干嘛這么定義,我想應該必要的時候會這么定義吧。
那么,既然說C++是帶類的C,那么在C++里面擴展一個自定義類型又該如何呢?當然,如果我說class在很
多時候等同于struct的話那么這個問題是不是就不再是問題了呢?ok,如果剛才的那個UserType到底表示什
么都不清楚的話,那么下面我們嘗試用一種能夠說清楚的類型來闡述自定義類型的定義:
?
//+---------------
?
class Point{
public:
? ? double x;
? ? double y;
};
?
//+---------------
?
class : C++ 關鍵字,表示接下來要定義一個類型啦。
?
? ? Point : 類型名,總是跟在class的后面,指明類型名是什么,class 和 類型名的中間還可以有其他的東
西,比如我們在寫com的時候使用的uuid,比如我們要導出一個類時候使用的__declspec(dllexport)等。
?
? ? {} : class 的代碼段。
?
? ? 在 C++ 里面,class 是一句完整的C++語句,C++語句都是以";"結束,所以在"}"后面需要要用表示結束
的";"號,否則你會遇到各種你預想不到的錯誤,當然,該語法對于C語言的struct也同樣實用那么class和struct
又有什么區(qū)別呢?在C語言里面,struct里面所定義的數據類型都是可以直接訪問的,簡單點說C語言的struct的
數據是共有的,同時C語言里的struct里面不可以有成員函數,當然這個限制在C++中已經被摒棄,在C++中,
struct和class的唯一區(qū)別就是默認權限的區(qū)別,在C語言中沒有權限的概念,但C++作為面向對象的編程語言,
所以自然提供了權限的概念,以便于數據的封裝,只是struct的默認權限是public,而class的默認權限是
private,public顧名思義是公共的,private是私有的,當然除了public和private外還存在一個權限:protected,
private和protected所限制的數據都是外部不能夠訪問的,那么他們的區(qū)別是什么呢?private是純粹的對數據
進行封裝,protected不但對數據進行封裝,還對繼承留下一個后門。如你們所見,這里我們使用的plubic權限,
public后面必須跟有":"號,所以在public下面的接口或者數據都是外部能夠直接訪問得到的。 那么在C++中,
我們什么時候使用struct什么時候使用class呢?這里沒有什么標準規(guī)范來限制,所以簡單點說就是凡是使用struct
的地方都可以使用class來替換,反之亦然,但是,通常于C++來說有個不成文的規(guī)矩,那就是如果僅僅只是簡單
的定義一個組合類型的話我們使用struct,否則我們都應該使用class。
?
?
構造函數
?
什么是構造函數,從名字上面來理解,我們可以簡單的認為就是構造對象的函數,一個類型想要被實例化,
那么它首先調用的便是這個構造函數,而從代碼的角度來理解的話構造函數就是名字和類型名一樣的函數,
該函數可以有參數,但沒有返回值,如果該函數沒有參數,那么該函數被稱為默認構造函數。
?
例如只定義了Point類的構造函數如圖:
Point類沒有默認構造函數。
編譯時出錯:可見編譯器在面對剛創(chuàng)建并未顯示初始化的對象時,對它進行默認初始化,若該類沒有
默認構造函數,則報錯。可見編譯器的責任是保證每個類被創(chuàng)建后,必須調用一個構造函數,令其是被初始化的。
?構造函數的形式我們已經了解:那么問題是我們自己編寫了自己的構造函數,那么構造函數的樣子
是不是就是這樣呢?答案是不會的。編譯器會給你添油加醋的。。。哈哈
編譯器會對其檢查。規(guī)則就是:
1.按照變量在類內出現的順序進行初始化,不管你定義構造函數時給出的初始值列表順序如何。
2.該類型既有類內初始值又在初始值列表中被初始化,那么采用初始值列表中的初始化,如果只
出現其一,那么就用出現的。如果兩者都沒有,如果該變量是內置類型,則不初始化,如果是
類對象,則執(zhí)行默認初始化,如果沒有默認構造函數則報錯。
驗證:1.合成的默認構造函數按順序初始化
?????????????????????
?????????????
?驗證2:若類內初始值與初始值列表都有則按初始值列表初始化
??
? ? ? ? ? ? ??
?
僅將初始值列表去掉
????? 驗證三:若某個累的成員變量在構造函數中,既沒有類內初始值,也沒有在初始值列表內被初始化,
? ? ? ? ? ? ? ? ? ? 則編譯器會在初始值列表內自動調用這個變量的默認構造函數,若沒有則報錯。
dot類沒有默認構造函數:
編譯器會在初始值列表中試圖調用dot的默認構造函數,而dot沒有,報錯。
果然如此#^_^:
?
?
?
?
?
?
?
?
?
?
explicit 的作用就是阻止了構造函數的調用。不能進行默認轉化(即讓編譯器自動調用相應的構造函數)。所以用explict修飾過得構造函數
只能用于直接初始化。explicit只能用于類內接受一個參數的構造函數的聲明。隱式轉換,理論上因該是編譯器調用某一個構造函數,然后
構造一個臨時對象,再利用這個臨時對象進行拷貝構造。但事實并非如此,編譯器往往不生成臨時對象,直接調用構造函數初始化目標對象。
?如果目標對象非引用的話。如果目標對象是引用,那么就會生成一個臨時對象。而一個臨時對象是右值類型,只能綁定const&。
?
Point p;
?
這句代碼直觀上理解我們可能誰都清楚,但是現在我們想要知道,當我們定義一個Point的對象p的時候我們實際都經歷了些什么?
?
第一,在棧上獲取了一塊內存。
?
?第二,調用了Point的構造函數。
?
什么?調用了Point的構造函數?Point的構造函數是什么鬼?說好的和Point同樣名字的函數怎么沒看到呢?嗯,這就是我們要說的
默認構造函數,也就是說,當我們不給我們的類指定構造函數的時候編譯器會為我們生成默認的構造函數,而這個函數什么,所以
雖然我們已經構造出一個Point的對象p出來,但是p里面的x和y是未初始化的,所以接下來我們需要針對xy進行各自的初始化,所以
無論出于什么樣的理由,我們應該給Point添加相應的構造函數。
?
//+-----------------
?
class Point{
public:
? ? Point(double __x = 0.0,double __y = 0.0):x(__x),y(__y){}
? ??
private:
? ? double x;
? ? double y;
};
?
//+-----------------
?
這次我們不但添加了構造函數,同時還將數據段放在private里面,我們將通過構造函數對數據進行初始化。該構造函數我們使用兩個double類型作為參數,
并且兩個參數都有默認值,所以我們下面的代碼:
?
Point p;
?
將等同于:
?
Point p(0.0,0.0);
?
此時的x y的值分別都是0.0
?
:x(__x),y(__y)
?
在構造函數的括號后面有個冒號,冒號后面跟了一段代碼,這段代碼叫初始化列表,我們的xy便是在這里進行初始化的,我們使用第一個參數對x進行初始化,
使用第二個參數對y進行初始化。當然我們也可以不適用初始化列表:
?
//+------------------
?
class Point{
public:
? ? Point(double __x = 0.0,double __y = 0.0){
? ? ? ? x = __x;
? ? ? ? y = __y;
? ? }
? ??
private:
? ? double x;
? ? double y;
};
?
//+-------------------
?
這樣的構造函數也是隨處可見的,只是這樣的寫法和上面的寫法有些不同(這不是廢話嗎?只要不瞎一看就是不同),哦!我這里說的不同是指效率上面的不同,
好吧,我們來剖析一下為什么不會不同,我們先來看看要實例化一個類我們所要經歷的步驟:
?
?
第一,構造數據成員
第二,執(zhí)行構造函數
?
所以,在執(zhí)行構造函數之前xy已經被實例化出來了,所以當我們執(zhí)行 x = __x 時又經歷了一個復雜的過程,這個過程后面細說(但是由于我們此處使用的是基本類
型,所以這個過程也就被忽略啦,如果是自定義類型的話這期間又會有各種問題的產生),所以不管怎么說,我們應該優(yōu)先選擇使用初始化列表的方法來對數據進行初始化。
我們自定義一個類型目的就是為了使用他所封裝的數據,但是像我們的Point類就是一個鐵公雞,就是說我們可以將數據放進去,但取不出來,嗯,這是一個問題,解決這個
問題的方法可以將數據段的private提升為public,這可以,但……
?
//+-----------------
?
void dealPoint(const Point& p) {
? ? const_cast<Point&>(p).x = 1000;
}
int main()
{
? ? Point p{ 100,200 };
? ? dealPoint(p);
? ? std::cout << p.x << std::endl;
? ? std::cin.get();
? ? ? ? return 0;
}
?
//+----------------
?
p的x的值直接被修改,當然有些時候我們可能想要這么干,但是這往往會帶來意向不到的災難性后果,因為你不知道什么時候哪根神經搭錯了忽然間很想修改這個值。
所以合理的做法應該是我們提供有方法直接訪問內部需要訪問的東西。
?
//+----------------
?
class Point{
public:
? ? Point(double __x = 0.0,double __y = 0.0):x(__x),y(__y){}
? ? double get_x() const{return x;}
? ? double get_y() const{return y;}
? ? void set_x(double __x){x = __x;}
? ? void set_y(double __y){y = __y;}
? ? void set(double __x,double __y){
? ? ? ? x = __x;
? ? ? ? y = __y;
? ? }
private:
? ? double x;
? ? double y;
};
?
int main()
{
? ? Point p(200, 300);
? ? std::cout << p.get_x()<<"\t"<<p.get_y() << std::endl;
? ? std::cin.get();
? ? ? ? return 0;
}
?
//+----------------
?
?
?
?
復制構造函數
?
如果我們有一個Point,我們想要當前的Point去構造出一個相同的Point的時候我們應該怎么說呢?
?
Point p(200, 300);
Point p2(p);
?
就目前來說,如果我們寫出這樣的代碼,編譯通過是完全沒問題的,同時運行也不會有任何問題。因為上面的第二句代碼執(zhí)行的并不是默認的構造函數,而是默認的復制構造函數,
什么是復制構造函數呢?
復制構造函數就是函數名和類型一樣,沒有返回類型,而參數是該類型,如果我們不指定復制構造函數的話那么編譯器會為我們的類升成默認的復制構造函數,所以上面的第二行代
碼執(zhí)行的便是復制構造函數,最終是p == p2.
那么怎么編寫復制構造函數呢?如下:
?
//+---------------
?
class Point{
public:
? ? Point(double __x = 0.0,double __y = 0.0):x(__x),y(__y){}
? ? Point(const Point& p):x(p.x),y(p.y){}
? ? double get_x() const{return x;}
? ? double get_y() const{return y;}
? ? void set_x(double __x){x = __x;}
? ? void set_y(double __y){y = __y;}
? ? void set(double __x,double __y){
? ? ? ? x = __x;
? ? ? ? y = __y;
? ? }
private:
? ? double x;
? ? double y;
};
?
?
//+------------------
?
賦值操作符
?
編譯器會為class生成的不只有默認的構造函數和默認的復制構造函數,同時還會生成默認的賦值操作,正因為有這個默認的賦值操作符,所以我們下面的代碼才會通過編譯:
?
Point p(200,300); // 調用構造函數
Point p2 = p; // 調用復制構造函數
Point p3; // 調用默認的構造函數
p3 = p2 // 調用默認的賦值操作符
?
如果我們不使用編譯器為我們準備的默認操作符的話,我們可以自己編寫我們的賦值操作符,賦值操作符是這樣的一個函數:
?
T& operator=(const T& other);
T 是我們的自定義類型。
?
所以如果我們自己編寫賦值操作符,應該這樣來:
?
?
//+-----------------
?
class Point{
public:
? ? Point(double __x = 0.0,double __y = 0.0):x(__x),y(__y){}
? ? Point(const Point& p):x(p.x),y(p.y){}
? ? Point& operator=(const Point& other){
? ? ? ? if(this == &other)
? ? ? ? ? ? return *this;
? ? ? ? x = other.x;
? ? ? ? y = other.y;
? ? ? ? return *this;
? ? }
? ? double get_x() const{return x;}
? ? double get_y() const{return y;}
? ? void set_x(double __x){x = __x;}
? ? void set_y(double __y){y = __y;}
? ? void set(double __x,double __y){
? ? ? ? x = __x;
? ? ? ? y = __y;
? ? }
private:
? ? double x;
? ? double y;
};
?
//+---------------------
?
?
一個空類
?
?
//+-----------------
?
class Empty{};
?
//+----------------
?
?
?
?
?
?
?
?
?
?
當我們寫下上面的類的時候,意味著我們寫了些什么?
?
1,默認構造函數。
2,默認的復制構造函數
3,默認的賦值操作符
4,默認取地址操作符(該操作符的重載此處不做解釋,熟悉之后自然也就明白了,所以該函數在一般的教科書中是不當作默認實現的函數,因為它本該存在)
5,析構函數
?
實際等同于:
?
//+-----------------
?
class Empty{
public:
? ? Empty(){}
? ? ~Empty(){}
? ? Empty(const Empty& other){}
? ? Empty& operator=(const & Empty& other){return *this;}
};
?
//+-----------------
?
第一次我們引入析構函數,析構函數和構造函數相對應,構造函數初始化資源,所以析構函數的功能就是清理資源,那么什么時候需要我們自己實現構造函數呢?
那就是當我們有資源需要我們手動釋放的時候,比如堆上的指針,比如com對象的Release等等,如果說上面我們所舉的Point例子其實是不需要復制操作符和
復制構造函數的話(因為默認的就很好),那么我們現在來說一個我們必須要手賦值制操作符和復制構造函數的例子——字符串處理類,String。
在C++里面字符串有char*表示,但是純粹的時候char*太過原始,一點都不對象,所以通常都會對char*進行封裝,當然想要做一個完備的字符串類出來可不是一件簡單的事,
所以這里只是作為一個例子,我們僅僅實現一些簡單的操作即可:
?
?
?
//+------------------
?
?
//
// 簡單的字符串處理類
//
class String{
public:
//
// 構造函數
//
String(const char* str = "") :mData(nullptr){
int len = strlen(str) + 1;
mData = new char[len];
memset(mData, 0, len );
memcpy(mData, str, len - 1);
}
?
?
//
// 析構函數
// 該函數絕不能使用缺省的,我們必須手動釋放資源
//?
~String(){
if (mData){
delete[] mData;
mData = nullptr;
}
}
?
?
//
// 復制構造函數
// 該函數不能使用缺省的,我們必須手動拷貝資源
//
String(const String& str):mData(nullptr){
int len = str.size();
len += 1;
mData = new char[len];
memset(mData, 0, len);
memcpy(mData, str.mData, len - 1);
}
?
//
// 賦值操作符
// 該函數不能使用缺省的,我們必須手動拷貝資源
//
String& operator=(const String& str){
if (this == &str){
return *this;
}
if (mData != nullptr){
delete[] mData;
mData = nullptr;
}
int len = str.size();
len += 1;
mData = new char[len];
memset(mData, 0, len);
memcpy(mData, str.mData, len - 1);
return *this;
}
?
//
// 獲取字符串長度
//
unsigned size() const{
if (mData == nullptr){
return 0;
}
return strlen(mData);
}
?
?
//
// 下標操作符
//
char& operator[](unsigned index){
if (mData == nullptr || index >= size()){
throw std::out_of_range("operator[](unsigned index)");
}
return mData[index];
}
const char& operator[](unsigned index) const{
return const_cast<String*>(this)->operator[](index);
}
?
//
// 檢查字符串是否為空
//
bool empty() const{
return this->size() == 0;
}
?
?
//
// 支持流的輸出
//
friend std::ostream& operator<<(std::ostream& os, const String& str){
if (str.empty()){
return os;
}
os << str.mData;
return os;
}
?
private:
char* mData{ nullptr };
};
?
?
//
// 測試代碼
//
int main(){
String str("Hello World");
std::cout <<"str = "<< str << std::endl;
String str2 = str;
std::cout << "str2 = " << str2 << std::endl;
String str3;
std::cout << str3.empty() << std::endl;
std::cout << str2.size() << std::endl;
str3 = str2;
str3[2] = 'H';
std::cout << str3.empty() << std::endl;
std::cout << str3 << std::endl;
system("pause");
return 0;
}
?
//+------------------
?
字符串的操作屬于最基本的操作,但同時也是最有講究的操作,幾乎每一個相對完善的C++類庫都提供有字符串處理類,比如標準庫中的string,MFC和ATL的CString,
Qt的QString,CEGUI的String,DuiLib的DuiString等等,所以字符串的處理雖然是基本的操作,卻也是最為重要的操作,網上流傳的C++面試題中更是將字符串的實現
作為一大考點,當然這不足為奇,因為要是現在一個完備的字符串類,需要考慮到方方面面的東西,后續(xù)我們會提供一個功能強大的字符串類,那么余下的就由各位去思考。
?右值引用與移動語義
1.介紹
Rvalue引用至少結決了兩個問題
1.實現移動語義
2.完美轉發(fā)
rvalue lvalue沒有明確的定義,大致定義如下:
lvalue:可以取地址
rvalue:不可取地址
?// lvalues:
//int i = 42;i = 43; // ok, i is an lvalueint* p = &i; // ok, i is an lvalueint& foo();foo() = 42; // ok, foo() is an lvalueint* p1 = &foo(); // ok, foo() is an lvalue// rvalues://int foobar();int j = 0;j = foobar(); // ok, foobar() is an rvalueint* p2 = &foobar(); // error, cannot take the address of an rvaluej = 42; // ok, 42 is an rvalue2.移動語義
假設X是一個類,它持有一個指向指向某種資源的指針或句柄,m_pResource.我的意思是說需要大量努力去構建,
拷貝或銷毀的類。一個不錯的例子是std::vector,它保存了一個對象的集合,存在于一個分配的內存數組中。從邏輯上
講,x的拷貝賦值運算符是這樣的:
X& X::operator=(X const & rhs)
{
//[..]
//Destruct the resource that m_pResource refers to
//Make a clon of what rhs.m_pResource refers to
//Attach the clone to m_pResource
//[...]
}
拷貝構造函數也是一樣的原理。我們以以下的方式使用X:
X foo();
X x;
//perhaps use x in various ways
x=foo();
最后一行包含以下步驟:
·destructs the resource held by x
·clones the resource from the temporary returned by foo
·destructs the temporary and thereby releases its resource
很明顯,交換資源指針(句柄)在x和臨時對象之間是可以的,而且更有效率,然后讓臨時對象析構函數破壞x的原始資源。
換句話說,在特殊情況下,賦值的右端是一個rvalue,我們希望復制賦值運算符像這樣運行:
// [...]
// swap m_pResource and rhs.m_pResource // [...]
這就叫做移動語義。在c++11里,這種理想的行為能夠被實現通過重載。
X& X::operator=(<mystery type> ths)
{
//[...]
//swap this->m_pResource and ths.m_pResource
//[...]
}
我們定義了一個拷貝賦值運算符的重載,我們的"mystery type"本質上必須是一個引用:我們希望mystery type
3.右值引用
如果X是任意類型,那么X&&被稱為X的右值引用。為了更好的區(qū)分,普通的引用X&現在被稱為左值引用。
右值引用在許多行為上與普通的引用X&是類似的,當然有部分是不同的。其中最最大的一個不同就是涉及到重載時,
左值更喜歡普通的左值引用,然而右值更喜歡新的右值引用
void foo(X& x); // lvalue reference overloadvoid foo(X&& x); // rvalue reference overloadX x;X foobar();foo(x); // argument is lvalue: calls foo(X&)foo(foobar()); // argument is rvalue: calls foo(X&&)
所以,它的要點是:
右值引用允許一個函數(我是被左值調用還是右值調用)用重載解析來進行綁定。
你當然可以再任何情況下以這種方式重載函數,比如上面的例子,但是在絕大多數情況下這種重載應該發(fā)生在拷貝構造和
拷貝賦值運算符上,以實現移動語義。
X& X::operator=(X const & rhs); // classical implementationX& X::operator=(X&& rhs){// Move semantics: exchange content between this and rhsreturn *this;} 為拷貝構造函數實現一個右值引用的重載時類似的。
注意:正像c++中大多數的情況一樣,乍一看上去是完美的。但在某些情況下,在拷貝賦值操作符的實現中,這樣簡單的交換this
和rhs并不完美。我們將在第四部分回到這個話題:強制移動語義。
| Note:?If you implement void foo(X&); but not void foo(X&&); then of course the behavior is unchanged:?foo?can be called on l-values, but not on r-values. If you implement void foo(X const &); but not void foo(X&&); then again, the behavior is unchanged:?foo?can be called on l-values and r-values, but it is not possible to make it distinguish between l-values and r-values. That is possible only by implementing void foo(X&&); as well. Finally, if you implement void foo(X&&); but neither one of void foo(X&); and void foo(X const &); then, according to the final version of C++11,?foo?can be called on r-values, but trying to call it on an l-value will trigger a compile error. |
眾所周知,正如C++標準的第一修正案所陳述:“委員會不會建立任何試圖絆住C++程序員的腳的規(guī)則。(The committee shall make no rule that prevents C++ programmers from shooting themselves in the foot.)”,正經來說,就是當面臨給予程序員更多控制還是減少他們粗心大意機會的選擇時,C++更傾向于及時可能導致犯錯,但是依然給予更多控制。正是基于這種精神,C++11允許你使用Move語義而不僅僅局限于是右值,而是還有左值,這都給與你充分的決定權。一個好的例子就是標準庫里面的swap方法。同以前一樣,給出一個類X,基于它我們可以重載拷貝構造函數和拷貝賦值操作符來在右值上面實現Move語義。
template<class T> void swap(T& a, T& b) { T tmp(a);a = b; b = tmp; } X a, b; swap(a, b);這里并沒有右值。所以,在swap函數中的三行沒有使用move語義。但是我們知道使用move語義是可行的:任何時候當一個變量作為源頭出現在一個拷貝構造函數或者賦值語句的時候,那個變量將不會再被使用,或者僅僅被作為賦值的目標。
在C++11中,標準庫中有一個叫做std::move的方法用來處理這種情況。這個函數只是將他的參數變成右值。因此,在C++11中,標準庫函數swap將會像如下所示:
template<class T> void swap(T& a, T& b) { T tmp(std::move(a));a = std::move(b); b = std::move(tmp); } X a, b; swap(a, b);現在所有在swap函數中的三行都使用了move語義。記住對于沒有實現move語義的類型(也就是說,并沒有為右值單獨重載一個拷貝構造函數和賦值表達式的類型),新的swap行為表現同舊的版本是一致的(注:其實嚴謹的來講,只有在參數前加上const才一致。單純的左值引用不能綁定在一個右值上)。
std::move是一個非常簡單的函數。不幸的是,盡管如此,我還不能將它的實現展現給你。我們將會在后面討論它。
在任何能使用std::move的地方使用它。如上面的swap函數所示,它給我們帶來如下好處:
- 對于實現了move語義的類型而言,需要標準庫算法和操作將會使用move語義,然后因此得到潛在的性能提升。一個重要的例子就是原地排序(inplace sorting):原地排序算法就是單純的交換元素,其他什么都不做,然后現在交換的過程將會在提供了move語義的類型中,充分利用到move語義提供的優(yōu)勢。
- 標準模板庫(STL)經常需要有些類型提供拷貝能力(copyability)。例如可以被用做容器元素的類型。通過嚴格的觀察,事實證明,在很多情況下,移動能力(moveability)就足夠了。因此,我們現在可以在某些以前并不被允許的地方使用可移動的而不是可復制的類型了(譬如unique_pointer)。舉例來說,這些類型現在可以被使用做標準模板庫的容器類型了。
既然我們知道了std::move,我就就需要知道為什么實現一個基于右值引用的拷貝賦值表達式的重載,如同前面我所展示的,依然是有些問題的。考慮一個簡單的變量間的賦值,像這樣:
a = b;
你期待在這里發(fā)生什么?你期待被a持有的對象被一份b的拷貝所替代,當然在整個交換過程中,你期待原來被a持有的對象會被析構。現在考慮這行代碼:
a = std::move(b);
如果move語義被實現為一個簡單的交換,那么這里的表現就將會是被a和b持有的對象將在a和b間交換。沒有任何東西被析構。當然原來被a持有的對象將會最終被析構,也就是說,當b離開了該代碼的范圍時。當然,除非b成為move的對象,在這種情況下,原來被a持有的對象又再次得到了一次。因此,只有拷貝賦值表達式的實現被精心考慮過后,我們并不知道原來被a持有的對象何時將被析構。
所以在某種意義上,我們在這里將會陷入到不確定性的泥沼中:一個變量被分配后,但是原來被該變量持有的對象卻還在其他位置。只有那個對象的析構函數并不會對外面有任何副作用的時候才是可行的。但是某些時候,析構函數確實會有這種副作用。一個例子就是在析構函數中釋放一個鎖。因此,析構函數中任何可能含有副作用的部分應該在拷貝賦值操作符的右值引用重載中清晰地表現出來:
X& X::operator=(X&& rhs) {// 執(zhí)行一次清理,要注意到那些在析構函數中可能產生副作用的地方。// 確保對象處于可析構的和可賦值的狀態(tài)// Move語義,交換this和rhs的內容return *this; }5.右值引用就是右值嗎?
同之前一樣,給出一個X類,讓我們可以重載它的拷貝構造函數和拷貝賦值操作符來實現move語義。現在做如下考慮:
void foo(X&& x) {X anotherX = x;// ... }一個有趣的問題就是,在foo函數內,哪一個x的拷貝構造函數重載將會被調用。這里,x是一個被聲明為右值引用的變量,也就是說,一個在一般情況下或者在更好的情況下(即使并不是必須的)指代一個右值。因此,也許可以期待x本身應該像是一個右值綁定,也就是說:
X(X&& rhs);應該被調用。換另一種說法,一個人可能會期待任何被聲明為右值引用的,其本身也是一個右值。然后右值引用的設計者們采取了更為微妙的一種解決方案:
被聲明為右值引用的可能既是左值也可能是右值。這里判斷的標準在于:如果它有名字,那么它就是左值。否則就是右值。
就上面的例子,被聲明為右值引用的有了一個名字,那么因此它是一個左值:
void foo(X&& x) {X anotherX = x; // 調用 X(X const & rhs) }這里就是一個被聲明為右值引用并且并沒有名字,因此它是一個右值:
X&& goo(); X x = goo(); // 調用 X(X&& rhs) 因為在表達式的右手邊// 并沒有名字這里有關于這種設計的原理闡述:假設默許move語義發(fā)生在一個有名字的東西上面,如下:
X anotherX = x; // x依然在作用域內!將是十分危險且混亂的,并且是很容易出錯的。因為我們剛剛移動的東西,依然在隨后的代碼中可以被訪問到。但是move語義的整個要點在于它就是應用在那些并不用在意移動后的情況的對象上,嚴格意義上而言,那些我們從它那里移動后會銷毀并且放任不管是沒有問題的。因此才有了這個規(guī)則:“如果它有個名字,那么它是一個左值。”
這里有一個例子顯示了「如果它有一個名字規(guī)則」的重要性。假設你已經寫了一個叫做Base的類,然后你通過重載Base的拷貝構造函數和賦值操作符來實現move語義。
Base(Base const & rhs); // 非move語義 Base(Base&& rhs); // move語義現在你寫了一個Derived的類,他繼承于Base。為了保證move語義被實施在Derived對象的Base部分,你必須也同時重載Derived的拷貝構造函數和賦值操作符。讓我們來看一下拷貝構造函數,拷貝賦值操作符的處理是類似的。對應于左值的版本比較簡單直接:
Derived(Derived const & rhs) : Base(rhs) {// 針對Derived特定的操作 }對應于右值的版本有一個很大的微妙的不同。下面就是有人沒有意識到「如果它有一個名字規(guī)則」可能寫出來的:
Derived(Derived&& rhs) : Base(rhs) // 錯誤:rhs是一個左值 {// 針對Derived特定的操作 f }如果我們像那樣寫代碼,那么non-moving的Base拷貝構造函數的版本將會被調用,因為rhs有一個名字,是一個左值。我們想要調用的是Base的moving構造函數,我們需要這樣寫:
Derived(Derived&& rhs) : Base(std::move(rhs)) // 好,調用Base(Base&& rhs) {// 針對Derived特定的操作6.Move語義和編譯器優(yōu)化
考慮下面這樣的函數定義:
X foo() {X x;// perhaps do something to xreturn x; }現在同以前一樣進行假設,給出一個X類,我們可以通過重載它的拷貝構造函數和拷貝賦值操作符來實現move語義。如果你看了一眼上面的函數定義的話,你可能會禁不住說:「等一下,這里有一份從x到foo函數返回值位置的值拷貝。讓我來確定我們使用了move語義」:
X foo() {X x;// perhaps do something to xreturn std::move(x); // making it worse! }不幸的是,這不僅不會讓事情變好,可能會變得更糟。任何現代的編譯器均可對原始的函數定義實現返回值優(yōu)化(return value optimization)。另一種說法,并不是在本地構造x然后將它拷貝出,而是編譯器會在foo的返回值的位置直接構造x對象。很明顯,這會比使用move語義更好。
所以你看,為了確切地有效地利用好右值引用和move語義,你需要充分理解并且對現在的編譯器的各種「優(yōu)化」有充分的考量,類似于返回值優(yōu)化和復制省略(copy elision)。Dave Abrahams在這方面寫了一系列文章,可以參見這里:http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/。這些細節(jié)可能都很微妙,但是,我們選擇C++作為我們的編程語言是有原因的,對吧?我們選擇了它,那么我們就應該認真對待它
左值???????????????????? 右值
可取地址 不可取地址
有名字????????????????? 無名子
持久????????????????????? 短暫
左值引用只可綁定左值
右值引用只可綁定右值
const類型的左值引用既可綁定左值,又可綁定右值
?
?
?
?
轉載于:https://www.cnblogs.com/invisible2/p/9113790.html
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
- 上一篇: requests库详解
- 下一篇: Flask入门flask-script