当前位置: 首页 > news >正文

在C++里如何释放内存的时候不调用对象的析构函数?

今天,看到一个有趣的面试题,问题是:在C++里如何释放内存的时候不调用对象的析构函数?

之所以有趣,是因为这个问题违反了C++中资源管理的RAII(资源获取即初始化),它要求资源的释放应当和对象的生命周期紧密相关。在正常情况下,当对象离开其作用域时,它的析构函数被调用,以释放它所管理的资源,比如内存、文件句柄或网络连接等。

然而,这个问题提出了一种特殊情况,在出于性能优化、特殊的内存管理策略,或是为了与低级操作系统功能或硬件直接交互的需求。在这些情况下,我们可能需要释放对象占用的内存,但又不希望执行其析构函数。

在C++中,如果真的需要这么做,有什么方法呢?我们一起来梳理看看。

placement new方式

可以通过使用 placement new 来在预先分配的内存块上构造对象,然后不显式调用它的析构函数。

#include <new> // 需要包含头文件newchar buffer[sizeof(MyClass)]; // 分配足够的内存来存放MyClass对象
MyClass* obj = new(buffer) MyClass(); // 在buffer上构造对象// ... 使用obj// 显式调用析构函数是这样的:
// obj->~MyClass();// 如果你不调用析构函数,对象的生命周期将结束,
// 但是它的析构函数不会被执行。但是由于对象用的是栈上的内存,内存会正常释放。

使用 placement new 需要你非常明确地知道自己在做什么,因为这样做会绕过正常的构造和析构过程。这可能导致资源泄露、内存未正确释放或其他未定义行为。

MyClass* obj = new MyClass(); // 常规地分配对象
// ... 在这里使用obj
operator delete(obj); // 释放内存但不调用析构函数

placement new的chromium的封装

chromium里面对placement new的设计模式提供了一套模板支持,如下:

template <typename T>
class NoDestructor {public:// Not constexpr; just write static constexpr T x = ...; if the value should// be a constexpr.template <typename... Args>explicit NoDestructor(Args&&... args) {new (storage_) T(std::forward<Args>(args)...);}// Allows copy and move construction of the contained type, to allow// construction from an initializer list, e.g. for std::vector.explicit NoDestructor(const T& x) { new (storage_) T(x); }explicit NoDestructor(T&& x) { new (storage_) T(std::move(x)); }NoDestructor(const NoDestructor&) = delete;NoDestructor& operator=(const NoDestructor&) = delete;~NoDestructor() = default;const T& operator*() const { return *get(); }T& operator*() { return *get(); }const T* operator->() const { return get(); }T* operator->() { return get(); }const T* get() const { return reinterpret_cast<const T*>(storage_); }T* get() { return reinterpret_cast<T*>(storage_); }private:alignas(T) char storage_[sizeof(T)];
};//使用方法:
void foo() {// std::string析构函数不会被调用,即便出了foo的scopeNoDestructor<std::string> s("Hello world!");
}

上述代码的细节说明:

  • new (storage_) T(x) 使用了 placement new 操作符。这个操作符的语法是 new (address) Type(arguments),它允许你在一个已经分配好的内存地址 address 上直接构造一个 Type 类型的对象。这个操作不会分配新的内存,而是使用你提供的内存地址。在这个例子中,storage_ 是一个足够大的字符数组,能够存放 T 类型的对象,而 alignas(T) 确保了这个数组的对齐方式与 T 类型相同。

  • T(x) 是调用 T 类型对象的复制构造函数,以 x 为参数来构造一个新的 T 实例。

NoDestructor 类的 storage_ 成员中直接构造一个 T 类型的对象。因为它使用了 placement new,所以不会为这个 T 对象分配新的堆内存,而是利用 storage_ 这块已经预留的栈内存。这也意味着 T 对象的析构函数不会在 NoDestructor 对象被销毁时自动调用,这正是 NoDestructor 的设计目的。

union方式

union类型的析构函数在执行body之后不会调用variant member对象的析构函数

#include <iostream>
template<class T>
union NoDestructor{T value;~Forget(){}
};struct A{~A(){std::cout<<"destroy A\n";}
};int main(){auto f =  NoDestructor<A>{A{}}; // 不会执行A的析构// f.value.~A(); // 需要手动调用析构, 否则不会析构
}

union 是一种特殊的类类型,它允许你在同一个内存地址存储不同的数据类型,但是一次只能使用其中一个成员。这意味着 union 的所有成员都共享同一块内存空间,所以其大小等于其最大成员的大小。

union 有一些限制,其中之一就是所有的成员函数必须是非虚(non-virtual)的。理解这一点需要知道虚函数和虚函数表(vtable)的工作原理。在C++中,当类有一个或多个虚函数时,编译器会为该类创建一个虚函数表。这个虚函数表是一个函数指针数组,用于支持动态绑定,也就是在运行时决定调用哪个函数。每个有虚函数的对象都会含有一个指向虚函数表的指针,通常称为vptr。在 union 的情况下,由于所有成员共享同一块内存空间,如果 union 允许虚函数存在,那么vptr的存储位置就会和 union 的其他成员发生冲突,导致不确定的行为。此外,由于 union 的成员可以是不同的数据类型,编译器也无法确定应该使用哪个成员的虚函数表。

