這篇文章以《C++ Primer》(第五版)為基礎(chǔ),結(jié)合自己的理解,將C++11的新特性加以總結(jié)、概括,以加深印象同時(shí)方便自己查閱。
1、long long類型
C++語(yǔ)言規(guī)定,一個(gè)int至少和一個(gè)short一樣大;一個(gè)long至少和一個(gè)int一樣大,一個(gè)long long至少和一個(gè)long一樣大,其中數(shù)據(jù)類型long long是在C++11中新定義的。下表列出了C++標(biāo)準(zhǔn)規(guī)定的尺寸的最小值,同時(shí)允許編譯器賦予這些類型更大的尺寸。

2、列表初始化
C++語(yǔ)言定義了初始化的好幾種不同形式,例如想要定義一個(gè)名為uints_sold的int變量并初始化為0,以下四條語(yǔ)句都可以做到:
int units_sold = 0;
int units_sold = {0}; //C++11新特性
int units_sold{0}; //C++11新特性
int units_sold(0);
int *pia = new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
作為C++11新標(biāo)準(zhǔn)的一部分,用花括號(hào)來(lái)初始化變量得到了全面的應(yīng)用,而在此之前,這種初始化的形式僅在某些受限的場(chǎng)合下才能使用,這種初始化的形式稱為列表初始化。無(wú)論是初始化對(duì)象還是某些時(shí)候?yàn)閷?duì)象賦新值,都可以使用這樣一組由花括號(hào)括起來(lái)的初始值了。
列表初始化還可以應(yīng)用于為vector等容器對(duì)象元素賦初值的方法:
vector<string> articles = {"a", "an", "the"};
list<string> authors = {"Milton", "Shakespeare"};
//關(guān)聯(lián)容器也適用:
set<string> exclude = {"the", "but", "and", "or"};
map<string, string> authors = { {"Joyce", "James"}, {"Austen", "Jane"} };
pair<string, int> word_count();
word_count.insert({str, 1}); //在新標(biāo)準(zhǔn)下,創(chuàng)建一個(gè)pair最簡(jiǎn)方法就是利用列表初始化。
C++11新標(biāo)準(zhǔn)允許使用花括號(hào)括起來(lái)的初始化列表作為賦值語(yǔ)句的右側(cè)運(yùn)算對(duì)象:
vector<int> v;
v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
同時(shí),C++11新標(biāo)準(zhǔn)還規(guī)定,函數(shù)可以返回花括號(hào)包圍的值的列表:
vector<string> process() {
......
return {"functionX", "okay"};
}
pair<string int> process(...) {
......
if(...)
return pair<string, int>(); //隱式構(gòu)造返回值
else
return {str, str.size()}; //列表初始化
}
當(dāng)用于內(nèi)置類型的變量時(shí),這種初始化形式有一個(gè)重要特點(diǎn):如果使用列表初始化且初始值存在丟失信息的風(fēng)險(xiǎn),編譯器將報(bào)錯(cuò):
long double pi = 3.14159265;
int a{pi}, b = {pi}; //錯(cuò)誤:轉(zhuǎn)換未執(zhí)行,因?yàn)榇嬖趤G失信息的危險(xiǎn)
int c(pi), d = (pi); //正確:轉(zhuǎn)化執(zhí)行,且確實(shí)丟失了部分值
使用long double的值初始化int變量可能丟失數(shù)據(jù),所以編譯器拒絕了a和b的初始化請(qǐng)求。其中,至少pi的小數(shù)部分會(huì)丟失,而且int也可能存不下pi的整數(shù)部分。
3、nullptr常量
一下列出幾個(gè)生成空指針的方法:
int *p1 = nullptr;
int *p2 = 0;
int *p3 = NULL;
在過(guò)去的程序中,會(huì)用到一個(gè)名為NULL的預(yù)處理變量來(lái)給指針賦值,這個(gè)變量在頭文件cstdlib中定義,它的值就是0。當(dāng)用到一個(gè)預(yù)處理變量時(shí),預(yù)處理器會(huì)自動(dòng)地將它替換為實(shí)際值,因此用NULL初始化指針和用0初始化指針是一樣的。這種處理方式將會(huì)導(dǎo)致C++中重載特性發(fā)生混亂:
void fun(char *);
void fun(int);
對(duì)于上述兩個(gè)函數(shù)來(lái)說(shuō),因?yàn)镹ULL被定義為0,那么fun(NULL)將會(huì)去調(diào)用void fun(int);,從而違反直觀。為了解決上述問(wèn)題,C++11引入了nullptr關(guān)鍵字,專門用來(lái)區(qū)分NULL和0。nullptr的類型為nullptr_t,能夠隱式的轉(zhuǎn)換為任何指針或成員指針的類型,也能和他們進(jìn)行相等或者不等的比較。
在新標(biāo)準(zhǔn)下,現(xiàn)在的C++程序最好使用nullptr,同時(shí)盡量避免使用NULL。
4、constexpr變量、constexpr函數(shù)及字面值常量類
constexpr變量
C++11新標(biāo)準(zhǔn)規(guī)定,允許將變量聲明為constexpr類型以便由編譯器來(lái)驗(yàn)證常量的值是否是一個(gè)常量表達(dá)式。聲明為constexpr的變量一定是一個(gè)常量,而且必須用常量表達(dá)式初始化:
constexpr int a = 20; //20是常量表達(dá)式
constexpr int b = a + 1; //a + 1是常量表達(dá)式
constexpr int c = fun1(); //只有當(dāng)fun1()是一個(gè)constexpr函數(shù)時(shí),才是一條正確的語(yǔ)句
ps:一個(gè)constexpr指針的初始值必須是nullptr、NULL或者是存儲(chǔ)于某個(gè)固定地址中的對(duì)象。
constexpr函數(shù)
constexpr函數(shù)需要遵循幾項(xiàng)約定:函數(shù)的返回類型及所有形參的類型都得是字面值類型(算術(shù)類型、引用和指針類型、枚舉類型、字面值常量類),且函數(shù)體中必須有且只有一條return語(yǔ)句。
constexpr int fun1() { return 1; }
當(dāng)fun1()的聲明如上時(shí),上文的c的聲明語(yǔ)句正確,因?yàn)榫幾g器能在程序編譯時(shí)驗(yàn)證fun1()函數(shù)返回的是常量表達(dá)式。在執(zhí)行該初始化任務(wù)時(shí),編譯器把對(duì)constexpr函數(shù)的的調(diào)用替換成其結(jié)果值。為了能在編譯過(guò)程中隨時(shí)展開,constexpr函數(shù)被隱式地指定為內(nèi)聯(lián)函數(shù)。
同時(shí),我們?cè)试Sconstexpr函數(shù)的返回值并非一個(gè)常量:
constexpr size_t fun2(int cnt) { return fun1() * cnt; }
當(dāng)fun2()的實(shí)參是常量表達(dá)式時(shí),它的返回值也是常量表達(dá)式,反之則不然:
int arr1[fun(2)]; //正確:fun2(2)是常量表達(dá)式,此時(shí)編譯器用相應(yīng)的結(jié)果替換對(duì)正確:fun2(2)函數(shù)的調(diào)用
int i = 2;
int arr2[fun(i)]; //錯(cuò)誤:fun2(i)不是常量表達(dá)式,編譯出錯(cuò)
ps:constexpr函數(shù)不一定返回常量表達(dá)式。
5、字面值常量類
和其他類不同,字面值類型的類可能含有constexpr函數(shù)成員。這樣的成員必須符合constexpr函數(shù)的所有要求,他們是隱式const的。
數(shù)據(jù)成員都是字面值類型的聚合類(所有成員都是public的;沒(méi)有定義任何構(gòu)造函數(shù);沒(méi)有類內(nèi)初始值;沒(méi)有基類、也沒(méi)有virtual函數(shù))是字面值常量類。如果一個(gè)類不是聚合類,但他符合下述要求,則也是一個(gè)字面值常量類:
- 數(shù)據(jù)成員都必須是字面值類型
- 類內(nèi)至少含有一個(gè)
constexpr構(gòu)造函數(shù) - 如果一個(gè)數(shù)據(jù)成員含有類內(nèi)初始值,則內(nèi)置類型成員的初始值必須是一條常量表達(dá)式;或者如果成員屬于某種類類型,則初始值必須使用成員自己的
constexpr構(gòu)造函數(shù) - 類必須使用析構(gòu)函數(shù)的默認(rèn)定義,該成員負(fù)責(zé)銷毀的對(duì)象
constexpr構(gòu)造函數(shù)
盡管構(gòu)造函數(shù)不能是const的(當(dāng)我們創(chuàng)建類的一個(gè)const對(duì)象時(shí),直到構(gòu)造函數(shù)完成初始化過(guò)程,對(duì)象才能真正取得其“常量”屬性。因此,構(gòu)造函數(shù)在const對(duì)象的構(gòu)造過(guò)程中可以向其寫值),但是字面值常量類的構(gòu)造函數(shù)可以是constexpr函數(shù)。事實(shí)上,一個(gè)字面值常量類必須至少提供一個(gè)constexpr構(gòu)造函數(shù)。
constexpr構(gòu)造函數(shù)可以聲明成=default(下文有詳細(xì)的解釋)的形式。否則,constexpr構(gòu)造函數(shù)就必須既符合構(gòu)造函數(shù)的要求(意味著不能包含返回語(yǔ)句),又符合constexpr函數(shù)的要求(意味著它能擁有的唯一可執(zhí)行語(yǔ)句就是返回語(yǔ)句)。綜合上述兩點(diǎn)可知,constexpr構(gòu)造函數(shù)一般來(lái)說(shuō)應(yīng)該是空的。我們通過(guò)前置constexpr關(guān)鍵字就可以聲明一個(gè)constexpr構(gòu)造函數(shù)了:
class Debug{
private:
bool hw;
bool io;
bool other;
public:
constexpr Debug(bool b = true): hw(b), io(b), other(b) {}
constexpr Debug(bool h, bool i, bool o): hw(h), io(i), other(o) {}
}
constexpr構(gòu)造函數(shù)必須初始化所有數(shù)據(jù)成員,初始值或者使用constexpr構(gòu)造函數(shù),或者是一條常量表達(dá)式。
constexpr構(gòu)造函數(shù)用于生成constexpr對(duì)象以及constexpr函數(shù)的參數(shù)或返回類型。
const適用于變量,并防止它們?cè)诖a中被修改。
constexpr告訴編譯器,這個(gè)表達(dá)式產(chǎn)生一個(gè)編譯時(shí)常量(根據(jù)編譯器的不同行為,常量又分為編譯時(shí)常量和運(yùn)行時(shí)常量,編譯時(shí)常量一定是運(yùn)行時(shí)常量,只是編譯時(shí)常量在編譯的時(shí)候就被計(jì)算執(zhí)行計(jì)算,并帶入到程序中一切可能用到它的計(jì)算式中。),所以它可以用在像數(shù)組長(zhǎng)度,賦值給const變量等等。**
6、類型別名聲明
新標(biāo)準(zhǔn)規(guī)定了一種新的方法,使用別名聲明來(lái)定義類型的別名,這種方法用關(guān)鍵字using作為別名聲明的開始,其后緊跟別名和等號(hào),其作用是把等號(hào)左側(cè)的名字規(guī)定成等號(hào)右側(cè)類型的別名。用法與typedef類似:
typedef double wages; //wages是double的同義詞
wages hourly, weekly; //等價(jià)于double hourly, weekly
using wages = double; //wages是double的同義詞
wages hourly, weekly; //等價(jià)于double hourly, weekly
由于模板不是一個(gè)類型,所以我們不能定義一個(gè)typedef引用一個(gè)模板,但是新標(biāo)準(zhǔn)允許我們?yōu)轭惸0宥x一個(gè)類型別名:
template<typename T> using twin = pair<T, T>;
twin<string> authors; //authors是一個(gè)pair<string, string>
7、auto類型指示符與尾指返回類型
一、auto類型指示符
編程時(shí)常常需要把表達(dá)式的值賦給變量,這就要求在聲明變量的時(shí)候清楚地知道表達(dá)式的類型。C++11引入auto類型說(shuō)明符,用它就能讓編譯器替我們分析表達(dá)式所屬的類型。auto讓編譯器通過(guò)初始值來(lái)推算變量的類型,因此auto定義的變量必須有初始值。
auto item = val1 + val2;
編譯器將根據(jù)val1和val2相加的結(jié)果判斷item的類型。如果val1和val2是類A的對(duì)象,則item的類型就是A;如果這兩個(gè)變量的類型是double,則item的類型就是double,以此類推。
使用auto也能在一條語(yǔ)句中聲明多個(gè)變量。因?yàn)橐粭l聲明語(yǔ)句只能有一個(gè)基本數(shù)據(jù)類型,所以該語(yǔ)句中所有變量的初始基本數(shù)據(jù)類型都必須一樣:
auto i = 0, *p = &i; //正確:i是整數(shù)、p是整型指針
auto sz = 0, pi = 3.14; //錯(cuò)誤:sz和pi的類型不一致
使用auto進(jìn)行類型推導(dǎo)的一個(gè)最為常見(jiàn)而且顯著的例子就是迭代器。在以前我們需要這樣來(lái)書寫一個(gè)迭代器:
for(vector<int>::const_iterator itr = vec.cbegin(); itr != vec.cend(); ++itr)
而有了auto之后可以:
// 由于cbegin()將返回vector<int>::const_iterator,所以itr也應(yīng)該是vector<int>::const_iterator類型
for(auto itr = vec.cbegin(); itr != vec.cend(); ++itr);
注意:auto不能用于函數(shù)傳參,因此下面的做法是無(wú)法通過(guò)編譯的(考慮重載的問(wèn)題,我們應(yīng)該使用模板):
int add(auto x, auto y);
此外,auto還不能用于推導(dǎo)數(shù)組類型。
其次,auto一般會(huì)忽略掉頂層const,底層const會(huì)被保留下來(lái)。
二、尾置返回類型
首先,我們思考一個(gè)問(wèn)題:一個(gè)函數(shù)如何返回?cái)?shù)組指針?
想要得到答案,我們需要明白下面的含義:
int array[10]; //array是一個(gè)含有10個(gè)整數(shù)的數(shù)組
int *p1[10]; //p1是一個(gè)含有10個(gè)指針的數(shù)組
int (*p2)[10] = &array; //p2是一個(gè)指針,它指向含有10個(gè)整數(shù)的數(shù)組
因此,如果我們想定義一個(gè)返回?cái)?shù)組指針的函數(shù),則數(shù)組的維度必須跟在函數(shù)名字之后。返回?cái)?shù)組指針的函數(shù)形式如下所示:
Type (*function(parameter_list)) [dimension]
//具體例子:
int (*func(int i)) [10];
在C++11中,我們可以使用尾置返回類型來(lái)簡(jiǎn)化上述聲明方法。任何函數(shù)的定義都能使用尾置返回類型,但這種形式對(duì)于返回類型比較復(fù)雜的函數(shù)最有效。尾置返回類型跟在形參列表后面并以一個(gè)->符號(hào)開頭。為了表示函數(shù)真正的返回類型跟在形參列表之后,我們?cè)诒緫?yīng)該出現(xiàn)的地方放置一個(gè)auto:
//上述示例可以改寫為:
auto fun(int i) -> int (*) [10];
同時(shí),上述方法還可以通過(guò)decltype關(guān)鍵字進(jìn)行改寫:
int odd[] = {1, 3, 5, 7, 9};
int even[] = {0, 2, 4, 6, 8};
decltype(odd) *arrPtr(int i) {
return (i % 2) ? &odd : &even;
}
因?yàn)?code>odd是數(shù)組,所有arrPtr返回一個(gè)指向含有5個(gè)整數(shù)的數(shù)組的指針。decltype并不負(fù)責(zé)把數(shù)組類型轉(zhuǎn)換成對(duì)應(yīng)的指針,所以decltype的結(jié)果是個(gè)數(shù)組,想要表示arrPtr返回指針還必須在函數(shù)聲明時(shí)加一個(gè)*。具體decltype細(xì)節(jié)將在下面說(shuō)明。
8、decltype類型指示符
C++11新標(biāo)準(zhǔn)引入了第二種類型說(shuō)明賦decltype,他的作用是選擇并返回操作數(shù)的數(shù)據(jù)類型。在此過(guò)程中,編譯器分析表達(dá)式并得到它的類型,卻不實(shí)際計(jì)算表達(dá)式的值:
decltype(fun()) sum = x; //sum的類型就是函數(shù)f的返回類型
編譯器并不實(shí)際調(diào)用函數(shù)fun(),而是使用當(dāng)調(diào)用發(fā)生時(shí)f的返回值類型作為sum。
const int ci = 0, &cj = ci;
decltype(ci) x = 0; //x的類型是const int
decltype(cj) y = x; //y的類型是const int &,y綁定到變量x
decltype(cj) z; //錯(cuò)誤:z是一個(gè)引用,必須初始化
你可能會(huì)思考,auto 能不能用于推導(dǎo)函數(shù)的返回類型。考慮這樣一個(gè)例子加法函數(shù)的例子,在傳統(tǒng) C++ 中我們必須這么寫:
template<typename R, typename T, typename U>
R add(T x, U y) {
return x+y;
}
這樣的代碼其實(shí)變得很丑陋,因?yàn)槌绦騿T在使用這個(gè)模板函數(shù)的時(shí)候,必須明確指出返回類型。但事實(shí)上我們并不知道add()這個(gè)函數(shù)會(huì)做什么樣的操作,獲得一個(gè)什么樣的返回類型。
在C++11中這個(gè)問(wèn)題得到解決。雖然你可能馬上回反應(yīng)出來(lái)使用decltype推導(dǎo)x+y的類型,寫出這樣的代碼:
decltype(x+y) add(T x, U y);
但事實(shí)上這樣的寫法并不能通過(guò)編譯。這是因?yàn)樵诰幾g器讀到decltype(x+y)時(shí),x和y尚未被定義。為了解決這個(gè)問(wèn)題,C++11還引入了一個(gè)叫做尾置返回類型(trailing return type),利用auto關(guān)鍵字將返回類型后置:
template<typename T, typename U>
auto add(T x, U y) -> decltype(x+y) {
return x+y;
}
從C++14開始是可以直接讓普通函數(shù)具備返回值推導(dǎo),因此下面的寫法變得合法:
template<typename T, typename U>
auto add(T x, U y) {
return x+y;
}
decltype和auto都可以用來(lái)推斷類型,但是二者有幾處明顯的差異:
- 1.
auto忽略頂層const,decltype保留頂層const; - 2.對(duì)引用操作,
auto推斷出原有類型,decltype推斷出引用; - 3.對(duì)解引用操作,
auto推斷出原有類型,decltype推斷出引用; - 4.
auto推斷時(shí)會(huì)實(shí)際執(zhí)行,decltype不會(huì)執(zhí)行,只做分析。
總之在使用中過(guò)程中和const、引用和指針結(jié)合時(shí)需要特別小心。
9、使用auto或decltype推斷string::size_type類型
對(duì)于string類的size函數(shù)來(lái)說(shuō),返回一個(gè)int或是一個(gè)unsigned都是合情理的,但是size函數(shù)實(shí)際上返回的是一個(gè)string::size_type類型的的值以體現(xiàn)標(biāo)準(zhǔn)庫(kù)類型與機(jī)器無(wú)關(guān)的特性。
盡管不太清楚string::size_type類型的細(xì)節(jié),但是有一點(diǎn)可以肯定,它是一個(gè)無(wú)符號(hào)類型的值,而且能足夠放下任何string對(duì)象的大小。
在C++11新標(biāo)準(zhǔn)中,允許編譯器通過(guò)auto或decltype來(lái)推斷變量的類型:
auto len_1 = line.size();
decltype(line.szie()) len_2 = line.size();
ps:由于size函數(shù)返回的是一個(gè)無(wú)符號(hào)整型,因此切記,如果表達(dá)式中混用了有符號(hào)數(shù)和無(wú)符號(hào)數(shù)將可能產(chǎn)生意想不到的結(jié)果。例如,假設(shè)n是一個(gè)具有負(fù)值的int,則表達(dá)式s.size() < n的判斷結(jié)果幾乎肯定是true。這是因?yàn)樨?fù)值n會(huì)自動(dòng)轉(zhuǎn)化成一個(gè)比較大的無(wú)符號(hào)值。
10、類內(nèi)初始化
C++11新標(biāo)準(zhǔn)規(guī)定,可以為數(shù)據(jù)成員提供一個(gè) 類內(nèi)初始值 。創(chuàng)建對(duì)象時(shí),類內(nèi)初始值將用于初始化數(shù)據(jù)成員。沒(méi)有初始值的成員將被默認(rèn)初始化。
class Window_mgr {
private:
//默認(rèn)情況下,一個(gè)Window_mgr包含一個(gè)標(biāo)準(zhǔn)尺寸的空白Screen
std::vector<Screen> screens{Screen(24, 80, "")};
};
如我們之前所知的,類內(nèi)初始值必須使用=的初始化形或者花括號(hào)括起來(lái)的直接初始化形式。
ps:類內(nèi)初始化對(duì)struct和class關(guān)鍵字都適用。實(shí)際上struct關(guān)鍵字和class關(guān)鍵字僅僅是形式上有所不同,我們可以用這個(gè)兩個(gè)關(guān)鍵字中的任何一個(gè)定義一個(gè)類,唯一的區(qū)別是,他們的默認(rèn)訪問(wèn)權(quán)限不太一樣。
11、范圍for語(yǔ)句
C++11新標(biāo)準(zhǔn)提供了一種語(yǔ)句:范圍for(range for)語(yǔ)句。這種語(yǔ)句遍歷給定序列中的每個(gè)元素并對(duì)序列中的每個(gè)值執(zhí)行某種操作,其語(yǔ)法是:
for(declaration: expression)
statement
其中,expression部分是一個(gè)對(duì)象,用于表示一個(gè)序列。declaration部分負(fù)責(zé)定義一個(gè)變量,該變量將被用于訪問(wèn)序列中的基礎(chǔ)元素。每次迭代,declaration部分的變量會(huì)被初始化為expression部分的下一個(gè)元素值:
string str("hello world");
for(auto c : str) //使用auto關(guān)鍵字讓編譯器來(lái)決定c的類型,這里是char類型
cout << c << endl;
如果想要改變string對(duì)象中字符的值,必須把循環(huán)變量定義成引用類型,以把整個(gè)string對(duì)象轉(zhuǎn)換成大寫為例:
string str("hello world");
for (auto &c : str)
c = toupper(c);
cout << str << endl;
此外范圍for語(yǔ)句還適用于容器遍歷:
vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
for (auto &r : v)
r *= 2; //將v中每個(gè)元素的值翻倍
注意:使用new語(yǔ)句得到的動(dòng)態(tài)數(shù)組不能使用范圍for語(yǔ)句,因?yàn)閯?dòng)態(tài)分配的內(nèi)存不是一個(gè)數(shù)組類型(維度是數(shù)組類型的一部分,而我們通過(guò)new得到的只是數(shù)組指針)。
12、定義vector對(duì)象的vector(向量的向量)
早期版本的C++標(biāo)準(zhǔn)中,如果vector的元素還是vector(或者其他模板類型),則其定義的形式與現(xiàn)在的C++11新標(biāo)準(zhǔn)略有不同。過(guò)去,必須在外層vector對(duì)象的右尖括號(hào)和其元素類型之間添加一個(gè)空格,如應(yīng)該寫成vector<vector<int> >而非vector<vector<int>>。
13、容器的cbegin和cend函數(shù)
begin和end返回的具體類型由對(duì)象是否為常量決定的,如果對(duì)象是常量,begin和end返回cosnt_iterator;如果不是常量,則返回iterator:
vector<int> v;
const vector<int> cv;
auto it1 = v.begin(); //it1的類型是vector<int>::iterator
auto it2 = cv.begin(); //it2的類型是vector<int>::const_iterator
有時(shí)候這種默認(rèn)的行為并非我們所需要的,如果對(duì)象只需要讀操作而無(wú)需寫操作的話最好使用常量類型。為了便于專門得到const_iterator類型的返回值,C++11新標(biāo)準(zhǔn)引入了cbegin和cend兩個(gè)新函數(shù):
auto it3 = v.cbegin(); //it3的類型也是vector<int>::const_iterator
14、數(shù)組的begin和end函數(shù)
為了讓數(shù)組指針的使用更加簡(jiǎn)單、更加安全,C++11新標(biāo)準(zhǔn)引入了兩個(gè)名為begin和end的函數(shù),這兩個(gè)函數(shù)與容器中的同名函數(shù)功能類似,不過(guò)數(shù)組畢竟不是類類型,因此這兩個(gè)函數(shù)不是成員函數(shù):
int ia[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
int *beg = begin(ia); //指向ia首元素的指針
int *last = end(ia); //指向ia尾元素的下一個(gè)位置的指針
這兩個(gè)函數(shù)定義在iterator頭文件中。下面程序負(fù)責(zé)找到arr中的第一個(gè)負(fù)數(shù):
int *p_beg = begin(arr), *p_end = end(arr);
while(p_beg != p_end && *p_beg >= 0)
p_beg++;
return {"functionX", "okay"};
注意:使用new語(yǔ)句得到的動(dòng)態(tài)數(shù)組不能使用begin()或end()語(yǔ)句,因?yàn)閯?dòng)態(tài)分配的內(nèi)存不是一個(gè)數(shù)組類型(維度是數(shù)組類型的一部分,而我們通過(guò)new得到的只是數(shù)組指針)。
15、除法的舍入規(guī)則
在除法運(yùn)算中,如果兩個(gè)運(yùn)算對(duì)象的符號(hào)相同則商為正(如果不為0的話),否則商為負(fù)。C++語(yǔ)言的早期版本允許結(jié)果為負(fù)值的商向上或向下取整,C++11新標(biāo)準(zhǔn)規(guī)定商一律向0取整(直接切除小數(shù)部分)。
16、標(biāo)準(zhǔn)庫(kù)initializer_list類和可變參數(shù)模板
為了編寫能處理不同數(shù)量實(shí)參的函數(shù),C++11新標(biāo)準(zhǔn)提供了兩種新方法:
一、如果所有的實(shí)參類型相同,可以傳遞一個(gè)名為initializer_list的標(biāo)準(zhǔn)庫(kù)類型,該類型是一種標(biāo)準(zhǔn)庫(kù)類型,用于表示某種特定類型的值的數(shù)組,其定義在同名的頭文件中,提供以下操作:
initializer_list<T> lst; //默認(rèn)初始化,T類型元素的空列表
initializer_list<T> lst{a, b, c...} //lst的元素和初始值一樣多;lst的元素是對(duì)應(yīng)初始值的副本;列表中的元素是const的
lst2(lst) //拷貝復(fù)制,拷貝后原始列表和副本共享元素
lst2 = lst //拷貝復(fù)制,拷貝后原始列表和副本共享元素
lst.size()
lst.begin()
lst.end()
和vecotr一樣,initializer_list也是一種模板類型,定義時(shí)必須說(shuō)明模板的類型。和vector不一樣的是initializer_list對(duì)象中的元素永遠(yuǎn)是常量值,我們無(wú)法改變initializer_list對(duì)象中的元素值。用法如下:
void error_msg(ErrCode e, initializer_list<string> il) {
......
}
error_msg(ErrCode(41), {"functionX", "okay"});
二、如果實(shí)參的類型不同,我們可以編寫一種特殊的函數(shù),也就是所謂的可變參數(shù)模板。
一個(gè)可變參數(shù)模板就是一個(gè)接受可變數(shù)目參數(shù)的模板函數(shù)或模板類??勺償?shù)目的參數(shù)被稱為參數(shù)包。存在兩種參數(shù)包:模板參數(shù)包,表示零個(gè)或多個(gè)模板參數(shù);函數(shù)參數(shù)包,表示零個(gè)或多個(gè)函數(shù)參數(shù)。
我們用一個(gè)省略號(hào)來(lái)支出一個(gè)模板參數(shù)或函數(shù)參數(shù)表示一個(gè)包。在一個(gè)模板參數(shù)列表中,class...或typename...支出接下來(lái)的參數(shù)表示零個(gè)或多個(gè)類型的列表;一個(gè)類型名后面跟一個(gè)省略號(hào)表示零個(gè)或多個(gè)給定類型的非類型參數(shù)的列表。在函數(shù)參數(shù)列表中,如果一個(gè)參數(shù)的類型是一個(gè)模板參數(shù)包,則此參數(shù)也是一個(gè)函數(shù)參數(shù)包:
//Args是一個(gè)模板參數(shù)包;rest是一個(gè)函數(shù)參數(shù)包
//Args表示零個(gè)或多個(gè)模板類型參數(shù)
//rest表示零個(gè)或多個(gè)函數(shù)參數(shù)
template <typename T, typename... Args>
void foo(const T &t, const Args... rest);
編譯器從函數(shù)的實(shí)參推斷模板參數(shù)類型。對(duì)于一個(gè)可變參數(shù)模板,編譯器還會(huì)推斷包中參數(shù)的數(shù)目:
int i = 0; double d = 3.14; string s = "hello world";
foo(i, s ,42, d); //包中有個(gè)3參數(shù),實(shí)例化為:void foo(const int&, const string&, const int&, const double&);
foo(s, 42, "hi"); //包中有個(gè)2參數(shù),實(shí)例化為:void foo(const string&, const int&, const char[3]&);
foo(d, s); //包中有個(gè)1參數(shù),實(shí)例化為:void foo(const double&, const string&);
foo("hi"); //包中有個(gè)0參數(shù),實(shí)例化為:void foo(const char[3]&);
可變參數(shù)函數(shù)通常是遞歸的。第一把調(diào)用處理包中的第一個(gè)實(shí)參,然后用剩余實(shí)參調(diào)用自身。
//用來(lái)終止遞歸并打印最后一個(gè)元素的函數(shù)
//此函數(shù)必須在可變參數(shù)版本的print定義之前聲明
template<typename T>
ostream &print(ostream &os, const T &t) {
return os << t; //包中最后一個(gè)元素之后不打印分隔符
}
//包中處理最后一個(gè)元素之外的其他元素都會(huì)調(diào)用這個(gè)版本的print
template<typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args&... rest) {
os << t << ","; //打印第一個(gè)實(shí)參
return print(os, rest...); //遞歸調(diào)用,打印其他實(shí)參
}
給定print(cout, i, s, 42);遞歸會(huì)執(zhí)行如下:
1、調(diào)用print(cout, i ,s, 42);t = i;rest... = s, 42
2、調(diào)用print(cout, s, 42);t = s;rest... = 42
3、調(diào)用print(cout, 42),非可變參數(shù)版本的print
17、= default和 = delete的用法
= default
對(duì)于一個(gè)類來(lái)說(shuō):
class Sales_data {
Sales_data() = default;
};
因?yàn)樵摌?gòu)造函數(shù)不接受任何實(shí)參,所以它是一個(gè)默認(rèn)構(gòu)造函數(shù),定義這個(gè)函數(shù)的目的是因?yàn)槲覀兗刃枰渌问降臉?gòu)造函數(shù),也需要默認(rèn)的構(gòu)造函數(shù)。如果我們需要默認(rèn)的行為,那么可以通過(guò)在參數(shù)列表后面寫上= default來(lái)要求編譯器生成構(gòu)造函數(shù),使用這種方式生成的構(gòu)造函數(shù)比用戶定義的默認(rèn)構(gòu)造函數(shù)具有更高的效率。= default既可以和聲明一起出現(xiàn)在類的內(nèi)部,也可以作為定義出現(xiàn)在類的外部,出現(xiàn)在內(nèi)部時(shí)會(huì)被默認(rèn)為內(nèi)聯(lián)的。
= delete
在新標(biāo)準(zhǔn)下,我們可以通過(guò)將拷貝構(gòu)造函數(shù)和拷貝賦值運(yùn)算符定義為刪除的函數(shù)來(lái)阻止拷貝。刪除的函數(shù)是這樣一種函數(shù):我們雖然聲明了他們,但不能以任何方式使用他們。在函數(shù)的參數(shù)列表后面加上= delete來(lái)指出我們希望將它定義為刪除的:
class NoCopy {
NoCopy() = default; //默認(rèn)構(gòu)造函數(shù)
NoCopy(const NoCopy &) = delete; //阻止拷貝
NoCopy &operator=(const NoCopy &) = delete; //阻止賦值
~NoCopy() = default; //合成的析構(gòu)函數(shù)
}
與= default不同,= delete必須出現(xiàn)在函數(shù)第一次聲明的時(shí)候,這個(gè)差異與這些聲明的含義在邏輯上是吻合的。一個(gè)默認(rèn)的成員只影響為這個(gè)成員而生成的代碼,因此= default直到編譯器生成代碼時(shí)才需要。而另一方面,編譯器需要知道一個(gè)函數(shù)是刪除的,以便禁止師徒使用它的操作,
與= default的另一個(gè)不同之處是,我們可以對(duì)任何函數(shù)指定= default(我們只能對(duì)編譯器可以合成的默認(rèn)構(gòu)造函數(shù)或拷貝控制成員使用= default)。雖然刪除函數(shù)的主要用途是禁止拷貝控制成員,但當(dāng)我們希望引導(dǎo)函數(shù)匹配過(guò)程時(shí),刪除函數(shù)有時(shí)也是有用的。但是謹(jǐn)記,我們不能刪除析構(gòu)函數(shù)。
18、委托構(gòu)造函數(shù)
C++11新標(biāo)準(zhǔn)擴(kuò)展了構(gòu)造函數(shù)初始值的功能,使我們可以定義所謂的委托構(gòu)造函數(shù)。一個(gè)委托構(gòu)造函數(shù)使用它所屬類的其他構(gòu)造函數(shù)執(zhí)行自己的初始化過(guò)程,或者說(shuō)它把它自己的一些(或全部)職責(zé)委托給了其他構(gòu)造函數(shù):
class Sales_data {
public:
//非委托構(gòu)造函數(shù)使用對(duì)應(yīng)的實(shí)參初始化成員
Sales_data(std::string s, unsigned cnt, double price):
bookNo(s), units_sold(cnt), revenue(cnt * price) {}
//其余構(gòu)造函數(shù)全部委托給另一個(gè)構(gòu)造函數(shù)
Sales_data(): Sales_data("", 0, 0) {}
Sales_data(std::string s): Sales_data(s, 0, 0) {}
Sales_data(std::istream &is): Sales_data() { read(is, *this); }
......
};
19、用string對(duì)象處理文件流對(duì)象
ifstream in(ifile);
ofstream out;
在新的C++11標(biāo)準(zhǔn)中,文件名既可以是庫(kù)類型string對(duì)象,也可以是C風(fēng)格的字符數(shù)組。舊版本的標(biāo)準(zhǔn)庫(kù)只允許C風(fēng)格的字符數(shù)組。
20、array和forward_list容器
array和forward_list是C++新標(biāo)準(zhǔn)增加的類型。與內(nèi)置數(shù)組相比,array是一種更安全、更容易使用的數(shù)組類型。與內(nèi)置數(shù)組類似,array對(duì)象的大小是固定的。因此,array不支持添加和刪除元素以及改變?nèi)萜鞔笮〉牟僮鳌?code>forward_list的設(shè)計(jì)目標(biāo)是達(dá)到與最好的手寫的單向鏈表數(shù)據(jù)結(jié)構(gòu)相當(dāng)?shù)男阅?。因此沒(méi)有size操作,因?yàn)楸4婊蛴?jì)算其大小就會(huì)比手寫鏈表多出額外的開銷。對(duì)其他容器而言,size保證是一個(gè)快速的常量時(shí)間的操作。
ps:新標(biāo)準(zhǔn)庫(kù)容器比舊版本快的多。新標(biāo)準(zhǔn)庫(kù)容器的性能幾乎肯定與最精心優(yōu)化過(guò)的同類數(shù)據(jù)結(jié)構(gòu)一樣好(通常會(huì)更好)?,F(xiàn)代C++程序應(yīng)該使用標(biāo)準(zhǔn)庫(kù)容器,而不是更原始的數(shù)據(jù)結(jié)構(gòu),如內(nèi)置數(shù)組。通常使用vector是最好的選擇,除非你有很好的理由選擇其他容器。
21、容器的非成員函數(shù)swap
swap操作交換兩個(gè)相同類型的容器內(nèi)容。調(diào)用swap之后,兩個(gè)容器中的元素將會(huì)交換:
vecotr<string> s1(10); //10個(gè)元素
vecotr<string> s2(24); //25個(gè)元素
swap(s1, s2);
調(diào)用swap后,s1將有24個(gè)元素,而s2將有10個(gè)元素。處array外,交換兩個(gè)容器內(nèi)容的操作抱枕會(huì)很快————元素本身并未交換,swap只是交換了兩個(gè)容器的內(nèi)部數(shù)據(jù)結(jié)構(gòu)。
ps:除array外,swap不對(duì)任何元素進(jìn)行拷貝、刪除或插入操作,因此可以保證在常數(shù)時(shí)間內(nèi)完成。
在新標(biāo)準(zhǔn)中,容器既提供了成員函數(shù)版本的swap,也提供了非成員函數(shù)版本的swap。非成員版本的swap在泛型變成中是非常重要的。統(tǒng)一使用非成員版本的swap是一個(gè)好習(xí)慣。
22、容器的insert成員返回類型
在新標(biāo)準(zhǔn)中,接受元素個(gè)數(shù)或范圍的insert版本返回指向第一個(gè)新加入元素的迭代器。在舊標(biāo)準(zhǔn)中,這些操作返回void。如果范圍為空,不插入任何元素,了insert操作會(huì)將第一個(gè)參數(shù)返回。
23、容器的emplace成員
C++新標(biāo)準(zhǔn)引入了三個(gè)新成員————emplace_front、emplace、emplace_back,這些操作構(gòu)造元素而不是拷貝元素,分別對(duì)應(yīng)與push_ront、push、push_back。當(dāng)我們調(diào)用一個(gè)emplace成員函數(shù)時(shí),則是將參數(shù)傳遞給元素類型的構(gòu)造函數(shù)。emplace成員使用這些參數(shù)在容器管理的內(nèi)存空間中直接構(gòu)造元素:
//在c的末尾構(gòu)造一個(gè)Sales_data對(duì)象
c.emplace_back("987-0590353403", 25, 15.99); //正確:將會(huì)自動(dòng)調(diào)用構(gòu)造函數(shù)
c.push_back("987-0590353403", 25, 15.99); //錯(cuò)誤:需要顯示調(diào)用構(gòu)造函數(shù)生成一個(gè)對(duì)象再添加元素
c.push_back(Sales_data("987-0590353403", 25, 15.99));//正確:創(chuàng)建一個(gè)零食的Sales_data對(duì)象傳遞給push_back
24、管理容器的成員函數(shù)
//shrink_to_fit只適用于vector、string和deque
//capacity和reserve只適用于vector和string
c.shrink_to_fit(); //請(qǐng)求將capacity()減小為與size()相同大小
c.capacity(); //不重新分配內(nèi)存的話,c可以保存多少元素
c.reserve(); //分配至少能容納n個(gè)元素的內(nèi)存空間
在新標(biāo)準(zhǔn)中,我們可以調(diào)用shrink_to_fit來(lái)要求特定容器退回不需要的內(nèi)存空間。此函數(shù)支出我們不再需要任何多余的內(nèi)存空間。但是,具體的實(shí)現(xiàn)可以選擇忽略次請(qǐng)求。也就是說(shuō),調(diào)用shrink_to_fit也并不一定保證退回內(nèi)存空間。
25、string的數(shù)值轉(zhuǎn)換
新標(biāo)準(zhǔn)引入了多個(gè)函數(shù),可以實(shí)現(xiàn)數(shù)值數(shù)據(jù)與標(biāo)準(zhǔn)庫(kù)string之間的轉(zhuǎn)換:
int i =42;
string s = to_string(i);
double d = stod(s);
要轉(zhuǎn)換為數(shù)值的string中第一個(gè)非空白符必須是數(shù)值中可能出現(xiàn)的字符:
string s2 = "pi = 3.14159265358";
d = stod(s2.substr(s2.find_first_of("+-.0123456789)));
string和數(shù)值之間的轉(zhuǎn)換有以下函數(shù):
to_string(val); //一組重載函數(shù),返回val的string表示,val可以是任何算數(shù)類型
stoi(s, p, b); //int
stol(s, p, b); //long
stoul(s, p, b); //unsigned long
stoll(s, p, b); //long long
stoull(s, p, b); //unsigned long long
stof(s, p); //float
stod(s, p); //double
stold(s, p); //long double
ps:如果string不能轉(zhuǎn)換一個(gè)數(shù)值,這些函數(shù)會(huì)拋出一個(gè)invalid_argument異常。
26、lambda表達(dá)式
匿名函數(shù)有函數(shù)體,但沒(méi)有函數(shù)名。匿名函數(shù)是很多高級(jí)語(yǔ)言都支持的概念,如lisp語(yǔ)言在1958年首先采用匿名函數(shù)。正因?yàn)槿绱?,C++11也同樣引入了lambda函數(shù)。在C++11中,你可以在源碼中內(nèi)聯(lián)一個(gè)lambda函數(shù),這就使得創(chuàng)建快速的、一次性的函數(shù)變得簡(jiǎn)單了。
相同類似功能我們也可以使用函數(shù)對(duì)象或者函數(shù)指針實(shí)現(xiàn):函數(shù)對(duì)象能維護(hù)狀態(tài),但語(yǔ)法開銷大,而函數(shù)指針語(yǔ)法開銷小,卻沒(méi)法保存范圍內(nèi)的狀態(tài)。lambda表達(dá)式正是結(jié)合了兩者的優(yōu)點(diǎn)。
聲明Lambda表達(dá)式
[capture list] (params list) mutable exception-> return type { function body };
[capture list] (params list) -> return type {function body};
[capture list] (params list) {function body};
[capture list] {function body};
capture list:捕獲外部變量列表
params list:形參列表
mutable指示符:用來(lái)說(shuō)用是否可以修改捕獲的變量
exception:異常設(shè)定
return type:返回類型
function body:函數(shù)體
簡(jiǎn)單的例子
在C++中我們對(duì)STL庫(kù)中的sort()運(yùn)用十分頻繁,接下來(lái)就是關(guān)于他的一個(gè)例子:
bool compare(int a,int b){
return a<b;
}
{
vector<int> vec{1,0,9,5,3,3,7,8,2};
sort(vec.begin(),vec.end(),compare);
//在C++11之前,我們使用STL的sort函數(shù),可以提供一個(gè)謂詞函數(shù)來(lái)為sort改變其排序判斷標(biāo)準(zhǔn)
}
接下來(lái)是lambda表達(dá)式的形式:
{
vector<int> vec{1,0,9,5,3,3,7,8,2};
sort(lbvec.begin(), lbvec.end(), [](int a, int b) -> bool { return a < b; }); // Lambda表達(dá)式
}
lambda不僅提高了代碼可讀性,且在例子中,這一個(gè)提供判斷依據(jù)的函數(shù)是只需調(diào)用一次,在這時(shí),lambda表達(dá)式就顯示出它的“即用即扔”的特點(diǎn),很適合這種不需要重復(fù)調(diào)用且運(yùn)用區(qū)域單一的情景(而不是去多寫一個(gè)compare的函數(shù))。當(dāng)然我們也可以定義一個(gè)可調(diào)用對(duì)象用來(lái)代表lambda表達(dá)式
{
vector<int> vec{1,0,9,5,3,3,7,8,2};
auto f = [](int a, int b) -> bool { return a < b; };
sort(lbvec.begin(), lbvec.end(), f);
}
捕獲外部變量
Lambda表達(dá)式可以捕獲外面變量,但需要我們提供一個(gè)謂詞函數(shù)([capture list]在聲明表達(dá)式最前)。類似參數(shù)傳遞方式:值傳遞、引入傳遞、指針傳遞。在Lambda表達(dá)式中,外部變量捕獲方式也類似:值捕獲、引用捕獲、隱式捕獲。
值捕獲
int a = 123;
auto f = [a] { cout << a << endl; };
f(); // 輸出:123
a = 321;
f(); // 輸出:123
值捕獲和參數(shù)傳遞中的值傳遞類似,被捕獲的值在Lambda表達(dá)式創(chuàng)建時(shí)通過(guò)值拷貝的方式傳入,因此Lambda表達(dá)式函數(shù)體中不能修改該外部變量的值;同樣,函數(shù)體外對(duì)于值的修改也不會(huì)改變被捕獲的值。
引用捕獲
當(dāng)以引用的方式捕獲一個(gè)變量時(shí),必須保證在lambda執(zhí)行時(shí)變量是存在的。
int a = 123;
auto f = [&a] { cout << a << endl; };
a = 321;
f(); // 輸出:321
引用捕獲的變量使用的實(shí)際上就是該引用所綁定的對(duì)象,因此引用對(duì)象的改變會(huì)改變函數(shù)體內(nèi)對(duì)該對(duì)象的引用的值。
隱式捕獲
除了顯式列出我們希望使用的來(lái)自所在函數(shù)的變量之外,還可以讓編譯器根據(jù)lambda體中的代碼來(lái)推斷我們要使用那些變量。隱式捕獲有兩種方式,分別是
[=]:以值補(bǔ)獲的方式捕獲外部所有變量
[&]:表示以引用捕獲的方式捕獲外部所有變量。
int a = 123 ,b=321;
auto df = [=] { cout << a << b << endl; }; // 值捕獲
auto rf = [&] { cout << a << b << endl; }; // 引用捕獲
其他捕獲方式
-
[ ]: 不捕獲任何變量(無(wú)參函數(shù)) -
[變量1,&變量2, …]: 值(引用)形式捕獲指定的多個(gè)外部變量 -
[this]: 值捕獲this指針 -
[=, &x]: 變量x以引用形式捕獲,其余變量以傳值形式捕獲
Lambda表達(dá)式的參數(shù) - 參數(shù)列表中不能有默認(rèn)參數(shù)
- 不支持可變參數(shù)
- 所有參數(shù)必須有參數(shù)名
參考文獻(xiàn):https://blog.csdn.net/qq_43265890/article/details/83218413**
27、標(biāo)準(zhǔn)庫(kù)bind函數(shù)
對(duì)于捕獲局部變量的lambda表達(dá)式來(lái)說(shuō),很容易滿足謂詞的要求(無(wú)論一元謂詞還是二元謂詞),但是如果想用函數(shù)的形式,解決謂詞長(zhǎng)度的問(wèn)題,我們就需要用到bind標(biāo)準(zhǔn)庫(kù)函數(shù),它定義在functional中。可以將bind函數(shù)看成一個(gè)通用的函數(shù)適配器,它接受一個(gè)可調(diào)用對(duì)象,生成一個(gè)新的可調(diào)用對(duì)象來(lái)適應(yīng)原對(duì)象的參數(shù)列表。調(diào)用bind的一般形式為:
auto newCallable = bind(callable, arg_list);
其中,newCallable本身是一個(gè)可調(diào)用對(duì)象,arg_list是一個(gè)逗號(hào)分隔的參數(shù)列表,對(duì)應(yīng)給定的callable的參數(shù)。即,當(dāng)我們調(diào)用newCallable時(shí),newCallable會(huì)調(diào)用callable并傳遞給它arg_list中的參數(shù)。
arg_list中的參數(shù)可能包括形如_n的名字,其中n是一個(gè)整數(shù)。這些參數(shù)是占位符,表示newCallable的參數(shù),他們占據(jù)了傳遞給newCallable的參數(shù)的“位置”。數(shù)值n表示生成的可調(diào)用對(duì)象中參數(shù)的位置:_1為newCallable的第一個(gè)參數(shù),_2為第二個(gè)參數(shù),以此類推:
auto check = bind(check_size, _1, 6);
bool b = check(s); //check(s)會(huì)調(diào)用check_size(s, 6);
//我們可以將原來(lái)基于lambda表達(dá)式的find_if調(diào)用替換為check_size的bind版本:
auto wc = find_if(words.begin(), words.end(), [sz](const string &a) {...});
auto wc = find_if(words.begin(), words.end(), bind(check_size, _1, sz));
名字_n都定義在一個(gè)名為placeholders的命名空間中,而這個(gè)命名空間本身定義在std命名空間中,因此對(duì)_1對(duì)應(yīng)的using聲明為:using std::placeholders::_1;或者使用using namespace std::placeholders;
28、無(wú)序容器
新標(biāo)準(zhǔn)定義了4個(gè)無(wú)序關(guān)聯(lián)容器(unordered associative container)。這些容器不是使用比較運(yùn)算符來(lái)組織元素,而是使用哈希函數(shù)和關(guān)鍵字類型的==運(yùn)算符。
29、智能指針
下一篇博客有詳細(xì)的說(shuō)明,這里不在贅述。
30、右值引用
在看《C++ Primer》中有關(guān)右值方面的問(wèn)題時(shí),對(duì)右值的應(yīng)用場(chǎng)景不是很理解,所以在網(wǎng)上找了下有關(guān)右值的文章,其中一篇文章《4行代碼看看右值引用》覺(jué)得寫得不錯(cuò)。本節(jié)以下內(nèi)容是原文,轉(zhuǎn)載于https://www.cnblogs.com/qicosmos/p/4283455.html。
概述
右值引用的概念有些讀者可能會(huì)感到陌生,其實(shí)他和C++98/03中的左值引用有些類似,例如,c++98/03中的左值引用是這樣的:
int i = 0;
int& j = i;
這里的int&是對(duì)左值進(jìn)行綁定(但是int&卻不能綁定右值),相應(yīng)的,對(duì)右值進(jìn)行綁定的引用就是右值引用,他的語(yǔ)法是這樣的A&&,通過(guò)雙引號(hào)來(lái)表示綁定類型為A的右值。通過(guò)&&我們就可以很方便的綁定右值了,比如我們可以這樣綁定一個(gè)右值:
int&& i = 0;
這里我們綁定了一個(gè)右值0,關(guān)于右值的概念會(huì)在后面介紹。右值引用是C++11中新增加的一個(gè)很重要的特性,他主是要用來(lái)解決C++98/03中遇到的兩個(gè)問(wèn)題,第一個(gè)問(wèn)題就是臨時(shí)對(duì)象非必要的昂貴的拷貝操作,第二個(gè)問(wèn)題是在模板函數(shù)中如何按照參數(shù)的實(shí)際類型進(jìn)行轉(zhuǎn)發(fā)。通過(guò)引入右值引用,很好的解決了這兩個(gè)問(wèn)題,改進(jìn)了程序性能,后面將會(huì)詳細(xì)介紹右值引用是如何解決這兩個(gè)問(wèn)題的。
和右值引用相關(guān)的概念比較多,比如:右值、純右值、將亡值、universal references、引用折疊、移動(dòng)語(yǔ)義、move語(yǔ)義和完美轉(zhuǎn)發(fā)等等。很多都是新概念,對(duì)于剛學(xué)習(xí)C++11右值引用的初學(xué)者來(lái)說(shuō),可能會(huì)覺(jué)得右值引用過(guò)于復(fù)雜,概念之間的關(guān)系難以理清。
右值引用實(shí)際上并沒(méi)有那么復(fù)雜,其實(shí)是關(guān)于4行代碼的故事,通過(guò)簡(jiǎn)單的4行代碼我們就能清晰的理解右值引用相關(guān)的概念了。本文希望帶領(lǐng)讀者通過(guò)4行代碼來(lái)理解右值引用相關(guān)的概念,理清他們之間的關(guān)系,并最終能透徹地掌握C++11的新特性--右值引用。
四行代碼的故事
第一行代碼的故事
int i = getVar();
上面的這行代碼很簡(jiǎn)單,從getVar()函數(shù)獲取一個(gè)整形值,然而,這行代碼會(huì)產(chǎn)生幾種類型的值呢?答案是會(huì)產(chǎn)生兩種類型的值,一種是左值i,一種是函數(shù)getVar()返回的臨時(shí)值,這個(gè)臨時(shí)值在表達(dá)式結(jié)束后就銷毀了,而左值i在表達(dá)式結(jié)束后仍然存在,這個(gè)臨時(shí)值就是右值,具體來(lái)說(shuō)是一個(gè)純右值,右值是不具名的。區(qū)分左值和右值的一個(gè)簡(jiǎn)單辦法是:看能不能對(duì)表達(dá)式取地址,如果能,則為左值,否則為右值。
所有的具名變量或?qū)ο蠖际亲笾担涿兞縿t是右值,比如,簡(jiǎn)單的賦值語(yǔ)句:
int i = 0;
在這條語(yǔ)句中,i 是左值,0 是字面量,就是右值。在上面的代碼中,i 可以被引用,0 就不可以了。具體來(lái)說(shuō)上面的表達(dá)式中等號(hào)右邊的0是純右值(prvalue),在C++11中所有的值必屬于左值、將亡值、純右值三者之一。比如,非引用返回的臨時(shí)變量、運(yùn)算表達(dá)式產(chǎn)生的臨時(shí)變量、原始字面量和lambda表達(dá)式等都是純右值。而將亡值是C++11新增的、與右值引用相關(guān)的表達(dá)式,比如,將要被移動(dòng)的對(duì)象、T&&函數(shù)返回值、std::move返回值和轉(zhuǎn)換為T&&的類型的轉(zhuǎn)換函數(shù)的返回值等。關(guān)于將亡值我們會(huì)在后面介紹,先看下面的代碼:
int j = 5;
auto f = []{return 5;};
上面的代碼中5是一個(gè)原始字面量, []{return 5;}是一個(gè)lambda表達(dá)式,都是屬于純右值,他們的特點(diǎn)是在表達(dá)式結(jié)束之后就銷毀了。
通過(guò)地行代碼我們對(duì)右值有了一個(gè)初步的認(rèn)識(shí),知道了什么是右值,接下來(lái)再來(lái)看看第行代碼。
第二行代碼的故事
T&& k = getVar();
第二行代碼和第一行代碼很像,只是相比第一行代碼多了“&&”,他就是右值引用,我們知道左值引用是對(duì)左值的引用,那么,對(duì)應(yīng)的,對(duì)右值的引用就是右值引用,而且右值是匿名變量,我們也只能通過(guò)引用的方式來(lái)獲取右值。雖然第二行代碼和第一行代碼看起來(lái)差別不大,但是實(shí)際上語(yǔ)義的差別很大,這里,getVar()產(chǎn)生的臨時(shí)值不會(huì)像第一行代碼那樣,在表達(dá)式結(jié)束之后就銷毀了,而是會(huì)被“續(xù)命”,他的生命周期將會(huì)通過(guò)右值引用得以延續(xù),和變量k的聲明周期一樣長(zhǎng)。
右值引用的第一個(gè)特點(diǎn)
通過(guò)右值引用的聲明,右值又“重獲新生”,其生命周期與右值引用類型變量的生命周期一樣長(zhǎng),只要該變量還活著,該右值臨時(shí)量將會(huì)一直存活下去。讓我們通過(guò)一個(gè)簡(jiǎn)單的例子來(lái)看看右值的生命周期。如下代碼所示。
#include <iostream>
using namespace std;
int g_constructCount=0;
int g_copyConstructCount=0;
int g_destructCount=0;
struct A
{
A(){
cout<<"construct: "<<++g_constructCount<<endl;
}
A(const A& a)
{
cout<<"copy construct: "<<++g_copyConstructCount <<endl;
}
~A()
{
cout<<"destruct: "<<++g_destructCount<<endl;
}
};
A GetA()
{
return A();
}
int main() {
A a = GetA();
return 0;
}
為了清楚的觀察臨時(shí)值,在編譯時(shí)設(shè)置編譯選項(xiàng)-fno-elide-constructors用來(lái)關(guān)閉返回值優(yōu)化效果。
輸出結(jié)果:
construct: 1
copy construct: 1
destruct: 1
copy construct: 2
destruct: 2
destruct: 3
從上面的例子中可以看到,在沒(méi)有返回值優(yōu)化的情況下,拷貝構(gòu)造函數(shù)調(diào)用了兩次,一次是GetA()函數(shù)內(nèi)部創(chuàng)建的對(duì)象返回出來(lái)構(gòu)造一個(gè)臨時(shí)對(duì)象產(chǎn)生的,另一次是在main函數(shù)中構(gòu)造a對(duì)象產(chǎn)生的。第二次的destruct是因?yàn)榕R時(shí)對(duì)象在構(gòu)造a對(duì)象之后就銷毀了。如果開啟返回值優(yōu)化的話,輸出結(jié)果將是:
construct: 1
destruct: 1
可以看到返回值優(yōu)化將會(huì)把臨時(shí)對(duì)象優(yōu)化掉,但這不是c++標(biāo)準(zhǔn),是各編譯器的優(yōu)化規(guī)則。我們?cè)诨氐街疤岬降目梢酝ㄟ^(guò)右值引用來(lái)延長(zhǎng)臨時(shí)右值的生命周期,如果上面的代碼中我們通過(guò)右值引用來(lái)綁定函數(shù)返回值的話,結(jié)果又會(huì)是什么樣的呢?在編譯時(shí)設(shè)置編譯選項(xiàng)-fno-elide-constructors。
int main() {
A&& a = GetA();
return 0;
}
輸出結(jié)果:
construct: 1
copy construct: 1
destruct: 1
destruct: 2
通過(guò)右值引用,比之前少了一次拷貝構(gòu)造和一次析構(gòu),原因在于右值引用綁定了右值,讓臨時(shí)右值的生命周期延長(zhǎng)了。我們可以利用這個(gè)特點(diǎn)做一些性能優(yōu)化,即避免臨時(shí)對(duì)象的拷貝構(gòu)造和析構(gòu),事實(shí)上,在c++98/03中,通過(guò)常量左值引用也經(jīng)常用來(lái)做性能優(yōu)化。上面的代碼改成:
const A& a = GetA();
輸出的結(jié)果和右值引用一樣,因?yàn)槌A孔笾狄檬且粋€(gè)“萬(wàn)能”的引用類型,可以接受左值、右值、常量左值和常量右值。需要注意的是普通的左值引用不能接受右值,比如這樣的寫法是不對(duì)的:
A& a = GetA();
上面的代碼會(huì)報(bào)一個(gè)編譯錯(cuò)誤,因?yàn)榉浅A孔笾狄弥荒芙邮茏笾怠?br>
右值引用的第二個(gè)特點(diǎn)
右值引用獨(dú)立于左值和右值。意思是右值引用類型的變量可能是左值也可能是右值。比如下面的例子:
int&& var1 = 1;
var1類型為右值引用,但var1本身是左值,因?yàn)榫呙兞慷际亲笾怠?br> 關(guān)于右值引用一個(gè)有意思的問(wèn)題是:T&&是什么,一定是右值嗎?讓我們來(lái)看看下面的例子:
template<typename T>
void f(T&& t){}
f(10); //t是右值
int x = 10;
f(x); //t是左值
從上面的代碼中可以看到,T&&表示的值類型不確定,可能是左值又可能是右值,這一點(diǎn)看起來(lái)有點(diǎn)奇怪,這就是右值引用的一個(gè)特點(diǎn)。
右值引用的第三個(gè)特點(diǎn)
T&& t在發(fā)生自動(dòng)類型推斷的時(shí)候,它是未定的引用類型(universal references),如果被一個(gè)左值初始化,它就是一個(gè)左值;如果它被一個(gè)右值初始化,它就是一個(gè)右值,它是左值還是右值取決于它的初始化。
我們?cè)倩剡^(guò)頭看上面的代碼,對(duì)于函數(shù)template<typename T>void f(T&& t),當(dāng)參數(shù)為右值10的時(shí)候,根據(jù)universal references的特點(diǎn),t被一個(gè)右值初始化,那么t就是右值;當(dāng)參數(shù)為左值x時(shí),t被一個(gè)左值引用初始化,那么t就是一個(gè)左值。需要注意的是,僅僅是當(dāng)發(fā)生自動(dòng)類型推導(dǎo)(如函數(shù)模板的類型自動(dòng)推導(dǎo),或auto關(guān)鍵字)的時(shí)候,T&&才是universal references。再看看下面的例子:
template<typename T>
void f(T&& param);
template<typename T>
class Test {
Test(Test&& rhs);
};
上面的例子中,param是universal reference,rhs是Test&&右值引用,因?yàn)槟0婧瘮?shù)f發(fā)生了類型推斷,而Test&&并沒(méi)有發(fā)生類型推導(dǎo),因?yàn)門est&&是確定的類型了。
正是因?yàn)橛抑狄每赡苁亲笾狄部赡苁怯抑担蕾囉诔跏蓟?,并不是一下子就確定的特點(diǎn),我們可以利用這一點(diǎn)做很多文章,比如后面要介紹的移動(dòng)語(yǔ)義和完美轉(zhuǎn)發(fā)。
這里再提一下引用折疊,正是因?yàn)橐肓擞抑狄茫钥赡艽嬖谧笾狄门c右值引用和右值引用與右值引用的折疊,C++11確定了引用折疊的規(guī)則,規(guī)則是這樣的:
- 所有的右值引用疊加到右值引用上仍然還是一個(gè)右值引用;
- 所有的其他引用類型之間的疊加都將變成左值引用。
第三行代碼的故事
T(T&& a) : m_val(val){ a.m_val=nullptr; }
這行代碼實(shí)際上來(lái)自于一個(gè)類的構(gòu)造函數(shù),構(gòu)造函數(shù)的一個(gè)參數(shù)是一個(gè)右值引用,為什么將右值引用作為構(gòu)造函數(shù)的參數(shù)呢?在解答這個(gè)問(wèn)題之前我們先看一個(gè)例子。如下代碼所示。
class A
{
public:
A():m_ptr(new int(0)){cout << "construct" << endl;}
A(const A& a):m_ptr(new int(*a.m_ptr)) //深拷貝的拷貝構(gòu)造函數(shù)
{
cout << "copy construct" << endl;
}
~A(){ delete m_ptr;}
private:
int* m_ptr;
};
int main() {
A a = GetA();
return 0;
}
輸出:
construct
copy construct
copy construct
這個(gè)例子很簡(jiǎn)單,一個(gè)帶有堆內(nèi)存的類,必須提供一個(gè)深拷貝拷貝構(gòu)造函數(shù),因?yàn)槟J(rèn)的拷貝構(gòu)造函數(shù)是淺拷貝,會(huì)發(fā)生“指針懸掛”的問(wèn)題。如果不提供深拷貝的拷貝構(gòu)造函數(shù),上面的測(cè)試代碼將會(huì)發(fā)生錯(cuò)誤(編譯選項(xiàng)-fno-elide-constructors),內(nèi)部的m_ptr將會(huì)被刪除兩次,一次是臨時(shí)右值析構(gòu)的時(shí)候刪除一次,第二次外面構(gòu)造的a對(duì)象釋放時(shí)刪除一次,而這兩個(gè)對(duì)象的m_ptr是同一個(gè)指針,這就是所謂的指針懸掛問(wèn)題。提供深拷貝的拷貝構(gòu)造函數(shù)雖然可以保證正確,但是在有些時(shí)候會(huì)造成額外的性能損耗,因?yàn)橛袝r(shí)候這種深拷貝是不必要的。比如下面的代碼:

