Item 9 避免在ctor和dtor中调用虚函数

本文探讨了C++中基类构造函数内调用虚函数的问题,指出这种做法不可行的原因,并提供了替代方案。文章强调了在基类构造函数中调用虚函数会导致编译错误或未定义行为,因为此时派生类对象尚未完全构建。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

看下面的程序有什么问题。

 

 

在基类的构造过程中,虚函数调用不会被传递到派生类中。因为基类的ctor是在派生类之前执行的,这时其数据成员还没有被初始化,即,派生对象还不存在!所以C++拒绝这样做。
不止虚函数,使用到RTTI的部分(dynamic_cast和typeid),在基类的ctor里,都不会被解析成派生类。
ctor和dtor同样。
一些编译器会对此给出警告。

 

● 注意,还有一种隐藏版本:

 

 

● 如果非要在基类里实现这样的功能,怎么办?下面是一种办法。

 

### C++ 虚函数内存模型实现原理及布局 C++ 中的虚函数通过一种称为“虚函数表”(vtable)的机制来实现多态性。虚函数表是一个由编译器生成的静态数组,包含指向类中所有虚函数的指针。每个支持虚函数的类对象都会在内存中包含一个指向该虚函数表的指针,通常称为 `vptr`。 以下是虚函数内存模型的具体实现原理及布局: #### 1. 虚函数表的生成 当定义一个类时,如果该类中存在虚函数编译器会为该类生成一个虚函数表。虚函数表的内容包括: - 每个虚函数的地址。 - 如果类中存在纯虚函数,则对应的虚函数表条目可能为空或指向一个默认实现。 ```cpp class Base { public: virtual void f1() { std::cout << "Base::f1" << std::endl; } virtual void f2() { std::cout << "Base::f2" << std::endl; } virtual ~Base() {} }; class Derived : public Base { public: void f1() override { std::cout << "Derived::f1" << std::endl; } }; ``` 对于上述代码,`Base` 类的虚函数表将包含两个条目:`f1` `f2` 的地址。`Derived` 类继承了 `Base` 并重写了 `f1`,因此其虚函数表中的 `f1` 条目将指向 `Derived::f1`,而 `f2` 条目仍指向 `Base::f2`[^1]。 #### 2. 对象内存布局 每个支持虚函数的类对象在内存中都包含一个隐式的 `vptr`,它指向该类的虚函数表。`vptr` 通常位于对象内存布局的起始位置。 以下是一个对象的典型内存布局示例: | 内存偏移 | 描述 | |----------|--------------------| | 0x00 | vptr (指向虚函数表) | | 0x08 | 数据成员 | #### 3. 虚函数调用机制 当通过基类指针或引用调用虚函数时,程序会执行以下步骤: 1. 获取对象的 `vptr`。 2. 使用 `vptr` 访问虚函数表。 3. 根据虚函数在表中的索引找到对应的函数地址。 4. 调用函数。 以下是一个简单的代码示例及其工作机制: ```cpp #include <iostream> class Base { public: virtual void f1() { std::cout << "Base::f1" << std::endl; } virtual void f2() { std::cout << "Base::f2" << std::endl; } virtual ~Base() {} }; class Derived : public Base { public: void f1() override { std::cout << "Derived::f1" << std::endl; } }; void call(Base* obj) { obj->f1(); // 虚函数调用 } int main() { Derived d; Base* b = &d; call(b); return 0; } ``` 在上述代码中,`call` 函数通过基类指针调用虚函数 `f1`。具体过程如下: - 编译器生成的汇编代码会从 `b` 指向的对象中取出 `vptr`。 - 使用 `vptr` 查找虚函数表中 `f1` 的地址。 - 调用实际的 `Derived::f1` 函数[^2]。 #### 4. 构造与析构过程中的虚函数行为 - **构造函数**:在构造过程中,对象的 `vptr` 会被初始化为当前类的虚函数表地址。因此,在构造函数调用虚函数时,只会调用当前类的虚函数实现,而不是派生类的实现。 - **析构函数**:析构函数可以是虚函数,且建议将基类的析构函数声明为虚函数,以确保在通过基类指针删除派生类对象时正确调用派生类的析构函数。 ### 示例代码 以下代码展示了虚函数在构造析构过程中的行为: ```cpp #include <iostream> class Base { public: Base() { std::cout << "Base::ctor" << std::endl; } virtual ~Base() { std::cout << "Base::dtor" << std::endl; } virtual void f() { std::cout << "Base::f" << std::endl; } }; class Derived : public Base { public: Derived() { std::cout << "Derived::ctor" << std::endl; } ~Derived() override { std::cout << "Derived::dtor" << std::endl; } void f() override { std::cout << "Derived::f" << std::endl; } }; void call(Base* obj) { obj->f(); } int main() { Derived d; Base* b = &d; call(b); // 输出 Derived::f return 0; } ``` 在上述代码中,`call` 函数调用的是 `Derived::f`,因为 `b` 指向的对象是 `Derived` 类型,其 `vptr` 指向 `Derived` 的虚函数表。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值