C++一些新的特性的理解
一、智能指針
為什么需要智能指針?
智能指針主要解決一下問題:
- 內(nèi)存泄漏:內(nèi)存手動(dòng)釋放,使用智能指針可以自動(dòng)釋放
- 共享所有權(quán)的指針的傳播和釋放,比如多線程使用同一個(gè)對(duì)象時(shí)析構(gòu)的問題。
C++里面的四個(gè)智能指針,auto_ptr,shared_ptr,unique_ptr,weak_ptr其中后3個(gè)是C++11支持,并且第一個(gè)已經(jīng)被C++棄用。
幾個(gè)指針的特點(diǎn)
- unique_ptr獨(dú)占對(duì)象的所有權(quán),由于沒有引用技術(shù),因此性能較好。
- shared_ptr共享對(duì)象的所有權(quán),但性能略差。
- weak_ptr配合shared_ptr,解決循環(huán)引用的問題。
三者如何選擇呢?
1.1 Shared_ptr內(nèi)存模型

從圖中我們可以知道我們主要是保存一個(gè)原始的指針對(duì)象,還有個(gè)控制快,保存了shared_ptr的引用計(jì)數(shù)等。
1.2智能指針使用場景案例
- 使用智能指針可以自己釋放占用的內(nèi)存
例子:
class A
{
public:
A();
~A();
private:
};
A::A()
{
std::cout << "A Created!" << std::endl;
}
A::~A()
{
std::cout << "A Deleted!" << std::endl;
}
int main()
{
auto Aptr = std::make_shared<A>();
auto Aptr1 = new A{};
return 0;
}
可以看到,智能指針會(huì)自動(dòng)釋放內(nèi)存

