共计 1612 个字符,预计需要花费 5 分钟才能阅读完成。
构造函数调用链
当创建一个类对象时,构造函数的调用遵循特定规则:

- 基类构造函数(按继承顺序)
- 成员变量构造函数(按声明顺序)
- 派生类构造函数体
class Base {
public:
Base() { std::cout << "Base constructor\n";}
};
class Member {
public:
Member() { std::cout << "Member constructor\n";}
};
class Derived : public Base {
Member m;
public:
Derived() { std::cout << "Derived constructor\n";}
};
执行 Derived d; 将输出:
Base constructor
Member constructor
Derived constructor
成员初始化列表的重要性
成员初始化列表决定了成员变量的构造顺序,且能避免不必要的默认构造 + 赋值操作:
class Resource {
int* ptr;
public:
Resource(int size) : ptr(new int[size]) {}
~Resource() { delete[] ptr; }
};
class Widget {
Resource res1;
Resource res2;
public:
// 初始化列表顺序应与成员声明顺序一致
Widget(int a, int b) : res1(a), res2(b) {}};
警告:初始化列表顺序必须与成员声明顺序一致,否则可能导致隐蔽的初始化依赖问题
虚函数的构造时行为
在构造函数中调用虚函数不会表现出多态特性,因为此时派生类尚未完成构造:
class Shape {
public:
Shape() { draw(); } // 危险操作!virtual void draw() { std::cout << "Shape::draw()\n"; }
};
class Circle : public Shape {
public:
void draw() override { std::cout << "Circle::draw()\n"; }
};
创建 Circle 对象时仍会调用Shape::draw()
完整的生命周期流程图
- 分配内存
- 基类构造(多重继承按声明顺序)
- 成员构造(按类内声明顺序)
- 构造函数体执行
- 对象使用期
- 析构函数调用(与构造完全逆序)
- 派生类析构函数体
- 成员析构(逆声明顺序)
- 基类析构(逆继承顺序)
- 释放内存
最佳实践准则
- 构造函数中避免调用虚函数,如需多态行为可使用工厂模式
- 对存在初始化依赖的成员变量:
- 调整声明顺序
- 或改用指针并在构造函数体中初始化
- 遵循 RAII 原则,所有资源获取:
- 在构造函数中完成
- 在析构函数中释放
思考题
- 对于
class D : public B1, public B2,当 B1 和 B2 有共同基类时,构造顺序如何变化? - 如果在构造函数中抛出异常,哪些析构函数会被调用?
典型陷阱案例
class FileHandler {
FILE* file;
Logger& logger; // 引用成员必须初始化
public:
FileHandler(const char* name, Logger& log)
: logger(log), file(fopen(name, "r")) {if(!file) throw std::runtime_error("Open failed");
}
~FileHandler() { if(file) fclose(file); }
};
这段代码存在两个隐患:
1. 如果 fopen 失败抛出异常,logger引用已经完成初始化但无法使用
2. 成员初始化顺序实际是 file 先于logger(与声明顺序相反)
修正方案:
private:
Logger& logger;
FILE* file;
// 初始化列表保持相同顺序
通过本文的梳理,读者应该能够:
– 准确预测任意类结构的函数调用顺序
– 避免常见的对象生命周期管理错误
– 编写出更安全的资源管理代码
建议读者在自己的开发环境中尝试修改示例代码,观察不同场景下的函数调用顺序,这将有助于建立直观理解。
正文完
