強(qiáng)類型枚舉
枚舉:分門別類與數(shù)值的名字
枚舉類型是C及C++中一個(gè)基本的內(nèi)置類型簸搞,不過也是一個(gè)有點(diǎn)"奇怪"的類型。從枚舉的本意上來講线罕,就是要定義一個(gè)類別澈吨,并窮舉同一類別下的個(gè)體以供代碼中使用。由于枚舉來源于C燥爷,所以出于設(shè)計(jì)上的簡單的目的蜈亩,枚舉值常常是對(duì)應(yīng)到整型數(shù)值的一些名字:enum Gender{Male,Female};
定義了Gender(性別)枚舉類型,其中包含兩種枚舉值Male及Famale前翎。編輯器會(huì)默認(rèn)為Male賦值0稚配,為Famale賦值1。這是C對(duì)名稱的簡單包裝鱼填,即將名稱對(duì)應(yīng)到數(shù)值药有。
而枚舉類型也可以是匿名的,匿名的枚舉會(huì)有意想不到的用處苹丸。比如當(dāng)程序員需要“數(shù)值的名字”的時(shí)候愤惰,我們常常可以使用以下3種方式來實(shí)現(xiàn)赘理。
#define Male 0
#define Female 1
宏的弱點(diǎn)在于其定義的只是預(yù)處理階段的名字宦言,如果代碼中有Male或者Female的字符串,無論在什么位置一律將被替換商模。所有奠旺,有的時(shí)候會(huì)干擾到正常的代碼,因此很多時(shí)候?yàn)榱吮苊膺@種情況施流,程序員會(huì)讓宏全部以大寫字母來命名响疚,以區(qū)別于正常的代碼。
而第二種方式---匿名的enum的狀況會(huì)好些瞪醋。
enum{Male,Female}
這里的匿名枚舉中的Male和Female都是編譯時(shí)期的名字忿晕,會(huì)得到編譯器的檢查。(怎么理解)
最好的結(jié)果是靜態(tài)常量:
const static int Male=0;
const static int Female=1;
靜態(tài)常量不僅僅是一個(gè)編譯時(shí)期的名字银受,編譯器還可能會(huì)為Male和Female在目標(biāo)代碼中產(chǎn)生實(shí)際的數(shù)據(jù)践盼,這會(huì)增加一點(diǎn)存儲(chǔ)空間鸦采。相比而言,匿名的枚舉似乎更為好用咕幻。
如果static的Male和Female聲明在class中渔伯,在一些較早的編譯器上不能為其就地賦值(賦值需要在class外),因此有人也采取了enum的方式在class中來代替常量聲明肄程。
enum有個(gè)很“奇怪”的設(shè)定锣吼,就是具名的enum類型的名字,以及enum的成員的名字都是全局可見的绷耍。這與C++中具名的namespace吐限、class/struct及union必須通過"名字::成員名"的方式訪問相比是格格不入的(namespace等被稱為強(qiáng)作用域類型鲜侥,而enum則是非強(qiáng)作用域類型)褂始。
例如:
namespace T{
enum Type{General,Light,Medium,Heavy};
}
namespace{
enum Category{General=1,Pistol,MachineGun,Cannon};
}
int main(){
T:: Type t=T::Light;
if(t==General)//忘記使用namespace
cout<<"General Weapon"<<endl;
return 0;
}//編譯選項(xiàng):g++5-1-1.cpp
Category在一個(gè)匿名namespace中,所以描函,所有枚舉成員名都默認(rèn)進(jìn)入全局名字空間崎苗。一旦,程序員在檢查t的值的時(shí)候忘記使用了 namespace T, 就會(huì)導(dǎo)致錯(cuò)誤的結(jié)果舀寓。
enum Type{First,Second,Third};//這個(gè)是具名的枚舉胆数,進(jìn)入全局命名空間
struct TestEnum{
enum Type2{Two,First,Three};//另一個(gè)具名的枚舉, 但是這個(gè)命名空間僅限于TestEnum結(jié)構(gòu)體內(nèi)。
void Test()
{
int i = First;
cout<< i;//1 //局部作用域的優(yōu)先級(jí)高于全局作用域
}
};
int main()
{
int i = First;
cout<<i;//0
TestEnum test;
test.Test();
cout<<i;//0
//int x = One; //error
int j = TestEnum::First;
cout<<j;//1
}
由于C中枚舉被設(shè)計(jì)為常量數(shù)值的"別名"的本性互墓,所以枚舉的成員總是可以被隱式地轉(zhuǎn)換為整型必尼。很多時(shí)候,這也是不安全的篡撵。
(同時(shí)判莉,對(duì)于匿名的枚舉,比如 enum{value = 0,value2=1} 他的功能等價(jià)于靜態(tài)成員變量育谬。)
enum Type{General,Light,Medium,Heavy};
//enum Category{General,Pistol,MachineGun,Cannon};//無法通過編譯券盅,重復(fù)定義了Genenral
enum Category{
Pistol,MachineGun,Cannon
};
struct Killer{
Killer(Type t,Category c): type(t), category(c){}
Type type;
Category category;
};
int main(){
Killer cool(General,MachineGun);
//...
//... 其他很多代碼 ...
//...
if(cool.type>=Pistol)
cout<<"It is not a pistol"<<endl;
//...
cout<<is_pod<Type>::value<<endl;//1
cout<<is_pod<Category>::value<<endl;//1
return 0;
}
在上述代碼中,類型Killer同時(shí) 擁有Type和Category兩種命名類型的枚舉類型成員膛檀。在一定時(shí)候锰镀,程序員想查看這位"冷酷"(cool)的殺手(Killer)是屬于什么Gategory的。但明顯咖刃,程序員用錯(cuò)了成員type泳炉。這是由于枚舉類型數(shù)值在進(jìn)行數(shù)值比較運(yùn)算時(shí),首先被隱式提升為int類型數(shù)據(jù)嚎杨,然后自由地進(jìn)行比較運(yùn)算花鹅。(事實(shí)上,我們的實(shí)驗(yàn)機(jī)上的編譯器會(huì)給出警告說不同枚舉類型枚舉成員間進(jìn)行了比較磕潮。)
為了解決這一問題翠胰,程序員一般會(huì)對(duì)枚舉類型進(jìn)行封裝容贝。下面是改良后的版本:
class Type{
public:
enum type{general,light,medium,heavy};
type val;
public:
Type(type t): val(t){} //構(gòu)造函數(shù)
bool operator>=(const Type&t){
return val>=t.val;
}
static const Type General,Light,Medium,Heavy;
};
const Type Type:: General(Type:: general); //const說明,這個(gè)對(duì)象是不可變的之景,然后Type是類名斤富,General是對(duì)象,然后(Type:: general)是對(duì)象的初始化锻狗。
const Type Type:: Light(Type:: light);
const Type Type:: Medium(Type:: medium);
const Type Type:: Heavy(Type:: heavy);
class Category{
public:
enum category{
pistol,machineGun,cannon
};
category val;
public:
Category(category c): val(c){} //構(gòu)造函數(shù)
bool operator>=(const Category&c){
return val>=c.val;
}
static const Category Pistol,MachineGun,Cannon;
};
const Category Category:: Pistol(Category::pistol);
const Category Category:: MachineGun(Category::MachineGun);
const Category Category:: Cannon(Category::Cannon);
struct Killer{
Killer(Type t,Category c):type(t),category(c){}
Type type;
Category category;
};
int main(){
//使用類型包裝后的enum
Killer notCool(Type:: General,Category::MachineGun);
//....其他代碼
if(notCool.type>=Type::General) //可以通過編譯
cout<<"It is not general" <<endl;
if(notCool.type>=Category::cannon Pistol)//該句無法編譯通過
cout<<"It is not a Pistol"<<endl;
//...
cout<<is_pod<Type>:: value<<endl;//0
cout<<is_pod<Category>:: value<<endl;//0
return 0;
}
封裝的代碼很長满力,簡單來說,封裝即是使得枚舉成員成為class的靜態(tài)成員轻纪。由于class中的數(shù)據(jù)不會(huì)被默認(rèn)轉(zhuǎn)換為整型數(shù)據(jù)(除非定義相關(guān)操作符函數(shù))油额,所以可以避免被隱式轉(zhuǎn)換。而且刻帚,通過封裝潦嘶,枚舉的成員也不再會(huì)污染全局名字空間了,使用時(shí)還必須帶上class的名字崇众,這樣一來掂僵,之前枚舉的一些小毛病都能夠得到克服。同時(shí)顷歌,這里還需要做操作符的重載锰蓬。
一些缺點(diǎn):由于封裝采用了靜態(tài)成員,原本屬于POD的enum被封裝成為非POD的了(is_pod均返回為0)眯漩,會(huì)導(dǎo)致一系列的損失芹扭。
(問題:什么是POD)
大多數(shù)系統(tǒng)的ABI規(guī)定,傳遞參數(shù)的時(shí)候如果參數(shù)是個(gè)結(jié)構(gòu)體赦抖,就不能使用寄存器來傳參(只能放在堆棧上)舱卡,而相對(duì)地,整型可以通過寄存器中傳遞摹芙。所以灼狰,一旦將class封裝版本的枚舉作為函數(shù)參數(shù)傳遞,就可能帶來一定的性能損失浮禾。
標(biāo)準(zhǔn)規(guī)定交胚,C++枚舉所基于的“基礎(chǔ)類型”是由編譯器來具體指定實(shí)現(xiàn)的,這回導(dǎo)致枚舉類型成員的基本類型的不確定性問題(尤其是符號(hào)性)盈电。
#include<iostream>
using namespace std;
enum C{
C1=1,C2=2
};
enum D{
D1=1,D2=2,Dbig=0xFFFFFFF0U
};
enum E{
E1=1,E2=2,Ebig=0xFFFFFFFFFLL
};
int main(){
cout<<sizeof(C1)<<endl;//4
cout<<Dbig<<endl;//編譯器輸入不同蝴簇,g++:4294967280
cout<<sizeof(D1) <<endl;//4
cout<<sizeof(Dbig)<<endl;//4
cout<<Ebig<<endl;//68719476735
cout<<sizeof(E1)<<endl;//8
return 0;
}
我們可以看到,編譯器會(huì)根據(jù)數(shù)據(jù)類型的不同對(duì)enum應(yīng)用不同的數(shù)據(jù)長度匆帚。在我們對(duì)g++的測(cè)試中熬词,普通的枚舉使用了4字節(jié)的內(nèi)容,而當(dāng)需要的時(shí)候,會(huì)拓展為8字節(jié)互拾。此外歪今,對(duì)于不同的編譯器,上例中Dbig的輸出結(jié)果將會(huì)不同:使用Visual C++編譯程序的輸出結(jié)果為-16颜矿,而使用g++來編譯輸出為4294967280寄猩。這是由于Visual C++總是使用無符號(hào)類型作為枚舉的底層實(shí)現(xiàn),而g++會(huì)根據(jù)枚舉的類型進(jìn)行變動(dòng)造成的骑疆。
(問:怎么理解Visual C++總是使用無符號(hào)類型作為枚舉的底層實(shí)現(xiàn))
強(qiáng)類型枚舉以及C++11對(duì)原有枚舉類型的擴(kuò)展
非強(qiáng)類型作用域田篇,允許隱式轉(zhuǎn)換為整型,占用存儲(chǔ)空間及符號(hào)性不確定箍铭,都是枚舉型的缺點(diǎn)泊柬。針對(duì)這些缺點(diǎn),新標(biāo)準(zhǔn)C++11引入了一種新的枚舉類型诈火,即 "枚舉型"兽赁,又稱 "強(qiáng)枚舉類型"(strong-typed enum)。
聲明強(qiáng)類型枚舉: 在enum后加上關(guān)鍵字class柄瑰。
enum class Type{General,Light,Medium,Heavy};
就聲明了一個(gè)強(qiáng)類型的枚舉Type闸氮。強(qiáng)類型的枚舉具有以下幾點(diǎn)優(yōu)勢(shì):
- 強(qiáng)作用域,強(qiáng)類型枚舉成員的名稱不會(huì)被輸出到其父作用域空間教沾。
- 轉(zhuǎn)換限制,強(qiáng)類型枚舉成員的值不可以與整型隱式地相互轉(zhuǎn)換译断。
- 可以指定底層類型授翻。強(qiáng)類型枚舉默認(rèn)的底層類型為int。 但也可以顯式地指定底層類型孙咪,具體方法為在枚舉名稱后面加上“:type”, 其中type可以是除wchar_t以外的任何整型堪唐。比如:
c++ enum class Type:char{ General, Light, Medium, Heavy};
就指定Type是基于char類型的強(qiáng)類型枚舉。
#include<iostream>
using namespace std;
enum class Type{ // 因?yàn)閷儆趶?qiáng)類型的翎蹈,所以淮菠,就不會(huì)輸出到父作用域空間
General,Light,Medium,Heavy
};
enum class Category{ // 這兩個(gè)類是單獨(dú)的,所以荤堪,Category里的General 和 Type里的General // 是分開的合陵。
General=1,Pistol,MachineGun,Cannon
};
int main(){
Type t=Type:: Light;
t=General;//編譯失敗,必須使用Type中的General Type::General
if(t==Category::General) //編譯失敗澄阳,必須使用Type中General
cout<<"General Weapon"<<endl;
if(t>Type::General) //通過編譯
cout<<"Not General Weapon"<<endl;
if(t>0) //編譯失敗拥知,無法轉(zhuǎn)換為Int類型
cout<<"Not General Weapon" <<en;
if((int)t>0)//通過編譯 強(qiáng)類型枚舉成員間仍然可以進(jìn)行數(shù)值式的比較,但不能夠隱式地轉(zhuǎn)為int型碎赢。事實(shí)上低剔,如果要將強(qiáng)類型枚舉轉(zhuǎn)化為其他類型,必須進(jìn)行顯示轉(zhuǎn)換。
cout<<"Not General Weapon"<<endl;
cout<<is_pod<Type>:: value<<endl;//1 Type 和 Category // 都是POD類型襟齿, 不會(huì)像class封裝版本一樣被編譯器視為結(jié)構(gòu)體姻锁, 書寫也很簡便。
cout<<is_pod<Category>:: value<<endl;//1
return 0;
}
我們定義了兩個(gè)強(qiáng)類型枚舉Type和Category, 它們都包含一個(gè)稱為General的成員猜欺。由于強(qiáng)類型枚舉成員的名字不會(huì)輸出到父作用域屋摔,因此不會(huì)有編譯問題。也由于不輸出成員名字替梨,所以我們?cè)谑褂迷擃愋统蓡T的時(shí)候必須加上其所屬的枚舉類型的名字钓试。此外,枚舉成員間仍可以進(jìn)行數(shù)值式的比較副瀑,但不能夠隱式轉(zhuǎn)換為int型弓熏。事實(shí)上,如果要將強(qiáng)類型枚舉轉(zhuǎn)化為其他類型糠睡,必須進(jìn)行顯式轉(zhuǎn)換挽鞠。
強(qiáng)類型制止enum成員和int之間的轉(zhuǎn)換,使得枚舉更加符合"枚舉"的本來意義狈孔,即對(duì)同類進(jìn)行列舉的一個(gè)集合信认,而定義其與數(shù)值間的關(guān)聯(lián)使之能夠默認(rèn)擁有一種對(duì)成員排列的機(jī)制。而制止成員名字輸出則進(jìn)一步避免了名字空間沖突的問題均抽。Type和Category都是POD類型嫁赏,不會(huì)像class封裝版本一樣被編譯器視為結(jié)構(gòu)體,書寫也很簡便油挥。在擁有類型安全和強(qiáng)作用域兩重優(yōu)點(diǎn)的情況下潦蝇,幾乎沒有任何額外的開銷。
此外深寥,由于可以指定底層基于的基本類型攘乒,我們可以避免編譯器不同而帶來的不可移植性。此外惋鹅,設(shè)置較小的基本類型也可以節(jié)省內(nèi)存空間则酝。
#include<iostream>
using namespace std;
enum class C:char{
C1=1,C2=2
};
// 強(qiáng)制型枚舉型,char 類型
enum class D:unsigned int{
D1=1,D2=2,Dbig=0xFFFFFFF0U
};
// 強(qiáng)制型枚舉型闰集,unsigned int 類型(因?yàn)镮nt 型是4個(gè)字節(jié)沽讹,所以在16進(jìn)制的情況下,最多8位)返十。
// 這里強(qiáng)制型枚舉型的長度取決于最長的那個(gè)值的長度妥泉,也取決于顯示定義的枚舉的基本類型。
int main(){
cout<<sizeof(C::C1)<<endl;//1
cout<<(unsigned int)D:: Dbig<<endl;//編譯器輸出一致洞坑,4294967280
cout<<sizeof(D:: D1) <<endl;//4
cout<<sizeof(D:: Dbig)<<endl;//4
return 0;
}
我們?yōu)閺?qiáng)類型枚舉C指定底層基本類型為char,因?yàn)槲覀冎挥蠧1盲链、C2兩個(gè)值較小的成員,一個(gè)char足以保存所有的枚舉類型。而對(duì)于強(qiáng)類型枚舉D刽沾,我們指定基本類型為unsigned int, 則所有編譯器都會(huì)使用無符號(hào)的unsigned int 來保存該枚舉本慕。故各個(gè)編譯器都能保證一致的輸出。
在新標(biāo)準(zhǔn)C++11中侧漓,原有枚舉類型的底層類型在默認(rèn)情況下锅尘,仍然由編譯器來具體指定實(shí)現(xiàn)。但也可以跟強(qiáng)類型枚舉一樣布蔗,都是枚舉名稱后面加上":type", 其中type可以是除wchar_t以外的任何整型藤违。比如:
enum Type:char{General,Light,Medium,Heavy};
在C++11中也是一個(gè)合法的enum聲明。第二個(gè)擴(kuò)展則是作用域的纵揍。在C++11中顿乒,枚舉成員的名字除了會(huì)自動(dòng)輸出到父作用域,也可以在枚舉類型定義的作用域內(nèi)有效泽谨。
enum Type{General, Light, Medium, Heavy};
Type t1=General; // 合法
Type t2=Type:: General; //合法
此外璧榄,我們?cè)诼暶鲝?qiáng)類型枚舉的時(shí)候,也可以使用關(guān)鍵字enum struct吧雹。事實(shí)上 enum struct 和 enum class 在語法上沒有任何區(qū)別(enum class的成員沒有公私之分骨杂,也不會(huì)使用模板來支持泛化的聲明)。
而對(duì)于匿名的enum class雄卷,由于enum class是強(qiáng)類型作用域的搓蚪,故匿名的enum class很可能什么都做不了
enum class{ General, Light, Medium,Heavy} weapon;
int main(){
weapon=General;//無法通過編譯
bool b=(weapon==weapon:: General);//無法編譯通過
return 0;
}
我們聲明了一個(gè)匿名的enum class實(shí)例weapon,卻無法對(duì)其設(shè)置值或者比較其值(這和匿名struct是不一樣的)。事實(shí)上龙亲,使用enum class的時(shí)候陕凹,應(yīng)該總是為enum class提供一個(gè)名字。
匿名 enum class 和 匿名 struct 的區(qū)別
匿名 struct 的變量可以訪問整個(gè) struct 的變量的信息鳄炉。
堆內(nèi)存管理:智能指針與垃圾回收
顯式內(nèi)存管理
程序員在處理現(xiàn)實(shí)生活的C/C++程序的時(shí)候,會(huì)遇到程序運(yùn)行時(shí)突然退出搜骡,或占用的內(nèi)容越來越多拂盯,最后不得不定期重啟。這些問題可以追溯到C/C++中的顯式堆內(nèi)存管理上记靡。通常情況下谈竿,這些癥狀都是由于
程序沒有正確處理堆內(nèi)存的分配與釋放造成的,從語言層面來講摸吠,我們可以將其歸納為以下的一些問題空凸。
- 野指針:一些內(nèi)存單元已被釋放,之前指向它的指針卻還在被使用寸痢。這些內(nèi)存有可能被運(yùn)行時(shí)系統(tǒng)重新分配給程序使用呀洲,從而導(dǎo)致了無法預(yù)測(cè)的錯(cuò)誤。
- 重復(fù)釋放:程序試圖去釋放已經(jīng)被釋放過的內(nèi)存單元,或者釋放已經(jīng)被重新分配過的內(nèi)存單元道逗,就會(huì)導(dǎo)致重復(fù)釋放錯(cuò)誤兵罢。通常重復(fù)釋放內(nèi)存會(huì)導(dǎo)致C/C++運(yùn)行時(shí)系統(tǒng)打印出大量錯(cuò)誤及診斷信息
- 內(nèi)存泄露:不再需要使用的內(nèi)存單元如果沒有被釋放就會(huì)導(dǎo)致內(nèi)存泄露。如果程序不斷地重復(fù)進(jìn)行這類操作滓窍,將會(huì)導(dǎo)致內(nèi)存占用劇增卖词。
雖然顯式的管理內(nèi)存在性能上有一定的優(yōu)勢(shì),但也被廣泛地認(rèn)為是容易出錯(cuò)的吏夯。隨著多線程程序的出現(xiàn)和廣泛使用此蜈,內(nèi)存管理不佳的情況還可能會(huì)變得更加嚴(yán)重。因此噪生,很多程序員也認(rèn)為編程語言應(yīng)該提供更好的機(jī)制裆赵,讓程序員擺脫內(nèi)存管理的細(xì)節(jié)。在C++中杠园,一個(gè)這樣的機(jī)制就是標(biāo)準(zhǔn)庫中的智能指針顾瞪。
在C++11新標(biāo)準(zhǔn)中,智能指針被進(jìn)行了改進(jìn)抛蚁,以更加適應(yīng)實(shí)際的應(yīng)用需求陈醒。而進(jìn)一步地,標(biāo)準(zhǔn)庫還提供了所謂 "最小垃圾回收" 的支持瞧甩。
C++11的智能指針
在C++98中钉跷,智能指針通過一個(gè)模板類型"auto_ptr"來實(shí)現(xiàn)。auto_ptr以對(duì)象的方式管理堆分配的內(nèi)存肚逸,并在適當(dāng)?shù)臅r(shí)間(比如析構(gòu))爷辙,釋放所獲得的堆內(nèi)存。這種堆內(nèi)存管理的方式只需要程序員將new操作返回的指針作為auto_ptr的初始值即可朦促,程序員不用再顯式地調(diào)用delete膝晾。
比如:auto_ptr(new int)
。但是在一定程度上避免了堆內(nèi)存忘記釋放而造成的問題务冕。不過auto_ptr有一些缺點(diǎn)(拷貝時(shí)返回一個(gè)左值血当,不能調(diào)用delete[]等),所以在C++11標(biāo)準(zhǔn)中改用unique_ptr禀忆、shared_ptr及weak_ptr等智能指針來自動(dòng)回收堆分配的對(duì)象臊旭。
下面是一個(gè)C++11中使用新的智能指針的簡單例子:
#include<iostream>
#include<memory>
using namespace std;
int main(){
unique_ptr<int> up1(new int(11));//無法復(fù)制的unique_ptr
//unique_ptr<int> up2=up1;//不能通過編譯。
cout<<*up1<<endl;//11
unique_ptr<int> up3=move(up1) ;//現(xiàn)在p3是數(shù)據(jù)唯一的unique_ptr智能指針
cout<<*up3<<endl;//11
//cout<<*up1<<endl;//運(yùn)行時(shí)錯(cuò)誤
up3.reset() ; //顯式釋放內(nèi)存
up1.reset() ; //不會(huì)導(dǎo)致運(yùn)行時(shí)錯(cuò)誤
//cout<<*up3<<endl;//運(yùn)行時(shí)錯(cuò)誤
shared_ptr<int> sp1(new int(22));
shared_ptr<int> sp2=sp1;
cout<<*sp1<<endl;//22
cout<<*sp2<<endl;//22
sp1.reset();
cout<<*sp2<<endl;//22
}
在上述代碼中箩退,使用了兩種不同的智能指針unique_ptr及shared_ptr來自動(dòng)地釋放堆對(duì)象的內(nèi)存离熏。由于每個(gè)智能指針都重載*運(yùn)算符,用戶可以使用*up1這樣的方式來訪問所分配的堆內(nèi)存戴涝。而在該指針析構(gòu)或者調(diào)用reset成員的時(shí)候滋戳,智能指針都可能釋放其擁有的堆內(nèi)存钻蔑。從作用上來講,unique_ptr和shared_ptr還是和以前的auto_ptr保持了一致胧瓜。
unique_ptr和shared_ptr 在對(duì)所占內(nèi)存的共享上還是有一定區(qū)別的矢棚。
直觀來看,unique_ptr形如其名地府喳,與所指對(duì)象的內(nèi)存綁定緊密郎仆,不能與其他unique_ptr類型的指針共享所指對(duì)象的內(nèi)存榨惠。比如辜纲,本例中的unique_ptr<int> up2=up1;不能通過編譯袖扛,是因?yàn)槊總€(gè)unique_ptr都是唯一地"擁有"所指向的對(duì)象內(nèi)存,由于up1唯一地占用了new分配的堆內(nèi)存弯蚜,所以u(píng)p2無法共享其"使用權(quán)"孔轴。事實(shí)上,這種"所有權(quán)"僅能夠通過標(biāo)準(zhǔn)庫的Move函數(shù)來轉(zhuǎn)移碎捺。我們可以看到代碼中up3的定義路鹰,unique_ptr<int> up3=move(up1); 一旦"所有權(quán)"轉(zhuǎn)移成功了,原來的unique_ptr指針就是去了對(duì)象內(nèi)存的所有權(quán)收厨。此時(shí)再使用已經(jīng)"失勢(shì)"的unique_ptr,就會(huì)導(dǎo)致運(yùn)行時(shí)的錯(cuò)誤晋柱。本例中的后段使用*up1就是很好的例子。
而unique_ptr則是一個(gè)刪除了拷貝構(gòu)造函數(shù)诵叁、保留了移動(dòng)構(gòu)造函數(shù)的指針封裝類型雁竞。程序員盡可以使用右值對(duì)unique_ptr對(duì)象進(jìn)行構(gòu)造,而且一旦構(gòu)造成功拧额,右值對(duì)象中的指針即被"竊取"碑诉,因此該右值對(duì)象即刻失去了對(duì)指針的"所有權(quán)"。
而shared_ptr同樣形如其名侥锦,允許多個(gè)該智能指針共享地"擁有"同一堆分配對(duì)象的內(nèi)存进栽。與unique_ptr不同的是,由于在實(shí)現(xiàn)上采用了引用計(jì)數(shù)恭垦,所以一旦一個(gè)shared_ptr指針放棄了"所有權(quán)"(失效)泪幌,其他的shared_ptr對(duì)對(duì)象內(nèi)存的引用并不會(huì)收到影響。只有引用計(jì)數(shù)歸零的時(shí)候署照,share_ptr才會(huì)真正釋放所占用的堆內(nèi)存的空間。
在C++11標(biāo)準(zhǔn)中吗浩,還有weak_ptr這個(gè)類模板建芙。weak_ptr的使用更為復(fù)雜一點(diǎn),它可以指向shared_ptr指針指向的對(duì)象內(nèi)存懂扼,卻并不擁有該內(nèi)存禁荸。而使用weak_ptr成員lock右蒲,則可返回其指向內(nèi)存的一個(gè)shared_ptr對(duì)象,且在所指對(duì)象內(nèi)存已經(jīng)無效時(shí)赶熟,返回指針空值瑰妄。
#include<iostream>
#include<memory>
using namespace std;
void Check(weak_ptr<int> &wp) {
shared_ptr<int> sp=wp.lock();//轉(zhuǎn)換為shared_ptr<int> 但是在函數(shù)結(jié)束的時(shí)候,指針的生存周期就已經(jīng)結(jié)束了映砖。
//在sp1及sp2都有效的時(shí)候间坐,調(diào)用wp的lock函數(shù),將返回一個(gè)有效的shared_ptr對(duì)象供使用邑退,如果沒有了竹宋,通過sp!=nullptr即可進(jìn)行判斷即可
if(sp!=nullptr)
cout<<"still"<<*sp<<endl;
else
cout<<"pointer is invalid."<<endl;
}
int main(){
shared_ptr<int> sp1(new int(22));
shared_ptr<int> sp2=sp1;
weak_ptr<int> wp=sp1;//指向shared_ptr<int>所指對(duì)象
cout<<*sp1<<endl;//22
cout<<*sp2<<endl;//22
Check(wp);//still 22
sp1.reset() ;
cout<<*sp2<<endl;//22
Check(wp);//still 22
sp2.reset();
Check(wp);//pointer is invalid
return 0;
}
我們定義了一個(gè)共享對(duì)象內(nèi)存的兩個(gè)shared_ptr--sp1及sp2。而weak_ptr wp同樣指向該對(duì)象內(nèi)存地技◎谄撸可以看到,在sp1及sp2都有效的時(shí)候莫矗,我們調(diào)用wp的lock函數(shù)飒硅,將返回一個(gè)有效的shared_ptr對(duì)象供使用,于是Check函數(shù)會(huì)輸出以下內(nèi)容:still 22
此后我們分別調(diào)用了sp1及sp2的reset函數(shù)作谚,這回導(dǎo)致對(duì)唯一堆內(nèi)存對(duì)象的引用計(jì)數(shù)降至0.而一旦引用計(jì)數(shù)歸0三娩, shared_ptr<int>就會(huì)釋放堆內(nèi)存空間,使之失效食磕。此時(shí)尽棕,我們?cè)僬{(diào)用weak_ptr的lock函數(shù)時(shí),則返回一個(gè)指針空值nullptr彬伦。這時(shí)Check函數(shù)則會(huì)打印出: pointer is invalid 整個(gè)過程中滔悉, 只有shared_ptr參與了引用計(jì)數(shù),而weak_ptr 沒有影響其指向的內(nèi)存的引用計(jì)數(shù)单绑。 因此可以驗(yàn)證 shared_ptr指針的有效性回官。
程序員用unique_ptr代替以前使用auto_ptr的代碼就可以使用C++11中的智能指針。而shared_ptr及weak_ptr則可用在用戶需要引用計(jì)數(shù)的地方搂橙。
垃圾回收的分類
我們把之前使用過歉提,現(xiàn)在不再使用或沒有任何指針再指向的內(nèi)存空間就被稱為“垃圾”。而將這些“垃圾”收集起來以便再次利用的機(jī)制区转,就被稱為“垃圾回收”苔巨。
垃圾回收的方式雖多,但主要可以分為兩大類:
1废离、基于引用計(jì)數(shù)(reference counting garbage collector) 的垃圾回收器
簡單地說侄泽,引用計(jì)數(shù)主要是使用系統(tǒng)記錄 對(duì)象被引用(引用、指針)的次數(shù)蜻韭。當(dāng)對(duì)象被引用的次數(shù)變?yōu)?時(shí)悼尾,該對(duì)象即可被視作 “垃圾” 而回收柿扣。使用引用計(jì)數(shù)做垃圾回收的算法的一個(gè)優(yōu)點(diǎn)是實(shí)現(xiàn)很簡單,與其他垃圾回收算法相比闺魏,該方法不會(huì)造成程序暫停未状,因?yàn)橛?jì)數(shù)的增減與對(duì)象的使用是緊密結(jié)合的。此外析桥,引用計(jì)數(shù)也不會(huì)對(duì)系統(tǒng)的緩存或者交換空間造成沖擊司草,因此被認(rèn)為“副作用”較小。但是這種方法比較難處理“環(huán)形引用”問題烹骨,此外由于計(jì)數(shù)帶來的額外開銷也不小翻伺,在實(shí)用上也有一定的限制。
2沮焕、基于跟蹤處理的垃圾回收器
相比于引用計(jì)數(shù)吨岭,跟蹤處理的垃圾回收機(jī)制被更為廣泛地應(yīng)用。其基本方法是產(chǎn)生跟蹤對(duì)象的關(guān)系圖峦树,然后進(jìn)行垃圾回收辣辫。使用跟蹤方式的垃圾回收算法主要有以下幾種:
(1)標(biāo)記-清除(Mark-Sweep)
這個(gè)算法可以分為兩個(gè)過程。首先該算法將程序中正在使用的對(duì)象視為 “根對(duì)象”魁巩,從根對(duì)象開始查找它們所引用的堆空間急灭,并在這些堆空間上做標(biāo)記。當(dāng)標(biāo)記結(jié)束后谷遂,所有被標(biāo)記的對(duì)象就是可達(dá)對(duì)象(Reachable Object) 或活對(duì)象(Live Object),而沒有被標(biāo)記的對(duì)象就被認(rèn)為是垃圾葬馋,在第二步的清掃階段會(huì)被回收掉。這種方法的特點(diǎn)是活的對(duì)象不會(huì)被移動(dòng)肾扰,但是其存在會(huì)出現(xiàn)大量的 內(nèi)存碎片 的問題畴嘶。
(2)標(biāo)記-整理(Mark-Compact)----------這種方法感覺要好用很多
這個(gè)算法標(biāo)記的方法和標(biāo)記-清除方法一樣,但是標(biāo)記完之后集晚,不再遍歷所有對(duì)象清掃垃圾了窗悯,而是將活的對(duì)象向“左”靠齊,這就解決了內(nèi)存碎片的問題偷拔。特點(diǎn)就是移動(dòng)活的對(duì)象蒋院,因此相對(duì)應(yīng)的,程序中所有對(duì)堆內(nèi)存的引用都必須更新莲绰。
(3)標(biāo)記-拷貝(Mark-Copy)
這種算法將堆空間分為兩個(gè)部分:From 和 To. 剛開始系統(tǒng)只從From的堆空間里面分配內(nèi)存欺旧,當(dāng)From分配滿的時(shí)候系統(tǒng)就開始垃圾回收:從From堆空間找出活的對(duì)象,拷貝到To的堆空間里蛤签。這樣一來切端,F(xiàn)rom的堆空間里面就全剩下垃圾了。而對(duì)象被拷貝到To里之后顷啼,在To里是緊湊排列的踏枣。接下來是需要將From和To交換一下角色(這里是如何進(jìn)行角色交換的),接著從新的From里面開始分配钙蒙。標(biāo)記-拷貝算法的一個(gè)問題是堆的利用率只有一半茵瀑,而且也需要移動(dòng)活的對(duì)象。此外躬厌,從某種意義上講马昨,這種算法其實(shí)是標(biāo)記-整理算法的另一種實(shí)現(xiàn)而已。
C++與垃圾回收
在C++11中扛施,智能指針等可以支持引用計(jì)數(shù)鸿捧。不過由于引用計(jì)數(shù)并不能有效解決形如“環(huán)形引用”等問題,其使用會(huì)受到一些限制疙渣。而且基于一些其他的原因匙奴,比如多線程程序等而引入的內(nèi)存管理上的困難,程序員可能也會(huì)需要垃圾回收妄荔。--------(這里教的是如何進(jìn)行手動(dòng)的垃圾回收)泼菌。
一些第三方的C/C++庫已經(jīng)支持標(biāo)記-清除方法的垃圾回收,比如一個(gè)比較著名的C/C++垃圾回收庫 ———— Boehm. 該垃圾回收器需要程序員使用庫中的 堆內(nèi)存分配函數(shù) (這個(gè)庫中的堆內(nèi)存分配函數(shù)是什么啦租?) 顯式地替代malloc,繼而將堆內(nèi)存的管理交給垃圾回收器來完成垃圾回收哗伯。不過由于C/C++中指針類型的使用非常靈活,這樣的庫在實(shí)際使用中會(huì)有一些限制篷角,可移植性也不好焊刹。
簡單來說,垃圾回收的不安全性源自于C/C++語言對(duì)指針的“放縱”恳蹲,即允許過分靈活的使用虐块。
int main(){
int*p=new int;
p+=10; //移動(dòng)指針,可能導(dǎo)致垃圾回收器
p-=10; //回收原來指向的內(nèi)存
*p=10; //再次使用原本相同的指針可能無效
}
(回收的時(shí)候阱缓,如果指針指向了其他的地方非凌,那么系統(tǒng)將會(huì)認(rèn)為指針曾指向的內(nèi)存不再使用。)
通過指針的自加和自減能夠使程序員輕松地找到“下一個(gè)” 同樣的對(duì)象(實(shí)際是一個(gè)迭代器的概念)荆针。不過對(duì)于垃圾回收來說敞嗡,一旦p指向了別的地址,則可認(rèn)為p曾指向的內(nèi)存不再使用航背。垃圾回收器可以據(jù)此對(duì)其進(jìn)行回收喉悴。這對(duì)之后p的使用(*p=10)帶來的后果是災(zāi)難性的。
int main(){
int*p = new int;
int*q = (int*) (reinterpret_cast<long long>(p)^2012); //q隱藏了p
//做一些工作玖媚,垃圾回收器可能已經(jīng)回收了p指向?qū)ο? q=(int*)(reinterpret_cast<long long>(q)^2012); //這里的q==p
*q=10;
}//編譯選項(xiàng):g++5-2-4.cpp
(reinterpret_cast<intptr_t>(p) 返回的是一個(gè)整型)
補(bǔ)充:reinterpret_cast 運(yùn)算符是用來處理無關(guān)類型之間的轉(zhuǎn)換箕肃;它會(huì)產(chǎn)生一個(gè)新的值,這個(gè)值會(huì)有與原始參數(shù)(expressoin)有完全相同的比特位今魔。我很好奇勺像,這個(gè)地方怎么用
reinterpret_cast<long long>(p)^2012 這地方怎么做隱藏的障贸。因?yàn)橛昧薧2012,然后再異或一次吟宦,就可以將參數(shù)解放出來篮洁。
但是在這個(gè)代碼里面,用指針q隱藏了指針p殃姓。而后袁波,又用可逆的異或運(yùn)算將p “恢復(fù)”了出來。在main函數(shù)中蜗侈,p實(shí)際所指向的內(nèi)存都是有效地篷牌,但由于該指針被隱藏了,垃圾回收器可以早早地將p指向的對(duì)象回收掉踏幻。同樣枷颊,語句*p=10的后果也是災(zāi)難性的。
指針的靈活使用可能是C/C++的一大優(yōu)勢(shì)叫倍,而對(duì)于垃圾回收來說偷卧,卻會(huì)帶來很大的困擾。被隱藏的指針會(huì)導(dǎo)致編譯器在分析指針的可達(dá)性(生命周期)時(shí)出錯(cuò)吆倦。而即使編譯器開發(fā)出了隱藏指針分析的手段听诸,其帶來的編譯開銷也不會(huì)讓程序員對(duì)編譯時(shí)間的顯著增長視而不見。
C++11和垃圾回收的解決方案是新接口蚕泽,就是讓程序員利用這樣的接口來提供編譯器代碼中存在指針不安全的區(qū)域晌梨。
C++11與最小垃圾回收支持
C++11新標(biāo)準(zhǔn)里面為了做到最小的垃圾回收支持,對(duì)“安全”的指針進(jìn)行了定義须妻,或者使用C++11中的術(shù)語說仔蝌,安全派生的指針。是指向由new分配的對(duì)象或其子對(duì)象的指針荒吏。安全派生指針的操作包括:
在解引用基礎(chǔ)上的引用敛惊,比如:&*p.
定義明確的指針操作,比如:p+1. //這里指針的長度應(yīng)該是和指針的類別掛鉤的绰更。
定義明確的指針轉(zhuǎn)換瞧挤,比如:static_cast<void*>(p).
指針和整型之間的reinterpret_cast, 比如:reinterpret_cast<intptr_t>(p).
(“解引用”,我到覺得可以從另一個(gè)角度理解儡湾,"*" 的作用是引用指針指向的變量特恬,引用其實(shí)就是引用該變量的地址,“解”就是把該地址對(duì)應(yīng)的東西解開徐钠,解出來癌刽,就像打開一個(gè)包裹一樣,那就是該變量的值了,所以稱為“解引用”)
(注意 intptr_t是C++11中一個(gè)可選擇實(shí)現(xiàn)的類型显拜,其長度等于平臺(tái)上指針的長度(通過decltype聲明)
在原來的代碼里面 reinterpret_cast<long long>(p) 是合法的安全派生操作衡奥,而轉(zhuǎn)化后的指針再進(jìn)行異或操作:
reinterpret_cast<long long>(p)2012之后,指針就不再是安全派生的了讼油,這是因?yàn)楫惢虿僮鳎?/sup>)不是一個(gè)安全派生操作杰赛。同理:
reinterpret_cast<long long>(q)^2012也不是安全派生指針。因此矮台,根據(jù)定義,在使用內(nèi)存回收器的情況下根时,*p=10的行為是不確定的瘦赫。
C++11的規(guī)則中,最小垃圾回收支持是基于安全派生指針這個(gè)概念的蛤迎。程序員可以通過
get_pointer_safety 函數(shù)查詢來確認(rèn)編譯器是否支持這個(gè)特性确虱。原型如下:
pointer_safety get_pointer_safety() noexcept
其返回一個(gè)pointer_safety類型的值。如果該值為 pointer_safety:: strict, 則表明編譯器支持最小垃圾回收及安全派生指針等相關(guān)概念替裆,如果該值為
pointer_safety::relax 或是 pointer_safety:: preferred, 則表明編譯器并不支持校辩,基本上和沒有垃圾回收的 C 和 C++98 一樣。
------------ 這說明辆童,通過這種方式可以檢測(cè)是否有最小垃圾回收機(jī)制 ----------------
如果程序員代碼中出現(xiàn)了指針不安全使用的情況宜咒,C++11允許程序員通過一些API來通知垃圾回收器不得回收該內(nèi)存。C++11的最小垃圾回收支持使用了垃圾回收的術(shù)語把鉴,即需聲明該內(nèi)存為“可到達(dá)”的故黑。
void declare_reachable(void*p);
template<class T>T*undeclare_reachable(T*p) noexcept;
declare_reachable () 顯示地通知垃圾回收器某一個(gè)對(duì)象應(yīng)被認(rèn)為可達(dá)的,即使它的所有指針都對(duì)回收器不可見庭砍。undeclare_reachable() 則可以取消這種可達(dá)聲明场晶。
#include <memory>
using namespace std;
int main(){
int* p=new int;
declare_reachable(p); //在p被隱藏之前聲明為可達(dá)的
int*q=(int*)((long long)p^2012);
// 解除可達(dá)聲明
q = undeclare_reachable<int>((int*)((long long)q^2012));
*q=10;
return 0;
}
p指針被不安全派生(隱藏)之內(nèi)使用declare_reachable聲明其實(shí)可達(dá)的。這樣一來怠缸,它會(huì)被垃圾回收器忽略而不會(huì)被回收诗轻。而在我們通過可逆的異或運(yùn)算使得q指針指向p所指對(duì)象時(shí),我們則使用了undeclare_reachable 來取消可達(dá)聲明揭北。注意 underclare_reachable 不是通知垃圾回收器 p 所指對(duì)象已經(jīng)可以回收扳炬。實(shí)際上,declare_reachable 和 undeclare_reachable 只是確立了一個(gè)代碼范圍罐呼,即在兩者之間的代碼運(yùn)行中鞠柄,p所指對(duì)象不會(huì)被垃圾回收器所回收。
declare_reachable 只需要傳入一個(gè)簡單的 void指針嫉柴,但 undeclare_reachable 卻被設(shè)計(jì)為一個(gè)函數(shù)模板厌杜。目的是為了返回合適類型以供程序使用。而垃圾回收器本來就知道指針?biāo)赶虻膬?nèi)存的大小,因此declare_reachable傳入void指針就已經(jīng)足夠了夯尽。
有的時(shí)候程序員會(huì)選擇在一大片連續(xù)的堆內(nèi)存上進(jìn)行指針式操作瞧壮,為了讓垃圾回收器不關(guān)心該區(qū)域,也可以使用 declare_no_pointers及undeclare_no_pointers函數(shù)來告訴垃圾回收器該內(nèi)存區(qū)域不存在有效的指針匙握。
void declare_no_pointers(char*p,size_t n) noexcept;
void underclare_no_pointers(char*p,size_t n) noexcept;
不過指定的是從p開始的連續(xù)n的內(nèi)存咆槽。
C++11標(biāo)準(zhǔn)中對(duì)指針的垃圾回收支持僅限于系統(tǒng)提供的new操作符分配的內(nèi)存,而malloc分配的內(nèi)存則會(huì)被認(rèn)為總是可達(dá)的圈纺,即無論何時(shí)垃圾回收器都不予回收秦忿。因此使用malloc等的較老代碼的堆內(nèi)存還是必須由程序員自己控制。