2. 使用智能指針可以自己釋放占用的內(nèi)存
1.3 shared_ptr共享的智能指針
std::shared_ptr使用引用計(jì)數(shù)每一個(gè)shared_ptr的拷貝都指向相同的內(nèi)存,最后一個(gè)shared_ptr析構(gòu)的時(shí)候,內(nèi)存才會(huì)被釋放。
shared_ptr共享被管理對(duì)象,同一時(shí)刻可以有多個(gè)shared_ptr擁有對(duì)象的所有權(quán),當(dāng)最后一個(gè)shared_ptr對(duì)象銷毀時(shí),被管理對(duì)象自動(dòng)銷毀。
簡單來說,shared_ptr實(shí)現(xiàn)包含了兩部分,
-
一個(gè)指向堆上創(chuàng)建的對(duì)象的裸指針,raw_ptr
-
一個(gè)指向內(nèi)部隱藏的、共享的管理對(duì)象.share_count_object
-
user_count,當(dāng)前這個(gè)堆上對(duì)象被多少對(duì)象引用了,簡單來說就是引用計(jì)數(shù)。
1.3.1 shared_ptr的基本用法和常用函數(shù)
s.get(): 返回shared_ptr中保存的裸指針;
s.reset():重置shared_ptr;
- reset()不帶參數(shù)時(shí),若智能指針s是唯一指向該對(duì)象的指針,則釋放。并置空。若智能指針P不是唯一指向該對(duì)象的指針,則引用技術(shù)減少一,同時(shí)將P置空。
- reset()帶參數(shù)時(shí),若智能指針s是唯一指向?qū)ο蟮闹羔槪瑒t釋放并指向新的對(duì)象。若P不是唯一的指針,則只減少引用技術(shù),并指向新的對(duì)象。如:
auto ptr = std::make_shared<int>(303);
ptr.reset(new int(10000));
s.use_count();返回shared_ptr的強(qiáng)引用計(jì)數(shù)。
s.unique();若use_count()為1.則返回true,否則返回false。
1. 初始化make_shared/reset
通過構(gòu)造函數(shù),std::make_shared_ptr輔助函數(shù)和reset方法來初始化shared_ptr,代碼如下:
std::shared_ptr<int>p1(new int(1));
std::shared_ptr<int>p2 = p1;
std::shared_ptr<int>p3;
p3.reset(new int(1));
if (p3)
{
std::cout << "p3 is not null!" << std::endl;
}
我們應(yīng)該優(yōu)先使用make_shared來構(gòu)造智能指針,因?yàn)樗咝?/strong>
auto sp1=make_shared<int>(100);
//或
shared_ptr<int>sql=make_shared<int>(100);
//相當(dāng)于
shared_ptr<int> sp1(new int(100))
不能將一個(gè)原始指針直接賦值給一個(gè)智能指針,例如,下面這種方式錯(cuò)誤的:
std::shared_ptr<int> p = new int(1);
shared_ptr不能通過"直接將原始這種賦值"來初始化,需要通過構(gòu)造函數(shù)和輔助方法來初始化。
- 對(duì)于一個(gè)未初始化的智能指針,可以通過reset方法來初始化;
- 當(dāng)智能指針有值的時(shí)候調(diào)用reset會(huì)引起引用計(jì)數(shù)減1.
另外智能指針可以通過重載的bool類型來操作符來判斷。
std::shared_ptr<int> p1;
p1.reset(new int(1));
std::shared_ptr<int>p2 = p1;
//引用次數(shù)此時(shí)應(yīng)該是2
std::cout << "p2.use_count() =" << p2.use_count() << std::endl;
p1.reset();
std::cout << "p1.reset()\n";
//引用次數(shù)此時(shí)應(yīng)該是1
std::cout << "p2.use_count() =" << p2.use_count() << std::endl;
if (!p1)
{
std::cout << "p1 is empty!\n";
}
if(!p2)
{
std::cout << "p2 is empty!\n";
}
p2.reset();
std::cout << "p2.reset()\n";
std::cout << "p2.use_count() =" << p2.use_count() << std::endl;
if (!p2)
{
std::cout << "p2 is empty!\n";
}
2.獲取原始指針get
當(dāng)需要獲取原始指針時(shí),可以通過get方法來返回原始指針,代碼如下所示:
std::shared_ptr<int> ptr (new int(1));
int *p=ptr.get();
//不可以 delete p;
謹(jǐn)慎使用p.get()的返回值,如果你不知道其危險(xiǎn)性則永遠(yuǎn)不要調(diào)用get()函數(shù)。
p.get()的返回值就相當(dāng)于一個(gè)裸指針的值,不合適的使用這個(gè)值,上述陷阱的所有錯(cuò)誤都有可能發(fā)聲方法,遵守一下幾個(gè)約定:
- 不要保存p.get()的返回值,無論是保存為裸指針還是shared_ptr都是錯(cuò)誤的。
- 保存為裸指針不知道什么時(shí)候就會(huì)變成空懸指針,保存為shared_ptr則產(chǎn)生了獨(dú)立指針
- 不要delete p.get()的返回值,會(huì)導(dǎo)致對(duì)一塊內(nèi)存delete兩次的錯(cuò)誤
3. 指定刪除器
如果用shared_pptr管理非new對(duì)象或是沒有析構(gòu)函數(shù)的類時(shí),應(yīng)當(dāng)為其傳遞合適的刪除器。
示例代碼如下:
void DeleteIntPtr(int *p)
{
std::cout << "call DeleteIntPtr" << std::endl;
delete p;
}
int main()
{
std::shared_ptr<int> p(new int(1), DeleteIntPtr);
return 0;
}
當(dāng)p的引用計(jì)數(shù)為0時(shí),自動(dòng)調(diào)用刪除器DeleteIntPtr來釋放對(duì)象的內(nèi)存,刪除器可以時(shí)一個(gè)lambda表達(dá)式,上面的寫法可以改為:
std::shared_ptr<int> p(new int(1), [](int *p) {
cout << "call lambda delete p" << endl;
delete p;});
當(dāng)我們用shared_ptr管理動(dòng)態(tài)數(shù)組時(shí),需要指定刪除器,因?yàn)閟hared_ptr的默認(rèn)刪除器不支持?jǐn)?shù)組對(duì)象,代碼如下所示::
std::shared_ptr<int> p(new int[20], [](int* p) {delete[] p; });
1.3.2 使用shared_ptr要注意的問題
1. 不要用一個(gè)原始指針初始化多個(gè)shared_ptr
例如下面錯(cuò)誤范例:
int * ptr=new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr); //邏輯錯(cuò)誤
2. 不要在函數(shù)實(shí)參中創(chuàng)建shared_ptr
對(duì)于下面的寫法:
function(shared_ptr<int>(new int),g());
因?yàn)镃++的函數(shù)參數(shù)的計(jì)算順序在不同的編譯器不同的約定下可能是不一樣的,一般是從右到左,但也可能從左到右,所以,可能的過程是先new int ,然后調(diào)用g(),如果恰好g()發(fā)生異常,而shared_ptr還沒有創(chuàng)建,則int內(nèi)存泄漏了,正確的寫法應(yīng)該是先創(chuàng)建智能指針,代碼如下:
shared_ptr<int> p(new int);
function(p,g());
3.通過shared_from_this()返回this指針
不要將this指針作為shared_ptr返回出來,因?yàn)閠his指針本質(zhì)上是一個(gè)裸指針,因此,這樣可能會(huì)到重復(fù)析構(gòu),看下面的例子。
class A
{
public:
std::shared_ptr<A> GetSelf()
{
return std::shared_ptr<A>(this); // 不要這么做
}
~A()
{
std::cout << "Destructor A" << std::endl;
}
};
int main()
{
std::shared_ptr<A> sp1(new A);
std::shared_ptr<A> sp2 = sp1->GetSelf();
return 0;
}
運(yùn)行后調(diào)用了兩次析構(gòu)函數(shù)。
在這個(gè)例子中,由于用同一個(gè)指針(this)構(gòu)造了兩個(gè)智能指針sp1和sp2,而他們之間是沒有任何關(guān)系的,在離開作用域之后this將會(huì)被構(gòu)造的兩個(gè)智能各自析構(gòu),導(dǎo)致重復(fù)析構(gòu)的錯(cuò)誤。
正確返回this的shared_ptr的做法是:讓目標(biāo)類通過std::enable_shared_from_this類,然后使用積累的成員函數(shù)shared_from_this()來返回this的shared_ptr,如下所示:
class A : public std::enable_shared_from_this<A>
{
public:
std::shared_ptr<A>GetSelf()
{
return shared_from_this(); //
}
~A()
{
std::cout << "Destructor A" << std::endl;
}
};
int main()
{
std::shared_ptr<A> sp1(new A);
std::shared_ptr<A> sp2 = sp1->GetSelf(); // ok
return 0;
}
4.避免循環(huán)引用
循環(huán)引用會(huì)導(dǎo)致內(nèi)存泄漏,比如:
class A;
class B;
class A {
public:
std::shared_ptr<B> bptr;
~A() {
cout << "A is deleted" << endl;
}
};
class B {
public:
std::shared_ptr<A> aptr;
~B() {
cout << "B is deleted" << endl;
}
};
int main()
{
{
std::shared_ptr<A> ap(new A);
std::shared_ptr<B> bp(new B);
ap->bptr = bp;
bp->aptr = ap;
}
cout<< "main leave" << endl; // 循環(huán)引用導(dǎo)致ap bp退出了作用域都沒有析構(gòu)
return 0;
}
循環(huán)引用導(dǎo)致ap和bp的引用次數(shù)為2,在離開作用域以后,ap和BP的引用計(jì)數(shù)減為1,并不會(huì)減為0,導(dǎo)致兩個(gè)指針都不會(huì)被析構(gòu),產(chǎn)生內(nèi)存泄漏。
解決的方法就是把A和B任何一個(gè)成員變量改為weak_ptr,具體方法見weak_ptr章節(jié)。
1.4unique_ptr獨(dú)占的智能指針
- unique_ptr是一個(gè)獨(dú)占性的智能指針,不能將其賦值給另一個(gè)unique_ptr.
- unique_ptr可以指向一個(gè)數(shù)組
- unique_ptr需要確定刪除器的類型
std::shared_ptr<int> ptr3(new int(1), [](int *p){delete p;}); // 正確
std::unique_ptr<int, void(*)(int*)> ptr5(new int(1), [](int *p){delete p;}); // 正確
unique_ptr是一個(gè)獨(dú)占型的智能指針,它不允許其他的智能智能共享其內(nèi)部的指針,不允許通過賦值將一個(gè)unique_ptr賦值給另一個(gè)unique_ptr,下面的錯(cuò)誤的示例
unique_ptr<T> my_ptr(new T);
unique_ptr<T> my_other_ptr = my_ptr; // 報(bào)錯(cuò),不能復(fù)制
unique_ptr不允許賦值,但可以通過函數(shù)返回給其他的unique_ptr,還可以通過std::move來轉(zhuǎn)移到其他的unique_ptr,這樣它本身就不再擁有原來指針的所有權(quán)了。例如
unique_ptr<T> my_ptr(new T); // 正確
unique_ptr<T> my_other_ptr = std::move(my_ptr); // 正確
unique_ptr<T> ptr = my_ptr; // 報(bào)錯(cuò),不能復(fù)制
std::make_shared是C++11的一部分,但std::make_unique不是。它實(shí)在c++14里加入標(biāo)準(zhǔn)庫的。、
auto upw1(std::make_unique<Widget>()); // with make func
std::unique_ptr<Widget> upw2(new Widget); // without make func
使用new的版本重復(fù)了被創(chuàng)建對(duì)象的鍵入,但是make_unique函數(shù)則沒有。重復(fù)類型違背了軟件工程的
一個(gè)重要原則:應(yīng)該避免代碼重復(fù),代碼中的重復(fù)會(huì)引起編譯次數(shù)增加,導(dǎo)致目標(biāo)代碼膨脹。
除了unique_ptr的獨(dú)占性, unique_ptr和shared_ptr還有一些區(qū)別,比如
- unique_ptr可以指向一個(gè)數(shù)組,代碼如下所示
std::unique_ptr<int []> ptr(new int[10]);
ptr[9] = 9;
std::shared_ptr<int []> ptr2(new int[10]); // 這個(gè)是不合法的
- unique_ptr指定刪除器和shared_ptr有區(qū)別
std::shared_ptr<int> ptr3(new int(1), [](int *p){delete p;}); // 正確
std::unique_ptr<int> ptr4(new int(1), [](int *p){delete p;}); // 錯(cuò)誤
unique_ptr需要確定刪除器的類型,所以不能像shared_ptr那樣直接指定刪除器,可以這樣寫:
std::unique_ptr<int, void(*)(int*)> ptr5(new int(1), [](int *p){delete p;}); //
正確
關(guān)于shared_ptr和unique_ptr的使用場景是要根據(jù)實(shí)際應(yīng)用需求來選擇。
如果希望只有一個(gè)智能指針管理資源或者管理數(shù)組就用unique_ptr,如果希望多個(gè)智能指針管理同一個(gè)
資源就用shared_ptr。
1.5 weak_ptr弱引用的智能指針
什么是weak_ptr
weak_ptr解決了什么問題;
weak_ptr為什么能解決問題。
share_ptr雖然已經(jīng)很好用了,但是有一點(diǎn)share_ptr智能指針還是有內(nèi)存泄露的情況,當(dāng)兩個(gè)對(duì)象相互
使用一個(gè)shared_ptr成員變量指向?qū)Ψ剑瑫?huì)造成循環(huán)引用,使引用計(jì)數(shù)失效,從而導(dǎo)致內(nèi)存泄漏。
weak_ptr 是一種不控制對(duì)象生命周期的智能指針, 它指向一個(gè) shared_ptr 管理的對(duì)象. 進(jìn)行該對(duì)象的內(nèi)
存管理的是那個(gè)強(qiáng)引用的shared_ptr, weak_ptr只是提供了對(duì)管理對(duì)象的一個(gè)訪問手段。
weak_ptr 設(shè)計(jì)的目的是為配合 shared_ptr 而引入的一種智能指針來協(xié)助 shared_ptr 工作, 它只可以從
一個(gè) shared_ptr 或另一個(gè) weak_ptr 對(duì)象構(gòu)造, 它的構(gòu)造和析構(gòu)不會(huì)引起引用記數(shù)的增加或減少。
1.5.1 weak_ptr的基本用法
- 通過use_count()方法獲取當(dāng)前觀察資源的引用計(jì)數(shù),如下所示:
std::shared_ptr<int> sp(new int(10));
std::weak_ptr<int> wp(sp);
std::cout << wp.use_count() << std::endl; //結(jié)果講輸出1
- 通過expired()方法判斷所觀察資源是否已經(jīng)釋放,如下所示:
std::shared_ptr<int> sp(new int(10));
std::weak_ptr<int> wp(sp);
if (wp.expired())
std::cout << "weak_ptr無效,資源已釋放";
else
std::cout << "weak_ptr有效";
- 通過lock方法獲取監(jiān)視的shared_ptr,如下所示:
lock有什么用處?
std::weak_ptr<int> gw;
void f2()
{
std::cout << "lock\n";
auto spt = gw.lock(); // 鎖好資源再去判斷是否有效
std::this_thread::sleep_for(std::chrono::seconds(2));
if (gw.expired()) {
std::cout << "gw Invalid, resource released\n";
}
else {
std::cout << "gw Valid, *spt = " << *spt << std::endl;
}
}
int main()
{
{
auto sp = std::make_shared<int>(42);
gw = sp;
std::thread([&]() {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "sp reset\n";
sp.reset();
}).detach();
f2();
}
f2();
return 0;
}
1.5.2 weak_ptr返回this指針
shared_ptr章節(jié)中提到不能直接將this指針返回shared_ptr,需要通過派生
std::enable_shared_from_this類,并通過其方法shared_from_this來返回指針,原因是
std::enable_shared_from_this類中有一個(gè)weak_ptr,這個(gè)weak_ptr用來觀察this智能指針,調(diào)用
shared_from_this()方法是,會(huì)調(diào)用內(nèi)部這個(gè)weak_ptr的lock()方法,將所觀察的shared_ptr返回,再看
前面的范例
#include <iostream>
#include <memory>
using namespace std;
class A: public std::enable_shared_from_this<A>
{
public:
shared_ptr<A>GetSelf()
{
return shared_from_this(); //
}
~A()
{
cout << "Destructor A" << endl;
}
};
int main()
{
shared_ptr<A> sp1(new A);
shared_ptr<A> sp2 = sp1->GetSelf(); // ok
return 0;
}
輸出結(jié)果如下:
Destructor A
在外面創(chuàng)建A對(duì)象的智能指針和通過對(duì)象返回this的智能指針都是安全的,因?yàn)閟hared_from_this()是內(nèi)
部的weak_ptr調(diào)用lock()方法之后返回的智能指針,在離開作用域之后,spy的引用計(jì)數(shù)減為0,A對(duì)象會(huì)
被析構(gòu),不會(huì)出現(xiàn)A對(duì)象被析構(gòu)兩次的問題。
需要注意的是,獲取自身智能指針的函數(shù)盡在shared_ptr的構(gòu)造函數(shù)被調(diào)用之后才能使用,因?yàn)?br>
enable_shared_from_this內(nèi)部的weak_ptr只有通過shared_ptr才能構(gòu)造。
1.5.3 weak_ptr解決循環(huán)引用問題
在shared_ptr章節(jié)提到智能指針循環(huán)引用的問題,因?yàn)橹悄苤羔樀难h(huán)引用會(huì)導(dǎo)致內(nèi)存泄漏,可以通過
weak_ptr解決該問題,只要將A或B的任意一個(gè)成員變量改為weak_ptr
#include <iostream>
#include <memory>
using namespace std;
class A;
class B;
class A {
public:
std::weak_ptr<B> bptr; // 修改為weak_ptr
~A() {
cout << "A is deleted" << endl;
}
};
class B {
public:
std::shared_ptr<A> aptr;
~B() {
cout << "B is deleted" << endl;
}
};
int main()
{
{
std::shared_ptr<A> ap(new A);
std::shared_ptr<B> bp(new B);
ap->bptr = bp;
bp->aptr = ap;
}
cout<< "main leave" << endl;
return 0;
}
這樣在對(duì)B的成員賦值時(shí),即執(zhí)行bp->aptr=ap;時(shí),由于aptr是weak_ptr,它并不會(huì)增加引用計(jì)數(shù),所
以ap的引用計(jì)數(shù)仍然會(huì)是1,在離開作用域之后,ap的引用計(jì)數(shù)為減為0,A指針會(huì)被析構(gòu),析構(gòu)后其內(nèi)
部的bptr的引用計(jì)數(shù)會(huì)被減為1,然后在離開作用域后bp引用計(jì)數(shù)又從1減為0,B對(duì)象也被析構(gòu),不會(huì)發(fā)
生內(nèi)存泄漏。
1.5.4 weak_ptr使用注意事項(xiàng)
1. weak_ptr在使用前需要檢查合法性。
weak_ptr<int> wp;
{
shared_ptr<int> sp(new int(1)); //sp.use_count()==1
wp = sp; //wp不會(huì)改變引用計(jì)數(shù),所以sp.use_count()==1
shared_ptr<int> sp_ok = wp.lock(); //wp沒有重載->操作符。只能這樣取所指向的對(duì)象
}
shared_ptr<int> sp_null = wp.lock(); //sp_null .use_count()==0
因?yàn)樯鲜龃a中sp和sp_ok離開了作用域,其容納的K對(duì)象已經(jīng)被釋放了。
得到了一個(gè)容納NULL指針的sp_null對(duì)象。在使用wp前需要調(diào)用wp.expired()函數(shù)判斷一下。
因?yàn)閣p還仍舊存在,雖然引用計(jì)數(shù)等于0,仍有某處“全局”性的存儲(chǔ)塊保存著這個(gè)計(jì)數(shù)信息。直到最后
一個(gè)weak_ptr對(duì)象被析構(gòu),這塊“堆”存儲(chǔ)塊才能被回收。否則weak_ptr無法直到自己所容納的那個(gè)指針
資源的當(dāng)前狀態(tài)。
如果shared_ptr sp_ok和weak_ptr wp;屬于同一個(gè)作用域呢?如下所示:
weak_ptr<int> wp;
shared_ptr<int> sp_ok;
{
shared_ptr<int> sp(new int(1)); //sp.use_count()==1
wp = sp; //wp不會(huì)改變引用計(jì)數(shù),所以sp.use_count()==1
sp_ok = wp.lock(); //wp沒有重載->操作符。只能這樣取所指向的對(duì)象
}
if(wp.expired()) {
cout << "shared_ptr is destroy" << endl;
} else {
cout << "shared_ptr no destroy" << endl;
}
1.6 智能指針安全性問題
引用計(jì)數(shù)本身是安全的,至于智能指針是否安全需要結(jié)合實(shí)際使用分情況討論:
情況1:多線程代碼操作的是同一個(gè)shared_ptr的對(duì)象,此時(shí)是不安全的。
比如std::thread的回調(diào)函數(shù),是一個(gè)lambda表達(dá)式,其中引用捕獲了一個(gè)shared_ptr.
std::thread td([&sp1]()){....});
又或者通過回調(diào)函數(shù)的參數(shù)傳入的shared_ptr對(duì)象,參數(shù)類型引用
void fn(shared_ptr<A>&sp) {
...
}
..
std::thread td(fn, sp1);
這時(shí)候必然不是線程安全的。
情況2:多線程代碼操作的不是同一個(gè)shared_ptr的對(duì)象
這里指的是管理的數(shù)據(jù)是同一份,而shared_ptr不是同一個(gè)對(duì)象。比如多線程回調(diào)的lambda的是按值捕
獲的對(duì)象。
std::thread td([sp1]()){....});
另個(gè)線程傳遞的shared_ptr是值傳遞,而非引用:
void fn(shared_ptr<A>sp) {
...
}
..
std::thread td(fn, sp1);
這時(shí)候每個(gè)線程內(nèi)看到的sp,他們所管理的是同一份數(shù)據(jù),用的是同一個(gè)引用計(jì)數(shù)。但是各自是不同的
對(duì)象,當(dāng)發(fā)生多線程中修改sp指向的操作的時(shí)候,是不會(huì)出現(xiàn)非預(yù)期的異常行為的。
也就是說,如下操作是安全的。
void fn(shared_ptr<A>sp) {
...
if(..){
sp = other_sp;
} else {
sp = other_sp2;
}
}
需要注意:所管理數(shù)據(jù)的線程安全性問題。顯而易見,所管理的對(duì)象必然不是線程安全的,必然 sp1、
sp2、sp3智能指針實(shí)際都是指向?qū)ο驛, 三個(gè)線程同時(shí)操作對(duì)象A,那對(duì)象的數(shù)據(jù)安全必然是需要對(duì)象
A自己去保證。
2.什么是左值引用、右值引用
引用本質(zhì)是別名,可以通過引用修改變量的值,傳參時(shí)傳引用可以避免拷貝。
2.1 左值引用
左值引用:能指向左值,不能指向右值的就是左值引用
但是,const左值引用是可以指向右值的:
const int &ref_a = 5; // 編譯通過
const左值引用不會(huì)修改指向值,值,因此可以指向右值,這也是為什么要使用 const & 作為函數(shù)參數(shù)的原因之一,如 std::vector的 push_back:
void push_back (const value_type& val);
如果沒有const,vec.push_back(5)這樣的代碼就無法編譯通過。
2.2 右值引用
再看下右值引用,右值引用的標(biāo)志是 && ,顧名思義,右值引用專門為右值而生,可以指向右值,不能指
向左值:
int &&ref_a_right = 5; // ok
int a = 5;
int &&ref_a_left = a; // 編譯不過,右值引用不可以指向左值
ref_a_right = 6; // 右值引用的用途:可以修改右值
2.3對(duì)左右值引用本質(zhì)的討論
我們必須先掌握這個(gè)概念——值類別。
值類別是 C++11 標(biāo)準(zhǔn)中新引入的概念,具體來說它是表達(dá)式的一種屬性,該屬性
將表達(dá)式分為 3 個(gè)類別,它們分別是左值(lvalue)、純右值(prvalue)和將亡值
(xvalue),如圖 6-1 所示。從前面的內(nèi)容中我們知道早在 C++98 的時(shí)候,已經(jīng)有了
一些關(guān)于左值和右值的概念了,只不過當(dāng)時(shí)這些概念對(duì)于 C++程序編寫并不重要。
但是由于 C++11 中右值引用的出現(xiàn),值類別被賦予了全新的含義。可惜的是,在
C++11 標(biāo)準(zhǔn)中并沒能夠清晰地定義它們,比如在 C++11 的標(biāo)準(zhǔn)文檔中,左值的概
念只有一句話:“指定一個(gè)函數(shù)或一個(gè)對(duì)象”,這樣的描述顯然是不清晰的。這種
糟糕的情況一直延續(xù)到 C++17 標(biāo)準(zhǔn)的推出才得到解決。所以現(xiàn)在是時(shí)候讓我們重
新認(rèn)識(shí)這些概念了。

