在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原则,让析构函数自动管理资源的释放。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/387151.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

自动化测试 pytest 中 scope 限制 fixture使用范围!

导读 fixture 是 pytest 中一个非常重要的模块&#xff0c;可以让代码更加简洁。 fixture 的 autouse 为 True 可以自动化加载 fixture。 如果不想每条用例执行前都运行初始化方法(可能多个fixture)怎么办&#xff1f;可不可以只运行一次初始化方法&#xff1f; 答&#xf…

17.延迟队列

介绍 延迟队列&#xff0c;队列内部是有序的&#xff0c;延迟队列中的元素是希望在指定时间到了以后或之前取出和处理。 死信队列中&#xff0c;消息TTL过期的情况其实就是延迟队列。 使用场景 1.订单在十分钟内未支付则自动取消。 2.新创建的店铺&#xff0c;如果十天内没…

【Ant Design Vue的更新日志】

🌈个人主页: 程序员不想敲代码啊 🏆CSDN优质创作者,CSDN实力新星,CSDN博客专家 👍点赞⭐评论⭐收藏 🤝希望本文对您有所裨益,如有不足之处,欢迎在评论区提出指正,让我们共同学习、交流进步! 以下是Ant Design Vue的更新日志 版本1.7.0(发布日期:2023年4月) …

TCP/IP协议——使用Socket套接字实现

目录 Socket 使用Socket实现TCP客户端和服务器的过程 使用Socket搭建TCP服务器 线程优化 向客户端发送消息 连接的断开 客户端主动断开 服务端主动断开 服务器完整的程序 使用Socket编写客户端程序连接TCP服务器 Socket Socket是一种网络通信协议&#xff0c;它允许…

渗透测试:筑牢网络安全的坚固防线

在当今这个互联网高度发达的时代&#xff0c;网络安全已成为维护社会稳定和经济发展的重要基石。随着互联网的普及&#xff0c;网络攻击手段日益复杂多变&#xff0c;各类安全威胁层出不穷。为了有效应对这些挑战&#xff0c;渗透测试作为一种重要的安全测试与评估方法&#xf…

arduino程序-数字输出-学用led(led电路及相关函数)(基础知识)

arduino程序-数字输出-学用led&#xff08;led电路及相关函数&#xff09;&#xff08;基础知识&#xff09; 1-10 数字输出1-学用ledLED发光二极管LED电压特性电阻 1-11 数字输出arduino控制LEDLed与arduino连接电路图高电平及低电平含义 1-10 数字输出1-学用led 元器件初步介…

关于 AGGLIGATOR(猛禽)网络宽频聚合器

AGGLIGATOR 是一个用于多个链路UDP/IP带宽聚合的工具软件&#xff0c;类似MTCP的作用&#xff0c;不过它是针对UDP/IP宽频聚合的。 举个例子&#xff1a; 中国大陆有三台公网服务器&#xff0c;中国香港有一台大带宽服务器。 那么&#xff1a; AGGLIGATOR 允许中国大陆的客户…

Day7-指针专题二

1. 字符指针与字符串 C语言通过使用字符数组来处理字符串 通常&#xff0c;我们把char数据类型的指针变量称为字符指针变量。字符指针变量与字符数组有着密切关系&#xff0c;它也被用来处理字符串 初始化字符指针是把内存中字符串的首地址赋予指针&#xff0c;并不是把该字符串…

独占电脑资源来执行一个应用

1. 背景 在人工智能时代&#xff0c;随着神经网络的发展&#xff0c;训练人工智能模型需要越来越多的硬件资源&#xff0c;例如&#xff0c;利用10万条棋局数据、使用一台PC电脑、完整地训练一次确定性神经网络五子棋模型&#xff0c;需要花费一年半的时间。随着训练数据的增长…

<PLC><HMI><汇川>在汇川HMI画面中,如何为UI设置全局样式?

前言 汇川的HMI软件是使用了Qt来编写的,因此在汇川的HMI程序编写过程,是支持使用qt的样式来自定义部件样式的,即qss格式。 概述 汇川的软件本身提供三个系统的style样式,我们可以直接使用,但是,如果系统提供的样式不符合你的需求,那么你可以对其进行修改,或者自己新建…

进程间通信与线程间通信的方法汇总

目录 一、进程间通信机制 管道(pipe)&#xff1a; 命名管道(FIFO)&#xff1a; 消息队列(MQ)&#xff1a; 信号量(semaphore)&#xff1a; 共享内存(shared memory)&#xff1a; 信号(signal)&#xff1a; 内存映射(mapped memory)&#xff1a; 内存映射和共享内存的区…

NFTScan 正式上线 ERC404 浏览器和 NFT API 数据服务

近日&#xff0c;NFTScan 团队正式对外发布了 ERC404 浏览器&#xff0c;将为 ERC404 生态的 NFT 开发者和用户提供简洁高效的 NFT 数据搜索查询服务。NFTScan 作为全球领先的 NFT 数据基础设施服务商&#xff0c;帮助用户更方便地访问和分析 ERC404 相关的 NFT 数据&#xff0…

git使用总结

概述 简介 Git是一种代码托管技术&#xff0c;很多代码托管平台也是基于Git来实现的。 Git可以帮我们做到很多的事情&#xff0c;比如代码的版本控制&#xff0c;分支管理等。 网址 git官网&#xff1a;https://git-scm.com/ 版本控制系统【VCS】 可以完整保存项目的快照&#…

力扣Hot100-543二叉树的直径

给你一棵二叉树的根节点&#xff0c;返回该树的 直径 。 二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。 两节点之间路径的 长度 由它们之间边数表示。 示例 1&#xff1a; 输入&#xff1a;root [1,2,3,4,5] 输出&a…

【Vue】权限控制

权限管理 分类&#xff1a; 页面权限功能(按钮)权限接口权限 vue3-element-admin 的实现方案 一般我们在业务中将 路由可以分为两种&#xff0c;constantRoutes 和 asyncRoutes。 constantRoutes&#xff1a; 代表那些不需要动态判断权限的路由&#xff0c;如登录页、404(或…

Skywalking 入门与实战

一 什么是 Skywalking? Skywalking 时一个开源的分布式追踪系统&#xff0c;用于检测、诊断和优化分布式系统的功能。它可以帮助开发者和运维人员深入了解分布式系统中各个组件之间的调用关系、性能瓶颈以及异常情况&#xff0c;从而提供系统级的性能优化和故障排查。 1.1 为…

嵌入式初学-C语言-八

#接嵌入式初学-C语言-七# 分支结构 分支结构&#xff1a;又被称之为选择结构 选择结构的形式 多分支 语法&#xff1a; if(条件1) { 语句1; } else if(条件2) { 语句2; } ... else { 语句n1; }案例&#xff1a; #include <stdio.h> int main() { // 需求&#xff…

Apache、nginx

一、Web 1、概述 Web&#xff1a;为⽤户提供的⼀种在互联⽹上浏览信息的服务&#xff0c;Web 服务是动态的、可交互的、跨平台的和图形化的。 Web 服务为⽤户提供各种互联⽹服务&#xff0c;这些服务包括信息浏览服务&#xff0c;以及各种交互式服务&#xff0c;包括聊天、购物…

线程的同步互斥

互斥 互斥保证了在一个时间内只有一个线程访问一个资源。 先看一段代码&#xff1a;三个线程同时对全局变量val进行--&#xff0c;同时val每自减一次其线程局部存储的全局变量 #include <iostream> #include <thread> #include <vector> #include <uni…