目錄
前言
列表初始化
{ }初始化
initializer_list類
類型推導
auto
decltype
范圍for
右值引用與移動語義
左值引用和右值引用
移動語義
1.移動構造
2.移動賦值?
3.stl容器相關更新?
右值引用和萬能引用
完美轉發
關鍵字
default
delete
final和override
可變參數模板
介紹
使用場景
lambda表達式
包裝器
bind函數
線程庫
后記
前言
? ? ? ? C++11 是 C++ 語言的一個重要更新,它加入了許多新的語言特性和標準庫組件,旨在提高代碼的可讀性、可維護性、可移植性和安全性,同時也提高了語言的表達能力和性能。C++11 的引入,對于 C++ 程序員來說是一個里程碑式的事件,它使得 C++ 語言更加現代化和高效。因此我們要作為一個重點去學習。在把本篇文章中,主要介紹一些新增特性,比如花括號初始化、initializer_list類、auto、范圍for等,其中較為重要的有右值引用、lambda表達式、線程庫,內容較大的特性會專門出一片文章講解,比如智能指針、異常相關新增特性等,下面來看看上述的詳細介紹吧。
列表初始化
-
{ }初始化
????????C++98允許使用花括號{ }對數組或者結構體元素進行統一的列表初始值設定,C++11擴大了用大括號括起的列表(初始化列表)的使用范圍,使其可用于所有的內置類型和用戶自定義的類型,使用初始化列表時,可添加等號(=),也可不添加。列表初始化也可以適用于new表達式中,自定義類型不僅可以通過構造函數使用圓括號構造,也可以使用花括號構造,舉例如下代碼塊。
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;
};
struct A
{int _a;int _aa;
};
int main()
{int a{ 0 };int arr1[] = { 1,2,3 };int arr2[] { 1,2,3 };int arr3[3] = { 0 };int arr4[3] { 0 };A a1 = { 1,2 };A a2 { 1,2 };int* ptr = new int[3]{ 1,1,1 };Date d1(1, 2, 3);Date d2{ 1,2,3 };return 0;
}
-
initializer_list類
????????initializer_list類似于數組和向量,可以存儲一組數據,并且支持迭代器,可以用于函數參數、構造函數和賦值運算符的參數中。通過使用initializer_list,可以輕松地傳遞一組數據給一個函數或者對象,而不必顯式地指定這組數據的長度或者元素類型。STL中的不少容器就增加 std::initializer_list作為參數的構造函數,比如
eg:
類型推導
-
auto
????????關鍵字auto在C++中是用于自動類型推導的關鍵字。當使用auto聲明變量時,編譯器會根據變量的初始化表達式自動推導出變量的類型。使用auto可以簡化代碼,特別是當變量類型較長或較復雜時。另外,auto還可以結合迭代器模板等使用,更加靈活和簡潔。當auto與&結合說明這是個引用變量,當auto與*結合說明是個指針變量,舉例如下:
eg:
-
decltype
????????decltype是一個關鍵字,用于獲取表達式的類型,而不是用于實例化一個對象,可以用于函數返回值類型推斷、模板參數類型推斷等,舉例:
eg:
范圍for
????????C++中的范圍for是一種遍歷容器、數組、字符串等可迭代對象的簡便方法,實際底層就是迭代器遍歷。范圍for循環通過在循環中聲明一個變量,在每次迭代中自動將其設為下一個元素的值來遍歷可迭代對象中的元素。
?eg:
int arr[] = {1, 2, 3, 4, 5};
for (int x : arr) {cout << x << " ";
}
// 輸出: 1 2 3 4 5
右值引用與移動語義
-
左值引用和右值引用
? ? ? ? 首先,無論是左值引用還是右值引用,都是給對象取別名,要弄明白左值引用和右值引用,先了解一下左值與右值是什么意思。對于左值,可以獲取它的地址+對它賦值,注意左值可以出現在賦值符號的左邊,也可以出現在右邊,左值引用就是對左值的引用,給左值取別名;在此之前所學的引用都是左值引用,左值引用使用一個&符號來聲明,比如:
int a = 10; //左值int* b = new int(1); //左值const int c = 1; //左值int& refa = a; //左值引用int*& refb = b; //左值引用const int& refc = c; //左值引用
? ? ? ? 對于右值,不能取地址+不能出現在賦值符號的左邊,是一個表示數據的表達式,比如字面常量、表達式返回值等,右值引用就是對右值的引用,給右值取別名,比如:?
int x = 0, y = 0;1; //右值x + y; //右值x + 1; //右值int&& rr1 = 1; //右值引用int&& rr2 = x + y; //右值引用int&& rr3 = x + 1; //右值引用//int&& ref4 = x; //報錯
左值引用與右值引用的比較:
? ? ? ? ①左值引用只能引用左值,不能引用右值,但是const左值引用既可引用左值,也可引用右值;
? ? ? ? ②右值引用只能右值,不能引用左值,但是右值引用可以move以后的左值;
其中move函數的作用就在于將左值強制轉換為右值,比如:
int a = 10;//int& d = 10; //左值引用引用不了右值const int& d = 10; //const左值引用可以引用右值//int&& e = a; //右值引用引用不了左值int&& e = move(a); //右值引用可以引用move之后的左值
-
移動語義
1.移動構造
? ? ? ? 那左值引用用的好好的,為什么要提出右值引用呢?我們想一下左值引用的短板,有這樣一個情況,當函數返回值是一個局部變量,出了作用域就會被銷毀,就不能使用(左值)引用返回,只能使用傳值返回,但是傳值返回至少會有一次拷貝構造(即使在編譯器優化以后),因此為了減少拷貝,下面考慮其他方法——引入移動構造、移動賦值。
? ? ? ? 在此之前,先介紹一下右值的分類,包括純右值(內置類型右值)和將亡值(自定義類型右值),對于純右值,就算是拷貝多次也無所謂,但是對于有申請資源的將亡值,拷貝一次都是極大地降低了效率,所以考慮將將亡值的資源轉給需要的新對象,也就是用將亡值即將不要的資源去構造給需要的對象,這可以大大的減少拷貝,提高效率。
? ? ? ? 通過下面一個具體的例子描述一下這個過程,在模擬實現string類時,有這樣一個int轉string的函數,如下圖,左邊是string的拷貝構造函數,右邊是To_string函數傳值返回的過程,正常編譯器優化情況下,會將str的資源拷貝一份給main函數中的str,但是我們發現這個To_string函數中的str就是一個將亡值,退出函數str就會被釋放,而main函數中的str正好是一個需要此資源的新對象,正如上面所說,將To_string函數中的str對象資源移動給main函數中的str,這樣就是一次移動構造,如第二圖。
? ? ? ? 對于移動構造函數,我們可以看到,參數是一個右值引用,函數體就是進行資源的交換或者說移動,為什么To_string函數返回之后會調用移動構造函數去構造str呢?因為這里編譯器會將To_string函數的局部變量返回值識別成一個右值(將亡值),就會自動去找最匹配的構造函數去構造對象。當沒有移動構造函數時,這里就會去調用拷貝構造函數,因為const左值引用也可以接收一個右值,當兩個都存在時,存在構造對象的地方會去調用最匹配的構造函數。
2.移動賦值?
? ? ? ? 不僅有移動構造,還有移動賦值,原理一樣,這里也簡單說一下,結合在一起較為容易理解。如下圖,對于移動賦值函數,參數依舊是右值引用,函數體內也是在不是兩個相同的對象賦值的情況以外,交換或移動將亡值的資源給目標對象,實現資源的轉移以提高效率。
3.stl容器相關更新?
? ? ? ? ?移動構造和移動賦值不僅解決了傳值返回的多次拷貝問題,而且這種資源移動的思想也應用到了stl的容器上,為相關接口增加了右值引用版本,以減少對象的拷貝,如:
eg:
? ? ? ? 同時,在原本6個默認成員函數的基礎上又增加了兩個默認成員函數——移動構造函數和移動賦值運算符重載。注意:
①如果你沒有自己實現移動構造函數,且沒有實現析構函數 、拷貝構造、拷貝賦值重載中的任 意一個。那么編譯器會自動生成一個默認移動構造;
②如果你沒有自己實現移動賦值重載函數,且沒有實現析構函數 、拷貝構造、拷貝賦值重載中 的任意一個,那么編譯器會自動生成一個默認移動賦值,
? ? ? ? 實際上,實現一個類有申請資源時,則得實現拷貝構造、析構、拷貝賦值以進行深拷貝,同時想減少拷貝,就得實現移動構造和移動賦值,但由于有了上面那三個,編譯器就不會自動生成,所以還是得自己實現這兩個,因此存在屬性申請資源時,自己實現拷貝構造、析構、拷貝賦值、移動構造、移動賦值。?
-
右值引用和萬能引用
????????萬能引用主要有兩種,一種是在函數模板中使用的一種引用類型,它的語法形式為“T&&”,其中T是一個模板參數。還有一種是“auto&&”,萬能引用可以接受任意類型的實參,并且保留了實參的左右值屬性。值得注意的是,必須存在類型推導才是萬能引用,否則是右值引用。舉例如下圖,特別要注意最后一個例子,其中push_back函數得參數雖然是T&&,但是在模板實例化時T的理性就已確定,不存在類型推導,而且在前面也提到過,這個是容器新增得右值引用版本接口,不是萬能引用。
eg:
-
完美轉發
? ? ? ? 完美轉發提供了一種機制來保留函數參數的完整類型信息,并將其轉發給另一個函數。傳統上,在C++中,當一個函數接收一個參數并將其轉發給另一個函數時,它會失去原始參數的類型信息(比如說右值引用版本的接口接收一個右值引用,但是在函數體內這個變量被當作左值去使用,那當我們需要去使用它的右值特性去調用其他相關函數時就沒有辦法了),此時C++完美轉發保留了它的左值或右值的屬性。語法如下:
template<typename T>
void func(T&& arg)
{other_func(std::forward<T>(arg)); //完美轉發
}
? ? ? ? 下面通過一個例子來展現一下完美轉發的使用場景,如下代碼是List類的模擬實現,僅包括尾插和插入函數,在mian函數中,尾插一個“1111”的常量字符串,毫無疑問,會匹配右值引用版本的push_back函數,其中需要復用insert函數,而且需要復用右值引用版本的insert函數,但是在push_back函數的函數體內,x已經被當作成了左值,已經失去了“1111”的右值特性,此時使用萬能轉發保持其屬性,繼續會匹配右值引用版本的insert函數,在這個函數體內,也需要去調用右值引用版本的Node節點的構造函數,也就是移動構造函數,也必須通過萬能引用去操作,在Node的移動構造函數中,我們也需要將右值版本的字符串放進_data中,也是通過萬能轉發的方法。
? ? ? ? 從上面的例子當中可以看出,萬能轉發在實際開發中也是較為需要的,較為重要的。
代碼:
template <class T>
struct ListNode
{//構造函數用來創節點ListNode(const T& x = T()) //左值版本:_data(x), _prev(nullptr), _next(nullptr){}ListNode(T&& x) //右值版本:_data(forward<T>(x)), _prev(nullptr), _next(nullptr){}T _data;ListNode<T>* _prev;ListNode<T>* _next;
};template <class T>
class List
{typedef ListNode<T> Lnode;
public://...iterator insert(iterator pos, const T& x) //左值版本{Lnode* newNode = new Lnode(x);pos._node->_prev->_next = newNode;newNode->_prev = pos._node->_prev;newNode->_next = pos._node;pos._node->_prev = newNode;return iterator(newNode); //返回插入位置的迭代器}iterator insert(iterator pos, T&& x) //右值版本{Lnode* newNode = new Lnode(forward<T>(x));pos._node->_prev->_next = newNode;newNode->_prev = pos._node->_prev;newNode->_next = pos._node;pos._node->_prev = newNode;return iterator(newNode); //返回插入位置的迭代器}void push_back(const T& x) //左值版本{insert(end(), x);}void push_back(T&& x) //右值版本{insert(end(), forward<T>(x));}
private:Lnode* _head;
};int main()
{List<string> lt;lt.push_back("1111");return 0;
}
關鍵字
-
default
????????關鍵字default用于強制生成默認函數,可以更好的控制默認函數。比如,當只有拷貝構造函數時,運行會報錯沒有默認構造函數,此時使用default強制自動生成即可,如下圖
eg:
?
-
delete
? ? ? ? 關鍵字delete用于禁止生成默認函數,只需在該函數聲明加上=delete即可,該語法指示編譯器不生成對應函數的默認版本,稱=delete修飾的函數為刪除函數。
eg:
-
final和override
? ? ? ? find和override關鍵字在之前的章節繼承和多態中講過,對于final,即可以修飾類不能被繼承,也可以修飾虛函數不能被重寫;對于override,放在子類中,檢查子類虛函數是否重寫了父類的虛函數,具體可見http://t.csdnimg.cn/5CvsA
http://t.csdnimg.cn/5CvsA
可變參數模板
-
介紹
? ? ? ? 可變參數模板可以讓我們編寫接受可變數量參數類型的函數和類模板。下面是一個基本可變參數的函數模板,args前面有省略號,稱為參數包,其中包含若干個模板參數,我們無法直接獲取其中的每個參數,只能展開參數包的方式獲取,在C++中,有兩種方式展開可變參數模板的參數包:遞歸函數方式展開和逗號表達式方式展開。
template <typename... Args>
void printArgs(Args... args)
{}
遞歸函數方式展開:
?????????遞歸展開是指在函數或類模板中遞歸調用自己,并將參數包展開為獨立的參數列表。這可以通過使用遞歸模板函數或類模板來實現。如下代碼,包括遞歸終止函數和普通展開函數,main函數中的ShowList調用過程為:
①1傳進t,其余初步傳進args參數包,繼續遞歸調用展開函數;
②'a'傳進t,其余傳進args參數包,此時參數包只剩一個參數"111"了;
③調用最匹配的函數,即遞歸終止函數,傳進t,之后遞歸結束,每個參數也最終獲取到了。
template <class T>
void ShowList(const T& t) //遞歸終止函數
{cout << t << endl;
}template <class T, class ...Args>
void ShowList(const T& t, Args... args) //展開函數
{cout << t << endl; //t就是參數包里的一個參數,這里進行使用即可ShowList(args...);
}int main()
{ShowList(1, 'a', "111");
}
逗號表達式方式展開:?
? ? ? ? 利用初始化列表來初始化一個變長數組,{(printarg(args), 0)...}將會展開成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... ),最終會創建一個元素值都為0的數組arr,由于是逗號表達式,在創建數組的過程中會先執行逗號表達式前面的部分printarg(args) 獲取到當前的參數,也就是說在構造int數組的過程中就將參數包展開了,因此獲取到參數包中的所有參數,注意這個數組的目的純粹是為了在數組構造的過程展開參數包,使用參數的地方是在PrintArg函數中。
template <class T>
void PrintArg(T t)
{cout << t << " ";
}//展開函數
template <class ...Args>
void ShowList(Args... args)
{int arr[] = { (PrintArg(args), 0)... };cout << endl;
}int main()
{ShowList(1, 'A', "111");return 0;
}
-
使用場景
? ? ? ? 如果上面的參數包、展開方式你并沒有看懂,那就作為了解即可,但是使用場景必須能看得懂,可變參數模板應用在stl容器的emplace相關接口上,比如
? ? ? ? 可以看到emplace接口參數,既支持模板的可變參數,又是萬能引用,也就是同時可以接受左值,也可以接受右值,下面看看如何使用這個接口:其中對于一個元素是pair類的vector,可以直接將一個pair的元素使用emplace_back插入,但是push_back的話就必須去調用make_pair函數。
int main()
{vector<pair<string, int>> v;v.emplace_back("1", 1);//v.push_back("1", 1); //報錯v.push_back(make_pair("1", 1));v.push_back({ "1", 1 });return 0;
}
lambda表達式
????????lambda表達式是一種匿名函數,可以在需要函數對象的任何地方使用。lambda表達式的基本語法如下:
[capture-list] (parameters) mutable -> return-type { function-body }
????????其中,
捕獲列表(capture-list):用于捕獲外部變量,該列表總是出現在lambda函數的開始位置,編譯器根據[ ]來判斷接下來的代碼是否為lambda函數,捕捉列表能夠捕捉上下文中的變量供lambda 函數使用,每個變量可以指定為按值捕獲或按引用捕獲,
- [var]:表示值傳遞方式捕捉變量var,正常情況下可讀不可寫,加上mutable變成了一份拷貝,就可讀可寫了
- [=]:表示值傳遞方式捕獲所有所在棧幀的變量(包括this)
- [&var]:表示引用傳遞捕捉變量var
- [&]:表示引用傳遞捕捉所有所在棧幀的變量(包括this)
- 由多個捕捉項組成,并以逗號分割
eg:
????????[=, &a, &b]:以引用傳遞的方式捕捉變量a和b,值傳遞方式捕捉其他所有變量;
????????[&,a, this]:值傳遞方式捕捉變量a和this,引用方式捕捉其他變量,
參數(parameters):用于傳遞參數,與普通函數的參數列表一致,如果不需要參數傳遞,則可以 連同()一起省略;
mutable:默認情況下,lambda函數總是一個const函數,mutable可以取消其常量性。注意使用該修飾符時,參數列表不可省略(即使參數為空);
返回類型(return-type):用于指定返回值類型,沒有返回值時此部分可省略。返回值類型明確情況下也可省略,由編譯器對返回類型進行推導;
函數體(function-body):用于實現函數的具體邏輯,可以使用捕獲列表的變量也可以使用參數列表的變量,
? ? ? ? 如下圖,fun2就是一個lambda表達式,值傳遞方式捕獲了上文的所有變量,其中b是引用傳遞,傳了一個參數c,返回值是int,這里不寫也沒事,因為編譯器會自動推導,函數體內運算以后返回b,之后調用此lambda表達式,需要傳一個參數,即可得到函數體內的計算結果。
注意:
? ? ? ? ①捕捉列表不允許變量重復傳遞,否則就會導致編譯錯誤,比如:[=, a]:=已經以值傳遞方式捕捉了所有變量,捕捉a重復,就會報錯;
? ? ? ? ②lambda表達式之間不能相互賦值,但可以拷貝構造一個lambda表達式,也可以賦值給相同類型的函數指針,比如
void (*PF)(); int main() {auto f1 = []{cout << "hello world" << endl; };auto f2 = []{cout << "hello world" << endl; };//f1 = f2; //報錯auto f3(f1);PF = f2;return 0; }
包裝器
? ? ? ? 包裝器,也叫適配器,是一種用于以統一的方式調用不同類型函數的抽象概念,本質是一個類模板。在引入lambda表達式之后,有沒有這樣一個問題,有的接口用函數實現,有的用函數對象實現,還有的用lambda表達式實現,萬一有場景需要把這些不同實現方式的接口聚合在一起,該用什么來接收這些接口呢?對!就是使用包裝器去接收,看看它的原型:
template <class T> function; template <class Ret, class... Args> class function<Ret(Args...)>;
其中,Ret: 被調用函數的返回類型,Args…:被調用函數的形參,使用方式我舉個例子,實現計算器的加減乘除功能,注意實現以及調用的細節。
int Add(int a, int b)
{return a + b;
}class Sub
{
public:int operator()(int a, int b){return a-b;}
};class func
{
public:int Div(int a, int b){return a / b;}
};int main()
{function<int(int, int)> ADD = Add; //函數名function<int(int, int)> SUB = Sub(); //函數對象function<int(func, int, int)> DIV = &func::Div; //非靜態成員函數function<int(int, int)> MUL = [](int a, int b) {return a * b; }; //lambda表達式cout << ADD(1, 2) << endl;cout << SUB(1, 2) << endl;cout << DIV(func(), 1, 2) << endl;cout << MUL(1, 2) << endl;return 0;
}
運行:?
bind函數
? ? ? ? bind函數是一個非常強大的函數對象適配器,它可以把一個函數和一些參數綁定起來,形成一個新的函數對象,該函數對象可以像原函數一樣調用,但是它已經部分確定了原函數的參數,同時還可以實現參數順序調整,原型如下:
template <typename F, typename... Args> auto bind(F&& f, Args&&... args);
先看把普通函數和成員函數的一些參數綁定的例子:
int Add(int a, int b)
{return a + b;
}class Func
{
public:int Mul(int a, int b){return a * b;}
};int main()
{//有了兩數相加函數,實現任意數加7的功能auto xPlus7 = bind(Add, placeholders::_1, 7);cout << xPlus7(1) << endl;//有了兩數相乘的成員函數,實現任意數加倍的功能auto increaseDouble = bind(&Func::Mul, Func(), placeholders::_1, 2);cout << increaseDouble(8) << endl;return 0;
}
運行:
再看調整參數的例子:
double Div(int a, int b)
{return (double)a / b;
}int main()
{auto Divide1 = bind(Div, placeholders::_1, placeholders::_2);auto Divide2 = bind(Div, placeholders::_2, placeholders::_1);cout << Divide1(8, 2) << endl;cout << Divide2(8, 2) << endl;return 0;
}
運行:
線程庫
? ? ? ? 學了之后再補充...
后記
? ? ? ? 從以上可以看出,c++11新增的知識點還是特別多的,本文章只是講述了較為重要的一部分,面試時被提問頻率高的一部分,還有一部分沒有提到,比如新增容器(如array),空指針nullptr,有一些大家可能已經熟練于心了,對于文中講過的知識點,其中包括范圍for、右值引用、lambda表達式都是重點中的重點,希望大家能夠真正的看懂并理解,拜拜!