本篇是对上一篇动态内存管理的总结提升,能够更好地帮助你理解使用动态内存管理😎

动态内存的魔鬼细节

对空指针解引用

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

解析

其分配的大小为 INT_MAX/4 字节,然而系统可能无法提供如此巨大的内存块以满足分配请求,所以在系统无法满足分配请求时,malloc 会返回一个空指针,直接对 *p 进行赋值操作,如果 p 的值是 NULL,那么这个赋值操作就会导致程序出现段错误(访问非法内存地址),即指向不存在的内存

修改后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void test()
{
int *p = (int *)malloc(INT_MAX / 4);
if (p!= NULL)
{
*p = 20;
free(p);
}
else
{
// 可以在这里添加一些处理内存分配失败的逻辑,比如打印错误信息等
printf("内存分配失败!\n");
}
}

可以在内存分配后添加对 p 是否为 NULL 的判断

动态内存越界访问

这里先介绍 exit 函数,用于终止当前正在执行的程序,EXIT_FAILURE 是一个预定义的宏,它通常被定义为一个非零值,当 exit 函数以 EXIT_FAILURE 作为参数被调用时,这表示程序是以一种错误状态退出的

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

解析

通过 malloc 分配的内存空间是用于存储 10 个 int 类型数据的,但是却输入了 11 个整数,当 i 的值达到 10 时,*(p + i) 这个操作就会超出所分配内存空间的边界,导致越界访问,可能导致程序出现程序崩溃、数据错误等

修改后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void test()
{
int i = 0;
int *p = (int *)malloc(10 * sizeof(int));
if (NULL == p)
{
exit(EXIT_FAILURE);
}
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
free(p);
}

循环在有效范围内对所分配内存空间中的元素进行赋值操作

对非动态内存 free

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

解析

这里没有开辟动态内存,只是创建了指针变量,并不是说只要是内存都能被 free 释放,free 函数在 C 语言中是用于释放通过动态内存分配函数(如 malloc、calloc、realloc 等)分配的内存空间

修改后的代码

1
2
3
4
5
6
7
8
9
10
11
void test()
{
int *p;
p = (int *)malloc(sizeof(int));
if (p!= NULL)
{
*p = 10;
// 使用完分配的内存后
free(p);
}
}

首先通过 malloc 函数动态分配了能够容纳一个 int 类型数据的内存空间,将其赋值给指针 p,在对该内存进行了必要的操作(如赋值等)之后,再使用 free 函数来释放这块动态分配的内存

使用 free 只释放一部分动态内存

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

解析

指针 p 进行自增操作(p++),使其不再指向所分配动态内存的起始位置,free 函数要求传入的指针必须指向通过动态内存分配函数所分配的内存块的起始位置,当传入不符合要求的指针给 free 函数时,可能会导致程序崩溃、内存泄漏等问题

修改后的代码

1
2
3
4
5
6
7
8
9
10
11
void test()
{
int *p = (int *)malloc(100);
int *original_p = p; // 保存起始指针

p++;

// 使用p进行其他操作后,当需要释放内存时

free(original_p);
}

通过引入一个新的指针 original_p 来保存最初通过 malloc 分配内存时得到的起始指针,在对 p 进行了自增等可能改变其指向的操作之后,当需要释放内存时,就使用 original_p 来调用 free 函数,这样就能正确地释放所分配的动态内存了

对同一块动态内存多次 free

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

解析

p 开辟的动态空间已经被 free 函数释放过一次了,重复释放同一块内存是不合法的

修改后的代码

1
2
3
4
5
6
7
8
9
10
11
void test()
{
int *p = (int *)malloc(100);
if (p!= NULL)
{
// 进行相关操作

free(p);
p = NULL; // 将指针置为NULL,避免后续误操作指向已释放内存的指针
}
}

忘记 free 动态内存

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;
}
}

int main()
{
test();
while(1);
}

这里进入一个无限循环 while(1),使得程序在执行完 test 函数后不会立即退出
后续没有对开辟的动态内存进行进一步的有效利用,由于没有调用 free 函数来归还这块动态分配的内存给系统的堆内存管理系统,随着程序的运行,如果多次调用 test 函数或者类似的函数进行大量的动态内存分配操作而不释放,就会导致内存泄漏

修改后的代码

1
2
3
4
5
6
7
8
9
10
void test()
{
int *p = (int *)malloc(100);
if (NULL!= p)
{
*p = 20;
// 完成赋值操作后,释放内存
free(p);
}
}

