1. 程式人生 > >一些常用的C++11新增特性

一些常用的C++11新增特性

C++11新標基於C++程式設計社群的大量實踐經驗,並吸收了很多Boost庫的特性,還對原有C++做了一些改進工作,是學習現代C++程式設計的基礎。這裡參考《C++ Primer Plus 第六版》,對一些常用的C++11新特性做一個總結:

1、統一的初始化

C++11支援對於所有的內建型別和使用者定義型別使用大括號方式的初始化列表,使用初始化列表時,可以新增等號,也可以沒有。

int x = {5}; // 以前只有陣列可以這樣初始化
double y {2.75};
int* ar = new int[4] {2,4,6,8};

classType s1(構造引數1, 構造引數2); // 傳統方式建立一個classType物件
classType s2{構造引數1, 構造引數2}; // C++11方式建立一個classType物件 classType s3 = {構造引數1, 構造引數2}; // C++11方式建立一個classType物件

注意:C++11還提供了一個用於建構函式的模板類initializer_list,STL容器的建構函式就使用了這個類

std::vector<int> a1(10);      // 宣告包含10個int元素的vector容器
std::vector<int> a2={10};     // 宣告容器a2,並初始化一個值為10的元素
std::vector<int
>
a3={4,6,1}; // 宣告容器a3,且初始化三個元素的值為4,6,1

2、右值引用和移動語意

傳統意義的左值,指的是出現在賦值語句左邊的表示式。例如:

int a = 20; // 這裡的a就是一個左值

傳統的C++引用都是使一個識別符號關聯到一個左值,所以傳統的C++引用也被稱為左值引用,例如:

int a = 20;
int& ra = a; // 即ra是變數a的引用(或別名)

C++11新增了右值表示式,採用&&表示。和左值引用(可以關聯到一個左值)類似,右值引用可以關聯到一個右值。具體說:

int a = 10;
int b = 20
; int c = a + b; // 這裡的a+b就是一個右值,以前是沒有辦法對a+b設定引用的 int&& r1 = a + b; // 根據C++11,r1可以關聯到a+b產生的那個臨時物件,a、b的值修改了,r1也不變

右值包括:字面常量(C風格的字串除外)、類似於x+y的表示式、帶有返回值的函式(返回值型別不是引用的那種函式)。基本上就是一些編譯器產生的臨時變數,在C++中尤其多。

右值引用的意義:就是為了和傳統左值引用區分開(好像是廢話啊)。要理解右值引用,需要先了解移動語意
在傳統C++,拷貝建構函式和賦值運算子的過載函式是兩個非常重要的概念。
如果一個類的資料成員中有指標變數,且需要這個類負責這些指標所指向資源的申請和回收。當這個類物件作為函式的引數、返回值或者是一個物件用於給另外一個物件進行初始化時,必須由使用者實現這個類的拷貝建構函式,否則採用編譯器自動生成的拷貝建構函式(淺拷貝),將導致多個物件擁有同一個資源,當銷燬這多個物件時,這些資源面臨重複釋放的問題。所以,在傳統C++的世界裡,我們似乎建立了一種觀念即“淺拷貝是不安全的,深拷貝是安全的”
但是,深拷貝帶來的一個問題就是需要重新申請記憶體,例如,C++編譯器常用臨時物件初始化一個左值物件。當構造這些臨時物件時,本身就執行了一遍記憶體申請的操作,再通過這些臨時物件初始化一個左值物件時,左值物件又會因為深拷貝再重新申請記憶體。很明顯這個過程,對於臨時物件顯得非常的多餘,而C++恰好是一種特別喜歡在背後生成大量臨時物件的語言,所以,當臨時物件、深拷貝、STL這些東西遇到一起的時候,經常會導致軟體效率奇低。
C++提出移動語意的原始目的就是要將臨時物件申請的資源通過移動語意直接轉移給左值物件(幾乎就是淺拷貝),並且,接下來臨時物件在析構的時候,也不會再釋放這些已經轉移的記憶體資源了。

理解了上述關於移動語意的目標了,所以接下來要做的就是,應該採用哪種方式讓編譯器知道什麼時候需要轉移,什麼時候不需要轉移呢?其實也很簡單,那就是,如果使用傳統的左值引用(T&)就表示不需要轉移,如果使用右值引用(T&&)則表示需要轉移。有了右值引用識別符號T&&,程式設計師通過定義移動建構函式移動賦值運算子函式就可以實現移動語意的目標了(因為編譯器不提供預設移動建構函式)。具體如下:

