距离上次更新本文已经过去了 365 天,文章部分内容可能已经过时,请注意甄别

在之前类和对象的博客里面,已经记录过了 C++ 中动态内存管理函数 newdelete 的基本使用。本篇博客是对 C++ 动态内存管理的进一步细化

[TOC]

1.C/C++ 内存分区

这是一个老生常谈的问题了,直接看下面这个图吧!

image-20220624163644747

这里的数据区其实就是静态区,而代码区是常量区。这里的 BBS 区先暂时 pass 掉。

要想辨别上面的几个内存分区,可以现来看下面这个代码,你能分的清楚它们都是存在内存的哪一个区域吗?

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int a = 1;//数据区
static int b = 1;//数据区
int main()
{
static int c = 1;//数据区
int d = 1;//栈

int arr1[10] = {1, 2, 3, 4};//栈
char arr2[] = "abcd";//栈
char* arr3 = "abcd";//"abcd"存在代码区
int* ptr = (int*)malloc(sizeof (int)*4);//堆
free (ptr);
//其中,ptr指针本身是存在栈区的
//同理,arr3指针本身存在栈区
//但是arr3指针指向的对象是存在代码区(静态区)
}

2.C++ 动态内存管理

在 C 语言中,基本的动态内存管理通过 malloc 和 free 实现

c
1
2
int* ptr = (int*)malloc(sizeof (int)*4);//堆
free (ptr);

在 C++ 中,对应产生了 new 和 delete,它们比前者更加高级,具有更多特性

2.1 基本认识 new 和 delete

下面是基本的使用方式,想必大家看了之后,是 “有手就行”😂

c
1
2
3
4
5
6
7
8
9
10
int*p1=new int;//开辟一个int类型的空间
int*p2=new int(10);//开辟一个int类型的空间,并初始化为10
int*p3=new int[10];//开辟10个int类型的空间
//注意后两个的括号区别!

delete p1;//销毁p1指向的单个空间
delete p2;//同上

//delete p3;//销毁p3指向的第一个空间,不能用于数组
delete[] p3;//销毁p3指向的数组

2.2 操作类对象

new 相比于 malloc,最大的区别在于处理自定义类型的时候。类和对象就是 C++ 中与 C 语言完全不同的自定义类型。

我们知道,当你使用类名创建一个对象的时候,编译器会自动调用这个对象的构造函数。那如果我们用 new 来创建一个自定义类型的对象呢?

cpp
1
2
3
4
5
6
7
8
9
10
11
class Stack{
private:
int* _a;
}
int main()
{
Stack* p1=(Stack*)malloc(sizeof(Stack));
Stack* p2=new Stack;

return 0;
}

这时候的区别就在于

  • new 在创建的对象的时候,会自动调用该对象的构造函数
  • malloc 在创建对象的时候,不会调用构造函数

这样就能解释,为什么 C++ 要单独弄出一个 new,而不是继续沿用 C 语言的 malloc 了。因为我们在 class 中定义成员变量的时候,大多数是定义成私有的。如果对象在创建的时候没有进行构造,我们很难从外部访问类内部的私有成员进行初始化操作。

所以 new 的出现,让我们能够在堆上开辟对象空间的同时,初始化这个对象

不难理解,delete 和 free 的区别也是如此:

  • 当你调用 delete 的时候,编译器会调用类的析构函数
  • 使用 free 不会调用析构函数,可能造成内存泄漏

image-20220624202643155

2.3 对象数组

cpp
1
2
3
4
5
6
7
// 申请单个Test类型的对象
Test* p1 = new Test;
delete p1;

// 申请10个Test类型的对象
Test* p2 = new Test[10];
delete[] p2;

和内置类型一样,我们也可以方便的使用 new 来实现开辟对象数组

注意,在 delete 操作的时候,一定要注意匹配问题,不能直接用 delete p2 来释放开辟的数组空间


2.4 给构造函数传参

如果这个类的构造函数是包含参数的话,还可以使用下面这种方式在开辟空间,调用构造函数时传参(注意括号区别)

cpp
1
2
Test* p3 = new Test(10);//给对象Test的构造函数传参
delete p3;

image-20220624202949406

3.operator new/delete 函数

