C++11拾穗

吳尼瑪發表於2019-02-20

C++11新關鍵字

alignas:指定對齊大小

alignof:獲取對齊大小

decltype

auto(重新定義):可作為返回值型別後置時的佔位符

static_assert:靜態斷言

using(重新定義):型別別名或者模板別名

noexcept:宣告函式不可以丟擲任何異常

export(棄用,不過未來可能留作他用)

nullptr

constexpr:可在在編譯期確認的常量表示式

thread_local:等價於TLS

快速初始化成員變數

C++11中支援使用等號 = 或者花括號 {} 進行就地的(也就是在宣告的時候)非靜態成員變數初始化。例如:

struct init{
    int a = 1;
    double b {1.2};
}
複製程式碼

在C++11標準支援了就地初始化非靜態成員的同時,初始化列表這個手段也被保留下來了。只不過初始化列表總是看起來“後作用於”非靜態成員。

final/override控制

C++11提供關鍵字final,作用是使派生類不可覆蓋它所修飾的虛擬函式。final關鍵字也可 用於基類中,但是這樣定義的虛擬函式就沒有意義了。final通常就是在繼承關係的“中途”終止派生類的過載。

在C++中對於基類宣告為virtual的函式,之後的過載版本都不需要再宣告該過載函式為virtual。即使在派生類中宣告瞭virtual,該關鍵字也是編譯器可以忽略的。另外,有的虛擬函式會“跨層”,沒有在父類中宣告的介面有可能是祖先的虛擬函式介面。所以C++11引入了虛擬函式描述符override,來幫助程式設計師寫繼承結構複雜的型別。如果派生類在虛擬函式宣告時使用了override描述符,那麼該函式必須過載其基類中的同名函式,否則程式碼將無法通過編譯。

繼承建構函式

C++11提供了繼承建構函式,在派生類中使用using宣告,就可以把基類中的建構函式繼承到派生類中。 但其實質是編譯器自動生成程式碼,通過呼叫父類建構函式來實現,不是真正意義上的“繼承”,僅僅是為了減少程式碼書寫量。

class A {
    A(int i) {}
    A(double d, int i) {}
    A(char *c , double d, int i) {}
    //... 更多建構函式
};

class B : A {
    using A::A; //繼承建構函式
    //...
}
複製程式碼

委派建構函式

C++11提供委派建構函式,可以簡化多建構函式的類的編寫。如果我們能將一個建構函式設定為“基準版本”,則其他建構函式可以通過委派“基準版本”來進行初始化。我們將這個“基準版本”稱為目標建構函式。

class Info {
public:
    Info() { InitRest(); }//目標建構函式
    Info(int i) : Info() { type = i; }//委派建構函式
    Info(char c) : Info() { name = c; }//委派建構函式
    
private:
    void InitRest(); { /* 其他初始化 */}
    int type {1};
    char name {'a'};
    //...
}
複製程式碼

注意:委派建構函式不能有初始化列表。如果委派建構函式要給變數賦初值,初始化程式碼必須放在函式體中。

在使用委派建構函式時,建議程式設計師抽象出最為“通用”的行為作為目標建構函式。

移動語義

拷貝構造與移動構造
在C++11中,這樣的“偷走”臨時變數中資源的建構函式,被稱為移動建構函式。而這樣的“偷”的行為,則稱之為“移動語義”。

class HasPtrMem(){
public:
    HasPtrMem() : d(new int(3)) {}
    HasPtrMem(const HasPtrMem & h) : d(new int(*h.d)) {}// 拷貝建構函式
    HasPtrMem(HasPtrMem && h) : d(h.d) {h.d = nullptr;}// 移動建構函式,需將臨時值的指標成員置空
    ~HasPtrMem() {delete d;}
private:
    int *d;
}
複製程式碼

左值,右值,右值引用

關於左值,右值很難去做一個非常明確的定義,但是在C++中有一個被廣泛認同的說法,那就是可以取地址的、有名字的就是左值,反之,不能取地址的、沒有名字的就是右值。

無論是宣告一個左值引用還是右值引用,都必須立即進行初始化。左值引用是具名變數值的別名,而右值引用則是不具名(匿名)變數的別名。

