开篇
- {P239}
X xx;
Y yy;
{
if (yy == xx.getValue())
…
}
其实被扩展成:
{
X temp1 = xx.getValue();
Y temp2 = temp1.operator Y();
int temp3 = yy.operator==(temp2);
If (temp3)
…
temp2.Y::~Y();
temp1.X::~X();
}
- 不容易从源代码看出表达式的复杂度。
对象的构造和析构
- {P241} 一般而言,我们将object尽可能放置在使用它的那个程序区段附近,这样可以节省非必要的对象的构造和析构成本
全局对象
-
C++程序中所有的global objects都被放置在程序的data segment中,如果不明确指定初值,object所配置的内存内容将为0(C并不自动设定初值)。如果global object有ctor和dtor的话,我们说它需要静态的初始化和内存释放操作。
-
{P243}
munch
策略(全局变量静态初始化):- 为每一个需要静态初始化的
文件
产生一个__sti()
函数(sti: static initialization),内含必要的constructor调用操作或者inline expansions。 - 为每一个需要静态的内存释放操作的
文件
中,产生__std()
函数(std: static deallocation),内含必要的constructor调用操作或者inline expansions。 - 提供一组running library”munch”函数:一个_main()函数(用于调用所有的__sti()函数),以及一个exit()函数(用于调用所有的__std()函数)。
- 为每一个需要静态初始化的
-
{P245} 在特定平台上的C++编译器开始出现,更有效率的方法也就有可能随之出现。eg: system V的ELF被扩充以增加支持
.init
和.fini
两个sections,这两个sections内含对象所需的信息,分别对应于静态初始化和释放操作。编译器特定的startup函数(通常名为crt0.o
)会完成这个平台特定的支持(分别针对静态初始化和释放操作的支持)。
局部静态对象
- {P249} 第一次进函数时才需要初始化,所以编译其间无法知道它们的顺序和集合。一般在这个函数中判断一个静态临时性对象。eg:
static struct Matrix *__0__F3 = 0;
Foo…
{
static struct Matrix __1mat_indentity;
__0__F3 ? 0 : (__ct__6MatrixFv(&__1mat_indentity), __0__F3 = (&__1mat_indentity));
}
最后一个统一的dtor函数来判断和回收它们。
Char __std__stat_0_c_j(){
…
__0__F3? __dt__6MatrixFv(__0__F3, 2) : 0;
…
}
- 新的规则要求编译单位中的局部静态对象必须被摧毁,而且以构造相反的顺序摧毁。
对象数组
{P250} 在cfront中,我们使用一个名为vec_new
的函数,以产生以class objects构造而成的数组。一些新近编译器,支持含有虚函数的情况,使用vec_vnew()。
void vec_new(
void* array, //数组起始地址
size_t elem_size, // 每一个class object大小
int elem_count, // 数组中的元素个数
void (*constructor)(void*), // 零个形参的默认构造函数指针
void (*destructor)(void*, char) //析构函数指针
)
Point knots[10];
vec_new(&knots, sizeof(Point), 10, Point::Point, 0);
- 同理,析构数组的函数有vec_delete(如果含有虚函数,使用vec_vdelete)。
void vec_delete(
void* array, //数组起始地址
size_t elem_size, //每一个class object大小
int elem_count, //数组中的元素个数
void (*destructor)(void*, char) //析构函数指针
)
Default ctors和数组
- {P252} 如果一个类的构造函数有一个或一个以上的默认参数值,使用stub ctor来过度。
class complex {
complex(double = 0.0, double = 0.0);
}
那么当我们写下complex c_array[10];时,编译器最终需要调用:
vec_new(&c_array, sizeof(complex), 10, &complex::complex, 0);
这里的complex::complex是stub ctor,在里面调用用户写的ctor。
complex::complex() {
complex(0.0, 0.0);
}
new 和 delete 运算符
内置类型 {P254}
int *pi = new int(5);
=>
int *pi;
if(pi = __new(sizeof(int))) // 1. 配置所需内存
*pi = 5; // 2. 赋初值
delete pi;
=>
if(pi != 0) __delete(pi);
非内置类型 {P255}
- new的处理
Point3d *origin = new Point3d;
=>
Point3d *origin;
if(origin = __new(sizeof(Point3d)))
origin->Point3d::Point3d(origin);
// 支持异常处理
Point3d *origin;
if(origin = __new(sizeof(Point3d))) {
try {
origin->Point3d::Point3d(origin);
}
catch(...) {
__delete(origin) // 回收内存
throw; // 继续往上抛
}
}
- delete的处理
delete origin;
=>
if(origin != 0) {
Point3d::~Point3d(origin);
__delete(origin);
}
new的实现(请注意,以下版本并未考虑EH):
extern void* operator new(size_t size) {
if(size == 0) // 第一个精妙之处,这里对于size为零特殊处理,
// 返回一个独一无二的指针。
size = 1;
void *last_alloc;
while(!(last_alloc = malloc(size))) {
if(_new_handler)
(*_new_handler)(); // 第二个精妙之处,
// 允许用户提供自己的_new_handler(),
// 每次循环都调用。
else
return 0;
}
return last_alloc;
}
- new运算符实际上总是以标准的C malloc()完成,虽然并没有规定一定得这么做不可。相同情况,delete函数也总是以标准的free()完成:
extern void operator delete(void* ptr) {
if(ptr)
free((char*) ptr);
}
针对数组的new语意 {P257 }
当我们这么写int* p_array = new int[5];
时,vec_new不会真正被调用,因为它的主要功能是将default ctor施加于class object所组成的每个数组元素上。int没有默认构造函数,倒是new运算符函数会被调用:
int* p_array = (int *) __new(5 * sizeof(int));
同理,以下vec_new也不会被调用,因为以下结构没有ctor或dtor。
struct simple_aggr { float f1, f2; }
simple_aggr* p_aggr = new simple_aggr[5];
Placemenet Operator new的语意
- {P263} 有一个预先定义好的重载的new运算符,称为placement operator new。它需要第二个参数,类型为void*:
void* operator new (size_t, void* p)
{
return p;
}
它的效果相当于:
Point2w *ptw = (Point2w*) arena;
if (ptw != 0)
ptw->Point2w::Point2w();
- 有一个轻微的不良行为。
void fooBar() {
Point2w *p2w = new (arena) Point2w;
// ... do it ...
// ... now manipulate a new object
p2w = new (arena) Point2w;
}
- 如果前面在这个arena的类有析构函数,那么需要调用该析构。
调用该析构的方法之一是将该指针delete掉。
但问题来了,这样回将这块空间回收,那么接下来无法继续使用该空间继续去new。正确的方法是显式调用dtor,如下:
p2w->~Point2w();
p2w = new (arena) Point2w();
临时性对象
- {P267} 有时候会产生临时性对象,视编译器的进取性(aggressiveness)以及上述操作发生时的程序语境而定。C++标准允许编译器在这方面有完全的自由度。
- 对于以下的代码,可能有一个临时对象来保存a+b的结果,也可能直接用a+b的结果来拷贝构造c。
T operator+(const T&, const T&);
T a, b;
T c = a + b;
- {P268} 但以下的代码只有细微的差距,但却无法不产生临时对象:
T c;
c = a + b;
等价于:
T c;
T temp;
temp.operator+(a, b);
c.operator=(temp); // 需要调用赋值重载
temp.T::~T();
所以一般来说 T c = a + b;更会有效率
- {P271} 临时对象的被摧毁,应该是对完整表达式(full-expression)求值过程中的最后一个步骤。完整表达式,非正式的说,它是被涵括的表达式中最外围的那个。但它有两个例外:
- 凡是有表达式执行结果的临时性对象,应该保存到object的初始化操作完成为止。
string proNameVersion = !verbose ? 0 : proName + progVersion;
临时对象来存储求和,它需要在整个表达式结束评估后尽快被摧毁。 - 当一个临时对象被一个reference绑定时。如果一个临时对象被绑定于一个reference,对象将残留,直到被初始化之ref的声明结束,或直到临时对象的生命范畴结束。
const String &space = " ";
例外1:
String temp;
operator+ (temp, progName, progVersion);
progNameVersion = temp.String::operator char*();
temp.String::~String();
例外2:
String temp;
temp.String::String(" ");
const String &space = temp;