class Useless
{
public:
    Useless(const Useless& f); // 傳統的拷貝建構函式
    Useless(Useless&& f); // 移動建構函式

    Useless& operator=(const Useless& f); // 傳統的賦值運算子過載函式
    Useless& operator=(seless&& f); // 移動賦值運算子函式

private:
    int n;
    char* pc;
}

// 移動建構函式
Useless::Useless(Useless&& f)
{
    n = f.n;
    pc = f.pc;
    f.n = 0;
    f.pc = nullptr;
}

//  移動賦值運算子函式實現
Useless& Useless::operator=(seless&& f)
{
    if (thhis == &f)
        return *this;
    delete []pc;
    n = f.n;
    pc = f.pc;
    f.n = 0;
    f.pc = nullptr;
    return *this;
}

強制移動:C++11新增std::move()函式可以無條件將其引數(左值)強制轉換為右值,轉化為右值後,以前由引數控制的資源就可以順利的轉移給另一個左值物件。需要注意的是,move函式執行完後,引數雖然還在,但是引數管理的資源已經轉移了,所以這個引數不能再參與後續的計算了,否則一般都會出現異常。

總結:對於大多數程式設計師而言,右值引用帶來的最大好處並非是自己編寫使用右值引用的程式碼。而是很多STL利用右值引用實現了移動語意,程式設計師在使用這些STL時,程式碼效能會更好。

3、智慧指標

智慧指標是支撐RAII(獲取資源即初始化)程式設計理念的重要方法。對於C++的RAII,簡單的講,就是所有在堆上分配的資源都委託一個棧物件管理。通過棧物件的自動釋放,實現對上資源的自動釋放,有效防止記憶體洩露。
C++11中的智慧指標包括三種:std::unique_ptrstd::shared_ptrstd::weak_ptr
這三種智慧指標都是模板類,每個例項化的智慧指標物件都是一個用於管理裸指標棧物件。所以,智慧指標物件一般都是直接定義在函式的棧上(不會使用new運算子建立),智慧指標物件的內部實現一定是管理一個原始指標,且這個原始指標最終要指向堆上分配的資源。
因為智慧指標過載指標運算子(-> 和 *)以返回封裝的原始指標,所以,程式設計師可以像使用普通指標一樣使用智慧指標。
std::unique_ptr智慧指標的建立和使用:

class Test
{
public:
    Test(){}
    Test(int x, int y)
    : x_(x),y_(y)
    {}

    void output()
    {
        std::cout << "x=" << x_ << " y=" << y_ << std::endl; 
    }

private:
    int x_;
    int y_;
};

int main()
{
    // C++11中一般只能用下面兩種方式建立unique_ptr
    std::unique_ptr<Test> pT4 = std::unique_ptr<Test>(new Test(4,4));
    std::unique_ptr<Test> pT4(new Test(4,4));

    std::unique_ptr<Test[]> pT3(new Test[4]); // 建立被智慧指標管理的物件陣列

    // unique_ptr獨佔指標物件,並保證指標所指物件生命週期與其一致。
    // unique_ptr物件禁止複製,無法通過值傳遞到函式(可以傳遞引用),
    // 也無法用於需要副本的任何標準模板庫(STL)演算法。
    std::unique_ptr<Test> pT5 = pT4; // 這一行會產生編譯錯誤
    std::unique_ptr<Test> pT6; // 允許建立一個空物件

    pT4->output(); // 像使用普通指標一樣,這一行可以正常執行
    pT6->output(); // pT6還是一個空物件,所以這一行會產生異常
    pT6= std::move(pT4); // pT4管理的指標指向的資源被強制轉移到pT6
    pT6->output(); // 此時pT6已不是空物件,所以這一行可以正常執行
    pT4->output(); // 此時pT4變成了空物件,所以這一行會產生異常

    if (pT4)       // 可以使用if語句判斷一個空物件,就像判斷普通指標一樣
        pT4->output();

    // 函式的返回值可以是unique_ptr,可用於初始化另一個unique_ptr物件
    std::unique_ptr<Test> pT7 = clone(7); 
    pT7->output();

    std::vector<std::unique_ptr<Test>> vec; // 建立一個儲存智慧指標物件的容器,
    vec.push_back(std::move(pT7)); // 使用移動語義將pT7的資源轉移到STL容器中

    pT6.reset(); // 解除pT6關聯的原始指標,則pT6變為空物件

}

