执行期语意学(Runtime Semantics)
有下面的代码
if(yy==xx.getValue())...........
其中xx 和yy定义为:
X xx;
Y yy;
class Y定义为:
class Y
{
public:
Y();
~Y();
bool operator==(const Y&) const;
//...........
};
Class X的定义为:
class X
{
public:
X();
~X();
operator Y()const;
X getValue();
//...........
};
上面第一行代码的第一转换为:
//resolution of intended operator
if(yy.operator==(xx.getValue()))
Y的equality(等号)运算符需要一个类型为Y的参数,然后getValue()传回的却是一个类型为X的object。本例中,X提供了一个conversion运算符,把一个X object转换为一个Y object。它必须施行于getValue()的返回值身上。
所以需要第二次转换:
//conversion of getVlaue()'s return value
if(yy.operator==(xx.getValue().operator Y())
以上的两次转换都是编译器根据class的隐含语意代替我们程序员所做的操作。
接下来我们利用一个临时对象来放置函数调用所传回的值:
- 产生一个临时的class X object,放置getValue()的返回值:
X temp1=xx.getValue();
- 产生一个临时的class Y obejct ,放置operator Y()的返回值:
X temp2=temp1.operator Y();
- 产生一个临时的int object,放置equality(等号)运算符的返回值:
int temp3=yy.operator==(temp2);
最后,适当的destructor将被施行于每一个临时性的class object身上,所以有伪代码:
//c++ 伪代码
//以下是条件语句 if(yy==xx.getValue())....的转换
{
X temp1=xx.getValue();
Y temp2=temp2.operator Y();
int temp3=yy.operator==(temp2);
if(temp3)........
temp2.Y::~Y();
temp1.X::~X();
}
6.1 对象的构造和解构(Object Construction and Destruction)
一般而言,Constructor和destructor的安插都如你所预期:
//c++ 伪代码
{
Point point;
//point.Point::Point() 一般而言会被安插在这里 (声明 之后)
.....
//point.Point::~Point() 一般而言会被安插在这里(结束之前)
}
destructor必须放在每一个离开点(当时object还存活);
全局对象
有以下程序片段:
Matrix identity;
main()
{
//identity 必须在此处被初始化
Matrix m1=identity;
...
return 0;
}
c++保证,一定会在main()函数中第一次用到identity之前,把identity构造出来,而在main()函数结束之前把identity摧毁掉。
像identity这样的global object如果有constructor和destructor,我们需要为它提供静态的初始化操作和内存释放操作。
c++程序中所有的global objects 都被放置在程序的data segment中,如果明确指定给他一个值,object将以该值为初值。否则object所配置到的内存内容为0。
cfront编译提供一个可移植但成本颇高的静态初始化方法(以及内存释放)方法,称为munch。
这些munch策略称为:
- 1、为每一个需要静态初始化的档案产生一个_sti()函数,内带必要的constructor调用操作或inline expansions。
- 2、类似情况,在每一个需要静态的内存释放操作的文件中,产生一个__std()函数,内带必要的destructor调用操作,或是其inline expansions。
- 3、提供一组runtime library “munch”函数:一个_main()函数(用以调用可执行文件中的所有_sti函数),以及一个exit()函数(以类似方式调用所有的_std函数)。
支持“nonclass objects的静态初始化”,在某种程度上是支持virtual base classes的一个副产品。以一个derived class的pointer或reference来存取virtual base class subobject,是一种nonconstant expression,必须在执行期才能加以评估求值。
局部静态对象
假设有以下程序片段:
const Matrix &
indenity()
{
static Matrix mat_idnetity;
//.......
return mat_identity;
}
local static class object保证了怎样的语意呢?
- mat_identity的constructor必须只能施行一次,虽然上述代码可能会被调用多次。
- mat_identity的destructor必须只能施行一次,虽然上述代码可能会被调用多次。
对象数组
假设有下面数组的定义:
Point knots[10];
如果Point既没有定义一个constructor也没有定义一个destructor,那么我们需要配置足够的内存以储存10个连续的Point元素。
然而Point的确定义了一个default destructor,所以这个destructor必须轮流施行于每一个元素之上。一般而言这是经由一个或多个runtime library函数达成。在cfont编译器中,我们使用一个被命名为vec_new()的函数,产生出以class objects构造而成的数组。而有的编译器则提供两个函数:一个用来处理“没有virtual base class”的class,另一个用来处理“内带virtual base class”的class。后一个函数通常被称为vec_vnew().而函数的类型通常如下:
void *
vec_new
{
void *array, //数组起始地址
size_t elem_size, //每一个class object的大小
int elem_count, //数组中元素数目
void (*constructor) (void *),
void (*destructor)(void *,char)
}
其中的constructor和destructor参数是这个class的default constructor和default destructor的函数指针。参数array带有的若不是具名数组的地址,就是0。如果是0,那么数据将经由应用程序的new运算符,被动态配置与heap中。(Sun编译器将“由class objects所组成的具名数组”和“动态配置而来的数组”的处理操作分为两个library函数:_vector_new2和_vec_con,它们各自拥有一个virtual base class函数实体)。参数elem_size表示数组中的元素数目。在vec_new()中,constructor施行于elem_count个元素之上。
在vec_new()中,constructor施行于elem_count个元素之上,下面是编译器可能针对我们的10个Point元素所做的vec_new()调用操作:
Point knots[10];
vec_new (&knots,sizeof(Point),10,&Point::Point,0);
如果Point也定义了一个destructor,当knots的生命结束时,该destructor也必须施行于那10个Point元素身上。这是经由一个类似的vec_delete()(或是一个vec_delete()------如果classes拥有virtual base classes的话)的runtime library函数完成,其函数类型如下:
void*
vec_delete(
void *array, //数组起始地址
size_t elem_size, //每一个class object的大小
int elem_count , //数组中的元素数目
void (*destructor)(void*,char)
)
有的编译器会另外增加一些参数,用以传递其他数值,以便能够有条件地导引vec_delete()的逻辑。在vec_delete()中,destructor被施行于elem_count个元素身上。
如果提供一个或多个明显初值给一个由class objects组成的数组,
Point knots[10]={
Point(),
Point(1.0,1.0,0.5),
-1.0
};
对于那些明显获得初值的元素,vec_new()不再有必要,对于那些尚未被初始化的元素,vec_new()的施行方式就像面对“由class elements组成的数组,而该数组没有explicit initialization list ”一样,所以上面的定义可能会被转化为:
Point knots[10];
//c++ 伪代码
// 明确地初始化前3个元素
Point::Point(&knots[0]);
Point::Point(&knots[1],1.0,1.0,0.5);
Point::Point(&knots[2],-1.0,0.0,0.0,);
//以vec_new初始化后7个元素
vec_new(&knots+3,sizeof(Point),7,&Point::Point,0);
new 和delete运算符
运算符new的使用,看起来似乎是一个单一运算,像这样:
int *pi=new int(5);
但事实上它是由两个步骤完成:
- 1、通过适当的new运算符函数实体,配置所需的内存:
// 调用函数库中的new 运算符
int *pi=__new(sizeof(int));
- 2、给配置得来的对象设立初值:
*pi=5;
更加需要注意的是,初始化操作应该在内存配置成功后才执行.
delete运算符的情况类似:
delete pi;
如果pi的值为0(为空),c++会要求delete运算符不要进行任何操作。
所以我们需要对此加上一层保护膜:
if(pi!=0)
__delete(pi);
pi所指对象之生命会因delete而结束,所以后继任何对pi的参考操作就不能再保证有良好的行为,并因此会被视为是一种不好的程序风格。然而,把pi继续当做一个指针来使用,仍然是可以的。
//ok :pi仍然指向合法空间
// 甚至即使储存于其中的object已经不合法
if(pi==sentinel)........
使用指针pi和使用pi所指对象,其差别在于哪一个的生命已经结束了。虽然该地址上的对象不再合法,但地址本身却仍然代表一个合法的程序空间。因此pi能够被继续使用,但只能在受限的情况下。
以constructor来配置一个class object ,情况类似:
Point3d *origin=new Point3d();
上面的代码会被转换为:
Point3d *origin;
// c++伪代码
if(origin=__new(sizeof(Point3d)))
origin=Point3d::Point3d(origin);
如果出现excepting handling(异常处理),那么转换结果会更加复杂:
//c++伪代码
if(origin==__new(sizeof(Point3d))){
try{
origin=Point3d::Point3d(origin);
}
catch(....){
//调用delete library function 以
//释放因new 而配置的内存
__delete(origin);
//将原来的exception上传
throw;
}
}
在这里,如果以new运算符配置object,而其constructor丢出一个exception,配置得来的内存就会被释放掉,然后exception在被丢出去(上传)。
Destructor的应用极为类似:
delete origin;
就会变成:
if(origin!=0)
//c++伪代码
Point3d::~Point3d(origin);
__delete(origin);
一般的library对于new运算符的实现操作都很直接了当,但有两个精巧之处:
extern void*
operator new(size_t size)
{
if(size==0)
size=1;
void *last_alloc;
while(!(last_alloc=malloc(size)))
{
if(__new_handler)
(*__new_handler){};
else
return 0;
}
return last_alloc;
}
语言要求每一次对new的调用都必须传回一个独一无二的指针,解决该问题的传统方法是传回一个指针,指向一个默认为1-byte的内存区块。这个实现技术的另一个有趣之处是,它允许使用者提供一个属于自己的_new_handler()函数,所以才每一次循环都调用_new_handler()之故。
new运算符实际上总是以标准的C malloc()完成的,同样delete运算符也是总以标准的 C free()完成:
extern void
operator delete(void *ptr)
{
if(ptr)
free((char*)ptr);
}
针对数组的new语意
当我们这么写:
int *p_array=new int[5];
时,vec_new()不会真正被调用,因为它的主要功能是把default constructor施行于class object所组成的数组的每一个元素身上。倒是new 运算符函数会被调用:
int *p_array=(int *)__new(5*sizeof(int));
相同情况下,如果是:
// struct simple_aggr{float f1,f2};
simple_aggr *p_aggr=new simple_aggr[5];
vec_new同样也不会被调用,因为simple_aggr并没有定义一个constructor或destructor,所以配置数组以及清除p_aggr数组的操作,只是单纯地获得内存和释放内存而已。这些操作由new和delete运算符来完成就绰绰有余。
然而如果class定义有一个default constructor,某些版本的vec_new()就会被调用,配置并构造class objects所组成的数组。
Point3d *p_array=new Point3d[10];
通常会被编译为:
Point3d *p_array;
p_array=vec_new(0,sizeof(Point3d),10,&Point3d::Point3d,&Point3d::~Point3d);
只有已经构造妥当的元素才需要destructor的施行,因为他们的内存已经被配置出来了,vec_new()有责任在exception发生的时候把那些内存释放掉。
当我们写下:
int array_size=10;
Point3d *p_array=new Point3d[array_size];
那么当我们需要删除数组时,可以这样写:
delete[ ] p_array;
只有在中括号出现时,编译器才寻找数组的维度,否则它便假设只有单独一个objects要被删除,如果没有提供中括号:
delete p_array;
那么只有第一个元素会被解构,其他的元素仍然存在。
那么编译器如何记录元素数目呢?一个明显的方法就是为vec_new()所传回的每一个内存区块配置一个额外的word,然后把元素数目包藏在那个word之中。通过这种被包藏的数值被称为所谓的cookie。cookie策略有一个普遍引起忧虑的话题就是,如果一个坏指针应该被交给delete_vec(),那么取出来的cookie自然是不合法的。一个不合法的元素数目和一个错误的起始地址,会导致destructor以非预期的次数被施行于一段非预期的区域。
如果我们配置一个数组,内带有10个Point3d objects,我们会预期Point和Point3d的constructor被调用各10次,每次作用于数组的一个元素:
//完全不是一个好主意
Point *ptr=new Point3d[10];
而当我们delete“由ptr所指向的10个Point3d元素时”,很明显的是,我们需要虚拟机制的帮助,以获得预期的Point destructor和Point3d destructor,每一次作用于数组中的每一个元素:
// 超出预期,只有Point::~Point被调用。。。
delete[ ] ptr;
施行于数组上的destructor,是根据交给vec_delete()函数之“被删除的指针类型的destructor”-----本例中正式Point destructor,这很明显并非我们希望的。此外,每一个元素的大小也一并被传递过去,这就是vec_delete()如何迭代走过每一个数组元素的方式。本例中被传递过去的是Point class object的大小而不是Point3d class object的大小。
我们应该避免以一个base class 指针指向一个derived class objects所组成的数组------如果derived class object 比其base 大的话,那么就只能程序员手动来处理:
for (int ix=0;ix<elem_count;++ix)
{
Point3d *p=&((Point3d*)ptr)[ix];
delete p;
}
程序员必须迭代走过整个数组,把delete运算符实施于每一个元素身上,调用操作将是virtual ,所以,Point3d和Point的destructor都会施行于数组中的每一个objects身上。
Placement Operator new的语意
有一个预先定义好的重载的new运算符,称为placement operator new,它需要第二个参数,类型为void*,调用方式如下:
Point2w *ptw=new (arena) Point2w;
其中arena指向内存中的一个区块,用以放置新产生出来的Point2w object,这个预先定义好的placement operator new的实现方法简直出乎意料的平凡,它只要将“获得的指针”所指的地址传回即可:
void *
operator new(size_t ,void *p)
{
return p;
}
如果只是传回其第二个参数,那么它的价值?
- 1、什么是placement new operator能够有效运行的另一半部扩充(而且是“arena的明确指定操作(explicit assignment)”所没有提供的)?
- 2、什么是arena指针的真正类型?该类型暗示了什么?
placement new operator所扩充的另一半边是将Point2w constructor自动施于arena所指的地址上:
//c++ 伪代码
Point2w *ptw=(Point2w*) arena;
if(ptw!=0)
ptw->Point2w::Point2w();
这正是使placement operator new威力如此强大的原因,这一份码决定objects被放置在哪里,编译系统保证object的constructor会施行于其上。
临时性对象
如果我们有一个函数,形式如下:
T operator+(const T&,const T&);
以及两个T objects ,a和b,那么:
a+b;
可能会导致一个临时性对象,以放置传回的对象。是否产生临时性对象是根据编译器和操作发生时的上下文关系。
例如
T a,b;
T c=a+b;
有三种方式:
- 1、编译器会产生一个临时性对象,放置a+b的结果,然后再使用T的copy constructor,把该临时性对象当做c的初始值。
- 2、可能会直接以拷贝构造的方式,将a+b的值放到c中,于是不需要临时性对象,以及对其constructor和destructor的调用。
- 3、视operator+()的定义而定,named return value优化也可能实施起来,这将导致直接在上述c对象中求表达式结果,避免执行copy constructor和具名对象的destructor。
几乎所有的c++编译器保证任何表达式,如果有这种形式:
T c=a+b;
而其中的加法运算符被定义为:
T operator+(const T&,const T&);
或
T T::operator(const T&);
那么实现时根本不产生一个临时性对象。
而意义相当的assignment叙述句:
c=a+b;
不能够忽略临时性对象。它会导致下面的结果:
//c++伪代码
// T temp =a+b;
T temp;
temp.operator(a,b); (1)
// c=temp
c.operator=(temp);
temp.T::~T();
标记为(1)的那一行,未构造的临时对象被赋值给operator+(),这意思是要不是“表达式的结果被copy constructed 至临时对象中”,就是“以临时对象取代NRV”。
不管哪一种情况,直接传递c到运算符函数中是有问题的。由于运算符函数并不为其外加参数调用一个destructor,所以必须在此调用之前先调用destructor。
所以初始化操作:
T c=a+b;
总是比下面的操作更有效率地被编译器转换:
c=a+b;