上面代碼中的GetA函數(shù)會(huì)返回臨時(shí)變量,然后通過(guò)這個(gè)臨時(shí)變量拷貝構(gòu)造了一個(gè)新的對(duì)象a,臨時(shí)變量在拷貝構(gòu)造完成之后就銷毀了,如果堆內(nèi)存很大的話,那么,這個(gè)拷貝構(gòu)造的代價(jià)會(huì)很大,帶來(lái)了額外的性能損失。每次都會(huì)產(chǎn)生臨時(shí)變量并造成額外的性能損失,有沒(méi)有辦法避免臨時(shí)變量造成的性能損失呢?答案是肯定的,C++11已經(jīng)有了解決方法,看看下面的代碼。如下代碼所示。
class A
{
public:
A() :m_ptr(new int(0)){}
A(const A& a):m_ptr(new int(*a.m_ptr)) //深拷貝的拷貝構(gòu)造函數(shù)
{
cout << "copy construct" << endl;
}
A(A&& a) :m_ptr(a.m_ptr)
{
a.m_ptr = nullptr;
cout << "move construct" << endl;
}
~A(){ delete m_ptr;}
private:
int* m_ptr;
};
int main(){
A a = Get(false);
}
//輸出:
construct
move construct
move construct
代碼3和2相比只多了一個(gè)構(gòu)造函數(shù),輸出結(jié)果表明,并沒(méi)有調(diào)用拷貝構(gòu)造函數(shù),只調(diào)用了move construct函數(shù),讓我們來(lái)看看這個(gè)move construct函數(shù):
A(A&& a) :m_ptr(a.m_ptr)
{
a.m_ptr = nullptr;
cout << "move construct" << endl;
}
這個(gè)構(gòu)造函數(shù)并沒(méi)有做深拷貝,僅僅是將指針的所有者轉(zhuǎn)移到了另外一個(gè)對(duì)象,同時(shí),將參數(shù)對(duì)象a的指針置為空,這里僅僅是做了淺拷貝,因此,這個(gè)構(gòu)造函數(shù)避免了臨時(shí)變量的深拷貝問(wèn)題。
上面這個(gè)函數(shù)其實(shí)就是移動(dòng)構(gòu)造函數(shù),他的參數(shù)是一個(gè)右值引用類型,這里的A&&表示右值,為什么?前面已經(jīng)提到,這里沒(méi)有發(fā)生類型推斷,是確定的右值引用類型。為什么會(huì)匹配到這個(gè)構(gòu)造函數(shù)?因?yàn)檫@個(gè)構(gòu)造函數(shù)只能接受右值參數(shù),而函數(shù)返回值是右值,所以就會(huì)匹配到這個(gè)構(gòu)造函數(shù)。這里的A&&可以看作是臨時(shí)值的標(biāo)識(shí),對(duì)于臨時(shí)值我們僅僅需要做淺拷貝即可,無(wú)需再做深拷貝,從而解決了前面提到的臨時(shí)變量拷貝構(gòu)造產(chǎn)生的性能損失的問(wèn)題。這就是所謂的移動(dòng)語(yǔ)義,右值引用的一個(gè)重要作用是用來(lái)支持移動(dòng)語(yǔ)義的。
需要注意的一個(gè)細(xì)節(jié)是,我們提供移動(dòng)構(gòu)造函數(shù)的同時(shí)也會(huì)提供一個(gè)拷貝構(gòu)造函數(shù),以防止移動(dòng)不成功的時(shí)候還能拷貝構(gòu)造,使我們的代碼更安全。
我們知道移動(dòng)語(yǔ)義是通過(guò)右值引用來(lái)匹配臨時(shí)值的,那么,普通的左值是否也能借助移動(dòng)語(yǔ)義來(lái)優(yōu)化性能呢,那該怎么做呢?事實(shí)上C++11為了解決這個(gè)問(wèn)題,提供了std::move方法來(lái)將左值轉(zhuǎn)換為右值,從而方便應(yīng)用移動(dòng)語(yǔ)義。move是將對(duì)象資源的所有權(quán)從一個(gè)對(duì)象轉(zhuǎn)移到另一個(gè)對(duì)象,只是轉(zhuǎn)移,沒(méi)有內(nèi)存的拷貝,這就是所謂的move語(yǔ)義。如下圖所示是深拷貝和move的區(qū)別。

