深入了解C++中各种不同意义的new和delete

简介: 深入了解C++中各种不同意义的new和delete

new 到底做了什么

new是C++的一个关键字、操作符。

当我们执行Test* pt = new Test();这句代码时,实际上干了三件事情:

  1. 分配内存
  2. 调用Constructor函数
  3. 返回分配好的指针


为什么这么说呢?口说无凭眼见为实,请接着往下看。

通过VS2022查看汇编代码进行验证

首先我们需要写一个空类,然后在main中new出这个类。代码可参考如下:

class A
{
public:
  A()
  {
  }
  ~A()
  {
  }
};
int main()
{
  A* p = new A();
  delete p;
  p = nullptr;
  return 0;
}


第一步:在创建这一行添加断点(可左击该行行首或者在该行按F9即可)。

第二步:开始调试到当前断点处(可按F5)。

第三步:在上方功能栏中点击【Debug】->【Windows】->【Disassembly】。中文对应的是【调试】->【窗口】->【反汇编】。详细请看下图。

操作完上面三步之后我们就到了汇编代码。由于重点不是研究汇编语言,所以这里我就仅对上面那三步进行标记。验证一下上面的一个猜想。


那么这里我们用到的new操作符,也就是new operator,在《C++ Primer》书中也被称为new expression

operator new

功能:只负责内存分配

operator new默认情况下调用分配内存的代码,去尝试在堆区获取一段空间,如果成功就返回,如果失败,则调用new_hander。有关new_hander我之前写了一篇:new_hander文章链接


重载类内operator new

下面对operator new重载,进行测试;

class A
{
public:
  A()
  {
    std::cout << "Call A Constructor!" << std::endl;
  }
  ~A()
  {
    std::cout << "Call A Destructor!" << std::endl;
  }
  void* operator new(size_t size)
  {
    std::cout << "Call operator new" << "\t size = " << size << std::endl;
    return ::operator new(size); // 通过::operator new调用了全局的new
  }
};
int main()
{
  A* pt = new A();
  delete pt;
  pt = nullptr;
  return 0;
}


运行结果:

可以看到先打印类内的operator new再调用constructor函数最后调用destructor函数。


重载全局 ::operator new

若要重载全局的::operator new时,最后就不能return 自身了需要写成malloc(size)。对应的delete也有delete operator 和 operator delete俩种,operator delete也是可以重载的。所以一般来说重载了operator new 就需要重载对应的operator delete了。

具体请看下面的代码:

新增一个全局的operator new函数

void* operator new(size_t size)
{
  std::cout << "Call global operator new" << "\t" << size << std::endl;
  return malloc(size);
}


运行结果:

直接调用operator new

该函数我们可以进行重载,但是第一参数的类型必须是size_t。而且我们还可以单独调用operator new。将返回一个void类型的指针。

在原有代码基础上,增加一个成员函数用于输出日志。

class A
{
public:
  A()
  {
    std::cout << "Call A Constructor!" << std::endl;
  }
  ~A()
  {
    std::cout << "Call A Destructor!" << std::endl;
  }
  void* operator new(size_t size)
  {
    std::cout << "Call operator new" << "\t size = " << size << std::endl;
    return ::operator new(size); // 通过::operator new调用了全局的new
  }
  void print()
  {
    std::cout << "ha ha !" << std::endl;
  }
};
void* operator new(size_t size)
{
  std::cout << "Call global operator new" << "\t size = " << size << std::endl;
  return malloc(size);
}
int main()
{
  void* rawMemory = operator new(sizeof(A));
  A* pa = static_cast<A*>(rawMemory);
  pa->print();
  delete pa;
  pa = nullptr;
  return 0;
}


运行结果:

可以看到只打印了全局的operator new函数已经析构函数。

Placement new

头文件:#include <new> 或者#include <new.h>

可以直接调用constructor函数,是operator new的一个特殊版本,也被称为placement new函数。


需要实现一个void* operator new(size_t, void* location)的重载版本。不需要申请内存只需要返回当前对象即可。

调用的语法:new(ObjectName) ClassName(构造函数的参数)

class A
{
public:
  A()
  {
    std::cout << "Call A Constructor!" << std::endl;
  }
  ~A()
  {
    std::cout << "Call A Destructor!" << std::endl;
  }
  void* operator new(size_t size)
  {
    std::cout << "Call operator new" << "\t size = " << size << std::endl;
    return ::operator new(size); // 通过::operator new调用了全局的new
  }
  void* operator new(size_t size, void* location)
  {
    std::cout << "Call operator new(size_t size, void* location)" << std::endl;
    return location;
  }
  void print()
  {
    std::cout << "ha ha !" << std::endl;
  }
};
int main()
{
  void* rawMemory = operator new(sizeof(A));
  A* pa = static_cast<A*>(rawMemory); // 创建内存
  new(pa) A(); // 调用构造函数
  pa->print();
  delete pa;
  pa = nullptr;
  return 0;
}


