在面試中,經常被問的一個問題就是:你了解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;
}
和拷貝構造函數類似,有幾點需要注意:
- 參數(右值)的符號必須是&&
- 參數(右值)不可以是常量,因為我們需要修改右值
- 參數(右值)的資源鏈接和標記必須修改,否則,右值的析構函數就會釋放資源。轉移到新對象的資源也就無效了。
標准庫函數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;
};
需要記住的事,在以下三種情況下會引起引用計數的變更:
- 調用構造函數時: SmartPointer p(new Object());
- 賦值構造函數時: SmartPointer p(const SmartPointer &p);
- 賦值時: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關鍵字的使用這里不做詳細介紹,如果用過自然可以說出,沒有用過的話這部分內容也不應該和面試官討論。