強類型枚舉
枚舉:分門別類與數值的名字
枚舉類型是C及C++中一個基本的內置類型,不過也是一個有點"奇怪"的類型。從枚舉的本意上來講,就是要定義一個類別,并窮舉同一類別下的個體以供代碼中使用。由于枚舉來源于C,所以出于設計上的簡單的目的,枚舉值常常是對應到整型數值的一些名字:enum Gender{Male,Female};
定義了Gender(性別)枚舉類型,其中包含兩種枚舉值Male及Famale。編輯器會默認為Male賦值0,為Famale賦值1。這是C對名稱的簡單包裝,即將名稱對應到數值。
而枚舉類型也可以是匿名的,匿名的枚舉會有意想不到的用處。比如當程序員需要“數值的名字”的時候,我們常常可以使用以下3種方式來實現。
#define Male 0
#define Female 1
宏的弱點在于其定義的只是預處理階段的名字,如果代碼中有Male或者Female的字符串,無論在什么位置一律將被替換。所有,有的時候會干擾到正常的代碼,因此很多時候為了避免這種情況,程序員會讓宏全部以大寫字母來命名,以區別于正常的代碼。
而第二種方式---匿名的enum的狀況會好些。
enum{Male,Female}
這里的匿名枚舉中的Male和Female都是編譯時期的名字,會得到編譯器的檢查。(怎么理解)
最好的結果是靜態常量:
const static int Male=0;
const static int Female=1;
靜態常量不僅僅是一個編譯時期的名字,編譯器還可能會為Male和Female在目標代碼中產生實際的數據,這會增加一點存儲空間。相比而言,匿名的枚舉似乎更為好用。
如果static的Male和Female聲明在class中,在一些較早的編譯器上不能為其就地賦值(賦值需要在class外),因此有人也采取了enum的方式在class中來代替常量聲明。
enum有個很“奇怪”的設定,就是具名的enum類型的名字,以及enum的成員的名字都是全局可見的。這與C++中具名的namespace、class/struct及union必須通過"名字::成員名"的方式訪問相比是格格不入的(namespace等被稱為強作用域類型,而enum則是非強作用域類型)。
例如:
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;
}//編譯選項:g++5-1-1.cpp
Category在一個匿名namespace中,所以,所有枚舉成員名都默認進入全局名字空間。一旦,程序員在檢查t的值的時候忘記使用了 namespace T, 就會導致錯誤的結果。
enum Type{First,Second,Third};//這個是具名的枚舉,進入全局命名空間
struct TestEnum{
enum Type2{Two,First,Three};//另一個具名的枚舉, 但是這個命名空間僅限于TestEnum結構體內。
void Test()
{
int i = First;
cout<< i;//1 //局部作用域的優先級高于全局作用域
}
};
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中枚舉被設計為常量數值的"別名"的本性,所以枚舉的成員總是可以被隱式地轉換為整型。很多時候,這也是不安全的。
(同時,對于匿名的枚舉,比如 enum{value = 0,value2=1} 他的功能等價于靜態成員變量。)
enum Type{General,Light,Medium,Heavy};
//enum Category{General,Pistol,MachineGun,Cannon};//無法通過編譯,重復定義了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同時 擁有Type和Category兩種命名類型的枚舉類型成員。在一定時候,程序員想查看這位"冷酷"(cool)的殺手(Killer)是屬于什么Gategory的。但明顯,程序員用錯了成員type。這是由于枚舉類型數值在進行數值比較運算時,首先被隱式提升為int類型數據,然后自由地進行比較運算。(事實上,我們的實驗機上的編譯器會給出警告說不同枚舉類型枚舉成員間進行了比較。)
為了解決這一問題,程序員一般會對枚舉類型進行封裝。下面是改良后的版本:
class Type{
public:
enum type{general,light,medium,heavy};
type val;
public:
Type(type t): val(t){} //構造函數
bool operator>=(const Type&t){
return val>=t.val;
}
static const Type General,Light,Medium,Heavy;
};
const Type Type:: General(Type:: general); //const說明,這個對象是不可變的,然后Type是類名,General是對象,然后(Type:: general)是對象的初始化。
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){} //構造函數
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的靜態成員。由于class中的數據不會被默認轉換為整型數據(除非定義相關操作符函數),所以可以避免被隱式轉換。而且,通過封裝,枚舉的成員也不再會污染全局名字空間了,使用時還必須帶上class的名字,這樣一來,之前枚舉的一些小毛病都能夠得到克服。同時,這里還需要做操作符的重載。
一些缺點:由于封裝采用了靜態成員,原本屬于POD的enum被封裝成為非POD的了(is_pod均返回為0),會導致一系列的損失。
(問題:什么是POD)
大多數系統的ABI規定,傳遞參數的時候如果參數是個結構體,就不能使用寄存器來傳參(只能放在堆棧上),而相對地,整型可以通過寄存器中傳遞。所以,一旦將class封裝版本的枚舉作為函數參數傳遞,就可能帶來一定的性能損失。
標準規定,C++枚舉所基于的“基礎類型”是由編譯器來具體指定實現的,這回導致枚舉類型成員的基本類型的不確定性問題(尤其是符號性)。
#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;
}
我們可以看到,編譯器會根據數據類型的不同對enum應用不同的數據長度。在我們對g++的測試中,普通的枚舉使用了4字節的內容,而當需要的時候,會拓展為8字節。此外,對于不同的編譯器,上例中Dbig的輸出結果將會不同:使用Visual C++編譯程序的輸出結果為-16,而使用g++來編譯輸出為4294967280。這是由于Visual C++總是使用無符號類型作為枚舉的底層實現,而g++會根據枚舉的類型進行變動造成的。
(問:怎么理解Visual C++總是使用無符號類型作為枚舉的底層實現)
強類型枚舉以及C++11對原有枚舉類型的擴展
非強類型作用域,允許隱式轉換為整型,占用存儲空間及符號性不確定,都是枚舉型的缺點。針對這些缺點,新標準C++11引入了一種新的枚舉類型,即 "枚舉型",又稱 "強枚舉類型"(strong-typed enum)。
聲明強類型枚舉: 在enum后加上關鍵字class。
enum class Type{General,Light,Medium,Heavy};
就聲明了一個強類型的枚舉Type。強類型的枚舉具有以下幾點優勢:
- 強作用域,強類型枚舉成員的名稱不會被輸出到其父作用域空間。
- 轉換限制,強類型枚舉成員的值不可以與整型隱式地相互轉換。
- 可以指定底層類型。強類型枚舉默認的底層類型為int。 但也可以顯式地指定底層類型,具體方法為在枚舉名稱后面加上“:type”, 其中type可以是除wchar_t以外的任何整型。比如:
c++ enum class Type:char{ General, Light, Medium, Heavy};
就指定Type是基于char類型的強類型枚舉。
#include<iostream>
using namespace std;
enum class Type{ // 因為屬于強類型的,所以,就不會輸出到父作用域空間
General,Light,Medium,Heavy
};
enum class Category{ // 這兩個類是單獨的,所以,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) //編譯失敗,無法轉換為Int類型
cout<<"Not General Weapon" <<en;
if((int)t>0)//通過編譯 強類型枚舉成員間仍然可以進行數值式的比較,但不能夠隱式地轉為int型。事實上,如果要將強類型枚舉轉化為其他類型,必須進行顯示轉換。
cout<<"Not General Weapon"<<endl;
cout<<is_pod<Type>:: value<<endl;//1 Type 和 Category // 都是POD類型, 不會像class封裝版本一樣被編譯器視為結構體, 書寫也很簡便。
cout<<is_pod<Category>:: value<<endl;//1
return 0;
}
我們定義了兩個強類型枚舉Type和Category, 它們都包含一個稱為General的成員。由于強類型枚舉成員的名字不會輸出到父作用域,因此不會有編譯問題。也由于不輸出成員名字,所以我們在使用該類型成員的時候必須加上其所屬的枚舉類型的名字。此外,枚舉成員間仍可以進行數值式的比較,但不能夠隱式轉換為int型。事實上,如果要將強類型枚舉轉化為其他類型,必須進行顯式轉換。
強類型制止enum成員和int之間的轉換,使得枚舉更加符合"枚舉"的本來意義,即對同類進行列舉的一個集合,而定義其與數值間的關聯使之能夠默認擁有一種對成員排列的機制。而制止成員名字輸出則進一步避免了名字空間沖突的問題。Type和Category都是POD類型,不會像class封裝版本一樣被編譯器視為結構體,書寫也很簡便。在擁有類型安全和強作用域兩重優點的情況下,幾乎沒有任何額外的開銷。
此外,由于可以指定底層基于的基本類型,我們可以避免編譯器不同而帶來的不可移植性。此外,設置較小的基本類型也可以節省內存空間。
#include<iostream>
using namespace std;
enum class C:char{
C1=1,C2=2
};
// 強制型枚舉型,char 類型
enum class D:unsigned int{
D1=1,D2=2,Dbig=0xFFFFFFF0U
};
// 強制型枚舉型,unsigned int 類型(因為Int 型是4個字節,所以在16進制的情況下,最多8位)。
// 這里強制型枚舉型的長度取決于最長的那個值的長度,也取決于顯示定義的枚舉的基本類型。
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;
}
我們為強類型枚舉C指定底層基本類型為char,因為我們只有C1、C2兩個值較小的成員,一個char足以保存所有的枚舉類型。而對于強類型枚舉D,我們指定基本類型為unsigned int, 則所有編譯器都會使用無符號的unsigned int 來保存該枚舉。故各個編譯器都能保證一致的輸出。
在新標準C++11中,原有枚舉類型的底層類型在默認情況下,仍然由編譯器來具體指定實現。但也可以跟強類型枚舉一樣,都是枚舉名稱后面加上":type", 其中type可以是除wchar_t以外的任何整型。比如:
enum Type:char{General,Light,Medium,Heavy};
在C++11中也是一個合法的enum聲明。第二個擴展則是作用域的。在C++11中,枚舉成員的名字除了會自動輸出到父作用域,也可以在枚舉類型定義的作用域內有效。
enum Type{General, Light, Medium, Heavy};
Type t1=General; // 合法
Type t2=Type:: General; //合法
此外,我們在聲明強類型枚舉的時候,也可以使用關鍵字enum struct。事實上 enum struct 和 enum class 在語法上沒有任何區別(enum class的成員沒有公私之分,也不會使用模板來支持泛化的聲明)。
而對于匿名的enum class,由于enum class是強類型作用域的,故匿名的enum class很可能什么都做不了
enum class{ General, Light, Medium,Heavy} weapon;
int main(){
weapon=General;//無法通過編譯
bool b=(weapon==weapon:: General);//無法編譯通過
return 0;
}
我們聲明了一個匿名的enum class實例weapon,卻無法對其設置值或者比較其值(這和匿名struct是不一樣的)。事實上,使用enum class的時候,應該總是為enum class提供一個名字。
匿名 enum class 和 匿名 struct 的區別
匿名 struct 的變量可以訪問整個 struct 的變量的信息。
堆內存管理:智能指針與垃圾回收
顯式內存管理
程序員在處理現實生活的C/C++程序的時候,會遇到程序運行時突然退出,或占用的內容越來越多,最后不得不定期重啟。這些問題可以追溯到C/C++中的顯式堆內存管理上。通常情況下,這些癥狀都是由于
程序沒有正確處理堆內存的分配與釋放造成的,從語言層面來講,我們可以將其歸納為以下的一些問題。
- 野指針:一些內存單元已被釋放,之前指向它的指針卻還在被使用。這些內存有可能被運行時系統重新分配給程序使用,從而導致了無法預測的錯誤。
- 重復釋放:程序試圖去釋放已經被釋放過的內存單元,或者釋放已經被重新分配過的內存單元,就會導致重復釋放錯誤。通常重復釋放內存會導致C/C++運行時系統打印出大量錯誤及診斷信息
- 內存泄露:不再需要使用的內存單元如果沒有被釋放就會導致內存泄露。如果程序不斷地重復進行這類操作,將會導致內存占用劇增。
雖然顯式的管理內存在性能上有一定的優勢,但也被廣泛地認為是容易出錯的。隨著多線程程序的出現和廣泛使用,內存管理不佳的情況還可能會變得更加嚴重。因此,很多程序員也認為編程語言應該提供更好的機制,讓程序員擺脫內存管理的細節。在C++中,一個這樣的機制就是標準庫中的智能指針。
在C++11新標準中,智能指針被進行了改進,以更加適應實際的應用需求。而進一步地,標準庫還提供了所謂 "最小垃圾回收" 的支持。
C++11的智能指針
在C++98中,智能指針通過一個模板類型"auto_ptr"來實現。auto_ptr以對象的方式管理堆分配的內存,并在適當的時間(比如析構),釋放所獲得的堆內存。這種堆內存管理的方式只需要程序員將new操作返回的指針作為auto_ptr的初始值即可,程序員不用再顯式地調用delete。
比如:auto_ptr(new int)
。但是在一定程度上避免了堆內存忘記釋放而造成的問題。不過auto_ptr有一些缺點(拷貝時返回一個左值,不能調用delete[]等),所以在C++11標準中改用unique_ptr、shared_ptr及weak_ptr等智能指針來自動回收堆分配的對象。
下面是一個C++11中使用新的智能指針的簡單例子:
#include<iostream>
#include<memory>
using namespace std;
int main(){
unique_ptr<int> up1(new int(11));//無法復制的unique_ptr
//unique_ptr<int> up2=up1;//不能通過編譯。
cout<<*up1<<endl;//11
unique_ptr<int> up3=move(up1) ;//現在p3是數據唯一的unique_ptr智能指針
cout<<*up3<<endl;//11
//cout<<*up1<<endl;//運行時錯誤
up3.reset() ; //顯式釋放內存
up1.reset() ; //不會導致運行時錯誤
//cout<<*up3<<endl;//運行時錯誤
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來自動地釋放堆對象的內存。由于每個智能指針都重載*運算符,用戶可以使用*up1這樣的方式來訪問所分配的堆內存。而在該指針析構或者調用reset成員的時候,智能指針都可能釋放其擁有的堆內存。從作用上來講,unique_ptr和shared_ptr還是和以前的auto_ptr保持了一致。
unique_ptr和shared_ptr 在對所占內存的共享上還是有一定區別的。
直觀來看,unique_ptr形如其名地,與所指對象的內存綁定緊密,不能與其他unique_ptr類型的指針共享所指對象的內存。比如,本例中的unique_ptr<int> up2=up1;不能通過編譯,是因為每個unique_ptr都是唯一地"擁有"所指向的對象內存,由于up1唯一地占用了new分配的堆內存,所以up2無法共享其"使用權"。事實上,這種"所有權"僅能夠通過標準庫的Move函數來轉移。我們可以看到代碼中up3的定義,unique_ptr<int> up3=move(up1); 一旦"所有權"轉移成功了,原來的unique_ptr指針就是去了對象內存的所有權。此時再使用已經"失勢"的unique_ptr,就會導致運行時的錯誤。本例中的后段使用*up1就是很好的例子。
而unique_ptr則是一個刪除了拷貝構造函數、保留了移動構造函數的指針封裝類型。程序員盡可以使用右值對unique_ptr對象進行構造,而且一旦構造成功,右值對象中的指針即被"竊取",因此該右值對象即刻失去了對指針的"所有權"。
而shared_ptr同樣形如其名,允許多個該智能指針共享地"擁有"同一堆分配對象的內存。與unique_ptr不同的是,由于在實現上采用了引用計數,所以一旦一個shared_ptr指針放棄了"所有權"(失效),其他的shared_ptr對對象內存的引用并不會收到影響。只有引用計數歸零的時候,share_ptr才會真正釋放所占用的堆內存的空間。
在C++11標準中,還有weak_ptr這個類模板。weak_ptr的使用更為復雜一點,它可以指向shared_ptr指針指向的對象內存,卻并不擁有該內存。而使用weak_ptr成員lock,則可返回其指向內存的一個shared_ptr對象,且在所指對象內存已經無效時,返回指針空值。
#include<iostream>
#include<memory>
using namespace std;
void Check(weak_ptr<int> &wp) {
shared_ptr<int> sp=wp.lock();//轉換為shared_ptr<int> 但是在函數結束的時候,指針的生存周期就已經結束了。
//在sp1及sp2都有效的時候,調用wp的lock函數,將返回一個有效的shared_ptr對象供使用,如果沒有了,通過sp!=nullptr即可進行判斷即可
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>所指對象
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;
}
我們定義了一個共享對象內存的兩個shared_ptr--sp1及sp2。而weak_ptr wp同樣指向該對象內存。可以看到,在sp1及sp2都有效的時候,我們調用wp的lock函數,將返回一個有效的shared_ptr對象供使用,于是Check函數會輸出以下內容:still 22
此后我們分別調用了sp1及sp2的reset函數,這回導致對唯一堆內存對象的引用計數降至0.而一旦引用計數歸0, shared_ptr<int>就會釋放堆內存空間,使之失效。此時,我們再調用weak_ptr的lock函數時,則返回一個指針空值nullptr。這時Check函數則會打印出: pointer is invalid 整個過程中, 只有shared_ptr參與了引用計數,而weak_ptr 沒有影響其指向的內存的引用計數。 因此可以驗證 shared_ptr指針的有效性。
程序員用unique_ptr代替以前使用auto_ptr的代碼就可以使用C++11中的智能指針。而shared_ptr及weak_ptr則可用在用戶需要引用計數的地方。
垃圾回收的分類
我們把之前使用過,現在不再使用或沒有任何指針再指向的內存空間就被稱為“垃圾”。而將這些“垃圾”收集起來以便再次利用的機制,就被稱為“垃圾回收”。
垃圾回收的方式雖多,但主要可以分為兩大類:
1、基于引用計數(reference counting garbage collector) 的垃圾回收器
簡單地說,引用計數主要是使用系統記錄 對象被引用(引用、指針)的次數。當對象被引用的次數變為0時,該對象即可被視作 “垃圾” 而回收。使用引用計數做垃圾回收的算法的一個優點是實現很簡單,與其他垃圾回收算法相比,該方法不會造成程序暫停,因為計數的增減與對象的使用是緊密結合的。此外,引用計數也不會對系統的緩存或者交換空間造成沖擊,因此被認為“副作用”較小。但是這種方法比較難處理“環形引用”問題,此外由于計數帶來的額外開銷也不小,在實用上也有一定的限制。
2、基于跟蹤處理的垃圾回收器
相比于引用計數,跟蹤處理的垃圾回收機制被更為廣泛地應用。其基本方法是產生跟蹤對象的關系圖,然后進行垃圾回收。使用跟蹤方式的垃圾回收算法主要有以下幾種:
(1)標記-清除(Mark-Sweep)
這個算法可以分為兩個過程。首先該算法將程序中正在使用的對象視為 “根對象”,從根對象開始查找它們所引用的堆空間,并在這些堆空間上做標記。當標記結束后,所有被標記的對象就是可達對象(Reachable Object) 或活對象(Live Object),而沒有被標記的對象就被認為是垃圾,在第二步的清掃階段會被回收掉。這種方法的特點是活的對象不會被移動,但是其存在會出現大量的 內存碎片 的問題。
(2)標記-整理(Mark-Compact)----------這種方法感覺要好用很多
這個算法標記的方法和標記-清除方法一樣,但是標記完之后,不再遍歷所有對象清掃垃圾了,而是將活的對象向“左”靠齊,這就解決了內存碎片的問題。特點就是移動活的對象,因此相對應的,程序中所有對堆內存的引用都必須更新。
(3)標記-拷貝(Mark-Copy)
這種算法將堆空間分為兩個部分:From 和 To. 剛開始系統只從From的堆空間里面分配內存,當From分配滿的時候系統就開始垃圾回收:從From堆空間找出活的對象,拷貝到To的堆空間里。這樣一來,From的堆空間里面就全剩下垃圾了。而對象被拷貝到To里之后,在To里是緊湊排列的。接下來是需要將From和To交換一下角色(這里是如何進行角色交換的),接著從新的From里面開始分配。標記-拷貝算法的一個問題是堆的利用率只有一半,而且也需要移動活的對象。此外,從某種意義上講,這種算法其實是標記-整理算法的另一種實現而已。
C++與垃圾回收
在C++11中,智能指針等可以支持引用計數。不過由于引用計數并不能有效解決形如“環形引用”等問題,其使用會受到一些限制。而且基于一些其他的原因,比如多線程程序等而引入的內存管理上的困難,程序員可能也會需要垃圾回收。--------(這里教的是如何進行手動的垃圾回收)。
一些第三方的C/C++庫已經支持標記-清除方法的垃圾回收,比如一個比較著名的C/C++垃圾回收庫 ———— Boehm. 該垃圾回收器需要程序員使用庫中的 堆內存分配函數 (這個庫中的堆內存分配函數是什么?) 顯式地替代malloc,繼而將堆內存的管理交給垃圾回收器來完成垃圾回收。不過由于C/C++中指針類型的使用非常靈活,這樣的庫在實際使用中會有一些限制,可移植性也不好。
簡單來說,垃圾回收的不安全性源自于C/C++語言對指針的“放縱”,即允許過分靈活的使用。
int main(){
int*p=new int;
p+=10; //移動指針,可能導致垃圾回收器
p-=10; //回收原來指向的內存
*p=10; //再次使用原本相同的指針可能無效
}
(回收的時候,如果指針指向了其他的地方,那么系統將會認為指針曾指向的內存不再使用。)
通過指針的自加和自減能夠使程序員輕松地找到“下一個” 同樣的對象(實際是一個迭代器的概念)。不過對于垃圾回收來說,一旦p指向了別的地址,則可認為p曾指向的內存不再使用。垃圾回收器可以據此對其進行回收。這對之后p的使用(*p=10)帶來的后果是災難性的。
int main(){
int*p = new int;
int*q = (int*) (reinterpret_cast<long long>(p)^2012); //q隱藏了p
//做一些工作,垃圾回收器可能已經回收了p指向對象
q=(int*)(reinterpret_cast<long long>(q)^2012); //這里的q==p
*q=10;
}//編譯選項:g++5-2-4.cpp
(reinterpret_cast<intptr_t>(p) 返回的是一個整型)
補充:reinterpret_cast 運算符是用來處理無關類型之間的轉換;它會產生一個新的值,這個值會有與原始參數(expressoin)有完全相同的比特位。我很好奇,這個地方怎么用
reinterpret_cast<long long>(p)^2012 這地方怎么做隱藏的。因為用了^2012,然后再異或一次,就可以將參數解放出來。
但是在這個代碼里面,用指針q隱藏了指針p。而后,又用可逆的異或運算將p “恢復”了出來。在main函數中,p實際所指向的內存都是有效地,但由于該指針被隱藏了,垃圾回收器可以早早地將p指向的對象回收掉。同樣,語句*p=10的后果也是災難性的。
指針的靈活使用可能是C/C++的一大優勢,而對于垃圾回收來說,卻會帶來很大的困擾。被隱藏的指針會導致編譯器在分析指針的可達性(生命周期)時出錯。而即使編譯器開發出了隱藏指針分析的手段,其帶來的編譯開銷也不會讓程序員對編譯時間的顯著增長視而不見。
C++11和垃圾回收的解決方案是新接口,就是讓程序員利用這樣的接口來提供編譯器代碼中存在指針不安全的區域。
C++11與最小垃圾回收支持
C++11新標準里面為了做到最小的垃圾回收支持,對“安全”的指針進行了定義,或者使用C++11中的術語說,安全派生的指針。是指向由new分配的對象或其子對象的指針。安全派生指針的操作包括:
在解引用基礎上的引用,比如:&*p.
定義明確的指針操作,比如:p+1. //這里指針的長度應該是和指針的類別掛鉤的。
定義明確的指針轉換,比如:static_cast<void*>(p).
指針和整型之間的reinterpret_cast, 比如:reinterpret_cast<intptr_t>(p).
(“解引用”,我到覺得可以從另一個角度理解,"*" 的作用是引用指針指向的變量,引用其實就是引用該變量的地址,“解”就是把該地址對應的東西解開,解出來,就像打開一個包裹一樣,那就是該變量的值了,所以稱為“解引用”)
(注意 intptr_t是C++11中一個可選擇實現的類型,其長度等于平臺上指針的長度(通過decltype聲明)
在原來的代碼里面 reinterpret_cast<long long>(p) 是合法的安全派生操作,而轉化后的指針再進行異或操作:
reinterpret_cast<long long>(p)2012之后,指針就不再是安全派生的了,這是因為異或操作()不是一個安全派生操作。同理:
reinterpret_cast<long long>(q)^2012也不是安全派生指針。因此,根據定義,在使用內存回收器的情況下,*p=10的行為是不確定的。
C++11的規則中,最小垃圾回收支持是基于安全派生指針這個概念的。程序員可以通過
get_pointer_safety 函數查詢來確認編譯器是否支持這個特性。原型如下:
pointer_safety get_pointer_safety() noexcept
其返回一個pointer_safety類型的值。如果該值為 pointer_safety:: strict, 則表明編譯器支持最小垃圾回收及安全派生指針等相關概念,如果該值為
pointer_safety::relax 或是 pointer_safety:: preferred, 則表明編譯器并不支持,基本上和沒有垃圾回收的 C 和 C++98 一樣。
------------ 這說明,通過這種方式可以檢測是否有最小垃圾回收機制 ----------------
如果程序員代碼中出現了指針不安全使用的情況,C++11允許程序員通過一些API來通知垃圾回收器不得回收該內存。C++11的最小垃圾回收支持使用了垃圾回收的術語,即需聲明該內存為“可到達”的。
void declare_reachable(void*p);
template<class T>T*undeclare_reachable(T*p) noexcept;
declare_reachable () 顯示地通知垃圾回收器某一個對象應被認為可達的,即使它的所有指針都對回收器不可見。undeclare_reachable() 則可以取消這種可達聲明。
#include <memory>
using namespace std;
int main(){
int* p=new int;
declare_reachable(p); //在p被隱藏之前聲明為可達的
int*q=(int*)((long long)p^2012);
// 解除可達聲明
q = undeclare_reachable<int>((int*)((long long)q^2012));
*q=10;
return 0;
}
p指針被不安全派生(隱藏)之內使用declare_reachable聲明其實可達的。這樣一來,它會被垃圾回收器忽略而不會被回收。而在我們通過可逆的異或運算使得q指針指向p所指對象時,我們則使用了undeclare_reachable 來取消可達聲明。注意 underclare_reachable 不是通知垃圾回收器 p 所指對象已經可以回收。實際上,declare_reachable 和 undeclare_reachable 只是確立了一個代碼范圍,即在兩者之間的代碼運行中,p所指對象不會被垃圾回收器所回收。
declare_reachable 只需要傳入一個簡單的 void指針,但 undeclare_reachable 卻被設計為一個函數模板。目的是為了返回合適類型以供程序使用。而垃圾回收器本來就知道指針所指向的內存的大小,因此declare_reachable傳入void指針就已經足夠了。
有的時候程序員會選擇在一大片連續的堆內存上進行指針式操作,為了讓垃圾回收器不關心該區域,也可以使用 declare_no_pointers及undeclare_no_pointers函數來告訴垃圾回收器該內存區域不存在有效的指針。
void declare_no_pointers(char*p,size_t n) noexcept;
void underclare_no_pointers(char*p,size_t n) noexcept;
不過指定的是從p開始的連續n的內存。
C++11標準中對指針的垃圾回收支持僅限于系統提供的new操作符分配的內存,而malloc分配的內存則會被認為總是可達的,即無論何時垃圾回收器都不予回收。因此使用malloc等的較老代碼的堆內存還是必須由程序員自己控制。