再看看下面的例子:
{
std::list< std::string> tokens;
//省略初始化...
std::list< std::string> t = tokens; //這里存在拷貝
}
std::list< std::string> tokens;
std::list< std::string> t = std::move(tokens); //這里沒(méi)有拷貝
如果不用std::move,拷貝的代價(jià)很大,性能較低。使用move幾乎沒(méi)有任何代價(jià),只是轉(zhuǎn)換了資源的所有權(quán)。他實(shí)際上將左值變成右值引用,然后應(yīng)用移動(dòng)語(yǔ)義,調(diào)用移動(dòng)構(gòu)造函數(shù),就避免了拷貝,提高了程序性能。如果一個(gè)對(duì)象內(nèi)部有較大的對(duì)內(nèi)存或者動(dòng)態(tài)數(shù)組時(shí),很有必要寫move語(yǔ)義的拷貝構(gòu)造函數(shù)和賦值函數(shù),避免無(wú)謂的深拷貝,以提高性能。事實(shí)上,C++11中所有的容器都實(shí)現(xiàn)了移動(dòng)語(yǔ)義,方便我們做性能優(yōu)化。
這里也要注意對(duì)move語(yǔ)義的誤解,move實(shí)際上它并不能移動(dòng)任何東西,它唯一的功能是將一個(gè)左值強(qiáng)制轉(zhuǎn)換為一個(gè)右值引用。如果是一些基本類型比如int和char[10]定長(zhǎng)數(shù)組等類型,使用move的話仍然會(huì)發(fā)生拷貝(因?yàn)闆](méi)有對(duì)應(yīng)的移動(dòng)構(gòu)造函數(shù))。所以,move對(duì)于含資源(堆內(nèi)存或句柄)的對(duì)象來(lái)說(shuō)更有意義。
第4行代碼故事
template <typename T>void f(T&& val){ foo(std::forward<T>(val)); }
C++11之前調(diào)用模板函數(shù)時(shí),存在一個(gè)比較頭疼的問(wèn)題,如何正確的傳遞參數(shù)。比如:
template <typename T>
void forwardValue(T& val)
{
processValue(val); //右值參數(shù)會(huì)變成左值
}
template <typename T>
void forwardValue(const T& val)
{
processValue(val); //參數(shù)都變成常量左值引用了
}
都不能按照參數(shù)的本來(lái)的類型進(jìn)行轉(zhuǎn)發(fā)。
C++11引入了完美轉(zhuǎn)發(fā):在函數(shù)模板中,完全依照模板的參數(shù)的類型(即保持參數(shù)的左值、右值特征),將參數(shù)傳遞給函數(shù)模板中調(diào)用的另外一個(gè)函數(shù)。C++11中的std::forward正是做這個(gè)事情的,他會(huì)按照參數(shù)的實(shí)際類型進(jìn)行轉(zhuǎn)發(fā)??聪旅娴睦樱?/p>
void processValue(int& a){ cout << "lvalue" << endl; }
void processValue(int&& a){ cout << "rvalue" << endl; }
template <typename T>
void forwardValue(T&& val)
{
processValue(std::forward<T>(val)); //照參數(shù)本來(lái)的類型進(jìn)行轉(zhuǎn)發(fā)。
}
void Testdelcl()
{
int i = 0;
forwardValue(i); //傳入左值
forwardValue(0);//傳入右值
}
輸出:
lvaue
rvalue
右值引用T&&是一個(gè)universal references,可以接受左值或者右值,正是這個(gè)特性讓他適合作為一個(gè)參數(shù)的路由,然后再通過(guò)std::forward按照參數(shù)的實(shí)際類型去匹配對(duì)應(yīng)的重載函數(shù),最終實(shí)現(xiàn)完美轉(zhuǎn)發(fā)。
我們可以結(jié)合完美轉(zhuǎn)發(fā)和移動(dòng)語(yǔ)義來(lái)實(shí)現(xiàn)一個(gè)泛型的工廠函數(shù),這個(gè)工廠函數(shù)可以創(chuàng)建所有類型的對(duì)象。具體實(shí)現(xiàn)如下:
template<typename… Args>
T* Instance(Args&&… args)
{
return new T(std::forward<Args >(args)…);
}
這個(gè)工廠函數(shù)的參數(shù)是右值引用類型,內(nèi)部使用std::forward按照參數(shù)的實(shí)際類型進(jìn)行轉(zhuǎn)發(fā),如果參數(shù)的實(shí)際類型是右值,那么創(chuàng)建的時(shí)候會(huì)自動(dòng)匹配移動(dòng)構(gòu)造,如果是左值則會(huì)匹配拷貝構(gòu)造。
總結(jié)
通過(guò)4行代碼我們知道了什么是右值和右值引用,以及右值引用的一些特點(diǎn),利用這些特點(diǎn)我們才方便實(shí)現(xiàn)移動(dòng)語(yǔ)義和完美轉(zhuǎn)發(fā)。C++11正是通過(guò)引入右值引用來(lái)優(yōu)化性能,具體來(lái)說(shuō)是通過(guò)移動(dòng)語(yǔ)義來(lái)避免無(wú)謂拷貝的問(wèn)題,通過(guò)move語(yǔ)義來(lái)將臨時(shí)生成的左值中的資源無(wú)代價(jià)的轉(zhuǎn)移到另外一個(gè)對(duì)象中去,通過(guò)完美轉(zhuǎn)發(fā)來(lái)解決不能按照參數(shù)實(shí)際類型來(lái)轉(zhuǎn)發(fā)的問(wèn)題(同時(shí),完美轉(zhuǎn)發(fā)獲得的一個(gè)好處是可以實(shí)現(xiàn)移動(dòng)語(yǔ)義)。
本節(jié)以上內(nèi)容轉(zhuǎn)載自https://www.cnblogs.com/qicosmos/p/4283455.html
31、可調(diào)用對(duì)象與function
C++語(yǔ)言中有幾種可調(diào)用對(duì)象:函數(shù)、函數(shù)指針、lambda表達(dá)式、bind創(chuàng)建的對(duì)象以及重載了函數(shù)調(diào)用運(yùn)算符的類。不同類型的可調(diào)用對(duì)象可共享同一種調(diào)用形式。調(diào)用形式指明了調(diào)用返回的類型以及傳遞給調(diào)用的實(shí)參類型??紤]下列不同類型的可調(diào)用對(duì)象:
int add(int i, int j) { return i + j; } //普通函數(shù)
auto mod = [](int i, int j) { return i % j; } //lambda表達(dá)式
class device { //函數(shù)對(duì)象類
int operator()(int denominator, int divisor) {
return denominator / divisor;
}
}
上面這些可調(diào)用對(duì)象分別對(duì)其參數(shù)執(zhí)行了不同的算術(shù)運(yùn)算,盡管他們類型各不相同,但是共享同一種調(diào)用形式:
int (int, int)
假設(shè)我們需要使用這些可調(diào)用對(duì)象構(gòu)建一個(gè)簡(jiǎn)單的桌面計(jì)算器。為實(shí)現(xiàn)這個(gè)目的,我們可以使用map定義一個(gè)函數(shù)表用于存儲(chǔ)并指向這些可調(diào)用對(duì)象的“指針”。當(dāng)程序需要執(zhí)行某個(gè)特定和操作時(shí),從表中查找該調(diào)用的函數(shù)。
//構(gòu)建從運(yùn)算符到函數(shù)指針的映射關(guān)系,其中函數(shù)接受兩個(gè)int,返回一個(gè)int
map<string, int(*)(int, int)> binops;
//{"+", add}是一個(gè)pair
binops.insert({"+", add}); //正確:add是一個(gè)指向正確類型的函數(shù)指針
binops.insert({"%", mod}); //錯(cuò)誤:mod不是一個(gè)函數(shù)指針
問(wèn)題在于mod是個(gè)lambda表達(dá)式,而每個(gè)lambda表達(dá)式有它自己的類型,該類型與存儲(chǔ)在binops中的值的類型不匹配。
我們可以使用一個(gè)名為function的新標(biāo)準(zhǔn)庫(kù)類型解決上述問(wèn)題。function是一個(gè)模板:
function<int(int, int)>
在這里我們聲明了一個(gè)function類型,它表示可以接受兩個(gè)int,返回一個(gè)int的可調(diào)用對(duì)象。因此,我們可以用這個(gè)新聲明的類型表示任意一種桌面計(jì)算器用到的類型:
function<int(int, int)> f1 = add;
function<int(int, int)> f2 = divide();
function<int(int, int)> f3 = [](int i, int j){ return i * j; };
//重新定義map
map<string, function<int(int, int)>> binops = {
{"+", add},
{"-", std::minus<int>()},
{"/", divide()},
{"*", [](int i, int j){ return i * j; }},
{"%", mod}
};
binops["+"](10, 5); //調(diào)用add(10, 5)
binops["-"](10, 5); //調(diào)用minus<int>對(duì)象的調(diào)用運(yùn)算符
binops["/"](10, 5); //調(diào)用divide對(duì)象的調(diào)用運(yùn)算符
binops["*"](10, 5); //調(diào)用lambda函數(shù)對(duì)象
binops["%"](10, 5); //調(diào)用lambda函數(shù)對(duì)象
但是對(duì)于重載函數(shù):
int add(int i, int j) { return i + j; }
Sales_data add(const Sales_data&, const Sales_data&);
map<string, function<int(int, int)>> binops;
binops.insert({"+", add}); //錯(cuò)誤
解決上述二義性問(wèn)題的途徑是存儲(chǔ)函數(shù)的指針而非函數(shù)的名字:
int (*fp)(int, int) = add;
binops.insert({"+", fp});
32、顯式的類型轉(zhuǎn)換運(yùn)算符
為了防止隱式類型轉(zhuǎn)換自動(dòng)發(fā)生而產(chǎn)生意想不到的結(jié)果,C++11引入了顯式的類型轉(zhuǎn)換運(yùn)算符:
class SamllInt {
public:
//編譯器不會(huì)自動(dòng)執(zhí)行這一類型的轉(zhuǎn)換
explicit operator int() const { return val; }
}
33、虛函數(shù)的override指示符
C++11新標(biāo)注允許派生類顯式地注明它使用某個(gè)成員函數(shù)覆蓋了它繼承的虛函數(shù)。具體做飯是在形參列表后面、或在const成員函數(shù)的const關(guān)鍵詞后面、或在引用成員函數(shù)的引用限定符后面添加一個(gè)關(guān)鍵字override。
這么做的好處是在使得程序員的意圖更加清晰的同時(shí)讓編譯器為我們發(fā)現(xiàn)一些錯(cuò)誤。
class B {
virtual void f1(int) const;
virtual void f2();
void f3();
}
clss D : B {
void f1(int) const override; //正確:與基類中的匹配
void f2(int) override; //錯(cuò)誤:B中沒(méi)有匹配的函數(shù)
void f3() override; //錯(cuò)誤:f3不是虛函數(shù)
void f4() override; //錯(cuò)誤:B中沒(méi)有f4
}
我們使用override所表達(dá)的意思是我們希望能覆蓋基類中的虛函數(shù)而實(shí)際上并未做到,所以編譯器會(huì)保存。
34、通過(guò)final阻止繼承
為實(shí)現(xiàn)一個(gè)類不被其他類繼承,C++11提供了final關(guān)鍵字阻止繼承的發(fā)生:
class NoDerived fianl { /* */ }; //此類不看作為基類
我們還可以把某個(gè)函數(shù)指定為final,如果我們以及把函數(shù)定義為final了,則之后的任何嘗試覆蓋該函數(shù)的操作都將引發(fā)錯(cuò)誤。
class B {
void f1(int) const fianl;
}
class D : B {
void f1(int) const; //錯(cuò)誤:B中的f1定義為final
}
35、繼承的構(gòu)造函數(shù)
C++11新標(biāo)準(zhǔn)中,派生類能夠重用其直接基類定義的構(gòu)造函數(shù),方法是提供一條注明了(直接)基類名的using聲明語(yǔ)句:
class Bulk_quote : public Disc_quote {
using Disc_quote::Disc_quote; //繼承Disc_quote的構(gòu)造函數(shù)
}
通常情況下,using語(yǔ)句只在當(dāng)前作用域可見(jiàn),但是當(dāng)用于構(gòu)造函數(shù)時(shí),using則會(huì)使編譯器產(chǎn)生代碼。對(duì)于基類的每個(gè)構(gòu)造函數(shù),編譯器都在派生類中生成一個(gè)形參列表完全相同的構(gòu)造函數(shù)。繼承的構(gòu)造函數(shù)等價(jià)于:
Bulk_quote(const std::string& book, double price, std::size_t qty, double disc) : Disc_quote(book, price, qty, disc) { }
36、tuple類型
當(dāng)我們希望將一些數(shù)據(jù)組合成單一對(duì)象,但又不想麻煩地定義一個(gè)新數(shù)據(jù)結(jié)構(gòu)來(lái)表示這些數(shù)據(jù)時(shí),tuple是非常有用的。tuple類型及器伴隨類型和函數(shù)都定義在tuple頭文件中。
不同tuple類型的成員類型也不相同,但是一個(gè)tuple可以有任意數(shù)量的成員。每個(gè)確定的tuple類型的成員數(shù)目是固定的,但一個(gè)tuple類型的成員數(shù)目可以與另一個(gè)tuple類型不同。
定義和初始化tuple:
tuple<size_t, size_t, size_t> threeD; //三個(gè)成員都設(shè)置為0
tuple<string, vector<double>, int, list<int>> someVal("constants", {3.14, 2.78}, 42, {0, 1, 2, 3, 4});
使用tuple的默認(rèn)構(gòu)造函數(shù),會(huì)對(duì)每個(gè)成員進(jìn)行值初始化;也可以為每個(gè)成員提供一個(gè)初始值。tuple的這個(gè)構(gòu)造函數(shù)也是explicit的,因此我們必須使用直接初始化語(yǔ)法:
tuple<size_t, size_t, size_t> threeD = {1, 2, 3}; //錯(cuò)誤
tuple<size_t, size_t, size_t> threeD{1, 2, 3}; //正確
標(biāo)準(zhǔn)庫(kù)定義了make_tuple函數(shù),我們還可以用它來(lái)生成tuple對(duì)象:
auto item = make_tuple("hello world", 1, 2.0);
//item的類型為tuple<const char*, int, double>
要訪問(wèn)一個(gè)tuple成員,就要使用一個(gè)名為get的標(biāo)準(zhǔn)庫(kù)函數(shù)模板。為了使用get,我們必須指定一個(gè)顯示模板實(shí)參,它支出我們想要訪問(wèn)第幾個(gè)成員。我們傳遞給get一個(gè)tuple對(duì)象,它返回指定成員的引用:
auto book = get<0>(item); //返回第一個(gè)成員
auto cnt = get<1>(item); //返回第二個(gè)成員
auto price = get<2>(item) / cnt; //返回最后一個(gè)成員
get<2>(item) *= 0.8; //打折20%
尖括號(hào)中的值必須是一個(gè)整形常量表達(dá)式。與往常一樣,我們從0開始計(jì)數(shù),意味著get<0>是第一個(gè)成員。
如果不知道一個(gè)tuple準(zhǔn)確的類型細(xì)節(jié)信息,可以用兩個(gè)輔助類模板來(lái)查詢tuple成員的數(shù)量和類型:
typedef decltype(item) trans; //trans是item的類型
//返回trans類型對(duì)象中成員的數(shù)量
size_t sz = tuple_size<trans>::value;
//cnt的類型與item中第二個(gè)成員相同
tuple_element<1, trans>::type cnt = get<1>(item)
為了使用tuple_size或tuple_element,我們需要知道一個(gè)tuple對(duì)象的類型。tuple的關(guān)系和相等運(yùn)算符的行為類似容器的對(duì)應(yīng)操作。這些運(yùn)算符逐對(duì)比較左側(cè)tuple和右側(cè)tuple的成員。只有兩個(gè)tuple具有相同數(shù)量的成員時(shí),我們才可以比較他們。
tuple最常見(jiàn)用途是從一個(gè)函數(shù)返回多個(gè)值。
37、bitset類型
標(biāo)準(zhǔn)庫(kù)還定義了bitset類,使得位運(yùn)算的使用更為容易,并且能夠處理超過(guò)最長(zhǎng)整形類型大小的位集合。bitset類定義在頭文件bitset中。當(dāng)我們定義一個(gè)bitset時(shí),需要聲明它包含多少個(gè)二進(jìn)制位:
bitset<32> bitvec(1U); //32位;低位為1,其他位為0
大小必須是一個(gè)常量表達(dá)式。bitset中的二進(jìn)制位也是未命名的,我們通過(guò)位置來(lái)訪問(wèn)它們。二進(jìn)制位尾指是從0開始編號(hào)的。因此,bitvec包含編號(hào)從0到31的32個(gè)二進(jìn)制位。編號(hào)從0開始的二進(jìn)制位被稱為低位,編號(hào)到31結(jié)束的二進(jìn)制位被稱為高位:
//bitvec1比初始值小;初始值中的高位被丟棄
bitset<13> bitvec1(0xbeef); //二進(jìn)制位序列為1111011101111
//bitvec2比初始值大;它的高位被置為0
bitset<20> bitvec2(0xbeef); //二進(jìn)制位序列為00001011111011101111
//在64位機(jī)器中,long long 0ULL是64個(gè)0比特,因此~0ULL是64個(gè)1
bitset<128> bitvec3(~0ULL); //0~63位為1;63~127位為0
bitset<32> bitvec4("1100"); //2、3兩位為1,剩余兩位為0
38、正則表達(dá)式
regex類表示一個(gè)正則表達(dá)式。除了初始化和賦值操作之外,regex還支持regex_match、regex_repalce、regex_search、sregex_iterator等操作。C++正則表達(dá)式庫(kù)定義在頭文件regex中。
39、隨機(jī)數(shù)引擎和隨機(jī)數(shù)分布類
C和C++都依賴與一個(gè)簡(jiǎn)單的C庫(kù)函數(shù)rand來(lái)生成隨機(jī)數(shù)。此函數(shù)生成均勻分布的偽隨機(jī)整數(shù),每個(gè)隨機(jī)數(shù)的范圍在0和一個(gè)系統(tǒng)相關(guān)的最大值之間。
rand函數(shù)有一些問(wèn)題:一些程序需要不同范圍的隨機(jī)數(shù)。一些應(yīng)用需要隨機(jī)浮點(diǎn)數(shù)。一些程序需要非均勻分布的數(shù)。而程序員為了解決這些問(wèn)題而師團(tuán)轉(zhuǎn)換rand生成的隨機(jī)數(shù)的范圍、類型或分布時(shí),常常會(huì)引入非隨機(jī)性。
定義在頭文件random中的隨機(jī)數(shù)庫(kù)通過(guò)一組協(xié)作的類來(lái)解決這些問(wèn)題:隨機(jī)數(shù)引擎類和隨機(jī)數(shù)分布類。一個(gè)引擎類可以生成unsigned隨機(jī)數(shù)序列,一個(gè)分布類使用一個(gè)引擎類生成指定類型的、在給定范圍內(nèi)的、服從特定概率分布的隨機(jī)數(shù)。
40、浮點(diǎn)數(shù)格式控制
操縱符scientific改變流的狀態(tài)來(lái)使用科學(xué)計(jì)數(shù)法。操縱符fixed改變流的狀態(tài)來(lái)使用定點(diǎn)十進(jìn)制。在新標(biāo)準(zhǔn)庫(kù)中,通過(guò)使用hexfloat也可以強(qiáng)制浮點(diǎn)數(shù)使用十六進(jìn)制格式。新標(biāo)準(zhǔn)庫(kù)還提供另一個(gè)名為defaultfloat的操縱符,它將劉恢復(fù)到默認(rèn)狀態(tài)——根據(jù)要打印的值選擇計(jì)數(shù)法。
cout << "default format:" << 100 * sqrt(2.0) << '\n'
<< "scientific:" << scientific << 100 * sqrt(2.0) << '\n'
<< "fixed decimal:" << fixed << 100 * sqrt(2.0) << '\n'
<< "hexadecimal:" << hexfloat << 100 * sqrt(2.0) << '\n'
<< "yse defaults:" << defaultfloat << 100 * sqrt(2.0) << '\n'
//輸出結(jié)果:
default format:141.421
scientific:1.414214e+002
fixed decimal:141.421356
hexadecimal:0x1.1ad7bcp+7
use defaults:141.421
41、noexcept異常說(shuō)明
對(duì)于用戶及編譯器來(lái)說(shuō),預(yù)先知道某個(gè)函數(shù)不會(huì)拋出異常顯然大有裨益。首先知道函數(shù)不會(huì)拋出異常有助于簡(jiǎn)化調(diào)用該函數(shù)的代碼;其次,如果編譯器確認(rèn)函數(shù)不會(huì)拋出異常,它就能執(zhí)行某些特殊的優(yōu)化操作,而這些優(yōu)化操作并不適用于可能出錯(cuò)的代碼。
在C++11中,我們可以通過(guò)noexcept關(guān)鍵字說(shuō)明某個(gè)函數(shù)不會(huì)拋出異常:
void recoup(int) noexcept;
對(duì)于一個(gè)函數(shù)來(lái)說(shuō),noexcept說(shuō)明要么出現(xiàn)在函數(shù)的所有聲明和定義語(yǔ)句中,要么一次也不出現(xiàn)。該說(shuō)明應(yīng)該在函數(shù)的尾置返回類型之前。在typedef或類型別名中則不能出現(xiàn)noexcept。在成員函數(shù)中,noexcept說(shuō)明符需要跟在const及引用限定符之后,而在final、override或虛函數(shù)的= 0之前。
noexcept說(shuō)明符的實(shí)參常常與noexcept運(yùn)算符混合使用。noexcept運(yùn)算符是一個(gè)一元運(yùn)算符,它返回值為一個(gè)bool類型的右值常量表達(dá)式,用于表示給定的表達(dá)式是否會(huì)拋出異常:
noexcept(recoup(i)) //如果recoup不拋出異常則結(jié)果為true
//更普通的形式是:
noexcept(e)
當(dāng)e調(diào)用的所有函數(shù)都做了不拋出說(shuō)明且e本身不含有throw語(yǔ)句時(shí),上述表達(dá)式為true,否則為false。我們可以使用noexcept運(yùn)算符得到如下的異常說(shuō)明:
void f() noexcept(noexcept(g())); //f和g的異常說(shuō)明一致
如果函數(shù)g承諾了不會(huì)拋出異常,則f也不會(huì)拋出異常。