深入理解C++11核心編程(三)---通用為本,專用為末

C++11的新特性具有廣泛的可用性,可以與其他已有的,或者新增的語言特性結(jié)合起來進行自由的組合,或者提升已有特性的通用性。

繼承構(gòu)造函數(shù)

C++中的自定義類型--類,具有可派生性,派生類可以自動獲得基類的成員變量和接口(虛函數(shù)和純虛函數(shù),這里指的是public派生)。不過基類的非虛函數(shù)則無法再被派生類使用了。這條規(guī)則對于類中最為特別的構(gòu)造函數(shù)也不例外,如果派生類要使用基類的構(gòu)造函數(shù),通常需要在構(gòu)造函數(shù)中顯式聲明。

struct A{A(int i){}};
struct B:A{B(int i):A(i)};

B派生于A,B又在構(gòu)造函數(shù)中調(diào)用A的構(gòu)造函數(shù),從而完成構(gòu)造函數(shù)的"傳遞"。在B中有成員的時候:

struct A{A(int i){}};
struct B:A{
B(int i):A(i),d(i){}
int d;
};

派生于結(jié)構(gòu)體A的結(jié)構(gòu)體B擁有一個成員變量d,那么在B的構(gòu)造函數(shù)B(int i)中,我們可以在初始化其基類A的同時初始化成員d。

有的時候,我們的基類可能擁有數(shù)量眾多的不同版本的構(gòu)造函數(shù),而派生類卻只有一些成員函數(shù)時,那么對于派生類而言,其構(gòu)造就等同于構(gòu)造基類。

在派生類中我們寫的構(gòu)造函數(shù)完完全全就是為了構(gòu)造基類。那么為了遵從于語法規(guī)則,我們還需要寫很多的"透傳"的構(gòu)造函數(shù)。

struct A{
A(int i){}
A(double d,int i){}
A(float f,int i,const char*c){}
//...
};
struct B:A{
B(int i): A(i){}
B(double d, int i): A(d,i){}
B(float f,int i,const char*c): A(f,i,c){}
//...
virtual void ExtraInterface(){}
};

我們的基類A有很多的構(gòu)造函數(shù)的版本,而繼承于A的派生類B實際上只是添加了一個接口ExtraInterface.那么如果我們在構(gòu)造B的時候想要擁有A這樣多的構(gòu)造方法的話,就必須一一"透傳"各個接口。

事實上,在C++中已經(jīng)有了一個好用的規(guī)則,就是如果派生類要使用基類的成員函數(shù)的話,可以通過using聲明(using-declaration)來完成。

#include<iostream> 
using namespace std;
struct Base{
    void f(double i){
        cout<<"Base:"<<i<<endl;
    };
};
struct Derived:Base{
        using Base:: f;
        void f(int i){
            cout<<"Derived:"<<i<<endl;
        }
    };

int main(){
    Base b;
    b.f(4.5); //Base:4.5
    Derived d;
    d.f(4.5); //Base:4.5
}

我們的基類Base和派生類Derived聲明了同名的函數(shù)f,不過在派生類中的版本跟基類有所不同。派生類中的f函數(shù)接受int類型為參數(shù),而基類中接受double類型的參數(shù)。我們這里使用了using 聲明,聲明派生類Derived也使用基類版本的函數(shù)f。這樣一來,派生類中實際就擁有了兩個f函數(shù)的版本。

C++11中,這個想法被擴展到了構(gòu)造函數(shù)上。子類可以通過使用using聲明來聲明繼承基類的構(gòu)造函數(shù)。

struct A{
A(int i){}
A(double d,int i){}
A(float f,int i,const char*c){}
//...
};
struct B:A{
using A:: A;//繼承構(gòu)造函數(shù)
//...
virtual void ExtraInterface(){}
};

我們通過using A:: A的聲明,把基類中的構(gòu)造函數(shù)悉數(shù)繼承到派生類B中。C++11標準繼承構(gòu)造函數(shù)被設(shè)計為跟派生類中的各種類默認函數(shù)(默認構(gòu)造、析構(gòu)、拷貝構(gòu)造等)一樣,是隱式聲明的。意味著如果一個繼承構(gòu)造函數(shù)不被相關(guān)代碼使用,編譯器不會為其產(chǎn)生真正的函數(shù)代碼。

不過,繼承構(gòu)造函數(shù)只會初始化基類中成員變量,對于派生類中的成員變量,則無能為力。只能通過初始化一個默認值的方式來解決。如果無法滿足需求的話,只能自己來實現(xiàn)一個構(gòu)造函數(shù),以達到基類和成員變量都能夠初始化的目的。

