C++11\14\17\20 特性介紹


轉:https://www.jianshu.com/p/8c4952e9edec

 

C++11 新特性

#01 auto 與 decltype

auto: 對於變量,指定要從其初始化器⾃動推導出其類型。⽰例:

auto a = 10; // 自動推導 a 為 int auto b = 10.2; // 自動推導 b 為 double auto c = &a; // 自動推導 c 為 int* auto d = "xxx"; // 自動推導 d 為 const char* 

decltype: 推導實體的聲明類型,或表達式的類型。為了解決 auto 關鍵字只能對變量進⾏類型推導的缺陷⽽出現。⽰例:

int a = 0; decltype(a) b = 1; // b 被推導為 int 類型 decltype(10.8) c = 5.5; // c 被推導為 double 類型 decltype(c + 100) d; // d 被推導為 double struct { double x; } aa; decltype(aa.x) y; // y 被推導為 double 類型 decltype(aa) bb; // 推斷匿名結構體類型 

C++11 中 auto 和 decltype 結合再借助「尾置返回類型」還可推導函數的返回類型。⽰例:

// 利⽤ auto 關鍵字將返回類型后置 template<typename T, typename U> auto add1(T x, U y) -> decltype(x + y) { return x + y; } 

C++14 開始⽀持僅⽤ auto 並實現返回類型推導,見下⽂ C++14 章節。

#02 defaulted 與 deleted 函數

在 C++ 中,如果程序員沒有⾃定義,那么編譯器會默認為程序員⽣成 「構造函數」、「拷貝構造函數」、「拷貝賦值函數」 等。

但如果程序員⾃定義了上述函數,編譯器則不會⾃動⽣成這些函數。

⽽在實際開發過程中,我們有時需要在保留⼀些默認函數的同時禁⽌⼀些默認函數

例如創建 「不允許拷貝的類」 時,在傳統 C++ 中,我們經常有如下的慣例代碼:

// 除非特別熟悉編譯器自動生成特殊成員函數的所有規則,否則意圖是不明確的 class noncopyable { public: // 由於下⽅有⾃定義的構造函數(拷⻉構造函數) // 編譯器不再⽣成默認構造函數,所以這⾥需要⼿動定義構造函數 // 但這種⼿動聲明的構造函數沒有編譯器⾃動⽣成的默認構造函數執⾏效率⾼ noncopyable() {}; private: // 將拷⻉構造函數和拷⻉賦值函數設置為 private // 但卻⽆法阻⽌友元函數以及類成員函數的調⽤ noncopyable(const noncopyable&); noncopyable& operator=(const noncopyable&); }; 

傳統 C++ 的慣例處理⽅式存在如下缺陷:

  1. 由於⾃定義了「拷貝構造函數」,編譯器不再⽣成「默認構造函數」,需要⼿動的顯式定義「無參構造函數」
  2. ⼿動顯式定義的「無參構造函數」效率低於「默認構造函數」
  3. 雖然「拷貝構造函數」和「拷貝賦值函數」是私有的,對外部隱藏。但⽆法阻⽌友元函數和類成員函數的調⽤
  4. 除⾮特別熟悉編譯器⾃動⽣成特殊成員函數的所有規則,否則意圖是不明確的

為此,C++11 引⼊了 defaultdelete 關鍵字,來顯式保留或禁止特殊成員函數:

class noncopyable { public: noncopyable() = default; noncopyable(const noncopyable&) = delete; noncopyable& operator=(const noncopyable&) = delete; }; 

#03 final 與 override

在傳統 C++ 中,按照如下⽅式覆蓋⽗類虛函數:

struct Base { virtual void foo(); }; struct SubClass: Base { void foo(); }; 

上述代碼存在⼀定的隱患:

  • 程序員並⾮想覆蓋⽗類虛函數,⽽是 定義了⼀個重名的成員函數。由於沒有編譯器的檢查導致了意外覆蓋且難以發現
  • ⽗類的虛函數被刪除后,編譯器不會進⾏檢查和警告,這可能引發嚴重的錯誤

為此,C++11 引⼊ override 顯式的聲明要覆蓋基類的虛函數,如果不存在這樣的虛函數,將不會通過編譯:

class Parent { virtual void watchTv(int); }; class Child : Parent { virtual void watchTv(int) override; // 合法 virtual void watchTv(double) override; // 非法,父類沒有此虛函數 }; 

final 則終⽌虛類被繼承或虛函數被覆蓋:

class Parent2 { virtual void eat() final; }; class Child2 final : Parent2 {}; // 合法 class Grandson : Child2 {}; // 非法,Child2 已經 Final,不可被繼承 class Child3 : Parent2 { void eat() override; // 非法,foo 已 final }; 

#04 尾置返回類型

看一個比較復雜的函數定義:

// func1(int arr[][3], int n) 為函數名和參數 // (* func1(int arr[][3], int n)) 表示對返回值進⾏解引⽤操作 // (* func1(int arr[][3], int n))[3] 表示返回值解引⽤后為⼀個⻓度為 3 的數組 // int (* func1(int arr[][3], int n))[3] 表示返回值解引⽤后為⼀個⻓度為 3 的 int 數組 int (* func1(int arr[][3], int n))[3] { return &arr[n]; } 

C++11 引⼊「尾置返回類型」,將「函數返回類型」通過 -> 符號連接到函數后面,配合 auto 簡化上述復雜函數的定義:

// 返回指向數組的指針 auto fun1(int arr[][3], int n) -> int(*)[3] { return &arr[n]; } 

尾置返回類型經常在 「lambda 表達式」、「模板函數返回」中使⽤:

// 使⽤尾置返回類型來聲明 lambda 表達式的返回類型 [capture list] (params list) mutable exception->return_type { function body } // 在模板函數返回中結合 auto\decltype 聲明模板函數返回值類型 template<typename T, typename U> auto add(T x, U y) -> decltype(x + y) { return x + y; } 

#05 右值引⽤

何為左值與右值
  • 左值:內存中有確定存儲地址的對象的表達式的值
  • 右值:所有不是左值的表達式的值。右值可分為「傳統純右值」和「將亡值

上述的「傳統純右值」和「將亡值」又是什么?

  • 純右值:即 C++11 之前的右值。包括:

    1. 常見的字面量如 0、"123"、或表達式為字面量
    2. 不具名的臨時對象,如函數返回臨時對象
  • 將亡值:隨着 C++11 引入的右值引用而來的概念。包括:

    1. 「返回右值引用的函數」的返回值。如返回類型為 T&& 的函數的返回值
    2. 「轉換為右值引用的轉換函數」的返回值,如 std::move() 函數的返回值

同時,左值 + 將亡值又被稱為「泛左值」。這幾個概念對於剛接觸的同學可能會比較混亂,我們梳理一下,如下圖所示:


 
value_type.png

左值還是右值可以通過取地址運算符 & 來進⾏判斷,能夠通過 & 正確取得地址的為左值,反之為右值。

int i = 0; int* p_i = &i; // 可通過 & 取出地址,固 i 為左值 cout << p_i << endl; int* p_i_plus = &(i + 1); // 非法,i + 1 為右值 int* p_i_const = &(0); // 非法,0 為右值 
何為左值引用與右值引用

C++11 之前,我們就經常使⽤對左值的引⽤,即左值引⽤,使用 & 符號聲明:

int j = 0; int& ref_j = j; // ref_j 為左值引⽤ int& ref_ret = getVal(); // ref_ret 為左值引用 int& ref_j_plus = j + 1; // ⾮法,左值引⽤不能作⽤於右值 int& ref_const = 0; // 非法,左值引用不能作用於右值 

如上例代碼所示,ref_j_plusref_const 為傳統 C++ 中經常使用的左值引用,無法作用於 j+10 這樣的右值。

C++11 引⼊了針對右值的引⽤,即右值引⽤,使用 && 符號聲明:

int&& ref_k_plus = (i + 1); // ref_k_plus 為右值引用,它綁定了右值 i + 1 int&& ref_k = 0; // ref_k 為右值引用,它綁定了右值 0 
右值引用的特點

以下述代碼為例:

int getVal() { return 1; } int main() { // 這里存在兩個值: // 1. val(左值) // 2. getVal() 返回的臨時變量(右值) // 其中 getVal() 返回的臨時變量賦值給 val 后會被銷毀 int val = getVal(); return 0; } 

上述代碼中,getVal 函數產⽣的 「臨時變量」 需要先復制給左值 val,然后再被銷毀。

但是如果使⽤右值引⽤:

// 使用 && 來表明 val 的類型為右值引用 // 這樣 getVal() 返回的臨時對象(右值) 將被「續命」 // 擁有與 val 一樣長的生命周期 int&& val = getVal(); 

上述代碼體現了右值引⽤的第⼀個特點

通過右值引⽤的聲明,右值可「重⽣」,⽣命周期與右值引⽤類型的變量⽣命周期⼀樣長。

再看如下例⼦:

template<typename T> void f(T&& t) {} f(10); // t 為右值 int x = 10; f(x); // t 為左值 

上述例⼦體現了右值引⽤的第⼆個特點

⾃動類型推斷(如模板函數等)的場景下,T&& t 是未定的引⽤類型,即 t 並⾮⼀定為右值。如果它被左值初始化,那么 t 就為左值。如果它被右值初始化,則它為右值。

正是由於上述特點,C++11 引入右值引⽤可以實現如下⽬的:

  • 實現移動語義。解決臨時對象的低效率拷貝問題
  • 實現完美轉發。解決函數轉發右值特征丟失的問題
右值引⽤帶來的移動語義

在 C++11 之前,臨時對象的賦值采⽤的是低效的拷貝。

舉例來講,整個過程如同將⼀個冰箱⾥的⼤象搬到另⼀個冰箱,傳統 C++ 的做法是第⼆個冰箱⾥復制⼀個⼀摸⼀樣的⼤象,再把第⼀個冰箱的⼤象銷毀,這顯然不是⼀個⾃然的操作⽅式。

看如下例⼦:

class HasPtrMem1 { public: HasPtrMem1() : d(new int(0)) {} ~HasPtrMem1() { delete d; } int* d; }; int main() { HasPtrMem1 a1; HasPtrMem1 b1(a1); cout << *a1.d << endl; cout << *b1.d << endl; return 0; } 

上述代碼中 HasPtrMem1 b(a) 將調⽤編譯器默認⽣成的「拷貝構造函數」進⾏拷貝,且進⾏的是按位拷貝(淺拷貝),這將導致懸掛指針問題[1]

懸掛指針問題[1]: 上述代碼在執⾏ main 函數后,將銷毀 a、b 對象,於是調⽤對應的析構函數執⾏ delete d 操作。但由 於 a、b 對象中的成員 d 指針同⼀塊內存,於是在其中⼀個對象被析構后,另⼀個對象中的指針 d 不再指向有效內存,這個對象的 d 就變成了懸掛指針。

在懸掛指針上釋放內存將導致嚴重的錯誤。所以針對上述場景必須進⾏深拷貝:

class HasPtrMem2 { public: HasPtrMem2() : d(new int(0)) {} HasPtrMem2(const HasPtrMem2& h) : d(new int(*h.d)) {} ~HasPtrMem2() { delete d; } int* d; }; int main() { HasPtrMem2 a2; HasPtrMem2 b2(a2); cout << *a2.d << endl; cout << *b2.d << endl; return 0 ; } 

在上述代碼中,我們⾃定義了拷貝構造函數的實現,我們通過 new 分配新的內存實現了深度拷貝,避免了「懸掛指針」的問題,但也引出了新的問題。

拷貝構造函數為指針成員分配新的內存並進⾏拷貝的做法是傳統 C++ 編程中是⼗分常見的。但有些時候我們並不需要這樣的拷貝:

HasPtrMem2 GetTemp() { return HasPtrMem2(); } int main() { HasPtrMem2 a = GetTemp(); } 

上述代碼中,GetTemp 返回的臨時對象進⾏深度拷貝操作,然后再被銷毀。如下圖所⽰:

 
copy_constructor.png

如果 HasPtrMem2 中的指針成員是復雜和龐⼤的數據類型,那么就會導致⼤量的性能消耗。

再回到⼤象移動的類⽐,其實更⾼效的做法是將⼤象直接從第⼀個冰箱拿出,然后放⼊第⼆個冰箱。同樣的,我們在將臨時對象賦值給某個變量時是否可以不⽤拷貝構造函數?答案是肯定的,如下圖所⽰:

 
move_constructor.png

在 C++11 中,像這樣「偷⾛」資源的構造函數,稱為 「移動構造函數」,這種「偷」的⾏為,稱為 「移動語義」,可理解為「移為⼰⽤」。

當然實現時需要在代碼中定義對應的「移動構造函數」:

class HasPtrMem3 { public: HasPtrMem3() : d(new int(0)) {} HasPtrMem3(const HasPtrMem3& h) : d(new int(*h.d)) {} HasPtrMem3(HasPtrMem3&& h) : d(h.d) { h.d = nullptr; } ~HasPtrMem3() { delete d; } int* d; }; 

注意「移動構造函數」依然會存在懸掛指針問題,所以在通過移動構造函數「偷」完資源后,要把臨時對象的 h.d 指針置為空,避免兩個指針指向同⼀個內存,在析構時被析構兩次。

「移動構造函數」中的參數為 HasPtrMem3&& h 為右值類型[2],⽽返回值的臨時對象就是右值類型,這也是為什么返回值臨時對象能夠匹配到「移動構造函數」的原因。

右值類型[2]: 注意和上⾯提到的右值引⽤第⼆個特點做區分,這⾥不是類型推導的場景,HasPtrMem3 是確定的類型,所以 HasPtrMem3&& h 就是確定的右值類型。

上述的移動語義是通過右值引⽤來匹配臨時值的,那么左值是否可以借助移動語義來優化性能呢?C++11 為我們 提供了 std::move 函數來實現這⼀⽬標:

{ std::list<std::string> tokens; // tokens 為左值 // 省略初始化... std::list<std::string> t = tokens; // 這里存在拷貝 } std::list<std::string> tokens; std::list<std::string> t = std::move(tokens); // 這里不存在拷貝 

std::move 函數實際沒有移動任何資源,它唯⼀做的就是將⼀個左值強制轉換成右值引⽤,從而匹配到「移動構造函數」或「移動賦值運算符」,應⽤移動語義實現資源移動。⽽ C++11 中所有的容器都實現了移動語義,所以使用了 list 容器的上述代碼能夠避免拷貝,提⾼性能。

右值引⽤帶來的完美轉發

傳統 C++ 中右值參數后被轉換成左值,即不能按照參數原先的類型進⾏轉發,如下所⽰:

template<typename T> void forwardValue1(T& val) { // 右值參數變為左值 processValue(val); } template<typename T> void forwardValue1(const T& val) { processValue(val); // 參數都變成常量左值引用了 } 

如何保持參數的左值、右值特征,C++11 引⼊了 std::forward,它將按照參數的實際類型進⾏轉發:

void processValue(int& a) { cout << "lvalue" << endl; } void processValue(int&& a) { cout << "rvalue" << endl; } template<typename T> void forwardValue2(T&& val) { // 照參數本來的類型進⾏轉發 processValue(std::forward<T>(val)); } int main() { int i = 0; forwardValue2(i); // 傳入左值,函數執行輸出 lvalue forwardValue2(0); // 傳入右值,函數執行輸出 rvalue return 0; } 

#06 移動構造函數與移動賦值運算符

在規則 #05 已經提及,不再贅述。

#07 有作⽤域枚舉

傳統 C++ 的枚舉類型存在如下問題:

  • 每⼀個枚舉值在其作⽤域內都是可見,容易引起命名沖突
// Color 下的 BLUE 和 Feeling 下的 BLUE 命名沖突 enum Color { RED, BLUE }; enum Feeling { EXCITED, BLUE }; 
  • 會被隱式轉換成 int,這在那些不該轉換成 int 的場景下可能導致錯誤
  • 不可指定枚舉的數據類型,導致代碼不易理解、不可進⾏前向聲明等

在傳統 C++ 中也有⼀些間接⽅案可以適當解決或緩解上述問題,例如使⽤命名空間

namespace Color { enum Type { RED, YELLOW, BLUE }; }; 

或使⽤類、結構體:

struct Color { enum Type { RED, YELLOW, BLUE }; }; 

但上述⽅案通常值解決了作⽤域問題,隱式轉換以及數據類型的問題⽆法解決。

C++11 引⼊了枚舉類解決上述問題:

// 定義枚舉值為 char 類型的枚舉類 enum class Color:char { RED, BLACK }; // 使⽤ Color c = Color::RED; 

#08 constexpr 與字⾯類型

constexpr: 在編譯期將表達式或函數編譯為常量結果

constexpr 修飾變量、函數:

// 修飾變量 constexpr int a = 1 + 2 + 3; char arr[a]; // 合法,a 是編譯期常量 // 修飾函數,使函數在編譯期會成為常量表達式(如果可以) // 如果 constexpr 函數返回的值不能在編譯器確定,則 constexpr 函數就會退化為運行期函數(這樣做的初衷是避免在為編譯期和運行期寫兩份相同代碼) // constexpr 函數的設計其實不夠嚴謹,所以 C++20 引入了 consteval (詳見下文 C++20 部分) // C++11 中,constexpr 修飾的函數只能包含 using 指令、typedef 語句以及 static_assert // C++14 實現了對其他語句的支持 constexpr int len_foo_constexpr() { return 5; } 

#09 初始化列表 - 擴展「初始化列表」的適⽤范圍

在 C++98/03 中,普通數組或 POD 類型 可以通過初始化列表的⽅式進⾏初始化,例如:

POD 類型見下文的 #18 條

int arr1[3] = { 1, 2, 3 }; long arr2[] = { 1, 3, 2, 4 }; struct A { int x; int y; } a = { 1, 2 }; 

C++11 擴展了「初始化列表」的適⽤范圍,使之可以適⽤於所有類型對象的初始化:

class Dog { public: Dog(string name, int age) { cout << name << " "; cout << age << endl; } }; Dog dog1 = {"cat1", 1}; Dog dog2 {"cat2", 2}; 

還可以通過 std::initializer_list 來實現更強⼤的「初始化列表」,例如:

class Dog { public: Dog(initializer_list<int> list) { for (initializer_list<int>::iterator it = list.begin(); it != list.end(); ++it) { cout << *it << " "; } cout << endl; } }; Dog dog3 = {1, 2, 3, 4, 5}; 

同時,初始化列表還可以⽤作普通函數的形參返回值

// 形參 void watch(Dog dog) { cout << "watch" << endl; } watch({"watch_dog", 4}); // Dog 作為返回值 getDefaultDog() { return {"default", 3}; } getDefaultDog(); 

#10 委托與繼承的構造函數

委托構造:在⼀個構造函數中調⽤同⼀個類的另⼀個構造函數
繼承構造:在 C++11 之前的 C++ 中,⼦類需要依次聲明⽗類擁有的構造函數,並傳遞相應的初始化參數。C++11 利⽤關鍵字 using 引⼊了繼承構造函數,使⽤⼀⾏語句讓編譯器⾃動完成上述⼯作。

class Parent { public: int value1; int value2; Parent() { value1 = 1; } Parent(int value) : Parent() { // 委托 Parent() 構造函數 value2 = value; } } class Child : public Parent { public: using Parent::Parent; // 繼承構造 } 

#11 花括號或等號初始化器

上⽂已提及,不再贅述

#12 nullptr

傳統 C++ 中 NULL 的定義存在很多缺陷,編譯器在實現時常常將其定義為 0,這會導致重載混亂。考慮如下代碼;

void foo(char*); void foo(int); 

當調⽤ foo(NULL) 時將匹配到 foo(int) 函數,這顯然會讓⼈感到迷惑。

C++11 引⼊了 nullptr (類型為 nullptr_t)關鍵字,以便區分空指針與 0,且 nullptr 能夠隱式的轉換為任何指針或成員指針的類型。

#13 long long

long: ⽬標類型將有⾄少 32 位的寬度
long long: ⽬標類型將有⾄少 64 位的寬度

如同 long 類型后綴需要 「l」 或 「L」,long long 類型后綴需要加上「ll」或「LL」。

#14 char16_t 與 char32_t

C++98 中為了表達 Unicode 字符串,引⼊了 wchar_t 類型,以解決 1 字節的 char 只能 256 個字符的問題。

但是由於 wchar_t 類型在不同平台上實現的長度不同,在代碼移植⽅⾯有⼀定的影響。於是 C++11 引⼊ char16_tchar32_t,他們擁有的固定的長度,分別為 2 個字節4 個字節

char16_t: UTF-16 字符表⽰的類型,要求⼤到⾜以表⽰任何 UTF-16 編碼單元( 16 位)。它與 std::uint_least16_t 具有相同的⼤⼩、符號性和對齊,但它是獨⽴的類型。

char32_t: - UTF-32 字符表⽰的類型,要求⼤到⾜以表⽰任何 UTF-32 編碼單元( 32 位)。它與 std::uint_least32_t 具有相同的⼤⼩、符號性和對齊,但它是獨⽴的類型。

同時 C++11 還定義了 3 個常量字符串前綴:

  • u8 代表 UTF-8 編碼
  • u 代表 UTF-16 編碼
  • U 代表 UTF-32 編碼
char16_t UTF16[] = u"中國"; // 使用 UTF-16 編碼存儲 char32_t UTF16[] = U"中國"; // 使用 UTF-32 編碼存儲 

#15 類型別名

傳統 C++ 中使⽤ typedef 來為類型定義⼀個新的名稱,C++11 中我們可以使⽤ using 達到同樣的效果,如下所⽰:

typedef std::ios_base::fmtflags flags; using flags = std::ios_base::fmtflags; 

既然有了 typedef 為什么還引⼊ using?當然是因為 using ⽐起 typedef 還能做更多。

typedef 是只能為「類型」定義新名稱,⽽模板則是 「⽤來產⽣類型」的,所以以下代碼是⾮法的:

template<typename T, typename U> class DogTemplate { public: T attr1; U aatr2; }; // 不合法 template<typename T> typedef DogTemplate<std::vector<T>, std::string> DogT; 

但使⽤ using 則可以為模板定義別名:

template<typename T> using DogT = DogTemplate<std::vector<T>, std::string>; 

#16 變長參數模板

在傳統 C++ 中,類模板或函數模板只能接受固定數量的模板參數。

⽽ C++11 允許任意多個、任意類別的模板參數,同時在定義時⽆需固定參數個數。如下所⽰:

template<typename... T> class DogT; // 傳⼊多個不同類型的模板參數 class DogT<int, std::vector<int>, std::map<std::string, std::vector<int>>> dogT; // 不傳⼊參數( 0 個參數) class DogT<> nothing; // 第⼀個參數必傳,之后為變⻓參數 template<typename require, typename... Args> class CatT; 

同樣的可⽀持模板函數:

template<typename... Args> void my_print(const std::string& str, Args... args) { // 使⽤ sizeof... 計算參數個數 std::cout << sizeof...(args) << std::endl; } 

#17 推⼴的(⾮平凡)聯合體

聯合體 Union 為我們提供了在⼀個結構內定義多種不同類型的成員的能⼒,但在傳統 C++ 中,並不是所有的數據類型都能成為聯合體的數據成員。例如:

struct Dog { Dog(int a, int b) : age(a), size(b) {} int age; int size; } union T { // C++11 之前為非法(d 不是 POD 類型) // C++11 之后合法 Dog d; int id; } 

有關 POD 類型參考下⽂的 #18 條

C++11 去除了上述聯合體的限制[3],標准規定了任何⾮引⽤類型都可以成為聯合體的數據成員

[3] 去除的原因是經過長期的實踐證明為了兼容 C 所做的限制沒有必要。

#18 推⼴的 POD (平凡類型與標准布局類型)

POD 為 Plain Old Data 的縮寫,Plain 突出其為⼀種普通數據類型,Old 體現其具有與 C 的兼容性,例如可以使⽤ memcpy() 函數進⾏復制、使⽤ memset() 函數進⾏初始化等。

具體地,C++11 將 POD 划分為兩個概念的合集:平凡的(trival)和標准布局的(standard layout)。

其中平凡的類或結構體應該符合如下要求:

  1. 擁有平凡的默認構造函數和析構函數。即不⾃定義任何構造函數,或通過 =default 來顯⽰指定使⽤默認構造函數
  2. 擁有平凡的拷貝構造函數和移動構造函數
  3. 擁有平凡的拷貝賦值運算符和移動賦值運算符
  4. 不包含虛函數以及虛基類

C++11 同時提供了輔助類模板 is_trivial 來實現是否平凡的判斷:

cout << is_trivial<DogT>::value << endl; 

POD 包含的另⼀個概念則是「標准布局」。標准布局的類或結構體需要符合如下要求:

  1. 所有⾮靜態成員有相同的訪問權限(public、private、protected)
  2. 類或結構體繼承時滿⾜如下兩個條件之⼀:
    2.1 ⼦類中有⾮靜態成員,且只有⼀個僅包含靜態成員的基類
    struct B1 { static int a; }; struct B2 { static int b; }; 
    2.2 基類有⾮靜態成員,則⼦類沒有⾮靜態成員
    struct B2 { int a; } ; struct D2 : B2 { static int d; }; 
    從上述條件可知,1. 只要⼦類和基類同時都有⾮靜態成員 2. ⼦類繼承多個基類,有多個基類同時有⾮靜態成員。 這兩種情況都不屬於標准布局。
  3. 類中第⼀個⾮靜態成員的類型與其基類不同
struct A : B { B b; }; // 非標准布局,第一個非靜態成員 b 就是基本類型 struct A : B { int a; B b; }; // 標准布局,第一個非靜態成員 a 不是基類 B 類型 
  1. 沒有虛函數或虛基類
  2. 所有⾮靜態數據成員均符合標准布局類型,其基類也符合標准布局(遞歸定義)

同樣 C++11 提供了輔助類模板 is_standard_layout 幫助我們判斷:

cout << is_standard_layout<Dog>::value << endl; 

最后,C++11 也提供了⼀次性判斷是否為 POD 的輔助類模板 is_pod:

cout << is_pod<Dog>::value << endl; 

了解 POD 的基本概念,POD 到底有怎樣的作⽤或好處呢?POD 能夠給我們帶來如下優點:

  1. 字節賦值。安全的使⽤ memset 和 memcpy 對 POD 類型進⾏初始化和拷貝等操作
  2. 兼容 C 內存布局。以便與 C 函數進⾏互操作
  3. 保證靜態初始化的安全。⽽靜態初始化可有效提⾼程序性能

#19 Unicode 字符串字⾯量

在 #14 已有所提及,C++11 定義了 3 個常量字符串前綴:

  • u8 代表 UTF-8 編碼
  • u 代表 UTF-16 編碼
  • U 代表 UTF-32 編碼

另外 C++11 還引⼊了⼀個字符串前綴 R 表⽰ 「原⽣字符串字⾯量」,所謂「原⽣字符串字⾯量」即表⽰字符串⽆需通過轉義處理特殊字符,所見即所得:

// ⽤法: R"分隔符 (原始字符 )分隔符" string path = R"(D:\workspace\vscode\java_demo)"; // - 作為分隔符, // 因為原始字符串含有 )",如果不添加 - 作為分隔符,則會導致字符串錯誤標示結束位置 // 分隔符應該盡量使用原始字符串中未出現的字符,以便正確標示開始與結尾 string path2 = R"-(a\b\c)"\daaa\e)-"; 

#20 ⽤戶定義字⾯量

⽤戶定義字⾯量即⽀持⽤戶定義類型的字⾯量。

傳統 C++ 提供了多種字⾯量,例如 "12.5" 為⼀個 double 類型字⾯量。"12.5f" 為⼀個 float 類型字⾯量。這些字⾯量是 C++ 標准中定義和規定的字⾯量,程序和⽤戶⽆法⾃定義新的字⾯量類型后綴

C++11 則是引⼊了⽤戶⾃定義字⾯量的能⼒。主要通過定義「字⾯量運算符函數」或函數模板實現。該運算符名稱由⼀對相鄰雙引號前導。字⾯量運算符通常在⽤戶定義字⾯量的地⽅被隱式調⽤。例如:

struct S { int value; }; // 用戶定義字面量運算符的實現 S operator ""_mysuffix(unsigned long long v) { S s_; S_.value = (int) v; return s_; } // 使用 S sv; // 101 為類型為 S 的字面量 // _mysuffix 是我們自定義的后綴,如同 float 的 f 一般 sv = 101_mysuffix; 

⽤戶⾃定義字⾯量通常由以下⼏種類型:

  1. 數值型字面量
    1.1 整數型字面量
    1.2 浮點型字面量
OutputType operator "" _suffix(unsigned long long); OutputType operator "" _suffix(long double); // Uses the 'unsigned long long' overload. OutputType some_variable = 1234_suffix; // Uses the 'long double' overload. OutputType another_variable = 3.1416_suffix; 
  1. 字符串字面量
OutputType operator "" _ssuffix(const char * string_values, size_t num_chars); OutputType operator "" _ssuffix(const wchar_t * string_values, size_t num_chars); OutputType operator "" _ssuffix(const char16_t * string_values, size_t num_chars); OutputType operator "" _ssuffix(const char32_t * string_values, size_t num_chars); // Uses the 'const char *' overload. OutputType some_variable = "1234"_ssuffix; // Uses the 'const char *' overload. OutputType some_variable = u8"1234"_ssuffix; // Uses the 'const wchar_t *' overload. OutputType some_variable = L"1234"_ssuffix; // Uses the 'const char16_t *' overload. OutputType some_variable = u"1234"_ssuffix; // Uses the 'const char32_t *' overload. OutputType some_variable = U"1234"_ssuffix; 
  1. 字符字面量
S operator "" _mysuffix(char value) { const char cv[] {value,'\0'}; S sv_ (cv); return sv_; } S cv {'h'_mysuffix}; 

盡整些花里胡哨的特性

#21 屬性

C++11 引⼊了所謂的 「屬性」來讓程序員在代碼中提供額外信息,例如:

// f() 永不返回 void f [[ noreturn ]] () { throw "error"; // 雖然不能返回,但可以拋出異常 } 

上述例⼦的展現了屬性的基本形式,noreturn 表⽰該函數永不返回。

C++11 引⼊了兩個屬性:

屬性 版本 修飾⽬標 作⽤
noreturn C++11 函數 指⽰函數不返回,沒有return語句,不正常執⾏完畢,但是可以通過出異常或 者exit()函數退出
carries_dependency C++11 函數、變量 指⽰釋放消費 std::memory_order 中的依賴鏈傳⼊和傳出該函數

概念與功能與 Java 中的注解有些類似

#22 Lambda 表達式

Lambda 表達式基本語法:

// [捕獲列表]:捕獲外部變量,詳見下文 // (參數列表): 函數參數列表 // mutable: 是否可以修改值捕獲的外部變量 // 異常屬性:exception 異常聲明 [捕獲列表](參數列表) mutable( 可選 ) 異常屬性 -> 返回類型 { // 函數體 } 

例如:

bool cmp(int a, int b) { return a < b; } int main() { int x = 0; // 傳統做法 sort(vec.begin(), vec.end(), cmp); // 使用 lambda sort(vec.begin(), vec.end(), [x](int a, int b) -> bool { return a < b; }); return 0; } 

lambda 表達式中的「捕獲列表」可以讓 lambda 表達式內部使用其可見范圍的外部變量,例如上例中的 x。捕獲列表一般有以下幾種類型:
1. 值捕獲
與參數傳遞中值傳遞類似,被捕獲的變量以值拷貝的方式傳入:

int a = 1; auto f1 = [a] () { a+= 1; cout << a << endl;}; a = 3; f1(); cout << a << endl; 

2. 引用捕獲
加上 & 符號,即可通過引用捕獲外部變量:

int a = 1; // 引用捕獲 auto f2 = [&a] () { cout << a << endl; }; a = 3; f2(); 

3. 隱式捕獲
無需顯示列出所有需要捕獲的外部變量,通過 [=] 可以通過「值捕獲」的方式捕獲所有外部變量,[&] 可以通過「引用捕獲」的方式捕獲所有外部變量:

int a = 1; auto f3 = [=] { cout << a << endl; }; // 值捕獲 f3(); // 輸出:1 auto f4 = [&] { cout << a << endl; }; // 引用捕獲 a = 2; f4(); // 輸出:2 

4. 混合方式
以上方式的混合,[=, &x] 表示變量 x 以引用形式捕獲,其余變量以傳值形式捕獲。

最終 lambda 捕獲外部變量總結如下表所示:

捕獲形式 說明
[] 不捕獲任何外部變量
[變量名, …] 默認以值得形式捕獲指定的多個外部變量(用逗號分隔),如果引用捕獲,需要顯示聲明(使用&說明符)
[this] 以值的形式捕獲this指針
[=] 以值的形式捕獲所有外部變量
[&] 以引用形式捕獲所有外部變量
[=, &x] 變量x以引用形式捕獲,其余變量以傳值形式捕獲
[&, x] 變量x以值的形式捕獲,其余變量以引用形式捕獲

#23 noexcept 說明符與 noexcept 運算符

C++11 將異常的聲明簡化為以下兩種情況:

  1. 函數可能拋出任何異常
void func(); // 可能拋出異常 
  1. 函數不可能拋出任何異常
void func() noexcept; // 不可能拋出異常 

使⽤ noexcept 能夠讓編譯器更好的優化代碼,同時 noexcept 修飾的函數如果拋出異常將會導致調⽤ std::terminate() ⽴即終⽌程序。

noexcept 還可作為運算符使⽤,來判斷⼀個表達式是否產⽣異常:

cout << noexcept(func()) << endl; 

#24 alignof 與 alignas

C++11 引⼊了 alignofalignas 來實現對內存對齊的控制。

alignof: 能夠獲取對齊⽅式
alignas: ⾃定義結構的對齊⽅式:

struct A { char a; int b; }; struct alignas(std::max_align_t) B { char a; int b; float c; }; cout << alignof(A) << endl; cout << alignof(B) << endl; 

#25 多線程內存模型

請參見 LevelDB 中的跳表實現 中的 「C++ 中的 atomic 和 memory_order」一節。

#26 線程局部存儲

在多線程程序中,全局以及靜態變量會被多個線程共享,這在某些場景下是符合期望和需求的。

但在另⼀些場景下,我們希望能有線程級的變量,這種變量是線程獨享的,不受其他線程影響。我們稱之為線程局部存儲(TLS, thread local storage)

C++11 引⼊了 thread_local ⽤來聲明線程局部存儲,如下所⽰:

int thread_local num; 

#27 GC 接口

眾所周知 C++ 是⼀門顯式堆內存管理的語⾔,程序員需要時時刻刻關注⾃⼰對內存空間的分配和銷毀。 ⽽如果程序員沒有正確進⾏堆內存管理,就會造成程序的異常、錯誤、崩潰等。從語⾔層⾯是講,這些不正確的內存管理主要有:

  • 野指針:內存已經被銷毀,但指向它的指針依然被使⽤
  • 重復釋放:釋放已經被釋放過的內存,或者釋放被重新分配過的內存,導致重復釋放錯誤
  • 內存泄漏:程序中不再需要的內存空間卻沒有被及時釋放,導致隨着程序不斷運⾏內存不斷被⽆謂消耗

顯式內存管理可以為程序員提供極⼤的編程靈活性,但也提⾼了出錯的概率。為此,C++11 進⼀步改造了智能指針,同時也提供了⼀個 「最⼩垃圾回收」的標准。

⽬前⾮常多的現代語⾔都全⾯⽀持「垃圾回收」,例如 Java、Python、C#、Ruby、PHP 等都⽀持「垃圾回收」。 為實現「垃圾回收」,最重要的⼀點就是判斷對象或內存何時能夠被回收。判斷對象或內存是否可回收的⽅法主要有:

  1. 引用計數
  2. 跟蹤處理(跟蹤對象關系圖)。如 Java 中的「對象可達性分析」。

確定了對象或內存可被回收后,就需要進⾏回收,⽽這⾥又存在不同的回收策略和回收算法(簡單描述):

  1. 標記-清除
    第⼀步對對象和內存進⾏標記是否可回收,第⼆步對標記的內存進⾏回收。顯然這種⽅法將導致⼤量的內存碎⽚
  2. 標記-整理
    第⼀步同樣是標記。但是第⼆步不是直接清理,⽽是將「活對象」向左靠齊(整理)。但移動⼤量對象,將導致程 序中的引⽤需要進⾏更新。如果對象死亡的⽐較多,就要進⾏⽐較多的移動操作。所以適合「長壽」的對象。
  3. 復制算法。將堆空間分為兩個部分:fromto。from 空間⽤滿后啟動掃描標記,找出其中活着的對象,將其復制到 to 空間, 然后清空 from 空間。之后原先 to 變成了 from 空間供程序分配內存,原先的 from 變成 to,等待下⼀次垃圾回收收容那些「幸存者」。如果有⼤量幸存者,那么拷貝將導致較⼤性能消耗。因此適合短壽「朝⽣暮死」的對象。

⽽在實現時通常采⽤「分代收集」算法,即將堆空間分為 「新⽣代」「⽼年代」,新⽣代朝⽣暮死適合「拷貝算法」,⽼年代長壽適合「標記清理」或「標記整理」。

上述介紹了垃圾回收的相關算法,C++11 則是制定了「最⼩垃圾回收」的標准,所謂「最⼩」指的其實就是它壓根就不是⼀個完整的 GC,⽽是為了后續的 GC 鋪墊,⽬前也只是提供了⼀些庫函數來輔助 GC,如:
declare_reachable(聲明⼀個對象不能被回收)、undeclare_reachable(聲明⼀個對象可以被回收)。

由於 C++ 中的指針⼗分靈活,這種靈活性將導致 GC 誤判從⽽回收內存,因此提供這兩個函數保護對象:

int* p1 = new int(1); p1 += 10; // 將導致 GC 回收內存空間 p1 -= 10; // 指針的靈活性:又移動回來了 *p1 = 10; // 內存已被回收,導致程序錯誤 // 使用 declare_reachable 保護對象不被 GC int* p2 = new int(2); declare_reachable(p2); // p2 不可回收 p2 += 10; // GC 不會回收 p2 -= 10; *p2 = 10; // 程序正常 

從上可知,這兩個函數就是為舊程序兼容即將到來[4]的 C++ GC 而設計的。

[4] 看樣子是不會到來了。

上述介紹了這么多,最后再來介紹最尷尬的⼀點:現在還沒有編譯器實現 C++11 有關 GC 的標准

可以暫時忽略這條 GC 特性,實際上 C++ 的很多特性都可以忽略

#28 范圍 for

類似 Java 中的 foreach 循環

std::vector<int> vec = {1, 2, 3, 4}; for (auto element : vec) { std::cout << element << std::endl; // read only } 

#29 static_assert

我們常⽤ assert,即運⾏時斷⾔。但很多事情不該在運⾏時采取判斷和檢查,而應該在編譯期就進⾏嚴格斷⾔,例如數組的長度等。

C++11 引⼊了 static_assert 實現編譯期斷⾔:

static_assert(sizeof(void *) == 4,"64位系統不支持"); 

#30 智能指針

C++98 提供了模板類型「auto_ptr」來實現智能指針。auto_ptr 以對象的⽅式管理分配的內存,並在適當的時機釋放內存。程序員只需要將 new 操作返回的指針作為 auto_ptr 的初始值即可,如下所⽰:

auto_ptr(new int); 

但 auto_ptr 存在「進⾏拷貝時會將原指針置為 NULL」等缺陷,因此 C++11 引⼊了 unique_ptr、shared_ptr、 weak_ptr 三種智能指針。

  • unique_ptr: unique_ptr 和指定對象的內存空間緊密綁定,不允許與其他 unique_ptr 指針共享同⼀個對象內存。即內存所有權在同⼀個時間內是唯⼀的,但所有權卻可以通過 #05 條中提及的 move 和移動語義進⾏來實現「所有權」 轉移。如下所⽰:
unique_ptr<int> p1(new int(111)); unique_ptr<int> p2 = p1; // ⾮法,不可共享內存所有權 unique_ptr<int> p3 = move(p1); // 合法,移交所有權。p1 將喪失所有權 p3.reset(); // 顯式釋放內存 
  • shared_ptr:與 unique_ptr 相對,可以共享內存所有權,即多個 shared_ptr 可以指向同⼀個對象的內存。同時 shared_ptr 采⽤引⽤計數法來判斷內存是否還被需要,從⽽判斷是否需要進⾏回收。
shared_ptr<int> p4(new int(222)); shared_ptr<int> p5 = p4; // 合法 p4.reset(); // 「釋放」內存 // 由於采⽤引⽤計數法,p4.reset() 僅僅使得引⽤數減⼀ // 所指向的內存由於仍有 p5 所指向,所以不會被回收 // 訪問 *p5 是合法且有效的 cout << *p5 << endl; // 輸出 222 
  • weak_ptr:weak_ptr 可以指向 shared_ptr 指向的內存,且在必要時可以通過成員 lock 來返回⼀個指向當前內存的 shared_ptr 指針,如果當前內存已經被釋放,那么將 lock() 返回 nullptr。⽽另⼀個重點則是 weak_ptr 不參與引⽤計數。如同⼀個「虛擬指針」⼀樣指向 shared_ptr 指向的對象內存,⼀⽅⾯不妨礙內存的釋放,另⼀⽅⾯又可以通過 weak_ptr 判斷內存是否有效以及是否已經被釋放:
shared_ptr<int> p6(new int(333)); shared_ptr<int> p7 = p6; weak_ptr<int> weak_p8 = p7; shared_ptr<int> p9_from_weak_p8 = weak_p8.lock(); if (p9_from_weak_p8 != nullptr) { cout << "內存有效" << endl; } else { cout << "內存已被釋放" << endl; } p6.reset(); p7.reset(); // weak_p8 // 內存已被釋放,即使 weak_p8 還「指向」該內存 

weak_ptr 還有⼀個⾮常重要的應⽤並是解決 shared_ptr 引⽤計數法所帶來的 「循環引⽤」問題。所謂「循環引⽤」 如下圖所⽰:

 
pointer1.png

由於 ObjA 和 ObjB 內部有成員變量相互引⽤,即使將 P1 和 P2 引⽤去除,這兩個對象的引⽤計數仍然不為 0。但實際上兩個對象已經不可訪問,理應被回收。

使⽤ weak_ptr 來實現上⾯兩個對象的相互引⽤則可以解決該問題,如下圖所⽰:

 
pointer2.png

將 P1 和 P2 引⽤去除,此時 ObjA 和 ObjB 內部是通過 weak_ptr 相互引用的,由於 weak_ptr 不參與引用計數,因此 ObjA 和 ObjB 的引用計數被判斷為 0,ObjA 和 ObjB 將被正確回收。

C++14 新特性

#01 變量模板

我們已經有了類模板、函數模板,現在 C++14 為我們帶來了變量模板:

template<class T> constexpr T pi = T(3.1415926535897932385); int main() { cout << pi<int> << endl; cout << pi<float> << endl; cout << pi<double> << endl; return 0; } // 當然在以前也可以通過函數模板來模擬 // 函數模板 template<class T> constexpr T pi_fn() { return T(3.1415926535897932385); } 

#02 泛型 lambda

所謂「泛型 lambda」,就是在形參聲明中使用 auto 類型指示說明符的 lambda。例如:

auto lambda = [](auto x, auto y) { return x + y; }; 

#03 lambda 初始化捕獲

C++11 lambda 已經為我們提供了值捕獲和引⽤捕獲,但針對的實際都是左值,⽽右值對象⽆法被捕獲,這個問題在 C++14 中得到了解決:

int a = 1; auto lambda1 = [value = 1 + a] {return value;}; std::unique_ptr ptr(new int(10)); // 移動捕獲 auto lambda2 = [value = std::move(ptr)] {return *value;}; 

#04 new/delete elision

不知怎么翻譯好,new/delete 消除?new/delete 省略?
cppreference c++14 列出了這條,但沒有詳細說明。

由於 C++14 新提供了 make_unique 函數,unique_ptr 可在析構是自動刪除,再加上 make_shared 和 shared_ptr,基本可以覆蓋大多數場景和需求了。所以從 C++14 開始, new/delete 的使用應該會大幅度減少。

#05 constexpr 函數上放松的限制

在 C++11 的 #08 條中已經提及 constexpr 修飾的函數除了可以包含 using 指令、typedef 語句以及 static_assert 斷⾔ 外,只能包含⼀條 return 語句。

⽽ C++14 則放開了該限制,constexpr 修飾的函數可包含 if/switch 等條件語句,也可包含 for 循環

#06 ⼆進制字⾯量

C++14 的數字可⽤⼆進制形式表達,前綴使⽤ 0b0B

int a = 0b101010; // C++14 

#07 數字分隔符

使⽤單引號 ' 來提⾼數字可讀性:

auto integer_literal = 100'0000; 

GC、模塊、協程等重大特性唯唯諾諾,可有可無的特性 C++ 重拳出擊!

#08 函數的返回類型推導

上文提及了 C++11 中使用 auto/decltype 配合尾置返回值實現了函數返回值的推導,C++14 實現了一個 auto 並自動推導返回值類型:

auto Func(); // 返回類型由編譯器推斷 

#09 帶默認成員初始化器的聚合類

C++11 增加了默認成員初始化器,如果構造函數沒有初始化某個成員,並且這個成員擁有默認成員初始化器,就會⽤默認成員初始化器來初始化成員。

而在 C++11 中,聚合類(aggregate type)的定義被改為「明確排除任何含有默認成員初始化器」的類型。

因此,在 C++11 中,如果⼀個類含有默認成員初始化器,就不允許使⽤聚合初始化。C++14放松了這⼀限制:

struct CXX14_aggregate { int x; int y = 42; // 帶有默認成員初始化器 }; // C++11 中不允許 // 但 C++14允許 且 a.y 將被初始化為42 CXX14_aggregate a = { 1 }; 

#10 decltype(auto)

允許 auto 的類型聲明使⽤ decltype 的規則。也即,允許不必顯式指定作為decltype參數的表達式,而使用decltype對於給定表達式的推斷規則。
—— From Wikipedia C++14

看一個例子:

// 在另一個函數中對下面兩個函數進行轉發調用 std::string lookup1(); std::string& lookup2(); // 在 C++11 中,需要這么實現 std::string look_up_a_string_1() { returnlookup1(); } std::string& look_up_a_string_2() { returnlookup2(); } // 在 C++14 中,可以通過 decltype(auto) 實現 decltype(auto) look_up_a_string_1() { return lookup1(); } decltype(auto) look_up_a_string_2() { return lookup2(); } 

C++17 新特性

#01 折疊表達式

上文介紹了 C++11 中介紹了「變長參數模板」(C++11 第 #16 條)。在 C++11 中對變長參數進行展開比較麻煩,通常采用遞歸函數的方式進行展開:

void print() { // 遞歸終止函數 cout << "last" << endl; } template <class T, class ...Args> void print(T head, Args... rest) { cout << "parameter " << head << endl; print(rest...); // 遞歸展開 rest 變長參數 } 

C++17 引入「折疊表達式」來進一步支持變長參數的展開:

// ⼀元左折疊 // 只有一個操作符 「-」,且展開符 ... 位於參數包 args 的左側,固為一元左折疊 template<typename... Args> auto sub_val_left(Args&&... args) { return (... - args); } auto t = sub_val_left(2, 3, 4); // ((2 - 3) - 4) = -5; // 一元右折疊 // 只有一個操作符 「-」,且展開符 ... 位於參數包 args 的右側,固為一元右折疊 template<typename... Args> auto sub_val_right(Args&&... args) { return (args - ...); } auto t = sub_val_right(2, 3, 4); // (2 - (3 - 4)) = 3; // 二元左折疊 // 左右有兩個操作符 ,且展開符 ... 位於參數包 args 的左側,固為二元左折疊 template<typename... Args> auto sub_one_left(Args&&... args) { return (1 - ... - args); } auto t = sub_one_left(2, 3, 4); // ((1 - 2) - 3) - 4 = -8 // 二元右折疊 // 左右有兩個操作符,且展開符 ... 位於參數包 args 的右側,固為二元右折疊 template<typename... Args> auto sub_one_right(Args&&... args) { return (args - ... - 1); } auto t = sub_one_right(2, 3, 4); // 2 - (3 - (4 - 1)) = 2 

#02 類模板實參推導

C++17 之前類模板⽆法進⾏參數推導:

std::pair<int, string> a{ 1, "a"s }; // 需要指明 int, string 類型 

C++17 實現了類模板的實參類型推導:

std::pair a{ 1, "a"s }; // C++17,類模板可自行推導實參類型 

#03 auto 占位的⾮類型模板形參

template<auto n> struct B { /* ... */ } B<5> b1; // OK: 非類型模板形參類型為 int B<'a'> b2; // OK: 非類型模板形參類型為 char B<2.5> b3; // 錯誤(C++20前):非類型模板形參類型不能是 double 

#04 編譯期的 constexpr if 語句

C++17 將 constexpr 這個關鍵字引⼊到 if 語句中,允許在代碼中聲明常量表達式的判斷條件

template<typename T> auto print_info(const T& t) { if constexpr (std::is_integral<T>::value) { return t + 1; } else { return t + 1.1; } } 

上述代碼將在編譯期進行 if 語句的判斷,從而在編譯期選定其中一條分支。

#05 內聯變量(inline 變量)

看一個例子:

// student.h extern int age; // 全局變量 struct Student { static int age; // 靜態成員變量 }; // student.cpp int age = 18; int Student::foo = 18; 

在 C++17 之前,如果想要使用全局變量或類的靜態成員變量,需要在頭文件中聲明,然后在每個 cpp 文件中定義。

C++17 支持聲明內聯變量達到相同的效果:

// student.h inline int age = 18; struct Student { static inline int age = 18; }; 

#06 結構化綁定

類似於 JavaScript 中的解構賦值

⽰例:

tuple<int, double, string> f() { return make_tuple(1, 2.3, "456"); } int main() { int arr[2] = {1,2}; // 創建 e[2] // 復制 arr 到 e, 然后 a1 指代 e[0], b1 指代 e[1] auto [a1, b1] = arr; cout << a1 << ", " << b1 << endl; // a2 指代 arr[0], b2 指代 arr[1] auto& [a2, b2] = arr; cout << a2 << "," << b2<< endl; // 結構化綁定 tuple auto [x, y, z] = f(); cout << x << ", " << y << ", " << z << endl; return 0; } 

#07 if/switch 語句的變量初始化

if/switch 語句聲明並初始化變量,形式為:if (init; condition) 和 switch (init; condition)。例⼦:

for (int i = 0; i < 10; i++) { // int count = 5; 這條初始化語句直接寫在 if 語句中 if (int count = 5; i > count) { cout << i << endl; } } // char c(getchar()); 這條初始化語句直接寫在 switch 語句中 switch (char c(getchar()); c) { case 'a': left(); break; case 'd': right(); break; default: break; } 

#08 u8-char

字符前綴:

u8'c-字符' // UTF-8 字符字面量 

注意和上文的「字符串前綴」相區分,C++11 引入的 u8 是字符串前綴,C++17 補充 u8 可作為字符的前綴。

#09 簡化的嵌套命名空間

namespace X { namespace Y { … }} // 傳統 namespace X::Y { … } // C++17 簡化命名空間 

#10 using 聲明語句可以聲明多個名稱

struct A { void f(int) {cout << "A::f(int)" << endl;} }; struct B { void f(double) {cout << "B::f(double)" << endl;} }; struct S : A, B { using A::f, B::f; // C++17 }; 

#11 將 noexcept 作為類型系統的一部分

與返回類型相似,異常說明成為函數類型的一部分,但不是函數簽名的一部分

// 下面函數是不同類型函數,但擁有相同的函數簽名 void g() noexcept(false); void g() noexcept(true); 

#12 新的求值順序規則

在 C++17 之前,為了滿足各個編譯器在不同平台上做相應的優化,C++ 對一些求值順序未做嚴格規定。最典型的例子如下:

cout << i << i++; // C++17 之前,未定義行為 a[i] = i++; // C++17 之前,未定義行為 f(++i, ++i); // C++17 之前,未定義行為 

具體的,C++17 規定了以下求值順序:

  • a.b
  • a->b
  • a->*b
  • a(b1, b2, b3)
  • b @= a
  • a[b]
  • a << b
  • a >> b

順序規則為:a 的求值和所有副作用先序於 b,但同一個字母的順序不定

#13 強制的復制消除(guaranteed copy elision)

C++17 引入「強制的復制消除」,以便在滿足一定條件下能夠確保消除對象的復制。

在 C++11 之前已經存在所謂的復制消除技術(copy elision),即編譯器的返回值優化 RVO/NRVO。

RVO(return value optimization): 返回值優化
NRVO(named return value optimization):具名返回值優化

看下面的例子:

T Func() { return T(); } 

在傳統的復制消除(copy elision)規則下,上述代碼將會產生一個臨時對象,並將其拷貝給「返回值」。這個過程可能會被優化掉,也就是拷貝/移動函數根本不會被調用。但程序還是必須提供相應的拷貝函數。

再看如下代碼:

T t = Func(); 

上述代碼會將返回值拷貝給 t,這個拷貝操作依然可能被優化掉,但同樣的,程序依然需要提供相應的拷貝函數。

從上文可知,在傳統的復制消除規則下,下面代碼是非法的:

// 傳統的復制消除即使優化了拷貝函數的調用 // 但還是會檢查是否定義了拷貝函數等 struct T { T() noexcept = default; T(const T&) = delete; // C++11 中如果不提供相應的拷貝函數將會導致 return 與 賦值錯誤 T(T&&) = delete; }; T Func() { return T(); } int main() { T t = Func(); } 

而「強制復制消除」對於純右值 prvalue[5],將會真正消除上述復制過程[6],也不會檢查是否提供了拷貝/移動函數,所以上述代碼在 C++17 中是合法的。

[5] 在 C++17 之前,純右值為臨時對象,而 C++17 對純右值 prvalue 的定義進行了擴展:能夠產生臨時對象但還未產生臨時對象的表達式,如上例代碼中的 Func();
[6] 消除的原理:在滿足「純右值賦值給泛左值」這個條件時,T t = Func(); 會被優化成類似於 T t = T(); 這中間不會產生臨時對象。

但另一方面,對於「具名臨時對象」,不會進行「強制復制消除」:

T Func() { T t = ...; ... return t; } 

T 還是必須提供拷貝/移動函數,所以 C++17 對於具名返回值優化 NRVO (named return value optimization) 沒有變化。

關於強制復制消除,可以參考下面鏈接的第一個回答,回答的很清楚:
How does guaranteed copy elision work?

這一切是否來源於 C++ 的初始設計問題: = 運算符的默認重載,賦予了 = 運算符對象拷貝的語義。

#14 lambda 表達式捕獲 *this

#include <iostream> struct Baz { auto foo() { // 通過 this 捕獲對象,之后在 lambda 即可訪問對象的成員變量 s return[this]{ std::cout << s << std::endl; }; } std::string s; }; int main() { auto f1 = Baz{ "ala" }.foo(); auto f2 = Baz{ "ula" }.foo(); f1(); f2(); } 

但上述代碼存在一個缺陷:捕獲的是當前對象,如果 lambda 表達式對成員變量的訪問超出了當前對象的生命周期,就會導致問題。

C++17 提供了 *this 捕獲當前對象的副本

auto foo() { return[*this]{ std::cout << s << std::endl; }; } 

#15 constexpr 的 lambda 表達式

C++17 的 lambda 聲明為 constexpr 類型,這樣的 lambda 表達式可以用在其他需要 constexpr 類型的上下文中。

int y = 32; auto func = [y]() constexpr { int x = 10; return y + x; }; 

#16 屬性命名空間不必重復

在上文的 C++11 #21 條中已經介紹了屬性的概念,對於由實現定義的行為的非標准屬性,可能會帶有命名空間:

[[gnu::always_inline, gnu::const, gnu::hot, nodiscard]] inline int f(); // 聲明 f 帶四個屬性 [[gnu::always_inline, gnu::const, gnu::hot, nodiscard]] int f(); // 同上,但使用含有四個屬性的單個屬性說明符 

C++11 中上述屬性的命名空間需要重復聲明,C++17 簡化了屬性命名空間的定義:

[[using gnu : const, always_inline, hot]] [[nodiscard]] int f[[gnu::always_inline]](); // 屬性可出現於多個說明符中 

#17 新屬性 [[fallthrough]] [[nodiscard]] 和 [[maybe_unused]]

C++11 僅自帶了兩個標准屬性,C++17 繼續擴展了幾個標准屬性。

fallthrough

// 以下代碼因為沒有 case 中沒有 break; // 所以將會發生 case 穿透 // 編譯時編譯器將會發出警告 int x = 2; switch (x) { case 2: result++; case 0: result++; default: result++; } // 有時候我們需要 case 穿透,如匹配到 2 就一直執行后續的 case // 此時可以使用屬性 [[fallthrough]],使用后,編譯器將不會發出警告 switch (x) { case 2: result++; [[fallthrough]]; // Added case 0: result++; [[fallthrough]]; // Added default: result++; } 

nodiscard
在開發過程中經常需要對函數返回值進行檢查,這一步驟在不少業務場景下是必須的,例如:

// 許多人會遺漏對返回值進行檢查的步驟 // 導致了很多業務層面潛在的缺陷 if (CallService() != ret) { // ... } // C++17 引入 [[nodiscard]] 屬性來「提醒」調用者檢查函數的返回值 [[nodiscard]] int CallService() { return CallServiceRemote(); } CallService(); // 如果只調用而不檢查,編譯器將發出警告 if (CallService() != ret) { // pass // ... } 

maybe_unused
如果我們以 -Wunused 與 -Wunused-parameter 編譯以下代碼,編譯器則可能報出警告:

int test(int a, int b, int c) { int result = a + b; #ifdef ENABLE_FEATURE_C result += c; #endif return result; } 

原因是編譯器認為 c 是未用到的變量,但實際上並非無用。C++17 中可以使用 [[maybe_unused]] 來抑制「針對未使用實體」的警告:

int test(int a, int b, [[maybe_unused]] int c) { int result = a + b; #ifdef ENABLE_FEATURE_C result += c; #endif return result; } 

#18 __has_include

表明指定名稱的頭或源文件是否存在:

#if __has_include("has_include.h") #define NUM 1 #else #define NUM 0 #endif 

C++20 新特性

#01 特性測試宏

為 C++11 和其后所引入的 C++ 語言和程序庫的功能特性定義了一組預處理器宏。使之成為檢測這些功能特性是否存在的一種簡單且可移植的方式。例如:

__has_cpp_attribute(fallthrough) // 判斷是否支持 fallthrough 屬性 #ifdef __cpp_binary_literals // 檢查「二進制字面量」特性是否存在 #ifdef __cpp_char8_t // char8 t #ifdef __cpp_coroutines // 協程 // ... 

#02 三路比較運算符 <=>

// 若 lhs < rhs 則 (a <=> b) < 0 // 若 lhs > rhs 則 (a <=> b) > 0 // 而若 lhs 和 rhs 相等/等價則 (a <=> b) == 0 lhs <=> rhs 

#04 范圍 for 中的初始化語句和初始化器

C++17 引入了 if/switch 的初始化語句,C++20 引入了范圍 for 的初始化:

// 將 auto list = getList(); 初始化語句直接放在了范圍 for 語句中 for (auto list = getList(); auto& ele : list) { // ele = .... } 

另外 C++20 的范圍 for 還可支持一定的函數式編程風格,例如引入管道符 | 實現函數組合:

// 范圍庫 auto even = [](int i){ return 0 == i % 2; }; auto square = [](int i) { return i * i; }; // ints 輸出到 std::view::filter(even) ,處理后得到所有偶數 // 上一個結果輸出到 std::view::transform(square),將所有偶數求平方 // 循環遍歷所有偶數的平方 for (int i : ints | std::view::filter(even) | std::view::transform(square)) { // ... } 

#05 char8_t

C++20 新增加 char8_t 類型。

char8_t 用來表示 UTF-8 字符,要求大到足以表示任何 UTF-8 編碼單元( 8 位)。

#06 [[no_unique_address]]

[[no_unique_address]] 屬性修飾的數據成員可以被優化為不占空間:

struct Empty {}; // 空類 struct X { int i; Empty e; }; struct Y { int i; [[no_unique_address]] Empty e; }; struct Z { char c; [[no_unique_address]] Empty e1, e2; }; struct W { char c[2]; [[no_unique_address]] Empty e1, e2; }; int main() { // 任何空類類型對象的大小至少為 1 static_assert(sizeof(Empty) >= 1); // 至少需要多一個字節以給 e 唯一地址 static_assert(sizeof(X) >= sizeof(int) + 1); // 優化掉空成員 std::cout << "sizeof(Y) == sizeof(int) is " << std::boolalpha << (sizeof(Y) == sizeof(int)) << '\n'; // e1 與 e2 不能共享同一地址,因為它們擁有相同類型,盡管它們標記有 [[no_unique_address]]。 // 然而,其中一者可以與 c 共享地址。 static_assert(sizeof(Z) >= 2); // e1 與 e2 不能擁有同一地址,但它們之一能與 c[0] 共享,而另一者與 c[1] 共享 std::cout << "sizeof(W) == 2 is " << (sizeof(W) == 2) << '\n'; } 

#07 [[likely]]

[[likely]] 屬性用來告訴編譯器哪條分支執行的概率會更大,從而幫助編譯器進行代碼編譯的優化

if (a > b) [[likely]] { // ... } 

第一直覺真的是奇葩特性,好奇能優化到什么程度以至於專門增加語言特性來要求程序員配合這種優化
包括下文的頭文件,讓我覺得 C++ 很多時候不是編譯器為程序員服務,而是程序員為編譯器服務

#08 [[unlikely]]

與 [[likely]] 相對應:

if (a>b) [[unlikely]] { // ... } 

#09 lambda 初始化捕獲中的包展開

在 C++20 之前,lambda 表達式對與包展開無法進行初始化捕獲,如果想要對包展開進行初始化捕獲,需要通過 make_tuple 和 apply 來實現,如下所示:

template <class... Args> auto delay_invoke_foo(Args... args) { // 對 args 進行 make_tuple,然后再用 apply 恢復 return [tup=std::make_tuple(std::move(args)...)]() -> decltype(auto) { return std::apply([](auto const&... args) -> decltype(auto) { return foo(args...); }, tup); }; } 

C++20 將直接支持 lambda 對包展開進行初始化捕獲,如下所示:

template <class... Args> auto delay_invoke_foo(Args... args) { // 直接 ...args = xxxxx return [...args=std::move(args)]() -> decltype(auto) { return foo(args...); }; } 

#10 移除了在多種上下文語境中,使用 typename 關鍵字以消除類型歧義的要求

P0634R3
C++20 之前,在使用了模板類型的地方需要使用 typename 來消除歧義,如下所示:

template<typename T> typename std::vector<T>::iterator // std::vector<T>::iterator 之前必須使用 typename 關鍵字 

C++20 則允許在一些上下文語境中省略 typename,如下所示:

template<typename T> std::vector<T>::iterator // 省略 typename 關鍵字 

#11 consteval、constinit

consteval
上文提及過 constexpr 函數可以在編譯期運行,也可以在運行期執行。C++20 為了更加明確場景和語義,提供了只能在編譯期執行的 consteval,consteval 修飾的函數返回的值如果不能在編譯器確定,則編譯無法通過。

constinit
在 C++ 中,對於靜態存儲期的變量的初始化,通常會有兩種情況:

  • 在編譯期初始化
  • 在被第一次加載聲明時初始化

其中第二種情況由於靜態變量初始化順序的原因存在着隱藏的風險。

所以 C++20 提供了 constinit,以便使某些應該在編譯期初始化的變量被確保的在編譯期初始化。

#12 更為寬松的 constexpr 要求

從 C++11 一直到 C++20 就一直在給 constexpr 「打補丁」,就不能一次性擴展其能力嗎

引用自 C++20 新增特性
C++20 中 constexpr 擴展的能力:

  • constexpr虛函數
    • constexpr 的虛函數可以重寫非 constexpr 的虛函數
    • 非 constexpr 虛函數將重載 constexpr 的虛函數
  • constexpr 函數支持:
    • 使用 dynamic_cast() 和 typeid
    • 動態內存分配
    • 更改union成員的值
    • 包含 try/catch
      • 但是不允許 throw 語句
      • 在觸發常量求值的時候 try/catch 不發生作用
      • 需要開啟 constexpr std::vector
  • constexpr 支持 string & vector 類型

#13 規定有符號整數以補碼實現

在 C++20 之前,有符號整數的實現沒有明確以標准的形式規定(雖然在實現時基本都采用補碼)。C++20 明確規定了有符號整數使用補碼實現。

#14 使用圓括號的聚合初始化

C++20 引入了一些新的聚合初始化形式,如下所示:

T object = { .designator = arg1 , .designator { arg2 } ... }; //(since C++20) T object { .designator = arg1 , .designator { arg2 } ... }; // (since C++20) T object (arg1, arg2, ...); // (since C++20) 

其中之前沒有過的就是第三種形式: T object (arg1, arg2, ...),使用圓括號進行初始化。

#15 協程

進程:操作系統資源分配的基本單元。調度涉及到用戶空間和內核空間的切換,資源消耗較大。
線程:操作系統運行的基本單元。在同一個進程資源的框架下,實現搶占式多任務,相對進程,降低了執行單元切換的資源消耗。
協程:和線程非常類似。但是轉變一個思路實現協作式多任務,由用戶來實現協作式調度(主動交出控制權)

高德納 Donald Knuth:

子程序就是協程的一種特例

協程是廣義的函數(子程序),只是它的流程由用戶進行一定程度的函數過程切換和控制

舉一個例子:

# 協程實現的生產者和消費者 def consumer(): r = '' while True: n = yield r if not n: return print('[CONSUMER] Consuming %s...' % n) time.sleep(1) r = '200 OK' def produce(c): c.next() n = 0 while n < 5: n = n + 1 print('[PRODUCER] Producing %s...' % n) r = c.send(n) print('[PRODUCER] Consumer return: %s' % r) c.close() if __name__=='__main__': c = consumer() produce(c) 

生產者生產消息,待消費者執行完畢后,通過 yield 讓出控制權切換回生產者繼續生產。

yield: 執行到這里主動讓出控制權,返回一個值,並等待上一個上下文對自己的進一步調度

上面是協程的的純粹概念,但是很多語言對協程會有不同的實現和封裝,導致協程的概念被進一步擴展和延伸。

例如 golang 中的 Goroutines 其實並不是一個純粹的協程概念,而是對協程和線程的封裝和實現,可以說在用戶狀態下的執行單元調度,同時又解決了傳統協程無法利用多核能力的缺陷。所以很多資料將其稱為 「輕量級線程」或 「用戶態線程」。

另外,在異步編程方面,協程有一個特別的優勢:
通過更符合人類直覺的順序執行來表達異步邏輯

在 JS 生態中(尤其以 Node.js 為代表)我們編寫異步邏輯,經常使用回調來實現結果返回。而如果是多層級異步調用的場景,容易陷入 「callback hell 回調地獄」。

如下所示:

fs.readFile(fileA, function (err, data) { fs.readFile(fileB, function (err, data) { // ... }); }); 

JS 后續引入了 Promise,簡化回調調用形式,如下所示:

readFile(fileA) .then(function(data){ console.log(data.toString()); }) .then(function(){ return readFile(fileB); }) .then(function(data){ console.log(data.toString()); }) .catch(function(err) { console.log(err); }); 

再后續引入了協程的一種實現——Generator 生成器

var fetch = require('node-fetch'); function* gen(){ var url = 'https://api.github.com/users/github'; var result = yield fetch(url); console.log(result.bio); } var g = gen(); var result = g.next(); result.value.then(function(data){ return data.json(); }).then(function(data){ g.next(data); }); 

Generator 函數可以暫停執行(yield)和恢復執行(next),這是它能用來實現異步編程的根本原因

而 JS 后續底層通過 yield/generator 實現的 async & await 異步編程體驗,也會使得 JS 程序員對協程的直觀感受為「回調調度器」。

而 C++20 引入的則是相對純粹的協程,例如可以實現一個 generator函數或者生成器:

experimental::generator<int> GetSequenceGenerator( int startValue, size_t numberOfValues) { for (int i = 0 startValue; i < startValue + numberOfValues; ++i){ time_t t = system_clock::to_time_t(system_clock::now()); cout << std:: ctime(&t); co_yield i; } } int main() { auto gen = GetSequenceGenerator(10, 5); for (const auto& value : gen) { cout << value << "(Press enter for next value)" << endl; cin.ignore(); } } 

#16 模塊

歷史包袱-頭文件

請看如下代碼:

// person.cpp int rest() { Play(); return 0; } // game.cpp int play() { LaunchSteam(); return 0; } 
  1. 由於 C/C++ 時代 .obj 等結果文件可能來自於其他語言。固每個源文件不與其他源文件產生關聯,需獨立編譯。在這樣的背景下,我們站在編譯器的角度嘗試編譯 person.cpp ,會發現編譯將無法進行。原因是 Play 的返回類型、參數類型等元信息無法獲取。那么是否可以生成外部符號等待鏈接階段呢?
  2. 答案是否定的。即無法推遲到鏈接階段。原因是 C++ 編譯時不會將函數的返回值、參數等元信息編譯進 .obj 等結果,固在鏈接階段依然獲取不到 Play 函數相關的元信息。之所以沒有像 Java/C# 等現代語言這樣將元信息寫到編譯結果中,是因為 C/C++ 時代內存等資源稀缺,所以想方設法的節省各種資源。

而由於上述歷史原因,導致了 C++ 最終將這種不便轉交給了程序員。程序員在調用另一個源文件的函數時需要事先聲明函數原型,而如果在每個使用到相應函數的源文件中都重復聲明一次就太過於低級,於是出現了所謂的頭文件,簡化聲明工作。

另一方面,頭文件從一定程度起到了接口描述的作用,但有些人把頭文件當作是「實現與接口分離的設計思想」下的成果就非常的牽強了。

頭文件本質上是圍繞着編譯期的一種概念,是 C/C++ 由於歷史原因不得不由程序員使用頭文件輔助編譯器完成編譯工作。

而接口的概念是圍繞着業務開發或編程階段的,是另一層面的事情。

如果不好理解,可以思考一下,Java/C# 沒有頭文件的語言是如何實現所謂「頭文件提供接口」這一功能的?

如果需要實現,編譯器可以直接從源碼文件抽離出接口信息生成接口文件即可,而且還可以根據訪問權限來決定哪些該對外暴露,哪些不能暴露。甚至可以以 .h 為后綴讓那些覺得「頭文件起到接口作用」的程序員好受些。

C++20 引入了模塊,模塊的其中一個作用就是將 header編譯單元統一在了一起。

// example 模塊 export module example; //聲明一個模塊名字為example export int add(int first, int second) { //可以導出的函數 return first + second; } // 使用 example 模塊 import example; //導入上述定義的模塊 int main() { add(1, 2); //調用example模塊中的函數 } 

#17 限定與概念(concepts)

concepts 是 C++20 的重要更新之一,它是模板能力的擴展。在 C++20 之前,我們的模板參數是沒有明確限定的,如下所示:

template<class L, class T> void find(const L& list, const T& t); // 從 list 列表中查找 t 

上面的參數類型 L 與 T 沒有任何的限制,但實際上是存在着隱含的限定條件的:

  • L 應該是一個可迭代類型
  • L 中的元素類型應該和 T 類型相同
  • L 中的元素應該和 T 類型可進行相等比較

程序員應當知曉上述隱含條件,否則編譯器就會輸出一堆錯誤。而現在可以通過 concepts 將上述限定條件告知編譯器,在使用錯誤將得到直觀的錯誤原因。

例如使用 concepts 限定參數可 hash:

// 定義概念 template<typename T> concept Hashable = requires(T a) { // 下面語句的限定含義為: // 限定 std::hash(a) 返回值可轉換成 std::size_t { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>; }; // 使用概念對模板參數進行限定 template<typename T> auto my_hash(T) requires Hashable<T> { // .... } 

對於上述的 my_hash 函數也可通過簡化的方式進行:

// 簡化 template<Hashable T> auto my_hash(T) { // .... } 

#18 縮略函數模板

通常聲明函數模板的形式如下:

template<class T> void f(T); template<C1 T> void f2(T); // C1 如果是一個 concept 概念 // ... 

C++20 可以采用 autoconcept auto 來實現更為簡短的函數模板聲明形式:

void f1(auto); // 等同於 template<class T> void f(T); void f2(C1 auto); // template<C1 T> void f2(T); // ... 

#19 數組長度推導

C++20 將允許 new int[]{1, 2, 3} 的寫法,編譯器可自動推導數組長度。

小結

C++ 最根本的設計理念就是為了運行效率服務,甚至專門增加新特性要求程序員配合編譯器來做優化。但另一方面, C++ 后期一直從 Java/JavaScript/Go/Python 等語言中借鑒特性,而其中很多是無關緊要的語法糖,對於真正至關重要的特性卻又一直拖到了 0202 年才推出標准。

C++ 20 真正在業界扎穩又是要到何年何月,至於形成與其他現代語言一樣完善、統一的生態更是遙不可期

這導致本就繁雜的 C++ 的語法隨着時間推移變得更加混亂,這進一步提高了 C++ 的學習與使用成本。唯一的好處就是提高了部分現有 C++ 程序員的自豪感,畢竟部分程序員是以自己掌握的工具難度為傲的。這些人不僅將「工具的難度」與「技術水平」掛鈎,有時甚至以此標榜自己的智商。建議有此想法的人閱讀並背誦新華字典全典或者用匯編完成所有工作

C++ 有其對應的應用場景,在一些運行效率要求極高的基礎組件的開發上,在絕大多數的游戲開發場景下,C++ 有其不可替代性。但在一些上層的應用場景,尤其是在更接近用戶的互聯網業務上使用 C++ 基本都是由於歷史債務[7]



作者:404_89_117_101
鏈接:https://www.jianshu.com/p/8c4952e9edec
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

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



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