每次调用 test 函数时,在对动态分配的内存进行赋值操作后,就会及时将这块内存归还给系统的堆内存管理系统,避免了内存泄漏的问题

动态内存经典试题解析

题1

1
2
3
4
5
6
7
8
9
10
11
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}

运行 Test 函数会有什么样的结果?

解析

在这里插入图片描述

  1. 首先创建了指针变量 str ,置为空指针,将 str 作为实参传给形参 p,此时 p 也为空指针,将开辟的 100 个字节的空间地址放在形参 p 中,但是此时是传值调用,在 p 上的操作并没有实际作用在 str 上,所以 str 依然是空指针
  2. 然后把“hello world” 拷贝到 str 里时,需要对 str 解引用操作,向 NULL 指针所指向的空间进行字符串复制操作会导致程序崩溃,产生段错误等未定义行为
  3. 此时最危险的是当函数执行完毕返回时,这个局部变量 p 就会被销毁,它所指向的刚分配的内存地址也就丢失了,那么可能会造成内存泄漏

修改后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void GetMemory(char **p)
{
*p = (char *)malloc(100);
}

void Test(void)
{
char *str = NULL;
GetMemory(&str);
if (str!= NULL)
{
strcpy(str, "hello world");
printf("%s", str);
free(str); // 使用完内存后要记得释放
str = NULL;
}
}

通过创建二级指针,实现传址调用,并且在 Test 函数使用完内存后,通过 free 释放所分配的内存,避免内存泄漏,并将 str 重新赋值为 NULL 以防止野指针出现

题2

1
2
3
4
5
6
7
8
9
10
11
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}

运行 Test 函数会有什么样的结果?

解析

在这里插入图片描述

  1. 当函数GetMemory执行完毕并返回时,其栈帧会被销毁,这也就意味着字符数组 p 所占用的内存空间已经被释放掉了,尽管函数返回了p的地址,但这个地址所指向的内容已经是无效的了
  2. 调用 GetMemory 函数期望获取一个有效的字符串指针并赋值给str,然后通过 printf 输出该字符串,然而,由于前面提到的 GetMemory 函数返回的指针指向的是已经被释放的栈内存,所以在执行 printf(str) 时,就会出现未定义行为

修改后的代码

1
2
3
4
5
6
7
8
9
10
11
12
char *GetMemory(void)
{
static char p[] = "hello world";
return p;
}

void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}

将字符数组p定义为静态数组,静态数组在程序的整个生命周期内都存在,不会随着函数的结束而被销毁

题3

1
2
3
4
5
6
7
8
9
10
11
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}

运行 Test 函数会有什么样的结果?

解析

这题和题1修改后的代码基本差不多,最重要的一点就是没有释放动态内存空间,导致了内存泄漏,即随着程序的运行,不断地分配内存但从不释放,最终耗尽系统的可用内存资源

修改后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
free(str);
str = NULL;
}

确保程序在使用完动态分配的内存后能够及时释放,避免内存泄漏和野指针相关的问题

题4

1
2
3
4
5
6
7
8
9
10
11
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}

运行 Test 函数会有什么样的结果?

解析

在释放内存之后,紧接着进行了 if(str!= NULL) 的判断,这里存在一个误区,虽然直观上感觉释放内存后 str 应该变为 NULL ,但实际上 free 函数只是释放了 str 所指向的内存块,并不会自动将 str 指针本身设置为 NULL,所以此时 str 指针的值仍然是之前指向的那块已释放内存的地址(虽然这块内存已经被释放,不能再正常使用了),即 if 条件依然成立,执行语句导致未定义行为

修改后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void Test(void)
{
char *str = (char *)malloc(100);
if (str!= NULL)
{
strcpy(str, "hello");
printf("%s", str);

free(str);
str = NULL;

// 重新分配内存用于新的操作
str = (char *)malloc(100);
if (str!= NULL)
{
strcpy(str, "world");
printf("%s", str);

free(str);
str = NULL;
}
}
}

在释放内存后及时将 str 指针设置为 NULL,并且当需要再次进行存储字符串等操作时,重新通过 malloc 分配了新的内存空间,这样可以保证操作的安全性和正确性

希望读者们多多三连支持

小编会继续更新

你们的鼓励就是我前进的动力!

请添加图片描述