共计 1317 个字符,预计需要花费 4 分钟才能阅读完成。
一个由初始化顺序引发的血案
上周排查一个诡异的崩溃问题时,发现是基类构造函数中调用了尚未初始化的派生类成员指针。这个案例完美展示了理解调用顺序的重要性:

class Base {
public:
Base() {
// 错误!此时 Derived::m_data 还未初始化
m_data->append("hello");
}
virtual ~Base() = default;
protected:
std::string* m_data;
};
class Derived : public Base {
public:
Derived() : m_actualData("world") {m_data = &m_actualData; // 初始化顺序错误}
private:
std::string m_actualData;
};
构造函数调用规则图解
- 单一继承链 :
- 基类构造函数(从最顶层祖先开始)
- 成员变量构造函数(按声明顺序)
-
自身构造函数体
-
多重继承 (菱形继承示例):
classDiagram
A <|-- B
A <|-- C
B <|-- D
C <|-- D
调用顺序:A → B → C → D(虚继承会改变这个顺序)
关键代码示例
示例 1:基础调用顺序
class Demo {
public:
Demo() { cout << "Constructor" << endl;}
~Demo() { cout << "Destructor" << endl;}
};
int main() {
Demo d; // 输出顺序:Constructor
return 0; // 输出顺序:Destructor
}
示例 2:虚函数的影响
class Animal {
public:
Animal() { speak(); } // 危险操作!virtual ~Animal() {}
virtual void speak() { cout << "?" << endl;}
};
class Cat : public Animal {
public:
void speak() override { cout << "Meow" << endl;}
};
// 实际输出是 "?" 而非 "Meow",因为构造期间虚表未就绪
示例 3:RAII 最佳实践
class FileHandler {
public:
explicit FileHandler(const char* path)
: m_file(fopen(path, "r")) { // 初始化列表直接初始化
if (!m_file) throw std::runtime_error(...);
}
~FileHandler() { if (m_file) fclose(m_file); }
private:
FILE* m_file; // 资源获取即初始化
};
初始化列表 vs 构造函数赋值
- 初始化列表 :
- 直接调用拷贝构造函数
- 对 const 成员和引用类型唯一选择
-
执行时机更早
-
构造函数内赋值 :
- 先调用默认构造再调用 operator=
- 多出一次临时对象构造
- 基准测试显示有 5 -15% 性能差距
四条黄金法则
- 成员变量始终用初始化列表(包括基础类型)
- 构造函数中绝不调用虚函数
- 多继承时使用虚继承避免菱形问题
- 析构函数必须为虚函数(当类被继承时)
思考题
当需要在 DLL 边界传递对象时:
– 如何保证跨模块的内存分配 / 释放安全?
– 虚函数表在不同编译器版本间的兼容方案?
– 对象生命周期由哪方管理?
欢迎在评论区分享你的解决方案!
正文完
