C++剛?cè)腴T,,以下為自己學(xué)習(xí)時整理的資料,,寫出來的是自己的理解,只是想加深自己的印象和以后方便自己查閱,,可能有不正當(dāng)?shù)牡胤健?br> 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 */
運(yùn)算符%俗稱"取余"或"取模"運(yùn)算符,負(fù)責(zé)計算兩個整數(shù)相除所得的余數(shù),,參與取余運(yùn)算的運(yùn)算對象必須是整數(shù)類型: - int ival = 42;
- double dval = 3.14;
- ival % 12; // 正確:結(jié)果是6
- ival % dval; // 錯誤:運(yùn)算對象是浮點(diǎn)類型
在除法運(yùn)算中,,如果兩個運(yùn)算對象的符號相同則商為正(如果不為0的話),否則商為負(fù),。C++語言的早期版本允許結(jié)果為負(fù)值的商向上或向下取整,,C++11新標(biāo)準(zhǔn)則規(guī)定商一律向0取整(即直接切除小數(shù)部分)。
根據(jù)取余運(yùn)算的定義,,如果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常量
其實(shí)這個常量跟NULL一樣,,都是0值,,只是NULL是預(yù)處理變量,在C++11中,,應(yīng)該盡量減少NULL的使用,。宏定義往往會導(dǎo)致非程序員本身需要的目
的(大部分是運(yùn)算符優(yōu)先級問題,比如#define duplicate(x)
x*x,如果你是想算兩個x+1后的結(jié)果再相乘,,如果你寫成duplicate(x+1),編譯器展開后會是
x+1*x+1,這樣明顯與原來的意圖完全不同),,而且,排除稍困難,,一般來說,,都會盡量避免使用。
6,、constexpr
constexpr可以說是對const變量的擴(kuò)展,,它只是約束const變量的右側(cè)的式子必須能在編譯時就能算出值。要理解這點(diǎn),,我們首先要搞清楚
const和constexpr之間的同異處.首先,,無論是constexpr或者const變量,值都是定義后不能改變的,,而且,,它們等號右側(cè)同樣都可
以為一個表達(dá)式;兩者的一個很重要的區(qū)別是const常量能在運(yùn)行時獲得值,,而constexpr卻是帶有約束性質(zhì)的,,它約束右側(cè)的式子只有在編譯時就能
算出值,如果不能的話,,編譯器側(cè)會報錯,。比如:const int max = 20; const int limit = max +
1;它們都是一個const變量同時,右側(cè)也是一個常量表達(dá)式,。但const int sz =
str.size();它是一個在運(yùn)行時取得具體值的const變量,,但在編譯時卻無法取得具體值,所以右側(cè)的表達(dá)式明顯不是一個常量表達(dá)式,。往往程序員
希望某一變量必須是常量以防止在運(yùn)行過程中產(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ù),,有以下幾點(diǎn)需要注意: 1)constexpr只能有一個return; 2)因?yàn)樾枰诰幾g時展開constexpr函數(shù),,所以constexpr函數(shù)默認(rèn)為inline函數(shù)。 3)主要語句不會在運(yùn)行時被執(zhí)行(注意,,是運(yùn)行時不會被執(zhí)行,,別想著a = 1;這種也算,這是運(yùn)行時才被執(zhí)行的語句,,是無法通過的,,說到底,只有第4點(diǎn)的語句才會合法),,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)該要注意的重要的一點(diǎn)是:constexpr構(gòu)造函數(shù)無論名稱怎么變,,但它仍然需要遵守constexpr函數(shù)的規(guī)則。這些規(guī)則總結(jié)為以下幾點(diǎn): 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ù)雜點(diǎn),,返回值還是一個函數(shù)指針的話,那更難以理解了,。新手一種會按我們上面說的那種基本語法去理
解,,typedef typeA
typeB,一個函數(shù)指針,很難找出那部分是對應(yīng)typeA,那部分是對應(yīng)typeB,,事實(shí)上,,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來為我們判斷類型,事實(shí)與我們靜態(tài)語言的原意相悖了,;auto更多應(yīng)用于泛型編程中,,它能減少你編寫特化模板的工作量。對于auto,,我們只需要
記住以下幾點(diǎn): 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)我們確實(shí)需要一個引用類型時,,我們可以寫成這樣auto
&ref = PI; 4)說這點(diǎn)前,,先說明下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,,事實(shí)上,指向常量的指針和聲明引用的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中也有新的定義,,之前我們必須確實(shí)地指定new類型和指針類型才能正確地在堆上分配內(nèi)存空間,,現(xiàn)在,C++11允許由編譯器推斷分配類型,,用法如下: auto p1 = new auto(std::string); // p1為指向string類型的指針
11,、decltype 我覺得C++11新增的這個特性,比auto更奇葩,,要理解這個東西,,說真的,難度不是一般的大,,說不得,,真想深入研究,你應(yīng)該重新翻翻編譯原理,。以下幾點(diǎn),,是我們必須注意的: 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)后得是必須要初始化的,,因?yàn)橐妙愋捅仨毝x的同時初始化。 3)注意這種情況:decltype(x + 0)——這里我們假定x是int&類型,引用類型進(jìn)行decltype是作為一條賦值語句的左值的,。最后的結(jié)果應(yīng)該是int類型,。 4)
這點(diǎn)是最重要的:加不加括號,,我們得到的類型可能是會不同的,。比如int i =
0;當(dāng)我們調(diào)用decltype(i)時,,這很容易理解,得到的類型肯定是int類型,;但如果我們加上一重或多重括號如:decltype((i)),,我
們得到的是int&類型。事實(shí)上,,變量也是可以作為一種賦值語句左值的特殊表達(dá)式,,從C++的初始化就可以看出,比如int
i(0);,可以看出,,這是一個函數(shù)調(diào)用的形式,,事實(shí)上,這個比較難理解,,我也不明白,,而這也僅僅是一個數(shù)學(xué)到的概念而已,a+b =
0是代數(shù)式,,那a和b都是代數(shù)式,。反正decltype是C++11新增的機(jī)制。 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所命名的實(shí)體的類型,。 如果e是一個函數(shù)調(diào)用或者一個重載操作符調(diào)用(忽略e的外層括?。敲磀ecltype(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ù)
的遍歷,,這些方法確實(shí)足夠簡單,,但說真的,這些方法方便的地方在于能動態(tài)判斷類型,,但我個人認(rèn)為,,這種范圍for對于C++來說,作用真不算強(qiáng)大,,因?yàn)?
C++不像C#這些一樣,,一個數(shù)組內(nèi)能混合不同的類型,C#的同一個數(shù)組內(nèi)能加入基本類型,,也能加入類類型,,但C++只能加入特定一種類型,所以,,每個數(shù)
組都是特定類型,,用auto其實(shí)作用不大,而傳統(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,,這是一種帶符號的類型,,因?yàn)椴钪悼赡軙樨?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因?yàn)橛幸环N構(gòu)造方法為initializer_list<T>
lst{a,b,c.....};使得它可以實(shí)現(xiàn)可變參數(shù)(我知道,,vector,,list一樣可以這樣做,但問題是,,vector和list總是一開始
就分配內(nèi)存空間的,,相比較下,initializer_list是輕量級的,。PS:這貨不支持下標(biāo)操作的,只能用指針,,或者在標(biāo)準(zhǔn)庫中叫迭代器更好點(diǎn)),,大概方法如下: void CoutList(initializer_list<string> il) { // 一般為了方便,我們不清楚il.begin()的類弄,,可以 用auto代替,,但事實(shí)上, begin返回的是一個以模板實(shí)例相同 的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的起始點(diǎn),,上面的函數(shù),我們可以增加一個表示傳入?yún)?shù)數(shù)量的形參,,可以可以這樣寫: void CountList(int nNumber,...) { va_list lt; //定義一個 va_list 參數(shù)表 va_start(lt,nNumber); // 初始化va_list,,并讓其指針指向參數(shù)表中的第一個參數(shù)
// 因?yàn)槭菞#詶5拙褪堑谝粋€參數(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 ) 但總體來說,,因?yàn)閕nitializer_list是輕量的,所以使用它應(yīng)該更方便一點(diǎn),。
16,、尾置返回類型
想像一下,當(dāng)你要定義一個函數(shù),,當(dāng)你需要一個函數(shù),,它接受一個int類型參數(shù),并返回一個指向含有10個int的數(shù)組,,你應(yīng)該怎么寫,?我就犯過傻,聰明地
直接返回數(shù)組,,接收時卻也是用數(shù)組,,但這是錯誤的,數(shù)組是不能拷貝的,,明顯是不能直接返回數(shù)組,,只能返回數(shù)組的指針或者引用,,而且,一般來說,,返回指針,,
在語法上會更煩瑣點(diǎn)。要寫一個返回指向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ù)雜度也是其實(shí)一個因素了),,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ù)。但事實(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)行初始化,只是這樣或者更直觀一點(diǎn),。
19,、委托構(gòu)造函數(shù) 看到這個名字時,我希望大家不要跟我一樣直接想到IOS的委托和C#的委托,,其實(shí)根本不是這么一回事,。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)萘看笮〉牟僮鳌?br> 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"}; 這種方式的初始化,其實(shí)也不過是調(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)建對象,而不必要浪費(fèi)再構(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),,一般情況下,,因?yàn)樗侵苯觕har型指針,通過
把最后非數(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ù)中的局部變量,。 我們要記住一點(diǎn),,lambda是一個類,而且,,沒有帶合成默認(rèn)構(gòu)造函數(shù)的類,,在我們定義時,編譯器會為我們自動生成這樣一個類,。 它格式如下:[capture list](parameter list)->return type {function body} [捕獲列表](形參列表)->返回類型 {函數(shù)體} 首先,,這種格式不是固定的,其中某些項(xiàng)能省略,,或者隱式調(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)的類有點(diǎn)細(xì)致區(qū)別,主要表現(xiàn)為: 1)lambda產(chǎn)生的類不含默認(rèn)構(gòu)造函數(shù),、賦值運(yùn)算符及默認(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類型,,則編譯器為我們生成類似這樣的一個未知名的類(別以為就是這樣,這只是類似而已,,并非就是一模一樣,,這點(diǎn)要清楚
的): 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)用運(yùn)算符,值捕獲時一般都是const成員函數(shù),,這是lambda所要求的——值捕獲到的值一般不會改變基值,。 bool operator() (const string&s) const // 因?yàn)樾螀⑹莄onst類型,所以成員函數(shù)也聲明為const成員函數(shù) { return s.size() >= sz; }
private: size_t sz; } 現(xiàn)在我們應(yīng)該已經(jīng)理解了,,lambda就是一個未命名的類,,它重載了調(diào)用運(yùn)算符。 下面分步來解釋lambda的幾個要點(diǎn): [形參列表]: 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的臨時變量,,但卻沒有在捕獲列表里列出,所以這是非法的,,編譯器會報錯;如果我們確實(shí)想使用,,就應(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(); }
因?yàn)槭侵悼截?,所以修改父函?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(); }
因?yàn)槭且贸跏蓟?,所以改變局部變量的值,,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顯式地寫在捕獲列表中,,則其它的變量都是隱式引用捕獲。在使用混合捕獲時必須知道幾點(diǎn): 3.1) 捕獲列表中第一個元素必須是一個&或=,。 3.2) 顯式捕獲的變量的捕獲方式必須與隱式捕獲的方式不同,。如果上面如果我們寫成auto f2 = [&,&v2]{return v1 + v2;};這就是一個錯誤的lambda表達(dá)式,因?yàn)椴东@列表中,,隱式的變量和顯示的變量都是引用方式,。
[可變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)绻厦嬉粯?,改變v1變量的值,,那么編譯器會提示你必須是可修改的左值;要想確實(shí)修改這個值,,我們可以用一個關(guān)鍵字解決:mutable,。 (順便說下C++中最容易讓人忘記的幾個關(guān)鍵字吧, 第一個explicit:
這貨我們平時很少用,,但如果你看過MFC的代碼,,你會發(fā)現(xiàn),其實(shí)這貨還是很上鏡的,。這個關(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類——事實(shí)上,,const類也只能調(diào)用const成員變量和成員函數(shù),好
吧,這個說了其實(shí)沒有意義,,一句話,其實(shí)才是我想說的:就算是const成員函數(shù)中,,它都是可變的,。另外,mutable只能作用于非const的變量,,
如果mutable const int a; 這樣是不允許的。正因?yàn)檫@個關(guān)鍵字破壞了類的封裝性,所以我們是比較少用的,。
第三個volatile: 這個是C引過來的,。或者做移植時經(jīng)常用到,,但說真的,,平時根本用不上,它主要是告訴編譯器,,不要對被它定義的變量進(jìn)行優(yōu)化,,因?yàn)闆]有用過,所以沒有什么好說的,。 ) 說完就回正文吧,。 在我們把上面的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í)是這樣
的,,值捕獲時,我們是直接拷貝父函數(shù)臨時變量的值的,,我們lambda生成的類內(nèi)的變量不是const的(雖然我們的調(diào)用重載函數(shù)是const的),,所以
我們加上mutable實(shí)際上只是對我們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ù)),,但往往只接受一個形參的函數(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)用運(yùn)算符。它的語法如下(為什么這貨跟sokcet的bind同名呀……)使用它要包含頭文件#include <functional>,而且,,它重載了很多個版本: auto newCallable = bind(callable,arg_list); bind()函數(shù)能作為通用適配器是因?yàn)樗小罢嘉环?。它的占位符以_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)實(shí)際上就是f(a,b,B,c,A),,這樣調(diào)用的,。利用這個原理,可以實(shí)現(xiàn)一個函數(shù),,可以比較大于,,也可以比較小于。比如有兩個過濾函數(shù),,是比較小于的,,但我們當(dāng)前有這樣一個函數(shù): bool compare(int a,int b) { return a > b;} 如果bind(_2,_1),把a(bǔ)和b反過來,,那就相當(dāng)于比較a < b,。 2)bind的非占位符,默認(rèn)情況下,,他們都是拷貝復(fù)制的,,但有時我們確實(shí)需要他們進(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á)式,,因?yàn)閛stream類型不允許復(fù)制,必須引用傳參,。我們可以用ref函數(shù)解決,。 auto f = bind(print,ref(os),_1,''); 跟ref一樣,我們還有一個cref函數(shù),,只不過,,它是生成一個保存const引用的類。
30,、無序容器【容器】 我們都知道,,維護(hù)元素的序列所花費(fèi)的代價是很高的,在我們學(xué)數(shù)據(jù)結(jié)構(gòu)時,,以最簡單的數(shù)組為例,,我們?nèi)绻迦胍粋€數(shù)組,并且需要保持其序列性,就算有好的算法,,但也難免要進(jìn)行多次的比較(注意,,一般是通過比較運(yù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。他們跟其它有序容器比較,,多了一個叫哈希策略的操作,。事實(shí)上,無序容器在組織元素不再是按比較運(yùn)算符進(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ù)制,,那要浪費(fèi)多少算法時間和內(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ù)的只剩,。句柄類的行完全是一個普通指針的行為(通過重載一系列的操作符實(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ì)的類定義,,不會寫,,因?yàn)殡S便網(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),,因?yà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來給變量定義一個屬性。現(xiàn)在C++11也引進(jìn)了這個“弱”的概念,,雖然跟OC還是有點(diǎn)區(qū)別,,但其實(shí)作用卻是一樣的,都是為了更安全地處理指針,。 一個weak_ptr必須與一個shared_ptr綁定才能使用,而且,,就算綁定了也不會增加shared_ptr的任何引用計數(shù),。正因?yàn)檫@種與其綁定或者說共享的對象的關(guān)系是如此不密切,,所以才說它是"弱",,就是說,他只是象征性指向一個對象,,而不論這個對象是否已經(jīng)銷毀,。 auto p = make_shared<int>(42); // 創(chuàng)建一個shared_ptr對象 weak_ptr<int> wp(p); // wp綁定p,,p引用計數(shù)不變 正是因?yàn)樗辉黾?所以我們明顯不能直接無所顧忌地使用它對我們的指針對象進(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,、允許使用作用域運(yùn)算符來獲取類成員的大小 在標(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)的實(shí)例后才行,。
35、=default 舊版本的C++中,,當(dāng)一個類內(nèi)我們沒有定義構(gòu)造函數(shù)、拷貝構(gòu)造函數(shù),、拷貝賦值運(yùn)算符和析構(gòu)函數(shù)時,,則編譯器會為我們定義一個合成的版本以讓我們的類能正常運(yùn)行,。對于默認(rèn)構(gòu)造函數(shù),,當(dāng)我們定義了自己版本的構(gòu)造函數(shù),編譯器則不會再為我們合成默認(rèn)構(gòu)造函數(shù)了,,而對于另外的三個成員函數(shù),,無論我們是否定義了自己的版本,,編譯器都會為我們合成一個,。(事實(shí)上,,當(dāng)一個類需要我們自己來控制其析構(gòu)時,,也幾乎肯定了必須要我們定義自己的拷貝構(gòu)造函數(shù)和拷貝賦值運(yùn)算符)。 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ù),因?yàn)镃++中對拷貝構(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&); // 拷貝賦值運(yùn)算符 ~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)實(shí)參——如果它不帶默認(rèn)實(shí)參的話,就相當(dāng)于是有兩個(這點(diǎn)對拷貝賦值運(yùn)算符是無效的,,因?yàn)閛perator=對參數(shù)是有要求的),,但無論如何,,它都不是正確的由編譯器合成的拷貝構(gòu)造函數(shù)的格式。 =default可以肯定地說,,它只能用于能合成的成員函數(shù),。 注意:上述的我們一般只聲明而不定義,,因?yàn)槿绻覀兌x的話就跟=default有沖突,,=default完全是說明由編譯器生成我們成員函數(shù)內(nèi)的其它代碼,所以它只是聲明而非定義,。
36、刪除函數(shù) 如果我們用過單例這種設(shè)計模式時,,又剛好需要定義一個類,,這個類不能多次構(gòu)造,只能在整個程序中只有一個實(shí)例存在,,這時我們一般都會阻止這個類有構(gòu)造函數(shù)。還記得我們一般做法是如何的嗎,?是的,,我們通過先寫一個構(gòu)造函數(shù),通過這個函數(shù)阻止了編譯器的合成構(gòu)造函數(shù),,再把這個構(gòu)造函數(shù)定義為private讓它不能在類外調(diào)用,;然后再小心翼翼地處理拷貝和拷貝賦值運(yùn)算符。讀過這種代碼的人都清楚,,如果沒有人告訴你這個是一個單例,,我估計你真得愣個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; // 拷貝賦值運(yùn)算符 ~test() = delete; // 析構(gòu)函數(shù) };
用這個方法,,在設(shè)計類似單例模式這種類時,我們可以不需要管我們的類的屬性是否為private,,直接后置=delete就行了,。你也可以把拷貝和賦值都干掉,,這個實(shí)例類連引用計數(shù)都可以不用了。但這個類卻只有簡單的處理邏輯的功能了,。這是題外話,,不多說。 另外,,當(dāng)我們把一個析構(gòu)函數(shù)定義為不可訪問或者刪除函數(shù),,我們的合成構(gòu)造函數(shù)和拷貝構(gòu)造函數(shù)(拷貝賦值運(yùn)算符仍然能合成,,因?yàn)橛袝r我們雖然不能構(gòu)造A對象,,但如果B派生了A,,在B有構(gòu)造函數(shù)的話,,仍然可以進(jìn)行賦值操作的,,當(dāng)然,A現(xiàn)在無法構(gòu)造,,這個前提也不成立,,但C++語義就是允許合成拷貝賦值運(yùn)行符)是被設(shè)置成=delete的,因?yàn)椴荒茚尫艑ο?,所以編譯器也不允許我們創(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ù)和/或一個移動賦值運(yùn)算符時,,則該類的合成拷貝函數(shù)和拷貝賦值運(yùn)算符將會被定義為刪除的,。但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)用拷貝賦值運(yùn)算符,,其實(shí)這個跟前面的內(nèi)置類型一樣,這個是賦值初始化,,而是賦值,。有些書上會叫賦值構(gòu)造函數(shù),所以我為什么要叫拷貝賦值運(yùn)算符就是不想弄亂,。
37,、右值引用 C++11為了支持移動操作,新引用了一種名為右值引用,。正如其名,,右值引用必須綁定到右值。對于左值和右值的定義,,可能一時是難以理解的,,按概念來說,它已經(jīng)不是以前C那樣以等號左側(cè)或右側(cè)來區(qū)分了,,所以也變得更艱澀難明了,。但我們只記住這樣一點(diǎn): 有名字且可以取地址的就是左值;沒有名字且無法取地址的就是右值,。(const左值引用算例外) 這樣從代碼行為上來理解,,會更容易掌握點(diǎn)。 我用通過&&來獲取一個右值的引用,,如: 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)行與左值變量相同的操作,比如改變其值,。因?yà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通過強(qiáng)制類型引用轉(zhuǎn)換成右值類型,。通過引用折疊機(jī)制,move的參數(shù)可以接受一個左值也可以接受一個右值,,但無論如何,,我們應(yīng)該都要遵守:盡量不使用一個移后源對象的值。其實(shí)看下頭文件我們也可以看到,,std::move就是把類型轉(zhuǎn)換下再返回而已(它本身是模板函數(shù),,類型會根據(jù)傳入的參數(shù)類型進(jìn)行實(shí)參推斷),但它返回的是與原來類型相關(guān)的右值引用,,即type&&類型,。 正因?yàn)橹皇沁M(jìn)行類型轉(zhuǎn)化,所以,,無論我們++i還是++r,都能改變i的值,,但不提倡這樣使用。(C++ primer這部分我真不理解,,郁悶,!不過,大家用了std::move后,,除了重新賦值和釋放這個變量外真不要作任務(wù)操作,,這是C++11的約定,大家應(yīng)該都遵守,如果你要搞特化,,別人看你代碼,不僅看得一頭霧水,,還要罵句SB的,。) 還有更重要的一點(diǎn):右值被引用后,即右值引用本身也是一個左值,。更詳細(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í)參,。先拿個例子比較下拷貝函數(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,因?yàn)橄旅嬉膕ource內(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ù)的實(shí)質(zhì)是怎么樣的呢?
從圖中可以看出,,移動構(gòu)造函數(shù)實(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的,。 事實(shí)上,移動構(gòu)造函數(shù)是解決指針,,特別是臨時指針在復(fù)制時,,如果數(shù)據(jù)量消耗效率的問題。比如上面的函數(shù),如果我們是一個內(nèi)置類型,,結(jié)果會如何呢,?: class A{ public: // 因?yàn)橛辛艘苿訕?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,因?yàn)橄旅嬉膕ource內(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ù)是一種機(jī)制,那輸出的結(jié)果就讓你哭了,。 說到底,,移動函數(shù)也是符合C++語義的,如果你什么都不做,,它也是什么都不處理,。上面的輸出結(jié)果中,,a和b是兩個類,,有各自的內(nèi)存空間,所以輸出內(nèi)置型的數(shù)據(jù)時(它在棧上,包括b這個指針本身,,也是在棧上),,它因?yàn)槭莾蓚€對象分別控制的,,所以第二行輸出的地址不同;第三行就比較有意思,,它輸出的是移動后的源和當(dāng)前變量的指針地址,,它們是相同的,為什么第一個是空指針呢,?因?yàn)槲覀冊谝苿雍?,把移后源設(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)實(shí)的,。事實(shí)上它也一般定義為const,。C++引進(jìn)移動構(gòu)造函數(shù)后,,就魚和熊掌都可兼得了。 最后,,我們注意到那個noexcept,,這也是C++11新引進(jìn)的。它位于參數(shù)列表和puvcwx列表開始的冒號之間,。它指示程序不拋出任何異常,,其實(shí)可以想像,我們只是“竅取”數(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,、解決自賦值問題,這個問題拷貝賦值運(yùn)算符也有的,,當(dāng)一個對象是其自身時(雖然我們一般不會遇到這種情況,,但仍然要小心有些程序員就是二愣子),我們不應(yīng)該進(jìn)行賦值,,特別是清空數(shù)據(jù)問題,,如果我們在賦值前清空了數(shù)據(jù),,則源對象因?yàn)槭亲陨恚矔R上清空數(shù)據(jù)的,,這樣我們的賦值不僅沒有意義,還要導(dǎo)致錯誤,。 以前面的例子為例,,我們弄一個簡單的移動賦值函數(shù),。
class A{ public: // 因?yàn)橛辛艘苿訕?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,因?yàn)橄旅嬉膕ource內(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ù)和拷貝賦值運(yùn)算符,,不然編譯器都會為我們合成一個默認(rèn)的版本,。但對于新增加的移動構(gòu)造函數(shù)和移動賦值函數(shù),,一般情況下,編譯器根本不會為我們合成這些移動操作的函數(shù),,但在特定的條件下,,卻還是會為我們合成移動操作的函數(shù)。這里我們還要加上另一條重要的信息:定義一個移動函數(shù)或者移動賦值運(yùn)算符的類必須也定義自己的拷貝操作法函數(shù),。否則,,這些成員默認(rèn)地被定義為刪除的,。 1、當(dāng)我們有任何版本的自定義拷貝構(gòu)造函數(shù),,拷貝賦值運(yùn)算符或者析構(gòu)函數(shù)時,,編譯器不會為我們合成移動操作類函數(shù)(有任何一個拷貝操作函數(shù)時都不會再合成移動操作函數(shù)),。對于存在拷貝構(gòu)造函數(shù)和拷貝賦值運(yùn)算符,,我們可能比較容易理解,,因?yàn)楫?dāng)我們定義了這兩個函數(shù)后,就算沒有移動操作函數(shù),,我們的類仍然能工作得很好:對于臨時變量,我們可以把臨時變量的堆數(shù)據(jù)完整拷貝過來,。說到低,移動操作類函數(shù),,只是為了讓類工作得更快,而非不可缺,。但對于析構(gòu)函數(shù),,我們可能會有所疑惑,我們應(yīng)該樣想:要析構(gòu)的地方,,都是我們的類存在指針時,,而當(dāng)類需要析構(gòu)函數(shù)時,幾乎也肯定了我們必須需要拷貝構(gòu)造函數(shù)和拷貝賦值運(yùn)算符了,。而也正因?yàn)閹в兄羔槻僮?,編譯器無法推斷我們需要做的操作,在不安全的前提下,,C++11規(guī)定(我說了,,移動操作函數(shù)只是為了類更好工作,而非不可缺)有析構(gòu)函數(shù)時不合成移動操作函數(shù)是明智的。 2,、當(dāng)類沒有定義任何版本的拷貝控制成員時(即有合成的卻沒有自定義的拷貝構(gòu)造函數(shù),、拷貝賦值運(yùn)算符或者析構(gòu)函數(shù)時),且類的每個非static數(shù)據(jù)成員都可以移動(前面說了,,這里移動其實(shí)就指“竅取”,,但有些成員是無法移動的,我們都知道,,盡管內(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,,它的行為就有點(diǎn)奇怪了,就算我們顯示定義一個移動操作函數(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àn)閅,,它是=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ù)或移動賦值運(yùn)算符被定義為刪除的(用=delete顯示定義刪除,或編譯器隱式在特定條件下把=default改變?yōu)?delete,,再次說明,,編譯器永遠(yuǎn)不會在正常情況下把移動操作函數(shù)隱式定義為=delete)或者聲明為不可訪問的(即private或protected),則類的移動構(gòu)造函數(shù)或移動賦值運(yùn)算符被定義為刪除的,。 3,、類似拷貝構(gòu)造函數(shù)(33點(diǎn)說到)。如果析構(gòu)函數(shù)被定義刪除或不可訪問的,,則類的移動構(gòu)造函數(shù)被定義為刪除的,。移動賦值運(yùn)算符仍然有效,但卻無意義,。 4,、類似拷貝賦值運(yùn)算類。當(dāng)類中有const成員或者引用時,,則類的移動賦值運(yùn)算符定義為刪除的,。這里順便解釋下為什么拷貝賦值運(yùn)算符在類中有const成員或者引用時會被刪除合成的。我們都知道,,在C++中,,const變量和引用類型變量,都是在定義的同時要初始化的,,而類中,,const變量是在聲明時直接定義的,而引用類型則是是在構(gòu)造函數(shù)中進(jìn)行列表化初始化的,,所以這種情況下,,拷貝賦值運(yùn)算符不能復(fù)制const對象,他只會自己原生有一個const變量,,值不會變,。它也不能復(fù)制引用成員,因?yàn)樗仨氃跇?gòu)造函數(shù)的列表中給定,,所以編譯器為了更安全點(diǎn),,直接就是在遇到這種情況下,不再合成拷貝賦值運(yùn)算符,,而是交由程序員自己處理了,。移動賦值運(yùn)算也是相同原理。移動拷貝仍然可用,,跟拷貝構(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引 用,則右邊的值可以是一個右值,,因?yàn)閏onst要求一個常量值,,而常量值都是一個右值。構(gòu)造函數(shù)形參可以 是非const而安全接受右值,,其實(shí)原理也相當(dāng)簡單:對于構(gòu)造函數(shù)來說,,主要責(zé)任是構(gòu)造一個新對象,即便修改 了右值,,也無所謂,,反正臨時的東西,扔了就行了,。但是普通函數(shù),,目的不是為了別的,就是為了處理傳進(jìn)來 的值,,傳進(jìn)來的引用,,是既能in又能out的。所以這里如果非const引用一個右值,,那肯定會讓out的結(jié)果失效。 所以對于普通函數(shù)而言,要想接受一個右值必須是一個const類型的形參,;沒有所謂的機(jī)制,,只是編譯器是這 樣定義的。 void hit(const int &) //假如沒有const說明右邊可以是右值,,則std::move后的值不能用于參數(shù)
{ cout << "function" << endl; }
int iv = 4; hit(std::move(iv)); // 正確
類的輸出結(jié)果:
有個提醒,,我不建議在除了類定義外的移動操作函數(shù)。因?yàn)樗浅2环€(wěn)定,,你不能確定移后源對象的確切狀態(tài),,是否仍然有其它的對象在使用它。我們說過,,移后源應(yīng)該是可以隨時釋放清空而不影響其它操作的,,而且,我們不能對其內(nèi)數(shù)據(jù)進(jìn)行操作,。但誰又能100%知道當(dāng)前移后源的狀態(tài)呢,,特別是大項(xiàng)目中,這個更是致命,。 41,、移動迭代器【標(biāo)準(zhǔn)庫】 新標(biāo)準(zhǔn)庫中新增加了一種移動迭代器適配器,它主要通過調(diào)用標(biāo)準(zhǔn)庫的make_move_iterator函數(shù)將一個普通迭代器轉(zhuǎn)換為一個移動迭代器,,而且,,返回后的迭代器的解引用運(yù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ù),因?yàn)镃++規(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; }
這個類的賦值運(yùn)算符只允許左側(cè)的操作數(shù)即類對象本身是一個左值。如果我們這樣操作: Foo f,i; std::move(f) = i; 這是無法成立的,。 2,、重載和引用函數(shù) C++11中新增加的這種引用函數(shù),跟const成員函數(shù)一樣,,可通過引用限定符來區(qū)分重載版本,。 前面已經(jīng)看到,我們定義const成員函數(shù)時,,可以定義兩個版本,,唯一罰沒是一個有const限定,一個沒有,。而對于有引用限定符的成員函數(shù),,有一點(diǎn)不一樣: *如果我們定義兩個(兩個是重載的最低要求,不然還叫什么重載)或兩個以上具有 相同名字和 相同參數(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; };
*注意點(diǎn):1)當(dāng)一個成員函數(shù)帶有引用限定符,,那么無論是聲明還是定義,,它都必須要帶上,。 2)引用限定符必須寫在函數(shù)的最后位置,如果是const成員函數(shù),,就寫在const后面,。
|