外观
Stage07|Modern C++ Foundation
Lesson083|Class、Object、构造、析构与对象生命周期
Tags: #C++17 #Class #Constructor #DestructorDifficulty: ⭐⭐⭐⭐☆
一、问题场景:为什么成员已经“初始化”却仍出错
cpp
class TextureView {
int width_;
std::vector<int> pixels_;
public:
TextureView(int width) {
width_ = width;
pixels_.resize(width_);
}
};构造函数体执行前,成员已经按声明顺序完成初始化。上面的代码先默认构造成员,再赋值或 resize;更准确的写法是成员初始化列表。
二、本课目标
- 理解类型、对象、存储和生命周期的区别。
- 掌握构造、析构和成员初始化顺序。
- 理解默认构造、委托构造和
explicit。 - 识别继承析构和虚函数风险。
- 设计始终处于有效状态的类。
三、回到 TaskSystem:把任务从字段变成对象
cpp
class Task {
public:
explicit Task(int id) : id_(id) {}
int id() const noexcept { return id_; }
private:
int id_;
};这个类的构造函数建立了不变量:Task 一旦构造成功就有合法 ID。后续加入回调、状态和结果时,仍要保持“对象可用”与“任务已经执行”是两个不同状态。
四、对象生命周期
text
取得存储
→ 开始对象生命周期
→ 构造基类
→ 按声明顺序构造成员
→ 执行构造函数体
→ 使用对象
→ 执行析构函数体
→ 逆序析构成员和基类
→ 释放或复用存储存储仍存在不代表其中对象的生命周期仍存在。这正是 placement new、对象池和跨语言对象映射容易出错的边界。
五、成员初始化顺序
cpp
class SpriteBatch {
std::size_t capacity_;
std::vector<int> vertices_;
public:
explicit SpriteBatch(std::size_t capacity)
: capacity_(capacity), vertices_(capacity) {}
};成员始终按类中声明顺序初始化,不按初始化列表书写顺序。让列表顺序与声明一致,开启 -Wreorder。
六、默认值与 explicit
cpp
class Seconds {
public:
explicit Seconds(double value) : value_(value) {}
private:
double value_{};
};explicit 阻止 Seconds s = 3.0 这类隐式转换,使单位和领域类型更清楚。成员默认初始化器则给所有构造路径提供安全基线。
七、析构与多态
如果通过基类指针删除派生对象,基类通常需要虚析构:
cpp
struct Task {
virtual ~Task() = default;
virtual void run() = 0;
};构造和析构期间调用虚函数不会表现为“完整派生对象的动态分派”。此时派生部分尚未构造或已经析构,不应依赖它。
八、可编译验证:记录顺序
cpp
#include <iostream>
#include <string>
struct Trace {
std::string name;
explicit Trace(std::string n) : name(std::move(n)) {
std::cout << "+ " << name << '\n';
}
~Trace() { std::cout << "- " << name << '\n'; }
};
struct Player {
Trace transform{"transform"};
Trace renderer{"renderer"};
Player() { std::cout << "Player body\n"; }
~Player() { std::cout << "Player destroy body\n"; }
};
int main() { Player player; }先预测输出,再编译运行。然后交换成员声明顺序,确认构造顺序随声明变化。
九、TaskSystem 中的生命周期问题
队列中的 Task 可能经历“创建、入队、取出、执行、完成、销毁”。如果析构函数释放了回调捕获的资源,执行线程必须在销毁前结束;如果通过基类接口销毁派生任务,基类需要虚析构。先画状态和 owner,再设计类。
十、故障诊断
text
构造中崩溃 → 成员初始化顺序、空依赖、异常
析构中崩溃 → 重复释放、依赖已先析构、虚调用
对象状态偶发无效 → 是否允许半初始化、移动后是否继续使用
通过基类释放异常 → 虚析构、所有权和 allocator 是否匹配十一、设计原则:建立类不变量
构造成功后,类应满足可描述的不变量。例如 Texture(width, height) 要么拥有匹配尺寸的像素缓冲,要么构造失败;不要创建一个必须再调用 init() 才勉强可用的半成品,除非框架生命周期确实强制如此。
十二、练习与答案
1. 初始化列表和构造函数体赋值有什么区别?
初始化列表直接构造成员;函数体赋值发生在成员已初始化之后,对不可默认构造、引用或 const 成员尤其关键。
2. 析构顺序为什么是构造的逆序?
后构造的对象可能依赖先构造对象,逆序析构可让依赖在使用期间仍存在。
3. 什么是类不变量?
对象在公开操作前后必须持续满足的约束。它让调用方不必反复猜对象是否完成初始化。
十三、本课总结
类不仅是一组字段和函数,更是对有效状态、生命周期和资源责任的约束。构造建立不变量,析构收回对象负责的资源。