运行结果:


这里的operator new的目的是要为对象找内存,然后返回一个指针指向它。在placement new的情况下,调用者已经知道指向内存的指针了,所以placement new唯一需要做的就是将已获得指针进行返回。虽然说size_t参数没有用到但是必须要加,之所以不给形参名是因为防止编译器抱怨“某某变量未被使用”。


删除与内存释放

为了避免内存泄漏,每一个动态分配都必须匹配一个释放动作。

内存释放的动作是由operator delete执行,函数原型:void operator delete(void* object);

当我们写了这句代码时delete pa;实际上执行了俩件事。

1、调用destructor函数

2、释放对象所占的内存资源

转换成代码就相当于:

pa->~A();
  operator delete(pa);


使用operator new创建对象该如何释放

当我们在创建对象时,没有调用constructor函数,那么释放内存时也不需要调用destructor函数。只需要operator delete(pa);

int main()
{
  void* rawMemory = operator new(sizeof(A));
. ...其他代码 
  operator delete(rawMemory);
  return 0;
}


上面这段代码其实就等价于C语言里面调用malloc和free函数

使用placement new创建对象时该如何释放

如果使用placement new在内存中产生对象,我们不能使用delete operator,因为会调用operator delete函数来释放内存。首先该内存并不是由该对象的operator new函数分配而来。它仅仅做了一个返回而已,所以这种情况下只需要调用destructor函数即可。


int main()
{
  void* rawMemory = operator new(sizeof(A));
  A* pa = static_cast<A*>(rawMemory); // 创建内存
  new(pa)A(); // 调用构造函数
  pa->~A();
  pa = nullptr;
  operator delete(rawMemory);
  return 0;
}


在上面这段代码中,pa对象就是使用placement new,所以最后只需要调用destructor函数。

针对数组的创建和释放

当我们使用A* pa = new A[10];这段代码时,分配内存的方式将会发生变化。

1、由operator new 改为 operator new[],也被叫为array new。同样array new也可以被重载,

2、array new必须调用数组中的每个对象的constructor函数。上面那个例子就会调用10个A的无参构造函数。

3、array new在释放内存时。上面那个例子就会调用10个A的destructor函数。

4、该类必须有无参构造函数。


所以我们同样也可以修改operator new[]所调用的 new operator函数,以及delete[] operator。


系统维护开销

在面对数组时,new 会额外分配空间来存储new的长度(一般为一个指针大小,32位平台下4字节,64位平台下8字节)。这个叫系统维护开销。

下面是测试代码,类A是个空类只占一个字节,正常来说应该申请10个字节的内存。

int main()
{
  A* pa = new A[10];
  delete[] pa;
  return 0;
}


32位环境下:

64位环境下:

可以看到对申请了一个指针的内存用来存放申请对象的个数。


总结

下面针对new的三种使用方式做了一个使用场景总结,切记操作对应的new 时还需要对应的delete。

1、需要将对象创建在堆区,那么就使用 new operator 也就是new操作符。它会帮你分配内存并调用constructor函数。

2、仅需要分配内存,那么就使用operator new,这样就不会调用constructor函数。

3、需要在堆区创建对象时自定义内存分配方式,那么就需要重写operator new函数然后使用new operator即可。

4、需要在已分配的内存中调用构造函数,那么就使用placement new。

目录
相关文章
|
1月前
|
C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(二)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
1月前
|
编译器 C++ 开发者
【C++】深入解析C/C++内存管理:new与delete的使用及原理(三)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
1月前
|
存储 C语言 C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(一)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
1月前
|
程序员 C语言 C++
C++入门5——C/C++动态内存管理(new与delete)
C++入门5——C/C++动态内存管理(new与delete)
68 1
|
2月前
|
C++
C++(十九)new/delete 重载
本文介绍了C++中`operator new/delete`重载的使用方法,并通过示例代码展示了如何自定义内存分配与释放的行为。重载`new`和`delete`可以实现内存的精细控制,而`new[]`和`delete[]`则用于处理数组的内存管理。不当使用可能导致内存泄漏或错误释放。
|
3月前
|
存储 程序员 编译器
c++学习笔记08 内存分区、new和delete的用法
C++内存管理的学习笔记08,介绍了内存分区的概念,包括代码区、全局区、堆区和栈区,以及如何在堆区使用`new`和`delete`进行内存分配和释放。
48 0
|
4月前
|
NoSQL 编译器 Redis
c++开发redis module问题之如果Redis加载了多个C++编写的模块,并且它们都重载了operator new,会有什么影响
c++开发redis module问题之如果Redis加载了多个C++编写的模块,并且它们都重载了operator new,会有什么影响
|
4月前
|
NoSQL Redis C++
c++开发redis module问题之避免多个C++模块之间因重载operator new而产生的冲突,如何解决
c++开发redis module问题之避免多个C++模块之间因重载operator new而产生的冲突,如何解决
|
10天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
37 4
|
11天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
34 4