飞道的博客

C++:59---特殊工具与技术之控制内存分配(重载new和delete、定位new表达式)

342人阅读  评论(0)
  • 某些应用程序对内存分配有特殊的需要,因此我们无法将标准内存管理机制直接应用于这些程序。它们常常需要自定义内存分配的细节,比如使用关键字new将对象放置在特定的内存空间中。为了实现这一目的,应用程序需要重载new运算符和delete运算符以控制内存分配的过程

一、重载new和delete

new和delete的工作原理

  • 重载new和delete比普通的运算符重载做的工作要多,在重载之前先了解一下new和delete的工作原理
  • new的工作原理一般分为三步:
    • 第一步:new表达式调用一个名为operator new(或operator new[])的标准库函数。该函数分配一块足够大的、原始的、未命名的内存空间以便存储特定类型的对象(或对象的数组)
    • 第二步:编译器运行相应的构造函数以构造这些对象,并为其传入初始值
    • 第三步:对象被分配了空间并构造完成,返回一个指向该对象的指针

   
  1. std:: string *sp = new std:: string( "a value"); //分配并初始化一个string对象
  2. std:: string *arr = new std:: string[ 10]; //分配10个默认初始化的string对象
  • delete的工作原理一般分为二步:
    • 第一步:对所指对象或所指数组中的元素执行对应的析构函数
    • 第二步:调用名为operator delete(或operator delete[])的标准库函数释放内存空间

   
  1. delete sp; //销毁sp,释放sp所指的内存空间
  2. delete[] arr; //销毁数组中的所有元素,然后释放对应的内存空间

重载new和delete之后的调用顺序

  • 应用程序可以在全局作用域中定义operator new函数和operator delete函数,也可以将它们定义为成员函数

  • 如果被分配(释放)的对象是类类型,那么编译器调用new(或delete)的查找顺序为:
    • 先去全局作用域中查找应用程序释放重载了new(或delete),如果有,就使用
    • 如果全局作用域没有重载,那么再去类中查找是否有重载的new(或delete)成员函数,如果有,就使用
    • 如果也没有成员函数版本,那么最终才去使用标准库提供的new(或delete)
  • 我们也可以使用作用域运算符令new(或delete)忽略定义在类中的函数,而直接使用全局作用域中的版本。例如:

   
  1. :: new //查找全局作用域中的new
  2. :: delete //查找全局作用域中的delete

标准库接口

  • 标准库定义了operator new函数和operator delete函数的8个重载版本。其中:
    • 前4个版本可能会抛出异常
    • 后4个版本则不会抛出异常

  • nothrow_t结构:定义在new头文件中的一个struct,这个类型不包含任何成员
  • nothrow对象:定义在new头文件中的一个const对象,用户可以通过这个对象请求new的非抛出异常版本(可以参阅此文章中的“bad_alloc异常处理”:https://blog.csdn.net/qq_41453285/article/details/95603693

重载的new和delete成员版本是静态的

  • 我们可以将new和delete重载为全局版本的或者是类成员版本的。如果重载的是类成员版本的,那么成员版本的new和delete将默认是静态的(我们无需为其声明static)
  • 原因:
    • 因为new用在对象构造之前,delete用在对象销毁之后,所以这两个成员必须是静态的
    • 而且它们不能操纵类的任何数据成员

重载new

  • 对于标准库的operator new和operator new[]来说:

    • 返回值类型必须是void*

    • 第一个形参的类型必须是size_t,且该形参不能含有默认实参

  • 如果我们自定义operator new和operator new[]:

    • 可以为它们提供额外的形参。此时,用到这些自定义函数的new表达式必须使用new的定位形式(见下文介绍),将实参传给新增的形参

  • 但是需要注意,下面的这个函数不能被用户重载(这种形式只供标准库使用,不能被用户重载):

void *operator new(size_t, void*);  //不允许重载这个版本

重载delete

  • 对于标准库的operator delete和operator delete[]来说:

    • 返回值类型必须是void

    • 第一个形参的类型必须是void*。执行一条delete表达式将调用相应的operator函数,并用指向待释放内存的指针来初始化void*形参

  • 如果我们自定义operator delete和operator delete[]:

    • 与析构函数类似,operator delete不允许抛出异常。所以当我们重载这些运算符时,必须使用noexcept异常说明符指定其不抛出异常

    • 当我们将delete或delete[]定义为类的成员时,该函数可以包含另外一个类型为size_t的形参:

      • 此时,该形参的初始值是第一个形参所指对象的字节数

      • size_t形参可用于删除继承体系中的对象。如果基类有一个虚析构函数,则传递给delete的字节数将因待删除指针所指对象的动态类型不同而有所区别。而且,实际运行的delete函数版本也由对象的动态类型决定

malloc函数、free函数

  • 我们虽然重载了operator new和operator delete函数,但是函数最终还是需要进行内存的分配和释放,那么当使用到内存的分配和释放时,可以调用C函数库的malloc和free函数
  • malloc和free函数参阅:https://blog.csdn.net/qq_41453285/article/details/88880389
  • 下面是重载operator new和operator delete的一种简单形式,例如:

   
  1. void *operator new(std::size_t size)
  2. {
  3. if ( void *mem = malloc(size))
  4. return mem;
  5. else
  6. throw std::bad_alloc();
  7. }
  8. void operator delete(void *mem)noexcept
  9. {
  10. free(mem);
  11. }

二、定位new表达式(placement new)

  • 定位new在另外一篇文章也单独介绍过,可以参阅:https://blog.csdn.net/qq_41453285/article/details/103547699
  • operator new和operator delete可以用来分配和释放空间,但是不会构造或销毁对象(需要我们自己在函数中书写)。但是,我们可以使用new的定位new(placement new)形式构造对象
  • 定位new为分配函数提供了额外信息,我们可以使用定位new传递一个地址,此时定位new的形式如下所示:
    • place_address:是一个指针
    • initializers:提供一个(可能为空的)以逗号分隔的初始值列表,该初始值列表将用于构造新分配的对象

  • 当仅通过一个地址值调用时,定位new使用operator new(size_t,void*)分配内存。这是一个我们无法自定义的operator new版本。该函数不分配任何内存,只是简单地返回指针实参;然后由new表达式负责在指定的地址初始化对象以完成整个工作。事实上,定位new允许我们在一个特定的、预先分配好的内存地址上构造函数

  • 定位new类似于allocator类的construct成员,但是有一个重要的区别:
    • 我们传递给construct的指针必须指向同一个allocator对象分配的空间
    • 但是传给定位new的指针无须指向operator new分配的内存。实际上,传给定位new表达式的指针甚至不需要指向动态内存

显式的析构函数调用

  • 类似于定位new和allocate类一样,对析构函数的显式调用也与使用destroy一样
  • 我们既可以通过对象调用析构函数,也可以通过对象的指针或引用调用析构函数,与调用成员函数类似:

   
  1. std:: string *sp = new std:: string( "a value");
  2. sp->~ string(); //调用析构函数销毁对象,但是sp所指的内存没有释放
  • 和调用destroy类似,调用析构函数可以销毁对象但是不会释放内存空间。因此我们可以反复利用这个内存空间。例如:

   
  1. std:: string *sp = new std:: string( "a value");
  2. sp->~ string(); //调用析构函数销毁对象,但是sp所指的内存没有释放
  3. std:: string *sp= new std:: string( "new value"); //重新使用sp所指的内存空间进行对象的构造


转载:https://blog.csdn.net/qq_41453285/article/details/104690501
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场