?📚?博主的專欄
🐧?Linux???|?? 🖥??C++???|?? 📊?數據結構??|?💡C++ 算法?| 🌐?C 語言
上篇文章:unordered_map、unordered_set底層編寫
下篇文章:C++11:新的類功能、模板的可變參數、包裝器
本篇文章主要講解C++11的這些內容:
1. C++11簡介
2. 列表初始化
3. 變量類型推導
4. 范圍for循環
5. 新增加容器---靜態數組array、forward_list以及unordered系列6. 右值引用
7. 完美轉發
8. lambda表達式
目錄
C++11簡介
統一的列表初始化(不是初始化列表)
std::initializer_list:
std::initializer_list使用場景:
C++11增加的聲明
auto
decltype(推導對象類型,再用此類型定義對象)
?編輯nullptr
范圍for循環
智能指針
STL中一些變化
右值引用
左值引用和右值引用
什么是左值?什么是左值引用?
什么是右值?什么是右值引用?
左值引用不能給右值區別名:(const加引用左值可以 )
右值引用不能給左值區別名:(可以給move以后的左值取別名)
右值引用意義
移動構造和移動賦值能夠提高傳值返回的效率
結合場景講解:
場景一(定義對象并賦值):
場景二(定義對象后再賦值):
右值引用本身就是一個左值
push_back等方法也有左、右值引用,提高鍵值效率(寫匿名對象好)
上面所講的效率提升,指的是自定義類型的深拷貝的類,因為深拷貝的類才有轉移資源的說法,對于內置類型和淺拷貝自定義類型,沒有移動系列方法
完美轉發
為什么用完美轉發
作用:
要知道完美轉發在函數模版里面是用來干嘛的
lambda表達式
C++98中的一個例子
lambda表達式語法
舉例:
lambda類型到底是什么?
捕捉a、b對象給lambda:
修改外面的a、b
捕捉列表是否能夠傳地址?不能直接傳
捕捉列表的其他使用:
lambda適合用的場景:
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++官網:
C++11 - cppreference.com小故事:
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++98中,標準允許使用花括號{}對數組或者結構體元素進行統一的列表初始值設定。比如:?
struct Point
{int _x;int _y;
};int main()
{int array1[] = { 1, 2, 3, 4, 5 };int array2[5] = { 0 };Point p = { 1, 2 };Point p2 = 1; //單參數構造函數的隱式類型轉化return 0;
}
C++11擴大了用大括號括起的列表(初始化列表)的使用范圍,使其可用于所有的內置類型和用戶自定義的類型,使用初始化列表時,可添加等號(=),也可不添加。,多參數隱式類型轉換,構造+拷貝構造編譯器優化成直接構造
struct Point
{int _x;int _y;
};
int main()
{int x1 = 1;int x2{ 2 };int array1[]{ 1, 2, 3, 4, 5 };int array2[5]{ 0 };Point p1 = { 1, 2 };//多參數構造函數的隱式類型轉換Point p2 = { 1 }; //單參數也可以用列表初始化Point p3{ 1, 2 };// C++11中列表初始化也可以適用于new表達式中int* pa = new int[4] { 0 };return 0;
}
創建對象時也可以使用列表初始化方式調用構造函數初始化
class Date
{
public:Date(int year, int month, int day):_year(year), _month(month), _day(day){cout << "Date(int year, int month, int day)" << endl;}
private:int _year;int _month;int _day;;
};
int main()
{Date d1(2022, 1, 1); // old style// C++11支持的列表初始化,這里會調用構造函數初始化Date d2{ 2022, 1, 2 };Date d3 = { 2022, 1, 3 };return 0;
}
如果不想讓隱式類型轉換發生可以在構造函數前添加explicit:
?
std::initializer_list:
按理來說當我們想要這樣初始化容器對象的時候:
int main()
{vector<int> v1;vector<int> v2(10, 1);vector<int> v3 = { 1, 2, 3, 4, 5 };vector<int> v4 = { 10, 20, 30, 40, 50 };return 0;
}
構造函數應該寫成這樣:
?
std::initializer_list的介紹文檔:
std::initializer_list使用場景:
std::initializer_list一般是作為構造函數的參數,C++11對STL中的不少容器就增加std::initializer_list作為參數的構造函數,這樣初始化容器對象就更方便了。也可以作為operator=的參數,這樣就可以用大括號賦值
cplusplus.com/reference/list/list/list/
cplusplus.com/reference/vector/vector/vector/
cplusplus.com/reference/map/map/map/
cplusplus.com/reference/vector/vector/operator=/
initializer_list就是一個常量數組,語法就直接支持將一個數組直接給initializer_list,在32為機器下是8字節,他是兩個指針,一個指針指向常量數組的開始,一個指針指向常量數組的結束
注意這兩種寫法的區別:當Y類型要賦值給x類型的時候,這時候就叫做隱式類型轉換,x得支持Y類型為參數的構造就可以
模擬實現vector也支持{}初始化和賦值vector的簡單使用和模擬實現這篇文章有怎么使用initializer_list來支持,查看目錄
int main()
{vector<int> v = { 1,2,3,4 };list<int> lt = { 1,2 };// 這里{"sort", "排序"}會先初始化構造一個pair對象map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };// 使用大括號對容器賦值v = { 10, 20, 30 };return 0;
}
從前的pair寫法,與最方便的寫法:在map和set講過使用方法
C++11增加的聲明
auto
在C++98中auto是一個存儲類型的說明符,表明變量是局部自動存儲類型,但是局部域中定義局部的變量默認就是自動存儲類型,所以auto就沒什么價值了。C++11中廢棄auto原來的用法,將其用于實現自動類型推斷。這樣要求必須進行顯示初始化,讓編譯器將定義對象的類型設置為初始化值的類型。
注意:auto的小特性:
int main() {int i = 0;auto x = i;//x++會不會影響i,不會,x就是i的拷貝x++;//k++會不會影響i,會,k就是i的別名auto& k = i;k++;return 0; }
//j是i的引用,別名int& j = i;//此時的y是j的什么?拷貝auto y = j;
y的地址和j、i的地址不相同?
decltype(推導對象類型,再用此類型定義對象)
關鍵字decltype將變量的類型聲明為表達式指定的類型。和typeid有點像(typeid是拿到真實的類型輸出字符串)decltype用來推導類型,并且可以使用推導出的類型給來定義一個新對象。
相比于auto的優勢在于:在這種情況下,如何拿到ret3的類型,來構造一個模版參數是ret3類型的B對象?使用typeid無法解決。
template<class T> class B { public:T* New(int n){return new T[n];} };auto func1() {list<int> lt;auto ret = lt.begin();return ret; }int main() {auto ret3 = func1();B<>return 0; }
decltype能幫我們進行模版傳參:
但是同auto一樣,雖然在編寫代碼的時候輕松了一些,但是代碼的可讀性降低:
nullptr
由于C++中NULL被定義成字面量0,這樣就可能回帶來一些問題,因為0既能指針常量,又能表示整形常量。所以出于清晰和安全的角度考慮,C++11中新增了nullptr,用于表示空指針。
范圍for循環
C++17支持:模塊化處理,x,y相當于pair里的first和second
但是在寫范圍for的時候想寫這種寫法,盡量加上const 和&,否則這個地方會有深拷貝的問題。
在這里x相當于pair的first是不能被修改的,y相當于second可以被修改,被修改不會影響x(是一個拷貝,因為拷貝的代價還挺大的,因此最好加上const和& ,不需要改變的值就加const)
智能指針
在之后單獨寫一篇文章來講
STL中一些變化
新容器?
用橘色圈起來是C++11中的一些幾個新容器,但是實際最有用的是unordered_map和unordered_set。這兩個我們前面已經進行了非常詳細的講解,其他的大家了解一下即可。
其中array是一個靜態的數組 ,為了替代C語言的靜態數組,好處在于對于越界的檢查更嚴格
實踐當中并沒有那么的有用。因為不如vector,也可以檢查越界,也可以動態開辟空間
forward_list向前的鏈表,單向鏈表,省一個指針,但在實際場景當中也不常用。
容器中的一些新方法
如果我們再細細去看會發現基本每個容器中都增加了一些C++11的方法,但是其實很多都是用得比較少的,用處不大,按理不需要更新。有些部分,很需要,但是遲遲不出,比如網絡庫。
比較有意義的(vector,list,map等等幾個都有):移動構造(右值引用),以及initializer_list
比如提供了cbegin和cend方法返回const迭代器等等,但是實際意義不大,因為begin和end也是可以返回const迭代器的,這些都是屬于錦上添花的操作。
實際上C++11更新后,容器中增加的新方法最后用的插入接口函數的右值引用版本:
http://www.cplusplus.com/reference/vector/vector/emplace_back/
emplace_back對標的就是push_back
http://www.cplusplus.com/reference/vector/vector/push_back/
-----------------------------------------------------------------------------------------
http://www.cplusplus.com/reference/map/map/emplace/
emplace對標的就是insert
http://www.cplusplus.com/reference/map/map/insert/
但是這些接口到底意義在哪?網上都說他們能提高效率,他們是如何提高效率的?
請看下面的右值引用和移動語義章節的講解。另外emplace還涉及模板的可變參數,也需要再繼續深入學習后面章節的知識。
右值引用
左值引用和右值引用
傳統的C++語法中就有引用的語法,而C++11中新增了的右值引用語法特性,所以從現在開始我們之前學習的引用就叫做左值引用。無論左值引用還是右值引用,都是給對象取別名。
什么是左值?什么是左值引用?
左值是一個表示數據的表達式(如變量名或解引用的指針),我們可獲取它的地址+可以對它賦值,左值可以出現賦值符號的左邊,右值不能出現在賦值符號左邊。定義時const修飾符后的左值,不能給他賦值,但是可以取它的地址。左值引用就是給左值的引用,給左值取別名。
//a是左值,10是右值
int a = 10;
//b是左值
int b = a;
//c是左值
const int c = 10;
//*p是左值
int* p = &a;
vector<int> v(10, 1);
//v[1]是左值,是一個表達式
v[1];cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &(*p) << endl;
cout << &(v[1]) << endl;
左值和右值的區分就在于:能否取地址,簡單來說左值可以被取地址,不能被取地址的就是右值
什么是右值?什么是右值引用?
右值也是一個表示數據的表達式,如:字面常量、表達式返回值,函數返回值(這個不能是左值引用返回)等等,右值可以出現在賦值符號的右邊,但是不能出現出現在賦值符號的左邊,右值不能取地址。右值引用就是對右值的引用,給右值取別名。
像是臨時對象和匿名對象等都是屬于右值
左值右值的意思就是他是一個表達式,它本身就是一個值或者是函數會返回值
左值引用不能給右值區別名:(const加引用左值可以 )
但是const左值加引用可以
// 左值引用能否給右值取別名 -- 不可以,但是const左值引用可以const string& ref1 = string("1111");const int& ref2 = 10;
右值引用不能給左值區別名:(可以給move以后的左值取別名)
右值引用能否給左值取別名 -- 不可以,但是可以給move以后的左值取別名
string s1("1111");
string&& rref5 = move(s1);
右值引用意義
移動構造和移動賦值能夠提高傳值返回的效率
引用的意義:減少拷貝,提高效率
左值引用解決了:引用傳值傳參傳引用返回(不用拷貝)
沒有徹底解決的問題:引用返回值的問題沒有徹底解決,如果返回值是一個func2中的局部對象,不能用引用返回,出了作用域就被銷毀
// 左值引用的場景 void func1(const string& s); string& func2();
結合場景講解:
比如說這里的to_string,把整形轉成字符串,轉成字符串就存到了一個局部對象str,這里就不能用左值引用返回,不能返回他的別名,因為他的別名的生命周期就在這函數作用域里面,能否使用右值引用返回?也不能。
pupu::string& to_string(int value)
{bool flag = true;if (value < 0){flag = false;value = 0 - value;}pupu::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;
}
我們使用一個之前寫過的String:來看看這里一共調用了多少次拷貝構造
namespace pupu
{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(string& s){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}// 拷貝構造// 左值string(const 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& operator=(const 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;}~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)string& operator+=(char ch){push_back(ch);return *this;}const char* c_str() const{return _str;}private:char* _str;size_t _size;size_t _capacity; // 不包含最后做標識的\0};pupu::string to_string(int value){bool flag = true;if (value < 0){flag = false;value = 0 - value;}pupu::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;}
}int main()
{pupu::string ret1 = pupu::to_string(1234);cout << ret1.c_str() << endl;return 0;
}
場景一(定義對象并賦值):
vs2019及以下可以看到的結果:這里的賦值過程是一個構造加一個深拷貝
改進方法:
添加左值引用:
在這里使用左值引用:會導致程序崩潰
右值引用:
也不能使用右值引用,move(str),會導致程序崩潰
str已經銷毀了,不管是左值引用還是右值引用,引用的值都被銷毀了,引用也不存在了
解決辦法:
加移動構造:和左值的拷貝構造構成重載,編譯器自己去匹配
// 移動構造string(string&& s):_str(nullptr){cout << "string(string&& s) -- 移動構造" << endl;}
而這個值已經是要被銷毀的,也就是將亡值,因此我們加上一個交換swap(s),使得將亡值給到ret
移動構造右值(將亡值)string(string&& s):_str(nullptr){cout << "string(string&& s) -- 移動構造" << endl;swap(s);}
測試:
int main() {pupu::string ret1 = pupu::to_string(1234);cout << ret1.c_str() << endl;return 0; }
這個值雖然是左值,但是可以強行move成右值,編譯器識別到兩次移動構造,直接給優化成一次了。
左值的資源是不能被掠奪的,而右值的資源可以被掠奪?
對比一下拷貝構造:拷貝你的資源,你再被銷毀,有了移動構造,被強行識別成了右值,因此就能掠奪資源,無需拷貝,直接拿走這個空間的值?
?看看底層:
?在這個程序結束之前,會先去調用移動構造
回到return str這里:str已經被銷毀了,資源已經被換給了ret
ret有了同樣的虛擬地址資源、數據:
嚴格的來說不是右值延長了 將亡變量 的生命周期,而是延長了將亡值,資源的生命周期,使得資源換給了接受變量,這個str還是在出作用域就被銷毀。?
如果你是一個左值我只能對你進行深拷貝,如果你是一個右值,我就可以轉移你的資源。
場景二(定義對象后再賦值):
看這個測試:(vs2022加上move好看效果,不然會被編譯器優化)
并縮減to_string代碼:
pupu::string to_string(int value) {pupu::string str;//...std::reverse(str.begin(), str.end());return move(str);//return的時候中間會產生臨時對象,他默認是一個左值,會走拷貝構造 }
int main() {pupu::string ret1;ret1 = pupu::to_string(1234);return 0; }
屏蔽掉移動構造:釋放了三次資源,拷貝了兩次資源
return 的時候會產生臨時變量,如果返回的對象比較小,就像現在,是存在寄存器中的,存不下的時候,就放在ret和str的棧幀之間(ret在被定義的時候,本身就是有資源的))
然后,str所存的值拷貝給臨時對象,str被銷毀:
然后這個臨時對象給ret1:
再去做深拷貝,臨時對象被銷毀:這里一共釋放了三次資源,拷貝了兩次資源
所以C++11就做了右值引用出來:
有了移動構造和移動賦值
首先將str識別成右值,不識別成右值,那么第一次就會是拷貝構造,第二次才是移動構造
移動構造和移動賦值的特點:如果你是右值,就直接轉移你的資源,如下圖,將str的資源轉給ret1
對于vs2022寫成move(str)更好觀察:這是還未添加移動構造的時候
添加移動賦值:
//移動賦值// s3 = 將亡值對象,不拷貝你,直接拿你的資源string& operator=(string&& s){cout << "string(string&& s) -- 移動賦值" << endl;swap(s);return *this;}
移動構造和移動賦值的過程:只釋放一次資源,沒有拷貝資源
臨時對象不再去拷貝,而是直接轉移走str的資源,str被銷毀
ret1原來自己是有一個資源的,在次和臨時對象進行了一次交換,互相指向對方的資源:
臨時對象是一個將亡值,被銷毀的時候順帶將ret1原有的資源也一起帶走銷毀。
由此可以看到有了移動構造和移動賦值徹底拯救了傳值返回的場景,提高了效率
再看示例:
楊輝三角,在C++11之前要這樣寫,才能保證效率:這個時候就沒有拷貝了,vv是ret的引用。
在C++11出來后:直接資源轉移
vv對象是指向這個vector的,移動構造就是直接讓,ret指向vv的資源,vv出作用域被銷毀
移動構造和移動賦值是什么,是本身傳給他們的就是一個右值, 然后在函數中利用右值的別名,只需要交換資源,沒有什么代價,而現代寫法沒有什么優化,雖然他們都是交換,但交換的不是一個東西,現代寫法是利用自己構造的對象來進行一個值交換,還是進行了深拷貝,移動賦值的交換是交換將亡值的資源。
以上我們講到了移動構造和移動賦值能夠提高傳值返回的效率
右值引用本身就是一個左值
此時的s1右值還是左值???
//右值引用 右值std::string&& s1 = std::string("111111");//s1是右值cout << &s1 << endl;
通過是否能取地址來確認:
已經確定了?std::string("111111"),是右值。
實際上是右值引用s1的屬性就是一個左值?
解釋:前面有講到過普通左值引用不能引用右值,const左值引用才能引用右值
因為,只有右值引用本身處理成左值,才能實現移動構造和移動賦值
如何做到的:
右值也是有地址的,只是不讓取?
底層右值引用也是存了右值的地址,才好轉移資源(重點不是可以取地址)。
是左值的真正意義在于:語法的邏輯自洽,能夠保證移動構造移動賦值,轉移資源的語法邏輯是自洽的。右值是不想被改變的,因此不能加左值引用,因為加了左值引用就能被改變,因此想加左值引用就要加const,保證右值不會被改變
那右值還有什么辦法能夠在不加const的情況下用左值引用???強轉:
std::string& s2 = (std::string&)std::string("111");
因為右值是有空間存儲的,所以就可以改他。
還可以先得到一個右值引用,再左值引用他的右值引用
std::string&& s3 = std::string("111111111");std::string& s4 = s3;
push_back等方法也有左、右值引用,提高鍵值效率(寫匿名對象好)
區別在于,如果是左值就匹配左值引用,右值就匹配右值引用
用于:push_back當中這里對于s1是調用了一個拷貝構造
鏈表里面要插入一個值的時候就還要去構造一個節點 ,這個節點里面得有一個string,這個鏈表存的是string,因此這里所做的是一個深拷貝,s1是一個左值,不能被轉移資源,只能做深拷貝。
而這里就是調用的移動構造,因為里面是一個右值是一個將亡值,可以被掠奪資源,被掠奪資源之后,就讓你置空
lt.push_back(pupu::string("2222"));
也能這樣寫:
看看底層:調用了之前自己所寫的list,給之前寫的list加上右值版本:無論是左值還是右值都掉用push_back,左值引用調用左值的,右值調用右值引用的
//右值版本void push_back(T&& x){insert(end(), forward<T>(x));}
但是:push_back是復用的insert,insert這里是左值引用的const
void insert(iterator pos, const T& val){Node* cur = pos._node;Node* newnode = new Node(val);Node* prev = cur->_prev;// prev newnode cur;prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;_size++;}
但是實際上,insert也有右值引用版本:
因此我們再添加一個右值引用版本的insert
void insert(iterator pos, T&& val){Node* cur = pos._node;Node* newnode = new Node(forward<T>(val));Node* prev = cur->_prev;// prev newnode cur;prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;_size++;}
還是未達到目的?
調試代碼發現沒有去調用右值引用:
push_back調用的是右值引用的:
但是insert卻沒有調用右值引用的:
這是因為,前面的右值引用要能夠去交換,它本身的屬性是左值,為了移動構造,因此想要就想到使用右值引用,就在傳參給insert的時候,傳右值move(x);
void push_back(const T& x){insert(end(), move(x));}
?但是還有一點:在insert中,要構造的時候,會傳給這個val,這個val也應該是string右值引用,會調用構造
因此又進入了鏈表的構造,這里是左值引用,也需要加右值引用版本的List構造,
每一層都需要有右值引用版本
到了new Node(val)時,這里又傳左值,因此,需要轉成move
這樣才能用到移動構造?
注意:move就是把左值屬性轉成右值。
如果我們現在list的模版參數是int、日期類,不再是string,push_back(),左右值引用的區別:沒區別,是左值還是右值,int沒有拷貝構造和移動構造,淺拷貝的類不存在轉移資源的說法。
上面所講的效率提升,指的是自定義類型的深拷貝的類,因為深拷貝的類才有轉移資源的說法,對于內置類型和淺拷貝自定義類型,沒有移動系列方法
完美轉發
模板中的&& 萬能引用
上面所添加的move的方式,的優化方式就是完美轉發:
模板中的&&不代表右值引用,而是萬能引用,其既能接收左值又能接收右值。
模板的萬能引用只是提供了能夠接收同時接收左值引用和右值引用的能力,但是引用類型的唯一作用就是限制了接收的類型,后續使用中都退化成了左值,
我們希望能夠在傳遞過程中保持它的左值或者右值的屬性, 就需要用我們下面學習的完美轉發
模板中的&&不代表右值引用,而是萬能引用,其既能接收左值又能接收右值。
模板的萬能引用只是提供了能夠接收同時接收左值引用和右值引用的能力,但是引用類型的唯一作用就是限制了接收的類型,后續使用中都退化成了左值。
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<T>(t)在傳參的過程中保持了t的原生類型屬性。// 函數模版里面,這里可以叫萬能引用
// 實參傳左值,就推成左值引用
// 實參傳右值,就推成右值引用
template<typename T>
void PerfectForward(T&& t)
{//Fun((T&&)t);Fun(forward<T>(t));
}
//以下等等都是函數模版要推導出來的函數
void PerfectForward(int&& t)
{Fun((int&&)t);
}void PerfectForward(int& t)
{Fun((int&)t);
}void PerfectForward(const int&& t)
{Fun((const int&&)t);
}void PerfectForward(const int& t)
{Fun((const int&)t);
}int main()
{PerfectForward(10); // 右值int a;PerfectForward(a); // 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b); // const 左值PerfectForward(std::move(b)); // const 右值return 0;
}
注意一下:右值也有const的概念?
為什么用完美轉發
函數模版里面,這里可以叫萬能引用,引用折疊,看起來我是右值引用,但是我是一個模版,因此是萬能引用,有了模版,靈活推導,但是要注意的是右值被右值引用,我的屬性會退化成左值引用
?通過我們傳什么值,推出什么引用的函數
這里不能用move來解決:使用了move就全變成右值引用了
解決辦法:不寫模版了,在需要傳右值的地方move
優化的解決辦法:完美轉發,move和完美轉發的本質都是類型轉化
作用:
你是什么值,我就保持你的什么屬性(比如如果是左值就直接返回,如果是右值就相當于加了move):
就像是也可以直接:在傳的時候強轉。
因此前面list時也可以將接收左右值的地方加上完美轉發
要知道完美轉發在函數模版里面是用來干嘛的
防止右值引用,向下傳值的時候丟失屬性,屬性為左值,但我們又想他屬性保持右值。
lambda表達式
C++98中的一個例子
在C++98中,如果想要對一個數據集合中的元素進行排序,可以使用std::sort方法
#include <algorithm>
#include <functional>
int main()
{int array[] = { 4,1,8,5,3,7,0,9,2,6 };// 默認按照小于比較,排出來結果是升序std::sort(array, array + sizeof(array) / sizeof(array[0]));// 如果需要降序,需要改變元素的比較規則std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());return 0;
}
如果待排序元素為自定義類型,需要用戶自定義排序時的比較規則:
struct Goods
{string _name; // 名字double _price; // 價格int _evaluate; // 評價Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};
struct ComparePriceLess
{bool operator()(const Goods& gl, const Goods& gr){return gl._price < gr._price;}
};
struct ComparePriceGreater
{bool operator()(const Goods& gl, const Goods& gr){return gl._price > gr._price;}
};
int main()
{vector<Goods> v = { { "蘋果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠蘿", 1.5, 4 } };sort(v.begin(), v.end(), ComparePriceLess());sort(v.begin(), v.end(), ComparePriceGreater());
}
隨著C++語法的發展,人們開始覺得上面的寫法太復雜了,每次為了實現一個algorithm算法,都要重新去寫一個類,如果每次比較的邏輯不一樣,還要去實現多個類,特別是相同類的命名,這些都給編程者帶來了極大的不便。因此,在C++11語法中出現了Lambda表達式。
lambda表達式語法
lambda表達式書寫格式:[capture-list] (parameters) mutable -> return-type { statement }
1. lambda表達式各部分說明
- [capture-list] : 捕捉列表,該列表總是出現在lambda函數的開始位置,編譯器根據[]來判斷接下來的代碼是否為lambda函數,捕捉列表能夠捕捉上下文中的變量供lambda函數使用。
- (parameters):參數列表。與普通函數的參數列表一致,如果不需要參數傳遞,則可以連同()一起省略
- mutable:默認情況下,lambda函數總是一個const函數,mutable可以取消其常量性。使用該修飾符時,參數列表不可省略(即使參數為空)。
- ->returntype:返回值類型。用追蹤返回類型形式聲明函數的返回值類型,沒有返回值時此部分可省略。返回值類型明確情況下,也可省略,由編譯器對返回類型進行推導。
- {statement}:函數體。在該函數體內,除了可以使用其參數外,還可以使用所有捕獲到的變量。
- 注意:
在lambda函數定義中,參數列表和返回值類型都是可選部分,而捕捉列表和函數體可以為空。因此C++11中最簡單的lambda函數為:[]{}; 該lambda函數不能做任何事情。
舉例:
1.捕捉列表不能省略 2.參數列表(如果不需要參數傳遞,就能全省略)3.mutable,可以省略,表示默認const 4.返回值類型 + 一個函數體(返回值類型通常可以省略,只要返回值是明確的情況下,因為會自動推導)
int main() {// lambda 1.捕捉列表不能省略 2.參數列表(如果不需要參數傳遞,就能全省略)3.mutable,可以省略,表示默認const 4.返回值,一個函數體auto add1 = [](int a, int b)->int {return a + b; };// 返回值可以省略auto add2 = [](int a, int b) {return a + b; };// 沒有參數,參數列表可以省略auto func1 = [] {cout << "hello world" << endl; };cout << typeid(add1).name() << endl;cout << typeid(add2).name() << endl;cout << typeid(func1).name() << endl;cout << add1(1, 2) << endl;func1();return 0; }
lambda函數是一個局部的匿名函數,如何調用這個函數
1.使用auto來推導,像仿函數或者,使用函數名調用一樣的,或者用包裝器
auto add1 = [](int a, int b)->int {return a + b; };// 返回值可以省略auto add2 = [](int a, int b) {return a + b; };cout << typeid(add1).name() << endl;cout << add1(1, 2) << endl;
因此像前面的自定義類型,需要用戶自定義排序時的比較規則:我們也可以使用lambda來寫一個價格的升序的函數:
auto priceless = [](const Goods& g1, const Goods& g2)
{return g1._price < g2._price;
};
sort(v.begin(), v.end(), priceless);
在實踐當中還能寫的更為簡潔:
我們可以直接將lambda表達式(是有類型的)看作是一個和priceless一樣的對象:
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._price < g2._price; });
直接將這個lambda表達式傳給了sort的compare模版。lambda的類型到底是什么編譯器知道
這樣寫無疑更加的便捷:
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._evaluate < g2._evaluate;});sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._evaluate > g2._evaluate;});
lambda類型到底是什么?
vs2022
vs2019
lambda的本質就是仿函數,lambda原理類似于范圍for,lambda編譯時,編譯器會生成對應的仿函數。對我們而言lambda的類型是未知的,編譯器知道。
我們現在想實現兩個數交換的lambda表達式,這里體現了auto的價值:
int main() {int a = 1, int b = 2;auto swap1 = [](int& x, int& y){int tmp = x;x = y;y = tmp;};swap1(a, b);return 0; }
捕捉a、b對象給lambda:
捕捉a,b給lambda用,默認捕捉過來的是const并且是傳值捕捉,上面的a,b和這里的a,b不是同一個a,b(可以認為是一個拷貝,并且加了const)
auto swap2 = [a, b](){int tmp = a;a = b;b = tmp;};swap2();
因此需要取消const也就是加mutable
auto swap2 = [a, b]() mutable{int tmp = a;a = b;b = tmp;};swap2();
lambda里的a,b是拷貝過來的,雖然被修改,也不會影響原來的a,b
修改外面的a、b
//引用int& REF = a;//一般引用是這樣的//引用的捕捉,不是取地址,不要混淆了auto swap3 = [&a, &b]() //容易混淆取地址和引用{int tmp = a;a = b;b = tmp;};swap3();
捕捉列表是否能夠傳地址?不能直接傳
間接傳:
int* pa = &a, * pb = &b;
auto swap3 = [pa, pb]() //容易混淆取地址和引用{int tmp = *pa;*pa = *pb;*pb = tmp;};
捕捉列表的其他使用:
int main()
{int a = 1, b = 2, c = 3, d = 4, e = 5;// 傳值捕捉所有對象auto func1 = [=](){return a + b + c * d;};cout << func1() << endl;// 傳引用捕捉所有對象auto func2 = [&](){a++;b++;c++;d++;e++;};func2();cout << a << b << c << d << e << endl;// 混合捕捉,傳引用捕捉所有對象,但是d和e傳值捕捉auto func3 = [&, d, e](){a++;b++;c++;//d++;//e++;};func3();cout << a << b << c << d << e << endl;// a b傳引用捕捉,d和e傳值捕捉auto func4 = [&a, &b, d, e]() mutable{a++;b++;d++;e++;};func4();cout << a << b << c << d << e << endl;return 0;
}
lambda適合用的場景:
想定義一個直接可以使用的小函數,就可以使用
結語:
? ? ? ?隨著這篇關于題目解析的博客接近尾聲,我衷心希望我所分享的內容能為你帶來一些啟發和幫助。學習和理解的過程往往充滿挑戰,但正是這些挑戰讓我們不斷成長和進步。我在準備這篇文章時,也深刻體會到了學習與分享的樂趣。
? ? ? ? ?在此,我要特別感謝每一位閱讀到這里的你。是你的關注和支持,給予了我持續寫作和分享的動力。我深知,無論我在某個領域有多少見解,都離不開大家的鼓勵與指正。因此,如果你在閱讀過程中有任何疑問、建議或是發現了文章中的不足之處,都歡迎你慷慨賜教。
? ? ? ? 你的每一條反饋都是我前進路上的寶貴財富。同時,我也非常期待能夠得到你的點贊、收藏,關注這將是對我莫大的支持和鼓勵。當然,我更期待的是能夠持續為你帶來有價值的內容,讓我們在知識的道路上共同前行。