对象的构造和析构
文章目录
一般而言,构造函数被安插在对象的定义处,而析构函数被安插在对象生命周期结束前:
// Pseudo C++ Code
{
Point point;
// point.Point::Point() 一般被安插在这儿
...
// point.Point::~Point() 一般被安插在这儿
}
如果一个区段(译注:以 { } 括起来的区域)或函数中有一个以上的离开点,情况会稍微混乱一些。Destructor必须被放在每一个离开点(当时object还存活)
一般而言我们会把object尽可能放置在使用它的那个程序区段附近,这样做可以节省不必要的对象产生操作和摧毁操作,
全局对象(Global Objects)
由于这样的限制,下面这些munch策略就浮现出来了:
- 为每一个需要静态初始化的文件产生一个_sti()函数,内含必要的constructor调用操作或inline expansions。
- 在每一个需要静态的内存释放操作(static deallocation)的文件中,产生一个__std()函数(译注:我想std就是static deallocation的缩写),内含必要的destructor调用操作,或是其 inline expansions。
- 提供一组runtime library“munch”函数:一个_main()函数(用以调用可执行文件中的所有__sti()函数),以及一个exit()函数(以类似方式调用所有的__std()函数)。
局部静态变量
const Matrix& identity()
{
static Matrix mat_identity;
// ...
return mat_identity;
}
因为静态语意保证了 mat_identity 在整个程序周期都存在,而不会在函数 identity()退出时被析构,所以:
- mat_identity的构造函数只能被施行一次,虽然identity()可以被调用多次。
- mat_identity 的析构函数只能被施行一次,虽然identity()可以被调用多次。
以下就是书中作者在cfront之中的做法。首先,我导人一个临时性对象以保护mat_identity 的初始化操作。第一次处理identity()时,这个临时对象被评估为false,于是constructor 会被调用,然后临时对象被改为true.这样就解决了构造的问题。而在相反的那一端,destructor也需要有条件地施行mat_identity身上,但只有在mat_identity已经被构造起来时才算数。要判断mat_identity是否被构造起来,很简单。如果那个临时对象为true,就表示构造好了。最后,destructor必须在“与text program file(也就是本例中的stat_0.c)有关联的静态内存释放函数(static deallocation function)”中被有条件地调用,可以理解为在整个程序退出之时按构造相反的顺序析构局部静态对象。
对象数组(Array of Objects)
Point knots[ 10 ];
对于constructor:
在cfront中,我们使用一个被命名为vec_new()的函数,产生出以class objects构造而成的数组。比较晚近的编译器,包括Borland、Microsoft和Sun,则是提供两个函数,一个用来处理“没有virtual base class”的class,另一个用来处理“内带 virtual base class”的class.后一个函数通常被称为vec_vnew().函数类型通常如下:
实际上背后做的工作则是:
- 分配充足的内存以存储10个Point元素;
- 为每个Point元素调用它们的默认构造函数(不论是合成的还是显式定义的,如果没有则调用有默认值的其他构造函数,如果不适配则无法运行)。编译器一般以一个或多个函数来完成这个任务。当数组的生命周期结束的时候,则要逐一调用析构函数,然后回收内存,编译器同样一个或多个函数来完成任务。
- 如果初始化了前几个对象,则先用给出的初值初始化前几个对象,剩下的对象调用默认构造函数。
对于destructor:
如果你想要在程序中取出一个constructor的地址,这是不可以的。
new和delete运算符
运算符new的使用,看起来似乎是个单一运算。但事实上它是由两个步骤完成的:
- 通过适当的new运算符函数实例,配置所需的内存
- 将配置得来的对象设立初值
寻找数组维度,对于delete运算符的效率带来极大的冲击,所以才导致这样的妥协:只有在中括号出现时,编译器才寻找数组的维度,否则它便假设只有单独一个objects要被删除。如果程序员没有提供必须的中括号,那么就只有第一个元素会被析构。其他的元素仍然存在——虽然其相关的内存已经被要求归还了。
new expression 和 operator new
- operator new和operator delete不是new expression和delete expression的重载,它们完全是另外的一个独立的东西,具有不同的语意,这与operator +是对+ expression的重载不同。
- new expression和delete expression是不能被重载的,可以看出它们与普通的expression 不同。
针对数组的new语意
将基类指针指向派生类数组,很容易出问题。
因为派生类和基类对象的大小是通常不一样的,对基类指针使用下标操作符时,编译器按基类对象大小寻址,而不是我们需要的派生类。
Placement Operator new 的语意 (布局new)
placement operator new 是重载 operator new 的一个标准、全局的版本,它不能够被自定义的版本代替。
用来在指定地址上构造对象,要注意的是,它并不分配内存,仅仅是 对指定地址调用构造函数。其调用方式如下:
Point *pt=new(p) Point;
它的实现方式异常简单,传回一个指针即 可:
void*
operator new(size_t,void *p ){//size_t被忽略
return p;
}
临时性对象
大多取决于编译器
何时生成临时对象
对于一个下面这样的程序片段:
T a, b;
T c=a+b;
死板一点来讲,它应当产生一个临时对象用来存储a+b的结果,然后以临时对象作为初值调用拷贝构造函数初始化对象c。而实际上编译器更愿意直接调用拷贝构造函数的方式将a+b的值放到c中,这样就不需要临时对象,和它的构造函数和拷贝构造函数的调用了。
但是对于一个没有出现目标对象的表达式 a + b,那么产生一个临时对象来存储运算结果,则是非常必要的。
临时对象的生命周期
**很多时候,产生临时对象是必不可少的,但是何时摧毁一个临时对象才是最佳行为呢?过早或过晚都不太适合,过早有可能使得程序错误,过晚的话又使得资源没有得到及时回收。**对于下面的程序:
string s1("hello "), s2("world "),s3("by Adoo");
std::cout<<s1+s2+s3<<std::endl;
显然保存s1+s2
结果的临时对象,如果在与s3进行加法之前析构,将会带来大麻烦。于是C++标准中有一条:
- 临时性对象的摧毁应当作为造成产生这个临时对象的完整表达式的最后一个步骤。
完整的表达式,是指涵盖的表达式中最外围的那个。我们再看上面那个字符串相加的表达式,当计算完成,而cout还未调用,此时我们析构掉存储最终结果的临时对象,岂不悲剧。其实上面的规定还有两个例外:
- 凡含有表达式执行结果的临时性对象,应该保存到Object的初始化操作完成为止。
- 如果临时性对象被绑定与一个引用,临时对象将残留,直至初始化的引用的生命结束,或直到临时对象的生命周期结束——视哪一种情况先达到,对应于这种情况:
::string s1("hello ");
::string &s=s1+"world";