一、为什么存在动态内存分配
C语言学习到现在,我们已经掌握和了解到的内存开辟方式是通过数据类型来定义变量,然后操作系统在栈区、静态区或者字符常量区上为该变量分配空间,例如:
int a; //在栈区上为 a 变量分配4个字节的空间 char arr[10]; //在栈区上为 arr 变量分配10个字节的空间 static int c; //在静态区上为 a 变量分配4个字节的空间 char* p = "abcdef"; //在栈区上为 p 变量分配4/8个字节的空间,在字符常量区上为常量字符串分配空间
但是上述的开辟空间的方式有两个特点:
- 空间开辟大小是固定的;
- 数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配;
但是对于空间的需求,不仅仅是上述的情况;有时候我们需要的空间大小在程序运行的时候才能知道, 那数组编译时开辟空间的方式就不能我们的需求了,所以C语言有了动态内存开辟(动态开辟的空间都是在堆区上的)。
二、动态内存开辟函数
1、malloc
函数功能
向内存申请一块连续可用的空间,并返回指向这块空间的指针。
函数参数
void* malloc (size_t size); # void* 函数返回值,申请成功返回指向开辟的空间的指针,申请失败则返回NULL; # size_t size 参数,指定要开辟多少个字节的空间;
函数使用
#include <stdio.h> #include <stdlib.h> //动态内存管理对应头文件 #include <string.h> //strerror对应头文件 #include <errno.h> //errno对应头文件 int main() { //申请40个字节的空间,交由指针变量p来管理 int* p = (int*)malloc(10 * sizeof(int)); //malloc申请空间可能会失败,所以要进行判断 //申请失败:打印错误信息并退出 if (p == NULL) { printf("%s\n", strerror(errno)); return 1; } //申请成功:使用 for (int i = 0; i < 10; i++) { p[i] = i; } for (int i = 0; i < 10; i++) { printf("%d ", p[i]); } //使用完:释放 free(p); //释放动态内存开辟的空间 p = NULL; //将p置空,防止野指针 }
注意事项
malloc 如果开辟成功,则返回一个指向成功开辟空间的指针;如果开辟失败,则返回一个NULL指针,因此 malloc 的返回值一定要做检查;
malloc 的返回值类型是 void* ,因为 malloc 函数并不知道需要开辟的空间的类型,所以我们在具体使用的时候需要进行一下强转;
如果给 malloc 的第二个参数 size 传一个0,这种行为是标准是未定义的,取决于编译器;
2、free
我们前面提到,动态内存空间的开辟都是在堆区的,在堆区上开辟的空间有一个特点,那就是堆区上的空间使用完之后不会自己主动释放,而是设计了一个释放动态内存的函数:free,需要程序员主动调用这个函数来释放空间;
当然,当我们关闭整个程序的时候,操作系统是会自动回收动态开辟的内存的(这就是为什么有的电脑故障关机重启之后问题就解决了);但是,在一些公司的大项目中,有的程序是需要7*24小时运行的,就比如腾讯云和阿里云的云服务器;
而一旦我们使用动态内存开辟的函数,比如malloc、realloc、calloc 开辟空间使用完忘记释放时,就会造成内存泄露(相当于你向内存申请了一块空间,但是你使用完之后不归还,这样别人也用不了这块空间了,虽然这块空间还存在,但是相当于没有了),这是我们就会发现,随着程序的持续运行,可供我们使用的内存会变得越来越少;
内存泄露是我们进行动态内存管理是最容易犯的错误,需要大家高度重视。
函数功能
用来释放动态开辟的内存。
函数参数
void free (void* ptr); # void* ptr 你要释放的空间的起始地址;
函数使用
在上面 malloc 函数的使用中我们已经演示了,将 p 的地址传递给 free 函数即可。
注意事项
- 如果参数 ptr 指向的空间不是动态开辟的,那么 free 函数的行为是未定义的;
- 如果参数 ptr 是NULL指针,则函数什么都不做;
3、calloc
函数功能
calloc 函数的功能和 malloc 十分相似,都是向堆区申请一块空间并返回空间的起始地址,但是 calloc 函数比 malloc 函数多了一个操作,那就是会将申请的空间里面数据全部初始化为0。
函数参数
void* calloc (size_t num, size_t size); # void* 函数返回值,申请成功返回动态开辟的空间的起始地址,申请失败则返回NULL; # size_t num 函数参数,用于指定要申请的元素个数: # size_t size 函数参数,用于指定每一个元素的大小(字节为单位);
函数使用
#include <stdio.h> #include <stdlib.h> //动态内存管理对应头文件 #include <string.h> //strerror对应头文件 #include <errno.h> //errno对应头文件 int main() { //申请40个字节的空间,交由指针变量p来管理 int* p = (int*)calloc(10, sizeof(int)); //calloc申请空间可能会失败,所以要进行判断 //申请失败:打印错误信息并退出 if (p == NULL) { printf("%s\n", strerror(errno)); return 1; } //申请成功:使用 for (int i = 0; i < 10; i++) { p[i] = i; } for (int i = 0; i < 10; i++) { printf("%d ", p[i]); } //使用完:释放 free(p); //释放动态内存开辟的空间 p = NULL; //将p置空,防止野指针 }
4、realloc
函数功能
调整已开辟的动态空间的大小。
函数参数
void* realloc(void* ptr, size_t size); # void* 函数返回值,开辟成功返回动态开辟的空间的起始地址,开辟失败则返回NULL; # void* ptr 函数参数,表示要调整的空间的起始地址; # size_t size 函数参数,新的空间的大小;
函数使用
#include <stdio.h> #include <stdlib.h> //动态内存管理对应头文件 #include <string.h> //strerror对应头文件 #include <errno.h> //errno对应头文件 int main() { //先开辟一块空间 int* p = (int*)malloc(10 * sizeof(int)); if (p == NULL) { printf("%s\n", strerror(errno)); return 1; } //扩容 //由于realloc可能会开辟失败,为了防止p指向realloc开辟失败的空间,从而丢失原来空间的情况,这里我们使用临时变量接受realloc的返回值 int* ptr = (int*)realloc(p, 20 * sizeof(int)); //申请失败:打印错误信息并退出 if (ptr == NULL) { printf("%s\n", strerror(errno)); return 1; } //申请成功:让p指向该空间并使用 p = ptr; for (int i = 0; i < 20; i++) { p[i] = i; } for (int i = 0; i < 20; i++) { printf("%d ", p[i]); } //使用完:释放 free(p); //释放动态内存开辟的空间 p = NULL; //将p置空,防止野指针 }
注意事项
realloc函数的出现让动态内存管理更加灵活;
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整;realloc 函数就可以做到对动态开辟内存大小的调整;
realloc 函数在调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间;
当 realloc 函数的第一个参数为NULL时,realloc 当作 malloc 函数使用;
realloc在调整内存空间的时候存在两种情况:
情况1:原有空间的后面有足够大的空间,可以让我们申请。这时扩展内存就在原有内存之后直接追加空间,原来空间的数据不发生变化。
情况2:原有空间的后面没有足够大的空间让我们申请。这时 realloc 函数会在堆空间上另找一个合适大小的连续空间来使用,这样函数返回的是一个新的内存地址;
所以我们在使用 realloc 函数的时候不要直接将重新调整的空间地址直接赋值给源空间地址,而是应该先进行空指针判断,避免开辟失败的同时还将源空间搞丢,造成内存泄漏;
三、常见的动态内存错误
1、对NULL指针的解引用操作
上面我们提到,malloc、calloc、realloc 这些函数向内存申请空间是有可能会失败的,申请失败函数就会返回空指针,如果我们不对函数的返回值进行判断,而直接对其解引用的话,就会造成程序崩溃;例如:
void test() { int* p = (int*)malloc(INT_MAX); *p = 20;//如果p的值是NULL,就会有问题 free(p); }
解决办法:在使用动态内存管理函数申请动态内存时,一定要记得检查函数的返回值是否为空。
2、对动态开辟空间的越界访问
这种情况和数组的越界访问十分相似,我们直接看示例:
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); }
3、使用free释放非动态开辟的空间
free 函数是专门用于释放动态开辟的空间的,如果对非动态开辟的空间进行 free 操作,会造成程序崩溃,示例:
void test() { int a = 10; int* p = &a; //在栈区上开辟空间 free(p); }
4、使用free释放一块动态内存的一部分
当我们成功开辟一块动态空间并将它交由一个指针变量来管理时,我们可能会在后面的程序中让该指针变量自增,从而让其不再指向该动态空间的起始位置,而是指向中间位置或者结尾,这时我们在对其进行free操作时,也会导致程序崩溃,因为free函数必须释放一整块动态内存,而不能释放它的一部分。示例如下:
void test() { int i = 0; int* p = (int*)malloc(10 * sizeof(int)); if (NULL == p) { exit(EXIT_FAILURE); } for (i = 0; i < 5; i++) { *p = i; p++; //指针变量p自增导致其丢失动态内存的起始地址 } free(p); }
解决办法:将申请的动态内存交由两个指针变量进行管理,其中一个用于各种操作,另外一个用于记录空间的起始地址。
5、对同一块动态内存多次释放
我们在写程序的时候可能在程序中的某一位置已经对动态内存进行释放了,但是随着后面代码的展开,我们可能忘记了而重复对一块动态内存进行释放。示例如下:
void test() { int* p = (int*)malloc(100); if (p == NULL) { exit(-1); } free(p); //....... free(p);//重复释放 }
解决办法:每次free掉一块动态内存时,都将相应的指针变量置空,这样即使后面重复释放,free(NULL) 也没有任何影响。
6、动态内存忘记释放(内存泄漏)
在讲解free函数的时候我们已经说过了内存泄漏的原因以及危害,对于内存泄露这个问题,可以说是防不胜防,我们只能谨慎的写好每一行代码,最大程度上避免内存泄漏。下面我举一个可能造成内存泄漏的经典案例:
void test() { int* p = (int*)malloc(10 * sizeof(int)); if (p == NULL) { exit(-1); } int flag = 0; scanf("%d", &flag); if (flag == 2) { //...... --程序逻辑 return; //内存泄漏 } else { //...... --程序逻辑 } free(p); p = NULL; }
我们发现,代码编写者以及十分注意内存泄露的问题了,在test函数的末尾对动态开辟的空间进行了释放,还把指针变量p置为了空,但是这个函数还是可能会造成内存泄露,因为当函数从flag == 2 的路径返回时,test函数不会对该空间进行释放,所以说,内存泄漏真的是防不胜防。