淘寶里網(wǎng)站建設(shè)公司可以嗎地推十大推廣app平臺
目錄
C++11簡介
統(tǒng)一的列表初始化
{}初始化
std::initializer_list
聲明
auto
decltype
nullptr
范圍for循環(huán)
智能指針
STL中的一些變化
右值引用和移動(dòng)語義
左值引用和右值引用
右值引用的意義
完美轉(zhuǎn)發(fā)
lambda表達(dá)式
新的類功能
可變參數(shù)模版?
包裝器
function包裝器
bind
線程庫
線程函數(shù)參數(shù)
lock_guard和unique_lock
原子性操作庫
兩個(gè)線程交替打印
C++11簡介
相比于C++98/03,C++11發(fā)生了較大的變化,大約新增了約140個(gè)新特性,以及修正了約600個(gè)缺陷,這使得C++更加強(qiáng)大。
統(tǒng)一的列表初始化
{}初始化
在C++98中,允許使用花括號{}對數(shù)組或者結(jié)構(gòu)體元素進(jìn)行統(tǒng)一的列表初始值設(shè)定。比如:
struct Point
{int _x;int _y;
};int main()
{int array1[] = { 1,2,3,4,5 };Point p = { 1,2 };return 0;
}
C++11擴(kuò)大了用大括號括起的列表的使用范圍,使其可用于所有的內(nèi)置類型和用戶自定義類型,列表初始化時(shí),可以添加等號(=),也可以不添加,但是不太建議去掉。
int arr1[]{ 1,2,3,4,5 };
int arr2[5]{ 0 };
Point p2{ 1,2 };
對于自定義類型創(chuàng)建對象時(shí)也可以用列表初始化方式調(diào)用構(gòu)造函數(shù)初始化:
class A
{
public:A(int x, int y):_x(x), _y(y){}A(int x):_x(x), _y(x){}
private:int _x;int _y;
};
int main()
{A a5(6);//單參數(shù)的隱式類型轉(zhuǎn)換A a3 = 1;A a4 {2};//多參數(shù)的隱式類型轉(zhuǎn)換//C++支持的列表初始化,這里{1,2}先調(diào)用構(gòu)造函數(shù)初始化,再調(diào)用拷貝構(gòu)造賦值給a1 A a1 = { 1,2 };A a2 { 3,4 };return 0;
}
std::initializer_list
//vector可以這樣初始化
vector<int> v1(10, 1);
//但是vector不能用{}初始化
vector<int> v2 = {1,2,3,4,5};
為什么vector用不了{(lán)}這樣構(gòu)造呢?{...}里面可能有1/2/3/4/....個(gè)元素,但是vector的構(gòu)造函數(shù)參數(shù)列表不知道到底該設(shè)置多少個(gè),要寫無數(shù)個(gè)構(gòu)造函數(shù)才行,就像下面這樣:
所以,為了支持這樣的初始化,引入了initializer_list,
因此,我們現(xiàn)在可以將{...}的常量數(shù)組轉(zhuǎn)化為initializer_list。
initializer_list的本質(zhì)是兩個(gè)指針,一個(gè)first指向第一個(gè)元素,一個(gè)last指向最后一個(gè)元素的下一個(gè),為了驗(yàn)證這樣一個(gè)事實(shí),我們算一下i1的大小,在32位平臺下,i1的大小是8(2個(gè)指針的大小):
initializer_list也支持簡單的迭代器begin和end,迭代器的類型是const T*,由于它指向常量數(shù)組,所以是const T*,不支持修改。?
所以,為了解決本節(jié)剛開始提出的問題,C++11使用了這樣的方式解決:
vector(initializer_list<T> i1);
這個(gè)構(gòu)造一勞永逸的解決了問題,不用像上面那樣麻煩的方式解決。
initializer_list可以接收常量數(shù)組,本質(zhì)是兩個(gè)指針,一個(gè)指向數(shù)組的開始,一個(gè)指向數(shù)組最后的下一個(gè)。這樣,就算{...}里有5個(gè)8個(gè),都可以傳給initializer_list。
當(dāng)? X自定義 = Y類型,這個(gè)時(shí)候就會隱式類型轉(zhuǎn)換,需要有 X(Y mm),即X支持Y為參數(shù)類型的構(gòu)造就可以。
在上面圖中,{1,2,3,4,5}會被識別成initializer_list,下面的那個(gè),等號右側(cè)生成一個(gè)臨時(shí)對象,再去拷貝構(gòu)造給v4。??
再舉一個(gè)例子,創(chuàng)建map對象時(shí),也可以用{}進(jìn)行創(chuàng)建:
聲明
auto
在C++11中,auto用于自動(dòng)推斷類型,這樣要求必須進(jìn)行顯示初始化。
int main()
{int i = 10;auto p = &i;cout << typeid(p).name() << endl;map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };//map<string, string>::iterator it = dict.begin();auto it = dict.begin();return 0;
}
decltype
關(guān)鍵字decltype將變量的類型聲明為表達(dá)式指定的類型。它的功能和typeid有點(diǎn)像,typeid可以拿到對象的類型,但是得到的只是一個(gè)單純的字符串,不能用來定義對象,如果想定義一個(gè)和typeid一樣的值,這就做不到了。
但是decltype可以做到:
但是,有人可能會認(rèn)為,我用一個(gè)auto不也可以嗎?但是,我們換一個(gè)場景:
我想用ret3的返回值類型去初始化class B的模版參數(shù),這時(shí)候typeid就不行了,但是可以用decltype來確定ret3的類型,
B<decltype(ret3)> b;
雖然auto和decltype雖然可以方便我們寫代碼,但是有些地方增加代碼讀起來的難度,所以慎用auto和decltype!
nullptr
由于C++中NULL被定義成字面量0,這樣就可能回帶來一些問題,因?yàn)?既能指針常量,又能表示整形常量。所以出于清晰和安全的角度考慮,C++11中新增了nullptr,用于表示空指針。
范圍for循環(huán)
我們在之前的學(xué)習(xí)中,已經(jīng)多次使用了范圍for,這里不再贅述。
只有一點(diǎn)需要注意,我們最好把&加上,即for(auto& e:v)或者for(const auto& e:v)(在不改變值的情況下加const),這樣可以防止深拷貝,提高效率。
智能指針
我們之后單獨(dú)學(xué)習(xí)這塊。
STL中的一些變化
新容器?
array:對越界檢查更嚴(yán)格,但是可以用vector來替代,所以用處不大,是一個(gè)挺失敗的設(shè)計(jì)。
forward_list:單向鏈表,相比list節(jié)省一個(gè)指針,但是也不好用。
容器內(nèi)部
幾乎在每個(gè)容器內(nèi)部,都增加了initializer_list、移動(dòng)構(gòu)造(這個(gè)很有用),
還有emplace、empalce_back:
右值引用和移動(dòng)語義
左值引用和右值引用
傳統(tǒng)的C++語法中就有引用的語法,而C++11中新增了的右值引用語法特性,所以從現(xiàn)在開始我們之前學(xué)習(xí)的引用就叫做左值引用。無論左值引用還是右值引用,都是給對象取別名。
什么是左值和右值
左值和右值的區(qū)分關(guān)鍵,是能否取地址??梢匀〉刂返氖亲笾?#xff0c;不能取地址的是右值。
因此,a、b、c均是左值。
左值不一定是值,也可能是表達(dá)式。如*p也是左值,因?yàn)樗梢匀〉刂贰?/p>
類似的,v[1]也是左值,它可以取地址。
右值也是一個(gè)表達(dá)數(shù)據(jù)的表達(dá)式,它不能取地址,如字面常量、匿名對象、臨時(shí)對象。
什么是左值引用和右值引用
引用就是取別名,左值引用就是給左值取別名,右值引用就是給右值取別名,
右值引用就是用&&表示:?
自定義的類型右值主要有兩種,一類是匿名對象,一類是傳值返回的臨時(shí)對象。
左值引用不能給右值取別名,但是const 左值引用可以,
const string& ref1 = string("1111");
右值引用也不能給左值取別名,但是可以給move以后得左值取別名。
左值引用總結(jié):
1. 左值引用只能引用左值,不能引用右值。
2. 但是const左值引用既可引用左值,也可引用右值。
右值引用總結(jié):
1. 右值引用只能右值,不能引用左值。
2. 但是右值引用可以move以后的左值。
右值引用的意義
引用的意義是減少拷貝,提高效率, 比如引用傳參,
void func1(const string& s);
如果不用引用,那傳參時(shí)就要拷貝,像map這種大一點(diǎn)的容器拷貝的代價(jià)很大,
還有傳引用返回,
string& func2();
左值引用的場景主要就是引用傳參和傳引用返回。但是,左值引用返回值的問題沒有徹底解決,因?yàn)?span style="color:#fe2c24">如果是func2中局部對象,不能用引用返回,它的生命周期就在func2里面,出了作用域就銷毀了,同時(shí),也不能用右值引用返回,右值引用也不能解決生命周期的問題,
例如,bit::string to_string(int value)函數(shù)中可以看到,這里只能使用傳值返回,傳值返回會導(dǎo)致至少1次拷貝構(gòu)造(如果是一些舊一點(diǎn)的編譯器可能是兩次拷貝構(gòu)造)。
to_string的返回值用左值引用和右值引用都不太行,因?yàn)榉祷刂祍tr出了作用域都會被銷毀,在C++11中,另辟蹊徑,增加了一個(gè)參數(shù)為右值引用的構(gòu)造函數(shù)--移動(dòng)構(gòu)造,(對比:參數(shù)為左值引用的構(gòu)造函數(shù)為拷貝構(gòu)造),移動(dòng)構(gòu)造和拷貝構(gòu)造構(gòu)成函數(shù)重載。
在沒有移動(dòng)構(gòu)造時(shí),拷貝構(gòu)造string(const string& s)既可以接收左值,又可以接收右值;但是在有了移動(dòng)構(gòu)造時(shí),編譯器會找最匹配的。
在C++11中,將右值做了區(qū)分,分為純右值和將亡值,內(nèi)置類型的那種是純右值,自定義類型的那種是將亡值,在移動(dòng)構(gòu)造中,可以這樣寫:
//移動(dòng)構(gòu)造
//右值(將亡值)
string(string&& s):_str(nullptr)
{cout << "string(string&& s) -- 移動(dòng)構(gòu)造" << endl;swap(s);
}
既然已經(jīng)是將亡值,那么不妨把我的和將亡值交換一下,移動(dòng)將亡值的資源,這樣就不用拷貝構(gòu)造了,效率就上來了? ??
有人說右值引用延長了str的生命周期,這種說法是不正確的,str出了作用域就會被銷毀,確切的說是延長了資源的生命周期。
如果是下面這種形式,如果沒有移動(dòng)拷貝和移動(dòng)賦值,那就是一次拷貝構(gòu)造和一次賦值拷貝,編譯器不敢將這兩步合二為一(因?yàn)檫@是兩個(gè)不同的操作),
如果有了移動(dòng)拷貝和移動(dòng)賦值,那就是一次移動(dòng)拷貝和一次移動(dòng)賦值,
移動(dòng)構(gòu)造、移動(dòng)賦值和我們之前拷貝構(gòu)造的現(xiàn)代寫法有點(diǎn)像,但是這兩種有本質(zhì)的區(qū)別,現(xiàn)代寫法是讓一個(gè)臨時(shí)對象去構(gòu)造并轉(zhuǎn)移它的資源,并沒有提高效率,而移動(dòng)構(gòu)造、移動(dòng)賦值給我的右值就是一個(gè)將亡值,直接轉(zhuǎn)移這個(gè)將亡值的資源,代價(jià)很小。
我們再來看一些關(guān)于右值引用的其他問題:
我們知道,std::string("111111111111")本身是右值,但是右值引用本身(s1)是左值,因?yàn)橹挥杏抑狄帽旧硖幚沓勺笾?#xff0c;才能實(shí)現(xiàn)移動(dòng)構(gòu)造和移動(dòng)賦值,轉(zhuǎn)移資源(右值不能轉(zhuǎn)移資源)。這樣的意思,是為了移動(dòng)構(gòu)造和移動(dòng)賦值的轉(zhuǎn)移資源的邏輯是自洽的。
我們來看一下C++11中其他地方用到右值引用的,
由于s1是左值,所以push_back不能調(diào)用移動(dòng)拷貝,只能做深拷貝,
但是,如果想上圖這樣,將匿名對象放到push_back的參數(shù)中,就會調(diào)用移動(dòng)構(gòu)造。在push_back這類函數(shù)時(shí),使用匿名對象就更好了,這樣就不會設(shè)計(jì)到深拷貝,只需要移動(dòng)構(gòu)造,提高效率。可見,右值引用不僅在返回值有意義,也在參數(shù)值有意義。
也可以這樣,這樣就會先發(fā)生隱式類型轉(zhuǎn)換,將const char*轉(zhuǎn)換為string,轉(zhuǎn)換時(shí)會產(chǎn)生臨時(shí)對象,是右值,臨時(shí)對象具有常性,會調(diào)用移動(dòng)構(gòu)造。
最后一個(gè)問題,如果list里的值類型是int(內(nèi)置類型)或者日期類(淺拷貝自定義類型),還會涉及到移動(dòng)拷貝和移動(dòng)構(gòu)造嗎?
不會涉及,上面效率提升,針對的是自定義類型的深拷貝的類,因?yàn)樯羁截惖念惒庞修D(zhuǎn)移資源的移動(dòng)系列函數(shù);對于內(nèi)置類型和淺拷貝自定義類型,沒有移動(dòng)系列函數(shù)。
完美轉(zhuǎn)發(fā)
模版中的&&萬能轉(zhuǎn)發(fā)
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }//函數(shù)模版里面,這里叫做萬能引用
template<typename T>
void PerfectForward(T&& t)
{Func(t);
}void PerfectForward(int& t)
{Fun(t);
}int main()
{PerfectForward(10); // 右值int a;PerfectForward(a); // 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b); // const 左值PerfectForward(std::move(b)); // const 右值return 0;
}
函數(shù)模板中的&&不代表右值引用,而是萬能引用,其既能接收左值又能接收右值。模板的萬能引用只是提供了能夠接收同時(shí)接收左值引用和右值引用的能力。
但是,我們看程序運(yùn)行的結(jié)果,
因?yàn)樵诤瘮?shù)參數(shù)t接收后,后續(xù)都退化成了左值,所以都會調(diào)用Fun(int& x)和Fun(const int& x)這兩個(gè)函數(shù),但是這不是我們想要的結(jié)果,我們希望能夠在傳遞的過程中保持它的左值或者右值的屬性,那么就需要用到完美轉(zhuǎn)發(fā):
std::forward 完美轉(zhuǎn)發(fā)在傳參的過程中保留對象原生類型屬性。
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }//函數(shù)模版里面,這里叫做萬能引用
//實(shí)參傳左值,就推成左值引用,實(shí)參傳右值,就推成右值引用
template<typename T>
void PerfectForward(T&& t)
{//std::forward<T>(t)在傳參的過程中保持了t的原生類型屬性Fun(std::forward<T>(t));
}void PerfectForward(int& t)
{Fun(t);
}int main()
{PerfectForward(10); // 右值int a;PerfectForward(a); // 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b); // const 左值PerfectForward(std::move(b)); // const 右值return 0;
}
什么時(shí)候用完美轉(zhuǎn)發(fā)呢?在函數(shù)模版里,想要達(dá)到傳什么就保持它的屬性,就用完美轉(zhuǎn)發(fā)。這是一道常見的面試題。
lambda表達(dá)式
在C++98中,如果想要對一個(gè)自定義類型進(jìn)行排序,需要用到仿函數(shù),用戶自定義排序的比較規(guī)則,比如,有這樣一個(gè)自定義類型,
struct Goods
{string _name; // 名字double _price; // 價(jià)格int _evaluate; // 評價(jià)Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};
我們有存儲Goods類型的vector,想要對這個(gè)vector按照價(jià)格排序,既可以排升序,又可以排降序,可以使用algorithm這個(gè)頭文件中的sort算法,但是如果需要對商品按照它的某一項(xiàng)屬性排序,如價(jià)格,就需要自己寫一個(gè)類來定義仿函數(shù),
struct Goods
{string _name; // 名字double _price; // 價(jià)格int _evaluate; // 評價(jià)Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};struct ComparePriceLess
{bool operator()(const Goods& g1, const Goods& g2){return g1._price < g2._price;}
};struct ComparePriceGreater
{bool operator()(const Goods& g1, const Goods& g2){return g1._price > g2._price;}
};int main()
{vector<Goods> v = { {"蘋果",2.3,2},{"香蕉",3.2,4}, {"西瓜",0.9,3} };sort(v.begin(), v.end(), ComparePriceGreater());return 0;
}
但是,上面的方法有點(diǎn)復(fù)雜,每次都要自己寫一個(gè)類,所以C++為了解決這樣的問題,引入了lambda表達(dá)式。
lambda表達(dá)式實(shí)際是一個(gè)匿名函數(shù),lambda表達(dá)式書寫格式:
lambda表達(dá)式書寫格式:[capture-list] (parameters) mutable -> return-type { statement }
各部分說明:
????????[capture-list] : 捕捉列表,該列表總是出現(xiàn)在lambda函數(shù)的開始位置,編譯器根據(jù)[]來判斷接下來的代碼是否為lambda函數(shù),捕捉列表能夠捕捉上下文中的變量供lambda函數(shù)使用。
????????(parameters):參數(shù)列表。與普通函數(shù)的參數(shù)列表一致,如果不需要參數(shù)傳遞,則可以連同()一起省略。
????????mutable:默認(rèn)情況下,lambda函數(shù)總是一個(gè)const函數(shù),mutable可以取消其常量性。使用該修飾符時(shí),參數(shù)列表不可省略(即使參數(shù)為空)。
????????->returntype:返回值類型。用追蹤返回類型形式聲明函數(shù)的返回值類型,沒有返回值時(shí)此部分可省略。返回值類型明確情況下,也可省略,由編譯器對返回類型進(jìn)行推導(dǎo)。
????????{statement}:函數(shù)體。在該函數(shù)體內(nèi),除了可以使用其參數(shù)外,還可以使用所有捕獲到的變量。
注意:在lambda函數(shù)定義中,參數(shù)列表和返回值類型都是可選部分,而捕捉列表和函數(shù)體可以為空。因此C++11中最簡單的lambda函數(shù)為:[]{}; 該lambda函數(shù)不能做任何事情。
對捕捉列表和mutable的進(jìn)一步說明:
為了解決傳值捕捉在函數(shù)內(nèi)部改變不會影響外面的問題,引入了引用捕捉,引用捕捉是在捕捉列表變量前加&(這其實(shí)和取地址&有些沖突,但是在lambda表達(dá)式捕捉列表中我們認(rèn)為&是引用捕捉)?:??
?捕捉列表描述了上下文中哪些數(shù)據(jù)可以被lambda使用,以及使用的方式傳值還是傳引用。
[var]:表示傳值捕捉變量var
[=]:表示傳值方式捕捉父作用域的所有變量
[&var]:表示引用方式捕捉變量var
[&]:表示引用方式捕捉父作用域的所有變量
[this]:表示傳值方式捕捉當(dāng)前的this指針
?除了上面?zhèn)髦岛蛡饕貌蹲椒绞揭酝?#xff0c;還有混合捕捉(一部分傳值捕捉,一部分傳引用捕捉):
因此,我們可以寫lambda表達(dá)式作為一個(gè)匿名函數(shù)傳給sort進(jìn)行排序:
int main()
{vector<Goods> v = { {"蘋果",2.3,2},{"香蕉",3.2,4}, {"西瓜",0.9,3} };auto priceLess = [](const Goods g1, const Goods g2)->bool {return g1._price < g2._price; };sort(v.begin(), v.end(), priceLess);//也可以直接傳lambda表達(dá)式sort(v.begin(), v.end(), [](const Goods g1, const Goods g2){return g1._price < g2._price;});sort(v.begin(), v.end(), [](const Goods g1, const Goods g2){return g1._price > g2._price;});sort(v.begin(), v.end(), [](const Goods g1, const Goods g2){return g1._evaluate < g2._evaluate; });sort(v.begin(), v.end(), [](const Goods g1, const Goods g2){return g1._evaluate > g2._evaluate;});sort(v.begin(), v.end(), [](const Goods g1, const Goods g2){return g1._name < g2._name;});sort(v.begin(), v.end(), [](const Goods g1, const Goods g2){return g1._name > g2._name;});return 0;
}
其中,priceLess的類型,我不知你不知只有編譯器知,見下圖,是隨機(jī)生成的類型。
其實(shí),lambda表達(dá)式就是仿函數(shù),lambda編譯時(shí),會生成對應(yīng)的仿函數(shù),上面的隨機(jī)名字就是仿函數(shù)類的名稱。
實(shí)際上,lambda表達(dá)式和范圍for很類似,范圍for替代成了迭代器,lambda替代成了仿函數(shù)。
新的類功能
在前面我們學(xué)習(xí)了右值引用,其實(shí),右值引用的特點(diǎn)是和左值引用的特點(diǎn)進(jìn)行了區(qū)分,是左值就匹配左值引用,是右值就匹配右值引用。移動(dòng)語義是,當(dāng)右值匹配到右值引用的時(shí)候,會調(diào)用移動(dòng)構(gòu)造和移動(dòng)拷貝。它們針對的是深拷貝的自定義類型對象,如string、vector、list等,可以轉(zhuǎn)移資源,提高效率。
原來的C++類中,有6個(gè)默認(rèn)成員函數(shù),構(gòu)造函數(shù)、析構(gòu)函數(shù)、拷貝構(gòu)造函數(shù)、拷貝賦值重載、取地址重載、const取地址重載,前4個(gè)最重要,后兩個(gè)用處不大。
C++11 新增了兩個(gè):移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值運(yùn)算符重載。
對于移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值運(yùn)算符重載,有一些需要注意的:
如果你沒有自己實(shí)現(xiàn)移動(dòng)構(gòu)造函數(shù),且沒有實(shí)現(xiàn)析構(gòu)函數(shù) 、拷貝構(gòu)造、拷貝賦值重載中的任意一個(gè)。那么編譯器會自動(dòng)生成一個(gè)默認(rèn)移動(dòng)構(gòu)造。默認(rèn)生成的移動(dòng)構(gòu)造函數(shù),對于內(nèi)置類型成員會執(zhí)行逐成員按字節(jié)拷貝,自定義類型成員,則需要看這個(gè)成員是否實(shí)現(xiàn)移動(dòng)構(gòu)造,如果實(shí)現(xiàn)了就調(diào)用移動(dòng)構(gòu)造,沒有實(shí)現(xiàn)就調(diào)用拷貝構(gòu)造。
如果你沒有自己實(shí)現(xiàn)移動(dòng)賦值重載函數(shù),且沒有實(shí)現(xiàn)析構(gòu)函數(shù) 、拷貝構(gòu)造、拷貝賦值重載中的任意一個(gè),那么編譯器會自動(dòng)生成一個(gè)默認(rèn)移動(dòng)賦值。默認(rèn)生成的移動(dòng)構(gòu)造函數(shù),對于內(nèi)置類型成員會執(zhí)行逐成員按字節(jié)拷貝,自定義類型成員,則需要看這個(gè)成員是否實(shí)現(xiàn)移動(dòng)賦值,如果實(shí)現(xiàn)了就調(diào)用移動(dòng)賦值,沒有實(shí)現(xiàn)就調(diào)用拷貝賦值。(默認(rèn)移動(dòng)賦值跟上面移動(dòng)構(gòu)造完全類似)。
如果提供了移動(dòng)構(gòu)造或者移動(dòng)賦值,編譯器不會自動(dòng)提供拷貝構(gòu)造和拷貝賦值。
?我們調(diào)試以下代碼可以得到驗(yàn)證:
class Person
{
public:Person(const char* name = "張三", int age = 18):_name(name), _age(age){}/*~Person(){}*/
private:ghs::string _name;int _age;
};int main()
{Person s1;//默認(rèn)拷貝構(gòu)造Person s2 = s1;Person s3 = std::move(s1);Person s4;s4 = std::move(s2);return 0;
}
?強(qiáng)制生成默認(rèn)函數(shù)的關(guān)鍵字default
C++可以更好地控制要使用的默認(rèn)函數(shù)。假設(shè)要使用某個(gè)默認(rèn)的函數(shù),但是因?yàn)槟承┰蜻@個(gè)函數(shù)沒有默認(rèn)生成。比如:當(dāng)我們提供了拷貝構(gòu)造,就不會生成移動(dòng)構(gòu)造,那么就可以使用default關(guān)鍵字指定移動(dòng)構(gòu)造生成。
class Person
{
public:Person(const char* name = "張三", int age = 18):_name(name), _age(age){}//強(qiáng)制生成Person(Person& p) = default;Person& operator=(Person& p) = default;Person(Person&& p) = default;Person& operator=(Person&& p) = default;~Person(){}
private:ghs::string _name;int _age;
};int main()
{Person s1;//默認(rèn)拷貝構(gòu)造Person s2 = s1;Person s3 = std::move(s1);Person s4;s4 = std::move(s2);return 0;
}
禁止生成默認(rèn)函數(shù)的關(guān)鍵字delete:
?如果能想要限制某些默認(rèn)函數(shù)的生成,在C++98中,是該函數(shù)設(shè)置成private,并且只聲明補(bǔ)丁已,這樣只要其他人想要調(diào)用就會報(bào)錯(cuò)。在C++11中更簡單,只需在該函數(shù)聲明加上=delete即可,該語法指示編譯器不生成對應(yīng)函數(shù)的默認(rèn)版本,稱=delete修飾的函數(shù)為刪除函數(shù)。
繼承和多態(tài)中的final與override關(guān)鍵字
這個(gè)我們在繼承和多態(tài)章節(jié)已經(jīng)進(jìn)行了詳細(xì)學(xué)習(xí)。
可變參數(shù)模版?
在C++11之前,模版中的參數(shù)數(shù)量是固定的,但是在C++11中引入了可變參數(shù)模版,這無疑是一大進(jìn)步。
我們先來看一個(gè)基本可變參數(shù)的函數(shù)模版:
template <class ...Args>
void ShowList(Args... args)
{}
其中,Args是一個(gè)模版參數(shù)包,代表0~N個(gè)類型,args是根據(jù)模版參數(shù)包定義的一個(gè)形參參數(shù)包。上面的參數(shù)Args前面有省略號,所以它是一個(gè)可變模版參數(shù),我們把帶省略號的參數(shù)成為參數(shù)包,包含0~N個(gè)模版參數(shù)。但是我們無法直接獲取參數(shù)包args中的每個(gè)參數(shù),只能通過展開參數(shù)包的方式來獲取參數(shù)包中的每個(gè)參數(shù)。由于語法不支持args[i]的方式獲取可變參數(shù),所以我們用一些方法來獲取參數(shù)包的每個(gè)值。
遞歸函數(shù)展開參數(shù)包
void _CppPrintf()
{cout << endl;
}
template<class T,class ... Args>
void _CppPrintf(const T& val, Args... args)
{cout << val << endl;_CppPrintf(args...);
}
template <class ... Args>
void CppPrintf(Args... args)
{_CppPrintf(args...);
}
int main()
{CppPrintf(1, 'A', std::string("sort"));return 0;
}
其編譯時(shí)遞歸推導(dǎo)過程如下:?
還有另外一個(gè)奇葩的推導(dǎo)方式:
STL中的emplace相關(guān)接口函數(shù)
可以看出,emplace_back對深拷貝類型有一定的優(yōu)化,但是不那么明顯,效率沒有提升很多
其實(shí),emplace_back對需要淺拷貝的效率更高,因?yàn)閜ush_back淺拷貝類型,只能調(diào)用拷貝構(gòu)造,即需要一次構(gòu)造+一次拷貝構(gòu)造,而emplace_back只需要調(diào)用一次構(gòu)造就行。
總之,emplace_back可以直接構(gòu)造,而無需調(diào)用拷貝構(gòu)造!
當(dāng)然,emplace_back除了上面的用多個(gè)參數(shù)進(jìn)行構(gòu)造,也可以用單參數(shù)構(gòu)造,
因此,當(dāng)使用emplace時(shí),實(shí)參建議的選擇順序是:參數(shù)包 > 右值 > 左值。
包裝器
function包裝器
function包裝器,也叫做適配器。C++中function本質(zhì)是一個(gè)類模版,也是一個(gè)包裝器。
ret = func(x);
上面的func可能是什么呢?可能是函數(shù)名、函數(shù)指針、仿函數(shù)或者lambda表達(dá)式,這些都是可調(diào)用對象!
但是,它們都多少有些問題:
函數(shù)指針? ? ? ?->? ? 類型定義復(fù)雜
仿函數(shù)對象? ?->? ? 要定義一個(gè)類,用的時(shí)候有點(diǎn)麻煩,不適合類型統(tǒng)一
lambda? ? ? ? ?->? ? 沒有類型概念? ? ?
template<class F, class T>
T useF(F f, T x)
{static int count = 0;cout << "count:" << ++count << endl;cout << "count:" << &count << endl;return f(x);
}
double f(double i)
{return i / 2;
}
struct Functor
{double operator()(double d){return d / 3;}
};
int main()
{// 函數(shù)名cout << useF(f, 11.11) << endl;// 函數(shù)對象cout << useF(Functor(), 11.11) << endl;// lamber表達(dá)式cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;return 0;
}
運(yùn)行以上程序,發(fā)現(xiàn)useF函數(shù)模版實(shí)例化了三份,這看起來有點(diǎn)麻煩,為了解決以上問題,引出了包裝器。
std::function在頭文件<functional>
//類模版原型
template <class Ret, class... Args>
class function<Ret(Args...)>;
模版參數(shù)說明:
Ret:被調(diào)用函數(shù)返回值
Args:被調(diào)用函數(shù)的形參
實(shí)際上,function的底層是仿函數(shù),在調(diào)用時(shí)會調(diào)用operator()。
int f(int a, int b)
{return a + b;
}
struct Functor
{
public:int operator() (int a, int b){return a + b;}
};//不是定義可調(diào)用對象,而是包裝可調(diào)用對象
int main()
{//空對象function<int(int, int)> fc1;//包裝函數(shù)指針function<int(int, int)> fc2 = f;//包裝仿函數(shù)對象function<int(int, int)> fc3 = Functor();//包裝lambdafunction<int(int, int)> fc4 = [](int x, int y) {return x + y; };cout << fc2(1, 2) << endl;//fc2本質(zhì)是調(diào)用了operator()cout << fc2.operator()(1, 2) << endl;cout << fc3(1, 2) << endl;cout << fc4(1, 2) << endl;return 0;
}
那我們不禁要問,為什么要給函數(shù)指針、仿函數(shù)、lambda外面套一個(gè)殼再使用呢,它們本身也是可以調(diào)用的啊。
包裝器的一些玩法:逆波蘭表達(dá)式求解
class Solution {
public:int evalRPN(vector<string>& tokens) {stack<int> st;//命令->動(dòng)作(函數(shù))map<string,function<int(int,int)>> m={{"+",[](int x,int y){return x+y;}},{"-",[](int x,int y){return x-y;}},{"*",[](int x,int y){return x*y;}},{"/",[](int x,int y){return x/y;}},};for(auto& e : tokens){if(m.count(e)) {function<int(int,int)> f = m[e];//操作符運(yùn)算int right = st.top();st.pop();int left = st.top();st.pop();st.push(f(left,right));}else{//操作數(shù)入棧st.push(stoi(e));}} return st.top(); }
};
另外,如果我們想要包裝成員函數(shù)指針,需要&類型::函數(shù)名這樣調(diào)用。成員函數(shù)指針又分靜態(tài)成員函數(shù)和非靜態(tài)成員函數(shù):
int main()
{//成員函數(shù)的函數(shù)指針 &類型::函數(shù)名//包裝靜態(tài)成員函數(shù)function<int(int, int)> f1 = &Plus::plusi;cout << f1(1, 2) << endl;//包裝非靜態(tài)成員函數(shù),不能直接Plus::plusd,需要在前面加取地址符&,靜態(tài)函數(shù)前可加可不加&/*function<double(double, double)> f2 = &Plus::plusd;cout << f2(1.1, 2.2) << endl;*///包裝器的參數(shù)要和成員函數(shù)的參數(shù)一致,成員函數(shù)第一個(gè)參數(shù)是一個(gè)隱含的this指針function<double(Plus*, double, double)> f3 = &Plus::plusd;Plus plus;cout << f3(&plus,1.1, 2.2) << endl;//但是,上面包裝非靜態(tài)的成員函數(shù)有點(diǎn)麻煩,還需要定義一個(gè)類對象//是通過指針&plus或者對象Plus()去調(diào)用plusd,所以這里傳指針和對象都可以function<double(Plus, double, double)> f4 = &Plus::plusd;cout << f4(Plus(), 1.1, 2.2) << endl;return 0;
}
bind
std::bind是一個(gè)函數(shù)模版,用于調(diào)整可調(diào)用對象的參數(shù)個(gè)數(shù)或者順序。
bind函數(shù)可以看做一個(gè)通用的函數(shù)適配器,它接受一個(gè)可調(diào)用對象,生成一個(gè)新的可調(diào)用對象來適應(yīng)原對象的參數(shù)列表。
調(diào)用bind的一般形式:auto newCallable = bind(callable,arg_list);
?newCallable本身是一個(gè)可調(diào)用對象,arg_list是一個(gè)逗號分隔的參數(shù)列表,給callable提供實(shí)參,當(dāng)我們調(diào)用newCallable時(shí),newCallable會調(diào)用callable,并傳給它arg_list中的參數(shù)。
fn是要綁定的函數(shù),args是要對應(yīng)的參數(shù),包含像_1、_2這樣的名字,這些名字是占位符,_1為newCallable中第一個(gè)參數(shù),_2為newCallable中第二個(gè)參數(shù),依次類推。Ret是返回值的類型,我們可以顯示實(shí)例化bind,以此來控制返回值的類型。
但是,調(diào)整順序的意義不大,了解即可。
除了調(diào)整順序以外,我們還可以調(diào)整參數(shù)個(gè)數(shù):
class Sub
{
public:int sub(int a, int b){return a - b;}
};int main()
{//綁定,調(diào)整參數(shù)個(gè)數(shù),把第一個(gè)參數(shù)用Sub()綁定死auto f4 = bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2);cout << f4(10, 5);cout << endl;auto f5 = bind(&Sub::sub, &sub, placeholders::_1, placeholders::_2);cout << f5(10, 5);return 0;
}
這樣,我們得到綁定后的對象為f4,調(diào)用f4時(shí),只需要傳兩個(gè)未綁定的參數(shù)a和b即可。
再來看一個(gè)例子:
void fx(const string& s, int x, int y)
{cout << s << " -> [血量:" << x << " 藍(lán):" << y <<"]" << endl;
}
int main()
{fx("趙云", 80, 46);fx("趙云", 78, 34);fx("趙云", 54, 13);return 0;
}
我們可以這樣調(diào)fx,這很正常,但是調(diào)fx時(shí)每次都要傳“趙云”這個(gè)參數(shù),其實(shí),我們可以綁定第一個(gè)參數(shù)一直是“趙云”,這樣只需要輸入?yún)?shù)x和y即可:
int main()
{auto f1 = bind(fx1, "趙云", placeholders::_1, placeholders::_2);f1(100, 89);f1(98, 76);return 0;
}
除了綁定第一個(gè)參數(shù),我們還可以綁定第二個(gè)參數(shù),比如在游戲中開掛,無論使用哪個(gè)角色,血量一直保持在100:
int main()
{auto f2 = bind(fx1, placeholders::_1, 100, placeholders::_2);f2("孫尚香", 46);f2("關(guān)羽", 93);return 0;
}
我們需要記住的是,_1代表第一個(gè)實(shí)參,_2代表第二個(gè)實(shí)參。
實(shí)際上,bind的返回值是一個(gè)類,里面重載了operator(),實(shí)際上會調(diào)用仿函數(shù)。
bind的返回除了傳給auto,也可以傳給function(因?yàn)閎ind的返回值是一個(gè)類,里面重載了仿函數(shù)),
線程庫
我們之前了解的多線程問題,都是和平臺相關(guān)的,比如windows和linux下都有自己的接口,這使得代碼的可移植性比較差。C++11中最重要的特性就是對線程進(jìn)行支持了,使得C++在多線程編程時(shí)不需要依賴第三方庫。要使用標(biāo)準(zhǔn)庫中的線程,必須包含<thread>頭文件。
注意:
1.當(dāng)創(chuàng)建線程對象后,如果使用無參構(gòu)造,沒有提供線程函數(shù),該對象沒有對應(yīng)任何線程。
get_id()的返回值類型是id類型,id類型為std::thread命名空間下瘋轉(zhuǎn)的一個(gè)類。
2.當(dāng)創(chuàng)建一個(gè)線程對象后,并且給線程關(guān)聯(lián)函數(shù),該線程就會被啟動(dòng),與主線程一起運(yùn)行。線程函數(shù)一般有3種提供方式:
- 函數(shù)指針
- lambda表達(dá)式
- 函數(shù)對象
函數(shù)指針:
void print(int n, int k)
{for (int i = k; i < n; i++){std::cout << i << " ";}std::cout << std::endl;
}int main()
{std::thread t1(print, 100, 0);std::thread t2(print, 200, 100);std::cout << t1.get_id() << std::endl;std::cout << t2.get_id() << std::endl;t1.join();t2.join();std::cout << std::this_thread::get_id << std::endl;return 0;
}
lambda表達(dá)式:
int main()
{int x = 0;std::mutex mtx;std::thread t1([&] {mtx.lock();for (int i = 0; i < 10000; i++){++x;}mtx.unlock();});std::thread t2([&]{mtx.lock();for (int i = 0; i < 10000; i++){++x;}mtx.unlock();});t1.join();t2.join();std::cout << x << std::endl;return 0;
}
可以使用this_thread類中的get_id來獲取主線程id。
3.線程不支持拷貝,不允許拷貝構(gòu)造和賦值,但是支持移動(dòng)構(gòu)造和移動(dòng)賦值。
線程函數(shù)參數(shù)
線程函數(shù)參數(shù)是以值拷貝的方式拷貝到線程棧空間中的,因此,即使線程參數(shù)為引用類型,在線程中也不能修改外部實(shí)參,因?yàn)槠鋵?shí)際引用的是線程棧中的拷貝,而不是外部實(shí)參。
如果想要通過形參改變外部實(shí)參,必須借助std::ref()函數(shù)或者傳入指針。
lock_guard和unique_lock
在多線程中,為了保證線程安全,需要通過鎖的方式控制。?
?如果把鎖放到for循環(huán)里面,也是線程安全的,但是這樣會導(dǎo)致線程之間頻繁切換,效率低。
上述代碼中,其缺陷是,鎖控制不好時(shí),可能會造成死鎖,比如在鎖中間代碼返回,或者在鎖的范圍內(nèi)拋異常。因此,C++采用RAII的方式對鎖進(jìn)行了封裝,即lock_guard和unique_lock。
那如果在func中前半部分希望加鎖,而后半部分不希望加鎖,只需要用{}把前半部分括住,定義一個(gè)局部域,LockGuard的生命周期就在這個(gè){}局部域了。
而unique_lock和lock_guard類似,只不過功能更豐富一些,支持手動(dòng)加鎖解鎖。
原子性操作庫
傳統(tǒng)解決線程安全的方法是對共享資源進(jìn)行加鎖保護(hù),雖然加鎖可以解決問題,但是加鎖的缺陷是,只要要有一個(gè)線程在對sum++,其他線程就會被阻塞,影響效率,而且可能造成死鎖。
為此,C++11中引入了原子操作,需要包含<atomic>庫。
在C++11中,程序員不需要對原子類型變量進(jìn)行加鎖解鎖操作,線程能夠?qū)υ宇愋妥兞炕コ庠L問。程序員可以使用atomic類模版,定義出需要的任意原子類型。
atomic<T> t; //聲明一個(gè)類型為T的原子類型變量t
兩個(gè)線程交替打印
要使兩個(gè)線程交替打印,需要使用到條件變量:?
條件變量condition_variable用于進(jìn)行進(jìn)程之間的互相通知。
為了使得線程交替打印,要保證線程1先執(zhí)行,線程2后執(zhí)行(哪怕把線程2放到前面)。
這里利用了wait,
由于條件變量wait不是線程安全的,因此要給wait傳互斥鎖,調(diào)用wait的線程被阻塞,直到被notified,wait的作用是使進(jìn)程間做到同步。在wait阻塞進(jìn)程時(shí),當(dāng)前進(jìn)程會先把鎖解掉,允許在這個(gè)鎖上阻塞的線程繼續(xù)走。當(dāng)這個(gè)進(jìn)程被喚醒后,這個(gè)進(jìn)程會解阻塞,并獲取到這個(gè)鎖。
notify_one會喚醒在這個(gè)條件變量上等待的一個(gè)線程,如果沒有現(xiàn)成在上面等待,什么都不做,如果有多個(gè)線程在等待,會選擇其中任意一個(gè)。
下面是兩個(gè)線程交替打印的代碼:
{std::mutex mtx;std::condition_variable c;int n = 100;bool flag = true;std::thread t1([&]() {int i = 0;while (i < n){std::unique_lock<std::mutex> lock(mtx);//flag=false t1就阻塞//flag=true t1就不會阻塞while (!flag){c.wait(lock);}std::cout << i << std::endl;flag = false;i += 2; // 偶數(shù)c.notify_one();}});std::thread t2([&]() {int j = 1;while (j < n){std::unique_lock<std::mutex> lock(mtx);//只要flag==true,t2就阻塞//只要flag==false,t2就不會阻塞while (flag)c.wait(lock);std::cout << j << std::endl;j += 2; // 奇數(shù)flag = true;c.notify_one();}});t1.join();t2.join();return 0;
}