共计 1450 个字符,预计需要花费 4 分钟才能阅读完成。
对象创建与构造函数的基本流程
当我们在 C ++ 中创建一个对象时,编译器会在背后执行一系列复杂的操作。构造函数作为对象生命周期的起点,其调用时机直接关系到程序的正确性和性能。

- 内存分配阶段:
- 对于栈对象,编译器会自动在栈上分配内存
- 对于堆对象,需要显式调用 new 运算符
-
全局 / 静态对象的内存在程序启动时就已经分配
-
构造函数调用阶段:
- 内存分配完成后立即调用构造函数
- 构造函数负责初始化对象的成员变量
- 构造函数执行期间对象的类型信息已经完全确定
不同内存分配方式的构造函数调用
栈分配对象
class MyClass {
public:
MyClass() { std::cout << "构造函数被调用 \n";}
};
void demo() {MyClass obj; // 构造函数在对象创建时自动调用}
- 构造函数在对象作用域开始时自动调用
- 析构函数在离开作用域时自动调用
堆分配对象
void demo() {MyClass* pObj = new MyClass(); // 1. 分配内存 2. 调用构造函数
delete pObj; // 1. 调用析构函数 2. 释放内存
}
- new 操作符将内存分配和构造函数调用合并
- 必须显式调用 delete 来触发析构和释放
全局 / 静态对象
MyClass globalObj; // 构造函数在 main()之前调用
void demo() {static MyClass staticObj; // 构造函数在第一次执行到此处时调用}
- 构造顺序在不同编译单元间是不确定的
- 析构顺序与构造顺序相反
继承体系中的构造函数调用
在继承关系中,构造函数的调用遵循特定规则:
- 普通继承:
- 先调用基类构造函数
- 然后调用成员对象的构造函数
-
最后调用派生类构造函数
-
虚继承:
- 虚基类的构造函数最先被调用
- 然后是非虚基类
- 最后是派生类
class Base {
public:
Base() { std::cout << "Base 构造 \n";}
};
class Derived : public Base {
public:
Derived() { std::cout << "Derived 构造 \n";}
};
// 输出顺序:// Base 构造
// Derived 构造
构造函数的高级用法
与 operator new 配合
void* raw = operator new(sizeof(MyClass)); // 仅分配内存
MyClass* obj = new(raw) MyClass(); // placement new 调用构造函数
成员初始化列表
class Example {
int a;
std::string b;
public:
Example() : a(42), b("hello") {} // 优于构造函数内赋值};
构造函数中的典型陷阱
- 构造函数内调用虚函数:
- 此时虚表可能未完全初始化
-
会调用当前类的实现而非派生类
-
构造函数抛出异常:
- 已构造的成员会被正确析构
-
但对象本身的内存需要妥善处理
-
初始化顺序依赖:
- 成员变量按声明顺序初始化
- 与初始化列表中的顺序无关
性能优化建议
- 对不会抛出异常的构造函数标记为
noexcept - 尽可能使用
constexpr构造函数 - 避免在构造函数中进行复杂计算
- 使用初始化列表而非赋值
内存布局图示
classDiagram
class MyClass {
+int data
+MyClass()}
note for MyClass " 内存布局:1. vptr (如果有虚函数)
2. data 成员
3. 填充字节(如有需要)"
思考题
- 如何设计线程安全的单例类构造函数?
- 移动构造函数与拷贝构造函数的调用时机有何不同?
在实际工程中,准确理解构造函数的调用时机可以帮助我们避免很多难以调试的问题。建议通过实际编码验证本文中的各种场景,加深理解。
正文完
