构造函数调用虚函数

C++与Java构造函数中调用虚函数
本文探讨了C++与Java中构造函数调用虚函数的行为差异及潜在风险。C++中此类调用可能导致未预期的结果,而Java则能确保正确调用子类方法。此外,还介绍了C++中的两种解决方案。

今天看android canvas 类的源代码看到 Canvas的构造函数之一:

public class Canvas{
...
    public Canvas() {
        if (!isHardwareAccelerated()) {
            ...
        } else {
            ...
        }
    }

    public boolean isHardwareAccelerated(){
        return false;
    }
}

public abstract class HardwareCanvas extends Canvas {
    @Override
    public boolean isHardwareAccelerated() {
        return true;
    }
    ...
}    

很明显,Canvas 构造函数调用的isHardwareAccelerated() 是希望子类重载用的。回想以前写C++代码的时候好像很少碰到在构造函数里面调用虚函数的情形,对于C++如何解决类似的问题似乎有些遗忘了,于是Google了一下,发现这里面还是有一些文章,于是摘抄下来,以备不时之需。


注意:

Calling virtual functions from a constructor or destructor is dangerous and should be avoided whenever possible.

在C++中通过构造函数或者析构函数调用虚函数是危险行为,应该尽量避免。


C++现实情形

C++对于构造函数中调用虚函数是支持的,但是需要注意的是当在构造函数中调用虚函数的时候对多态的支持尚未完成。对于上述Canvas 中调用 isHardwareAccelerated() 的情形, 在C++中是不可能调用到 HardwareCanvas::isHardwareAccelerated() 的。

C++中虚函数表会先于构造函数的调用被建立, 当 Canvas 调用虚函数 isHardwareAccelerated() 的时候, 此时HardwareCanvas 子类的任何信息包括虚表还没有建立起来,因此这时候调用的是 Canvas 自己的 isHardwareAccelerated().


Java现实情形

Java采取了与C++的保守相比更激进的做法。在我们创建HardwareCanvas类的实例的时候,它会先把整个类层次结构建立起来,然后再调用基类构造函数和子类的构造函数,这样在Canvas类的构造函数调用的时候,多态就已经能够发挥作用了。这也是我们能够看到上面的代码的现实原因!


危险在哪里?

可以看到我们的例子中 isHardwareAccelerated() 仅仅简单的返回了 ture, false, 并没有访问 HardwareCanvas 的任何数据。 假如我们的 isHardwareAccelerated() 不小心访问了这些数据,那很显然在 HardwareCanvas 构造函数没有调用之前,这些数据仅仅是默认值,有可能是 null, 这就给我们隐含了未知的风险,有可能导致运行时的崩溃。


C++的解决方法

C++中的解决方法其实很简单,简单到我们大多数人一直在用却不知道原因。

 class Canvas
 {
     public:  
        typedef std::unique_ptr<Canvas> Ptr;      
        void init()
        {
            ...
            if(!isHardwareAccelerated() )
            {
               ...
            }
            else
            {
               ...
            }
       }
     protected:
        virtual bool isHardwareAccelerated(){ return false;}

     public:
        static Ptr create()
        {
             Ptr p( /*...use a factory to create a HardwareCanvas object via new...*/ );
             p->init();
             return p;
        }


     protected:
         Canvas();
};

class HardwareAccelerated : public Canvas
{
protected:
    bool isHardwareAccelerated() { return true; }
};

基本思想就是 把 构造函数中需要调用虚函数的部分分离出来并放到一个 init 函数中。
这样我们可以保证 init 被调用的时候 HardwareCanvas 构造函数已经被创建, 此时访问它的成员就安全了。


还有一种方法适用于虚函数不需要访问子类的成员的时候,比如我们的Canvas 类的 isHardwareAccelerated().

  • 将需要多态支持的部分抽象成一个基类或者接口
class Helper
{
    public:
    virtual bool isHardwareAccelerated(){ return false;}
};
  • 继承基类或实现接口
class Helper_A : public Helper
{
    public:
    virtual bool isHardwareAccelerated() { return true;}
};
  • 基类构造函数接受一个Helper类的引用:
class Canvas
{
    public:
    Canvas( Helper& );
};
  • 子类的构造函数传入自定义的Helper
class HardwareCanvas : public Canvas 
{
    public:
    HardwareCanvas( ) : Canvas( getHelper()){} 

   private:
   static Helper_A& getHelper();
};

参考:

1. http://stackoverflow.com/questions/962132/calling-virtual-functions-inside-constructors.
2. https://isocpp.org/wiki/faq/strange-inheritance#calling-virtuals-from-ctor-idiom.