標準庫在<type_traits>標頭檔案中提供了3個模板類:is_rvalue_reference、is_lvalue_reference、is_reference,可供我們進行判斷引用型別是左值引用還是右值引用。

std::move:強制轉化為右值

C++11中提供了std::move這個函式使我們能將一個左值強制轉化為右值引用,繼而我們可以通過右值引用使用該值,以用於移動語義。

// 使用上面例子中的HasPtrMem
HasPtrMem a;
HasPtrMem b(std::move(a));// 呼叫移動建構函式
複製程式碼

事實上,為了保證移動語義的傳遞,程式設計師在編寫移動建構函式的時候,應該總是記得使用std::move轉換擁有形如堆記憶體、檔案控制程式碼等資源的成員為右值,這樣依賴,如果成語支援移動構造的話,就可以實現其移動語義。而即使成員沒有移動建構函式,那麼接受常量左值的建構函式版本也會輕鬆地實現拷貝構造,因此也不會引起大的問題。

顯示轉換操作符

在C++中,有一個非常好也非常壞的特性,就是隱式型別轉換。隱式型別轉換的“自動性”可以讓程式設計師免於層層構造型別。但也是由於它的自動性,會在一些程式設計師意想不到的地方出現嚴重的但不易被發現的錯誤。

關鍵字explicit主要用於修飾的建構函式,其作用主要就是阻止建構函式的隱式轉換。

C++11中將explicit的使用範圍擴充套件到了自定義的型別轉換操作符上,以支援所謂的“顯示型別轉換”。

列表初始化

在C++11中可以使用花括號“{}”來進行初始化,這種初始化方式被稱為列表初始化,已經稱為C++語言的一個基本功能。我們甚至可以使用列表初始化的方式對vector、map等非內建的複雜的資料型別進行初始化。

而且,C++11中,標準總是傾向於使用更為通用的方式來支援新的特性。標準模板庫中容器對初始化列表的支援源自<initilizer_list>這個標頭檔案中的initilizer_list類别範本的支援。我們可以通過宣告已initilizer_list模板類為引數的建構函式,來使得自定義的類使用列表初始化。

enum Gender {boy, girl};
class People{
public:
    People(initilizer_list<pair<string, Gender>> l) {//initilizer_list的建構函式
        auto i = l.begin();
        for (; i != l.end(); ++i)
            data.pushback(*i);
    }
private:
    vector<pair<string, Gender>> data;
};

People ship2012 = {{"Garfield", boy}, {"HelloKitty", girl}};
複製程式碼

另外,使用列表初始化可以有效的防止型別收窄。型別收窄一般是指一些可以使得資料變化或者精度丟失的隱式型別轉換。

auto型別推導

auto宣告的變數必須被初始化,以使編譯器能夠從其初始化表示式中推匯出其型別。

auto使用時需注意:

(1)、可以使用const、volatile、pointer(*)、reference(&)、rvalue reference(&&)等說明符和宣告符來修飾auto關鍵字;

(2)、用auto宣告的變數必須初始化;

(3)、auto不能與其它任何型別說明符一起使用;

(4)、方法、引數或模板引數不能被宣告為auto;

(5)、定義在堆上的變數,使用了auto的表示式必須被初始化;

(6)、auto是一個佔位符,不是型別,不能用於型別轉換或其它一些操作,如sizeof、typeid;

(7)、auto關鍵字內宣告的宣告符列表的所有符號必須解析為同一型別;

(8)、auto不能自動推導成CV-qualifiers(constant& volatile qualifiers),除非被宣告為引用型別;

(9)、auto會退化成指向陣列的指標,除非被宣告為引用;

(10)、auto不能作為函式的返回型,在C++14中是可以的。

decltype

decltype的型別推導並不是像auto一樣是從變數宣告的初始化表示式獲得變數的型別,decltype總是以一個普通的表示式為引數,返回該表示式的型別。而與auto相同的是,作為一個型別指示符,decltype可以將獲得的型別來定義另外一個變數。與auto相同,decltype型別推導也是在編譯時進行的。

decltype只能接受表示式做引數,像函式名做引數的表示式decltype(hash)這種是無法通過編譯的。

