共计 1564 个字符,预计需要花费 4 分钟才能阅读完成。
从一次崩溃说起
上周排查了一个诡异的崩溃问题:程序在退出时随机发生段错误。通过 core dump 分析,发现是某个全局对象的析构函数中访问了已释放的内存。根本原因令人哭笑不得——这个对象的成员变量依赖另一个全局对象,而 C ++ 标准不保证全局对象的销毁顺序!

// 反面教材示例
Logger g_logger; // 先析构
class DataProcessor {
std::vector<std::string>* m_cache;
public:
DataProcessor() { m_cache = new std::vector<std::string>(); }
~DataProcessor() {g_logger.write("Cleaning cache"); // 崩溃点!delete m_cache;
}
} g_processor;
构造函数调用链揭秘
基础调用顺序
- 分配内存(栈或堆)
- 按声明顺序初始化成员变量(注意:与初始化列表顺序无关!)
- 执行构造函数体
class Demo {
int m_a, m_b;
public:
// 初始化列表顺序不影响实际初始化顺序
Demo() : m_b(1), m_a(2) {cout << "Constructor body" << endl;}
};
/* 实际执行顺序:1. m_a = 2
2. m_b = 1
3. 打印 "Constructor body"
*/
继承体系下的调用规则
多重继承时,基类构造函数按继承声明顺序调用(从左到右):
class Base1 {public: Base1() {cout << "Base1" << endl;} };
class Base2 {public: Base2() {cout << "Base2" << endl;} };
// 继承顺序决定构造顺序
class Derived : public Base1, public Base2 {
public:
Derived() { cout << "Derived" << endl;}
};
/* 输出顺序:Base1
Base2
Derived
*/
虚函数调用陷阱
构造函数执行期间,虚函数机制尚未完全建立。以下代码会出人意料地调用基类版本:
class Base {
public:
Base() { log(); } // 危险操作!virtual void log() { cout << "Base" << endl;}
};
class Derived : public Base {
public:
void log() override { cout << "Derived" << endl;}
};
// 实际输出:Base 而非 Derived!
RAII 的正确打开方式
资源获取应放在构造函数最后阶段,确保其他成员已初始化完毕:
class SafeFile {
FILE* m_fp;
std::string m_path;
public:
SafeFile(const std::string& path)
: m_path(path), // 先初始化基本成员
m_fp(nullptr) {m_fp = fopen(path.c_str(), "r"); // 最后获取资源
if(!m_fp) throw std::runtime_error("Open failed");
}
~SafeFile() {if(m_fp) fclose(m_fp);
}
};
初始化列表 vs 构造函数赋值
性能对比实验(使用 100 万次循环测试):
| 方式 | 耗时 (ms) |
|---|---|
| 初始化列表 | 125 |
| 构造函数体内赋值 | 217 |
原因:初始化列表直接构造成员,而赋值操作会先调用默认构造再调用 operator=
四条黄金法则
- 成员变量声明顺序就是初始化顺序
- 基类构造优先于成员变量初始化
- 绝对不要在构造函数 / 析构函数中调用虚函数
- 使用 RAII 管理所有资源
思考题
当我们需要在 DLL 边界传递对象时:
– 如何保证构造 / 析构函数在同一个模块中执行?
– 虚函数表指针在不同模块间是否有效?
– 内存分配 / 释放如何跨模块匹配?
(提示:考虑使用抽象接口工厂模式)
正文完
