浅谈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
和delete
new 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),这种特性,在这里不多做解释。
完