看到这个名字,估计你和我一样,会下意识的认为这个是 c++ 中对 new 和 delete 操作符的重载。nope!这两个实际上是 C++ 中实现 new 和 delete 的一部分函数

为啥说是一部分呢,让我们来康康它的源码

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)
{
void *p;
/* malloc (0) is unpredictable; avoid it. */
if (sz == 0)
sz = 1;

while (__builtin_expect ((p = malloc (sz)) == 0, false))
{
new_handler handler = std::get_new_handler ();
if (! handler)
_GLIBCXX_THROW_OR_ABORT(bad_alloc());
handler ();
}

return p;
}

你回复下,这个函数最终使用了 malloc 来开辟空间,只是在这之上,new 还引入了抛异常机制

  • C 语言中,malloc 错误会返回 null 指针
  • C++ 中,new 错误会执行抛 bad_alloc 异常操作

不知抛异常是什么?我们可以暂且不用理解它。只需要知道,当 new 失败的时候,控制台会直接报错终止程序,而不是和 malloc 一样,将指针变空指针,从而导致可能出现的解引用空指针操作

实际上,当我们 new 一个对象的时候,会执行下面两个函数

cpp
1
2
operator new
对象的构造函数

在 VS 中打开调试,转到反汇编,你便可以看到编译器 call 这两个函数的操作

image-20220625101016238

再来看看 operator delete 的代码

https://cplusplus.com/reference/new/operator%20delete/

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void operator delete(void *pUserData)
{
_CrtMemBlockHeader * pHead;
RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
if (pUserData == NULL)
return;
_mlock(_HEAP_LOCK); /* block other threads */
__TRY
/* get a pointer to memory block header */
pHead = pHdr(pUserData);
/* verify block type */
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
_free_dbg( pUserData, pHead->nBlockUse );
__FINALLY
_munlock(_HEAP_LOCK); /* release other threads */
__END_TRY_FINALLY
return;
}

我们发现,delete 最终也是通过调用 free 实现的

image-20220625101848948

但是这里的反汇编,我就有点看不懂了。看起来也调用了一个 Stack 函数,姑且认为那个就是 Stack 的析构函数吧


3.1 new 和 delete 的实现原理

看到这里,让我们来总结一下 new 和 delete 的实现原理。

new 的原理:

  • 调用 opeartor new 申请空间,错误时抛异常
  • 如果操作的是自定义类型,则调用构造函数

new int [N] 原理:

  • 调用 operator new[](没错库里面还有一个这个函数)
  • operator new[] 中实际调用 N 个 operator new 来申请空间
  • 如果操作的是自定义类型,则还会分别调用 N 次构造函数

delete 的原理:

  • 在原始空间上调用析构函数,清楚内容
  • 调用 operator delete 执行对象空间的 free 操作

delete int [N] 的原理:

  • 调用 N 次析构函数
  • 调用 operator delete [],实际调用 N 个 operator delete 来释放 N 个对象的空间

编译器在分配内存时通常会在分配的内存块前面存储一些额外的信息,以便在释放内存时正确计算要释放的空间大小。这些额外的信息可能包括分配的内存块大小、指向析构函数的指针等。因此,当调用 delete 操作符时,编译器可以从所分配的内存块中提取这些信息,并使用它来确定要释放的空间大小。

3.2 delete 加 [] 和不加的区别

 1、delete 释放new分配的单个对象指针指向的内存

​ 2、delete [] 释放 new 分配的对象数组指针指向的内存

​ 3、delete 处理单个类类型,先会调用析构函数,释放它所占资源,然后释放它所占内存空间。

​ 4、delete 处理数组类类型的时候,会对每一个数组对象都调用它们的析构函数,然后再释放它们所占用的内存空间。所以对于类类型的数组如果不调用 delete [], 那就只调用了下标为 0 的对象的析构函数,可能会产生问题。

​ 5、两个都会释放所占内存,对于内置类型不管是数组还是单个对象,都可以混用,没有关系,因为对于内置类型,它只干一件事,就是释放它们所占内存

​ 6、如果对于单个类对象,delete 和 delete [] 都可以,因为 delete 是知道它要释放多大空间的,加不加 [] 括号的区别是对不对每个对象调用析构函数,如果只有一个的话,它就调用一次,所以没有关系。

4. 定位 new

定位 new 表达式会在已分配的原始内存空间中调用构造函数初始化对象