表達(dá)式首先被分為了泛左值(glvalue)和右值(rvalue),其中泛左值被進(jìn)一步劃
分為左值和將亡值,右值又被劃分為將亡值和純右值。理解這些概念的關(guān)鍵在于泛
左值、純右值和將亡值。
1.所謂泛左值是指一個(gè)通過評(píng)估能夠確定對(duì)象、位域或函數(shù)的標(biāo)識(shí)的表達(dá)式。
簡單來說,它確定了對(duì)象或者函數(shù)的標(biāo)識(shí)(具名對(duì)象)。
2.而純右值是指一個(gè)通過評(píng)估能夠用于初始化對(duì)象和位域,或者能夠計(jì)算運(yùn)算
符操作數(shù)的值的表達(dá)式。
3.將亡值屬于泛左值的一種,它表示資源可以被重用的對(duì)象和位域,通常這是
因?yàn)樗鼈兘咏渖芷诘哪┪玻硗庖部赡苁墙?jīng)過右值引用的轉(zhuǎn)換產(chǎn)生的。
2.3.1 右值引用有辦法指向左值嗎?
有辦法,使用 std::move :
nt a = 5; // a是個(gè)左值
int &ref_a_left = a; // 左值引用指向左值
int &&ref_a_right = std::move(a); // 通過std::move將左值轉(zhuǎn)化為右值,可以被右值引用指向
cout << a; // 打印結(jié)果:5
在上邊的代碼里,看上去是左值a通過std::move移動(dòng)到了右值ref_a_right中,那是不是a里邊就沒有值
了?并不是,打印出a的值仍然是5。
std::move是一個(gè)非常有迷惑性的函數(shù):
- 不理解左右值概念的人們往往以為它能把一個(gè)變量里的內(nèi)容移動(dòng)到另一個(gè)變量;
- 但事實(shí)上std::move移動(dòng)不了什么,唯一的功能是把左值強(qiáng)制轉(zhuǎn)化為右值,讓右值引用可以指向左值。其實(shí)現(xiàn)等同于一個(gè)類型轉(zhuǎn)換:
static_cast<T&&>(lvalue)。 所以,單純的std::move(xxx)
不會(huì)有性能提升。
同樣的,**右值引用能指向右值****,本質(zhì)上也是把右值提升為一個(gè)左值,并定義一個(gè)右值引用通過std::move
指向該左值:
int &&ref_a = 5;
ref_a = 6;
等同于以下代碼:
int temp = 5;
int &&ref_a = std::move(temp);
ref_a = 6;
// 此時(shí)temp等于?
2.3.2 左值引用、右值引用本身是左值還是右值?
被聲明出來的左、右值引用都是左值.因?yàn)楸宦暶鞒龅淖笥抑狄檬?strong>有地址的,也位于等號(hào)左邊。仔細(xì)
看下邊代碼:
// 形參是個(gè)右值引用
void change(int&& right_value) {
right_value = 8;
}
int main() {
int a = 5; // a是個(gè)左值
int &ref_a_left = a; // ref_a_left是個(gè)左值引用
int &&ref_a_right = std::move(a); // ref_a_right是個(gè)右值引用
change(a); // 編譯不過,a是左值,change參數(shù)要求右值
change(ref_a_left); // 編譯不過,左值引用ref_a_left本身也是個(gè)左值
change(ref_a_right); // 編譯不過,右值引用ref_a_right本身也是個(gè)左值
change(std::move(a)); // 編譯通過
change(std::move(ref_a_right)); // 編譯通過
change(std::move(ref_a_left)); // 編譯通過
change(5); // 當(dāng)然可以直接接右值,編譯通過
cout << &a << ' ';
cout << &ref_a_left << ' ';
cout << &ref_a_right;
// 打印這三個(gè)左值的地址,都是一樣的
}
看完后你可能有個(gè)問題,std::move會(huì)返回一個(gè)右值引用int &&,它是左值還是右值呢? 從表達(dá)式int &&ref = std::move(a)來看,右值引用ref指向的必須是右值,所以move返回的int &&是個(gè)右值。
所以右值引用既可能是左值,又可能是右值嗎? 確實(shí)如此:右值引用既可以是左值也可以是右值,如果
有名稱則為左值,否則是右值。
或者說:作為函數(shù)返回值的&&是右值,直接聲明出來的&&是左值.這同樣也符合前面章節(jié)對(duì)左值,
右值的判定方式:其實(shí)引用和普通變量是一樣的, int &&ref = std::move(a) 和 int a = 5 沒有什
么區(qū)別,等號(hào)左邊就是左值,右邊就是右值。
最后,從上述分析中我們得到如下結(jié)論:
- 從性能上講,左右值引用沒有區(qū)別,傳參使用左右值引用都可以避免拷貝。
- 右值引用可以直接指向右值,也可以通過std::move指向左值;而左值引用只能指向左值(const左
值引用也能指向右值)。 - 作為函數(shù)形參時(shí),右值引用更靈活。雖然const左值引用也可以做到左右值都接受,但它無法修
改,有一定局限性。
void f(const int& n) {
n += 1; // 編譯失敗,const左值引用不能修改指向變量
}
void f2(int && n) {
n += 1; // ok
}
int main() {
f(5);
f2(5);
}
3 右值引用和std::move使用場景
std::move只是類型轉(zhuǎn)換工具,不會(huì)對(duì)性能有好處;
右值引用在作為函數(shù)形參時(shí)更具靈活性。他們有什么實(shí)際應(yīng)用場景嗎?
3.1 右值引用優(yōu)化性能,避免深拷貝
淺拷貝重復(fù)釋放
對(duì)于含有堆內(nèi)存的類,我們需要提供深拷貝的拷貝構(gòu)造函數(shù),如果使用默認(rèn)構(gòu)造函數(shù),會(huì)導(dǎo)致堆內(nèi)存的
重復(fù)刪除,比如下面的代碼:
#include <iostream>
using namespace std;
class A
{
public:
A() :m_ptr(new int(0)) {
cout << "constructor A" << endl;
}
~A(){
cout << "destructor A, m_ptr:" << m_ptr << endl;
delete m_ptr;
m_ptr = nullptr;
}
private:
int* m_ptr;
};
// 為了避免返回值優(yōu)化,此函數(shù)故意這樣寫
A Get(bool flag)
{
A a;
A b;
cout << "ready return" << endl;
if (flag)
return a;
else
return b;
}
int main()
{
{
A a = Get(false); // 運(yùn)行報(bào)錯(cuò)
}
cout << "main finish" << endl;
return 0;
}
打印
constructor A
constructor A
ready return
destructor A, m_ptr:000002628A9323D0
destructor A, m_ptr:000002628A92A0B0
destructor A, m_ptr:000002628A9323D0
深拷貝構(gòu)造函數(shù)
在上面的代碼中,默認(rèn)構(gòu)造函數(shù)是淺拷貝,main函數(shù)的 a 和Get函數(shù)的 b 會(huì)指向同一個(gè)指針 m_ptr,在
析構(gòu)的時(shí)候會(huì)導(dǎo)致重復(fù)刪除該指針。正確的做法是提供深拷貝的拷貝構(gòu)造函數(shù),比如下面的代碼:
#include <iostream>
using namespace std;
class A
{
public:
A() :m_ptr(new int(0)) {
cout << "constructor A" << endl;
}
A(const A& a) :m_ptr(new int(*a.m_ptr)) {
cout << "copy constructor A" << endl;
}
~A(){
cout << "destructor A, m_ptr:" << m_ptr << endl;
delete m_ptr;
m_ptr = nullptr;
}
private:
int* m_ptr;
};
// 為了避免返回值優(yōu)化,此函數(shù)故意這樣寫
A Get(bool flag)
{
A a;
A b;
cout << "ready return" << endl;
if (flag)
return a;
else
return b;
}
int main()
{
{
A a = Get(false); // 正確運(yùn)行
}
cout << "main finish" << endl;
return 0;
}
constructor A
constructor A
ready return
copy constructor A
destructor A, m_ptr:000002455ECEA0B0
destructor A, m_ptr:000002455ECEA070
destructor A, m_ptr:000002455ECEA0F0
main finish
移動(dòng)構(gòu)造函數(shù)
這樣就可以保證拷貝構(gòu)造時(shí)的安全性,但有時(shí)這種拷貝構(gòu)造卻是不必要的,比如上面代碼中的拷貝構(gòu)造
就是不必要的。上面代碼中的 Get 函數(shù)會(huì)返回臨時(shí)變量,然后通過這個(gè)臨時(shí)變量拷貝構(gòu)造了一個(gè)新的對(duì)
象 b,臨時(shí)變量在拷貝構(gòu)造完成之后就銷毀了,如果堆內(nèi)存很大,那么,這個(gè)拷貝構(gòu)造的代價(jià)會(huì)很大,
帶來了額外的性能損耗。有沒有辦法避免臨時(shí)對(duì)象的拷貝構(gòu)造呢?答案是肯定的。看下面的代碼:
#include <iostream>
using namespace std;
class A
{
public:
A() :m_ptr(new int(0)) {
cout << "constructor A" << endl;
}
A(const A& a) :m_ptr(new int(*a.m_ptr)) {
cout << "copy constructor A" << endl;
}
// 移動(dòng)構(gòu)造函數(shù),可以淺拷貝
A(A&& a) :m_ptr(a.m_ptr) {
a.m_ptr = nullptr; // 為防止a析構(gòu)時(shí)delete data,提前置空其m_ptr
cout << "move constructor A" << endl;
}
~A(){
cout << "destructor A, m_ptr:" << m_ptr << endl;
if(m_ptr)
delete m_ptr;
}
private:
int* m_ptr;
};
// 為了避免返回值優(yōu)化,此函數(shù)故意這樣寫
A Get(bool flag)
{
A a;
A b;
cout << "ready return" << endl;
if (flag)
return a;
else
return b;
}
int main()
{
{
A a = Get(false); // 正確運(yùn)行
}
cout << "main finish" << endl;
return 0;
}
constructor A
constructor A
ready return
move constructor A
destructor A, m_ptr:0000000000000000
destructor A, m_ptr:000002640DF0A030
destructor A, m_ptr:000002640DF0A070
main finish
上面的代碼中沒有了拷貝構(gòu)造,取而代之的是移動(dòng)構(gòu)造( Move Construct)。從移動(dòng)構(gòu)造函數(shù)的實(shí)現(xiàn)
中可以看到,它的參數(shù)是一個(gè)右值引用類型的參數(shù) A&&,這里沒有深拷貝,只有淺拷貝,這樣就避免了
對(duì)臨時(shí)對(duì)象的深拷貝,提高了性能。這里的 A&& 用來根據(jù)參數(shù)是左值還是右值來建立分支,如果是臨時(shí)
值,則會(huì)選擇移動(dòng)構(gòu)造函數(shù)。移動(dòng)構(gòu)造函數(shù)只是將臨時(shí)對(duì)象的資源做了淺拷貝,不需要對(duì)其進(jìn)行深拷
貝,從而避免了額外的拷貝,提高性能。這也就是所謂的移動(dòng)語義( move 語義),右值引用的一個(gè)重
要目的是用來支持移動(dòng)語義的。
移動(dòng)語義可以將資源(堆、系統(tǒng)對(duì)象等)通過淺拷貝方式從一個(gè)對(duì)象轉(zhuǎn)移到另一個(gè)對(duì)象,這樣能夠減少
不必要的臨時(shí)對(duì)象的創(chuàng)建、拷貝以及銷毀,可以大幅度提高 C++ 應(yīng)用程序的性能,消除臨時(shí)對(duì)象的維護(hù)
(創(chuàng)建和銷毀)對(duì)性能的影響。
3.2 移動(dòng)(move )語義
move是將對(duì)象的狀態(tài)或者所有權(quán)從一個(gè)對(duì)象轉(zhuǎn)移到另一個(gè)對(duì)象,只是轉(zhuǎn)義,沒有內(nèi)存拷貝。要move語
義起作用,核心在于需要對(duì)應(yīng)類型的構(gòu)造函數(shù)支持。

