C++特性探寻-构造函数和析构函数
2008-10-28 19:17
357 查看
2008-09-19 20:46
构造函数提供了一种机制,通过它有机会完成必要的初始化工作,从而使对象成为有意义 的存在物,而不仅仅只是一块原始的空间。 但是,我们逐渐了解到,构造函数具有的地位,不仅对于用户(程序员),对于编译器履 行职责也极为重要。通过这个机制,它让C++的一些基本的特性,如继承、多态得到了正确 的贯彻和表现。 首先不难理解的一点是在构造函数中,要确保基类对象的正确构造,如果是从基类继承的 话。因为继承类对象至少可以被“低”看为一个基类对象,具有后者的所有行为和表现, 所以基类的构造函数首先被调用。如果所谓的基类也是从其他类继承过来的,这就形成了 一个调用链。最后的情况是,最基础的类的构造函数首先被执行,然后才是上一层的构造 函数,如此到最外层的继承类。这个过程必须是严格有序的。如果没有这个次序保证,继 承类就有机会在基类还没构建好的情况下就访问基类的数据或函数,这将导致不可预料的 、灾难性的后果。 只有首先确保基类的正确构造,接下来才能进行继承类本身的构造。因为类中可能含有成 员对象,必须保证这些对象也被构造,按照一定的次序(一般就是变量声明的次序)。可 能其中一个或一些成员对象的构造需要参数,这需要在类的构造函数中提供所有需要的参 数(构成所谓显式的“初始化列表”)。 看起来这就是构造函数的全部隐含(或半隐含)工作?不是的。我们忽视了一个极为重要 的东西,我称之为类关联信息(表)。类关联信息是编译时生成的、为运行时所需的类的 附加数据(可以认为这些数据放在全局数据区中)。我们熟悉的虚函数表(v-table),我 把它归在类关联信息的范畴之中(最初,我以为虚函数表就是全部,但这个理解有点狭义 )。此外,还包含运行时类型信息(RTTI)。此外,不排除我们还不清楚的其它辅助数据 (总之,类关联信息是一种广义的、统一的称呼)。如果编译器为类生成了类关联信息, 那么毫无疑问,必须在构造函数中将它与当前对象(类的实例)关联起来(也许简单到只 需要设置一个指针即可)。 例如,如果类中含有虚函数,或者,它覆盖了基类中的虚函数(两种情况下都意味着类有 自己的虚函数表)。那么,设置正确的关联后,将存在一个指针(v-ptr),它正确地指向 了该虚函数表(v-table)。此后,多态才能表现出所期望的正确行为。 再次指出(次序的重要性),设置关联,或者狭义地说,设置v-ptr必须发生在对基类构造 函数的调用之后。因为,继承类如果有自己的虚函数表,那么v-ptr会被改写,以指向该表 ,即使此前v-ptr已经被基类所设置。这是合法的,也正是所期望的。但是语义上我们绝不 允许基类可以改写继承类所设置的v-ptr。如果v-ptr设置发生在基类构造调用之前,那么 这种非法的一幕就会发生。 所有上述的事情完成之后(对于用户来说它们几乎是隐含的),才真正开始执行用户的初 始化代码。 在构造函数中调用虚函数,会发生什么?发生的情况也许是始料未及的。当前类的对象正 在构建,v-ptr指向的是当前类的虚函数表。此时,还没到继承类执行它自己的代码(如设 置v-ptr,执行初始化代码)的时候,那一切发生在当前类的构造函数执行完毕之后。所以 ,将要执行的是本类的虚函数版本,而不是可能被覆盖的继承类的版本。这里有一个反面 的证据。假如v-ptr设置发生在基类构造函数调用之前,让我们有机会调用继承类的虚函数 版本,这意味着什么?继承类还没有完成初始化(因而对象还没有构建好),我们企图在 一个没有构建好的对象上执行它的成员函数,可以想见后果是灾难性的。 类的构造函数何时被调用?在对象被创建的时候。对象可能位于栈上,全局数据区,或堆 上。对象可能会在声明的地方创建,这样的对象位于栈上或全局数据区。对象也可以使用 new操作符动态地创建,这样的对象将位于堆上。 析构函数 析构函数提供了一种和构造函数相反的机制,允许在销毁一个对象之前(亦即回收对象所 占用的空间),让对象释放自己所使用的资源。再次,这里所关心的是语言底层所发生的 事情。 与构造函数的执行次序刚好相反,析构函数从最外层的类开始执行,最基础的类的析构函 数最后执行,看起来就象一层层的剥壳。这个次序要得到严格保证的理由也是明显的(违 反次序又将产生相关性问题)。结果看起来是这样的:用户析构代码->成员对象析构->基 类析构函数。 析构函数在对象行将被销毁时调用。当对象超出其作用域,例如一个函数内部的局部变量 ,在函数返回时将被自动销毁。对于在堆上创建的对象,我们只能通过对象指针p,执行d elete p来销毁它。现在就有一个问题。p指向了某个类型(例如A)的对象,但是却可能是 通过上溯造型(upcast)得来的,因此它指向的实际上是一个B的对象,那么将发生什么? 显然,正确的做法是把对象作为B的实例来销毁。也就是说,调用B的析构函数。我们也许 很快想到,首先,在运行时刻可以知道这个对象“实际上”是什么类型(那个最晚的派生 类,本例中假定就是B),例如通过RTTI。然后,根据实际的类型,“找到”并调用该类的 析构函数。然而,这个想法不会自然实现,需要在编译时把类的类型信息和一个指向析构 函数的指针关联起来。不过,上述考虑似乎复杂了点。我们可以把这个析构函数指针放入 虚函数表中(某个特别的位置)。理由是,与虚函数非常相似,存储这个析构函数指针的 slot可以为继承类所覆盖(从而指向继承类的析构函数),而且继承类应该总是覆盖它( 然而下面将看到,实际的情况与“总是覆盖”有出入)。 无论怎样,析构函数指针是存储在前面称之的类关联信息(表)中,编译器不难找到。因 而当通过对象指针(即使已经上溯造型)执行删除操作时,总是能够调用正确的析构函数 。但是,在多个编译器的试验发现,只有把基类的析构函数声明为虚的,继承类才会覆盖 它。看来的确采用虚函数的技术来对待析构函数了。如果基类的析构函数没有声明为虚的 ,则执行前面的delete p时,只有A的析构函数得到执行,而B的没有执行! 这是令人惊讶的。竟然允许“不完全”析构的情况发生,把选择权以及责任交给了程序员 !我不能确定这么做的主要理由是什么。也许还是因为C++如此看重效率,它迫使程序员在 必要的时候显式声明他的需求,因为类似虚函数的间接调用要占用额外的空间和时间。 前面考察了虚函数在构造函数中的行为,继续来看一下在析构函数中的情形。因为析构过 程是自外向里的,当前类在调用虚函数的时候,其继承类此前已经完成了析构,不再是可 用的。因而,是不可以调用继承类的虚函数版本的。结果,调用的仍然是本地版本(和构 造函数中的结论一样,虚函数机制被忽略)。 |
相关文章推荐
- C++特性探寻-构造函数和析构函数
- C++的优秀特性3:构造函数和析构函数
- C++新特性(构造函数/析构函数/常类型)
- C++语言特性:构造函数,析构函数,虚函数,内联函数,静态成员函数,重载,覆盖,隐藏
- C++--构造函数与析构函数
- C++ 派生类的构造函数和析构函数
- C++不要在构造函数和析构函数中调用虚函数
- C++中显示调用构造函数和析构函数
- 读几个小程序了解c++:Part 01(构造函数、析构函数、指针、静态数据成员)
- C++函数之类的构造函数析构函数
- C++之构造函数、析构函数抛出异常的问题
- C++多态中的构造函数与析构函数
- 学习笔记之深入浅出MFC 第8章 C++重要性质---构造函数与析构函数
- C++学习之构造函数、析构函数
- C++学习篇——构造函数与析构函数
- C++:实例解析构造函数、析构函数、拷贝构造函数等
- C++:构造函数和析构函数能否为虚函数
- 【C++】不要在构造函数或析构函数内调用虚函数
- C++学习笔记-----不要在构造函数和析构函数中调用虚函数
- C++ 构造函数与析构函数详解(一)---局部变量