std::unique_ptr<Test> clone(int p)
{
    std::unique_ptr<Test> pTest(new Test(p, p));
    return pTest;    // 返回unique_ptr
}

std::unique_ptr的成員函式:

函式名 作用
release 返回一個指向被管理物件的指標,並釋放所有權
reset 替換所管理的物件
swap 交換所管理的物件
get 返回指向被管理物件的指標
get_deleter 返回刪除器,用於被管理物件的析構
operator bool 檢查是否有關聯的被管理物件
operator* 對指向被管理物件的指標進行解引用
operator-> 對指向被管理物件的指標進行解引用
operator[] 提供對所管理陣列的按索引訪問

總結std::unique_ptr的特點:
1、堆資源的管理者始終只有一個,一般不會導致堆資源的生存週期被意外的延長。
2、函式內部建立堆資源,並將堆資源轉移到函式外部使用和管理,且不會產生堆資源所有權混亂的問題。
3、一般情況下使用std::unique_ptr就足夠支援堆資源自動管理了,如果有多執行緒共同使用堆資源,或者是智慧指標物件既要儲存到STL容器中,同時還要在容器外部使用,此時就必須考慮使用std::shared_ptr(注意不要引起迴圈引用)。

int main()
{
    // C+11允許用下面三種方式建立shared_ptr
    std::shared_ptr<Test>sp1(std::make_shared<Test>(1,1));
    std::shared_ptr<Test>sp2 = std::make_shared<Test>(2,2);
    std::shared_ptr<Test>sp3(new Test(3,3));

    // shared_ptr允許複製,此時shared_ref_count=2
    std::shared_ptr<Test>sp4 = sp1;

    // weak_ptr總是通過shared_ptr複製,但是不會導致的shared_ref_count增加
    std::weak_ptr<Test>wp5 = sp4;
    std::shared_ptr<Test>sp6 = wp5.lock();// 使用weak_ptr之前必須先通過lock觀察
    if (sp6)
    {
        sp6->output();
    }
}

把一個 shared_ptr物件傳遞給另一個函式的幾種方式
1、通過值傳遞shared_ptr,將會呼叫智慧指標的複製建構函式生成一個屬於被呼叫方的shared_ptr,此時智慧指標的引用計數會增加,被呼叫方也成為了一個所有者。這次操作中有少量的開銷,很大程度上取決於傳遞了多少 shared_ptr 物件。當呼叫方和被呼叫方之間的程式碼協定 (隱式或顯式) 要求被呼叫方是所有者,使用此選項。
2、通過引用或常量引用來傳遞 shared_ptr。在這種情況下,引用計數不增加,並且只要呼叫方不超出範圍(正常情況下,呼叫函式和被呼叫函式都在一個執行緒,肯定不會超出範圍),被呼叫方就可以訪問指標。或者,被呼叫方可以決定建立一個基於引用的 shared_ptr,從而成為一個共享所有者。當呼叫者並不知道被被呼叫方,或當您必須傳遞一個 shared_ptr,並希望避免由於效能原因的複製操作,請使用此選項。
3、獲取被 shared_ptr管理指標,並向函式傳遞這個原始指標。這使得被呼叫方可以根據指標使用物件,但原來的 shared_ptr中的引用計數不會增加或物件的生存期也不會擴充套件生。如果被呼叫方用接收到的原始指標建立一個shared_ptr,則新的 shared_ptr 是獨立於原來的(一般都不應該這麼幹)。

4、Lambda函式

以前C++中,一些短小的函式,可以寫成行內函數,到了C++11中,則可以使用Lambda函式來表示。
所以,lambda函式仍然是一個可呼叫的程式碼單元,可以將其理解為一個未命名的行內函數,C++11中Lambda表示式具體形式如下:
[capture](parameters)->return-type{body}
如果沒有引數,空的圓括號()可以省略,如果函式沒有返回值得話,“->return-type”也可以省略。

[](int x, int y) -> int { int z = x + y; return z; } // 一個完整的lambda函式定義
[](int x, int y) { return x + y; } // 隱式返回型別
[](int& x) { ++x; }   // 如果沒有return語句,則lambda函式的返回型別可省略
[]() { ++global_x; }  // 沒有引數,僅訪問某個全域性變數
[]{ ++global_x; }     // 沒有引數可省略了(),僅訪問某個全域性變數

