C++11新特性梳理


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

  • “語法糖”:nullptr, auto自動類型推導,范圍for循環,初始化列表, lambda表達式等
  • 右值引用和移動語義
  • 智能指針
  • C++11多線程編程:thread庫及其相配套的同步原語mutex, lock_guard, condition_variable, 以及異步std::furture

1. “語法糖”

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

auto自動類型推導

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

  • auto聲明的變量必須要初始化,否則編譯器不能判斷變量的類型。
  • auto不能被聲明為返回值,auto不能作為形參,auto不能被修飾為模板參數

關於效率: auto實際上實在編譯時對變量進行了類型推導,所以不會對程序的運行效率造成不良影響。另外,auto並不會影響編譯速度,因為編譯時本來也要右側推導然后判斷與左側是否匹配。
關於具體的推導規則,可以參考這里

lambda表達式

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

[捕獲區](參數區){代碼區};

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

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

  • [a,&b] 其中 a 以復制捕獲而 b 以引用捕獲。
  • [this] 以引用捕獲當前對象( *this )
  • [&] 以引用捕獲所有用於 lambda 體內的自動變量,並以引用捕獲當前對象,若存在
  • [=] 以復制捕獲所有用於 lambda 體內的自動變量,並以引用捕獲當前對象,若存在
  • [] 不捕獲,大部分情況下不捕獲就可以了

一般使用場景:sort等自定義比較函數、用thread起簡單的線程。

2. 右值引用與移動語義

右值引用是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&&的類型推導: 右值實參為右值引用,左值實參仍然為左值

3.智能指針

核心思想:為防止內存泄露等問題,用一個對象來管理野指針,使得在該對象構造時獲得該指針管理權,析構時自動釋放(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多線程編程

線程 #include <thread>

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關鍵字的使用這里不做詳細介紹,如果用過自然可以說出,沒有用過的話這部分內容也不應該和面試官討論。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM