目錄
1、C++11簡介
2、統一的列表初始化
2.1、{}初始化
2.2、std::initializer_list
3、變量類型推導
3.1、auto
3.2、decltype
3.3、nullptr
4、范圍for循環
5、STL中一些變化
6、右值引用和移動語義
6.1、左值引用和右值引用
6.2、右值引用使用場景和意義
6.3、完美轉發
1、C++11簡介
在2003年C++標準委員會曾經提交了一份技術勘誤表(簡稱TC1),使得C++03這個名字已經取代了C++98稱為C++11之前的最新C++標準名稱。
不過由于C++03(TC1)主要是對C++98標準中的漏洞進行修復,語言的核心部分則沒有改動,因此人們習慣性的把兩個標準合并稱為C++98 / 03標準。
從C++0x到C++11,C++標準10年磨一劍,第二個真正意義上的標準珊珊來遲。
相比于C++98 / 03,C++11則帶來了數量可觀的變化,其中包含了約140個新特性,以及對C++03標準中約600個缺陷的修正,這使得C++11更像是從C++98 / 03中孕育出的一種新語言。
相比較而言,C++11能更好地用于系統開發和庫開發、語法更加泛華和簡單化、更加穩定和安全,不僅功能更強大,而且能提升程序員的開發效率,公司實際項目開發中也用得比較多,所以要作為一個重點去學習。
C++11引入的新特性
關于C++2X最新特性的討論
查看C++各種庫說明的一個好用的網站
C++官網
小故事:
1998年是C++標準委員會成立的第一年,本來計劃以后每5年視實際需要更新一次標準,C++國際標準委員會在研究C++ 03的下一個版本的時候,一開始計劃是2007年發布,所以最初這個標準叫C++ 07。但是到06年的時候,官方覺得2007年肯定完不成C++ 07,而且官方覺得2008年可能也完不成。最后干脆叫C++ 0x。x的意思是不知道到底能在07還是08還是09年完成。結果2010年的時候也沒完成,最后在2011年終于完成了C++標準。所以最終定名為C++11。C++官網
2、統一的列表初始化
想達到的目的就是:一切都可以用列表{}初始化。
注意:列表初始化跟初始化列表(這是類的構造函數里的東西)不是一個概念。
2.1、{}初始化
在C++98中,標準允許使用花括號{}對數組或者結構體元素進行統一的列表初始值設定。
C++11擴大了用大括號括起的列表(列表初始化)的使用范圍,使其可用于所有的內置類型和用戶自定義的類型,使用列表初始化時,可添加等號(=),也可不添加。
創建對象時也可以使用列表初始化方式調用構造函數初始化。
栗子:
struct Point
{int _x;int _y;
};
class A
{
public:// explicit A(int x, int y) // explicit關鍵字,加上之后就不允許隱式類型的轉換了A(int x, int y): _x(x), _y(y){}A(int x): _x(x), _y(x){}private:int _x;int _y;
};
void test1()
{// C語言帶過來的int array1[] = {1, 2, 3, 4, 5};int array2[5] = {0};Point p = {1, 2}; // 結構體的初始化列表// C++11有的,一切都可用列表初始化int array3[5]{1};int i{1};// 單參數的隱式類型轉換A aa1 = 1;A aa2 = {1};A aa3{1};// 多參數的隱式類型轉換A aa4 = {1, 2};A aa5{3, 4};const A& aa6 = { 7,7 }; // 不加 const 是不行的,因為這中間有臨時變量,臨時變量具有常性。 // 注:這句代碼在 C++11 之后才支持
}
2.2、std::initializer_list
std::initializer_list的介紹文檔
栗子:
void test2()
{// the type of il is an initializer_listauto il = {10, 20, 30};initializer_list<int> il2 = {10, 20, 30};cout << typeid(il).name() << endl;cout << sizeof(il2) << endl; // 8/16// std::initializer_list 雖然是一個容器,但是它本身并沒有去新開空間,本質就是兩個指針,一個begin指向常量數組的開頭,一個end指向常量數組的結尾vector<int> v1;vector<int> v2(10, 1);// 隱式類型轉換vector<int> v3 = {1, 2, 3, 4, 5};vector<int> v4{10, 20, 30};// 構造vector<int> v5({10, 20, 30});// 補充:X自定義 = Y類型 --> 想要達成隱式類型轉換,則 自定義X 必須支持 Y 為參數類型的構造// 1、pair多參數隱式類型轉換// 2、initializer_list<pair>的構造map<string, string> dict = {{"sort", "排序"}, {"insert", "插入"}}; // 在沒有支持initializer_list之前,這一句代碼可是要分成三句寫的,有了initializer_list之后,就方便了很多
}
總結:當容器想用不固定的數據個數初始化時,initializer_list就派上用場了。
注:所有的容器都支持 initializer_list。
3、變量類型推導
c++11提供了多種簡化聲明的方式,尤其是在使用模板時。
3.1、auto
在C++98中auto是一個存儲類型的說明符,表明變量是局部自動存儲類型,但是局部域中定義局部的變量默認就是自動存儲類型,所以auto就沒什么價值了。
C++11中廢棄auto原來的用法,將其用于實現自動類型推斷。
這樣要求必須進行顯示初始化,讓編譯器將定義對象的類型設置為初始化值的類型。
栗子:
void test3()
{int i = 0;auto &x = i; // auto可以加引用&++x; // x的修改會影響iint &j = i;auto y = j; // 這里的 y 是單純的 int 還是 int& 呢?// 答:從語法層面上講 y 就是 int,也可以打開監視窗口,&y,&j,&i 看看,會發現y和j、i的地址不一樣,j、i的地址是一樣的// j 雖然是 i 的別名(int&),但 j 的本質也是int。// 所以:這里就是一個很普通的拷貝,實際上這句代碼就等價于 auto y = i;++y;pair<string, string> kv = {"sort", "排序"};// auto [x, y] = kv; // 這是C++17支持的一種寫法(當前編譯器默認支持到C++14)
}
3.2、decltype
關鍵字decltype將變量的類型聲明為表達式指定的類型。
栗子:
template <class T>
class B
{
public:T *New(int n){return new T[n];}
};
auto func2()
{list<int> it;auto ret = it.begin();return ret;
}
auto func1()
{auto ret = func2();return ret;
}
void test4()
{list<int>::iterator it1;cout << typeid(it1).name() << endl; // typeid 是能直接拿到這個變量類型的最原始的名字(字符串)(你所看到的類型名可能是typedef過的)// typeid(it1).name() it2; // typeid 推出的只是一個單純的字符串,不能用來定義一個新的對象// decltype 可以幫你推斷出()內的變量的類型,并且你可以直接使用 decltype 推斷出的結果(類型)來定義新的變量。decltype(it1) it2; // it2和it1的類型是一樣的cout << typeid(it2).name() << endl;// 光從上面這幾句代碼,體現不出decltype的作用,因為auto也有這樣的功能,而且寫起來還更方便auto it3 = it1;cout << typeid(it3).name() << endl;auto ret = func1();// 此時如果你想要用ret的類型去實例化出一個B類型的對象,該怎么辦?(假設這里的func不只套了這么幾層,套了好多層,那你想要知道 ret 到底是什么類型,就會很麻煩)// 那么 decltype 此時就派上用場了,decltype 推斷出的結果(類型)可以用來做模板的傳參B<decltype(ret)> bb1;map<string, string> dict = { {"insert","插入"}, {"erase","刪除"} };auto it4 = dict.begin();B<decltype(it4)> bb2;B<map<string, string>::iterator> bb3; // 這句代碼可讀性更好
}
注:decltype 感覺就是跟 auto 配套使用的,用來解決一些auto搞出的問題
總結:auto 和 decltype 還是要慎用,雖然兩者都可以幫助你縮短代碼量,但是很影響可讀性。
3.3、nullptr
注:也是為了解決一些歷史遺留的問題而造出的東西。
由于C++中NULL被定義成字面量0,這樣就可能回帶來一些問題,因為0既能表示指針常量,又能表示整形常量。
所以出于清晰和安全的角度考慮,C++11中新增了nullptr,用于表示空指針。
#ifndef NULL
#ifdef __cplusplus
#define NULL ? 0
#else
#define NULL ? ((void *)0)
#endif
#endif
4、范圍for循環
栗子:
void test5()
{map<string, string> m = {{"sort", "排序"}, {"insert", "插入"}};for (auto &[x, y] : m) // 這里的auto最好加上引用&和const(無需修改就加上const),否則會有深拷貝的問題(如果有大量的string要拷貝的話,會影響程序的效率){cout << x << ":" << y << endl;// x += "1"; // map里的key是不能被修改的y += "1";}
}
5、STL中一些變化
總結:
- 增加的4個新容器中,也就 unordered_map、unordered_set 有點用,另外兩個(array、forward_list)沒什么屁用。
- 給所有容器添加了 initializer_list 構造。
- 給所有容器添加了 移動賦值和移動構造。
- 給所有容器添加了 emplace系列(與右值引用和模板的可變參數有關)。
6、右值引用和移動語義
6.1、左值引用和右值引用
傳統的C++語法中就有引用的語法,而C++11中新增了的右值引用語法特性,之前學習的引用就叫做左值引用。
無論左值引用還是右值引用,都是給對象取別名。
那么什么是左值?什么是左值引用?
左值是一個表示數據的表達式(如變量名或解引用的指針),我們可以獲取它的地址,左值可以出現在賦值符號的左邊/右邊。
而右值則不能出現在賦值符號的左邊。
左值引用就是給左值的引用,給左值取別名。
那么什么是右值?什么是右值引用?
右值也是一個表示數據的表達式,如:字面常量、表達式返回值,函數返回值(這個不能是左值引用返回)等等。
右值可以出現在賦值符號的右邊,但是不能出現出現在賦值符號的左邊,右值不能取地址。
右值引用就是對右值的引用,給右值取別名。
總之,簡單說可以取地址的就是左值,不能取地址的就是右值。
注:不能說可以修改的就是左值,不能修改的就是右值。
栗子:
const int val = 7; // 這里的 val 還是左值void test6()
{// 左值:可以取地址的int a = 10;int b = a;const int c = 10;int *p = &a;vector<int> v(10, 1);(void)v[1]; // 強轉成 void 取消警告cout << &a << endl;cout << &b << endl;cout << &c << endl;cout << &(*p) << endl;cout << &(v[1]) << endl;// 右值:不可以取地址的// 10、string("111")、to_string(123)、x+y// cout << &10 << endl; // 字面常量// cout << &string("111") << endl; // 匿名對象// cout << &to_string(123) << endl; // 該函數的返回值返回的是一個臨時拷貝、臨時對象int x = 1, y = 2;// cout << &(x + y) << endl; // 表達式返回值// 幾個右值引用的栗子// 為了方便理解,補充兩個概念:純右值(內置類型) 將亡值(自定義類型)int &&rref1 = (x + y); // 純右值(內置類型)string &&rref2 = string("111"); // 將亡值(自定義類型) // 生命周期就這一行string &&rref3 = to_string(123); // 將亡值(自定義類型) // 生命周期就這一行int &&rref4 = 10; // 純右值(內置類型)// 左值引用能否給右值取別名?// 答:不可以,但是 const 左值引用可以const string &ref1 = string("111");const int &ref2 = 10;// 右值引用能否給左值取別名?// 答:不可以,但是可以給 move 以后的左值取別名string s1("222");// string&& rref5 = s1;string &&rref6 = move(s1);// 補充:當需要用右值引用引用一個左值時,可以通過move函數將左值轉化為右值。// C++11中,std::move()函數位于 頭文件中,該函數名字具有迷惑性,它并不搬移任何東西,// 唯一的功能就是將一個左值強制轉化為右值引用,然后實現移動語義。// 下面的s2是左值還是右值呢?string &&s2 = string("111");// 要驗證s2是左值還是右值實際上很簡單,只需要看一下能不能取的出s2的地址就好了。cout << &s2 << endl;// 總結:右值引用本身的屬性是左值!!!// 需要注意的是右值是不能取地址的,但是給右值取別名后,會導致右值被存儲到特定位置,且可以取到該位置的地址。// 下面的移動構造和移動賦值也說明了右值引用的屬性是左值。// 因為只有右值引用本身被處理成了左值,才能實現移動構造和移動賦值,才能轉移資源。-- 因為中間要 swap// 通過反匯編也是能看到右值是有地址的,只是語法上規定不能取。(沒有地址的話,右值存在哪里呢?右值也得有個地方給它存吧)// 右值引用本身是左值的意義是:為了 移動構造和移動賦值 中要 轉移資源 的語法邏輯能夠邏輯自洽。// 如果右值引用的屬性是右值,那么移動構造和移動賦值中要轉移資源的語法邏輯就是矛盾的了。// 右值是不能被改變的。(可以理解為右值帶有const屬性)// 補充:即使沒有右值引用,實際上也是有辦法能夠改變右值的// ex:string& s = (string&)string("kk"); // 強轉可以讓普通的左值引用直接引用右值// 因為不管是什么值,終歸要有空間去存儲它,有空間,不就能改了嘛。
}
6.2、右值引用使用場景和意義
前面我們可以看到左值引用既可以引用左值和又可以引用右值。
那為什么C++11還要提出右值引用呢?是不是化蛇添足呢?
下面我們來看看左值引用的短板,右值引用是如何補齊這個短板的!
首先,引用的意義是什么?
答:引用的意義就是為了減少拷貝,提高效率。
比如:
左值引用解決的場景
void function1(const string &s); // 減少傳參時的拷貝的消耗string &function2(); ? ? ? ? ? ? // 傳引用返回,減少拷貝的消耗
但是左值引用沒有徹底解決返回值的拷貝消耗問題,因為不是什么情況下都能傳引用返回的。
比如當返回值是function2中的局部對象,就不能用引用返回。
舉個栗子:
namespace kk
{class string{public:typedef char *iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}string(const char *str = ""): _size(strlen(str)), _capacity(_size){cout << "string(char* str) -- 構造" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}// s1.swap(s2)void swap(kk::string &s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}// 拷貝構造(左值引用)string(const kk::string &s) // 左值的拷貝構造會去調用這個: _str(nullptr){cout << "string(const string& s) -- 深拷貝" << endl;_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}// 移動構造(右值引用) -- 移動將亡值的資源string(kk::string &&s) // 右值的移動(拷貝)構造會去調用這個: _str(nullptr){cout << "string(string&& s) -- 移動構造" << endl;swap(s);}// 賦值重載kk::string &operator=(const kk::string &s){cout << "string& operator=(string s) -- 深拷貝" << endl;char *tmp = new char[s._capacity + 1];strcpy(tmp, s._str);delete[] _str;_str = tmp;_size = s._size;_capacity = s._capacity;return *this;}// 移動賦值kk::string &operator=(kk::string &&s){cout << "operator=(string&& s) -- 移動賦值" << endl;swap(s);return *this;}~string(){delete[] _str;_str = nullptr;}char &operator[](size_t pos){assert(pos < _size);return _str[pos];}void reserve(size_t n){if (n > _capacity){char *tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void push_back(char ch){if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}// string operator+=(char ch)kk::string &operator+=(char ch){push_back(ch);return *this;}const char *c_str() const{return _str;}private:char *_str = nullptr;size_t _size = 0;size_t _capacity = 0; // 不包含最后做標識的\0};kk::string to_string(int value){bool flag = true;if (value < 0){flag = false;value = 0 - value;}kk::string str;while (value > 0){int x = value % 10;value /= 10;str += ('0' + x);}if (flag == false){str += '-';}std::reverse(str.begin(), str.end());return str; // 雖然這里的str是左值,但是你加不加move,都是可以的,因為編譯器已經幫你處理好了(編譯器會強行認為這是右值),它都會去調用移動構造/移動賦值。// 注意:右值引用/這里的移動構造 并不會延長對象的生命周期,str的生命周期到了,該銷毀還是銷毀,只是發生了資源轉移。(嚴格來說,是延長了資源的生命周期)}
}
總結:右值引用徹底拯救了傳值返回所引發的效率低下的問題。一般的傳值返回需要進行 一次拷貝構造 + 一次賦值/拷貝構造 -- 拷貝構造和普通的賦值的成本很大。有了右值引用之后,傳值返回變成了 一次移動構造 + 一次移動賦值/移動構造 -- 移動構造和移動賦值只是對資源的轉移,成本很低。
舉個栗子:
vector<vector<int>> func(int rows); // 該函數 在C++11之前(右值引用出來之前) 不能這樣寫,因為效率十分的低下!!!void func(int rows, vector<vector<int>>& vv); // 該函數 在C++11之前(右值引用出來之前) 只能這樣寫,才能保證效率。
6.3、完美轉發
直接上代碼:
void Fun(int &x) { cout << "左值引用" << endl; }
void Fun(const int &x) { cout << "const 左值引用" << endl; }
void Fun(int &&x) { cout << "右值引用" << endl; }
void Fun(const int &&x) { cout << "const 右值引用" << endl; }
// std::forward 完美轉發在傳參的過程中保留對象原生類型屬性
// std::forward<T>(t)在傳參的過程中保持了t的原生類型屬性。
// 在函數模板里面,這里可以稱之為萬能引用(引用折疊)
template <typename T>
void PerfectForward(T &&t) // 注意:模板中的&&不代表右值引用,而是萬能引用,其既能接收左值又能接收右值。// 這里的 T&& 這一坨都是模板,而不只是那個T。// 這里的t的屬性根據你傳過來的參數推導。// 你傳的是左值,t就是左值,你傳的右值,t就是右值。// 你傳的 const 左值,t就是 const 左值。// 你傳的 const 右值,t就是 const 右值。
{Fun(t); // 如果僅僅只是這樣寫傳參的話,最后走的全是 左值引用 和 const 左值引用。-- 因為右值引用的屬性還是左值Fun(std::forward<T>(t)); // 要這樣寫,才能保證t的屬性不變,不會發生從右值退化成左值(右值引用)的情況。-- 注意不能用 move,因為這里是模板,你不知道傳過來的值本身是左值還是右值引用(右值退化成左值)?用了 move,就變成了全部都是調用 右值引用 和 const右值引用Fun((T &&)t); // 這樣寫的效果跟完美轉發一樣。 // 但要注意,這不是官方的寫法,不太清楚這種寫法有沒有什么缺陷。
}
void test7()
{PerfectForward(10); // 右值int a;PerfectForward(a); // 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b); // const 左值PerfectForward(std::move(b)); // const 右值
}
總結:完美轉發的意義就是在一個既能接收左值又能接收右值的函數模板中,當需要保持一個右值引用的屬性保持不變,不想其退化成左值的時候,就用完美轉發。