浅谈C++的new和delete
文章目录
- 前言
 - 1. 数据的存储方式
 - 2. C语言的管理方式
 - 3. C++的管理方式
 - 3.1 new 和 delete
 - 3.2 new 和 delete[ ]
 
- 4. new和delete底层
 - 5. 对比new和malloc区别
 
前言
这里介绍new作为堆区内存开辟的操作符的用法和细节
 
我们知道C语言管理内存的函数是:malloc、calloc、realloc和free。C语言是面向过程的编程语言;而C++是面向对象的编程语言。这就注定了C++用不惯C语言的那一套动态内存管理的方式,于是C++引入了两个操作符:new和delete。这还是为了更好地处理自定义类型。所以现在我们就会从几个方面来讨论这些内存管理方式:
- 程序中的数据类型和内存分布
 - C语言的管理方式:简要介绍
malloc、calloc、realloc和free - C++的处理方式及语法
 new和delete的底层malloc和new的区别
1. 数据的存储方式
在一个程序中,数据类型大概可以分为如下部分:
- 局部数据
 - 全局数据和静态数据
 - 常量数据
 - 动态数据
 - ……
 
大约有如上的数据是在内存中被存储的,那么对于内存来说,他应该如何有效的管理这些数据呢?应该如何把这些数据合理的存放呢?现在我们就需要来了解,C/C++中程序内存区域的划分了
 
- 栈:又叫做堆栈,其主要作用就是用来存储局部变量,是一种即用即销毁的区域。存储的比如是:非静态局部变量、函数参数、返回值……栈区的运用习惯是先使用高地址后使用低地址。栈区是向下生长的
 - 堆:程序运行的时候用于动态内存开辟的。堆区是向上生长的。
 - 数据段:存储全局数据和静态数据
 - 代码段:可执行的代码和只读的常量(例如:常量字符串)
 
先来看这样一个程序:
#include<stdio.h>
#include<stdlib.h>int globalVar = 1;
static int staticGlobalVar = 1;
int main()
{static int staticVar = 1;int localVar = 1;int num1[10] = {1,2,3,4};char char2[] = "abcd";const char* pChar3 = "abcd";int* ptr1 = (int*)malloc(sizeof(int)* 4);int* ptr2 = (int*)calloc(4, sizeof(int));int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);free(ptr1);free(ptr3);return 0;
}
 
- 来看下面的问题和选项:
 

2. C语言的管理方式
-  
malloc- 在堆区上申请一块空间,返回这片空间的起始地址的指针(
void*类型)。 
 - 在堆区上申请一块空间,返回这片空间的起始地址的指针(
 -  
calloc- 函数原型和
malloc大有不同,主要区别是:它会将开辟好的空间,每字节初始化为0。 
 - 函数原型和
 -  
realloc- 用于修改已有的内存大小,有两种方式:原地扩容和异地扩容。
 
 
对于C语言开辟的内存,需要使用函数free进行释放,不然就会造成内存泄露。同时关于malloc、calloc、realloc的使用还是来举个例子:
#include<stdio.h>
#include<stdlib.h>int main()
{int* ptr1 = (int*)malloc(sizeof(int)*4);int* ptr2 = (int*)calloc(4, sizeof(int));int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 8);free(ptr1);free(ptr3); //不要尝试free ptr2,因为有可能发生了异地扩容,ptr2就是野指针return 0;
}
 
对于如上的三个函数来说,如果开辟空间失败,那么就会返回一个空指针NULL。所以有些编译器需要我们检查返回的指针是否是空指针(例如VS2022)。
3. C++的管理方式
3.1 new 和 delete
特别需要注意的是:`new`和`delete`是操作符,不是函数!
 
new和delete用于动态申请单个数据类型的空间。不用强制类型转换,同时还可以初始化。语法如下
 例3.1:
#include<iostream>
using namespace std;int main()
{int* ptr1 = new int(1); //初始化为1int b = 3;int* ptr2 = new int(b); //用b来初始化delete ptr1;delete ptr2; //一定要记得释放return 0;
}
 
基本语法如下:
//   new 类型 (初始化内容)
//  delete 指针
 
这里我们可以看到new和malloc在内置类型上似乎没有太大区别,不就是多了一个初始化吗?
- 更重要的是,在前言部分已经提到了:C++是面向对象的开发语言,那么
new的优势更体现在自定义类型,即自定义类型上。 
例3.2:有如下一个A类
#include<iostream>
using namespace std;class A
{
public:A(int a = 0):_a(a){cout << "A(int a) -- 构造" << endl;}A(const A& val):_a(val._a){cout << "A(const A& val) -- 深拷贝" << endl;}
private:
int _a;
};int main()
{A* ptr1 = new A(1);cout << "========" << endl;A aa(12);cout << "========" << endl;A* ptr2 = new A(aa);delete ptr1;delete ptr2;return 0;
}
 