//2-3-2-move
#include <iostream>
#include <vector>
#include <cstdio>
#include <cstdlib>
#include <string.h>
using namespace std;
class MyString {
private:
char* m_data;
size_t m_len;
void copy_data(const char *s) {
m_data = new char[m_len+1];
memcpy(m_data, s, m_len);
m_data[m_len] = '\0';
}
public:
MyString() {
m_data = NULL;
m_len = 0;
}
MyString(const char* p) {
m_len = strlen (p);
copy_data(p);
}
MyString(const MyString& str) {
m_len = str.m_len;
copy_data(str.m_data);
std::cout << "Copy Constructor is called! source: " << str.m_data << std::endl;
}
MyString& operator=(const MyString& str) {
if (this != &str) {
m_len = str.m_len;
copy_data(str.m_data);
}
std::cout << "Copy Assignment is called! source: " << str.m_data << std::endl;
return *this;
}
// 用c++11的右值引用來定義這兩個(gè)函數(shù)
MyString(MyString&& str) {
std::cout << "Move Constructor is called! source: " << str.m_data << std::endl;
m_len = str.m_len;
m_data = str.m_data; //避免了不必要的拷貝
str.m_len = 0;
str.m_data = NULL;
}
MyString& operator=(MyString&& str) {
std::cout << "Move Assignment is called! source: " << str.m_data << std::endl;
if (this != &str) {
m_len = str.m_len;
m_data = str.m_data; //避免了不必要的拷貝
str.m_len = 0;
str.m_data = NULL;
}
return *this;
}
virtual ~MyString() {
if (m_data) free(m_data);
}
};
int main()
{
MyString a;
a = MyString("Hello"); // Move Assignment
MyString b = a; // Copy Constructor
MyString c = std::move(a); // Move Constructor is called! 將左值轉(zhuǎn)為右值
std::vector<MyString> vec;
vec.push_back(MyString("World")); // Move Constructor is called!
return 0;
}
有了右值引用和轉(zhuǎn)移語義,我們?cè)谠O(shè)計(jì)和實(shí)現(xiàn)類時(shí),對(duì)于需要?jiǎng)討B(tài)申請(qǐng)大量資源的類,應(yīng)該設(shè)計(jì)右值引
用的拷貝構(gòu)造函數(shù)和賦值函數(shù),以提高應(yīng)用程序的效率
3.3 forward 完美轉(zhuǎn)發(fā)
forward 完美轉(zhuǎn)發(fā)實(shí)現(xiàn)了參數(shù)在傳遞過程中保持其值屬性的功能,即若是左值,則傳遞之后仍然是左
值,若是右值,則傳遞之后仍然是右值。
現(xiàn)存在一個(gè)函數(shù)
Template<class T>
void func(T &&val);
根據(jù)前面所描述的,這種引用類型既可以對(duì)左值引用,亦可以對(duì)右值引用。
但要注意,引用以后,這個(gè)val值它本質(zhì)上是一個(gè)左值!
看下面例子
int &&a = 10;
int &&b = a; //錯(cuò)誤
注意這里,a是一個(gè)右值引用,但其本身a也有內(nèi)存名字,所以a本身是一個(gè)左值,再用右值引用引用a這
是不對(duì)的。
因此我們有了std::forward()完美轉(zhuǎn)發(fā),這種T &&val中的val是左值,但如果我們用std::forward (val),
就會(huì)按照參數(shù)原來的類型轉(zhuǎn)發(fā);
int &&a = 10;
int &&b = std::forward<int>(a);
這樣是正確的!
通過范例鞏固下知識(shí):
//2-3-3-forward1
#include <iostream>
using namespace std;
template <class T>
void Print(T &t)
{
cout << "L" << t << endl;
}
template <class T>
void Print(T &&t)
{
cout << "R" << t << endl;
}
template <class T>
void func(T &&t)
{
Print(t);
Print(std::move(t));
Print(std::forward<T>(t));
}
int main()
{
cout << "-- func(1)" << endl;
func(1);
int x = 10;
int y = 20;
cout << "-- func(x)" << endl;
func(x); // x本身是左值
cout << "-- func(std::forward<int>(y))" << endl;
func(std::forward<int>(y)); //
return 0;
}
-- func(1)
L1
R1
R1
-- func(x)
L10
R10
L10
-- func(std::forward
L20
R20
R20
解釋:
func(1) :由于1是右值,所以未定的引用類型T&&v被一個(gè)右值初始化后變成了一個(gè)右值引用,但是在
func()函數(shù)體內(nèi)部,調(diào)用PrintT(v) 時(shí),v又變成了一個(gè)左值(因?yàn)樵趕td::forward里它已經(jīng)變成了一個(gè)具
名的變量,所以它是一個(gè)左值),因此,示例測試結(jié)果第一個(gè)PrintT被調(diào)用,打印出“L1"
調(diào)用PrintT(std::forward(v))時(shí),由于std::forward會(huì)按參數(shù)原來的類型轉(zhuǎn)發(fā),因此,它還是一個(gè)右值
(這里已經(jīng)發(fā)生了類型推導(dǎo),所以這里的T&&不是一個(gè)未定的引用類型,會(huì)調(diào)用void PrintT(T&&t)函
數(shù)打印 “R1”.調(diào)用PrintT(std::move(v))是將v變成一個(gè)右值(v本身也是右值),因此,它將輸出”R1"
func(x)未定的引用類型T&&v被一個(gè)左值初始化后變成了一個(gè)左值引用,因此,在調(diào)用
PrintT(std::forward(v))時(shí)它會(huì)被轉(zhuǎn)發(fā)到void PrintT(T&t).
forward將左值轉(zhuǎn)換為右值:
MyString str1 = "hello";
MyString str2(str1);
MyString str3 = Fun();
MyString str4 = move(str2);
MyString str5(forward<MyString>(str3));
綜合示例
//2-3-3-forward2
#include "stdio.h"
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
class A
{
public:
A() : m_ptr(NULL), m_nSize(0) {}
A(int *ptr, int nSize)
{
m_nSize = nSize;
m_ptr = new int[nSize];
printf("A(int *ptr, int nSize) m_ptr:%p\n", m_ptr);
if (m_ptr)
{
memcpy(m_ptr, ptr, sizeof(sizeof(int) * nSize));
}
}
A(const A &other) // 拷貝構(gòu)造函數(shù)實(shí)現(xiàn)深拷貝
{
m_nSize = other.m_nSize;
if (other.m_ptr)
{
printf("A(const A &other) m_ptr:%p\n", m_ptr);
if(m_ptr)
delete[] m_ptr;
printf("delete[] m_ptr\n");
m_ptr = new int[m_nSize];
memcpy(m_ptr, other.m_ptr, sizeof(sizeof(int) * m_nSize));
}
else
{
if(m_ptr)
delete[] m_ptr;
m_ptr = NULL;
}
cout << "A(const int &i)" << endl;
}
// 右值引用移動(dòng)構(gòu)造函數(shù)
A(A &&other)
{
m_ptr = NULL;
m_nSize = other.m_nSize;
if (other.m_ptr)
{
m_ptr = move(other.m_ptr); // 移動(dòng)語義
other.m_ptr = NULL;
}
}
~A()
{
if (m_ptr)
{
delete[] m_ptr;
m_ptr = NULL;
}
}
void deleteptr()
{
if (m_ptr)
{
delete[] m_ptr;
m_ptr = NULL;
}
}
int *m_ptr = NULL; // 增加初始化
int m_nSize = 0;
};
int main()
{
int arr[] = {1, 2, 3};
A a(arr, sizeof(arr) / sizeof(arr[0]));
cout << "m_ptr in a Addr: 0x" << a.m_ptr << endl;
A b(a);
cout << "m_ptr in b Addr: 0x" << b.m_ptr << endl;
b.deleteptr();
A c(std::forward<A>(a)); // 完美轉(zhuǎn)換
cout << "m_ptr in c Addr: 0x" << c.m_ptr << endl;
c.deleteptr();
vector<int> vect{1, 2, 3, 4, 5};
cout << "before move vect size: " << vect.size() << endl;
vector<int> vect1 = move(vect);
cout << "after move vect size: " << vect.size() << endl;
cout << "new vect1 size: " << vect1.size() << endl;
return 0;
}
3.4 emplace_back 減少內(nèi)存拷貝和移動(dòng)
對(duì)于STL容器,C++11后引入了emplace_back接口。
emplace_back是就地構(gòu)造,不用構(gòu)造后再次復(fù)制到容器中。因此效率更高。
考慮這樣的語句:
vector<string> testVec;
testVec.push_back(string(16, 'a'));
上述語句足夠簡單易懂,將一個(gè)string對(duì)象添加到testVec中。底層實(shí)現(xiàn):
- 首先,string(16, ‘a(chǎn)’)會(huì)創(chuàng)建一個(gè)string類型的臨時(shí)對(duì)象,這涉及到一次string構(gòu)造過程。
- 其次,vector內(nèi)會(huì)創(chuàng)建一個(gè)新的string對(duì)象,這是第二次構(gòu)造。
- 最后在push_back結(jié)束時(shí),最開始的臨時(shí)對(duì)象會(huì)被析構(gòu)。加在一起,這兩行代碼會(huì)涉及到兩次
string構(gòu)造和一次析構(gòu)。
c++11可以用emplace_back代替push_back,emplace_back可以直接在vector中構(gòu)建一個(gè)對(duì)象,而非
創(chuàng)建一個(gè)臨時(shí)對(duì)象,再放進(jìn)vector,再銷毀。emplace_back可以省略一次構(gòu)建和一次析構(gòu),從而達(dá)到優(yōu)
化的目的。
//2-5-emplace_back
#include <vector>
#include <string>
#include "time_interval.h"
int main() {
std::vector<std::string> v;
int count = 10000000;
v.reserve(count); //預(yù)分配十萬大小,排除掉分配內(nèi)存的時(shí)間
{
TIME_INTERVAL_SCOPE("push_back string:");
for (int i = 0; i < count; i++)
{
std::string temp("ceshi");
v.push_back(temp);// push_back(const string&),參數(shù)是左值引用
}
}
v.clear();
{
TIME_INTERVAL_SCOPE("push_back move(string):");
for (int i = 0; i < count; i++)
{
std::string temp("ceshi");
v.push_back(std::move(temp));// push_back(string &&), 參數(shù)是右值引用
}
}
v.clear();
{
TIME_INTERVAL_SCOPE("push_back(string):");
for (int i = 0; i < count; i++)
{
v.push_back(std::string("ceshi"));// push_back(string &&), 參數(shù)是右值引用
}
}
v.clear();
{
TIME_INTERVAL_SCOPE("push_back(c string):");
for (int i = 0; i < count; i++)
{
v.push_back("ceshi");// push_back(string &&), 參數(shù)是右值引用
}
}
v.clear();
{
TIME_INTERVAL_SCOPE("emplace_back(c string):");
for (int i = 0; i < count; i++)
{
v.emplace_back("ceshi");// 只有一次構(gòu)造函數(shù),不調(diào)用拷貝構(gòu)造函數(shù),速度最快
}
}
}
結(jié)果
push_back string:9439 ms
push_back move(string):8566 ms
push_back(string):7990 ms
push_back(c string):8427 ms
emplace_back(c string):3581 ms
3.5 小結(jié)
C++11 在性能上做了很大的改進(jìn),最大程度減少了內(nèi)存移動(dòng)和復(fù)制,通過右值引用、 forward、
emplace 和一些無序容器我們可以大幅度改進(jìn)程序性能。
- 右值引用僅僅是通過改變資源的所有者來避免內(nèi)存的拷貝,能大幅度提高性能。
- forward 能根據(jù)參數(shù)的實(shí)際類型轉(zhuǎn)發(fā)給正確的函數(shù)。
- emplace 系列函數(shù)通過直接構(gòu)造對(duì)象的方式避免了內(nèi)存的拷貝和移動(dòng)。
四 匿名函數(shù)lambda
重點(diǎn):
- 怎么傳遞參數(shù)
- 傳引用還是傳值
4.1匿名函數(shù)的基本語法
[捕獲列表](參數(shù)列表) mutable(可選) 異常屬性 -> 返回類型 {
// 函數(shù)體
}
語法規(guī)則:lambda表達(dá)式可以看成是一般函數(shù)的函數(shù)名被略去,返回值使用了一個(gè) -> 的形式表示。唯
一與普通函數(shù)不同的是增加了“捕獲列表”。
//[捕獲列表](參數(shù)列表)->返回類型{函數(shù)體}
int main()
{
auto Add = [](int a, int b)->int {
return a + b;
};
std::cout << Add(1, 2) << std::endl; //輸出3
return 0;
}
一般情況下,編譯器可以自動(dòng)推斷出lambda表達(dá)式的返回類型,所以我們可以不指定返回類型,即:
//[捕獲列表](參數(shù)列表){函數(shù)體}
int main()
{
auto Add = [](int a, int b) {
return a + b;
};
std::cout << Add(1, 2) << std::endl; //輸出3
return 0;
}
但是如果函數(shù)體內(nèi)有多個(gè)return語句時(shí),編譯器無法自動(dòng)推斷出返回類型,此時(shí)必須指定返回類型。
//[捕獲列表](參數(shù)列表){函數(shù)體}
int main()
{
auto Add = [](int a, int b) {
return a + b;
};
std::cout << Add(1, 2) << std::endl; //輸出3
return 0;
}
但是如果函數(shù)體內(nèi)有多個(gè)return語句時(shí),編譯器無法自動(dòng)推斷出返回類型,此時(shí)必須指定返回類型。
4.2 捕獲列表
有時(shí)候,需要在匿名函數(shù)內(nèi)使用外部變量,所以用捕獲列表來傳遞參數(shù)。根據(jù)傳遞參數(shù)的行為,捕獲列
表可分為以下幾種:
1 值捕獲
與參數(shù)傳值類似,值捕獲的前提是變量可以拷貝,不同之處則在于,被捕獲的變量在 lambda表達(dá)式被
創(chuàng)建時(shí)拷貝,而非調(diào)用時(shí)才拷貝:
void test3()
{
cout << "test3" << endl;
int c = 12;
int d = 30;
auto Add = [c, d](int a, int b)->int {
cout << "d = " << d << endl;
return c;
};
d = 20;
std::cout << Add(1, 2) << std::endl;
}
2 引用捕獲
與引用傳參類似,引用捕獲保存的是引用,值會(huì)發(fā)生變化。
如果Add中加入一句:c = a;
void test5()
{
cout << "test5" << endl;
int c = 12;
int d = 30;
auto Add = [&c, &d](int a, int b)->int {
c = a; // 編譯對(duì)的
cout << "d = " << d << endl;
return c;
};
d = 20;
std::cout << Add(1, 2) << std::endl;
}
3 隱式捕獲
手動(dòng)書寫捕獲列表有時(shí)候是非常復(fù)雜的,這種機(jī)械性的工作可以交給編譯器來處理,這時(shí)候可以在捕獲
列表中寫一個(gè) & 或 = 向編譯器聲明采用引用捕獲或者值捕獲。
void test7()
{
cout << "test7" << endl;
int c = 12;
int d = 30;
// 把捕獲列表的&改成=再測試
auto Add = [&](int a, int b)->int {
c = a; // 編譯對(duì)的
cout << "d = " << d << endl;
return c;
};
d = 20;
std::cout << Add(1, 2) << std::endl;
std::cout << "c:" << c<< std::endl;
}
4 空捕獲列表
捕獲列表'[]'中為空,表示Lambda不能使用所在函數(shù)中的變量。
void test8()
{
cout << "test7" << endl;
int c = 12;
int d = 30;
// 把捕獲列表的&改成=再測試
auto Add = [](int a, int b)->int {
cout << "d = " << d << endl; // 編譯報(bào)錯(cuò)
return c;// 編譯報(bào)錯(cuò)
};
d = 20;
std::cout << Add(1, 2) << std::endl;
std::cout << "c:" << c<< std::endl;
}
5 表達(dá)式捕獲
上面提到的值捕獲、引用捕獲都是已經(jīng)在外層作用域聲明的變量,因此這些捕獲方式捕獲的均為左值,
而不能捕獲右值。
C++14之后支持捕獲右值,允許捕獲的成員用任意的表達(dá)式進(jìn)行初始化,被聲明的捕獲變量類型會(huì)根據(jù)
表達(dá)式進(jìn)行判斷,判斷方式與使用 auto 本質(zhì)上是相同的:
void test9()
{
cout << "test9" << endl;
auto important = std::make_unique<int>(1);
auto add = [v1 = 1, v2 = std::move(important)](int x, int y) -> int {
return x + y + v1 + (*v2);
};
std::cout << add(3,4) << std::endl;
}
6 泛型 Lambda
在C++14之前,lambda表示的形參只能指定具體的類型,沒法泛型化。從 C++14 開始, Lambda 函數(shù)
的形式參數(shù)可以使用 auto關(guān)鍵字來產(chǎn)生意義上的泛型:
//泛型 Lambda C++14
void test10()
{
cout << "test10" << endl;
auto add = [](auto x, auto y) {
return x+y;
};
std::cout << add(1, 2) << std::endl;
std::cout << add(1.1, 1.2) << std::endl;
}
7 可變lambda
- 采用值捕獲的方式,lambda不能修改其值,如果想要修改,使用mutable修飾
- 采用引用捕獲的方式,lambda可以直接修改其值
void test12() {
cout << "test12" << endl;
int v = 5;
// 值捕獲方式,使用mutable修飾,可以改變捕獲的變量值
auto ff = [v]() mutable {return ++v;};
v = 0;
auto j = ff(); // j為6
}
void test13() {
cout << "test13" << endl;
int v = 5;
// 采用引用捕獲方式,可以直接修改變量值
auto ff = [&v] {return ++v;};
v = 0;
auto j = ff(); // v引用已修改,j為1
}
總結(jié)
- 如果捕獲列表為[&],則表示所有的外部變量都按引用傳遞給lambda使用;
- 如果捕獲列表為[=],則表示所有的外部變量都按值傳遞給lambda使用;
- 匿名函數(shù)構(gòu)建的時(shí)候?qū)τ诎粗祩鬟f的捕獲列表,會(huì)立即將當(dāng)前可以取到的值拷貝一份作為常數(shù),然
后將該常數(shù)作為參數(shù)傳遞。
Lambda捕獲列表總結(jié)
| [] | 空捕獲列表,Lambda不能使用所在函數(shù)中的變量。 |
|---|---|
| [names] | names是一個(gè)逗號(hào)分隔的名字列表,這些名字都是Lambda所在函數(shù)的局部變量。默認(rèn)情況這些變量會(huì)被拷貝,然后按值傳遞,名字前面如果使用了&,則按引用傳遞 |
| [&] | 隱式捕獲列表,Lambda體內(nèi)使用的局部變量都按引用方式傳遞 |
| [=] | 隱式捕獲列表,Lanbda體內(nèi)使用的局部變量都按值傳遞 |
| [&,identifier_list] | identifier_list是一個(gè)逗號(hào)分隔的列表,包含0個(gè)或多個(gè)來自所在函數(shù)的變量,這些變量采用值捕獲的方式,其他變量則被隱式捕獲,采用引用方式傳遞,identifier_list中的名字前面不能使用&。 |
| [=,identifier_list] | identifier_list中的變量采用引用方式捕獲,而被隱式捕獲的變量都采用按值傳遞的方式捕獲。identifier_list中的名字不能包含this,且這些名字面前必須使用&。 |
五 C++11標(biāo)準(zhǔn)庫(STL)
STL定義了強(qiáng)大的、基于模板的、可復(fù)用的組件,實(shí)現(xiàn)了許多通用的數(shù)據(jù)結(jié)構(gòu)及處理這些數(shù)據(jù)結(jié)構(gòu)的算
法。其中包含三個(gè)關(guān)鍵組件——容器(container,流行的模板數(shù)據(jù)結(jié)構(gòu))、迭代器(iterator)和算法
(algorithm)。
| 組件 | 描述 |
|---|---|
| 容器 | 容器是用來管理某一類對(duì)象的集合。C++ 提供了各種不同類型的容器,比如 deque、list、vector、map 等。 |
| 迭代器 | 迭代器用于遍歷對(duì)象集合的元素。這些集合可能是容器,也可能是容器的子集。 |
| 算法 | 算法作用于容器。它們提供了執(zhí)行各種操作的方式,包括對(duì)容器內(nèi)容執(zhí)行初始化、排序、搜索和轉(zhuǎn)換等操作。 |
5.1 容器簡介
STL容器,可將其分為四類:序列容器、有序關(guān)聯(lián)容器、無序關(guān)聯(lián)容器、容器適配器、序列容器:
| 標(biāo)準(zhǔn)庫容器類 | 描述 |
|---|---|
| array | 固定大小,直接訪問任意元素 |
| deque | 從前部或后部進(jìn)行快速插入和刪除操作,直接訪問任何元素 |
| forward_list | 單鏈表,在任意位置快速插入和刪除 |
| list | 雙向鏈表,在任意位置進(jìn)行快速插入和刪除操作 |
| vector | 從后部進(jìn)行快速插入和刪除操作,直接訪問任意元素 |
有序關(guān)聯(lián)容器(鍵按順序保存):
| 標(biāo)準(zhǔn)庫容器類 | 描述 |
|---|---|
| set | 快速查找,無重復(fù)元素 |
| multiset | 快速查找,可有重復(fù)元素 |
| map | 一對(duì)一映射,無重復(fù)元素,基于鍵快速查找 |
| multimap | 一對(duì)一映射,可有重復(fù)元素,基于鍵快速查找 |
無序關(guān)聯(lián)容器:
| 標(biāo)準(zhǔn)庫容器類 | 描述 |
|---|---|
| unordered_set | 快速查找,無重復(fù)元素 |
| unordered_multiset | 快速查找,可有重復(fù)元素 |
| unordered_map | 一對(duì)一映射,無重復(fù)元素,基于鍵快速查找 |
| unordered_multimap | 一對(duì)一映射,可有重復(fù)元素,基于鍵快速查找 |
容器適配器:
| 標(biāo)準(zhǔn)庫容器類 | 描述 |
|---|---|
| stack | 后進(jìn)先出(LIFO) |
| queue | 先進(jìn)先出(FIFO) |
| priority_queue | 優(yōu)先級(jí)最高的元素先出 |
序列容器描述了線性的數(shù)據(jù)結(jié)構(gòu)(也就是說,其中的元素在概念上” 排成一行"), 例如數(shù)組、向量和 鏈
表。
關(guān)聯(lián)容器描述非線性的容器,它們通常可以快速鎖定其中的元素。這種容器可以存儲(chǔ)值的集合或者鍵-
值對(duì)
棧和隊(duì)列都是在序列容器的基礎(chǔ)上加以約束條件得到的,因此STL把stack和queue作為容器適配器來實(shí)
現(xiàn),這樣就可以使程序以一種約束方式來處理線性容器。類型string支持的功能跟線性容器一樣, 但是
它只能存儲(chǔ)字符數(shù)據(jù).
5.2 迭代器簡介
迭代器在很多方面與指針類似,也是用于指向首類容器中的元素(還有一些其他用途,后面將會(huì)提
到)。 迭代器存有它們所指的特定容器的狀態(tài)信息,即迭代器對(duì)每種類型的容器都有一個(gè)實(shí)現(xiàn)。 有些迭
代器的操作在不同容器間是統(tǒng)一的。 例如,*運(yùn)算符間接引用一個(gè)迭代器,這樣就可以使用它所指向的
元素。++運(yùn)算符使得迭代器指向容器中的下一個(gè)元素(和數(shù)組中指針遞增后指向數(shù)組的下一個(gè)元素類
似)。
STL 首類容器提供了成員函數(shù) begin 和 end。函數(shù) begin 返回一個(gè)指向容器中第一個(gè)元素的迭代器,函
數(shù) end 返回一個(gè)指向容器中最后一個(gè)元素的下一個(gè)元素(這個(gè)元素并不存在,常用于判斷是否到達(dá)了容
器的結(jié)束位僅)的迭代器。 如果迭代器 i 指向一個(gè)特定的元素,那么 ++i 指向這個(gè)元素的下一個(gè)元素。*
i 指代的是i指向的元素。 從函數(shù) end 中返回的迭代器只在相等或不等的比較中使用,來判斷這個(gè)“移動(dòng)
的迭代器” (在這里指i)是否到達(dá)了容器的末端。
使用一個(gè) iterator 對(duì)象來指向一個(gè)可以修改的容器元素,使用一個(gè) const_iterator 對(duì)象來指向一個(gè)不能
修改的容器元素。
| 類型 | 描述 |
|---|---|
| 隨機(jī)訪問迭代器(randomaccess) | 在雙向迭代湍基礎(chǔ)上增加了直接訪問容器中任意元素的功能, 即可以向前或 |
| 向后跳轉(zhuǎn)任意個(gè)元素 | |
| 雙向迭代器(bidirectional) | 在前向迭代器基礎(chǔ)上增加了向后移動(dòng)的功能。支持多遍掃描算法 |
| 前向迭代器(forword) | 綜合輸入和輸出迭代器的功能,并能保持它們?cè)谌萜髦械奈恢茫ㄗ鳛闋顟B(tài)信息),可以使用同一個(gè)迭代器兩次遍歷一個(gè)容器(稱為多遍掃描算法) |
| 輸出迭代器(output) | 用于將元素寫入容器。 輸出迭代楛每次只能向前移動(dòng)一個(gè)元索。 輸出迭代器只支持一遍掃描算法,不能使用相同的輸出迭代器兩次遍歷一個(gè)序列容器 |
| 輸入迭代器(input) | 用于從容器讀取元素。 輸入迭代器每次只能向前移動(dòng)一個(gè)元素。 輸入迭代器只支持一遍掃描算法,不能使用相同的輸入迭代器兩次遍歷一個(gè)序列容器 |
每種容器所支持的迭代器類型決定了這種容器是否可以在指定的 STL 算 法中使用。 支持隨機(jī)訪問迭代
器的容器可用于所有的 STL 算法(除了那些需要改變?nèi)萜鞔笮〉乃惴ǎ@樣的算法不能在數(shù)組和 array
對(duì)象中使用)。 指向 數(shù)組的指針可以代替迭代器用于幾乎所有的 STL 算法中,包括那些要求隨機(jī)訪問
迭代器的算法。 下表顯示了每種 STL 容器所支持的迭代器類型。 注意, vector 、 deque 、 list 、 set、 multiset 、 map 、 multimap以及 string 和數(shù)組都可以使用迭代器遍歷.
| 容器 | 支持的迭代器類型 | 容器 | 支持的迭代器類型 |
|---|---|---|---|
| vector | 隨機(jī)訪問迭代器 | set | 雙向迭代器 |
| array | 隨機(jī)訪問迭代器 | multiset | 雙向迭代器 |
| deque | 隨機(jī)訪問迭代器 | map | 雙向迭代器 |
| list | 雙向迭代器 | multimap | 雙向迭代器 |
| forword_list | 前向迭代器 | unordered_set | 雙向迭代器 |
| stack | 不支持迭代器 | unordered_multiset | 雙向迭代器 |
| queue | 不支持迭代器 | unordered_map | 雙向迭代器 |
| priority_queue | 不支持迭代器 | unordered_multimap | 雙向迭代器 |
下表顯示了在 STL容器的類定義中出現(xiàn)的幾種預(yù)定義的迭代器 typedef。不是每種 typedef 都出現(xiàn)在每
個(gè)容器中。 我們使用常量版本的迭代器來訪問只讀容器或不應(yīng)該被更改的非只讀容器,使用反向迭代器
來以相反的方向訪問容器。
| 為迭代器預(yù)先定義的typedef | ++的方向 | 讀寫能力 |
|---|---|---|
| iterator | 向前 | 讀/寫 |
| const_iterator | 向前 | 讀 |
| reverse_iterator | 向后 | 讀/寫 |
| const_reverse_iterator | 向后 | 讀 |
下表顯示了可作用在每種迭代器上的操作。 除了給出的對(duì)于所有迭代器都有的運(yùn)算符,迭代器還必須提
供默認(rèn)構(gòu)造函數(shù)、拷貝構(gòu)造函數(shù)和拷貝賦值操作符。 前向迭代器支持++ 和所有的輸入和輸出迭代器的
功能。 雙向迭代器支持–操作和前向迭代器的功能。 隨機(jī)訪問迭代器支持所有在表中給出的操作。 另
外, 對(duì)于輸入迭代器和輸出迭代器,不能在保存迭代器之后再使用保存的值
| 迭代器操作 | 描述 |
|---|---|
| 適用所有迭代器的操作 | |
| ++p | 前置自增迭代器 |
| p++ | 后置自增迭代器 |
| p=p1 | 將一個(gè)迭代器賦值給另一個(gè)迭代器 |
| 輸入迭代器 | |
| *p | 間接引用一個(gè)迭代器 |
| p->m | 使用迭代器讀取元素m |
| p==p1 | 比較兩個(gè)迭代器是否相等 |
| p!=p1 | 比較兩個(gè)迭代器是否不相等 |
| 輸出迭代器 | |
| *p | 間接引用一個(gè)迭代器 |
| p=p1 | 把一個(gè)迭代器賦值給另一個(gè) |
| 前向迭代器 | 前向迭代器提供了輸入和輸出迭代器的所有功能 |
| 雙向迭代器 | |
| –p | q |
| p– | 后置自減迭代器 |
| 隨機(jī)訪問迭代器 | |
| p+=i | 迭代器p前進(jìn)i個(gè)位置 |
| p-=i | 迭代器p后退i個(gè)位置 |
| p+i | 在迭代器p 的位置上前進(jìn)i個(gè)位置 |
| p-i | 在迭代器p的位置上后退i個(gè)位置 |
| p-p1 | 表達(dá)式的值是一個(gè)整數(shù),它代表同一個(gè)容器中兩個(gè)元素間的距離 |
| p[i] | 返回與迭代器p的位置相距i的元素 |
| p<p1 | 若迭代器p小于p1(即容器中p在p1前)則返回 true, 否則返回 false |
| p<=p1 | 若迭代器p小千或等于p1 (即容器中p 在p1前或位咒相同)則返回 true, 否則返回 false |
| p>p1 | 若迭代器p 大于p1(即容器中p在p1后)則返回true, 否則返回false |
| p>=p1 | 若迭代器p大于或等于p1(即容楛中p在p1后或位置相同)則返回 true, 否則返回 false |
5.3 map與unordered_map(紅黑樹VS哈希表)
C++11 增加了無序容器 unordered_map/unordered_multimap 和
unordered_set/unordered_multiset,由于這些容器中的元素是不排序的,因此,比有序容器
map/multimap 和 set/multiset 效率更高。 map 和 set 內(nèi)部是紅黑樹,在插入元素時(shí)會(huì)自動(dòng)排序,而
無序容器內(nèi)部是散列表( Hash Table),通過哈希( Hash),而不是排序來快速操作元素,使得效率
更高。由于無序容器內(nèi)部是散列表,因此無序容器的 key 需要提供 hash_value 函數(shù),其他用法和
map/set 的用法是一樣的。不過對(duì)于自定義的 key,需要提供 Hash 函數(shù)和比較函數(shù)。
5.3.1 map和unordered_map的差別
需要引入的頭文件不同
map: #include < map >
unordered_map: #include < unordered_map >
內(nèi)部實(shí)現(xiàn)機(jī)理不同
- map: map內(nèi)部實(shí)現(xiàn)了一個(gè)紅黑樹(紅黑樹是非嚴(yán)格平衡二叉搜索樹,而AVL是嚴(yán)格平衡二叉搜
索樹),紅黑樹具有自動(dòng)排序的功能,因此map內(nèi)部的所有元素都是有序的,紅黑樹的每一個(gè)節(jié)點(diǎn)
都代表著map的一個(gè)元素。 - unordered_map: unordered_map內(nèi)部實(shí)現(xiàn)了一個(gè)哈希表(也叫散列表,通過把關(guān)鍵碼值映射到
Hash表中一個(gè)位置來訪問記錄,查找的時(shí)間復(fù)雜度可達(dá)到O(1),其在海量數(shù)據(jù)處理中有著廣泛應(yīng)
用)。因此,其元素的排列順序是無序的。
5.3.2 優(yōu)缺點(diǎn)以及適用處
map:
- 優(yōu)點(diǎn):
- 有序性,這是map結(jié)構(gòu)最大的優(yōu)點(diǎn),其元素的有序性在很多應(yīng)用中都會(huì)簡化很多的操作
- 紅黑樹,內(nèi)部實(shí)現(xiàn)一個(gè)紅黑書使得map的很多操作在lgn的時(shí)間復(fù)雜度下就可以實(shí)現(xiàn),因此效率非
常的高
- 缺點(diǎn):
- 空間占用率高,因?yàn)閙ap內(nèi)部實(shí)現(xiàn)了紅黑樹,雖然提高了運(yùn)行效率,但是因?yàn)槊恳粋€(gè)節(jié)點(diǎn)都需要額
外保存父節(jié)點(diǎn)、孩子節(jié)點(diǎn)和紅/黑性質(zhì),使得每一個(gè)節(jié)點(diǎn)都占用大量的空間
- 適用處:
對(duì)于那些有順序要求的問題,用map會(huì)更高效一些
unordered_map:
- 優(yōu)點(diǎn): 因?yàn)閮?nèi)部實(shí)現(xiàn)了哈希表,因此其查找速度非常的快
- 缺點(diǎn): 哈希表的建立比較耗費(fèi)時(shí)間
- 適用處:對(duì)于查找問題,unordered_map會(huì)更加高效一些,因此遇到查找問題,常會(huì)考慮一下用unordered_map
5.3.3 總結(jié)
- 內(nèi)存占有率的問題就轉(zhuǎn)化成紅黑樹 VS hash表 , 還是unorder_map占用的內(nèi)存要高。
- 但是unordered_map執(zhí)行效率要比map高很多
- 對(duì)于unordered_map或unordered_set容器,其遍歷順序與創(chuàng)建該容器時(shí)輸入的順序不一定相
同,因?yàn)楸闅v是按照哈希表從前往后依次遍歷的
推薦一個(gè)零聲學(xué)院免費(fèi)教程,個(gè)人覺得老師講得不錯(cuò),
分享給大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,
fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,
TCP/IP,協(xié)程,DPDK等技術(shù)內(nèi)容,點(diǎn)擊立即學(xué)習(xí):
服務(wù)器
音視頻
dpdk
Linux內(nèi)核

浙公網(wǎng)安備 33010602011771號(hào)