<think>我们正在讨论C++构造函数调用虚函数的行为。根据引用[1]和引用[3],在构造函数调用虚函数是允许的,但不会发生多态行为。具体来说,当基类构造函数调用时,它调用虚函数是基类自己的版本,而不是派生类重写的版本。这是因为在基类构造函数执行期间,派生类部分尚未构造,因此无法调用派生类的虚函数。引用[4]进一步指出,C++标准(12.7.3条)规定在构造过程中,虚函数调用不会进行多态,而是调用当前正在构造的类(即基类)的版本。 因此,虽然语法上允许,但通常不建议在构造函数调用虚函数,因为它不会按多态方式工作,可能导致意料之外的行为。关于安全性:从语法和编译角度是安全的,不会导致程序崩溃或未定义行为(只要虚函数有定义)。但是,从设计角度,它可能不符合预期(因为不会调用派生类的重写函数),因此需要谨慎使用。 下面通过一个例子说明:```cpp#include<iostream> usingnamespace std; classBase {public:Base() {//在基类构造函数调用虚函数func();}virtualvoidfunc(){cout <<"Base::func()"<< endl;} };class Derived: publicBase {public:Derived() :Base(){//在派生类构造函数调用虚函数func();}virtualvoid func() override{cout <<"Derived::func()" <<endl;}}; intmain(){Derived d;//创建派生类对象return0;} ```输出: ```Base::func()Derived::func() ```解释:-在构造`Derived`对象时,先调用基类`Base`的构造函数。此时,虽然对象最终是`Derived`类型,但在基类构造期间,对象类型被视为`Base`(派生类部分尚未构造),因此调用`Base::func()`。 -然后进入派生类构造函数,此时派生类部分开始构造,调用虚函数`func`,由于此时对象已经构造到派生类阶段,所以调用`Derived::func()`。注意:在基类构造函数调用虚函数时,不会调用派生类重写的版本。这是因为在基类构造期间,派生类对象还没有完全构造,C++为了避免在对象未完全构造时调用其成员函数(可能导致未定义行为),所以规定在构造期间虚函数机制不生效,即不会进行动态绑定。 所以,结论是:在构造函数调用虚函数是安全的(不会导致程序崩溃),但行为是确定的(调用当前类的版本),不会发生多态。因此,如果设计意图是希望调用派生类的重写函数,那么这样做就是不安全的(因为不会按预期工作)。通常建议避免在构造函数调用虚函数。§§相关问题§§1.为什么在C++构造函数调用虚函数不会发生多态? 2.在基类构造函数调用虚函数时,对象类型是什么? 3.除了构造函数,在析构函数中调用虚函数会有什么行为?</think>### C++构造函数调用虚函数的行为分析 在C++中,构造函数调用虚函数是语法上允许的,但存在特定行为和潜在问题,需要谨慎使用: #### 行为特点 1. **无多态行为** 在构造函数调用虚函数时,不会触发多态机制(即使函数被声明为`virtual`)。基类构造函数调用虚函数始终是**基类自身的版本**,而非派生类的重写版本[^4]。这是因为: - 构造顺序:先构造基类,再构造派生类 - 在基类构造期间,派生类对象尚未完全构造 - C++标准规定此时对象类型视为基类类型(12.7.3条)[^4] 2. **安全但不符合预期** 语法上安全(不会崩溃),但行为可能不符合开发者预期: ```cpp class Base { public: Base() { func(); // 总是调用Base::func() } virtual void func() { cout << "Base"; } }; class Derived : public Base { public: virtual void func() override { cout << "Derived"; } }; int main() { Derived d; // 输出"Base"而非"Derived" } ``` #### 潜在问题 1. **资源未初始化风险** 若派生类的虚函数依赖派生类特有的成员变量: ```cpp class Derived : public Base { File* file_handle; // 派生类特有资源 public: Derived() : file_handle(open_file()) {} virtual void func() override { file_handle->write(); // 基类构造时file_handle未初始化! } }; ``` 此时基类构造函数调用`func()`将访问未初始化的`file_handle`,导致未定义行为。 2. **设计矛盾** 虚函数的设计初衷是实现运行时多态,但在构造阶段对象类型是静态确定的,二者存在根本性矛盾[^3]。 #### 最佳实践建议 ✅ **避免在构造函数调用虚函数** - 改用普通函数或模板方法模式 - 通过`init()`函数分离初始化和构造逻辑 ✅ 若必须调用,应: 1. 将函数声明为`final`防止派生类重写 2. 在文档中明确说明行为 3. 确保函数不依赖派生类状态 > 示例代码验证: > ```cpp > #include <iostream> > using namespace std; > > class A { > public: > A() { fun(); } > virtual void fun() { cout << "A fun\n"; } > }; > > class B : public A { > public: > B() { fun(); } > virtual void fun() { cout << "B fun\n"; } > }; > > int main() { > B b; // 输出: A fun → B fun > A* p = new B(); > p->fun(); // 输出: B fun > } > ``` > 输出结果证明: > - 基类构造阶段调用`A::fun()` > - 派生类构造阶段调用`B::fun()` > - 构造完成后多态生效[^1][^3]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值