1. 程式人生 > >《隨筆十一》—— C++中的 “ C++ 11 新特性梳理 ”

《隨筆十一》—— C++中的 “ C++ 11 新特性梳理 ”

 

在面試中,經常被問的一個問題就是:你瞭解C++11哪些新特性?一般而言,回答以下四個方面就夠了:

“語法糖”:nullptr, auto自動型別推導,範圍for迴圈,初始化列表, lambda表示式等

右值引用和移動語義

智慧指標

C++11多執行緒程式設計:thread庫及其相配套的同步原語mutex, lock_guard, condition_variable, 以及非同步std::furture

“語法糖”

這部分內容一般是一句話帶過的,但是有時候也需要說一些,比較重重要的就是auto和lambda。


auto自動型別推導


 

C語言也有auto關鍵字,但是其含義只是與static變數做一個區分,一個變數不指定的話預設就是auto。。因為很少有人去用這個東西,所以在C++11中就把原有的auto功能給廢棄掉了,而變成了現在的自動型別推導關鍵字。用法很簡單不多贅述,比如寫一個auto a = 3, 編譯器就會自動推導a的型別為int. 在遍歷某些STL容器的時候,不用去宣告那些迭代器的型別,也不用去使用typedef就能很簡潔的實現遍歷了。

auto的使用有以下兩點必須注意:

auto宣告的變數必須要初始化,否則編譯器不能判斷變數的型別。

auto不能被宣告為返回值,auto不能作為形參,auto不能被修飾為模板引數

 

關於效率: auto實際上實在編譯時對變數進行了型別推導,所以不會對程式的執行效率造成不良影響。另外,auto並不會影響編譯速度,因為編譯時本來也要右側推導然後判斷與左側是否匹配。


關於具體的推導規則,可以參考這裡 http://www.cnblogs.com/boydfd/p/4950334.html


lambda表示式


lambda表示式是匿名函式,可以認為是一個可執行體 functor,語法規則如下:

[捕獲區](引數區){程式碼區};

auto add = [](int a, int b) {return a + b};

就我的理解而言,捕獲的意思即為將一些變數展開使得為lambda內部可見,具體方式有如下幾種:

[a,&b] 其中 a 以複製捕獲而 b 以引用捕獲。

[this] 以引用捕獲當前物件( *this )

[&] 以引用捕獲所有用於 lambda 體內的自動變數,並以引用捕獲當前物件,若存在

[=] 以複製捕獲所有用於 lambda 體內的自動變數,並以引用捕獲當前物件,若存在

[] 不捕獲,大部分情況下不捕獲就可以了

一般使用場景:sort等自定義比較函式、用thread起簡單的執行緒。

 


右值引用與移動語義


右值引用是C++11新特性,它實現了轉移語義和完美轉發,主要目的有兩個方面

消除兩個物件互動時不必要的物件拷貝,節省運算儲存資源,提高效率

能夠更簡潔明確地定義泛型函式,C++中的變數要麼是左值、要麼是右值。通俗的左值定義指的是非臨時變數,而左值指的是臨時物件。左值引用的符號是一個&,右值引用是兩個&&

 


移動語義


轉移語義可以將資源 (堆、系統物件等)從一個物件轉移到另一個物件,這樣可以減少不必要的臨時物件的建立、拷貝及銷燬。移動語義與拷貝語義是相對的,可以類比檔案的剪下和拷貝。在現有的C++機制中,自定義的類要實現轉移語義,需要定義移動建構函式,還可以定義轉移賦值操作符。

 

以string類的移動建構函式為例

MyString(MyString&& str) {
    std::cout << "Move Ctor source from " << str._data << endl;
    _len = str._len;
    _data = str._data;
    str._len = 0;
    str._data = NULL;
}

和拷貝建構函式類似,有幾點需要注意:

1、引數(右值)的符號必須是&&

2、引數(右值)不可以是常量,因為我們需要修改右值

3、引數(右值)的資源連結和標記必須修改,否則,右值的解構函式就會釋放資源。轉移到新物件的資源也就無效了。

標準庫函式std::move --- 將左值變成一個右值

編譯器只對右值引用才能呼叫移動建構函式,那麼如果已知一個命名物件不再被使用,此時仍然想呼叫它的移動建構函式,也就是把一個左值引用當做右值引用來使用,該怎麼做呢?用std::move,這個函式以非常簡單的方式將左值引用轉換為右值引用。

 


完美轉發 Perfect Forwarding


完美轉發使用這樣的場景:需要將一組引數原封不動地傳遞給另一個函式。原封不動不僅僅是引數的值不變,在C++中還有以下的兩組屬性:​​​​​​​

左值 / 右值

const / non-const
完美轉發就是在引數傳遞過程中,所有這些屬性和引數值都不能改變。在泛型函式中,這樣的需求十分普遍。
為了保證這些屬性,泛型函式需要過載各種版本,左值右值不同版本,還要分別對應不同的const關係,但是如果只定義一個右值引用引數的函式版本,這個問題就迎刃而解了,原因在於:


C++11對T&&的型別推導: 右值實參為右值引用,左值實參仍然為左值

 