正因为这些原因,C++标准规定 union 不能包含虚函数。所有的成员函数,包括构造函数和析构函数,都必须是非虚的。这样就保证了 union 成员之间不会发生内存覆盖,同时也避免了动态绑定相关的复杂性。

在C++11及以后的版本中,union 可以包含非静态数据成员的构造函数和析构函数,但是仍然不能包含虚函数。如果 union 包含一个或多个非平凡的成员(比如包含自己的构造函数或析构函数的类类型成员),那么你需要负责正确地构造和析构这些成员,因为 union 不会自动为你做这些事情。

利用union的这个特性,就能轻松实现“释放内存的时候不调用对象的析构函数”。

但是,在使用union的时候,这个特性反而是一个坑,需要小心处理。一般来说,需要手动判断哪个成员是有效的,并显式地调用该成员的析构函数。类似这样:

union U {Type1 member1;Type2 member2;// ...~U() {switch (active_member) {case Member1:member1.~Type1();  // 显式调用析构函数break;case Member2:member2.~Type2();  // 显式调用析构函数break;// ...}}
};

jmp 方式

直接通过longjmp,跳出作用域,避免析构函数调用:

#include <setjmp.h>
int main()
{jmp_buf buf {};if (setjmp(buf) == 0) {string s(p); // 对象s不会析构longjmp(buf, 1);   }
}

不过通过longjmp没有很好的封装形式,语义上也过于隐晦,因此不常用于这个场景。

结语

这个面试题既有趣也有深度,它提供了一个探讨C++语言内存和资源管理机制的机会,同时考察面试者对C++底层细节的了解程度。然而,在实际的软件开发中,绝大多数情况下都应该遵循RAII原则,让析构函数自动管理资源的释放。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Final Draft for Mac v13.1.0激活版:专业剧本写作软件
  • 【Python】基础学习技能提升代码样例2:小功能块
  • UE5C++中,NewObject<>()和CreateDefaultSubobject<>()的区别
  • 网络通信---UDP
  • C语言 写一个函数days,实现某日在本年中是第几天计算。
  • c++中grpc简单使用---函数介绍及其代码演示
  • 如何处理selenium Webdriver中的文本框?
  • Linux环境docker部署Firefox结合内网穿透远程使用浏览器测试
  • SpringBoot 日志
  • C:图案打印
  • C++——QT:保姆级教程,从下载到安装到用QT写出第一个程序
  • Android串口开发及读取完整数据的解决方法
  • Vite项目中根据不同打包命令配置不同的后端接口地址,proxy解决跨域
  • Linux中的文件操作
  • 学习java的设计模式
  • Eureka 2.0 开源流产,真的对你影响很大吗?
  • Git 使用集
  • js算法-归并排序(merge_sort)
  • thinkphp5.1 easywechat4 微信第三方开放平台
  • vue 配置sass、scss全局变量
  • windows下mongoDB的环境配置
  • 表单中readonly的input等标签,禁止光标进入(focus)的几种方式
  • 记一次和乔布斯合作最难忘的经历
  • 网页视频流m3u8/ts视频下载
  • 这几个编码小技巧将令你 PHP 代码更加简洁
  • 新年再起“裁员潮”,“钢铁侠”马斯克要一举裁掉SpaceX 600余名员工 ...
  • ​​​​​​​Installing ROS on the Raspberry Pi
  • ‌前端列表展示1000条大量数据时,后端通常需要进行一定的处理。‌
  • #控制台大学课堂点名问题_课堂随机点名
  • #我与Java虚拟机的故事#连载19:等我技术变强了,我会去看你的 ​
  • (2)MFC+openGL单文档框架glFrame
  • (2024,Vision-LSTM,ViL,xLSTM,ViT,ViM,双向扫描)xLSTM 作为通用视觉骨干
  • (LeetCode 49)Anagrams
  • (附源码)计算机毕业设计ssm高校《大学语文》课程作业在线管理系统
  • (每日持续更新)jdk api之FileReader基础、应用、实战
  • (十二)springboot实战——SSE服务推送事件案例实现
  • (十一)手动添加用户和文件的特殊权限
  • (算法)硬币问题
  • (微服务实战)预付卡平台支付交易系统卡充值业务流程设计
  • (转)jdk与jre的区别
  • (状压dp)uva 10817 Headmaster's Headache
  • .bat批处理(九):替换带有等号=的字符串的子串
  • .gitattributes 文件
  • .locked1、locked勒索病毒解密方法|勒索病毒解决|勒索病毒恢复|数据库修复
  • .NET 8.0 中有哪些新的变化?
  • .NET CORE 3.1 集成JWT鉴权和授权2
  • .net 程序发生了一个不可捕获的异常
  • .NET/C# 异常处理:写一个空的 try 块代码,而把重要代码写到 finally 中(Constrained Execution Regions)
  • .net安装_还在用第三方安装.NET?Win10自带.NET3.5安装
  • .NET大文件上传知识整理
  • .vue文件怎么使用_vue调试工具vue-devtools的安装
  • @AutoConfigurationPackage的使用
  • @JSONField或@JsonProperty注解使用
  • [ CTF ] WriteUp-2022年春秋杯网络安全联赛-冬季赛
  • [ 常用工具篇 ] AntSword 蚁剑安装及使用详解