C语言之动态内存分配与释放
通用指针类型void
通用类型指针具有以下特点:
类型无关,赋值灵活:由于指针本质上是一个存储内存地址的变量,而内存地址是没有类型的,所以void指针可以存储任意类型数据的地址,指向任意类型对象。无论是整数、浮点数、字符或数组、结构体等类型都可以用void指针指向。表现在代码中就是:可以将任意类型指针(地址)赋值给void指针,这个过程一般是没有风险的。如下列代码:
int a = 42;
void* void_ptr = &a;// void指针转换成其它类型指针,在C++语法中必须加上显式类型转换说明
// 但C语言支持void指针隐式类型转换成各种其它指针类型,所以这个强转语法可加可不加
// float* float_ptr = (float*)void_ptr;// 错误的类型转换
float* float_ptr = void_ptr;// 解引用产生未定义行为
printf("%f\n", *float_ptr);
关于通用指针类型转换成其它指针类型,C语言和C++在语法上会有明显差别:
-
C语言更灵活允许隐式的类型转换,所以强转语法可加可不加。
-
C++则不支持这类隐式类型转换,必须要加上强转的语法,否则编译无法通过。
在C语言中,想要在堆上动态分配内存空间,主要依赖三个函数来完成,它们都声明在头文件<stdlib.h>当中:
- malloc
- calloc
- realloc
内存分配函数malloc
函数全名:memory allocation
函数声明:void* malloc(size_t size);
函数作用:
-
此函数会在堆空间上分配一片连续的,size个字节大小的内存块
-
此函数不会对内存块中的数据进行初始化,内存块中的数据是随机未定义的。
函数返回值:
-
如果分配成功,此函数会返回指向该内存块地址(首字节地址)的指针。注意返回的指针类型是void指针,在操作之前需要进行转换。
-
如果分配失败,此函数会返回一个空指针(NULL)。
内存泄漏
以往我们创建数组,创建结构体都是在栈上完成的,它们是栈数组、栈结构体,它们的内存空间都是由栈自动管理完成的。
但使用动态内存分配函数创建的数组、结构体都被存储在堆上,栈上存储的只不过是它的指针,如下图所示:
堆上存储的数据由程序员手动管理生命周期,手动申请内存资源,也需要手动释放内存空间。
内存泄漏是指程序在运行过程中,未能适时释放不再使用的内存区域,导致这部分内存在程序的生命周期内始终无法被重用。
内存泄漏在短时间内可能对程序而言,不是巨大的、致命的风险,但:长时间运行或频繁执行的程序中如果存在内存泄漏,随着时间的推移,被泄漏的内存累积会越来越多,最终可能导致程序运行缓慢甚至崩溃,特别是在内存有限的系统中。
内存释放函数free
为了避免内存泄漏,在确定动态分配的内存不再使用后,要及时调用free函数释放它。
函数声明:void free(void *ptr);
函数参数:必须是堆上申请内存块的地址(首字节地址),不能传递别的指针,否则会引发未定义行为。
函数功能:
- free函数并不会修改它所释放的内存区域中存储的任何数据。free 的作用仅仅是告诉操作系统这块内存不再被使用了,可以将其标记为可用状态,以供将来的内存分配请求使用。
- 释放后的内存区域中的数据一般仍然会继续存在,直到被下一次的内存分配操作覆盖。当然即便free前的原始数据一直存在未被覆盖,这片内存区域也不再可用了,因为你不知道什么时候数据就会被覆盖掉了。
- free函数不会修改传入指针指向的内容,更不会对实参指针本身做任何修改。
free函数调用后,指针指向的内存块就被释放了。但free函数不会改变传入的实参指针本身,所以free后的实参指针就变成了指向一片已释放区域的指针。这就是"悬空指针",悬空指针是野指针的一种特例,使用悬空指针同样会引发未定义行为。
为了避免悬空指针为程序安全带来隐患,推荐在free掉指针指向的内存块后,及时将指针置为空指针。
// 伪代码,p是一个指针类型
p = malloc(...);
free(p);
p = NULL;
总结:
-
正确传参free函数。free函数需要传入指向动态分配内存块首字节的指针,free之前不妨检查指针是否已被移动。
-
在free内存块后,建议立刻将指针设置为NULL。这样可以规避一些常见的问题:
-
避免了"double free"的风险。对空指针调用
free函数
是安全的,它不会有任何效果。 -
减少悬空指针出现的风险。解引用空指针导致程序崩溃,比悬空指针带来的未定义行为要更容易检测和修正。
-
-
慎重改变堆区指针的指向。指向堆区域的指针,如果需要改变它的指向,在改变之前应当考虑指向的内存块是否需要free。
-
多函数共同管理同一块内存区域时,应严格遵循单一原则。尤其是,哪个函数用于分配内存,哪个函数用于free释放内存,这两个函数一定要明确单一的职责。
// 在堆上动态分配内存拼接两个字符串
char* dynamic_strcat(const char* prefix, const char* suffix) {
// 计算拼接后字符串的长度int new_str_len = strlen(prefix) + strlen(suffix);
char *new_str = malloc(new_str_len + 1); // char在各平台上长度都是1,所以不用乘了if (new_str == NULL) {printf("ERROR: malloc failed in dynamic_strcat!\n");exit(1);}
// 长度是精确计算得出的,不用担心越界访问strcat(strcpy(new_str, prefix), suffix);
return new_str;
}int main(void) {char str1[] = "hello";
char str2[] = " world!";char* result_str = dynamic_strcat(str1, str2); // 注意只要涉及动态内存分配,一律用指针类型。这里不能用数组类型
puts(result_str);// 现在不再使用result字符串了,不要忘记free它
free(result_str);return 0;
}
清零内存分配函数calloc
函数全名:cleared allocation,该函数的最大特点是分配内存空间时会自动初始化0值。
函数声明: void* calloc(size_t num, size_t size);
。
函数参数:
num
表示要分配的元素数量size
表示每个元素的内存大小
此函数也会在堆空间上分配一片连续的内存空间,但不同的是,它基于元素的个数以及每个元素的大小来进行内存分配,所以calloc常用于在堆上分配数组的内存空间。
函数返回值:返回值在分配成功和失败时,和malloc是一致的。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>int main(void) {// 分配一个长度为10的整数数组int len = 10;int *arr = calloc(len, sizeof(int));int* p = arr;for (int i = 0; i < len; i++){printf("%d\n", *p++); // 此时数组中的元素都具有0值,而不是随机未定义的}// 使用完毕,不要忘记freefree(arr);return 0;
}
总得来说,推荐在动态分配数组内存空间时,尤其是需要将内存空间初始化为0值时,使用calloc函数。
内存重分配函数realloc
函数全名:reallocation,表示内存重新分配。
函数声明:void* realloc(void* ptr, size_t new_size);
函数参数:
ptr
:指向原来已分配内存的内存块。new_size
:新的内存块大小。
函数功能:
该函数根据参数取值的不同,可能表现为malloc或free函数的行为:
- 如果ptr指针是一个空指针,那么该函数的行为和malloc一致——分配new_size字节的内存空间,并且返回该内存块的指针。
- 如果new_size的取值为0,那么该函数的行为就是free函数,会释放ptr指向的内存块。
如果没有出现上述两种特殊情况,realloc用于重新调整已分配内存块的大小(也就是ptr指针指向的已分配内存块的大小):
- 当new_size的取值和已分配的内存块大小一致时,此函数不会做任何操作。
- 当new_size的取值比已分配的内存块小时,会在旧内存块的尾部(高地址)截断,被截断抛弃的内存块会被自动释放。
- 当new_size的取值比已分配的内存块大时(新内存块比旧内存块大时),会尽可能地尝试原地扩大旧内存块(这样效率高);
- 如果无法原地进行扩大,则会在别处申请空间分配new_size大小的新内存块,并将旧内存块中的数据全部复制进去后,将旧内存块自动释放。
- 不管采用哪种方式扩展旧内存块,新扩展部分的内存区域都不会初始化,仍只具有随机值。
函数返回值:
如果realloc函数分配内存空间成功,它会返回指向新内存块的指针,若失败,仍会返回空指针,且不会改变旧内存块。
总之,realloc函数适用于调整已分配内存块的大小,特别是在动态数组或数据结构的大小需要在程序运行时增加或减少时使用。
惯用法:
// p和arr_p指针类型一致
p = realloc(arr_p, new_size);
if (p == NULL){// 分配失败处理
}
// 代码运行到这里,realloc分配内存成功
arr_p = p;
这样写既避免了arr_p成为悬空指针,也不会因为realloc分配失败导致内存泄漏。