运行结果是什么呢?

 一个执行构造,另一个执行拷贝构造,很类似于在栈区进行对象的创建(意味着如果没有默认构造函数,就一定需要传参)。如果你传入的是一个右值的话,编译器可能会优化;当然也可能为你进行移动构造。
在这里想要表达的是:new会根据你传入的初始化内容,调用对应的构造函数:(构造函数、拷贝构造函数、移动构造函数……),当然,这个结果是存在优化的(当同一行代码存在多次构造或者拷贝构造编译器会进行优化)。所以我们在这里可以看到,new和malloc差距是很明显的,因为new可以完成自定义类型的初始化工作,而malloc是无法完成的。
3.2 new 和 delete[ ]
在这里出现了[],我们当然很容易想到数组。
new除了进行单个对象的申请空间,还可以进行像malloc那样的申请连续堆区空间,并且返回这段空间的起始地址的指针。
 例3.3:
#include<iostream>
using namespace std;int main()
{int* ptr = new int[10]{1, 2, 2}; //继承了数组的玩法,可以像数组一样初始化,注意这里没有用=链接{},剩下的默认值就是缺省值delete[] ptr;return 0;
}
 
语法:
// new  类型[数量N]{ 初始化内容 }
// delete [] 指针
 
对于自定义类型来说,同样受用。只不过是:调用了N次构造函数。
 例3.4:
#include<iostream>
using namespace std;class A
{
public:A(int a = 0):_a(a){cout << "A(int a) -- 构造" << endl;}A(const A& val):_a(val._a){cout << "A(const A& val) -- 深拷贝" << endl;}
private:
int _a;
};int main()
{A* ptr = new A[10]{1,1,1,A(1)}; //隐式类型转换或者匿名对象都可以。delete[] ptr;return 0;
}
 
-  
在这里特别强调:一定要搭配使用开辟和释放
new和deletenew T[N]和delete[ ]malloc和free
 
一定不要混合使用!!!
4. new和delete底层
在这里不会深入地谈论new和delete的底层,需要的是大概了解其工作原理就可以了。
 
来看系统提供的两个全局函数operator new和operator delete:首先需要知道的是,new和delete的实现就是依靠的是operator new和operator delete。使用new就是调用函数operator new(运算符)
 
 上面这张图片是operator new和operator delete的实现。我们从中读取关键信息:
operator new实际上是通过malloc来申请空间的delete实际上调用的是free。事实也确实是这样,可是为什么需要绕一个弯来调用malloc呢?而不是直接new通过malloc来实现空间的开辟呢?
- C++不想使用C语言那套通过返回值(错误码)来判断程序是否出现问题。而是希望抛出一个异常来告诉程序员,系统出BUG了,使用
operator new的一部分原因也是因为希望开辟空间失败的问题让程序抛出异常,而不是通过返回值(设置错误码)来实现。 
那么对于new来说,那么直接开空间就不适合直接套用malloc,而是需要采用operator new来进行进一步处理。所以大概调用的逻辑是:

 实际上的new运行过程就是:malloc + 调用构造函数
所以在这里给出new和delete的运行机制:
-  
new
- 调用
operator new申请空间 - 在申请的空间上,进行构造函数初始化,完成对象的构建
 
 - 调用
 -  
delete
- 在空间上执行析构函数,完成对象中的资源清理工作
 - 调用
operator delete完成对空间的释放 
 
new T[N] 和 delete[ ] 皆是类似的道理
5. 对比new和malloc区别
经过上面的探讨,我们也看得出来new和malloc有些差距:
new是操作符,malloc是函数。new内存开辟失败是抛异常,malloc是返回nullptr,设置错误码。new不需要手动计算开辟内存大小,malloc需要传入总共字节数。new可以实现申请对象的初始化,而malloc不能实现初始化。new不需要强制类型转换指针,而malloc需要强制类型转换指针。- 在进行自定义类型的申请时
new会开空间+调用构造函数,delete会调用析构函数+释放空间;而malloc不会对于自定义类型有任何额外的操作。 
- 实际上这里谈到了异常,还有内存泄露等一系列话题,我们在其它章节谈到。
 - 实际上new还有另外的玩法:定位
new(placement new),这种特性,在这里不多做解释。 
完
