首页 > 系统相关 >在C++里如何释放内存的时候不调用对象的析构函数?

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

时间:2024-07-24 18:28:36浏览次数:18  
标签:函数 union C++ 内存 析构 NoDestructor new

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

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

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

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

placement new方式

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

#include <new> // 需要包含头文件new

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

标签:函数,union,C++,内存,析构,NoDestructor,new
From: https://blog.csdn.net/hebhljdx/article/details/140658457

相关文章

  • 【C语言】动态内存管理详解!!!
    目录为什么存在动态内存分配?动态内存函数的介绍常见的动态内存错误几个经典的笔试题 C/C++程序的内存开辟柔性数组为什么存在动态内存分配?在动态内存函数之前,我们知道的内存开辟有两种。1. 在栈空间上开辟四个字节。intval=20;2. 在栈空间上开辟10个字节的......
  • C++this指针--指针的介绍用法以及相关注意事项
    什么是this指针this指针是在C++中用来指向当前对象的特殊指针。它是每个非静态成员函数的隐式参数,指向调用该函数的对象。在C++类的成员函数中,除了静态成员函数外,每个成员函数都有一个隐含的this指针,它指向调用该函数的对象。这个指针可以让成员函数访问调用它的对象......
  • Java内存模型全解析:解决共享变量可见性与指令重排难题
    本期说一下Java内存模型(JavaMemoryModel,JMM)及共享变量可见性问题。“以下内容出自本人整理的面试秘籍。点击此处,无套路免费获取面试秘籍JMM是什么?答:Java内存模型(JavaMemoryModel,JMM)抽象了线程和主内存之间的关系就比如说线程之间的共享变量必须存储在主内存......
  • 关于内存条选择的一些看法
    先叠甲,只是个人看法以及近期学的知识,欢迎大家讨论,我的知识面也不够全面电脑的内存条分为DDR4与DDR5需要与主板的卡槽对应,挑选需要关注三个核心:容量,频率,参数。先说一下容量,内存条的容量的选择关乎于你的日常工作量,需不需要多开各类办公软件,以及你玩什么类型的游戏,容量越大越流畅......
  • 为什么C++模板只能在头文件中实现
    为什么C++模板只能在头文件中实现答案:模板的实现并非必须在头文件中。bug再现:当我尝试将模板的定义和实现分别保存在头文件(Foo.h)和实现文件(Foo.cpp)中时,程序在链接时报错:错误 LNK2019 无法解析的外部符号"public:void__cdeclFoo<int>::doSomething(int)"(?doSome......
  • C++ opencv putText
    C++opencv putText  #include<opencv2/opencv.hpp>intmain(){//创建一个空白图像cv::Matimg(400,400,CV_8UC3,cv::Scalar(255,255,255));//设置文本内容std::stringtext="Hello,OpenCV!";//设置文本起始坐标(左下角坐标)......
  • Java 内存模型
    Author:ACatSmilingSince:2024-07-24概念Java内存模型:JavaMemoryModel,简称JMM,是Java语言中定义的一组规则和规范,用于解决多线程环境下的内存可见性和有序性问题。JMM确定了线程之间如何通过内存进行交互,并规定了变量的读取和写入操作的行为。JMM能干吗?通过JMM来......
  • 记一次Echart 内存泄露问题排查
    最近发现一个web项目总是莫名其妙的内存增长,然后进行定位后来发现问题大概率出在Eharts上。于是乎就开始搜索关于echarts内存增长的一些例子,但是都没有结果。其中翻博客时发现甚至有人换成一维数组就问题就解决了,当然这个试过之后对我来说解决不了问题。(这样能解决掉也真是离......
  • 内存管理-22-KASLR
    基于msm-5.4一、简介1.什么是KASLRKASLR是kerneladdressspacelayoutrandomization的缩写,直译过来就是内核地址空间布局随机化。KASLR技术允许将kernelimage映射到vmalloc区域的任何位置(待确认哦)。2.引入KASLR的原因没有KASLR的时候,kernelimage都会映射到一个固......
  • c++ 《小技巧》
    使用swap回收多余空间#include<vector>#include<iostream>usingnamespacestd;intmain(){vector<int>v;for(inti=0;i<100000;++i){v.push_back(i);}cout<<v.size()<<endl;//100000cout<......