基類構(gòu)造函數(shù)的參數(shù)會有默認值。對于繼承構(gòu)造函數(shù)來講,參數(shù)的默認值是不會被繼承的。事實上,默認值會導致基類產(chǎn)生多個構(gòu)造函數(shù)的版本,這些函數(shù)版本都會被派生類繼承。

struct A{
A(int a=3,double=2.4){}
}
struct B:A{
using A:: A;
};

我們的基類的構(gòu)造函數(shù)A(int a=3,double=2.4)有一個接受兩個參數(shù)的構(gòu)造函數(shù),且兩個參數(shù)均有默認值。那么A到底有多少個可能的構(gòu)造函數(shù)版本呢?
事實上,B可能從A中繼承來的候選繼承構(gòu)造函數(shù)有如下一些:
A(int=3,double=2.4); 這是使用兩個參數(shù)的情況。
A(int=3); 這是減掉一個參數(shù)的情況。
A(const A&); 這是默認的復制構(gòu)造函數(shù)。
A(); 這是不使用參數(shù)的情況。
相應(yīng)地,B中的構(gòu)造函數(shù)將會包括以下一些:
B(int,double); 這是一個繼承構(gòu)造函數(shù)。
B(int);這是減少掉一個參數(shù)的繼承構(gòu)造函數(shù)。
B(const B&); 這是復制構(gòu)造函數(shù),這不是繼承來的。
B(); 這是不包含參數(shù)默認構(gòu)造函數(shù)。

可以看見,參數(shù)默認值會導致多個構(gòu)造函數(shù)版本的產(chǎn)生,因此程序員在使用有參數(shù)默認值的構(gòu)造函數(shù)的基類的時候,必須小心。

繼承構(gòu)造函數(shù)“沖突”的情況。通常發(fā)生在派生類擁有多個基類的時候。多個基類中的部分構(gòu)造函數(shù)可能導致派生類中的繼承構(gòu)造函數(shù)的函數(shù)名、參數(shù)(有的時候,我們也稱其為函數(shù)簽名)都相同,那么繼承類中的沖突的繼承構(gòu)造函數(shù)將導致不合法的派生類代碼。

struct A{A(int){}};
struct B{B(int){}};
struct C:A,B{
using A:: A;
using B:: B;
};

A和B 的構(gòu)造函數(shù)會導致C中重復定義相同類型的繼承構(gòu)造函數(shù)。可以通過顯式定義繼承類的沖突的構(gòu)造函數(shù),阻止隱式生成相應(yīng)的繼承構(gòu)造函數(shù)來解決沖突。比如:

struct C:A,B{
using A:: A;
using B:: B;
C(int){} //其中的構(gòu)造函數(shù)C(int)就很好地解決了繼承構(gòu)造函數(shù)的沖突問題。(為什么能夠解決繼承構(gòu)造函數(shù)從圖的問題。)
};

如果,基類的構(gòu)造函數(shù)被聲明為私有成員函數(shù),或者派生類是從基類中虛繼承的,那么就不能夠在派生類中聲明繼承構(gòu)造函數(shù)。此外,如果一旦使用了繼承構(gòu)造函數(shù),編譯器就不會再為派生類生成默認構(gòu)造函數(shù)了,程序員必須注意繼承構(gòu)造函數(shù)沒有包含一個無參數(shù)的版本。

#include<iostream> 
using namespace std;
struct A{
    A(int){
    }
};
struct B:A{
    using A:: A;
};
    
int main(){
    B b;//B沒有默認構(gòu)造函數(shù) 
    B b(3);//構(gòu)造函數(shù)。 
}

委派構(gòu)造函數(shù)

與繼承構(gòu)造函數(shù)類似,委派構(gòu)造函數(shù)也是C++11中對C++的構(gòu)造函數(shù)的一項改進,其目的也是為了減少程序員書寫構(gòu)造函數(shù)的時間。通過委派其他構(gòu)造函數(shù),多構(gòu)造函數(shù)的類編寫將更加容易。
一個代碼冗余的例子:

#include<iostream> 
using namespace std;
class Info{
public:
Info() :type(1), name('a'){ //一次初始化,可以初始化很多變量。
    InitRest();
}
Info(int i):type(i), name('a'){
    InitRest();
}
Info(char e):type(1), name('e'){
    InitRest();
}
private:
    void InitRest(){/*其他初始化*/ 
    }
    int type;
    char name;
};
    