啊嘞,new 不是会自己调用构造函数吗?这个定位 new 有是来干什么的?

查阅了一些我现在看不懂的资料后,了解到,定位 new 的操作多半是配合自己写的内存池来进行操作。在之前博客中出现的 Tcmalloc 就是谷歌写的一个内存池;

4.1 什么是内存池?

当我们使用 new 或者 malloc 时,是通过编译器向操作系统申请空间

而内存池就是一个我们写的预先申请内存空间的模块

这个模块会在执行后,先预先向操作系统要一个相对较大的空间。我们后续的操作就是在这个已经开辟好的空间中再次申请空间来实现的

因为这样就是从自己的口袋里面拿东西,没有中间商赚差价,效率就会提高不少

但是这样就没有了 new 本身自动调用构造函数的优势,需要我们自己来调用构造函数

4.2 定位 new 的使用

定位 new 的英文叫 placemaent new,要知道,别看到这个英文不知道它啥意思;

cpp
1
2
3
new(place_address) type
或者
new(place_address) type(initializer-list)

以下面这个类为例

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Stack {
public:
Stack(int num = 5)
{
_a = new int[num];
_capa = num;
cout << "Stack(int)" << endl;
}

~Stack()
{
delete[] _a;
_capa = 0;
cout << "~Stack()" << endl;
}
private:
int* _a;
int _capa;
};

我们先使用 malloc 来模拟没有调用构造函数的情况,再使用定位 new 来调用构造函数

cpp
1
2
3
4
5
int main()
{
Stack* p= (Stack*)malloc(sizeof(Stack));
new(p) Stack; //如果类的构造函数有参数时,此处需要传参
}

可以看到,编译器成功调用了构造函数

image-20220625103737599


5. 更多知识点

5.1 malloc 和 new 的区别

malloc/free 和 new/delete 的共同点是:都是从堆上申请空间,并且需要用户手动释放

不同的地方是:

  1. malloc 和 free 是函数,new 和 delete 是操作符
  2. malloc 申请的空间不会初始化,new 可以初始化(通过 new int(10))
  3. malloc 申请空间时,需要手动计算空间大小并传递,new 只需在其后跟上空间的类型即可(其实就是省略了 sizeof 这一步的操作)
  4. malloc 的返回值为 void*, 在使用时必须强转,new 不需要,因为 new 后跟的是空间的类型
  5. malloc 申请空间失败时,返回的是 NULL,因此使用时必须判空,new 不需要,但是 new 需要捕获异常
  6. 申请自定义类型对象时,malloc/free 只会开辟空间,不会调用构造函数与析构函数,而 new 在申请空间后会调用构造函数完成对象的初始化,delete 在释放空间前会调用析构函数完成空间中内存的清理

5.2 内存泄漏

我们知道,当堆区申请的空间没有进行释放的时候,就会出现内存泄漏,造成内存的浪费,甚至导致操作系统 boom!

  • 堆内存泄漏

堆内存指的是程序执行中依据须要分配通过 malloc / calloc / realloc / new 等从堆中分配的一块内存, 用完后必须通过调用相应的 free 或者 delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生堆内存泄漏

  • 系统资源导致的泄漏

系统资源泄漏 指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统 资源的浪费,严重可导致系统效能减少,系统执行不稳定。

除了忘记 free 或者 delete 之外,另外的一些情况导致程序提前终止,也会出现内存泄漏

cpp
1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
Stack* p1 = (Stack*)malloc(sizeof(Stack));
Stack* p2 = new Stack;


free(p1);
return 1;//只是做个示例,实际上哪有人这么写代码啊!

delete p2;
return 0;
}

比如上面这个函数中,free 之后执行了 return,跳过了 delete 的操作,即导致 p2 的内存没有被释放,出现了内存泄漏

解决内存泄漏有很多办法,其中最好的办法就是维持一个良好的代码风格,避免出现忘记释放内存的情况!

5.3 深拷贝

这个麻烦大家移步之前类和对象的博客啦

https://blog.csdn.net/muxuen/article/details/124881928?spm=1001.2014.3001.5501


结语

本篇博客到这里就结束了

期末考试其实已经结束 5 天了,我还在摸鱼…… 呜呜

QQ图片20220418131327