C++11新標(biāo)準(zhǔn)
C++剛?cè)腴T,以下為自己學(xué)習(xí)時整理的資料,寫出來的是自己的理解,只是想加深自己的印象和以后方便自己查閱,可能有不正當(dāng)?shù)牡胤健?div style="height:15px;">
1、新增算術(shù)類型
longlong,最小不比long小,一般為64位。
2、列表初始化
int units_sold = {0};或者 int units_sold{0};非11標(biāo)準(zhǔn)下的C++中,只有特定的情況下才能使用該形式。 比如數(shù)組的初始化,類構(gòu)造函數(shù)的初始化,結(jié)構(gòu)體的填充。相比傳統(tǒng)的賦值初始化,如果右側(cè)的數(shù)值類型相對于左側(cè)類型更大的話,側(cè)對于這種窄化現(xiàn)象,編譯器會報錯。如:int k = {3.14};一個double是8個字節(jié),int一般是4個字節(jié),這時編譯器就會報錯。
在容器中,也支持這種方法,比如std::map容器,比如我們定義一個std::map<string,int> m_map,以前我們需要對它進(jìn)行插入,一般做法是 m_map.insert(value_type("test",1));,這樣用一個函數(shù)來生成pare<string,int>再進(jìn)行插入,但現(xiàn)在有種更方便的方法,讓我們能在初始化時進(jìn)行賦值,像下面這樣:
map<string,int> m_map = {{"test",1},{"test2",2}};
每一組值都用{}括起來,跟列表初始化使用上是一致的。
3、列表初始化返回值
如果我們有一個函數(shù),它要求我們返回一個vector<string>類型的對象,那么我們或者是這樣寫的:
vector<string> test()
{
vector<string> temp("1","2","3");
return temp;
}
函數(shù)體內(nèi)定義一個臨時的vector變量,返回時直接返回臨時變量(PS:別說什么生命周期,這不是引用,這是值復(fù)制)。而C++11定義了列表初始化返回值:
return {}
上面的函數(shù)可以寫成如下方式:
vector<string> test()
{
return{"1","2","3"};
}
這樣直接返回一個初始化列表,用于初始化vector變量,這樣同樣會生成臨時變量,但卻可以減少代碼量。
4、商一律向0取整(即直接切除小數(shù)部分)
21 % -5; /* 結(jié)果是 1 */
運算符%俗稱"取余"或"取模"運算符,負(fù)責(zé)計算兩個整數(shù)相除所得的余數(shù),參與取余運算的運算對象必須是整數(shù)類型:
int ival = 42;
double dval = 3.14;
ival % 12; // 正確:結(jié)果是6
ival % dval; // 錯誤:運算對象是浮點類型
在除法運算中,如果兩個運算對象的符號相同則商為正(如果不為0的話),否則商為負(fù)。C++語言的早期版本允許結(jié)果為負(fù)值的商向上或向下取整,C++11新標(biāo)準(zhǔn)則規(guī)定商一律向0取整(即直接切除小數(shù)部分)。
根據(jù)取余運算的定義,如果m和n是整數(shù)且n非0,則表達(dá)式(m/n)*n+m%n的求值結(jié)果與m相等(這里的又乘又除的,無非是去掉小數(shù)部分)。隱含的意思是,如果m%n不等于0,則它的符號和m相同。C++語言的早期版本允許m%n的符號匹配n的符號,而且商向負(fù)無窮一側(cè)取整,這一方式在新標(biāo)準(zhǔn)中已經(jīng)被禁止使用了。除了 m導(dǎo)致溢出的特殊情況,其他時候(-m)/n和m/(-n)都等于-(m/n),m%(-n)等于m%n,(-m)%n等于-(m%n)。具體示例如下:
21 % 6; /* 結(jié)果是3 */ 21 / 6; /* 結(jié)果是3 */
21 % 7; /* 結(jié)果是0 */ 21 / 7; /* 結(jié)果是3 */
-21 % -8; /* 結(jié)果是-5 */ -21 / -8; /* 結(jié)果是2 */
21 % -5; /* 結(jié)果是1 */ 21 / -5; /* 結(jié)果是-4 */
5、nullptr常量
其實這個常量跟NULL一樣,都是0值,只是NULL是預(yù)處理變量,在C++11中,應(yīng)該盡量減少NULL的使用。宏定義往往會導(dǎo)致非程序員本身需要的目的(大部分是運算符優(yōu)先級問題,比如#define duplicate(x) x*x,如果你是想算兩個x+1后的結(jié)果再相乘,如果你寫成duplicate(x+1),編譯器展開后會是 x+1*x+1,這樣明顯與原來的意圖完全不同),而且,排除稍困難,一般來說,都會盡量避免使用。
6、constexpr
constexpr可以說是對const變量的擴(kuò)展,它只是約束const變量的右側(cè)的式子必須能在編譯時就能算出值。要理解這點,我們首先要搞清楚 const和constexpr之間的同異處.首先,無論是constexpr或者const變量,值都是定義后不能改變的,而且,它們等號右側(cè)同樣都可以為一個表達(dá)式;兩者的一個很重要的區(qū)別是const常量能在運行時獲得值,而constexpr卻是帶有約束性質(zhì)的,它約束右側(cè)的式子只有在編譯時就能算出值,如果不能的話,編譯器側(cè)會報錯。比如:const int max = 20; const int limit = max + 1;它們都是一個const變量同時,右側(cè)也是一個常量表達(dá)式。但const int sz = str.size();它是一個在運行時取得具體值的const變量,但在編譯時卻無法取得具體值,所以右側(cè)的表達(dá)式明顯不是一個常量表達(dá)式。往往程序員希望某一變量必須是常量以防止在運行過程中產(chǎn)生非意料之外的值(該過程大都由粗心導(dǎo)致的疏忽),所以C++11中才會加入一個新的constexpr關(guān)鍵字,這樣的話,當(dāng)定義constexpr int sz = str.size();時,就會由編譯器發(fā)出提示,而避免上述情況的發(fā)生。
7、constexpr函數(shù)
constexpr函數(shù)是指能用于常量表達(dá)式的函數(shù)。比如上面的如果constepr int size = size() + 1;成立的話,size(int max)就是一個constexpr函數(shù),它定義或者如果下:
int size() { return 30;}
對于constexpr函數(shù),有以下幾點需要注意:
1)constexpr只能有一個return;
2)因為需要在編譯時展開constexpr函數(shù),所以constexpr函數(shù)默認(rèn)為inline函數(shù)。
3)主要語句不會在運行時被執(zhí)行(注意,是運行時不會被執(zhí)行,別想著a = 1;這種也算,這是運行時才被執(zhí)行的語句,是無法通過的,說到底,只有第4點的語句才會合法),constexpr函數(shù)體內(nèi)可包含任意數(shù)量語句。
4)constexpr函數(shù)體內(nèi)可以包括空語句,類型別名和using聲明,主要是編譯能執(zhí)行的語句都可以。
本質(zhì)上,constexpr函數(shù)跟constexpr表達(dá)式一樣,只是用于編譯時進(jìn)行常量檢測。
8、constexpr構(gòu)造函數(shù)
在說這個前,我們應(yīng)該要注意的重要的一點是:constexpr構(gòu)造函數(shù)無論名稱怎么變,但它仍然需要遵守constexpr函數(shù)的規(guī)則。這些規(guī)則總結(jié)為以下幾點:
1)constexpr構(gòu)造函數(shù)可以聲明為=default或者刪除函數(shù)的形式。前者相當(dāng)于編譯器合成的默認(rèn)構(gòu)造函數(shù),一般情況下幾乎不執(zhí)行任務(wù)初始化工作,所以不會與constexpr函數(shù)的約束沖突;后者相當(dāng)于屏蔽類的某個功能,現(xiàn)在先不用管,后面會提到。
2)對于第1種情況外的constexpr構(gòu)造函數(shù),constexpr的限制就比較多了:它既要滿足構(gòu)造函數(shù)的限制條件,也必須得滿足自身的限制條件。前者讓它不能有返回語句(構(gòu)造函數(shù)是沒有返回值的),后者限制了函數(shù)體內(nèi)的內(nèi)容必須是能在編譯時確定的,也即是說,除了返回語句外其它語句都不成立(當(dāng)然還可以有類型別名之類的語句,但這些語句真的想不出有什么必要聲明在一個構(gòu)造函數(shù)中),所以通常它的函數(shù)體都是空的。
3)constexpr構(gòu)造函數(shù)必須初始化所有數(shù)據(jù)成員,而且初始值只能是常量,比如一個constexpr函數(shù)的委托構(gòu)造函數(shù)的形式或者一條常量表達(dá)式。
// 如C++ Primer中的例子
class Debug
{
public:
constexpr Debug(bool b = true):hw(b),io(b),other(b){}
constexpr Debug(bool n,bool i,bool o):hw(h),io(i).other(0){}
private:
bool hw,io,other;
}
可以看出,初始化列表中的變量都是常量,true和false都是常量表達(dá)式。如果是int類型,如果賦值是一個字面常量也是可以的,初始化列表中的右值主要是常量表達(dá)式都是正確的。
9、類型別名
類型別名是C和C++一樣存在的,以前的typedef的用法為typedef typeA typeB,把typeA定義為typeB的一個別名,這種用法,更像是宏定義的用法,并沒有等號表達(dá)式直觀;C++11中新增一種定義類型別名的方法,該方法更符合我們平時的使用習(xí)慣:using typeA = typeB;這種方法在平時可能并無法表現(xiàn)多大的優(yōu)越性,但如果用于函數(shù)指針時,卻是很直觀的。比如有如下函數(shù) int add(int a,int b);如果定義指針,以前的做法是typedef int (add*)(int a,int b);這種語法,對于新手來說,是很難理解的,如果再復(fù)雜點,返回值還是一個函數(shù)指針的話,那更難以理解了。新手一種會按我們上面說的那種基本語法去理解,typedef typeA typeB,一個函數(shù)指針,很難找出那部分是對應(yīng)typeA,那部分是對應(yīng)typeB,事實上,typeA就是add,它可以是任何名稱,只是我這里剛好與函數(shù)名是一樣而已,如果寫作typedef int (addptr*)(int a,int b);你就會知道,定義一個函數(shù)指針與函數(shù)名完全無關(guān),而只是與返回類型和參數(shù)類型有關(guān)(甚至可以沒有參數(shù)名稱:typedef int (add*)(int,int);),但無論怎么說,這種方法仍然是不太直觀的,現(xiàn)在C++11中可以 這樣用了。
using addptr = int (int,int);它很明確指出,addptr就是一個函數(shù)指針,它指向的函數(shù)類型為:返回值是int,并帶有兩個int形參的所有函數(shù)。
10、auto類型
auto是C++11中新增加的特性,對于我們平時編寫代碼,我建議是少用應(yīng)該盡量少用,只有在寫代碼時知道我每步會產(chǎn)生的結(jié)果時才會寫出健壯的程序,僅 auto來為我們判斷類型,事實與我們靜態(tài)語言的原意相悖了;auto更多應(yīng)用于泛型編程中,它能減少你編寫特化模板的工作量。對于auto,我們只需要記住以下幾點:
1)auto是編譯時得到的類型,也就是說,它是讓編譯器替我們分析表達(dá)式所屬的類型的,所以有時候auto產(chǎn)生的值,如果對它沒有足夠的理解,你會被弄糊涂。
2)auto只能推斷一種類型,比如當(dāng)一條語句中聲明多個變量時,如果變量類型不同,是會產(chǎn)生錯誤的:auto sz = 0,pi = 3.14;一個為int型,一個為double型,這時auto是不能正常工作的。
3)當(dāng)右值是一個引用類型時,auto的類型不是一個引用類型,而是引用類型的值。比如double &PI = pi;auto ref = PI;ref的類型不是double&而是跟pi的類型一樣,為一個double類型。當(dāng)我們確實需要一個引用類型時,我們可以寫成這樣auto &ref = PI;
4)說這點前,先說明下C++ Primer中提到的頂層const和底層const的概念。當(dāng)一個const變量自身無法改變時,我們稱為頂層const,當(dāng)一個const變量本身可改變,但關(guān)聯(lián)的變量無法改變時我們稱為底層const。也即是頂層也就是表面,底層也就是表面掩住的內(nèi)容。如果:const double pi = 3.14;double *const ptr = π(注意,不要寫成double const *ptr,這個是跟const double* ptr一樣的),這 2個都是頂層const,他們本身的值無法改變;const double *ptr2 = π;const double PI = π這2個都是底層const,事實上,指向常量的指針和聲明引用的const都是底層const; auto會忽略掉頂層const的性質(zhì),但保留底層const;比如auto x = pi;這時x的類型為double而非const double,同樣地,當(dāng)auto x = ptr時,x的類型為double*;而auto x = ptr2,此時的x為const double*類型,它說明指向的值不可改變。如果我們想保留頂層const,只能手動寫上符號,如auto const x = π
總的來說,auto能判斷內(nèi)置類型,能判斷指針類型,但卻無法正確判斷出引用類型和頂層const類型,對于頂層const類型和引用類型(所有的const引用類型都是頂層const類型)只能通過手動增加的方法保留特性。
5)由編譯器確定動態(tài)分配數(shù)據(jù)的類型,C++11的auto在new中也有新的定義,之前我們必須確實地指定new類型和指針類型才能正確地在堆上分配內(nèi)存空間,現(xiàn)在,C++11允許由編譯器推斷分配類型,用法如下:
auto p1 = new auto(std::string); // p1為指向string類型的指針
11、decltype
我覺得C++11新增的這個特性,比auto更奇葩,要理解這個東西,說真的,難度不是一般的大,說不得,真想深入研究,你應(yīng)該重新翻翻編譯原理。以下幾點,是我們必須注意的:
1)decltype是推斷類型的,它允許變量和表達(dá)式,但無論是那一種,它都忽略值而只關(guān)注類型的,比如表達(dá)式,并不會調(diào)用表達(dá)式而只是推斷當(dāng)表達(dá)式被調(diào)用時,會返回一個什么樣的類型。
2)decltype 在處理頂層const跟auto不同,它返回的類型是包括頂層const的,也就是說當(dāng)double *const ptr = π時auto x = ptr;類型是double* ,但如果用decltype(ptr)得到類型卻是完整的double *const類型。對于引用,也是一樣的。但引用必須注意,如果const int &x = pi;decltype(x)后得是必須要初始化的,因為引用類型必須定義的同時初始化。
3)注意這種情況:decltype(x + 0)——這里我們假定x是int&類型,引用類型進(jìn)行decltype是作為一條賦值語句的左值的。最后的結(jié)果應(yīng)該是int類型。
4)這點是最重要的:加不加括號,我們得到的類型可能是會不同的。比如int i = 0;當(dāng)我們調(diào)用decltype(i)時,這很容易理解,得到的類型肯定是int類型;但如果我們加上一重或多重括號如:decltype((i)),我們得到的是int&類型。事實上,變量也是可以作為一種賦值語句左值的特殊表達(dá)式,從C++的初始化就可以看出,比如int i(0);,可以看出,這是一個函數(shù)調(diào)用的形式,事實上,這個比較難理解,我也不明白,而這也僅僅是一個數(shù)學(xué)到的概念而已,a+b = 0是代數(shù)式,那a和b都是代數(shù)式。反正decltype是C++11新增的機制。
5) decltype可用于返回類型:比如下面這段代碼:(選自C++ primer)
int odd[] = {1,3,5,7,9};
int even[] = {0,2,,4,,6,8};
// 返回一個指針,該指針指向含有5個整數(shù)的數(shù)組
decltype(odd) * arrPtr(int i)
{
return (i%2) ? &odd:&even; // 返回一個指向數(shù)組的指針
}
這里是特別需要注意的,函數(shù)的返回的是數(shù)組的地址,但decltype(odd)得到的類型卻是一個含有5個元素的數(shù)組,它不會幫我們把數(shù)組轉(zhuǎn)換成指針類型,所以還要在addPtr前加上*表示返回類型的是一個指向含有5個元素的數(shù)組的指針。
主要是我們記住,如果加了括號,declstype的類型永遠(yuǎn)是引用。最后總結(jié)是,這樣的:
如果e是一個沒有外層括弧的標(biāo)識符表達(dá)式或者類成員訪問表達(dá)式,那么decltype(e)就是e所命名的實體的類型。
如果e是一個函數(shù)調(diào)用或者一個重載操作符調(diào)用(忽略e的外層括弧),那么decltype(e)就是該函數(shù)的返回類型。
否則,假設(shè)e的類型是T:若e是一個左值,則decltype(e)就是T&;若e是一個右值,則decltype(e)就是T。
12、范圍for
看C#,JAVA,都有for...each語法,javaScript有for...in語法,OC也有自己的迭代遍歷語法,總的來說,對于特定的數(shù)據(jù)的遍歷,這些方法確實足夠簡單,但說真的,這些方法方便的地方在于能動態(tài)判斷類型,但我個人認(rèn)為,這種范圍for對于C++來說,作用真不算強大,因為 C++不像C#這些一樣,一個數(shù)組內(nèi)能混合不同的類型,C#的同一個數(shù)組內(nèi)能加入基本類型,也能加入類類型,但C++只能加入特定一種類型,所以,每個數(shù)組都是特定類型,用auto其實作用不大,而傳統(tǒng)的for語句,跟使用范圍for相比,并沒有多大的不方便吧。
C++11新加入for(auto value:data);
for (auto c : str) //對于str中的每個字符
cout<<c<<endl; // 輸出當(dāng)前字符,后面緊跟一個換行符
13、cbegin()和cend()【容器】
傳統(tǒng)的C++中,標(biāo)準(zhǔn)庫中的所有容器都有begin()和end()函數(shù),分別取得容器的首元素和最后元素的下一次位置。但以上兩個函數(shù),返回的都是 iterator類型。而新增加的cbegin()和cend()函數(shù),返回的是const_iterator類型。
14、beign()和end()【標(biāo)準(zhǔn)庫】
比如有如下數(shù)組:
int arr[] = {1,2,3,4,5,.........};
傳統(tǒng)C++要遍歷會作出如下操作:
for(int i = 0; i != sizeof(arr) / sizeof(arr[0]); ++i){};
或者
for(int *p = arr[0]; p != arr[0] + sizeof(arr) / sizeof(arr[0]; ++i){};
前者在某種程度上是安全的,但對于后者,要取得有效數(shù)據(jù)的下一個位置時,卻是極易出錯的。所以,為了讓指針操作更安全,C++11提供了begin()和end()函數(shù),分別指向數(shù)據(jù)的起始和結(jié)束位置的下一位置【他們都是指針類型】。我們現(xiàn)在可以更安全地遍歷數(shù)組等數(shù)據(jù)了:
for(int *p = begin(arr); p != end(arr); ++p){};
甚至,這兩個函數(shù)也提供了跟迭代器一樣的操作,退兩個迭指針相減的結(jié)果就是它們之間的與數(shù)據(jù)類型相關(guān)的距離,比如,arr如果有5個元素,則auto diff = end(arr) - begin(arr),diff的值為5,而且,diff的類型是標(biāo)準(zhǔn)庫類型ptrdiff_t,這是一種帶符號的類型,因為差值可能會為負(fù)數(shù)。
15、initializer_list類
傳統(tǒng)的可變參數(shù)一般是用va_list的一組宏來解決的?,F(xiàn)在,C++11新增了模板類initializer_list,在便利性上得到提升。它跟va_list最大區(qū)別是,va_list中的va_arg的參數(shù)可以為任意類型,但 initilizer_list只允許未知的形參使用相同的類型。
對于initializer_list,一個最重要的概念是,initializer_list內(nèi)所有的元素都是常量,這與vector這些容器是不一樣的。
initializer_list因為有一種構(gòu)造方法為initializer_list<T> lst{a,b,c.....};使得它可以實現(xiàn)可變參數(shù)(我知道,vector,list一樣可以這樣做,但問題是,vector和list總是一開始就分配內(nèi)存空間的,相比較下,initializer_list是輕量級的。PS:這貨不支持下標(biāo)操作的,只能用指針,或者在標(biāo)準(zhǔn)庫中叫迭代器更好點),大概方法如下:
void CoutList(initializer_list<string> il)
{
// 一般為了方便,我們不清楚il.begin()的類弄,可以 用auto代替,但事實上, begin返回的是一個以模板實例相同 的const類型。
for(auto beg = il.begin(); beg != il.end(); ++beg)
{
cout << *beg << endl;
}
調(diào)用時可以這樣,CoutList({"1","2","3"});,相較傳統(tǒng)的va_list宏而言,這種方法雖然簡結(jié)了,但對于傳統(tǒng)的C++程序員來說,多了個花括號,可能已經(jīng)不算正規(guī)的函數(shù)調(diào)用了。不過,它為我們提供了一種方法,如果上面的函數(shù),用va_list是無法達(dá)到的,原因就是,va_list必須至少帶一個參數(shù),以作為va_start的起始點,上面的函數(shù),我們可以增加一個表示傳入?yún)?shù)數(shù)量的形參,可以可以這樣寫:
void CountList(int nNumber,...)
{
va_list lt; //定義一個 va_list 參數(shù)表
va_start(lt,nNumber); // 初始化va_list,并讓其指針指向參數(shù)表中的第一個參數(shù)
// 因為是棧,所以棧底就是第一個參數(shù),+1是從棧頂向棧低取值,用第一個參數(shù)判斷循環(huán)結(jié)束是可以的
for(int i = 0; i != nNumber; ++i)
{
// 取得參數(shù)表中的第i個參數(shù),總設(shè)置string類型,此時lt會自動指向當(dāng)前位置的下一位置(ap是先移動一個位置再返回值的。)
string tmp = va_arg(lt,string);
cout << tmp << endl;
}
va_end(lt); // 用完清空是好習(xí)慣
}
調(diào)用時可以用如下方法:CoutList(3,"1","2","3");
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) // 這個是字節(jié)對齊,自己參考文檔。(http://yangtaixiao.blog.163.com/blog/static/42235441201441233325695/)
#define _crt_va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
#define _crt_va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define _crt_va_end(ap) ( ap = (va_list)0 )
但總體來說,因為initializer_list是輕量的,所以使用它應(yīng)該更方便一點。
16、尾置返回類型
想像一下,當(dāng)你要定義一個函數(shù),當(dāng)你需要一個函數(shù),它接受一個int類型參數(shù),并返回一個指向含有10個int的數(shù)組,你應(yīng)該怎么寫?我就犯過傻,聰明地直接返回數(shù)組,接收時卻也是用數(shù)組,但這是錯誤的,數(shù)組是不能拷貝的,明顯是不能直接返回數(shù)組,只能返回數(shù)組的指針或者引用,而且,一般來說,返回指針,在語法上會更煩瑣點。要寫一個返回指向10個int數(shù)組的指針,一般會用如下方法:
1)定義別名
typedef int arrT[10]; 或者用新標(biāo)準(zhǔn) using arrT = int[10];
//定義函數(shù)
arrT* func(int);
2) 不定義別名
int (*func(int i))[10];
這個函數(shù)的語法本身就讓人難以理解,所以為了解決這種復(fù)雜的返回類型(我覺得C和C++之所以難學(xué),這語法的復(fù)雜度也是其實一個因素了),C++新增了尾置返回類型,就是把返回類型提后,我們只需要把返回類型清晰地標(biāo)明就可以了,比如func就能寫成這樣:
auto func(int i)->int(*)[10];
另外,尾置法有一個特別的地方在于:普通的函數(shù),函數(shù)的返回值是會比函數(shù)的參數(shù)先確定類型的,但尾置法卻是在確認(rèn)函數(shù)參數(shù)后再確認(rèn)返回類型的。
17、default生成默認(rèn)構(gòu)造函數(shù)
我們都清楚,如果我們自己本身沒有對類定義構(gòu)造函數(shù),編譯器會為我們合成一個構(gòu)造函數(shù)。但事實上,這個簡單的構(gòu)造函數(shù),在定義時可能根本不會調(diào)用,更是別指望它會幫你初始化成員變量(自己試下就知道了,除了全局的內(nèi)置變量,C++任務(wù)位置的變量,都不會自動幫你初始化)。而當(dāng)我們自己定義一個構(gòu)造函數(shù)后,編譯器就不會再為我們合成默認(rèn)構(gòu)造函數(shù)了,如果我們在需要自定義的構(gòu)造函數(shù)時,卻仍然希望編譯器為我們合成默認(rèn)構(gòu)造函數(shù),我們就需要用到新標(biāo)準(zhǔn)中的default關(guān)鍵字了。用法如下:
class A
{
public:
void A(int a):iNumber(a){}
A() = default; // 當(dāng)在類內(nèi)部定義時,跟合成的默認(rèn)構(gòu)造函數(shù)一樣,它是inline的
A(){} // 當(dāng)我們存在合成默認(rèn)構(gòu)造函數(shù)時,我們手動定義就會提示重定義了,這個應(yīng)該都清楚
}
類外定義
class A
{
public:
A() ;
}
A::A() = default; // 類外定義時,該合成默念構(gòu)造函數(shù)就不再是inline的了。
18、類內(nèi)初始值
我會告訴你我很討厭這個嗎?我是感覺這個完全破壞了構(gòu)造函數(shù)存在的意義。
C++11中,允許通過以符號=或者花括號為類內(nèi)一個變量提供類內(nèi)初始值。如:
class B
{
private:
int Size = 0;
std::vector<int> vt{1,2,3};
};
當(dāng)然,我們也可以在構(gòu)造函數(shù)中進(jìn)行初始化,只是這樣或者更直觀一點。
19、委托構(gòu)造函數(shù)
看到這個名字時,我希望大家不要跟我一樣直接想到IOS的委托和C#的委托,其實根本不是這么一回事。C++11新擴(kuò)展的這個函數(shù)功能是使委托構(gòu)造函數(shù)可以執(zhí)行它所屬類的其它構(gòu)造函數(shù)。
具體方法如下:
class A
{
public:
A(int size):iSize(size){}
A():A(30)
{
area = 90;
}
private:
int iSize;
double area;
};
委托構(gòu)造函數(shù)在語法上類似派生類調(diào)用基類構(gòu)造函數(shù),而且,同樣地,是先執(zhí)行被委托的構(gòu)造函數(shù),再執(zhí)行自身括號內(nèi)的內(nèi)容。
20、文件流對象的文件名支持標(biāo)準(zhǔn)庫string
舊版本的標(biāo)準(zhǔn)庫中的fstream只支持C風(fēng)格字符數(shù)組,如果我們用標(biāo)準(zhǔn)庫中的string還得string.c_str()轉(zhuǎn)換一下,但現(xiàn)在C++11的fstream已經(jīng)允許直接使用string字符串了。
21、標(biāo)準(zhǔn)庫新增forward_list和array類型
forward_list是單向鏈表,在鏈表的任何位置插入/刪除數(shù)據(jù)速度都很快。
array行為跟內(nèi)置數(shù)組一樣,大小也是固定的,也不支持添加/刪除或者改變?nèi)萘看笮〉牟僮鳌?div style="height:15px;">
22、容器列表初始化【容器】
C++11支持以花括號形式的列表初始化方法,同時,標(biāo)準(zhǔn)庫幾乎所有的容器都能享受此種新特性。
如:vector<int> vt("1","2","3");舊標(biāo)準(zhǔn)只能用這種方式函數(shù)構(gòu)造函數(shù)進(jìn)行初始化。而C++11中能使用如下方式:
vector<int> vt = {"1","2","3"};
這種方式的初始化,其實也不過是調(diào)用vector的賦值構(gòu)造構(gòu)造函數(shù)。
23、提供了swap函數(shù)的非成員版本【標(biāo)準(zhǔn)庫】
舊的C++的標(biāo)準(zhǔn)庫版本只提供成員函數(shù)版本的swap,而現(xiàn)在也提供了非成員版本的swap函數(shù)。它在泛型中使用會更方便。(不太了解泛型,也不明白這個……)
24、改進(jìn)的inser操作【容器】
新標(biāo)準(zhǔn)中,insert數(shù)據(jù)后,它會返回一個指向第一個新增加元素的迭代器,不同于以往,它是返回一個void值。
25、emplace_front、emplace和emplace_back 【容器】
C++11新增加的這個新成員,它允許我們通過提供數(shù)據(jù)的值來直接初始化容器內(nèi)的數(shù)據(jù)。比如我們有如下一個類:
class em
{
public:
em(string name,int age,bool sex)
{
m_name = name;
m_age = age;
m_sex = sex;
}
private:
string m_name;
int m_age;
bool m_sex;
};
如果我們用一個vector裝載它,如果我們要在后面插入一個新成員,我們一般做法是這樣:
vector<em> vt;
vt.pushback(em("stupid",30,0));
這樣做首先是構(gòu)造一個臨時的局部em對象,然后再通過拷貝的方式把這個臨時對象拷貝到容器內(nèi)。但如果我們使用新的emplace函數(shù),我們可以這樣做:
vt.emplace("stupid",30,0);
這樣做的結(jié)果是直接在容器內(nèi)的內(nèi)存空間中創(chuàng)建對象,而不必要浪費再構(gòu)建臨時變量和進(jìn)行拷貝了。
很明顯,emplace比傳統(tǒng)的插入函數(shù)具有更高的效率。
26、shrink_to_fit【容器】
C++11標(biāo)準(zhǔn)為有預(yù)分配內(nèi)存功能的容器(vector,deque,string)提供一個名為shrink_to_fit的函數(shù),他能將容器的capacity()減少為與size()相同大小,即讓容器把多出來的內(nèi)存空間釋放,但該函數(shù)并不一定保證能100%釋放這些多余的空間。
27、string數(shù)值轉(zhuǎn)換【標(biāo)準(zhǔn)庫】
舊版C++中,要把一個數(shù)值字符轉(zhuǎn)化成字符串,我們只能使用itoa()的C函數(shù)或者sprintf()等諸如此類要手動事先分配內(nèi)存的方法。但C++11標(biāo)準(zhǔn)為string提供了一組函數(shù)(也是定義在string頭文件內(nèi)),使得能把幾乎所有的數(shù)值類型轉(zhuǎn)化為string,而不需要我們事先手動分配內(nèi)存空間。同樣地,也定義了一組從string轉(zhuǎn)換為數(shù)個類型的函數(shù)。如:
int i = 30;
string s = to_string(i); //數(shù)值轉(zhuǎn)為string,to_string()是一個重載函數(shù)
double pi = stod(s); // 把字符串"30"轉(zhuǎn)化為一個double值
如果string不能轉(zhuǎn)換為一個數(shù)值,拋出invalid_argument異常;如果得到的數(shù)值無法用任何類型來表示,則拋出一個out_of_range類型。
函數(shù) 說明
to_string
這個函數(shù)是重載函數(shù),能滿足所有內(nèi)置類型,而且,有相對應(yīng)的寬字節(jié)to_wstring
stoi(s,p,b)
p表示一個size_t*的指針類型,默認(rèn)為空值。當(dāng)它不為空,轉(zhuǎn)換成功時就表示第一個為非數(shù)值字符的下標(biāo),一般情況下,因為它是直接char型指針,通過把最后非數(shù)值字符的地址值和起始地址值相減的,所以也即表示了成功轉(zhuǎn)換的字符的數(shù)量,如"10"轉(zhuǎn)成功為數(shù)值10時,這個值為2。b表示轉(zhuǎn)換基準(zhǔn),這個跟atoi一樣,默認(rèn)是10,表示10進(jìn)制。s則重載了string和wstring版本。
stol(s,p,b)
同上
stoul(s,p,b)同上
stoll(s,p,b)
同上
stoull(s,p,b)
同上
stof(s,p)
p的含義同上
stod(s,p)
p的含義同上
stold(s,p)
p的含義同上
28、lambda
C++中的閉包命名為lambda,或者匿名函數(shù),他遵守閉包的特性,子函數(shù)可以使用父函數(shù)中的局部變量。
我們要記住一點,lambda是一個類,而且,沒有帶合成默認(rèn)構(gòu)造函數(shù)的類,在我們定義時,編譯器會為我們自動生成這樣一個類。
它格式如下:[capture list](parameter list)->return type {function body}
[捕獲列表](形參列表)->返回類型 {函數(shù)體}
首先,這種格式不是固定的,其中某些項能省略,或者隱式調(diào)用,但無論如何,捕獲列表和函數(shù)體是必須要永遠(yuǎn)包含的,那怕捕獲列表和函數(shù)體為空。lambda在函數(shù)體內(nèi)或函數(shù)體外都能定義。例如:
auto fun = []{return 0;}; // 不要忘記最后的分號
為了說明lambda的本質(zhì),有必要把某些內(nèi)容提前進(jìn)行說明。
lambda的本質(zhì),我們可以這樣理解:編譯器以lambda表達(dá)式為基礎(chǔ)生成一個未命名的類。該類中的成員數(shù)據(jù)就是捕獲列表中羅列的所有數(shù)據(jù)。但lambda表達(dá)式產(chǎn)生的類,卻與傳統(tǒng)的類有點細(xì)致區(qū)別,主要表現(xiàn)為:
1)lambda產(chǎn)生的類不含默認(rèn)構(gòu)造函數(shù)、賦值運算符及默認(rèn)析構(gòu)函數(shù)。
2)lambda產(chǎn)生的類含有默認(rèn)拷貝構(gòu)造函數(shù)、默認(rèn)移動構(gòu)造函數(shù)。當(dāng)然,這得視捕獲列表中的變量的數(shù)據(jù)類型決定,一般地,一個類如果沒有定義自身的拷貝構(gòu)造函數(shù),而且,本身的非static成員變量都能移動的話,就會合成默認(rèn)移動構(gòu)造函數(shù),反之,則合成默認(rèn)拷貝構(gòu)造函數(shù)。
比如有這樣一個lambda表達(dá)式:[sz](const string&s) {return s.size() >= sz;};如果sz為size_t類型,則編譯器為我們生成類似這樣的一個未知名的類(別以為就是這樣,這只是類似而已,并非就是一模一樣,這點要清楚的):
class unnamed
{
public:
// 它不會合成默認(rèn)構(gòu)造函數(shù),但卻會根據(jù)捕獲列表生成相應(yīng)的構(gòu)造函數(shù),用以初始化成員變量
unnamed(size_t data):sz(data){}
// 它會根據(jù)lambda表達(dá)式的函數(shù)體的return語句推斷返回類型,并重載調(diào)用運算符,值捕獲時一般都是const成員函數(shù),這是lambda所要求的——值捕獲到的值一般不會改變基值。
bool operator() (const string&s) const // 因為形參是const類型,所以成員函數(shù)也聲明為const成員函數(shù)
{
return s.size() >= sz;
}
private:
size_t sz;
}
現(xiàn)在我們應(yīng)該已經(jīng)理解了,lambda就是一個未命名的類,它重載了調(diào)用運算符。
下面分步來解釋lambda的幾個要點:
[形參列表]:
1)如果沒有提供形參表,就表示指定一個空參數(shù)列表;lambda的形參不能帶有默認(rèn)值。
[捕獲列表]:
1)如果捕獲列表為空,則表示不捕獲任何局部變量。如果沒有提供返回類型,但lambda能根據(jù)函數(shù)體的代碼來推斷返回類型:1.如果有return,則根據(jù)return后的表達(dá)式來推斷;2.如果沒有return就是void。
2)如果捕獲列表為空,則我們不能在函數(shù)體內(nèi)定義對局部變量進(jìn)行任何操作。下面這樣會報錯:
int GetBiger(double first, double second)
{
double result = (first > second) ? first : second;
auto func = []{return result; };
return func();
}
當(dāng)然,上面的函數(shù)完全是沒有意義的,但這個是演示,我們在lambda里使用了GetBiger的名叫result的臨時變量,但卻沒有在捕獲列表里列出,所以這是非法的,編譯器會報錯;如果我們確實想使用,就應(yīng)該把局部變量放于捕獲列表中:
int GetBiger(double first, double second)
{
double result = (first > second) ? first : second;
auto func = [result]{return result; };
return func();
}
這樣就沒有問題了。
[值捕獲]:
lambda如果采用值捕獲(即不帶引用符號)時,與傳值參數(shù)行為一致,即由lambda表達(dá)式生成的新類型(下面會說,lambda本質(zhì)是一個未命名的類類型)中的成員數(shù)據(jù)在構(gòu)造時會拷貝父函數(shù)中局部臨時變量的一個副本,在這種情況下,更改lambda內(nèi)的值不會影響父函數(shù)變量的值,同樣地,更改父函數(shù)局部變量的值也不會影響lambda內(nèi)的捕獲值。如:
void func()
{
size_t v1 = 42; // 函數(shù)內(nèi)局部臨時變量
// 捕獲列表為值捕獲,在創(chuàng)建lambda時(生成類時調(diào)用構(gòu)造函數(shù))就已經(jīng)拷貝
auto f2 = [v1]{return v1;};
v1 = 0;
auto j = f2();
}
因為是值拷貝,所以修改父函數(shù)的局部變量,不會影響lambda生成的類的成員變量,所以j的值仍然為42。
[ 引用捕獲]:
如果上面的式子改寫成如下形式
void func()
{
size_t v1 = 42; // 函數(shù)內(nèi)局部臨時變量
auto f2 = [&v1]{return v1;}; // 在創(chuàng)建lambda時時引用類型,格式為引用符號寫在捕獲變量前面
v1 = 0;
auto j = f2();
}
因為是引用初始化,所以改變局部變量的值,lambda表達(dá)式生成的未命名的類內(nèi)數(shù)據(jù)成員也跟著改變值。結(jié)果是j的值為0。
[隱式捕獲]:
有時我們在式子中要用到某些變量,但為了節(jié)約時間,我們不愿意把變量一個個寫上去,或者說,我們要用到的變量很多,不想一個個寫上去。那么我們這時就可以使用隱匿捕獲,讓編譯器根據(jù)我們的函數(shù)體來推斷我們將要用到那些變量。隱式捕獲有三種方式:
1) 隱式值捕獲
void func()
{
size_t v1 = 42;
auto f2 = [=]{return v1;}; // 值捕獲
v1 = 0;
auto j = f2();
}
上面的式子,捕獲列表中,我們用=代替了我們要捕獲的變量。
=表示采用值捕獲方式。
2)隱式引用捕獲
void func()
{
size_t v1 = 42;
auto f2 = [&]{return v1;}; // 引用捕獲
v1 = 0;
auto j = f2();
}
上面的式子,捕獲列表中,我們用&代替了我們要捕獲的變量。
%表示采用值捕獲方式。
3)混合使用隱式捕獲和顯式捕獲
void func()
{
size_t v1 = 42;
size_t v2 = 30;
auto f2 = [&,v2]{return v1 + v2;}; // 混合使用隱式和顯示捕獲
v1 = 0;
auto j = f2();
}
這里v2顯式地寫在捕獲列表中,則其它的變量都是隱式引用捕獲。在使用混合捕獲時必須知道幾點:
3.1) 捕獲列表中第一個元素必須是一個&或=。
3.2) 顯式捕獲的變量的捕獲方式必須與隱式捕獲的方式不同。如果上面如果我們寫成auto f2 = [&,&v2]{return v1 + v2;};這就是一個錯誤的lambda表達(dá)式,因為捕獲列表中,隱式的變量和顯示的變量都是引用方式。
[可變lambda]:
1)當(dāng)lambda表達(dá)式的一個形參是值拷貝的話,lambda是不會改變其內(nèi)的初始值的(還記得上面提到的那個未命名的類類型嗎?里面的調(diào)用重載函數(shù)是const類型的,它是不會改變類內(nèi)的變量的值的,而lambda只有一個可調(diào)用重載函數(shù)是允許用戶調(diào)用,所以這個const的成員函數(shù)沒有權(quán)限改變lambda內(nèi)的值。)如果下面的表達(dá)式:
void func()
{
size_t v1 = 42;
auto f2 = [v1]{return ++v1;}; // 值捕獲
v1 = 0;
auto j = f2();
}
這種情況下,如果我們?nèi)绻厦嬉粯樱淖僾1變量的值,那么編譯器會提示你必須是可修改的左值;要想確實修改這個值,我們可以用一個關(guān)鍵字解決:mutable。
(順便說下C++中最容易讓人忘記的幾個關(guān)鍵字吧,
第一個explicit:
這貨我們平時很少用,但如果你看過MFC的代碼,你會發(fā)現(xiàn),其實這貨還是很上鏡的。這個關(guān)鍵字是必須在類內(nèi)構(gòu)造函數(shù)中使用,而且,它還有幾個限制條件:1、構(gòu)造函數(shù)必須只允許接受一個形參,當(dāng)然,如果你有3個,而其它的都帶有默認(rèn)值,也是可以的。2、explicit只能在類內(nèi)定義。如果你想在類外explicit A::A(int a)這樣定義是不允許的。explicit是阻止了數(shù)據(jù)的隱式轉(zhuǎn)換,比如,A(double a){},當(dāng)我們int a = 10;A(a);時,會把int型的a類型上升為double,但如果explicit A(double a){}的話,A(a)則是不允許的。當(dāng)然,如果你要A((double)a)的話,這又是可以的。
第二個mutable:
當(dāng)某數(shù)據(jù)定義成mutable后,無論在任何時候他都是可變的。包括const類——事實上,const類也只能調(diào)用const成員變量和成員函數(shù),好吧,這個說了其實沒有意義,一句話,其實才是我想說的:就算是const成員函數(shù)中,它都是可變的。另外,mutable只能作用于非const的變量,如果mutable const int a; 這樣是不允許的。正因為這個關(guān)鍵字破壞了類的封裝性,所以我們是比較少用的。
第三個volatile:
這個是C引過來的?;蛘咦鲆浦矔r經(jīng)常用到,但說真的,平時根本用不上,它主要是告訴編譯器,不要對被它定義的變量進(jìn)行優(yōu)化,因為沒有用過,所以沒有什么好說的。
)
說完就回正文吧。
在我們把上面的lambda的參數(shù)列表首加上mutable,改為:
void func()
{
size_t v1 = 42;
auto f2 = [v1] () mutable {return ++v1;}; // 值捕獲
v1 = 0;
auto j = f2();
}
這樣是完全沒有問題的。上面的參數(shù)列表就算是空,你也必須帶上(),不然你編譯不過。
2)當(dāng)我們?yōu)橐貌东@時,變量應(yīng)該說是任何時候都是可變的,但有個例外,當(dāng)你的捕獲類型本身是const類型時,你的變量是無法改變的。例如:
void func()
{
const size_t v1 = 42;
auto f2 = [&v1] {return ++v1;}; //引用捕獲
v1 = 0;
auto j = f2();
}
這樣是錯誤的。或者聰明的你會想著,我可以加上mutable呀,這樣不就行了嗎?好吧,如果你是新手,會這樣想,說明你還是學(xué)得很快的。但事實是這樣的,值捕獲時,我們是直接拷貝父函數(shù)臨時變量的值的,我們lambda生成的類內(nèi)的變量不是const的(雖然我們的調(diào)用重載函數(shù)是const的),所以我們加上mutable實際上只是對我們lambda的類內(nèi)變量有影響,跟父函數(shù)的臨時變量沒有半毛錢關(guān)系。引用卻是不同的,當(dāng)我們定義一個引用時,比如下面代碼:
int a = 10; int b = 20; int &ref = a; ref = b; // 這樣我們是可行的,大家都能理解。
const int a = 10; const int b = 20; int &ref = a; ref = b; // const變量只能用const引用,紅色部分是不允許的,所以如果父函數(shù)的臨時變量是一個const變量,那么lambda的捕獲列表中的參數(shù)是不能更改的。
而一個const的變量,是無法加上mutable關(guān)鍵字的。
[指定lambda返回類型]:
前面說到了,如果lambda內(nèi)有一條return語句,則lambda會根據(jù)表達(dá)式推斷返回類型;如果沒有返回語句,就會返回void類型,比如下面的例子:
[](int i){return (i > 0) ? i : -i;}
它能很好工作,但如果變成這樣呢?
[](int i){ (i > 0) ? i : -i;}
這樣的話,沒有return,它只返回void,完全與我們意圖不符合。
如果改成這樣:
[](int i){ if(i > 0) return i; else return -i;}或者[](int i){ if(i > 0) return i; return -i;}
這樣是無法編譯通過的(有些編譯器還是可以通過的,VS2013就可以)。我們要說的只是指定返回類型。
我們可以這樣指定lambda表達(dá)式返回特定的類型:
[](int i)->int { if(i > 0) return i; else return -i;}
29、bind函數(shù)【標(biāo)準(zhǔn)庫】
在C++的標(biāo)準(zhǔn)庫中,所有與查找或者比較有關(guān)的算法函數(shù),都會接受一個所謂的謂詞(其實它就是一個函數(shù)指針的形參),這個謂詞一般情況下都會是一元謂詞(即只接受一個形參的函數(shù)),但往往只接受一個形參的函數(shù)并不能滿足我們的需求。比如,我們調(diào)用find_if,查找某個string內(nèi)所有滿足大于6個字母的單詞,這時我們可以定義一個比較函數(shù)(C++ Primer)。
bool check_size(const string &s,string::size_type sz)
{
return s.size() >= sz;
}
我們需要調(diào)用find_if,但這個函數(shù)的謂詞是一元謂詞,我們根本無法讓它指向一個只含一個形參的函數(shù)式。而C++11中提供了一個新的函數(shù)——bind;它會為我們生成一個類,跟lambda類似,它可能(我猜的)也含有一個重載了的調(diào)用運算符。它的語法如下(為什么這貨跟sokcet的bind同名呀……)使用它要包含頭文件#include <functional>,而且,它重載了很多個版本:
auto newCallable = bind(callable,arg_list);
bind()函數(shù)能作為通用適配器是因為它有“占位符”。它的占位符以_n的形式表示,數(shù)值n表示生成的可調(diào)用對象中參數(shù)的位置:_1表示第一個參數(shù)。_2表示第二個,依此類推。
我們可以這樣綁定check_size函數(shù),讓它“轉(zhuǎn)”化為只需要一個形參就可以調(diào)用的函數(shù)。
auto check6 = bind(check_size,_1,6);
這個表達(dá)式,用bind綁定了,生成一個新的函數(shù)。bind()內(nèi)的語句,我們可以這樣理解:首先,我們要綁定的是check_size這個函數(shù),然后,它的第一個參數(shù)用了一個占位符,表示我們要求在check6中有一個形參,用來代替_1的位置(即check_size的第一個參數(shù));最后的6表示第二個參數(shù)。
所以我們可以這樣調(diào)用check6:
string s = "test";
bool b1 = check6(s);
所以調(diào)用find_if可以這樣:
auto wc = find_if(words.begin(),words.end(),bind(check_size,_1,sz));
或者
auto wc = find_if(words.begin(),words.end(),check6(sz));
1)我們再來看下占位符還有什么特性。占位符位于placeholders名稱空間。
而占位符最特別的地方是可以重整參數(shù)順序,比如有一面這樣一條函數(shù):
auto g = bind(f,a,b,_2,c,_1);
這個函數(shù),g,只有兩個參數(shù),當(dāng)我們g(A,B)時,第一個參數(shù)會被_1占用,而第二個參數(shù)會被_2占用,所以g(A,B)實際上就是f(a,b,B,c,A),這樣調(diào)用的。利用這個原理,可以實現(xiàn)一個函數(shù),可以比較大于,也可以比較小于。比如有兩個過濾函數(shù),是比較小于的,但我們當(dāng)前有這樣一個函數(shù):
bool compare(int a,int b) { return a > b;}
如果bind(_2,_1),把a和b反過來,那就相當(dāng)于比較a < b。
2)bind的非占位符,默認(rèn)情況下,他們都是拷貝復(fù)制的,但有時我們確實需要他們進(jìn)行引用復(fù)制,我們可以用ref函數(shù)或者cref函數(shù)來解決非占位符參數(shù)的引用傳值,ref和cref都會生成相關(guān)的類。
ostream& print(ostream &os,const string &s,char c)
{
return os << s << c;
}
如果我們這樣綁定:
auto f = bind(print,os,_1,'');
這就是一個錯誤的表達(dá)式,因為ostream類型不允許復(fù)制,必須引用傳參。我們可以用ref函數(shù)解決。
auto f = bind(print,ref(os),_1,'');
跟ref一樣,我們還有一個cref函數(shù),只不過,它是生成一個保存const引用的類。
30、無序容器【容器】
我們都知道,維護(hù)元素的序列所花費的代價是很高的,在我們學(xué)數(shù)據(jù)結(jié)構(gòu)時,以最簡單的數(shù)組為例,我們?nèi)绻迦胍粋€數(shù)組,并且需要保持其序列性,就算有好的算法,但也難免要進(jìn)行多次的比較(注意,一般是通過比較運算符進(jìn)行的),當(dāng)數(shù)據(jù)結(jié)構(gòu)復(fù)雜,數(shù)據(jù)量大時,這個代價就是很大的。C++11為我們定義了4個無序關(guān)聯(lián)容器:unordered_set、unordered_multiset、unordered_map和unordered_multimap。他們跟其它有序容器比較,多了一個叫哈希策略的操作。事實上,無序容器在組織元素不再是按比較運算符進(jìn)行了,而是通過一個哈希函數(shù)(即使每個元素都有一個自變量,而能通過某一式子可以通過自變量得到結(jié)果的這個式子就叫哈希函數(shù))來組織的,所以,它也是散列的。這里不給具體的例子,這些容器的操作方式也是跟有序的大同小異的,具體自行參考MSDN.
31、智能指針【標(biāo)準(zhǔn)庫】
還記得前座賣文胸的山田同學(xué)嗎……額,說錯,還記得以前我們要寫一個內(nèi)置有指針數(shù)據(jù)類型的類有多痛苦嗎?
比如我們定義一個類,類內(nèi)需要一個能裝很多string類型的成員變量,它或者是讀取下載圖片的每個JSON的地址,如果我們有1000個圖片,就要維護(hù)1000個這樣的數(shù)據(jù),量是很大的,如果我們腦殘地在類內(nèi)把該成員定義成vector<string>,想像下,如果我們要賦值,如果我們要拷貝復(fù)制,那要浪費多少算法時間和內(nèi)存空間呀,或者你已經(jīng)想到,你可以重載賦值和拷貝復(fù)制函數(shù),直接傳遞一個指針,對,就是這樣,但我們要先解決引用的問題,如果我們這個類A test; A test2(test); 如果你這樣做,不處理好引用計數(shù)問題,當(dāng)我們test超過生命周期了,但test2是復(fù)制test的內(nèi)容的,所以指針的值也一起復(fù)制了;這樣,test2內(nèi)的數(shù)據(jù)成員就指向一個未知的內(nèi)存地址了。所以,我們需要智能指針,如果引用計數(shù),當(dāng)拷貝復(fù)制時,我們需要這個引用計數(shù)+1,這樣,當(dāng)我析構(gòu)時,我就能判斷,當(dāng)前引用計數(shù)數(shù)量,如果仍然有其它對象引用我當(dāng)前的內(nèi)存地址,我則不能馬上析構(gòu)。
以前的解決方案一般分為1、在每個類中定義一個含有引用計數(shù)的指針類,它種方式叫值類型。當(dāng)然,為了這個值類型(它一般定義成我們自定義類中的私有成員)能完整操作我們類內(nèi)的成員,完全可以定義成某一個類的友元,這樣就能訪問類內(nèi)私有成員了。2、定義一個叫句柄類的類,這個類并非像值類型一樣,在每個需要智能指針的類內(nèi)都要定義一個能用性較差的類,而這個類主要目的僅僅是提供引用計數(shù)的只剩。句柄類的行完全是一個普通指針的行為(通過重載一系列的操作符實現(xiàn)),當(dāng)它定義為模板類時,也解決了通用性的問題。而C++11標(biāo)準(zhǔn)庫中,就為我們新增加了這樣的句柄類型的智能指針類:shared_ptr,unique_ptr和weak_ptr。
1、shared_ptr
這個類就是一個中規(guī)中紀(jì)的句柄類,它的行為大致上跟一個指針完全是一樣的,只是多了幾個函數(shù)。(這部分不說了,自己看下頭文件或者查下資料吧,要說太長,也沒有必要)這個類主要與一個位于memory中的標(biāo)準(zhǔn)函數(shù)庫中的,叫make_shared的類配合使用,以做到最安全的分配(make_shared的返回類型就是一個與模板類型相關(guān)的shared_ptr類型)。詳細(xì)的類定義,不會寫,因為隨便網(wǎng)上找一大堆,我只是記錄學(xué)習(xí)筆記的而已,用法一般這樣。
class A
{
...........
private:
shared_ptr<vector<string>> = make_shared<vector<string>("1","2")>;
};
2、unique_ptr
這個類的行為,跟舊版本的auto_ptr有部分相似,但我并不想說他們的區(qū)別,畢竟我以前就幾乎沒怎么用過auto_ptr,真要用到智能指針,也是手動自己寫一個功能有限的。
unique_ptr正如其名,同一時刻,它永遠(yuǎn)只能得到一個對象的所有權(quán)。我們要記住一個名詞,就是當(dāng)對它賦值時,我們不叫某某指針指向什么值,我們應(yīng)該習(xí)慣性地說某某unique_ptr擁有某個值的所有權(quán),因為這樣我們更容易理解它的一些方法 。
首先,當(dāng)unique_ptr擁有某一個值的所有權(quán)時,我們不能簡單地通過賦值或者拷貝的方法對其進(jìn)行重新綁定其它值,我們只能通過它內(nèi)的release或reset改變其所有權(quán)。如:
unique_ptr<int> p1(new int(0)); // unique_ptr的構(gòu)造接受的是一個指針變量
unique_ptr<int> p2 (new int(1));
p2 = p1; // 這是不允許的
我們可以用如下方法改變其所有權(quán)。
1) 在構(gòu)建p2時傳一個指針,而這個指針是由p1釋放后返回的
// 記住這里,p1.release()返回的是p1這個智能指針容器所保存的指針
unique_ptr<int> p2 (p1.release());
注意:release()只是切斷p1這個智能指針容器對new int(0)這個語句分配的指針的控制,但它只是返回這個指針的值,而并不會自動delete這個指針,所以當(dāng)我們直接p1.realease()這樣操作,不僅不會釋放掉new int(0)這個內(nèi)存塊,還會造成我們無法再次找回這個指針的地址。
2)利用reset釋放
reset()有一個可選參數(shù),這個參數(shù)是可以釋放指針?biāo)赶虻膶ο蟛⒅弥羔槥榭?比如p1.reset()就會釋放p1所指向的內(nèi)存塊,并讓p1這個指針為null,這就是可以解決上述紅色部分的問題。當(dāng)它帶一個參數(shù)時,這個參數(shù)表示重新指向給定的指針,如:
p2.reset(p1.release());
這個表達(dá)式就是先釋放p2所指向的內(nèi)存塊,再重新指向p1的內(nèi)存地址?;蛘咧匦路峙涞刂?。
p2.reset(new int(3));
3、weak_ptr
現(xiàn)在的高級語言,我們通常都會聽到所謂的“弱指針”,比如OC,經(jīng)常需要用到weak,strong來給變量定義一個屬性?,F(xiàn)在C++11也引進(jìn)了這個“弱”的概念,雖然跟OC還是有點區(qū)別,但其實作用卻是一樣的,都是為了更安全地處理指針。
一個weak_ptr必須與一個shared_ptr綁定才能使用,而且,就算綁定了也不會增加shared_ptr的任何引用計數(shù)。正因為這種與其綁定或者說共享的對象的關(guān)系是如此不密切,所以才說它是"弱",就是說,他只是象征性指向一個對象,而不論這個對象是否已經(jīng)銷毀。
auto p = make_shared<int>(42); // 創(chuàng)建一個shared_ptr對象
weak_ptr<int> wp(p); // wp綁定p,p引用計數(shù)不變
正是因為它不增加,所以我們明顯不能直接無所顧忌地使用它對我們的指針對象進(jìn)行操作(果然是弱呀,這跟我們內(nèi)置的指針差不多,唯一的好處就是作為一個類,還是有不少函數(shù)可以使用,大大減少我們的寫代碼的數(shù)量,如果我們需要臨時的指針指向shared_ptr,而又不想改變原有對象的生命周期(引用計數(shù)是影響對象的生命周期的),我們就可以用用這個弱得很的智能指針了),我們需要先用lock成員函數(shù)來判斷wp所綁定的對象是否仍然存在。當(dāng)存在時就返回這個shared_ptr對象,否則就返回一個空的shared_ptr對象。
if(shared_ptr<int> np = wp.lock()) // 返回一個shared_ptr對象,if這個對象不為空時才執(zhí)行下面的語句
{
// 對wp返回的shared_ptr進(jìn)行操作
}
32、定位new
舊標(biāo)準(zhǔn)的C++,當(dāng)我們new一個內(nèi)存區(qū)域時,如果空間不夠,會直接拋出一個bad_alloc的錯誤,并中斷程序,而指針的值側(cè)為null?,F(xiàn)在,C++11有一種新形式,我們稱之為定準(zhǔn)new,它我們向new傳遞額外的參數(shù),如:
int *p = new (nonthrow) int; // 當(dāng)分配失敗時,不拋出bad_alloc,而且,p指針為nullptr
上面的表達(dá)式,向new傳遞一個nonthrow的參數(shù),告訴new,不拋出異常。
33、類內(nèi)初始值
創(chuàng)建對象時,類內(nèi)初始值將用于初始化數(shù)據(jù)成員。沒有初始值的成員將被默認(rèn)初始化。如果我們沒有提供任何的初始值,也沒有在構(gòu)造函數(shù)里定義任何相關(guān)的初始化操作。那就按舊版本一樣,全局的內(nèi)置類型由系統(tǒng)決定初始值,局部的仍然是不確定的值。
class CC
{
public:
CC() {}
~CC() {}
private:
int a = 7; // 類內(nèi)初始化,C++11 可用
}
34、允許使用作用域運算符來獲取類成員的大小
在標(biāo)準(zhǔn)C++,sizeof可以作用在對象以及類別上。但是不能夠做以下的事:
struct ST
{
int a = 0; // 聲明時就必須要定義
static int b;
};
cout << sizeof(ST::b) << endl; // 靜態(tài)成員本來就是這樣取值的,所以跟新的有沖突,只有非staic可以
cout << sizeof(hasY::a) << endl; // 直接由hasY型別取得非靜態(tài)成員的大小,C++03不行。 C++11允許
以前我們要取一個數(shù)據(jù)的大小,必須要在有類生成了相應(yīng)的實例后才行。
35、=default
舊版本的C++中,當(dāng)一個類內(nèi)我們沒有定義構(gòu)造函數(shù)、拷貝構(gòu)造函數(shù)、拷貝賦值運算符和析構(gòu)函數(shù)時,則編譯器會為我們定義一個合成的版本以讓我們的類能正常運行。對于默認(rèn)構(gòu)造函數(shù),當(dāng)我們定義了自己版本的構(gòu)造函數(shù),編譯器則不會再為我們合成默認(rèn)構(gòu)造函數(shù)了,而對于另外的三個成員函數(shù),無論我們是否定義了自己的版本,編譯器都會為我們合成一個。(事實上,當(dāng)一個類需要我們自己來控制其析構(gòu)時,也幾乎肯定了必須要我們定義自己的拷貝構(gòu)造函數(shù)和拷貝賦值運算符)。
C++11為我們提供了一個=default來顯式地要求編譯器生成合成的版本。
class test
{
public:
// 這個構(gòu)造函數(shù)跟編譯器合成的默認(rèn)構(gòu)造函數(shù)完全一樣,但它100%不是我們編譯器為我們合成的而是我們自己寫上的,所以我們需要=default讓編譯器生成合成的版本,這樣這個構(gòu)造函數(shù)的作為就跟編譯器默認(rèn)為我們合成的合成構(gòu)造函數(shù)的位為一樣。
test() {} = default;
test(bool b) = default
{
cout << b << ednl;
}
test(const test&) = default; // 拷貝構(gòu)造函數(shù)
test(const test& T,int i = 0);{}// 拷貝構(gòu)造函數(shù),他們能重載,但這個不能加=default后面會說
// 下面這個只是一個帶兩參數(shù)的構(gòu)造函數(shù),因為C++中對拷貝構(gòu)造函數(shù)定義是這樣的:如果一個構(gòu)造函數(shù)的第一個參數(shù)是自身類類型的引用,且任何額外參數(shù)都有默認(rèn)值,則此構(gòu)造函數(shù)是拷貝構(gòu)造函數(shù)。這個對于下面說的移動拷貝構(gòu)造函數(shù)同樣適合。
test(const test& T,double i);{}
test& operator=(const test&); // 拷貝賦值運算符
~test() = default; // 析構(gòu)函數(shù)
};
test& test::operator=(const test&) = default; // 類外指定,即在類定義時指定=default
=default在類內(nèi)出現(xiàn)時,所修飾的成員函數(shù)都為inline版本,而類外出現(xiàn)時,則為非inline版本。
嗯,我剛接觸這個時,做了一件很腦殘的事。上面有注譯。這是編譯過不去的,不多說,我只是腦殘地試了下。
test(const test&,int i = 0) = default; // 錯誤
當(dāng)然,如果沒有=default,這個成員函數(shù)是正確的,它是一個拷貝構(gòu)造函數(shù),當(dāng)這種除了第一個參數(shù)是本身類的引用外,如還帶有額外參數(shù)時,這個額外的參數(shù)必須要有默認(rèn)實參——如果它不帶默認(rèn)實參的話,就相當(dāng)于是有兩個(這點對拷貝賦值運算符是無效的,因為operator=對參數(shù)是有要求的),但無論如何,它都不是正確的由編譯器合成的拷貝構(gòu)造函數(shù)的格式。
=default可以肯定地說,它只能用于能合成的成員函數(shù)。
注意:上述的我們一般只聲明而不定義,因為如果我們定義的話就跟=default有沖突,=default完全是說明由編譯器生成我們成員函數(shù)內(nèi)的其它代碼,所以它只是聲明而非定義。
36、刪除函數(shù)
如果我們用過單例這種設(shè)計模式時,又剛好需要定義一個類,這個類不能多次構(gòu)造,只能在整個程序中只有一個實例存在,這時我們一般都會阻止這個類有構(gòu)造函數(shù)。還記得我們一般做法是如何的嗎?是的,我們通過先寫一個構(gòu)造函數(shù),通過這個函數(shù)阻止了編譯器的合成構(gòu)造函數(shù),再把這個構(gòu)造函數(shù)定義為private讓它不能在類外調(diào)用;然后再小心翼翼地處理拷貝和拷貝賦值運算符。讀過這種代碼的人都清楚,如果沒有人告訴你這個是一個單例,我估計你真得愣個10來秒才能看明白意圖。
現(xiàn)在C++11為我們提供了更直觀的方法(C++11新增了很多后置的關(guān)鍵字,包括后面會提到的override,都是使語言看起來更簡潔,C++開始不斷向簡潔的語言風(fēng)格進(jìn)化著)。通過使用=delete來刪除能合成的成員函數(shù)。=delete有一個跟=default不同的地方,即=delete必須定義在類的聲明中,以確保編譯器能盡早知道那個函數(shù)是我們指定刪除的。而類是否合成默認(rèn)版本,則是編譯器在類定義時決定的。所以=default和=delete這種差異,并不與C++的原本邏輯相悖。
class test
{
public:
test() {} = delete;
test(const test&) = delete; // 拷貝構(gòu)造函數(shù)
test& operator=(const test&) = delete; // 拷貝賦值運算符
~test() = delete; // 析構(gòu)函數(shù)
};
用這個方法,在設(shè)計類似單例模式這種類時,我們可以不需要管我們的類的屬性是否為private,直接后置=delete就行了。你也可以把拷貝和賦值都干掉,這個實例類連引用計數(shù)都可以不用了。但這個類卻只有簡單的處理邏輯的功能了。這是題外話,不多說。
另外,當(dāng)我們把一個析構(gòu)函數(shù)定義為不可訪問或者刪除函數(shù),我們的合成構(gòu)造函數(shù)和拷貝構(gòu)造函數(shù)(拷貝賦值運算符仍然能合成,因為有時我們雖然不能構(gòu)造A對象,但如果B派生了A,在B有構(gòu)造函數(shù)的話,仍然可以進(jìn)行賦值操作的,當(dāng)然,A現(xiàn)在無法構(gòu)造,這個前提也不成立,但C++語義就是允許合成拷貝賦值運行符)是被設(shè)置成=delete的,因為不能釋放對象,所以編譯器也不允許我們創(chuàng)建對象,但我們?nèi)匀豢梢宰远x構(gòu)造函數(shù),達(dá)到創(chuàng)建對象的目的,但這樣的話,如果存在指針對象,就會無法釋放了。
class hasY
{
public:
hasY() = default;
hasY(const hasY&)
{
cout << "copy" << endl;
}
hasY& operator=(const hasY&)
{
cout << "=copy" << endl;
return *this;
}
~hasY() = delete;
};
hasY hy;
hasY hy2 = hy;
hasY hy3;
hy3 = hy;
當(dāng)我們定義了一個移動構(gòu)造函數(shù)和/或一個移動賦值運算符時,則該類的合成拷貝函數(shù)和拷貝賦值運算符將會被定義為刪除的。但100%情況是當(dāng)需要自定義移動操作函數(shù)時,我們都需要先定義拷貝操作函數(shù)??偠灾苿硬僮骱瘮?shù)和拷貝操作函數(shù)是相類的。
*拷貝構(gòu)造函數(shù)和拷貝賦值函數(shù)的不同之處
這個不算C++11的內(nèi)容,是老生常談了,但我還是想寫,主要是如果不弄明白,移動操作函數(shù)和拷貝操作函數(shù)之間的相互關(guān)系可能就會亂了(注意那個藍(lán)色的和/或)。在C++中,列表初始化,初始化,賦值,賦值初始化是要區(qū)分的:
int i(0); // 舊版初始化
int i{0};或int i ={0}; // C++11列表初始化
int i = 0; // 這是賦值初始化,是初始化
int j; j = i; // 這個才是賦值,而拷貝操作函數(shù)也有相同性質(zhì)。
class hasY
{
public:
hasY() = default;
hasY(const hasY&)
{
cout << "copy" << endl;
}
hasY& operator=(const hasY&)
{
cout << "=copy" << endl;
return *this;
}
};
1、hasY hy; //調(diào)用合成構(gòu)造函數(shù)
2、hasY hy2 = hy; // 輸出copy,編譯器跳過=號,相當(dāng)于hasY hy2(hy)
3、hasY hy3; // 調(diào)用合成構(gòu)造函數(shù)
4、hy3 = hy; // 輸出=copy
上面的代碼,可能很多人會覺得2處是調(diào)用拷貝賦值運算符,其實這個跟前面的內(nèi)置類型一樣,這個是賦值初始化,而是賦值。有些書上會叫賦值構(gòu)造函數(shù),所以我為什么要叫拷貝賦值運算符就是不想弄亂。
37、右值引用
C++11為了支持移動操作,新引用了一種名為右值引用。正如其名,右值引用必須綁定到右值。對于左值和右值的定義,可能一時是難以理解的,按概念來說,它已經(jīng)不是以前C那樣以等號左側(cè)或右側(cè)來區(qū)分了,所以也變得更艱澀難明了。但我們只記住這樣一點:
有名字且可以取地址的就是左值;沒有名字且無法取地址的就是右值。(const左值引用算例外)
這樣從代碼行為上來理解,會更容易掌握點。
我用通過&&來獲取一個右值的引用,如:
int i = 0;
int &r = i; // 正確,左值引用
int &&rr = i; // 錯誤,i有名字能取地址,是一個左值,左值不能綁定右值引用
// 錯誤,i * 42這個表達(dá)式會生成一個臨時變量,我們無法取地址,所以它是一個右值,右值不能綁定左值引用int &r2 = i * 42;
int &&rr2 = i * 42; // 正確,右值引用綁定一個右值
// 正確,但這個引用之所以成立,是被const限制的,const要求被綁定的值是一個右值,所以這個表達(dá)式成立。const int &r3 = i * 42;
當(dāng)一個原則上不能再改變的右值被成功綁定后,我們就能通過右值引用這個別名對其進(jìn)行與左值變量相同的操作,比如改變其值。因為它引當(dāng)于有名字了且可以取地址了。
int &&rr2 = i * 42;
cout << rr2 << " " << ++rr2 << endl; // 輸出為42 43
盡管要求不能把一個左值綁定到一個右值引用,但C++11中仍然有方法解決這一問題。
可以可以使用std::move(別落下std::,下面會解釋的)來把一個左值顯式地轉(zhuǎn)換為右值。
int &&rr3 = std::move(r);
std::move的作用就是返回一個右值引用,這個右值引用綁定的值是把move的參數(shù)r通過強制類型引用轉(zhuǎn)換成右值類型。通過引用折疊機制,move的參數(shù)可以接受一個左值也可以接受一個右值,但無論如何,我們應(yīng)該都要遵守:盡量不使用一個移后源對象的值。其實看下頭文件我們也可以看到,std::move就是把類型轉(zhuǎn)換下再返回而已(它本身是模板函數(shù),類型會根據(jù)傳入的參數(shù)類型進(jìn)行實參推斷),但它返回的是與原來類型相關(guān)的右值引用,即type&&類型。
正因為只是進(jìn)行類型轉(zhuǎn)化,所以,無論我們++i還是++r,都能改變i的值,但不提倡這樣使用。(C++ primer這部分我真不理解,郁悶!不過,大家用了std::move后,除了重新賦值和釋放這個變量外真不要作任務(wù)操作,這是C++11的約定,大家應(yīng)該都遵守,如果你要搞特化,別人看你代碼,不僅看得一頭霧水,還要罵句SB的。)
還有更重要的一點:右值被引用后,即右值引用本身也是一個左值。更詳細(xì)的請看下面的引用折疊。
38、移動構(gòu)造函數(shù)
移動構(gòu)造函數(shù)是C++11新引進(jìn)的概念,目的在于讓當(dāng)以某個臨時變量,或者說即將結(jié)束生命周期的對象(也就是右值)作為構(gòu)造參數(shù)時,可以達(dá)到比舊版的拷貝構(gòu)造更快的效率。移動構(gòu)造函數(shù)跟拷貝構(gòu)造一樣,要求每個參數(shù)必須為一個本身類類型的右值引用,任何額外的參數(shù)都必須有默認(rèn)實參。先拿個例子比較下拷貝函數(shù)和構(gòu)造函數(shù)區(qū)別吧。
class A
{
public:
A(){cout << "construct" << endl;} //1
A(const A&){cout << "copy" << endl;} //2
A(const A&&) {cout << "move" << endl;} //3
};
void main()
{
A a;
A b(a);
A c(std::move(b));
}
上面代碼輸出的結(jié)果如下圖:
紅框里的輸出結(jié)果就是了。這里應(yīng)該很清晰了,當(dāng)我們利用std::move把類b轉(zhuǎn)成一個右值時,就調(diào)用了移動構(gòu)造函數(shù)。前面我們的例子太簡單了,一般的移動構(gòu)造函數(shù)至少要遵守這樣的原由:必須讓被竊取的對象與原來的資源切斷聯(lián)系,這里特別要注意指針。
class A
{
public:
// 這里不能用const,因為下面要更改source內(nèi)的值
A(const A&& source) noexcept:a(source.a),b(source.b)
{
source.a = source.b = nullptr;
}
public:
int *a;
char *b;
};
上面的移動構(gòu)造函數(shù)的實質(zhì)是怎么樣的呢?
從圖中可以看出,移動構(gòu)造函數(shù)實質(zhì)就是“竅取”資源,它本身不分配任何資源,所以也沒有調(diào)用到相應(yīng)的構(gòu)造函數(shù)。上圖這種情況,當(dāng)我們“竅取”了b的資源后,所以b的資源的歸屬權(quán)已經(jīng)變更了,根據(jù)移動函數(shù)的約定:當(dāng)移動后的源應(yīng)該是銷毀無害的,但如果我們不處理的話,當(dāng)我們b生命周期結(jié)束,肯定會影響到對象c的。
事實上,移動構(gòu)造函數(shù)是解決指針,特別是臨時指針在復(fù)制時,如果數(shù)據(jù)量消耗效率的問題。比如上面的函數(shù),如果我們是一個內(nèi)置類型,結(jié)果會如何呢?:
class A{
public:
// 因為有了移動構(gòu)造函數(shù),不會再合成默認(rèn)構(gòu)造函數(shù)了
A():a(0),b(new char('a')){}
A(A& source):a(source.a)
{
// 不像移動構(gòu)造函數(shù),拷貝構(gòu)造函數(shù)一般都是另外分配內(nèi)存空間后再進(jìn)行賦值
b = new char(*(source.b));
}
// 這里不能用const,因為下面要更改source內(nèi)的值
A(A&& source) noexcept:a(source.a),b(source.b)
{
source.b = nullptr;
}
public:
int a;
char *b;
};
void main()
{
A a;
cout << (void*)a.b << endl;
A b(std::move(a));
cout << &(a.a) << " " &(b.a) << endl;
cout << (void*)a.b << " " << (void*)b.b << endl;
cout << &(a.b) << " " << &(b.b) << endl;
}
如果你前面讓我誤導(dǎo)了(我承認(rèn)我那圖很爛,我們我只寫了堆和棧,但并不詳盡,反正堆一般是new分配的),認(rèn)為移動函數(shù)是一種機制,那輸出的結(jié)果就讓你哭了。
說到底,移動函數(shù)也是符合C++語義的,如果你什么都不做,它也是什么都不處理。上面的輸出結(jié)果中,a和b是兩個類,有各自的內(nèi)存空間,所以輸出內(nèi)置型的數(shù)據(jù)時(它在棧上,包括b這個指針本身,也是在棧上),它因為是兩個對象分別控制的,所以第二行輸出的地址不同;第三行就比較有意思,它輸出的是移動后的源和當(dāng)前變量的指針地址,它們是相同的,為什么第一個是空指針呢?因為我們在移動后,把移后源設(shè)置為nullptr,但我在第一行cout輸出了未清空的地址。第四行,它也是輸出棧上的指針變量,所以它的地址也是不同的。
考慮一下,只是為了解決指針的復(fù)制問題,我們?nèi)匀豢梢岳每截悩?gòu)造函數(shù)執(zhí)行跟移動構(gòu)造函數(shù)一樣的操作(代碼一樣時),但這樣雖然有移動構(gòu)造函數(shù)的功能,但它有兩個問題(假設(shè)舊版本沒有std::move情況下):
1、它無法將一個臨時變量快速處理(即無法支持右值引用)
2、假設(shè)我們對拷貝構(gòu)造函數(shù)這樣做了,那很明顯,當(dāng)我們需要對象連堆都一起分離出來該怎么辦呢?所以說,這種情況下,我們這樣做是不現(xiàn)實的。事實上它也一般定義為const。C++引進(jìn)移動構(gòu)造函數(shù)后,就魚和熊掌都可兼得了。
最后,我們注意到那個noexcept,這也是C++11新引進(jìn)的。它位于參數(shù)列表和puvcwx列表開始的冒號之間。它指示程序不拋出任何異常,其實可以想像,我們只是“竅取”數(shù)據(jù),并沒有進(jìn)行任何移動類的操作,所以根本不會發(fā)生什么錯誤。標(biāo)準(zhǔn)庫中的vector類中的pushback也有這種東西,失敗時并不會彈出一個異常。
39、移動賦值函數(shù)
移動賦值函數(shù)跟移動構(gòu)造函數(shù)差不多,只是比后者多了兩個步驟:
1、每次賦值前,都要先把原來的數(shù)據(jù)清空。如果有數(shù)據(jù)成員指向分配好的內(nèi)存空間,我們側(cè)要釋放這部分內(nèi)存。不然肯定會出現(xiàn)懸掛問題的。
2、解決自賦值問題,這個問題拷貝賦值運算符也有的,當(dāng)一個對象是其自身時(雖然我們一般不會遇到這種情況,但仍然要小心有些程序員就是二愣子),我們不應(yīng)該進(jìn)行賦值,特別是清空數(shù)據(jù)問題,如果我們在賦值前清空了數(shù)據(jù),則源對象因為是自身,也會馬上清空數(shù)據(jù)的,這樣我們的賦值不僅沒有意義,還要導(dǎo)致錯誤。
以前面的例子為例,我們弄一個簡單的移動賦值函數(shù)。
class A{
public:
// 因為有了移動構(gòu)造函數(shù),不會再合成默認(rèn)構(gòu)造函數(shù)了
A():a(0),b(new char('a')){}
A(A& source):a(source.a)
{
// 不像移動構(gòu)造函數(shù),拷貝構(gòu)造函數(shù)一般都是另外分配內(nèi)存空間后再進(jìn)行賦值
b = new char(*(source.b));
}
// 這里不能用const,因為下面要更改source內(nèi)的值
A(A&& source) noexcept:a(source.a),b(source.b)
{
source.b = nullptr;
}
A& operator=(A&& source) noexcept
{
if(this != &source) // 比較地址,不能自身進(jìn)行賦值構(gòu)造
{
// 清空數(shù)據(jù)
free();
this->a = source.a;
this->b = source.b;
// 記得處理好移動后源的數(shù)據(jù),指針的一定要清空,保證它銷毀是無害的
source.b = nullptr;
}
return *this;
}
public:
int a;
char *b;
};
40、編譯器合成的移動構(gòu)造函數(shù)和移動賦值函數(shù)
我們都知道,在C++11中(舊版本也是一樣只是沒有=delete行為),除非我們顯式刪除拷貝構(gòu)造函數(shù)和拷貝賦值運算符,不然編譯器都會為我們合成一個默認(rèn)的版本。但對于新增加的移動構(gòu)造函數(shù)和移動賦值函數(shù),一般情況下,編譯器根本不會為我們合成這些移動操作的函數(shù),但在特定的條件下,卻還是會為我們合成移動操作的函數(shù)。這里我們還要加上另一條重要的信息:定義一個移動函數(shù)或者移動賦值運算符的類必須也定義自己的拷貝操作法函數(shù)。否則,這些成員默認(rèn)地被定義為刪除的。
1、當(dāng)我們有任何版本的自定義拷貝構(gòu)造函數(shù),拷貝賦值運算符或者析構(gòu)函數(shù)時,編譯器不會為我們合成移動操作類函數(shù)(有任何一個拷貝操作函數(shù)時都不會再合成移動操作函數(shù))。對于存在拷貝構(gòu)造函數(shù)和拷貝賦值運算符,我們可能比較容易理解,因為當(dāng)我們定義了這兩個函數(shù)后,就算沒有移動操作函數(shù),我們的類仍然能工作得很好:對于臨時變量,我們可以把臨時變量的堆數(shù)據(jù)完整拷貝過來。說到低,移動操作類函數(shù),只是為了讓類工作得更快,而非不可缺。但對于析構(gòu)函數(shù),我們可能會有所疑惑,我們應(yīng)該樣想:要析構(gòu)的地方,都是我們的類存在指針時,而當(dāng)類需要析構(gòu)函數(shù)時,幾乎也肯定了我們必須需要拷貝構(gòu)造函數(shù)和拷貝賦值運算符了。而也正因為帶有指針操作,編譯器無法推斷我們需要做的操作,在不安全的前提下,C++11規(guī)定(我說了,移動操作函數(shù)只是為了類更好工作,而非不可缺)有析構(gòu)函數(shù)時不合成移動操作函數(shù)是明智的。
2、當(dāng)類沒有定義任何版本的拷貝控制成員時(即有合成的卻沒有自定義的拷貝構(gòu)造函數(shù)、拷貝賦值運算符或者析構(gòu)函數(shù)時),且類的每個非static數(shù)據(jù)成員都可以移動(前面說了,這里移動其實就指“竅取”,但有些成員是無法移動的,我們都知道,盡管內(nèi)存空間可以不變,但棧內(nèi)的指向該空間的指針變量是不同位置的,即移后源和當(dāng)前類棧內(nèi)的變量是不同的,這樣應(yīng)該清楚了吧,比如,類內(nèi)有自定義類時時,而該類本身又沒有定義移動操作函數(shù)的這種情況就是其中一種存在不可移動數(shù)據(jù)成員的情況了。),編譯器才會為我們合成移動構(gòu)造函數(shù)或移動賦值函數(shù)。
但無論如何,編譯器都不會隱式把移動操作函數(shù)定義為刪除函數(shù)。
再來討論下=default和=delete,后者是幾乎用不上的,它不同于拷貝操作函數(shù),就算有自定義的,編譯器也會為我們合成,移動操作函數(shù)原來能合成的條件就比較苛刻,所以真的幾乎用不上。至于=default,它的行為就有點奇怪了,就算我們顯示定義一個移動操作函數(shù)為=default時,它卻在特定條件下,無視=default而表示為刪除函數(shù)。情況有如下這些(以上條件都是在顯示=default時才會發(fā)生,沒有的話只會發(fā)生前面提到的那2種情況):
1、當(dāng)顯示定義=default移動操作函數(shù)時,但類本身卻有成員不能移動,則編譯器會把移動操作定義為刪除函數(shù)。(這種情況幾乎都是類內(nèi)有類成員變量,且該類沒有定義移動操作函數(shù)才出現(xiàn)的)如:
struct hasY
{
hasY() = default;
hasY(hasY&&) = default; // 因為Y,它是=delete的
//假設(shè)這里的類Y定義有自己的拷貝構(gòu)造函數(shù)(不會再合成移動操作函數(shù)),
卻也沒有定義自己的移動操作法函數(shù)
Y mem;
};
hasY hy,hy2 = std::move(hy); // 錯誤,沒有移動構(gòu)造法函數(shù),它是刪除的
2、如果類成員內(nèi)有成員的移動構(gòu)造函數(shù)或移動賦值運算符被定義為刪除的(用=delete顯示定義刪除,或編譯器隱式在特定條件下把=default改變?yōu)?delete,再次說明,編譯器永遠(yuǎn)不會在正常情況下把移動操作函數(shù)隱式定義為=delete)或者聲明為不可訪問的(即private或protected),則類的移動構(gòu)造函數(shù)或移動賦值運算符被定義為刪除的。
3、類似拷貝構(gòu)造函數(shù)(33點說到)。如果析構(gòu)函數(shù)被定義刪除或不可訪問的,則類的移動構(gòu)造函數(shù)被定義為刪除的。移動賦值運算符仍然有效,但卻無意義。
4、類似拷貝賦值運算類。當(dāng)類中有const成員或者引用時,則類的移動賦值運算符定義為刪除的。這里順便解釋下為什么拷貝賦值運算符在類中有const成員或者引用時會被刪除合成的。我們都知道,在C++中,const變量和引用類型變量,都是在定義的同時要初始化的,而類中,const變量是在聲明時直接定義的,而引用類型則是是在構(gòu)造函數(shù)中進(jìn)行列表化初始化的,所以這種情況下,拷貝賦值運算符不能復(fù)制const對象,他只會自己原生有一個const變量,值不會變。它也不能復(fù)制引用成員,因為他必須在構(gòu)造函數(shù)的列表中給定,所以編譯器為了更安全點,直接就是在遇到這種情況下,不再合成拷貝賦值運算符,而是交由程序員自己處理了。移動賦值運算也是相同原理。移動拷貝仍然可用,跟拷貝構(gòu)造函數(shù)一樣,合成的拷貝函數(shù),逐值拷貝,把拷貝源對象的值作為新構(gòu)造對象的值傳遞。
我們再談下關(guān)于拷貝操作函數(shù)和移動操作函數(shù)之間的交互。如果一個類,本身沒有定義任何的移動操作函數(shù),那么,我們能否處理一個右值呢?假如有如下這樣一個類:
class A
{
public:
A() = default;
A(const A&) // 定義了拷貝構(gòu)造函數(shù)后,編譯器不會再合成任何的移動構(gòu)造函數(shù)
{
cout << "copy" << endl;
}
};
A x;
A y(x); // 拷貝構(gòu)造函數(shù),x為一個左值,它能取地址,有名稱的
A z(std::move(x));
//盡管std::move后的類型為A&&類型,但沒有移動操作函數(shù)時,仍然是調(diào)用拷貝構(gòu)造函數(shù),而且是肯定安
全的。這時會進(jìn)行隱匿的類型轉(zhuǎn)換,把一個右值引用A&&轉(zhuǎn)換為一個const A&引用,即使我們拷貝構(gòu)造函數(shù)
定義為A(&),它仍然能接受一個A&&的右值類型。
*前面說右值引用時提過的const變量應(yīng)該記得吧?任務(wù)一個引用都要求右邊的值是左值,但如果是const引
用,則右邊的值可以是一個右值,因為const要求一個常量值,而常量值都是一個右值。構(gòu)造函數(shù)形參可以
是非const而安全接受右值,其實原理也相當(dāng)簡單:對于構(gòu)造函數(shù)來說,主要責(zé)任是構(gòu)造一個新對象,即便修改
了右值,也無所謂,反正臨時的東西,扔了就行了。但是普通函數(shù),目的不是為了別的,就是為了處理傳進(jìn)來
的值,傳進(jìn)來的引用,是既能in又能out的。所以這里如果非const引用一個右值,那肯定會讓out的結(jié)果失效。
所以對于普通函數(shù)而言,要想接受一個右值必須是一個const類型的形參;沒有所謂的機制,只是編譯器是這
樣定義的。
void hit(const int &) //假如沒有const說明右邊可以是右值,則std::move后的值不能用于參數(shù)
{
cout << "function" << endl;
}
int iv = 4;
hit(std::move(iv)); // 正確
類的輸出結(jié)果:
有個提醒,我不建議在除了類定義外的移動操作函數(shù)。因為它非常不穩(wěn)定,你不能確定移后源對象的確切狀態(tài),是否仍然有其它的對象在使用它。我們說過,移后源應(yīng)該是可以隨時釋放清空而不影響其它操作的,而且,我們不能對其內(nèi)數(shù)據(jù)進(jìn)行操作。但誰又能100%知道當(dāng)前移后源的狀態(tài)呢,特別是大項目中,這個更是致命。
41、移動迭代器【標(biāo)準(zhǔn)庫】
新標(biāo)準(zhǔn)庫中新增加了一種移動迭代器適配器,它主要通過調(diào)用標(biāo)準(zhǔn)庫的make_move_iterator函數(shù)將一個普通迭代器轉(zhuǎn)換為一個移動迭代器,而且,返回后的迭代器的解引用運算返回的是一個右值引用。還記得標(biāo)準(zhǔn)庫中新加入的begin()和end()嗎?主要我們這樣操作:
make_move_iterator(begin())就可以返回一個移動迭代器,想像一下,如果你的類支持移動操作,如果直接傳遞一個右值就可以達(dá)到優(yōu)化效率的效果。
38、引用函數(shù)
在說這個之前,我們先回故一下類內(nèi)有關(guān)const區(qū)分重載版本的內(nèi)容。
看到了嗎?我們的const版本函數(shù),就算跟普通成員名稱,返回值和參數(shù)完全一致,但它仍然是一個重載版本。而對于以const作為形參的函數(shù)。卻不是重載函數(shù),因為C++規(guī)定,頂層const作為形參時,是會被忽略const標(biāo)識的。
還有一種情況。
string s1,s2;
s1 + s2 = "test";
上面這段代碼,s1+s2后是由s1返回一個臨時變量(即右值),但我們卻對右值進(jìn)行了重新賦值。當(dāng)然,在語法上是沒有問題,但使用上卻幾乎無法重獲這個"test"字符串,除非我們std::move()了它。這種用法是很普遍的,但我們卻很想禁止這種用法。以前不行,C++11為我們準(zhǔn)備了新的工具。
好了,現(xiàn)在我們說下新引進(jìn)的“引用函數(shù)”吧。
1、語法
我們通過在移動操作函數(shù)或拷貝操作函數(shù)后增加引用限定符,就可以阻某些用法。
引用限定符(只能用于非static)有兩種,分別是"&"和"&&",前者表示this(類對象本身)只可以指向一個左值,而后者表示this只可以指向一個右值。
class Foo
{
public:
Foo &operator= (const Foo&) &;
};
Foo& Foo::operator=(const Foo& )&
{
return *this;
}
這個類的賦值運算符只允許左側(cè)的操作數(shù)即類對象本身是一個左值。如果我們這樣操作:
Foo f,i;
std::move(f) = i;
這是無法成立的。
2、重載和引用函數(shù)
C++11中新增加的這種引用函數(shù),跟const成員函數(shù)一樣,可通過引用限定符來區(qū)分重載版本。
前面已經(jīng)看到,我們定義const成員函數(shù)時,可以定義兩個版本,唯一罰沒是一個有const限定,一個沒有。而對于有引用限定符的成員函數(shù),有一點不一樣:*如果我們定義兩個(兩個是重載的最低要求,不然還叫什么重載)或兩個以上具有相同名字和相同參數(shù)列表的成員函數(shù)(C++中,返回值不作為重載的考量依據(jù))時,就必須對所有重載的幾個函數(shù)都加上引用限定符,讓它變?yōu)橐孟薅愋偷暮瘮?shù),或者所有不加作為普通成員函數(shù)定義。
class Foo
{
public:
Foo storted() && ;
// 錯誤:要么把前面的變?yōu)槠胀ǔ蓡T函數(shù),要么在const后面也加上&&(記得,是&&不是&,引用限定符是分兩種的)
Foo sorted() const;
Foo sorted(Comp*);
Foo sorted(Comp*) const;
};
*注意點:
1)當(dāng)一個成員函數(shù)帶有引用限定符,那么無論是聲明還是定義,它都必須要帶上。
2)引用限定符必須寫在函數(shù)的最后位置,如果是const成員函數(shù),就寫在const后面。