C++11的一些常見特性
因為面試被問到了,C++的新特性,但從未歸納過,故將整理c++11,c++17,c++20的常見特性,並用例子實現一遍。加油!!!
1.nullptr
C++用nullptr代替NULL,原因NULL在C++中會被定義為0或(void*)0,取決於編譯器。
C++ 不允許直接將 void * 隱式轉換到其他型別,但如果 NULL 被定義為 ((void*)0),那麼當編譯char *ch = NULL;時,NULL 只好被定義為 0。
從而會引發過載的一些問題,例如
void foo(char *a); void foo(int a);
為了避免這塊從而引入nullptr,nullptr 的型別為 nullptr_t,能夠隱式的轉換為任何指標或成員指標的型別,也能和他們進行相等或者不等的比較。
const class nullptr_t { public: template<class T> inline operator T*() const { return 0; } template<class C, class T> inline operator T C::*() const { return 0; } private: void operator&() const; } nullptr = {};
2.型別推導
C++11 引入了 auto 和 decltype 這兩個關鍵字實現了型別推導,讓編譯器來操心變數的型別。
auto
auto 在很早以前就已經進入了 C++,但是他始終作為一個儲存型別的指示符存在,與 register 並存。在傳統 C++ 中,如果一個變數沒有宣告為 register 變數,將自動被視為一個 auto 變數。而隨著 register 被棄用,對 auto 的語義變更也就非常自然了。(register以前就是將變數放到暫存器中,不能直接用地址取,對register使用取地址操作會讓register失效,反之預設是auto,現已廢棄)
注意:auto只能推匯出資料的不加引用(&)的資料型別
但是實測可以推匯出const char *和char *,推導不出const char和char,說明頂層const的const會被忽略
此外,auto 還不能用於推導陣列型別
auto 的推導規則
- 在不宣告為引用或指標時,auto會忽略等號右邊的引用型別和const、volatile限定
- 在宣告為引用或者指標時,auto會保留等號右邊的引用和const、volatile屬性
auto i; // error: declaration of variable 't' with deduced type 'auto' requires an initializer //因此我們在使用auto時,必須對該變數進行初始化。 auto i= 0; //0為int型別,auto自動推匯出int型別 auto j = 2.0; //auto 自動推匯出型別為float int a = 0; auto b = a; //a 為int型別 auto &c = a; //c為a的引用 auto *d = &a; //d為a的指標 auto i = 1, b = "hello World"; //error: 'auto' deduced as 'int' in declaration of 'i' and deduced as 'const char *' in declaration of 'b' /* auto 作為成員變數的使用*/ class test_A { public: test_A() {} auto a = 0; //error: 'auto' not allowed in non-static class member static auto b = 0; //error: non-const static data member must be initialized out of line static const auto c = 0; }; /*c11 中的使用*/ auto func = [&] { cout << "xxx"; }; // 不關心lambda表示式究竟是什麼型別 auto asyncfunc = std::async(std::launch::async, func);
decltype
decltype用於推導表示式型別,這裡只用於編譯器分析表示式的型別,表示式實際不會進行運算
注意:decltype不會像auto一樣忽略引用和const、volatile屬性,decltype會保留表示式的引用和const、volatile屬性
decltype 的推導規則
對於decltype(exp)有:
- exp是表示式,decltype(exp)和exp型別相同
- exp是函式呼叫,decltype(exp)和函式返回值型別相同
- 其它情況,若exp是左值,decltype(exp)是exp型別的左值引用
int a = 0, b = 0; decltype(a + b) c = 0; // c是int,因為(a+b)返回一個右值 decltype(a += b) d = c;// d是int&,因為(a+=b)返回一個左值 d = 20; cout << "c " << c << endl; // 輸出c 20
auto 與 decltype 配合
decltype(auto)是C++14新增的型別指示符,可以用來宣告變數以及指示函式返回型別。在使用時,會將 “=”號左邊的表示式替換掉auto,再根據decltype的語法規則來確定型別。舉個例子:int e = 4; const int* f = &e; // f是底層const decltype(auto) j = f;//j的型別是const int* 並且指向的是e
基於範圍的 for 迴圈
C++11 引入了基於範圍的迭代寫法,我們擁有了能夠寫出像 Python 一樣簡潔的迴圈語句。
// & 啟用了引用 for(auto &i : arr) { std::cout << i << std::endl; }
初始化列表
C++11 提供了統一的語法來初始化任意的物件,例如:
struct A { int a; float b; }; struct B { B(int _a, float _b): a(_a), b(_b) {} private: int a; float b; }; A a {1, 1.1}; // 統一的初始化語法 B b {2, 2.2};對於在函式體中初始化,是在所有的資料成員被分配記憶體空間後才進行的。 列表初始化是給資料成員分配記憶體空間時就進行初始化,就是說分配一個數據成員只要冒號後有此資料成 員的賦值表示式(此表示式必須是括號賦值表示式),那麼分配了記憶體空間後在進入函式體之前給資料成員 賦值,就是說初始化這個資料成員此時函式體還未執行。
預設模板引數
//這裡用到了auto和decltype的結合推導
template<typename T = int, typename U = int> auto add(T x, U y) -> decltype(x+y) { return x+y; }
Lambda 表示式
Lambda 表示式,實際上就是提供了一個類似匿名函式的特性,而匿名函式則是在需要一個函式,但是又不想費力去命名一個函式的情況下去使用的。
Lambda 表示式的基本語法如下:
[ caputrue ] ( params ) opt -> ret { body; };
1) capture是捕獲列表;
2) params是引數表;(選填)
3) opt是函式選項;可以填mutable,exception,attribute(選填)
mutable說明lambda表示式體內的程式碼可以修改被捕獲的變數,並且可以訪問被捕獲的物件的non-const方法。
exception說明lambda表示式是否丟擲異常以及何種異常。
attribute用來宣告屬性。
4) ret是返回值型別(拖尾返回型別)。(選填)
5) body是函式體。
捕獲列表:lambda表示式的捕獲列表精細控制了lambda表示式能夠訪問的外部變數,以及如何訪問這些變數。
1) []不捕獲任何變數。
2) [&]捕獲外部作用域中所有變數,並作為引用在函式體中使用(按引用捕獲)。
3) [=]捕獲外部作用域中所有變數,並作為副本在函式體中使用(按值捕獲)。注意值捕獲的前提是變數可以拷貝,且被捕獲的變數在 lambda 表示式被建立時拷貝,而非呼叫時才拷貝。如果希望lambda表示式在呼叫時能即時訪問外部變數,我們應當使用引用方式捕獲。
int a = 0; auto f = [=] { return a; }; a+=1; cout << f() << endl; //輸出0 int a = 0; auto f = [&a] { return a; }; a+=1; cout << f() <<endl; //輸出1
4) [=,&foo]按值捕獲外部作用域中所有變數,並按引用捕獲foo變數。
5) [bar]按值捕獲bar變數,同時不捕獲其他變數。
6) [this]捕獲當前類中的this指標,讓lambda表示式擁有和當前類成員函式同樣的訪問許可權。如果已經使用了&或者=,就預設新增此選項。捕獲this的目的是可以在lamda中使用當前類的成員函式和成員變數。
class A { public: int i_ = 0; void func(int x,int y){ auto x1 = [] { return i_; }; //error,沒有捕獲外部變數 auto x2 = [=] { return i_ + x + y; }; //OK auto x3 = [&] { return i_ + x + y; }; //OK auto x4 = [this] { return i_; }; //OK auto x5 = [this] { return i_ + x + y; }; //error,沒有捕獲x,y auto x6 = [this, x, y] { return i_ + x + y; }; //OK auto x7 = [this] { return i_++; }; //OK }; int a=0 , b=1; auto f1 = [] { return a; }; //error,沒有捕獲外部變數 auto f2 = [&] { return a++ }; //OK auto f3 = [=] { return a; }; //OK auto f4 = [=] {return a++; }; //error,a是以複製方式捕獲的,無法修改 auto f5 = [a] { return a+b; }; //error,沒有捕獲變數b auto f6 = [a, &b] { return a + (b++); }; //OK auto f7 = [=, &b] { return a + (b++); }; //OK
注意f4,雖然按值捕獲的變數值均複製一份儲存在lambda表示式變數中,修改他們也並不會真正影響到外部,但我們卻仍然無法修改它們。如果希望去修改按值捕獲的外部變數,需要顯示指明lambda表示式為mutable。被mutable修飾的lambda表示式就算沒有引數也要寫明引數列表。
原因:lambda表示式可以說是就地定義仿函式閉包的“語法糖”。它的捕獲列表捕獲住的任何外部變數,最終會變為閉包型別的成員變數。按照C++標準,lambda表示式的operator()預設是const的,一個const成員函式是無法修改成員變數的值的。而mutable的作用,就在於取消operator()的const。
lambda表示式的大致原理:每當你定義一個lambda表示式後,編譯器會自動生成一個匿名類(這個類過載了()運算子),我們稱為閉包型別(closure type)。那麼在執行時,這個lambda表示式就會返回一個匿名的閉包例項,是一個右值。所以,我們上面的lambda表示式的結果就是一個個閉包。對於複製傳值捕捉方式,類中會相應新增對應型別的非靜態資料成員。在執行時,會用複製的值初始化這些成員變數,從而生成閉包。對於引用捕獲方式,無論是否標記mutable,都可以在lambda表示式中修改捕獲的值。至於閉包類中是否有對應成員,C++標準中給出的答案是:不清楚的,與具體實現有關。
lambda表示式是不能被賦值的:
auto a = [] { cout << "A" << endl; }; auto b = [] { cout << "B" << endl; }; a = b; // 非法,lambda無法賦值 auto c = a; // 合法,生成一個副本
lambda表示式一個更重要的應用是其可以用於函式的引數,通過這種方式可以實現回撥函式。
最常用的是在STL演算法中,比如你要統計一個數組中滿足特定條件的元素數量,通過lambda表示式給出條件,傳遞給count_if函式:
int value = 3; vector<int> v {1, 3, 5, 2, 6, 10}; int count = std::count_if(v.beigin(), v.end(), [value](int x) { return x > value; });
再比如你想生成斐波那契數列,然後儲存在陣列中,此時你可以使用generate函式,並輔助lambda表示式:
vector<int> v(10); int a = 0; int b = 1; std::generate(v.begin(), v.end(), [&a, &b] { int value = b; b = b + a; a = value; return value; }); // 此時v {1, 1, 2, 3, 5, 8, 13, 21, 34, 55}
當需要遍歷容器並對每個元素進行操作時:
std::vector<int> v = { 1, 2, 3, 4, 5, 6 }; int even_count = 0; for_each(v.begin(), v.end(), [&even_count](int val){ if(!(val & 1)){ ++ even_count; } }); std::cout << "The number of even is " << even_count << std::endl;
新增容器
std::array棧上的陣列
std::forward_list 單向連結串列(不提供size()函式)
std::unordered_map/std::unordered_multimap 和 std::unordered_set/std::unordered_multiset。
無序容器中的元素是不進行排序的,內部通過 Hash 表實現,插入和搜尋元素的平均複雜度為 O(constant)。
std::tuple元組,不可變的字典
右值引用和move語義
為什麼要有右值引用。
1.效率性,如果一個變數不用了,我們用移動構造可以複用之前的記憶體,從而只需要實現指標的轉移,而不是重新去申請一塊記憶體進行賦值,可能會有阻塞(即使很小),效率慢。
2.安全性,當我們用左值可能呼叫類的成員函式,這會導致不可預知的行為,右值卻是非常安全的,因為複製建構函式之後,我們不能再使用這個臨時物件了,因為這個轉移後的臨時物件會在下一行之前銷燬掉。
td::move僅僅是簡單地將左值轉換為右值,它本身並沒有轉移任何東西。它僅僅是讓物件可以轉移。
當然,如果你在使用了mova(a)之後,還繼續使用a,那無疑是搬起石頭砸自己的腳,還是會導致嚴重的執行錯誤。
總之,std::move(some_lvalue)將左值轉換為右值(可以理解為一種型別轉換),使接下來的轉移成為可能。