[TOC]

直入主题,动态内存管理!🕵️‍♂️

1.为什么会有动态内存分配?

我们一般使用以下两种方式开辟内存

1
2
int a = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间

但是上述的开辟空间的方式有两个特点:

  • 空间开辟大小是固定的。
  • 数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

但是对于空间的需求,不仅仅是上述的情况。有些时候,我们并不能提前知道需要的空间大小,而部分编译器并不支持变长数组。这时候以数组的方式开辟连续空间的方法就有点不适用了。

其次,全局变量/局部变量是存放在栈区里面的。如果存放的变量太多,就会出现栈溢出的错误。

这时候就轮到动态内存管理上场了!

2.动态内存函数

2.1 malloc

1
2
#include <stdlib.h>//malloc的头文件
void* malloc (size_t size);

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。

  • 如果开辟成功,则返回一个指向开辟好空间的指针。
  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  • 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
  • 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <errno.h>
#include <string.h>
#include <stdlib.h>

int main()
{
//开辟10个整型的空间
//int arr[10];
int* p = (int*)malloc(40);
if (NULL == p)
{
printf("%s\n", strerror(errno));
//这个函数在之前介绍过,用于转义错误代码
return 0;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}

//释放
free(p);
p = NULL;

return 0;
}

2.2 free

上述代码里面出现了另外一个重要的函数,free

free函数用来释放动态开辟的内存,头文件是<stdlib.h>

  • 如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的
  • 如果参数ptr是NULL指针,则函数什么事都不做。

free函数不能用来释放栈区里面的空间,栈区的空间由编译器进行自动创建和自动释放

重点!

被free之后的指针p指向的空间已经不属于我们的应用程序了。最好在free之后立马把指针置为NULL避免访问野指针。

1
2
free(p);
p = NULL;

2.3 calloc

1
void* calloc (size_t num, size_t size);

calloc函数的功能和malloc基本一致,但是有一点不同:

  • calloc函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0。
  • 与malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为全0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <errno.h>
#include <string.h>
#include <stdlib.h>
int main()
{
//开辟10个整型的空间
//int arr[10];
int* p = (int*)calloc(10, sizeof(int));
if (NULL == p)
{
printf("%s\n", strerror(errno));
return 0;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}

//释放
free(p);
p = NULL;

return 0;
}

添加断点并调试,可以看到calloc函数把这一块内存都初始化为0了

如果这块空间需要初始化,使用calloc函数比malloc更好

image-20220129152133507


2.4 realloc

  • 该函数用于更改已经创建好的动态内存空间(可改大可改小)
1
void* realloc (void* ptr, size_t size);
  • ptr 是要调整的内存地址
  • size 调整之后新大小
  • 返回值为调整之后的内存起始地址
  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间
  • 如果开辟失败,返回空指针NULL

在进行扩容操作的时候,会出现两种情况:

  1. 原动态内存空间之后有足够空间进行扩容
  2. 原动态内存空间之后无足够空间

如果是第一种情况,realloc函数会在这之后增添空间,分配给ptr指针。原来空间的数据不发生变化。

如果是第二种情况,realloc会找一块新的空间,开辟好后返回给ptr指针,并把原空间里的数据移动到新空间的对应位置。

情况2的时候,开辟完新空间之后,会把原来的空间给free掉(只有开辟成功才会释放原来的空间)

image-20220129152613292

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <errno.h>
#include <string.h>
#include <stdlib.h>
int main()
{
//开辟10个整型的空间
//int arr[10];
int* p = (int*)calloc(10, sizeof(int));
if (NULL == p)
{
printf("%s\n", strerror(errno));
return 0;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
//需要增容 - 80
int*ptr = (int*)realloc(p, 80);
if (NULL != ptr)
{
p = ptr;
ptr = NULL;
}
//继续使用了

//释放
free(p);
p = NULL;

return 0;
}

几点注意:

  • realloc(p, 80) 80指的是开辟之后新的空间大小为80,而不是增加80的空间
  • realloc函数可能开辟失败,这时候如果将开辟失败的返回值NULL赋值给了原有指针p,就很危险。
  • 采用中间指针变量ptr,先判断realloc函数是否开辟成功,若成功,则赋值给p指针。
  • 赋值给p之后ptr指针就没用了,置为空指针。

最后我们不需要对ptr进行free,因为ptr的指向和p是一样的

free(p)的时候,ptr所指向的空间也被free掉了

  • realloc缩小空间的时候,会把原来空间后面的内容都剔除

3.常见错误

3.1对NULL指针的解引用

1
2
3
4
5
6
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}

INT_MAX是int类型的最大值,可以在头文件limits.h里面查询并使用

3.2对动态内存空间的越界访问

和数组一样,动态内存空间也是不能越界访问的!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
exit(EXIT_FAILURE);
}
for(i=0; i<=10; i++)//应该是i<10
{
*(p+i) = i;//当i是10的时候越界访问
}
free(p);
}

3.3对非动态内存空间进行free

1
2
3
4
5
6
void test()
{
int a = 10;
int *p = &a;
free(p);//err
}

3.4使用free释放动态内存空间的一部分

1
2
3
4
5
6
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}

image-20220129154541447

需要用一个指针来记住起始地址,用另外一个指针来进行赋值等操作


3.5对一个空间进行重复释放

1
2
3
4
5
6
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}

在free之后立马把p置为空指针,就能避免这个问题

  • free函数接收空指针,不做任何操作

3.6内存泄漏

如果忘记释放动态内存开辟的空间,就会导致内存泄漏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void test()
{
int *p = (int*)malloc(100);
if(NULL != p)
{
*p = 20;
}
//没有释放p的空间
}
int main()
{
test();
while(1);
}

一般情况下,谁使用就谁释放。函数里使用就在函数里释放,除非需要传回主函数进行操作。

如果没有传回主函数,也没在函数里进行释放,该指针变量已经被销毁了,无法进行释放操作!

如果主函数里程序没有结束,就造成了内存浪费


4.C/C++程序中内存区域划分

C/C++程序中的内存区域一般分为4个区:

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
  2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
  3. 数据段(静态区/全局区):存放全局变量、静态数据。程序结束后由系统释放。
  4. 常量区:用于存放初始化变量的值(比如常量字符串的区域);
  5. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

有了下面这幅图,我们就可以更好的理解在《初识C语言》中讲的static关键字修饰局部变量的例子了。

image.png

实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁,所以生命周期变长

image-20220129154835218


结语

动态内存其实就是数组的高级形式,它能让我们更方便的管理开辟的连续的空间。

你学废了吗?🤤