Lambda函式可以引用在它之外宣告的變數. 這些變數的集合叫做一個閉包. 閉包被定義在Lambda表示式宣告中的方括號[]內. 這個機制允許這些變數被按值或按引用捕獲。

[]        //未定義變數.試圖在Lambda內使用任何外部變數都是錯誤的.
[x, &y]   //x 按值捕獲, y 按引用捕獲.
[&]       //用到的任何外部變數都隱式按引用捕獲
[=]       //用到的任何外部變數都隱式按值捕獲
[&, x]    //x顯式地按值捕獲. 其它變數按引用捕獲
[=, &z]   //z按引用捕獲. 其它變數按值捕獲

從使用上看,Lambda函式的作用和函式指標或函式物件相似。具體的使用方式如下:

int main()
{
    std::vector<int> number={1,2,3,4,5,6,7,8,9,10};

    int count = 0;
    std::for_each(number.begin(), number.end(), 
                  [&count](int x){count = count+x;});
    printf("count = %d \n", count);

    // 也可以給Lambda函式定義一個名字,這樣可以通過名字呼叫Lambda函式
    auto func = [] () { printf("Hello world"); };  
    func(); 
}

如上所示,Lambda函式的好處就是函式的定義和函式的使用在一起,所以,一般要求Lambda函式必須很短。

5、包裝器

如果理解函式指標、函式物件、(具名)Lambda函式。我們會突然發現,C++中有多種類似函式的呼叫形式,這些呼叫形式的存在對於普通程式原來講其實無所謂,夠用就行。但是對於模板化程式設計來說,太多的呼叫形式會導致使用了這些呼叫形式的模板被例項化多次。所以,C++11提出了將多種函式呼叫形式統一的包裝器std::function。

所以,簡單的說,std::function是對C++中各種可呼叫實體(普通函式、Lambda表示式、函式指標、以及其它函式物件等)的一個統一封裝,形成一個新的統一可呼叫物件(std::function物件)(主要是對模板化程式設計很有意義)。

#include <functional>
#include <iostream>
using namespace std;

std::function< int(int)> Functional;

// 普通函式
int TestFunc(int a)
{
    return a;
}

// Lambda表示式
auto lambda = [](int a)->int{ return a; };

// 仿函式(functor)
class Functor
{
public:
    int operator()(int a)
    {
        return a;
    }
};

// 1.類成員函式
// 2.類靜態函式
class TestClass
{
public:
    int ClassMember(int a) { return a; }
    static int StaticMember(int a) { return a; }
};

int main()
{
    // 普通函式
    Functional = TestFunc;
    int result = Functional(10);
    cout << "普通函式:"<< result << endl;

    // Lambda表示式
    Functional = lambda;
    result = Functional(20);
    cout << "Lambda表示式:"<< result << endl;

    // 仿函式
    Functor testFunctor;
    Functional = testFunctor;
    result = Functional(30);
    cout << "仿函式:"<< result << endl;

    // 類成員函式
    TestClass testObj;
    Functional = std::bind(&TestClass::ClassMember, testObj, std::placeholders::_1);
    result = Functional(40);
    cout << "類成員函式:"<< result << endl;

    // 類靜態函式
    Functional = TestClass::StaticMember;
    result = Functional(50);
    cout << "類靜態函式:"<< result << endl;

    return 0;
}

std::bind機制可以預先把已有的變數和可呼叫實體的某些引數繫結,產生一個新的可呼叫實體,這種機制在回撥函式的使用過程中也頗為有用。以前類的成員函式因為引數中隱含this指標,所以無法直接註冊為回撥函式,現在可以通過bind提前繫結this指標註冊為回撥函式bind的返回值是可呼叫實體,可以直接賦給std::function物件。

auto newConnectionCallback_ = std::bind(&EchoServer::newConnection, this, std::placeholders::_1);

bind能夠在繫結時候就同時繫結一部分引數,未提供的引數則使用佔位符表示,然後在執行時傳入實際的引數值。如上所示,EchoServer::newConnection表示被包裝的函式,第一個引數表示繫結函式所屬物件的this指標。std::placeholders::_1表示是新函式的第1個引數(依次類推)。

bind的返回值是可呼叫實體,可以直接賦給std::function物件。