现代 C++ 编程实战
吴咏炜
前 Intel 资深软件架构师
36209 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 59 讲
现代 C++ 编程实战
15
15
1.0x
00:00/00:00
登录|注册

47 | 生存还是灭亡:浅论对象的生存期

你好,我是吴咏炜。
最近看到了一些实际的代码问题,让我觉得有必要来聊聊对象的生存期:一个对象是从什么时候开始被视作已经“存在”,到什么时候被视作已经“消亡”?

一个问题

我把我看到的问题简化和归纳一下。设有以下类:
class Obj {
public:
Obj();
~Obj();
void init();
void cleanup();
private:
// 有一些 POD 数据
};
然后,使用它的地方大致是这样子:
Obj* ptr = (Obj*)mmap(…);
ptr->init();
// 开始使用 *ptr
你看出问题在哪里了吗?
事实上,按照 C++ 标准的规定,以上代码具有未定义行为。原因是,代码里在 Obj 对象的生存期之外使用该对象了——Obj 对象根本还没“创建”出来呢。

普通对象的生存期

C++ 里的对象并不在程序运行期间一直存在。它们只在某段特定时间里在某个内存位置存在,这就是对象的生存期。生存期有开始,也有结束。
上面的 Obj 是“普通”类,具有构造函数、析构函数和访问控制。对于这样的对象,C++ 里规定,它的生存期从构造函数被调用开始,到析构函数被调用结束。
如果声明一个对象类型的变量(如 Obj obj;),显然对象的生存期不需要我们手工管理。而对于具有动态生存期的对象(它们的存储期和生存期可以分离),我们就需要对生存期予以特别关注。在最通常的用法里,new Obj() 会分配内存并调用构造函数(开始对象的生存期),delete ptr 则会调用析构函数(结束对象的生存期)并释放内存。但是,我们使用 ptr->~Obj() 提前结束对象的生存期、不释放内存也是可以的。事实上,在实现 vector 这样的容器时,我们就需要分离对象的生存期和存储期,以便重复利用对象所占用的内存。反过来,如果一个对象的析构函数是平凡的——析构动作不实际执行任何代码——那调用析构函数也并不必要。在这种情况下,当这个对象占用的内存被释放(或者内存被其他对象占用)时,这个对象的生存期也自然结束。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
  • 解释
  • 总结

1. C++对象的生存期从构造函数被调用开始,到析构函数被调用结束,对于动态生存期的对象,需要特别关注其生存期和存储期的分离。 2. 遵守C++规则能够获得更健壮的代码,而跳过构造函数可能导致对象的基类和数据成员未被正确初始化,引发实际的紊乱或崩溃。 3. 正确开始对象生存期的方式包括使用正常的对象创建方式,如直接定义变量、使用new表达式,以及使用布置new(placement new)来分配特殊存储空间。 4. 对象的生存期和存储期的分离是为了充分优化编译器的空间,避免未定义行为,确保代码的健壮性。 5. C++23标准引入了`start_lifetime_as`和`start_lifetime_as_array`函数模板,用于指定内存区域的隐式生存期对象或数组,并返回指向该对象或数组的指针。 6. 可平凡复制的对象需要确保能够安全地使用 `memcpy` 或 `memmove` 来复制对象,这是一个比 POD 更弱的要求。 7. 可平凡复制的类型不一定是 POD,而可平凡复制是一个比 POD 更弱的要求。 8. 在C++20之前,可以使用特定的代码来模拟实现`bit_cast`,用于将对象解释成另一种类型,并返回新对象。 9. 使用 `reinterpret_cast` 或 C 风格转型来对指针进行转换是不太安全的,违反了严格别名规则,具有未定义行为。 10. 在 C 的世界里,使用联合体来进行类型双关(type punning)是安全的,但在 C++ 中可能存在未定义行为。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《现代 C++ 编程实战》
新⼈⾸单¥59
立即购买
登录 后留言

精选留言

由作者筛选后的优质留言将会公开显示,欢迎踊跃留言。
收起评论
显示
设置
留言
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部