对象的构造与析构
所谓有生即有死,对象有创建也该有销毁。又有所谓人有千奇百怪各有死法不同,对象的销毁也有很多种,不过不同于人的是,人的出生大致都相同的而死法却各自不同,对象的创建方法直接决定了它的销毁方法。
自动对象
最常见的自动对象,声明的时候在栈上创建,离开作用域就由系统自动销毁。在一个函数中咱们免不了声明几个局部变量,在函数结束的大括号那里,这些局部变量应该按什么顺序销毁呢?它们将按照构造的逆序被销毁。
自由存储对象
而自由存储对象,new的时候在堆上创建,delete的时候销毁。
类成员对象
作为别的类或者结构的成员的对象,它们的创建与销毁当然是随着作为容器的那个对象一起创建与销毁,但是它们也有自己的顺序。
首先,成员的构造函数,如果有的话,一定先于容器的构造函数被执行。这是一个自下而上的顺序。
第二,不管初始式列表怎么写,成员对象都是按照类里面声明成员的顺序来创建。这里就可能出现下面的问题:
class CBad
{
看上去似乎很完美,甚至编译也能通过但一运行就报错,很简单,构造函数的初始式按照声明的顺序创建成员对象,那么应该先运行len(strlen(p)),但p还没有初始化啊,肯定会出问题。所以最好的办法是将初始式列表的顺序与成员声明的顺序保持一致。
在销毁的时候,析构函数的调用就是一个自上而下的顺序,先调用容器类的析构函数然后才调用成员的析构函数。而成员销毁的顺序与构造时的顺序相反。
数组
数组的创建当然也是在声明的时候完成,例如
person people[100];
系统将自动调用100次person的默认构造函数。然后在作用域结束时,系统再自动调用100次析构函数。而如果我们使用了new给数组分配空间,情况又不一样了。
person* p=new people[100];
构造函数仍然按照调用100次,而析构函数则必须显式调用,而且对于数组必须用delete[],这时系统将先调用100次析构函数,然后再销毁指针p释放内存。
局部静态对象
对于局部静态对象,控制线程第一次通过它的定义时调用构造函数。例如,在某个函数里面定义一个静态对象,第一次调用这个函数时将调用该对象的构造函数,但是在函数结束时,局部静态对象并不被销毁,而且下次如果再调用这个函数,也不会再调用静态对象的构造函数。
程序结束时,所有的静态对象将按照构造的逆顺序调用析构函数。
非局部对象
所谓非局部对象,一般是指全局变量、名字空间的变量、以及所有类的静态变量。在main()函数激活之前,这些对象就会完成构造。在同一编译单位中,所有的非局部变量按照它们声明的顺序构造,而在不同的编译单位中,构造顺序由编译器决定。
在main()函数结束时,按照构造的逆顺序调用非局部对象的析构函数。
在这里存在一个问题,在某个文件里我们定义了一个静态对象,而在另一个文件里我们需要使用它,既然编译顺序不受控制,我们怎么能保证使用的是已经正确初始化的对象呢?这时候我们需要用到一个叫做第一次开关的东西,实际上就是一个静态bool flag,它能告诉我们某个对象是不是已经被初始化了。
临时对象
a=x+y*z的过程实际上是用y和z的副本相乘,得到一个临时对象,比如说我们命名为_mul,然后再用x和_mul的副本相加得到最终的结果,这仍然是一个临时对象,我们再给它起名为_sum,最后才是将_sum的副本赋给a。
在这一长串的过程中,临时对象不断被悄悄地构造,然后等这个表达式结束时它们又悄悄地被销毁。整个过程无声无息,编译器工作得非常好,完全不需要我们操心。然而,总还是会出一些问题。
例如
string astr="hello";
这样的写法并不罕见,但是非常危险。astr+bstr会得到一个临时对象,.c_str()将返回一个从这个临时对象抽取出来的C风格字符串,然后cstr指针指向它。看上去没什么问题,但是仔细一问就会出麻烦,既然astr+bstr是一个临时对象,那在这个表达式结束时它将被销毁,此时.c_str()返回的那个字符串还存在吗?说实话,第一次看到这个问题,我也紧张了好一下,因为从C#转过来的人谁不会写这种语句,可是一测试,居然能正常输出。看来编译器帮忙回避了这个问题,不过,谁又能保证每个编译器都能帮这个忙呢?
放置语法
new 出来的对象放在堆上,但是如果确实需要放在别的地方可以吗?万能的C++当然可以做到。
void*buf=reinterpret_cast<void*>(0xF00F);
显然我们需要显式调用析构函数来销毁这个对象,还要想办法释放p指向的存储。所以,这种技术基本上就是错误的来源。
这是一篇介绍类构造和析构很好的文章,值得阅读。
文章来源:http://blog.sina.com.cn/s/blog_721f09800100ly70.html