深度探索c++对象模型第六章笔记

执行期语意学(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;

猜你喜欢

转载自blog.csdn.net/weixin_39116058/article/details/85170047