int main(){
    return 0;
}

在代碼中,我們聲明了一個Info的自定義類型。該類型擁有2個成員變量以及3個構(gòu)造函數(shù)。這里的3個構(gòu)造函數(shù)都聲明了初始化列表來初始化成員type和name,并且都調(diào)用了相同的InitRest??梢钥吹剑顺跏蓟斜碛械牟煌?,而其他的部分,3個構(gòu)造函數(shù)基本上是相似的,因此其代碼存在著很多重復。
改進方法1:非靜態(tài)變量的初始化

#include<iostream> 
using namespace std;
class Info{
    public:
    Info(){
        InitRest();
    }
    Info(int i): type(i){
        InitRest();
    }
    private:
    void InitRest(){
    }
    int type{1};
    char name{'a'};
};

雖然構(gòu)造函數(shù)簡單了不少,但是每個構(gòu)造函數(shù)還是需要調(diào)用InitRest函數(shù)進行初始化。能不能在一些構(gòu)造函數(shù)中連InitRest都不用調(diào)用呢?

我們將一個構(gòu)造函數(shù)設(shè)定為"基派版本",比如本例中的Info()版本的構(gòu)造函數(shù),而其他構(gòu)造函數(shù)可以通過委派"基準版本"來進行初始化。

Info(){InitRest();}
Info(int i){this->Info(); type=i;}
Info(char e){this->Info(); name=e;}

我們通過this指針調(diào)用我們的"基準版本"的構(gòu)造函數(shù)。但是一般的編譯器都會阻止this->Info()的編譯。原則上,編譯器不允許在構(gòu)造函數(shù)中調(diào)用構(gòu)造函數(shù),即使參數(shù)看起來并不相同。

還有一種是用placement new 來強制在本對象地址(this指針所指地址)上調(diào)用類的構(gòu)造函數(shù)。這樣,就可以繞過編譯器的檢查,從而在2個構(gòu)造函數(shù)中調(diào)用我們的"基準版本"。但是在已經(jīng)初始化一部分的對象上再次調(diào)用構(gòu)造函數(shù),卻是危險的做法。

在C++11中,我們可以委派構(gòu)造函數(shù)來達到期望的效果。C++11中的委派構(gòu)造函數(shù)是在構(gòu)造函數(shù)的初始化列表位置進行構(gòu)造的、委派的。

#include<iostream> 
using namespace std;
class Info{
    public:
    Info(){
        InitRest();
    }
    Info(int i): Info(){
        type=i;
    }
    Info(char e): Info(){
        name=e;
    }
    private:
    void InitRest(){
    }
    int type{1};
    char name{'a'};
};

在 Info(int) 和 Info(char) 的初始化列表的位置,調(diào)用了"基準版本"的構(gòu)造函數(shù) Info() 。 這里我們?yōu)榱藚^(qū)分被調(diào)用者和調(diào)用者,稱在初始化列表中調(diào)用"基準版本"的構(gòu)造函數(shù)為委派構(gòu)造函數(shù),而被調(diào)用的"基本版本"則為目標構(gòu)造函數(shù)。在C++11中,所謂委派構(gòu)造,就是指委派函數(shù)將構(gòu)造的任務(wù)委派給了目標構(gòu)造函數(shù)來完成這樣一種類構(gòu)造的方式。

委派構(gòu)造函數(shù)只能在函數(shù)體中為 type、name 等成員賦初值。 這是由于委派構(gòu)造函數(shù)不能有初始化列表造成的。在C++中,構(gòu)造函數(shù)不能同時"委派"和使用初始化列表,所以如果委派構(gòu)造函數(shù)要給變量賦初值,初始化代碼必須放在函數(shù)體中。比如:

struct Rule1{
int i;
Rule1(int a):i(a){}
Rule1():Rule1(40),i(1){}//無法通過編譯

Rule1的委派構(gòu)造函數(shù)Rule1() 的寫法就是非法的。我們不能在初始化列表中既初始化成員,為委托其他構(gòu)造函數(shù)完成構(gòu)造。
(初始化列表的初始化方式總是優(yōu)于構(gòu)造函數(shù)完成的(實際上在編譯完成時就已經(jīng)決定了))
稍微改造一下目標構(gòu)造函數(shù),使得委派構(gòu)造函數(shù)依然可以在初始化列表中初始化所有成員。

class Info{
    public:
    Info():Info(1,'a'){}
    Info(int i): Info(i,'a'){}
    Info(char e): Info(1,e){}
    private:
    Info(int i,char e): type(i), name(e){/*其他初始化*/}
    int type;
    char name;
    //...
};

我們定義了一個私有的目標構(gòu)造函數(shù)Info(int,char), 這個構(gòu)造函數(shù)接受兩個參數(shù),并將參數(shù)在初始化列表中初始化。由于這個目標構(gòu)造函數(shù)的存在,我們可以不再需要InitRest函數(shù)了,而是將其代碼都放入Info(int,char)中。這樣,其他委派構(gòu)造函數(shù)就可以委派該目標構(gòu)造函數(shù)來完成構(gòu)造。

在使用委派構(gòu)造函數(shù)的時候,我們建議程序員抽象出最為"通用"的行為做目標構(gòu)造函數(shù)。這樣做一來代碼清晰,二來行為也更加正確。由于在C++11中,目標構(gòu)造函數(shù)的執(zhí)行總是先于委派構(gòu)造函數(shù)而造成的。因此避免目標構(gòu)造函數(shù)和委派構(gòu)造函數(shù)體中初始化同樣的成員通常是必要的,

在構(gòu)造函數(shù)比較多的時候,我們可能會擁有不止一個委派構(gòu)造函數(shù),而一些目標構(gòu)造函數(shù)很可能也是委派構(gòu)造函數(shù),這樣一來,我們就可以在委派構(gòu)造函數(shù)中形成鏈狀的委派構(gòu)造關(guān)系。

class Info{
public:
Info(): Info(1){} //委派構(gòu)造函數(shù)
Info(int i): Info(i,'a'){} //即是目標構(gòu)造函數(shù),也是委派構(gòu)造函數(shù)
Info(char e): Info(1,e){}
private:
Info(int i,char e): type(i), name(e){/*其他初始化*/}//目標構(gòu)造函數(shù)
int type;
char name;
};

鏈狀委托構(gòu)造,這里我們使Info() 委托Info(int)進行構(gòu)造,而Info(int)又委托Info(int,char)進行構(gòu)造。在委托構(gòu)造的鏈狀關(guān)系中,就是不能形成委托環(huán)。比如:

struct Rule2{
int i,c;
Rule2():Rule2(2){}
Rule2(int i):Rule2('c'){}
Rule2(char c):Rule2(2){}
};

Rule2定義中,Rule2()、Rule2(int)和Rule2(char)都依賴于別的構(gòu)造函數(shù),形成環(huán)委托構(gòu)造關(guān)系。這樣的代碼通常會導致編譯錯誤。委托構(gòu)造的一個很實際的應(yīng)用就是使用構(gòu)造模板函數(shù)產(chǎn)生目標構(gòu)造函數(shù)。

#include<list>
#include<vector>
#include<deque>
using namespace std;
class TDConstructed{
    template<class T>TDConstructed(T first, T last):l(first,last){} //盡可能還是多理解這個地方
    list<int> l;    
    public:
        TDConstructed(vector<short> &v):TDConstructed(v.begin(),v.end()){}
        TDConstructed(deque<int> &d):TDConstructed(d.begin(),d.end()){}
};

我們定義了一個構(gòu)造函數(shù)模板。通過兩個委派構(gòu)造函數(shù)的委托,構(gòu)造函數(shù)模板會被實例化。T會分別被推導為 vector<short>::iterator 和 deque<int>::iterator 兩種類型。這樣一來, 我們的TDConstructed類就可以很容易地接受多種容器對其進行初始化。

(委托構(gòu)造使得構(gòu)造函數(shù)的泛型編程成為了一種可能)

在異常處理方面,如果在委派構(gòu)造函數(shù)中使用try的話,那么從目標構(gòu)造函數(shù)中產(chǎn)生的異常,都可以在委派構(gòu)造函數(shù)中被捕捉到。我們看下面的例子:

#include <iostream>
using namespace std;
class DCExcept{
    public:
        DCExcept(double d)
        try: DCExcept(1,d){
            cout<<"Run the body."<<endl;
        //其他初始化 
        }
        catch(...){
            cout<<"caught exception."<<endl;
        }
    private:
        DCExcept(int i,double d){
            cout<<"going to throw!"<endl;
            throw 0; //拋出異常。
        }
    int type;
    double data;
};
int main(){
    DCExcept a(1.2);
}

我們在目標構(gòu)造函數(shù)DCException(int,double)跑出了一個異常,并在委派構(gòu)造函數(shù)DCExcept(int)中進行了捕捉。而委派構(gòu)造函數(shù)的函數(shù)體部分的代碼并沒有被執(zhí)行。這樣的設(shè)計是合理的,因為如果函數(shù)體依賴于目標構(gòu)造函數(shù)構(gòu)造的結(jié)果,那么當目標構(gòu)造函數(shù)構(gòu)造發(fā)生異常的情況下,還是不要執(zhí)行委派構(gòu)造函數(shù)函數(shù)體中的代碼為好。

右值引用:移動語義和完美轉(zhuǎn)發(fā)

指針成員與拷貝構(gòu)造
對C++程序員來說,編寫C++程序有一條必須注意的規(guī)則,就是在類中包含了一個指針成員的話,那么就要特別小心拷貝構(gòu)造函數(shù)的編寫,因為一不小心,就會出現(xiàn)內(nèi)存泄露。
#include <iostream>
using namespace std;
class HasPtrMem{
    public:
        HasPtrMem(): d(new int(0)){}
        ~HasPtrMem() {
            delete d;
        }   
        int *d; //指針成員d
};
int main(){
    HasPtrMem a;
    HasPtrMem b(a);
    cout<<*a.d<<endl;//0
    cout<<*b.d<<endl;//0
}

我們定義了一個HasPtrMem的類。這個類包含一個指針成員,該成員在構(gòu)造時接受一個new操作分配堆內(nèi)存返回的指針,而在析構(gòu)的時候則會被delete操作用于釋放之前分配的堆內(nèi)存。在main函數(shù)中,我們聲明了HsaPtrMem類型的變量a,又使用a初始化了變量b。按照C++語法,這會調(diào)用HasPtrMem的拷貝構(gòu)造函數(shù)。(這里的拷貝構(gòu)造函數(shù)由編譯器隱式生成,其作用是執(zhí)行類似于memcpy的按位拷貝。這樣的構(gòu)造方式有一個問題,就是a.d和b.d都指向同一塊堆內(nèi)存。因此在main作用域結(jié)束的時候,a和b的析構(gòu)函數(shù)紛紛被調(diào)用,當其中之一完成析構(gòu)之后(比如b),那么a.d就成了一個"懸掛指針",因為其不再指向有效的內(nèi)存了。那么在該懸掛指針上釋放內(nèi)存就會造成嚴重的錯誤。

這樣的拷貝方式,在C++中也常被稱為"淺拷貝"。而在為聲明構(gòu)造函數(shù)的情況下,C++也會為類生成一個淺拷貝的構(gòu)造函數(shù)。通常最佳的解決方案是用戶自定義拷貝構(gòu)造函數(shù)來實現(xiàn)"深拷貝":

#include <iostream>
using namespace std;
class HasPtrMem{
    public:
        HasPtrMem(): d(new int(0)){
            cout<<"Construct:"<<endl;
        }
        HasPtrMem(HasPtrMem&h): d(new int(*h.d)){
            cout<<"Copy construct:"<<endl; 
        } //拷貝構(gòu)造函數(shù),從堆中分配內(nèi)存,并用*h.d初始化
        ~HasPtrMem() {
            delete d;
        }   
        int *d; //指針成員d
};
int main(){
    HasPtrMem a;
    HasPtrMem b(a);
    cout<<*a.d<<endl;//0
    cout<<*b.d<<endl;//0
}

(問題:淺拷貝和深拷貝 的差別)
我們?yōu)镠asPtrMem添加了一個拷貝構(gòu)造函數(shù)。拷貝構(gòu)造函數(shù)從堆中分配內(nèi)存,將該分配來的內(nèi)存的指針交還給d, 又使用*(h.d)對 *d進行了初始化。通過這樣的方法,就避免了懸掛指針的困擾。

拷貝構(gòu)造函數(shù)中為指針成員分配新的內(nèi)存再進行內(nèi)容拷貝的做法在C++編程中幾乎被視為不可違背的。不過在一些時候,我們確實不需要這樣的拷貝語義。

#include <iostream>
using namespace std;
class HasPtrMem{
    public:
        HasPtrMem(): d(new int(0)){
            cout<<"Construct:" << ++n_cstr<<endl;
        }
        HasPtrMem(const HasPtrMem&h): d(new int(*h.d)){
            cout<<"Copy construct:"<< ++n_cptr<<endl;
        } //拷貝構(gòu)造函數(shù),從堆中分配內(nèi)存,并用*h.d初始化
        ~HasPtrMem() {
            cout<<"Destruct:"<<++n_dstr<<endl;
        }
        int *d; 
        static int n_cstr;
        static int n_dstr;
        static int n_cptr;
};
int HasPtrMem::n_cstr=0;
int HasPtrMem::n_dstr=0;
int HasPtrMem::n_cptr=0;
HasPtrMem GetTemp(){
    return HasPtrMem();
}
int main(){
    HasPtrMem a=GetTemp();
}

(回顧:靜態(tài)變量和非靜態(tài)變量)
數(shù)據(jù)成員可以分靜態(tài)變量、非靜態(tài)變量兩種.
靜態(tài)成員:靜態(tài)類中的成員加入static修飾符,即是靜態(tài)成員.可以直接使用類名+靜態(tài)成員名訪問此靜態(tài)成員,因為靜態(tài)成員存在于內(nèi)存,非靜態(tài)成員需要實例化才會分配內(nèi)存,所以靜態(tài)成員不能訪問非靜態(tài)的成員..因為靜態(tài)成員存在于內(nèi)存,所以非靜態(tài)成員可以直接訪問類中靜態(tài)的成員.

非成靜態(tài)員:所有沒有加Static的成員都是非靜態(tài)成員,當類被實例化之后,可以通過實例化的類名進行訪問..非靜態(tài)成員的生存期決定于該類的生存期..而靜態(tài)成員則不存在生存期的概念,因為靜態(tài)成員始終駐留在內(nèi)容中..

我們聲明了一個返回一個HasPtrMem變量的函數(shù)。為了記錄構(gòu)造函數(shù)、拷貝構(gòu)造函數(shù),以及析構(gòu)函數(shù)調(diào)用的次數(shù),我們用了一些靜態(tài)變量。在main函數(shù)中,我們簡單地聲明了一個HasPtrMem的變量a,要求它使用GetTemp的返回值進行初始化。

//正常情況下的輸出:
Construct:1
Copy construct:1 //這個是臨時對象的構(gòu)造
Destruct:1 //這個應(yīng)該是臨時對象的析構(gòu)
Copy construct:2
Destruct:2
Destruct:3
但是在C++11或者非C++里面的結(jié)果
只是一個淺拷貝

這里的構(gòu)造函數(shù)被調(diào)用了一次,是GetTemp函數(shù)中HasPtrMem()表達式顯示地調(diào)用了構(gòu)造函數(shù)而打印出來的。而拷貝構(gòu)造函數(shù)則被調(diào)用了兩回。一次是從GetTemp函數(shù)中HasPtrMem()生成的變量上拷貝構(gòu)造出來一個臨時值,以用做GetTemp的返回值,而另一次則是由臨時值構(gòu)造出main中變量a調(diào)用的。對應(yīng)的,析構(gòu)函數(shù)也就調(diào)用了3次。


ttt.jpg-132.9kB
ttt.jpg-132.9kB

最頭疼的就是拷貝構(gòu)造函數(shù)的調(diào)用。在上面的代碼上,類HasPtrMem只有一個Int類型的指針。如果HasPtrMem的指針指向非常大的堆內(nèi)存數(shù)據(jù)的話,那么拷貝構(gòu)造函數(shù)就會非常昂貴??梢韵胂螅坏┻@樣,a的初始化表達式的執(zhí)行速度非常慢。臨時變量的產(chǎn)生和銷毀以及拷貝的發(fā)生對于程序員來說基本上是透明的,不會影響程序的正常值,因而即使該問題導致程序的性能不如預期,也不易被程序員察覺(事實上,編譯器常常對函數(shù)返回值有專門的優(yōu)化)

然后,按照C++的語義,臨時對象將在語句結(jié)束后被析構(gòu),會釋放它所包含的堆內(nèi)存資源。而a在拷貝構(gòu)造的時候,又會被分配堆內(nèi)存。這樣意義不大,所以,考慮在臨時對象構(gòu)造a的時候不分配內(nèi)存,即不使用拷貝構(gòu)造。

剩下的就是移動構(gòu)造:

tt1.jpg-313.6kB
tt1.jpg-313.6kB

上半部分從臨時變量中拷貝構(gòu)造變量a的做法,即在拷貝時分配新的堆內(nèi)存,并從臨時對象的堆內(nèi)存中拷貝內(nèi)容至a.d。而構(gòu)造完成后,臨時對象將析構(gòu),因此,其擁有的堆內(nèi)存資源會被析構(gòu)函數(shù)釋放。

下半部分,在構(gòu)造函數(shù)時使得a.d指向臨時對象的堆內(nèi)存資源。同時我們保證臨時對象不釋放所指向的堆內(nèi)存,那么,在構(gòu)造完成后,臨時對象被析構(gòu),a就從中"偷"到了臨時對象所擁有的堆內(nèi)存資源。

在 C++11 中,這樣的"偷走"臨時變量中資源的構(gòu)造函數(shù),就被稱為"移動構(gòu)造函數(shù)"。

#include <iostream>
using namespace std;
class HasPtrMem{
    public:
        HasPtrMem(): d(new int(3)){
            cout<<"Construct:" << ++n_cstr<<endl;
        }
        HasPtrMem(const HasPtrMem&h): d(new int(*h.d)){
            cout<<"Copy construct:"<< ++n_cptr<<endl;
        } //拷貝構(gòu)造函數(shù),從堆中分配內(nèi)存,并用*h.d初始化
        HasPtrMem(HasPtrMem &&h):d(h.d){
            h.d=nullptr;//將臨時值得指針成員置空。
            cout<<"Move construct:"<<++n_mvtr<<endl; 
        }
        ~HasPtrMem() {
            delete d;
            cout<<"Destruct:"<<++n_dstr<<endl;
        }
        int *d; 
        static int n_cstr;
        static int n_dstr;
        static int n_cptr;
        static int n_mvtr;
};
int HasPtrMem::n_cstr=0;
int HasPtrMem::n_dstr=0;
int HasPtrMem::n_cptr=0;
int HasPtrMem::n_mvtr=0;
HasPtrMem GetTemp(){
    HasPtrMem h;
    cout<<"Resource from"<<__func__<<":"<<hex<<h.d<<endl;
    return h; 
}
int main(){
    //HasPtrMem b;
    HasPtrMem a=GetTemp();
    cout<<"Resource from"<<__func__<<":"<<hex<<a.d<<endl;
 }
 

這里其實,就多了一個構(gòu)造函數(shù)HasPtrMem(HasPtrMem&&), 這個就是我們所謂的移動構(gòu)造函數(shù)。與拷貝構(gòu)造函數(shù)不同的是,移動構(gòu)造函數(shù)接受一個所謂的"右值引用"的參數(shù),關(guān)于右值,讀者可以暫時理解為臨時變量的引用。移動構(gòu)造函數(shù)使用了參數(shù)h的成員d初始化了本對象的成員d(而不是像拷貝構(gòu)造函數(shù)一樣需要分配內(nèi)存,然后將內(nèi)存一次拷貝到新分配的內(nèi)存中),隨后h的成員d置為指針空值nullptr。完成了移動構(gòu)造函數(shù)的全過程。

所謂的偷堆內(nèi)存,就是指將本對象d指向h.d所指的內(nèi)存這一條語句,相應(yīng)的,我們還將h的成員d置為指針空值。

//理論上的結(jié)果:
Construct:1
Resource from GetTemp:0x603010
Move construct:1
Destruct:1
Move construct:2
Destruct:2
Resource from main:0x603010
Destruct:3
//實際上的結(jié)果:似乎只要涉及到需要臨時變量的生成的時候,都會有問題。
Construct:1
Resource from GetTemp:0x603010
Resource from main:0x603010
Destruct:1

如果堆內(nèi)存不是一個int長度的數(shù)據(jù),而是以MBty為單位的堆空間,那么這樣的移動帶來的性能提升是非常驚人的。

如果傳了引用或者指針到函數(shù)里面作為參數(shù),效果雖然不差。但是從使用的方便性上來看效果卻不好,如果函數(shù)返回臨時值的話,可以在單條語句里面完成很多計算,比如可以很自然地寫出如下語句:

Caculate(GetTemp(), SomeOther(Maybe(),Useful(Values,2)));

但如果通過傳引用或者指針的方法而不返回值的話,通常就需要很多語句來完成上面的工作。

string*a; vector b;//事先聲明一些變量用于傳遞返回值
...
Useful(Values,2,a);//最后一個參數(shù)是指針,用于返回結(jié)果
SomeOther(Maybe(),a,b);//最后一個參數(shù)是引用,用于返回結(jié)果
Caculate(GetTemp(), b);

當聲明這些傳遞返回值的變量為全局的,函數(shù)再將這些引用和指針作為返回值返回給調(diào)用者,我們也需要Caculate調(diào)用之前聲明好所有的引用和指針。函數(shù)返回臨時變量的好處就是不需要聲明變量,也不需要知道生命期。程序員只需要按照最自然的方式,使用最簡單語句就可以完成大量的工作。

然后,移動語義何時會被觸發(fā)。之前我們只是提到了臨時對象,一旦我們用到的是個臨時變量,那么移動構(gòu)造語義就可以得以執(zhí)行。**那么,在C++中如何判斷產(chǎn)生了臨時對象?如何將其用于移動構(gòu)造函數(shù)?是否只有臨時變量可以用于移動構(gòu)造?.....

在C++98/03的語言和庫中,以及存在了一些移動語義相關(guān)的概念:

A. 智能指針的拷貝(auto_ptr "copy")
B. 鏈表拼接(list::splice)
c. 容器內(nèi)的置換(swap on containers)

這些操作都包含了從一個對象到另一個對象的資源轉(zhuǎn)移的過程,唯一欠缺的是統(tǒng)一的語法和語義的支持,來使我們可以使用通用的代碼移動任意的對象。如果能夠任意地使用對象的移動,而不是拷貝,那么標準庫中的很多地方的性能都會大大提高。

左值、右值與右值引用

在C語言中,我們常常會提起左值(lvalue)、右值(rvalue),編譯器報出的錯誤信息里面有時也會包含左值、右值的說法。不過,左值、右值通常不是通過一個嚴謹?shù)亩x而為人所知的,大多數(shù)時候左右值的定義與其判別方法是一體的。一個最典型的判別方法就是,在賦值表達式中,出現(xiàn)在等號左邊的就是"左值",而在等號右邊的,則稱為"右值"。不過C++中,有一個被廣泛認同的說法,那就是可以取值的、有名字的就是左值,反之,不能取地址的、沒有名字的就是右值。更為細致地,在C++11中,右值是由兩個概念構(gòu)成得,一個是將亡值,另一個則是純右值。

純右值就是C++98標準中右值的概念,講的是用于辨別臨時變量和一些不跟對象關(guān)聯(lián)的值。比如非引用返回的函數(shù)返回的臨時變量值,就是一個純右值。一些運算表達式,比如1+3產(chǎn)生的臨時變量值,也是純右值。而不跟對象關(guān)聯(lián)的字面量值,比如:2、'c'、true,也是純右值。此外,類型轉(zhuǎn)換函數(shù)的返回值、lambda表達式等,也都是右值。

而將亡值則是C++11新增的跟右值引用相關(guān)的表達式,這樣表達式通常是將要被移動的對象。比如返回右值引用T&&的函數(shù)返回值、std::move的返回值,或者轉(zhuǎn)換為T&&的類型轉(zhuǎn)換函數(shù)的返回值。而剩下的,可以標識函數(shù)、對象的值都屬于左值。在C++11的程序中,所有的值比屬于左值、將亡值、純右值三者之一。

在C++11中,右值引用就是對一個右值進行引用的類型。事實上,由于右值通常不具有名字,我們也只能通過引用的方式找到它的存在。通常情況下,我們只能是從右值表達式獲得其引用。比如:

T&&a = ReturnRvalue();

假設(shè)ReturnRvalue返回一個右值,我們就聲明了一個名為a的右值引用,其值等于ReturnRvalue函數(shù)返回的臨時變量的值。

右值引用和左值引用都是屬于引用類型。無論是聲明一個左值引用還是右值引用,都必須立即進行初始化。原因可以理解為是引用類型本身自己并不擁有所綁定對象的內(nèi)存,只是該對象的一個別名。左值引用是具名變量值的別名,而右值引用則是不具名(匿名)變量的別名。(也就是說需要找一個寄主)

(問題:我可不可以理解為移動構(gòu)造函數(shù)比拷貝構(gòu)造函數(shù)更適合右值。所以對于對應(yīng)的右值,移動構(gòu)造函數(shù)更容易被匹配到。)

通常情況下,右值引用是不能夠綁定到任何的左值的。

int c
int &&d=c

相對地,在C++98標準中就已經(jīng)出現(xiàn)的左值引用是否可以綁定到右值(由右值進行初始化)?

T&e = ReturnRvalue();
const T&f = ReturnRvalue();

這里一共有11個特性,先只學到這里,因為我需要先對C++先過一遍,然后,再回來看右值和強制轉(zhuǎn)換的部分。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容