深入理解C++11核心编程(五)--提高类型安全
强类型枚举
枚举:分门别类与数值的名字
枚举类型是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等的较老代码的堆内存还是必须由程序员自己控制。