智慧指標


核心思想:為防止記憶體洩露等問題,用一個物件來管理野指標,使得在該物件構造時獲得該指標管理權,析構時自動釋放(delete).


基於此思想C++98提供了第一個智慧指標:auto_ptr


auto_ptr基於所有權轉移的語義,即將一個就的auto_ptr賦值給另外一個新的auto_ptr時,舊的那一個就不再擁有該指標的控制權(內部指標被賦值為null),那麼這就會帶來一些根本性的破綻:

函式引數傳遞時,會有隱式的賦值,那麼原來的auto_ptr自動失去了控制權

自我賦值時,會將自己內部指標賦值為null,造成bug

因為auto_ptr的各種bug,C++11標準基本廢棄了這種型別的智慧指標,轉而帶來了三種全新的智慧指標:

shared_ptr,基於引用計數的智慧指標,會統計當前有多少個物件同時擁有該內部指標;當引用計數降為0時,自動釋放

weak_ptr,基於引用計數的智慧指標在面對迴圈引用的問題將無能為力,因此C++11還引入weak_ptr與之配套使用,weak_ptr只引用,不計數

unique_ptr:遵循獨佔語義的智慧指標,在任何時間點,資源智慧唯一地被一個unique_ptr所佔有,當其離開作用域時自動析構。資源所有權的轉移只能通過std::move()而不能通過賦值

展現知識廣度:

Java等語言的中垃圾回收機制
垃圾收集器將記憶體視為一張有向可達圖,該圖的節點被分成一組根節點和一組堆節點。每個堆節點對應一個記憶體分配塊,當存在一條從任意根節點出發到達某堆節點p的有向路徑時,我們就說節點p是可達的。在任意時刻,不可達節點屬於垃圾。垃圾收集器通過維護這一張圖,並通過定期地釋放不可達節點並將它們返回給空閒連結串列,來定期地回收它們。
所以,聊到這裡還可以引申malloc的分配機制、夥伴系統、虛擬記憶體等等概念​​​​​​​

這裡給出一個shared_ptr的簡單實現:​​​​​​​

class Counter {
    friend class SmartPointPro;
public:
    Counter(){
        ptr = NULL;
        cnt = 0;
    }
    Counter(Object* p){
        ptr = p;
        cnt = 1;
    }
    ~Counter(){
        delete ptr;
    }

private:
    Object* ptr;
    int cnt;
};

class SmartPointPro {
public:
    SmartPointerPro(Object* p){
        ptr_counter = new Counter(p);
    }
    SmartPointerPro(const SmartPointerPro &sp){
        ptr_counter = sp.ptr_counter;
        ++ptr_counter->cnt;
    }
    SmartPointerPro& operator=(const SmartPointerPro &sp){
        ++sp.ptr_counter->cnt;
        --ptr_counter.cnt;
        if(ptr_counter.cnt == 0)
            delete ptr_counter;
        ptr_counter = sp.ptr_counter;
    }
    ~SmartPointerPro(){
        --ptr_counter->cnt;
        if(ptr_counter.cnt == 0)
            delete ptr_counter;
    }
private:
    Counter *ptr_counter;
};

需要記住的事,在以下三種情況下會引起引用計數的變更:

1、呼叫建構函式時: SmartPointer p(new Object());

2、賦值建構函式時: SmartPointer p(const SmartPointer &p);

3、賦值時:SmartPointer p1(new Object()); SmartPointer p2 = p1;


C++11多執行緒程式設計


std::thread可以和普通函式和lambda表示式搭配使用。它還允許向執行緒執行函式傳遞任意多引數。

#include <thread>
void func() {
 // do some work here
}
int main() {
   std::thread thr(func);
   t.join();
   return 0;
} 

上面就是一個最簡單的使用std::thread的例子,函式func()在新起的執行緒中執行。呼叫join()函式是為了阻塞主執行緒,直到這個新起的執行緒執行完畢。執行緒函式的返回值都會被忽略,但執行緒函式可以接受任意數目的輸入引數。


std::thread的其他成員函式


joinable(): 判斷執行緒物件是否可以join,當執行緒物件被析構的時候如果物件``joinable()==true會導致std::terminate`被呼叫。

join(): 阻塞當前程序(通常是主執行緒),等待建立的新執行緒執行完畢被作業系統回收。

detach(): 將執行緒分離,從此執行緒物件受作業系統管轄。

 


執行緒管理函式


除了std::thread的成員函式外,在std::this_thread名稱空間也定義了一系列函式用於管理當前執行緒。

函式名 作用
get_id 返回當前執行緒的id
yield 告知排程器執行其他執行緒,可用於當前處於繁忙的等待狀態。相當於主動讓出剩下的執行時間,具體的排程演算法取決於實現
sleep_for 指定的一段時間內停止當前執行緒的執行
sleep_until 停止當前執行緒的執行直到指定的時間點

至於mutex, condition_variable等同步原語以及future關鍵字的使用這裡不做詳細介紹,如果用過自然可以說出,沒有用過的話這部分內容也不應該和麵試官討論。