非原创,在学习
有这样一个简单的案例:
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; // conversion运算符
X getValue();
// ...
};
Y类重载了==运算符,形参是Y,在本例中笔记的是yy == xx.getValue();在X类中,有一个conversion运算符,将X对象转换为Y对象,这使得案例成立。
这里会产生一系列的临时变量。
(1)产生一个临时变量Class X object,放置getValue()的返回值
X temp1 = xx.getValue();
(2)产生一个临时变量class Y object,放置operator Y()的返回值
Y temp2 = temp1.operator Y();
(3)产生一个临时变量int object,放置equality(等号)运算符的返回值
int temp3 = yy.operator==(temp2);
最后,适当的destructor 将被施行于每一个临时性的class object身上。这导致我们的式子被转换为以下形式:
// C++伪码
// 以下是条件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();
}
略麻烦
一般而言,constructor和destructor的安插都如你所预期:
// c++伪码
{
Point point;
// 构造函数会被安插在这里
// point.Point::Point()
// ...
// 析构函数会被安插在这里
// point.Point::~Point();
}
构造函数的调用放在返回值之前。
有以下片段:
Matrix identity;
int main(void)
{
// 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。因此在下面这段码中:
int v1 = 1024;
int v2;
初始化全局变量/全局对象
有以下案例:
const Matrix&
identity() {
static Matrix mat_identity;
// ...
return mat_identity;
}
局部静态对象保证了什么样的语意?
mat_identity的constructor必须只能施行一次,虽然上述函数可能会被调用多次。
mat_identity的destructor必须只能施行一次,虽然上述函数可能会被调用多次。
编译器的策略之一就是,无条件地在程序起始( startup)时构造出对象来。然而这会导致所有的local static class objects都在程序起始时被初始化,即使它们所在的那个函数从不曾被调用过。因此,只在identity()被调用时才把mat_identity构造起来,是比较好的做法(现在的C++ Standard已经强制要求这一点)。我们应该怎么做呢?
以下就是我在 cfront之中的做法。首先,我导入一个临时性对象以保护mat_identity 的初始化操作。第一次处理identity()时,这个临时对象被评估为false,于是constructor会被调用,然后临时对象被改为true。这样就解决了构造的问题。而在相反的那一端,destructor也需要有条件地施行于mat_identity身上,但只有在mat_identity已经被构造起来时才算数。要判断mat_identity是否被构造起来,很简单。如果那个临时对象为true,就表示构造好了。困难的是由于cfront产生C码,mat_identity对函数而言仍然是local,因此我没办法在静态的内存释放函数〈 static deallocation function)中存取它。噢,伤脑筋!解决的方法有点诡异,结构化语言避之唯恐不及:我取出 local object的地址。(当然啦,由于object是 static,其地址在 downstream component中将会被转换到程序内用来放置 global object的 data segment中)。
最后,destructor必须在“与text program file (也就是本例中的 stat_0.c)有关联的静态内存释放函数( staic deallocation function)”中被有条件地调用。
有以下数组定义:
Point knots[10];
如果 Point 既没有定义一个constructor也没有定义一个destructor,那么我们的工作不会比建立一个“内建(build-in)类型所组成的数组”更多,也就是说,我们只需配置足够的内存以储存10个连续的Point元素。
然而Point的确定义了一个default destructor,所以这个destructor必须轮流施行于每一个元素之上。一般而言这是经由一个或多个runtime library函数达成。
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 consructor和default destructor的函数指针。参数array带有的若不是具名数组(本例为knots)的地址,就是0。如果是0,那么数组将经由应用程序的new运算符被动态分配到heap中。参数elem_size表示数组中的元素数目。
如果你想要在程序中取出一个constructor的地址,这是不可以的。当然啦,这是编译器在支持vec_new()时该做的事情。
举个例子,在 cfront 2.0 之前,声明一个由 class objects所组成的数组,意味着这个class必须没有声明constructors或一个default constructor(没有参数那种)。一个constructor不可以取一个或一个以上的默认参数值。这是违反直觉的,会导致以下的大错。
这一段没啥结论,就说了之前的一些错误写法
运算符new 的使用,看起来似乎是一个单一运算,像这样
int* pi = new int(5);
但事实上它是由以下两个步骤完成:
1 通过适当的new运算符函数实体配置所需的内存:
// 调用函数库中的new运算符
int* pi = _new(sizeof(int));
2 给配置得来的对象设立初始值
*pi = 5;
更进一步地,初始化操作应该在内存配置成功(经由new运算符)后才执行:
// new运算符的两个分离步骤
// given: int* pi = new int(5);
// 重写声明
int* pi;
if (pi == _new(sizeof(int))) {
*pi = 5; // 成功了才能初始化
}
好像用malloc和new的时候,还没出过错。。。。。。
delete运算符的情况类似。
对象的new也是这样,先申请空间,在初始化,如果处理异常,程序则会稍微复杂点
malloc申请出错返回NULL
new申请出错会抛出异常
针对数组:
int* p_array = new int[5];
vec_new()不会真正被调用,因为它的主要功能是把default constructor施行于class objects 所组成的数组的每一个元素身上。倒是new运算符函数会被调用:
int* p_array = (int*)_new(5 * sizeof(int));
相同的情况
// struct simple_aggr { float f1, f2; };
simple_aggr* paggr = 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);
还记得吗,在个别的数组元素构造过程中,如果发生exception,destructor就会被传递给vec_new()。只有已经构造妥当的元素才需要destructor的施行,因为它们的内存已经被配置出来了,vec_new()有责任在exception发生的时候把那些内存释放掉。
在C++2.0版之前,将数组的真正大小提供给程序的delete运算符,是程序员的责任。
delete [size]p_array;
现在这样写
delete []p_array;
如果类中自己定义了构造函数和析构函数,在new 和 delete的时候,会调用构造函数和析构函数。
最好就是避免以一个base class指针指向一个derived class objects所组成的数组——如果derived class object比其 base 大的话。
有一个预先定义好的重载的( overloaded )new运算符,称为placement operator new。它需要第二个参数,类型为void*。调用方式如下:
Point2w* ptw = new(arena)Point2w;
其中arena指向内存中的一个区块,用以放置新产生出来的 Point2w object.这个预先定义好的 placement operator new的实现方法简直是出乎意料的平凡.它只要将“获得的指针(译注:上例的arena)”所指的地址传回即可:
void*
operator new(size_t, void* p)
{
return p;
}
如果它的作用只是传回其第二个参数,那么它有什么价值呢?也就是说,为什么不简单地这么写算了(这不就是实际所发生的操作吗):
Point2w* ptw = (Point2w*)arena;
事实上这只是所发生的操作的一半而已。Placement new operator所扩充的另一半边是将Point2w constructor自动实施于arena所指的地址上:
// c++伪码
Point2w* ptw = (Point2w*)arena;
if (pte != 0)
{
ptw->Point2w::Point2w();
}
这正是使placement operator new威力如此强大的原因。这一份码决定objects被放置在哪里;编译系统保证object的constructor会施行于其上。
但是还有一个问题
// 让arean成为全局性定义
void fooBar()
{
Point2w* p2w = new(arena)Point2w;
// ...
// ...
p2w = new(arena)Point2w;
}
如果placement operator在原已存在的一个object 上构造新的 object,而该现有的 object 有一个destructor,这个destructor并不会被调用。调用该destructor 的方法之一是将那个指针delete掉。不过在此例中如果你像下面这样做,绝对是个错误:
// 以下并不是实施destructor的正确方法
delete p2w;
p2w = new(arena)Point2w;
剩下的唯一问题是一个设计上的问题:在我们的例子中对placement operator的第一次调用,会将新object 构造于原已存在的 object 之上?还是会构造于全新地址上?也就是说,如果我们这样写:
Point2w* p2w = new(arena)Point2w;
我们如何知道arena所指的这块区域是否需要先解构?这个问题在语言层面上并没有解答。一个合理的习俗是令执行new的这一端也要负责执行destructor的责任。
如果我们有一个函数,形式如下:
T operator+(const T&, const T&);
以及两个T objects,a和b,那么a+b
可能会导致一个临时性对象,以放置传回的对象。是否会导致一个临时性对象,视编译器的进取性( aggressiveness)以及上述操作发生时的程序上下关系( programcontext)而定。例如下面这个片段:
T a, b;
T c = a + b;
编译器会产生一个临时性对象,放置a + b的结果,然后再使用T的copy constructor,把该临时性对象当做c的初始值。然而比较更可能的转换是直接以拷贝构造的方式,将a + b 的值放到c中(2.3节对于加法运算符的转换曾有讨论),于是就不需要临时性对象,以及对其constructor和 destructor的调用了。
理论上,C++ Standard允许编译器厂商有完全的自由度。但实际上,由于市场的竞争,几乎保证任何表达式( expression)如果有这种形式:
T c = a + b;
其中的加法运算符被定义成:
T operator+(const T&, const T&);
或
T T::operator+(const T&);
那么实现时根本不产生一个临时性对象。
推荐下面这种形式:
T c = a + b;
临时性对象的被摧毁,应该是对完整表达式( full-expression)求值过程中的最后一个步骤。该完整表达式造成临时对象的产生。
临时性对象的生命规则有两个例外。第一个例外发生在表达式被用来初始化一个object时。
临时性对象的生命规则的第二个例外是“当一个临时性对象被一个reference绑定”时。
做了一个测试。
这份研究对于良好的优化提供了一个具有说服力的证明。目前存在有一些优化工具,的确把临时对象的一些成分放进了缓存器中。当编译器厂商把他们的焦点从语言特性的支持(与Standard C++比较)转移到实现技术的优劣上时,如反聚合(disaggregation)这般的优化操作就会更普遍了。