追蹤返回型別

利用auto和decltype以及返回型別後置的語法就能實現追蹤返回型別。比如,我們想寫一個泛型的加法函式時可能直觀的寫下如下程式碼。

template <typename T1, typename T2>
decltype(t1 + t2) sum(const T1& t1, const T2& t2) {
   return t1 + t2;
}
複製程式碼

但是,這樣寫是編譯不過的,因為編譯器只會從左往右地讀入符號,這裡的t1和t2在編譯器看來都是未宣告的。正確的寫法是這樣的。

template <typename T1, typename T2>
auto sum(const T1& t1, const T2 & t2) -> decltype(t1 + t2) {
    return t1 + t2;
}
複製程式碼

基於範圍的for迴圈

語法很簡單,不贅述。主要使用時需要注意兩點。一是使用基於範圍的for迴圈需要for迴圈迭代的範圍是可確定的。二是,基於範圍的for迴圈要求迭代物件實現++和==等操作符,這點標準庫中的容器不會有問題,但使用者自定義的類需要自己實現。

強型別列舉

原來的列舉型別有非強型別作用域,允許隱式轉換為整型,佔用儲存控制元件及符號性不確定的缺點。C++11引入了強型別列舉來解決問題。

宣告強型別列舉只需要在enum後加上關鍵字class。比如:

enum class Type { General, Light, Medium, Heavy};

強型別列舉具有一下幾點優勢:

  • 強作用域,強型別列舉成員的名稱不會輸出到其父作用域空間。
  • 轉換限制,強型別列舉成員的值不可以與整型隱式地相互轉換。
  • 可以指定底層型別。強型別列舉預設的底層型別為int,但也可以顯式地指定底層型別,具體方法為在列舉名稱後面加上":type",其中type可以是除了wchar_t以外的任何整型。比如:

enum class Type: char { General, Light, Medium, Heavy};

由於enum class是強型別作用域的,故匿名的enum class很可能什麼都做不了。

常量表示式函式

常量表示式函式需要滿足幾個條件,否則不能用constexpr關鍵字進行修飾:

  • 函式只能包含return語句。
  • 函式必須有返回值。
  • 在使用前必須已經定義。
  • return返回語句中不能使用非常量表示式的函式、全域性資料,且必須是一個常量表示式。

constexpr int GetConst() { return 1; }

常量表示式值

const int i = 0;

constexpr int j = 0;

constexpr表示的就是編譯期常量,const表示的是執行期常量。

大部分情況下這兩個定義是沒有區別的。不過i只要在全域性範圍內宣告,編譯器一定會為它產生資料;而對於j,如果沒有地方呼叫它,編譯器可以選擇不為它生成資料。

並且,預設只有內建型別才能修飾為常量表示式值,自定義型別如果要成為常量表示式值,必須定義一個constexpr修飾的建構函式。

tuple元組

當我們希望將一些資料組合成單一物件,但又不想麻煩地定義一個新的資料結構來表示這些資料時,tuple是非常有用的。一般,tuple可以用於函式返回多個返回值。

tuple容器, 可以使用直接初始化, 和"make_tuple()"初始化, 訪問元素使用"get<>()"方法, 注意get裡面的位置資訊, 必須是常量表示式(const expression)。

可以通過"std::tuple_size<decltype(t)>::value"獲取元素數量; "std::tuple_element<0, decltype(t)>::type"獲取元素型別。

如果tuple型別進行比較, 則需要保持元素數量相同, 型別可以比較。

#include <iostream>  
#include <vector>  
#include <string>  
#include <tuple>  
  
using namespace std;  
  
std::tuple<std::string, int>  
giveName(void)  
{  
    std::string cw("Caroline");  
    int a(2013);  
    std::tuple<std::string, int> t = std::make_tuple(cw, a);  
    return t;  
}  
  
