C++11 新功能
文章目錄
- C++11
- 一 列表初始化
- 單個對象的多參數列表初始化
- 多個對象的列表初始化
- 自己編寫list使用initializer_list支持多對象的列表初始化
- 二 stl中的一些變化
- array
- forword_list
- unordered_map unordered_set
- 三 右值引用和移動語義
- 左值引用& vs 右值引用&&
- 右值引用的應用
- 移動構造
- 移動賦值
- 完美轉發
- 使用完美轉發之后:
- emplace_back()
- 類的新功能
- 類成員初始化
- 四 可調用對象類型
- lambda表達式
- lambda表達式各部分說明
- 捕捉聲明:
- 注意事項:
- 函數對象(仿函數)與lambda表達式
- 五 關鍵字
- auto
- decltype
- final-不讓繼承
- override-檢查能否被重寫
- default
- delete
- 六 模板的可變參數
- 逗號表達式展開參數包
C++11
在2003年C++標準委員會曾經提交了一份技術勘誤表(簡稱TC1),使得C++03這個名字已經取代了C++98稱為
C++11之前的最新C++標準名稱。不過由于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++98中,標準允許使用花括號{}對數組元素進行統一的列表初始值設定。比如:
int array1[] = {1,2,3,4,5};int array2[5] = {0};
對于一些自定義的類型,卻無法使用這樣的初始化。比如:
vector<int> v{1,2,3,4,5};就無法通過編譯,導致每次定義vector時,都需要先把vector定義出來,然后使用循環對其賦初始值,非常不方便。C++11擴大了用大括號括起的列表(初始化列表)的使用范圍,使其可用于所有的內置類型和用戶自定義的類型,使用初始化列表時,可添加等號(=)也可不添加。列表初始化可以在{}之前使用等號,其效果與不使用=沒有什么區別。
單個對象的多參數列表初始化
#include<iostream> #include<vector> #include<map> #include<string> using namespace std; struct B {int _x;int _y; }; class Point { public:Point(int x = 0, int y = 0):_x(x), _y(y){} private:int _x;int _y; }; class A { public://內含單參數的構造函數A(int a):_a(a){}/*explicit A(int a):_a(a){}*/ private:int _a; }; int main() {//自定義類型// 當添加explicit之后就不讓轉換了//單參數構造函數,支持隱士類型轉換A aa(1);A aa2 = 2;//隱士類型轉換int->A//同理string s1("hello");string s2="world";//const char* ->stringvector<string>v;v.push_back("world");//C++11-多參數,支持隱士類型轉換Point p{ 1,2 };B b{1,2};//兼容C語言int* ptr1 = new int[5] {1, 2, 3, 4, 5};Point* ptr2 = new Point[2]{ {1,2},{3,4} };//內置類型int x1 = { 10 };int x2{ 10 };int x3{ 1 + 2 };int x4 = { 1 + 2 };int arr1[5]{ 1,2,3,4,5 };int arr2[]{ 1,2,3,4,5 };//動態數組C++98中不支持int* arr3 = new int[5] {1, 2, 3, 4, 5};vector<int>v{ 1,2,3,4,5 };map<int, int> m{ {1,1},{2,2},{3,3} };return 0; }多個對象的列表初始化
多個對象想要支持列表初始化,需給該類(模板類)添加一個帶有initializer_list類型參數的構造函數即可,常見的類比如vector,list,map,set在C++11中都支持initializer_list類型參數的構造函數。注意:initializer_list是系統自定義的類模板,該類模板中主要有三個方法:begin()、end()迭代器以及獲取區間中元素個數的方法size()。
編譯器自己識別{}為initializer_list類型進行轉化。
比如vector:
#include <initializer_list> template<class T> class Vector { public:// ... Vector(initializer_list<T> l) : _capacity(l.size()), _size(0){_array = new T[_capacity];for (auto e : l)_array[_size++] = e;}Vector<T>& operator=(initializer_list<T> l) {delete[] _array;size_t i = 0;for (auto e : l)_array[i++] = e;return *this;}// ... private:T* _array;size_t _capacity;size_t _size; }; int main() {//自己實現的Vector<int> vv = { 1,2,3,4,5 };Vector<int> vv2 = vv;//多個對象支持列表初始化vector<int>v1 = {1,2,3,4,5};list<int> l1 = {1,2,3,4,5};pair<string, string>kv("left","左邊");map<string, string>dict = { {"insert","插入"},kv};initializer_list<int> ilt = {1,2,32,4,5}; }自己編寫list使用initializer_list支持多對象的列表初始化
注意:如果使用迭代器時報錯,前面加上typename initializer_list<T>:;iterator ,這是在類模板中再去找他的內嵌類型,未實例化之前可能取不到。告訴編譯器類模板實例化了之后再去調用類里面的迭代器。
二 stl中的一些變化
- 新增一些容器
- 已有容器增加一些好用或者提高效率的接口。比如列表初始化initializer_list,右值引用相關接口提高效率。移動構造,移動賦值。cbegin(),cend(),emplace_back(),emplace().
array
定長數組,相比于變長數組vector。
優點:支持迭代器,更好的兼容STL容器。對于越界的檢查。
std::array template < class T, size_t N > class array;int main() {array<int, 10>a;int a1[10];//數組a1[14] = 0;//抽查行為,*(a+14)=0,越界可能不檢查a[14] = 0;//a.operator[](14)=0,對于函數的調用,肯定檢查return 0; }forword_list
forward_list,單鏈表,支持頭插頭刪(push_front() pop_front()),支持在節點后面插入刪除,不支持尾插尾刪和在節點之前的操作
unordered_map unordered_set
三 右值引用和移動語義
C++98中提出了引用的概念,引用即別名,引用變量與其引用實體公共同一塊內存空間,而引用的底層是通過指針來實現的,因此使用引用,可以提高程序的可讀性。
左值與右值是C語言中的概念,但C標準并沒有給出嚴格的區分方式,一般認為:
-
可以放在=左邊的,變量或者解引用的指針,我們可以獲取他的地址+可以對他賦值,稱為左值。const修飾的左值可以取地址,但是不可以賦值。
-
只能放在=右邊的,或者不能取地址的稱為右值。表示數據的表達式如:字面常量,表達式返回值,傳值返回函數的返回值,臨時對象也是右值。
左值引用& vs 右值引用&&
const int b=10;//常量,函數返回值等不能取地址的都是右值 int main() { //可以取地址對象就是左值const int b = 10;//b是左值,可以取地址不能賦值const int& r3=b; //左值引用 //右值double x = 1.1, y = 2.2;x + y;double &&r4=fmin(x,y);//右值引用就是右值的別名int&& rr1 = 10;//左值引用不能直接引用右值,得加上constconst int& r1 = x + y;const int& r2 = 10;const int& r3 = fmin(x,y);const int& p1 = (10+20);//void push_back(const T& x)這樣傳參,對面左值引用和右值引用均可傳過去//右值引用不能直接引用左值,但是可以右值引用move以后的左值int d = 10;int* p = &d;int*&& rr1 = move(p);int n = 10;int&& p2 = move(n);const int p = 20;const int&& rr2 = move(p2); //給右值取別名之后會改變存儲位置,另開辟個空間存儲右值,別名就是個左值了cout<<&p2<<endl;return 0; }右值引用的應用
- 為了彌補左值引用的不足。
左值引用:傳值傳參會調用拷貝構造函數,作為參數基本完美解決問題;作為返回值以下問題就不完美只能解決部分問題,所以可用右值引用優化。
string& operator+=(char ch) {push_back(ch);return *this;//處理作用域還在,就很完美 } string operator+(char ch) {string tmp(*this);push_back(ch);return tmp;//出了作用域就會被銷毀,傳值返回會多一次拷貝構造,然后再析構,不完美。//所以只能用傳值返回是右值 }- 右值引用如何解決operator+的拷貝構造問題呢?
提供一個移動構造,是右值只會去移動構造中,走最匹配的那個函數。
注意:C++11中將右值分為:自定義類型叫將亡值,或者純右值。
移動構造
//拷貝構造 string(const string &s)//左值:_str(nullptr),_size(0),_capacity(0) {string tmp(s._str);swap(tmp); }//移動構造,一種資源轉移,避免了資源的些許浪費,少一層拷貝 string(string &&s)//右值(臨時對象也是一種右值):str(nullptr) {this->swap(s);return *this; }int main() {string s("hello world");string s1 = s;string s2 = move(s);//將s左值的屬性修改為右值屬性,賦予了別人將自己的資源拿走的權利//單純的move并不會對于s造成影響,當傳給別人時就會有影響。return 0; } //右值引用理解場景2 string to_string(int value) {string str;while(value){int val=value%10;str+=('0'+val);value/=10;}reverse(str.begin(),str.end());return str; } int main() {string ret=to_string(1234); }? 首先,如果編譯器不優化,str拷貝構造臨時對象,臨時對象(在main函數的棧幀中)作為to_string的返回值構造ret。優化之后,在to_string結束之前,用str構造ret,從兩次拷貝構造優化為只有一次拷貝構造。
-
什么情況可優化?
當用臨時對象去構造ret時,也就是有ret接收時會進行優化。當沒有ret時,因為str在to_string函數結束之后要被銷毀,必須得有一個臨時對象作為返回值返回,還沒人接收時就無法優化。
? 如果有了移動構造之后,
-
如果不考慮優化的存在,原來的兩次拷貝構造變成一次拷貝構造,一次移動構造,因為str是左值調用一次拷貝構造產生臨時變量,此時產生的拷貝的臨時對象被認為是右值,調用移動構造。
-
優化之后,就變成了一次移動構造,魯莽地直接將str認為是右值,
-
如果有ret接收,直接移動構造交給ret,完成一次資源轉移。
-
如果沒有ret接收,之前直接將ret視為右值的存在在to_string()函數銷毀時作為函數返回值返回即可。
-
-
資源轉移:有ret接收,就資源轉移給ret,如果沒ret接收,就資源轉移給一個函數必有的函數返回值。
- 移動構造提升效率一個例子:
使得C++11 效率更高,當然,如果vv是靜態或者是全局的,出了作用域還在,直接用左值返回就OK了。
移動賦值
//移動賦值 string& operator=(string&& s) {cout<<"轉移資源"<<endl;swap(s);return *this; } //正常深拷貝賦值 string & operator=(const string &s) {cout<<"深拷貝"<<endl;string tmp(s);swap(tmp);return *this; } int main() {yuanwei::string s1("hello");yuanwei::string s2("world");s2=move(s1);//此時是右值,走匹配的右值引用,走移動賦值函數實現資源轉移//避免移動構造,先有一個對象string ret;ret=to_string(12345); }如果有移動賦值時,將str作為右值直接進行移動構造,資源轉移給臨時對象,再用臨時對象移動賦值給ret,兩次資源轉移。將臨時對象這個已經存在的對象交給ret這個已經存在的對象。
如果沒有移動賦值函數,to_string()的str被識別為右值移動構造資源轉移給臨時對象,臨時對象賦值走一遍深拷貝交給已經存在的ret。
- 容器的插入接口也提供了一個右值引用的版本
完美轉發
模板中的&&不代表右值引用,而是萬能引用,其既能接收左值又能接收右值。
模板的萬能引用只是提供了能夠接收同時接收左值引用和右值引用的能力,
但是引用類型的唯一作用就是限制了接收的類型,后續使用中都退化成了左值,
我們希望能夠在傳遞過程中保持它的左值或者右值的屬性, 就需要用我們下面學習的完美轉發
- 退化的例子,如圖所示:
使用完美轉發之后:
- 只要右值引用,再傳遞其他函數調用,要保持右值屬性,必須用完美轉發,然后走的yuanwei::string的移動賦值,資源轉移。
emplace_back()
push_back VS emplace_back
//二者相比,右值版本不會更加高效,差不多 //左值版本 emplace 會更高效,因為他不存在深拷貝的問題 int main() {std::list<pair<int, char>>mylist;//兩次資源轉移,先構造右值,再移動構造mylist.push_back(make_pair(1,'A'));//一個類型參數mylist.push_back({1,'a'});//pair支持{}初始化//兩次直接構造mylist.emplace_back(make_pair(1,'a'));//整體作為單參數對象mylist.emplace_back(1,'a');//多參數傳值也支持return 0; }emplace_back()是直接構造,pushback()是先構造對象,然后再進行資源轉移.
類的新功能
-
原來的默認構造函數有6個,重要的有4個:析構函數,構造函數,拷貝構造函數,拷貝賦值函數.
- 默認生成的構造函數不寫的話,會自動生成并在初始化列表階段調用自定義成員的構造函數.
-
新的是移動構造函數,移動賦值函數。這倆的使用規則是相同的,下面只介紹一個:
如果在類中,你沒有實現移動構造函數,并且你沒有實現你的析構函數,拷貝構造函數,拷貝賦值函數其中一個,那么會給你生成默認移動構造函數;他對于內置類型是采用值拷貝的方式,對于自定義類型,當自定義類型本身是提供移動構造函數的話,就調用。如果沒有,就去調用拷貝構造函數。
同理移動賦值函數。
類成員初始化
類內聲明內置類型的時候給個缺省值
四 可調用對象類型
- C-函數指針void(*p)();
- C++98-仿函數/函數對象
- C++11-lambda表達式/匿名函數
lambda表達式
lambda表達式書寫格式:[capture-list] (parameters) mutable -> return-type { statement }
lambda表達式各部分說明
[capture-list] : 捕捉列表,該列表總是出現在lambda函數的開始位置,編譯器根據[]來判斷接下來的代碼是否為lambda函數**,**捕捉列表能夠捕捉上下文中的變量供lambda函數使用。所以不能省略。
(parameters):參數列表。與普通函數的參數列表一致,如果不需要參數傳遞,則可以連同()一起省略
mutable:默認情況下,lambda函數總是一個const函數,mutable可以取消其常量性。使用該修飾符時,參數列表不可省略(即使參數為空)。
->returntype:返回值類型。用追蹤返回類型形式聲明函數的返回值類型,沒有返回值時此部分可省略。返回值類型明確情況下,也可省略,由編譯器對返回類型進行推導。
{statement}:函數體。在該函數體內,除了可以使用其參數外,還可以使用所有捕獲到的變量。
注意: 在lambda函數定義中,參數列表和返回值類型都是可選部分,而捕捉列表和函數體可以為空。因此C++11中最簡單的lambda函數為:[]{}; 該lambda函數不能做任何事情。
int main() {//lambda實現兩個數相加的功能auto add1=[](int a,int b) ->int {return a + b; };//調用方式cout<<add1(1,2)<<endl;auto add3=add1;decltype(add1) add3=add1; //返回值可以省略auto add2 = [](int a,int b){return a + b; }; //沒有參數可以省略參數列表沒有返回值auto func1 = [] { cout << "hello" << endl; }; //捕捉列表不能省略[]auto swap1 = [](int& a,int& b){int c = a;a = b;b = c; };swap1(3,4);int a = 3, b = 4; auto fun1 = [&](int c) {b = a + c; };//auto fun1 = [&a,&b](int c) {b = a + c; };//傳引用捕捉才能改變值fun1(10);cout << a << " " << b << endl;int a = 0;int b = 1; //傳值捕捉,調用之后并不能改變ab的值,只是一份拷貝交換了.//加上mutable lambda函數總是一個const函數,mutable可以取消其常量性。使用該修飾符,參數列表不能省略。//但是還是不能交換成功auto swap2 = [a, b]()mutable {int c = a;a = b;b = c;};swap2();//捕捉就不需要傳參數了//傳引用捕捉auto swap3 = [&a, &b]() {int c = a; a = b;b = c;};swap3(); }捕捉聲明:
捕捉列表描述了上下文中那些數據可以被lambda使用,以及使用的方式傳值還是傳引用。
[var]:表示值傳遞方式捕捉變量var
[=]:表示值傳遞方式捕獲所有父作用域中的變量(包括this),如果指定某一個變量進行值傳遞,[a,b]指明出來
[&var]:表示引用傳遞指定的捕捉變量var
[&]:表示引用傳遞捕捉所有父作用域中的變量(包括this)
[this]:表示值傳遞方式捕捉當前的this指針
注意事項:
a. 父作用域指包含lambda函數的語句塊
b. 語法上捕捉列表可由多個捕捉項組成,并以逗號分割。比如:[=, &a, &b]:以引用傳遞的方式捕捉變量a和b,值傳遞方式捕捉其他所有變量 [&,a, this]:值傳遞方式捕捉變量a和this,引用方式捕捉其他變量
c. 捕捉列表不允許變量重復傳遞,否則就會導致編譯錯誤。 比如:[=, a]:=已經以值傳遞方式捕捉了所有變量,捕捉a重復。
d. 在塊作用域以外的lambda函數捕捉列表必須為空。在全局中無法捕捉變量。
e. 在塊作用域中的lambda函數僅能捕捉父作用域中局部變量,捕捉任何非此作用域或者非局部變量都會導致編譯報錯。
f. lambda表達式之間不能相互賦值,即使看起來類型相同
void (*PF)(); int main() {auto f1 = [] {cout << "hello world" << endl; };auto f2 = [] {cout << "hello world" << endl; };//f1 = f2; // 編譯失敗--->提示找不到operator=()// 允許使用一個lambda表達式拷貝構造一個新的副本auto f3(f2);f3();// 可以將lambda表達式賦值給相同類型的函數指針PF = f2;PF();return 0; }函數對象(仿函數)與lambda表達式
函數對象,又稱為仿函數,即可以像函數一樣使用的對象,就是在類中重載了operator()運算符的類對象。
lambda表達式,底層原理其實是被處理成一個lambda_uuid的一個仿函數類。
class Rate { public:Rate(double rate) : _rate(rate){}double operator()(double money, int year){return money * _rate * year;} private:double _rate; }; int main() {// 函數對象double rate = 0.49;Rate r1(rate);r1(10000, 2);// lamberauto r2 = [=](double monty, int year)->double {return monty * rate * year; };r2(10000, 2);//cout << typeid(r2).name() <<endl;return 0; }從使用方式上來看,函數對象與lambda表達式完全一樣。函數對象將rate作為其成員變量,在定義對象時給出初始值即可,lambda表達式通過捕獲列表可以直接將該變量捕獲到。
實際在底層編譯器對于lambda表達式的處理方式,完全就是按照函數對象的方式處理的,即:如果定義了一個lambda表達式,編譯器會**自動生成一個類,在該類中重載了operator()。**仿函數對象去調用operator()。
五 關鍵字
auto
在定義變量時,必須先給出變量的實際類型,編譯器才允許定義,但有些情況下可能不知道需要實際類型怎
么給,或者類型寫起來特別復雜。C++11中,可以使用auto來根據變量初始化表達式類型推導變量的實際類型,可以給程序的書寫提供許多方便。將程序中c與it的類型換成auto,程序可以通過編譯,而且更加簡潔
decltype
auto使用的前提是:必須要對auto聲明的類型進行初始化,否則編譯器無法推導出auto的實際類型。但有時候可能需要根據表達式運行完成之后結果的類型進行推導,因為編譯期間,代碼不會運行,此時auto也就無能為力。如果能用加完之后結果的實際類型作為函數的返回值類型就不會出錯,但這需要程序運行完才能知道結果的實際類型,即RTTI(Run-Time Type Identifification 運行時類型識別)。
C++98中確實已經支持RTTI:typeid只能查看類型不能用其結果類定義類型。dynamic_cast只能應用于含有虛函數的繼承體系中運行時類型識別的缺陷是降低程序運行的效率。
int func(int a) {return a; } int main() {int a = 10;int b = 20;// 用decltype推演a+b的實際類型,作為定義c的類型decltype(a + b) c;//C++98 const int ->int 會存在區別,decltype就不會存在cout << typeid(c).name() << endl;//聲明函數指針類型int(*pfunc1)(int) = func;auto pfunc2 = func;decltype(pfunc2) pfunc3 = func;//和auto配合使用decltype(&func) pfunc4 = func;map<string, string> dict = { {"left","左邊"}};auto it = dict.begin();//decltype的使用場景:要頂一個auto推導對象的拷貝decltype(it) copyIt1 = it;auto copyIt2 = it;//vector<auto>無法通過編譯vector<decltype(it)> v;v.push_back(it); }final-不讓繼承
override-檢查能否被重寫
default
? 在C++中對于空類編譯器會生成一些默認的成員函數,比如:構造函數、拷貝構造函數、運算符重載、析構函數和&和const&的重載、移動構造、移動拷貝構造等函數。如果在類中顯式定義了,編譯器將不會重新生成默認版本。有時候這樣的規則可能被忘記,最常見的是聲明了帶參數的構造函數,必要時則需要定義不帶參數的版本以實例化無參的對象。而且有時編譯器會生成,有時又不生成,容易造成混亂,于是C++11讓程序員可以控制是否需要編譯器生成。顯示缺省函數。在C++11中,可以在默認函數定義或者聲明時加上=default,從而顯式的指示編譯器生成該函數的默認版本,用=default修飾的函數稱為顯式缺省函數。
class A { public:A(int a): _a(a){}// 顯式缺省構造函數,由編譯器生成A() = default;// 在類中聲明,在類外定義時讓編譯器生成默認賦值運算符重載A& operator=(const A& a); private:int _a; }; A& A::operator=(const A& a) = default; int main() {A a1(10);A a2;a2 = a1;return 0; }delete
刪除默認函數。如果能想要限制某些默認函數的生成,在C++98中,是該函數設置成private,并且不給定義,這樣只要其他人想要調用就會報錯。在C++11中更簡單,只需在該函數聲明加上=delete即可,該語法指示編譯器不生成對應函數的默認版本,稱=delete修飾的函數為刪除函數.
比如:單例模式下,規定聲明的對象只能有一個,這個對象是不允許被拷貝構造其他的對象的,C++98 的方法一是,讓拷貝構造函數私有化,在類的外面就調用不到了,但是呢,在類的里面中的某個函數還是可以調用的,同時造成默認構造函數就無法生成了。如果類的里面也不讓調用時,也就是方法二:在私有中,只聲明但是不實現來防止拷貝。注意:如果不設置成私有的話,別人可以在類的外面實現拷貝構造函數,從而控制你提供的類做出修改。
C++11 就直接提供了關鍵字delete,在拷貝構造函數的后面加上:delete 函數就變成了已刪除函數,也無論私有與否了。
避免刪除函數和explicit一起使(explicit是防止函數進行隱式類型轉換)
class A {public:A(int a): _a(a){}// 禁止編譯器生成默認的拷貝構造函數以及賦值運算符重載A(const A&) = delete;A& operator(const A&) = delete;private:int _a; }; int main() {A a1(10);// 編譯失敗,因為該類沒有拷貝構造函數//A a2(a1);// 編譯失敗,因為該類沒有賦值運算符重載A a3(20);a3 = a2;return 0; }六 模板的可變參數
- printf
- 解析打印參數包中的類型和值
逗號表達式展開參數包
//參數包,傳一個或者多個 template<class T> void PrintArg(T val) {T copy(val);cout << typeid(T).name() << ":" << val << endl; } template<class ...Args> void ShowList(Args... args) {//{}列表初始化,開多大空間取決于可變參數個數,依次取出參數包//但是函數沒有返回值,創建數組需要元素有返回值,所以用逗號表達式,帶個0int arr[] = {(PrintArg(args),0)...};//逗號表達式,0是返回值。cout << endl; } //帶返回值,不用逗號表達式 template<class T> int PrintArg(T val) {T copy(val);cout << typeid(T).name() << ":" << val << endl;return 0; } template<class ...Args> void ShowList(Args... args) {int arr[] = { PrintArg(args)... };//0是返回值cout << endl; } int main() {ShowList(1);ShowList(1, 'A');ShowList(1,'A',string("sort"));return 0; } endl;ShowList(args...);//遞歸依次到達下一個參數 } int main() {ShowList(1);ShowList(1, 'A');ShowList(1,'A',string("sort"));return 0; }總結
- 上一篇: 管培生走下神坛,“高管捷径”破灭
- 下一篇: 你该知道的Pyecharts简易上手教程