1. 程式人生 > 其它 >C++11的一些常見特性

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 的推導規則

  1. 在不宣告為引用或指標時,auto會忽略等號右邊的引用型別和const、volatile限定
  2. 在宣告為引用或者指標時,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)有:

  1. exp是表示式,decltype(exp)和exp型別相同
  2. exp是函式呼叫,decltype(exp)和函式返回值型別相同
  3. 其它情況,若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)將左值轉換為右值(可以理解為一種型別轉換),使接下來的轉移成為可能。