实用经验 61 绝不让构造函数为虚函数

每个类都可能会定义几个特殊的成员函数,这些函数用于完成class的初始化工作。这种特殊的成员函数被称之为构造函数。和普通函数一样,构造函数可以接受一个或多个参数。

关于构造函数

  • 构造函数是一种特殊的函数,构造函数的名称必须和类名称一样,并且不能指定返回值类型。
  • 声明一个类时,你可以不定义构造函数。但这并不说明此类就没有构造函数了。这种情况下编译器会偷偷替你实现一个默认构造函数(合成默认构造函数)。也就是说每个类都至少存在一个构造函数。
  • 构造函数虽然不能指定返回值。但它可接受多个形参,而且还支持重载机制。

一般情况下,编译器会为每个类生成一个公有的默认构造函数,但有两种特殊情况例外:

  • 一个类显示的声明了构造函数,这种情况下编译器不会帮你生成公有默认构造函数。如果程序需要一个默认构造函数,则需要程序员显示提供。
  • 一个类声明了一个非public的构造函数,编译器就不会生成公有的默认构造函数了。

构造函数的重载和普通函数的重载机制是一样的。具体实现机制你可参考实用经验48。和普通重载函数唯一的区别在于构造函数完全依靠形参表决定采用哪个构造函数。在实例生成时,类对象通过构造函数输入的实参类型匹配构造函数,找到一个形参类型与其相同的构造函数,作为此次构造调用的构造函数。

我们都知道构造函数是为了完成对象的创建,但其实际执行流程没有说的那么简单,下面我们就來介绍 一下构造函数的执行过程。

构造函数的执行是一个复杂的过程,主要是牵涉了继承机制的缘故。我们以类C为例分析一下构造函数的执行过程。假设一个C的继承结构如图9-1所示。

在这里插入图片描述

图9-1 C类继承关系图

只有首先确保基类的正确构造,接下来才能顺利进行继承类自身的构造。此外因为类中可能含有成员对象,必须要保证这些对象也要按照一定的次序构造(一般就是变量声明的次序)。可能其中一个或一些成员对象的构造需要参数,这需要在类的构造函数中提供所有需要的参数(构成所谓显式的“初始化列表”)。

综上所述,在有继承的类的初始化过程中,如果要实现本类的构造,首先要完成基类的构造。然后才能进行本类的构造。因为继承类对象至少可以被“低”看为一个基类对象,具有基类的所有行为和表现,所以基类的构造函数首先被调用。如果所谓的基类也是从其他类继承过来的,这就形成了一个调用链。最后的情况是,最基础的类的构造函数首先被执行,然后才是上一层的构造函数,如此到最外层的继承类。这个过程必须是严格有序的。如果没有这个次序保证,继承类就有机会在基类还没构建好的情况下就访问基类的数据或函数,这将导致不可预料的、灾难性的后果。

注意:如果派生类的基类存在virtual函数时,在派生类的构造过程中,除了上述所述的所有过程外还存在虚函数表的创建。关于虚函数表可参见实用经验92所述。

根据上面的分析,我们可得知C的构造过程如下:首先调用A的构造函数完成A基类的构造,然后再调用B的构造函数完成B基类的构造,最后是类C的构造函数的调用。最终完成整个C类的构造。

下面继续讨论构造函数的调用。一般情况下,如果你声明或创建类对象时,构造函数由C++调用机制自动调用,而不需要你显示的调用。同时编译器还会根据实际的应用场景决定类创建时,到底调用哪个构造函数。实现这种机制的可能是C++所具备的函数重载,也有可能是普通构造、默认构造、合成构造、拷贝构造的差异。但这并不是我们所关注的,这些是由C++编译器的设计者们该关注的。我们仅需关注的是各类构造函数何时被调用,而不必关心怎么会调用的。

但有一个例外,就是placement new。在这种情况下构造函数完成在一段已申请好的内存上实现类的构造。需要我们手动调用,编译器无法帮助我们实现构造函数的自动调用。

关于拷贝构造函数的调用

  • 当类的一个对象去初始化该类的另一个对象时,拷贝构造函数会被调用;
  • 如果函数的形参是类的对象,调用函数进行形参和实参结合时,拷贝构造函数会被调用;
  • 如果函数的返回值是类对象,函数调用完成返回时,拷贝构造函数会被调用。

简述完了类的构造函数,我们继续讨论是否构造函数可声明为virtual函数?我们首先给出结论:构造还是无论在什么时候都不能声明为virtual函数。

(1)从存储空间角度,分析构造函数不能声明为virtual函数。

我们都知道,如果一个类只要存在一个virtual函数。此类在创建时就会创建一个vtable。类就是通过此表确定函数调用的,也就是动态绑定。这个vtable存储于对象的内存空间的。这而问题就出现了,如果构造函数是虚的,就需要通过 vtable来调用,可是这时对象还没有实例化,也就是内存空间还没有,更不可能存在vtable,无法找到vtable,所以构造函数不能是虚函数。

反过来,如果构造函数声明为virtual函数,且为C++编译器所支持。我们在创建类对象构造函数调用时,首先会去查找此类的vtable,而此时的vtable不存在。即使可以执行成功,最后构造函数所产生的效果也是我们无法预测的。

(2)从使用角度,分析构造函数为何不能声明为virtual函数。

虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。

虚函数的作用在于通过父类的指针或引用来调用它的时候,能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因为这时虚函数机制还未形成。因此也就规定构造函数不能是虚函数。

(3)从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数。

(4)当一个构造函数被调用时,它做的首要的事情之一是初始化它的vtable指针。因此,它只能知道它是“当前”类的,而完全忽视这个对象后面是否还有继承者。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码- -既不是为基类,也不是为它的派生类(因为类不知道谁继承它)。

所以它使用的vtable指针必须是对于这个类的VTABLE。而且,只要它是最后的构造函数调用,那么在这个对象的生命期内, vtable指针将保持被初始化为指向这个VTABLE,但如果接着还有一个更晚派生的构造函数被调用,这个构造函数又将设置vtable指针指向它的VTABLE等。直到最后的构造函数结束。vtable指针的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是从基类到更加派生 类顺序的另一个理由。

但是,当这一系列构造函数调用正发生时,每个构造函数都已经设置vtable指针指向它自己的 VTAB LE。如果函数调用使用虚机制,它将只产生通过它自己的VTABLE的调用,而不是最后的VTABLE(所有构造函数被调用后才会有最后的VTABLE)。

请谨记

  • 构造函数不能声明为虚函数,因为虚函数的调用需要class构造完成之后方可调用。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值