c++11 新特性 (轉)
本文基本上涵蓋了c++11的所有新特性,並有詳細程式碼介紹其用法,對關鍵知識點做了深入分析,對重要的知識點我單獨寫了相關文章並附上了相關連結,我整理了完備的c++新特性腦圖(由於圖片太大,我沒有放在文章裡,同學可以在後臺回覆訊息“新特性”,即可下載完整圖片)。
auto & decltype
關於C++11新特性,最先提到的肯定是型別推導,C++11引入了auto和decltype關鍵字,使用他們可以在編譯期就推匯出變數或者表示式的型別,方便開發者編碼也簡化了程式碼。
- auto:讓編譯器在編譯器就推匯出變數的型別,可以通過=右邊的型別推匯出變數的型別。
auto a = 10; // 10是int型,可以自動推匯出a是int
- decltype:相對於auto用於推導變數型別,而decltype則用於推導表示式型別,這裡只用於編譯器分析表示式的型別,表示式實際不會進行運算。
cont int &i = 1; int a = 2; decltype(i) b = 2; // b是const int&
關於auto和decltype的詳細介紹請看:一文吃透C++11中auto和decltype知識點
左值右值
眾所周知C++11新增了右值引用,這裡涉及到很多概念:
- 左值:可以取地址並且有名字的東西就是左值。
- 右值:不能取地址的沒有名字的東西就是右值。
- 純右值:運算表示式產生的臨時變數、不和物件關聯的原始字面量、非引用返回的臨時變數、lambda表示式等都是純右值。
- 將亡值:可以理解為即將要銷燬的值。
- 左值引用:對左值進行引用的型別。
- 右值引用:對右值進行引用的型別。
- 移動語義:轉移資源所有權,類似於轉讓或者資源竊取的意思,對於那塊資源,轉為自己所擁有,別人不再擁有也不會再使用。
- 完美轉發:可以寫一個接受任意實參的函式模板,並轉發到其它函式,目標函式會收到與轉發函式完全相同的實參。
- 返回值優化:當函式需要返回一個物件例項時候,就會建立一個臨時物件並通過複製建構函式將目標物件複製到臨時物件,這裡有複製建構函式和解構函式會被多餘的呼叫到,有代價,而通過返回值優化,C++標準允許省略呼叫這些複製建構函式。
這裡的詳細介紹請看:左值引用、右值引用、移動語義、完美轉發,你知道的不知道的都在這裡
列表初始化
在C++11中可以直接在變數名後面加上初始化列表來進行物件的初始化,詳細介紹一定要看這篇文章:學會C++11列表初始化
std::function & std::bind & lambda表示式
c++11新增了std::function、std::bind、lambda表示式等封裝使函式呼叫更加方便,詳細介紹請看:搞定c++11新特性std::function和lambda表示式
模板的改進
C++11關於模板有一些細節的改進:
- 模板的右尖括號
- 模板的別名
- 函式模板的預設模板引數
詳細介紹請看:C++11的模板改進
併發
c++11關於併發引入了好多好東西,有:
- std::thread相關
- std::mutex相關
- std::lock相關
- std::atomic相關
- std::call_once相關
- volatile相關
- std::condition_variable相關
- std::future相關
- async相關
詳細介紹請看:c++11新特性之執行緒相關所有知識點
這裡也使用c++11來實現的執行緒池和定時器,可以看:
智慧指標
很多人談到c++,說它特別難,可能有一部分就是因為c++的記憶體管理吧,不像java那樣有虛擬機器動態的管理記憶體,在程式執行過程中可能就會出現記憶體洩漏,然而這種問題其實都可以通過c++11引入的智慧指標來解決,相反我還認為這種記憶體管理還是c++語言的優勢,因為盡在掌握。
c++11引入了三種智慧指標:
- std::shared_ptr
- std::weak_ptr
- std::unique_ptr
詳細介紹請看:c++11新特性之智慧指標
基於範圍的for迴圈
直接看程式碼
vector<int> vec; for (auto iter = vec.begin(); iter != vec.end(); iter++) { // before c++11 cout << *iter << endl; } for (int i : vec) { // c++11基於範圍的for迴圈 cout << "i" << endl; }
委託建構函式
委託建構函式允許在同一個類中一個建構函式呼叫另外一個建構函式,可以在變數初始化時簡化操作,通過程式碼來感受下委託建構函式的妙處吧:
不使用委託建構函式:
struct A { A(){} A(int a) { a_ = a; } A(int a, int b) { // 好麻煩 a_ = a; b_ = b; } A(int a, int b, int c) { // 好麻煩 a_ = a; b_ = b; c_ = c; } int a_; int b_; int c_; };
使用委託建構函式:
struct A { A(){} A(int a) { a_ = a; } A(int a, int b) : A(a) { b_ = b; } A(int a, int b, int c) : A(a, b) { c_ = c; } int a_; int b_; int c_; };
初始化變數是不是方便了許多。
繼承建構函式
繼承建構函式可以讓派生類直接使用基類的建構函式,如果有一個派生類,我們希望派生類採用和基類一樣的構造方式,可以直接使用基類的建構函式,而不是再重新寫一遍建構函式,老規矩,看程式碼:
不使用繼承建構函式:
struct Base { Base() {} Base(int a) { a_ = a; } Base(int a, int b) : Base(a) { b_ = b; } Base(int a, int b, int c) : Base(a, b) { c_ = c; } int a_; int b_; int c_; }; struct Derived : Base { Derived() {} Derived(int a) : Base(a) {} // 好麻煩 Derived(int a, int b) : Base(a, b) {} // 好麻煩 Derived(int a, int b, int c) : Base(a, b, c) {} // 好麻煩 }; int main() { Derived a(1, 2, 3); return 0; }
使用繼承建構函式:
struct Base { Base() {} Base(int a) { a_ = a; } Base(int a, int b) : Base(a) { b_ = b; } Base(int a, int b, int c) : Base(a, b) { c_ = c; } int a_; int b_; int c_; }; struct Derived : Base { using Base::Base; }; int main() { Derived a(1, 2, 3); return 0; }
只需要使用using Base::Base繼承建構函式,就免去了很多重寫程式碼的麻煩。
nullptr
nullptr是c++11用來表示空指標新引入的常量值,在c++中如果表示空指標語義時建議使用nullptr而不要使用NULL,因為NULL本質上是個int型的0,其實不是個指標。舉例:
void func(void *ptr) { cout << "func ptr" << endl; } void func(int i) { cout << "func i" << endl; } int main() { func(NULL); // 編譯失敗,會產生二義性 func(nullptr); // 輸出func ptr return 0; }
final & override
c++11關於繼承新增了兩個關鍵字,final用於修飾一個類,表示禁止該類進一步派生和虛擬函式的進一步過載,override用於修飾派生類中的成員函式,標明該函式重寫了基類函式,如果一個函式聲明瞭override但父類卻沒有這個虛擬函式,編譯報錯,使用override關鍵字可以避免開發者在重寫基類函式時無意產生的錯誤。
示例程式碼1:
struct Base { virtual void func() { cout << "base" << endl; } }; struct Derived : public Base{ void func() override { // 確保func被重寫 cout << "derived" << endl; } void fu() override { // error,基類沒有fu(),不可以被重寫 } };
示例程式碼2:
struct Base final { virtual void func() { cout << "base" << endl; } }; struct Derived : public Base{ // 編譯失敗,final修飾的類不可以被繼承 void func() override { cout << "derived" << endl; } };
default
c++11引入default特性,多數時候用於宣告建構函式為預設建構函式,如果類中有了自定義的建構函式,編譯器就不會隱式生成預設建構函式,如下程式碼:
struct A { int a; A(int i) { a = i; } }; int main() { A a; // 編譯出錯 return 0; }
上面程式碼編譯出錯,因為沒有匹配的建構函式,因為編譯器沒有生成預設建構函式,而通過default,程式設計師只需在函式聲明後加上“=default;
”,就可將該函式宣告為 defaulted 函式,編譯器將為顯式宣告的 defaulted 函式自動生成函式體,如下:
struct A { A() = default; int a; A(int i) { a = i; } }; int main() { A a; return 0; }
編譯通過。
delete
c++中,如果開發人員沒有定義特殊成員函式,那麼編譯器在需要特殊成員函式時候會隱式自動生成一個預設的特殊成員函式,例如拷貝建構函式或者拷貝賦值操作符,如下程式碼:
struct A { A() = default; int a; A(int i) { a = i; } }; int main() { A a1; A a2 = a1; // 正確,呼叫編譯器隱式生成的預設拷貝建構函式 A a3; a3 = a1; // 正確,呼叫編譯器隱式生成的預設拷貝賦值操作符 }
而我們有時候想禁止物件的拷貝與賦值,可以使用delete修飾,如下:
struct A { A() = default; A(const A&) = delete; A& operator=(const A&) = delete; int a; A(int i) { a = i; } }; int main() { A a1; A a2 = a1; // 錯誤,拷貝建構函式被禁用 A a3; a3 = a1; // 錯誤,拷貝賦值操作符被禁用 }
delele函式在c++11中很常用,std::unique_ptr就是通過delete修飾來禁止物件的拷貝的。
explicit
explicit專用於修飾建構函式,表示只能顯式構造,不可以被隱式轉換,根據程式碼看explicit的作用:
不用explicit:
struct A { A(int value) { // 沒有explicit關鍵字 cout << "value" << endl; } }; int main() { A a = 1; // 可以隱式轉換 return 0; }
使用explicit:
struct A { explicit A(int value) { cout << "value" << endl; } }; int main() { A a = 1; // error,不可以隱式轉換 A aa(2); // ok return 0; }
const
因為要講後面的constexpr,所以這裡簡單介紹下const。
const字面意思為只讀,可用於定義變數,表示變數是隻讀的,不可以更改,如果更改,編譯期間就會報錯。
主要用法如下:
- 用於定義常量,const的修飾的變數不可更改。
const int value = 5;
- 指標也可以使用const,這裡有個小技巧,從右向左讀,即可知道const究竟修飾的是指標還是指標所指向的內容。
char *const ptr; // 指標本身是常量 const char* ptr; // 指標指向的變數為常量
- 在函式引數中使用const,一般會傳遞類物件時會傳遞一個const的引用或者指標,這樣可以避免物件的拷貝,也可以防止物件被修改。
class A{}; void func(const A& a);
- const修飾類的成員變數,表示是成員常量,不能被修改,可以在初始化列表中被賦值。
class A { const int value = 5; }; class B { const int value; B(int v) : value(v){} };
- 修飾類成員函式,表示在該函式內不可以修改該類的成員變數。
class A{ void func() const; };
- 修飾類物件,類物件只能呼叫該物件的const成員函式。
class A { void func() const; }; const A a; a.func();
constexpr
constexpr是c++11新引入的關鍵字,用於編譯時的常量和常量函式,這裡直接介紹constexpr和const的區別:
兩者都代表可讀,const只表示read only的語義,只保證了執行時不可以被修改,但它修飾的仍然有可能是個動態變數,而constexpr修飾的才是真正的常量,它會在編譯期間就會被計算出來,整個執行過程中都不可以被改變,constexpr可以用於修飾函式,這個函式的返回值會盡可能在編譯期間被計算出來當作一個常量,但是如果編譯期間此函式不能被計算出來,那它就會當作一個普通函式被處理。如下程式碼:
#include<iostream> using namespace std; constexpr int func(int i) { return i + 1; } int main() { int i = 2; func(i);// 普通函式 func(2);// 編譯期間就會被計算出來 }
enum class
c++11新增有作用域的列舉型別,看程式碼
不帶作用域的列舉程式碼:
enum AColor { kRed, kGreen, kBlue }; enum BColor { kWhite, kBlack, kYellow }; int main() { if (kRed == kWhite) { cout << "red == white" << endl; } return 0; }
如上程式碼,不帶作用域的列舉型別可以自動轉換成整形,且不同的列舉可以相互比較,程式碼中的紅色居然可以和白色比較,這都是潛在的難以除錯的bug,而這種完全可以通過有作用域的列舉來規避。
有作用域的列舉程式碼:
enum class AColor { kRed, kGreen, kBlue }; enum class BColor { kWhite, kBlack, kYellow }; int main() { if (AColor::kRed == BColor::kWhite) { // 編譯失敗 cout << "red == white" << endl; } return 0; }
使用帶有作用域的列舉型別後,對不同的列舉進行比較會導致編譯失敗,消除潛在bug,同時帶作用域的列舉型別可以選擇底層型別,預設是int,可以改成char等別的型別。
enum class AColor : char { kRed, kGreen, kBlue };
我們平時程式設計過程中使用列舉,一定要使用有作用域的列舉取代傳統的列舉。
非受限聯合體
c++11之前union中資料成員的型別不允許有非POD型別,而這個限制在c++11被取消,允許資料成員型別有非POD型別,看程式碼:
struct A { int a; int *b; }; union U { A a; // 非POD型別 c++11之前不可以這樣定義聯合體 int b; };
對於什麼是POD型別,大家可以自行查下資料,大體上可以理解為物件可以直接memcpy的型別。
sizeof
c++11中sizeof可以用的類的資料成員上,看程式碼:
c++11前:
struct A { int data[10]; int a; }; int main() { A a; cout << "size " << sizeof(a.data) << endl; return 0; }
c++11後:
struct A { int data[10]; int a; }; int main() { cout << "size " << sizeof(A::data) << endl; return 0; }
想知道類中資料成員的大小在c++11中是不是方便了許多,而不需要定義一個物件,在計算物件的成員大小。
assertion
static_assert(true/false, message);
c++11引入static_assert宣告,用於在編譯期間檢查,如果第一個引數值為false,則列印message,編譯失敗。
自定義字面量
c++11可以自定義字面量,我們平時c++中都或多或少使用過chrono中的時間,例如:
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 100ms std::this_thread::sleep_for(std::chrono::seconds(100)); // 100s
其實沒必要這麼麻煩,也可以這麼寫:
std::this_thread::sleep_for(100ms); // c++14裡可以這麼使用,這裡只是舉個自定義字面量使用的例子 std::this_thread::sleep_for(100s);
這就是自定義字面量的使用,示例如下:
struct mytype { unsigned long long value; }; constexpr mytype operator"" _mytype ( unsigned long long n ) { return mytype{n}; } mytype mm = 123_mytype; cout << mm.value << endl;
關於自定義字面量,可以看下chrono的原始碼,相信大家會有很大收穫,需要原始碼分析chrono的話,可以留言給我。
記憶體對齊
什麼是記憶體對齊
理論上計算機對於任何變數的訪問都可以從任意位置開始,然而實際上系統會對這些變數的存放地址有限制,通常將變數首地址設為某個數N的倍數,這就是記憶體對齊。
為什麼要記憶體對齊
- 硬體平臺限制,記憶體以位元組為單位,不同硬體平臺不一定支援任何記憶體地址的存取,一般可能以雙位元組、4位元組等為單位存取記憶體,為了保證處理器正確存取資料,需要進行記憶體對齊。
- 提高CPU記憶體訪問速度,一般處理器的記憶體存取粒度都是N的整數倍,假如訪問N大小的資料,沒有進行記憶體對齊,有可能就需要兩次訪問才可以讀取出資料,而進行記憶體對齊可以一次性把資料全部讀取出來,提高效率。
在c++11之前如果想建立記憶體對齊需要:
void align_cpp11_before() { static char data[sizeof(void *) + sizeof(A)]; const uintptr_t kAlign = sizeof(void *) - 1; char *align_ptr = reinterpret_cast<char *>(reinterpret_cast<uintptr_t>(data + kAlign) & ~kAlign); A *attr = new (align_ptr) A; }
c++11關於記憶體對齊新增了一些函式:
void align_cpp11_after() { static std::aligned_storage<sizeof(A), alignof(A)>::type data; A *attr = new (&data) A; }
還有:alignof()、std::alignment_of()、alignas(),關於記憶體對齊詳情可以看這篇文章:記憶體對齊之格式修訂版
thread_local
c++11引入thread_local,用thread_local修飾的變數具有thread週期,每一個執行緒都擁有並只擁有一個該變數的獨立例項,一般用於需要保證執行緒安全的函式中。
#include <iostream> #include <thread> class A { public: A() {} ~A() {} void test(const std::string &name) { thread_local int count = 0; ++count; std::cout << name << ": " << count << std::endl; } }; void func(const std::string &name) { A a1; a1.test(name); a1.test(name); A a2; a2.test(name); a2.test(name); } int main() { std::thread(func, "thread1").join(); std::thread(func, "thread2").join(); return 0; }
輸出:
thread1: 1 thread1: 2 thread1: 3 thread1: 4 thread2: 1 thread2: 2 thread2: 3 thread2: 4
驗證上述說法,對於一個執行緒私有變數,一個執行緒擁有且只擁有一個該例項,類似於static。
基礎數值型別
c++11新增了幾種資料型別:long long、char16_t、char32_t等
隨機數功能
c++11關於隨機數功能則較之前豐富了很多,典型的可以選擇概率分佈型別,先看如下程式碼:
#include <time.h> #include <iostream> #include <random> using namespace std; int main() { std::default_random_engine random(time(nullptr)); std::uniform_int_distribution<int> int_dis(0, 100); // 整數均勻分佈 std::uniform_real_distribution<float> real_dis(0.0, 1.0); // 浮點數均勻分佈 for (int i = 0; i < 10; ++i) { cout << int_dis(random) << ' '; } cout << endl; for (int i = 0; i < 10; ++i) { cout << real_dis(random) << ' '; } cout << endl; return 0; }
輸出:
38 100 93 7 66 0 68 99 41 7 0.232202 0.617716 0.959241 0.970859 0.230406 0.430682 0.477359 0.971858 0.0171148 0.64863
程式碼中舉例的是整數均勻分佈和浮點數均勻分佈,c++11提供的概率分佈型別還有好多,例如伯努利分佈、正態分佈等,具體可以見最後的參考資料。
正則表示式
c++11引入了regex庫更好的支援正則表示式,見程式碼:
#include <iostream> #include <iterator> #include <regex> #include <string> int main() { std::string s = "I know, I'll use2 regular expressions."; // 忽略大小寫 std::regex self_regex("REGULAR EXPRESSIONS", std::regex_constants::icase); if (std::regex_search(s, self_regex)) { std::cout << "Text contains the phrase 'regular expressions'\n"; } std::regex word_regex("(\\w+)"); // 匹配字母數字等字元 auto words_begin = std::sregex_iterator(s.begin(), s.end(), word_regex); auto words_end = std::sregex_iterator(); std::cout << "Found " << std::distance(words_begin, words_end) << " words\n"; const int N = 6; std::cout << "Words longer than " << N << " characters:\n"; for (std::sregex_iterator i = words_begin; i != words_end; ++i) { std::smatch match = *i; std::string match_str = match.str(); if (match_str.size() > N) { std::cout << " " << match_str << '\n'; } } std::regex long_word_regex("(\\w{7,})"); // 超過7個字元的單詞用[]包圍 std::string new_s = std::regex_replace(s, long_word_regex, "[$&]"); std::cout << new_s << '\n'; }
chrono
c++11關於時間引入了chrono庫,源於boost,功能強大,chrono主要有三個點:
- duration
- time_point
- clocks
duration
std::chrono::duration表示一段時間,常見的單位有s、ms等,示例程式碼:
// 拿休眠一段時間舉例,這裡表示休眠100ms std::this_thread::sleep_for(std::chrono::milliseconds(100));
sleep_for裡面其實就是std::chrono::duration,表示一段時間,實際是這樣:
typedef duration<int64_t, milli> milliseconds;
typedef duration<int64_t> seconds;
duration具體模板如下:
1 template <class Rep, class Period = ratio<1> > class duration;
Rep表示一種數值型別,用來表示Period的數量,比如int、float、double,Period是ratio型別,用來表示【用秒錶示的時間單位】比如second,常用的duration<Rep, Period>已經定義好了,在std::chrono::duration下:
- ratio<3600, 1>:hours
- ratio<60, 1>:minutes
- ratio<1, 1>:seconds
- ratio<1, 1000>:microseconds
- ratio<1, 1000000>:microseconds
- ratio<1, 1000000000>:nanosecons
ratio的具體模板如下:
template <intmax_t N, intmax_t D = 1> class ratio;
N代表分子,D代表分母,所以ratio表示一個分數,我們可以自定義Period,比如ratio<2, 1>表示單位時間是2秒。
time_point
表示一個具體時間點,如2020年5月10日10點10分10秒,拿獲取當前時間舉例:
std::chrono::time_point<std::chrono::high_resolution_clock> Now() { return std::chrono::high_resolution_clock::now(); } // std::chrono::high_resolution_clock為高精度時鐘,下面會提到
clocks
時鐘,chrono裡面提供了三種時鐘:
- steady_clock
- system_clock
- high_resolution_clock
steady_clock
穩定的時間間隔,表示相對時間,相對於系統開機啟動的時間,無論系統時間如何被更改,後一次呼叫now()肯定比前一次呼叫now()的數值大,可用於計時。
system_clock
表示當前的系統時鐘,可以用於獲取當前時間:
int main() { using std::chrono::system_clock; system_clock::time_point today = system_clock::now(); std::time_t tt = system_clock::to_time_t(today); std::cout << "today is: " << ctime(&tt); return 0; } // today is: Sun May 10 09:48:36 2020
high_resolution_clock
high_resolution_clock表示系統可用的最高精度的時鐘,實際上就是system_clock或者steady_clock其中一種的定義,官方沒有說明具體是哪個,不同系統可能不一樣,我之前看gcc chrono原始碼中high_resolution_clock是steady_clock的typedef。
更多關於chrono的介紹可以看下我之前的文章:RAII妙用之計算函式耗時
新增資料結構
- std::forward_list:單向連結串列,只可以前進,在特定場景下使用,相比於std::list節省了記憶體,提高了效能
-
std::forward_list<int> fl = {1, 2, 3, 4, 5}; for (const auto &elem : fl) { cout << elem; }
- std::unordered_map:基於hash表實現的map,內部不會排序,使用方法和set類似
- std::array:陣列,在越界訪問時丟擲異常,建議使用std::array替代普通的陣列
- std::tuple:元組型別,類似pair,但比pair擴充套件性好
typedef std::tuple<int, double, int, double> Mytuple; Mytuple t(0, 1, 2, 3); std::cout << "0 " << std::get<0>(t); std::cout << "1 " << std::get<1>(t); std::cout << "2 " << std::get<2>(t); std::cout << "3 " << std::get<3>(t);
新增演算法
- all_of:檢測表示式是否對範圍[first, last)中所有元素都返回true,如果都滿足,則返回true
std::vector<int> v(10, 2); if (std::all_of(v.cbegin(), v.cend(), [](int i) { return i % 2 == 0; })) { std::cout << "All numbers are even\n"; }
- any_of:檢測表示式是否對範圍[first, last)中至少一個元素返回true,如果滿足,則返回true,否則返回false,用法和上面一樣
- none_of:檢測表示式是否對範圍[first, last)中所有元素都不返回true,如果都不滿足,則返回true,否則返回false,用法和上面一樣
- find_if_not:找到第一個不符合要求的元素迭代器,和find_if相反
- copy_if:複製滿足條件的元素
- itoa:對容器內的元素按序遞增
std::vector<int> l(10); std::iota(l.begin(), l.end(), 19); // 19為初始值 for (auto n : l) std::cout << n << ' '; // 19 20 21 22 23 24 25 26 27 28
- minmax_element:返回容器內最大元素和最小元素位置
int main() { std::vector<int> v = {3, 9, 1, 4, 2, 5, 9}; auto result = std::minmax_element(v.begin(), v.end()); std::cout << "min element at: " << *(result.first) << '\n'; std::cout << "max element at: " << *(result.second) << '\n'; return 0; } // min element at: 1 // max element at: 9
- is_sorted、is_sorted_until:返回容器內元素是否已經排好序。
關於c++11的新特性基本上就是這些,相信各位看完一定會有所收穫。
參考資料
https://zh.cppreference.com/w/cpp/language/range-for
https://juejin.im/post/5dcaa857e51d457f7675360b
https://zhuanlan.zhihu.com/p/21930436
https://zh.wikipedia.org/wiki/Nullptr
https://zh.wikipedia.org/wiki/Constexpr
https://zh.cppreference.com/w/cpp/language/enum
https://kheresy.wordpress.com/2019/03/27/using-enum-class/
https://zh.cppreference.com/w/cpp/language/union
http://c.biancheng.net/view/7165.html
https://zhuanlan.zhihu.com/p/77585472
http://www.cplusplus.com/reference/random/
https://zh.cppreference.com/w/cpp/regex
https://www.cnblogs.com/jwk000/p/3560086.html
https://zh.cppreference.com/w/cpp/algorithm/all_any_none_of
轉自:https://cloud.tencent.com/developer/article/1745592