int main()  
{  
    std::tuple<int, double, std::string> t(64, 128.0, "Caroline");  
    std::tuple<std::string, std::string, int> t2 =  
            std::make_tuple("Caroline", "Wendy", 1992);  
  
    //返回元素個數  
    size_t num = std::tuple_size<decltype(t)>::value;  
    std::cout << "num = " << num << std::endl;  
  
    //獲取第1個值的元素型別  
    std::tuple_element<1, decltype(t)>::type cnt = std::get<1>(t);  
    std::cout << "cnt = " << cnt << std::endl;  
  
    //比較  
    std::tuple<int, int> ti(24, 48);  
    std::tuple<double, double> td(28.0, 56.0);  
    bool b = (ti < td);  
    std::cout << "b = " << b << std::endl;  
  
    //tuple作為返回值  
    auto a = giveName();  
    std::cout << "name: " << get<0>(a)  
            << " years: " << get<1>(a) << std::endl;  
  
    return 0;  
}  
複製程式碼

nullptr

在C++11中,nullptr是一個所謂“指標空值型別”的編譯期常量。指標空值型別被命名為nullptr_t。

需要注意的是,nullptr是C++11中的關鍵字,它是有型別的,且僅可以被隱式轉化為指標型別,其型別定義是:

typedef decltype(nullptr) nullptr_t;

lambda函式

lambda表示式的語法定義如下:

[capture] (parameters) mutable ->return-type {statement};

  • [capture]:捕捉列表。捕捉列表總是出現在lambda函式的開始處。實質上,[]是lambda引出符(即獨特的標誌符),編譯器根據該引出符判斷接下來的程式碼是否是lambda函式。捕捉列表能夠捕捉上下文中的變數以供lambda函式使用。捕捉列表由一個或多個捕捉項組成,並以逗號分隔,捕捉列表一般有以下幾種形式:

    • [var] 表示值傳遞方式捕捉變數var。

    • [=] 表示值傳遞方式捕捉所有父作用域的變數(包括this指標)。

    • [&var] 表示引用傳遞捕捉變數var。

    • [&] 表示引用傳遞捕捉所有父作用域的變數(包括this指標)。

    • [this]表示值傳遞方式捕捉當前的this指標。

    • [=,&a,&b]表示以引用傳遞的方式捕捉變數 a 和 b,而以值傳遞方式捕捉其他所有的變數。

    • [&,a,this]表示以值傳遞的方式捕捉 a 和 this,而以引用傳遞方式捕捉其他所有變數。

    備註:父作用域是指包含lambda函式的語句塊。

  • (parameters):引數列表。與普通函式的引數列表一致。如果不需要引數傳遞,則可以連同括號()一起省略。

  • mutable :mutable修飾符。預設情況下,lambda函式總是一個const函式,mutable可以取消其常量性。在使用該修飾符時,引數列表不可省略(即使引數為空)。

  • ->return-type :返回型別。用追蹤返回型別形式宣告函式的返回型別。出於方便,不需要返回值的時候也可以連同符號->一起省略。此外,在返回型別明確的情況下,也可以省略該部分,讓編譯器對返回型別進行推導。

  • {statement} :函式體。內容與普通函式一樣,不過除了可以使用引數之外,還可以使用所有捕獲的變數。

// 簡單示例
int a = 20, b = 10;

auto totalAB = [] (int x, int y)->int{ return x + y; };
int aAddb = totalAB(a, b);
cout << "aAddb :" << aAddb << endl;

// lambda與STL
vector<int> v{ 1, 2, 3, 4, 5 }; 
  
for_each( v.begin(), v.end(), [] (int val)  
{  
    cout << val;  
} );  
複製程式碼

在現階段,通常編譯器都會把lambda函式轉化為一個仿函式物件。這是編譯器實現lambda函式的一種方式。因此,C++11中,lambda函式可以視為仿函式的一種等價形式。

原生字串字面量

原生字串字面量的意思就是所見即所得,在程式碼中的字串常量是怎麼樣的,我們得到的就是怎麼樣的,不需要轉義字元來控制特定的字元。

在C++11中程式設計師只需要在字串前面加入字首字母R,並在引號中使用括號左右標識即可將該字串宣告為原生字串了。

// 輸出帶引號的字串"你好"
int main()
{
    std::string str = "\"你好\"";
    std::cout << str << std::endl;

    const char* str1 = R"("你好")";
    std::cout << str1 << std::endl;

    return 0;
}
複製程式碼

相關文章