一、引言

当我们写了一段程序,创建了一个变量或者一个数组,这些操作都需要在内存中开辟出一块空间。但是我们过去的这些操作有一定的局限性:开辟的空间大小是固定的,并且数组在申明的时候,必须指定数组的长度,数组空间一旦确定大小就无法再调整了。

虽然在某些编译器(例如gcc)中。允许我们使用一个变量来指定数组的大小,但是在大部分编译器中这种变长数组都是不允许的。所以C语言引入了动态内存的开辟方式,让程序员可以自己申请和释放空间,这种方法就比较的灵活了。


二、malloc函数和free函数

2.1 malloc函数

首先明确一点:在使用动态内存管理函数的时候要包含头文件

C语言给我们提供了这么一个动态内存开辟的函数:

void* malloc ( size_t size );

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

注意!

  • 如果malloc开辟成功,则会返回一个指向开辟出的这块空间的指针;如果malloc开辟失败,则会返回一个NULL指针,因此:malloc的返回值一定要作检查
  • 返回的指针的类型为void*,所以我们在使用malloc的时候需要对其返回的指针进行强制类型转换,具体类型由我们的需求而定
  • 如果malloc的参数为0,这种情况是标准未定义的,具体执行情况看编译器

2.2 free函数

为什么要把malloc和free放在一起讲呢?当你未来在写代码时使用了malloc函数或者其他动态内存管理函数的时候,就必须要用到free函数,接下来我们具体讲解一下

free函数是C语言专门用来做动态内存的释放和回收的函数,其原型如下:

void free ( void* ptr );

free函数可以用来释放我们通过malloc动态开辟的内存空间,如果一块动态开辟的内存空间没有被free函数释放的话,就会造成内存泄漏

注意!

  • 如果参数 ptr 指向的空间不是动态内存开辟的则会报错
  • 如果参数 ptr 为 NULL ,则free函数什么都不做

free函数和malloc函数一样要通过头文件声明

学会了这两个函数之后,我们就可以开始练习写代码了

#include #include int main(){int *arr = (int *)malloc(sizeof(int) * 10);if (arr == NULL){perror("malloc fail");return 1;}free(arr);arr = NULL; // 为什么要置空?return 0;}

上面,指针arr指向了我们动态开辟的一块40个字节的空间,然后我们free掉这一块空间后,又给arr置空了,为什么要这么做呢?

实际上,arr指向的空间被释放掉后就变成了野指针,为了防止错误的操作,我们就对其进行置空防止后续有人错误使用


三、calloc函数

C语言还提供了calloc函数用来进行动态内存分配,这个函数也和malloc很相似

void* calloc ( size_t num , size_t size );

calloc函数可以为 num 个大小为 size 的元素开辟一块空间,并且把这块空间的每个字节都初始化为0,这也是它和malloc最大的区别

例如:

#include #include int main(){int *arr = (int *)calloc(10, sizeof(int));if (arr == NULL){perror("calloc fail");return 1;}free(arr);arr = NULL;return 0;}

输出结果为

所以如果我们需要对动态开辟的内存初始化的话,calloc是更好的选择


四、realloc函数

动态内存管理就是让我们更加自由的去开辟内存,但是光看上面几个函数似乎还不够自由。这里就引入一个动态内存管理的好帮手:realloc函数。这个函数的出现让动态内存管理更加灵活。

当我们动态开辟内存之后,有时会发现开辟出的空间太小了不够用,有时又会发现开辟出的空间太大了有点浪费。为了合理的使用内存,对内存的大小做灵活的调整,就需要使用realloc函数来对动态开辟的内存大小进行调整。

void* realloc ( void* ptr , size_t size );

其中,ptr 是待调整大小的内存空间的地址,size 是调整之后的新大小,返回值是调整后的内存的起始位置地址。

realloc在调整内存空间时存在两种情况:

  1. 原空间后面有足够大的空间,可以在原有内存之后直接增加新的空间,原空间的数据不发生变化
  2. 原空间后面没有足够大的空间,就会在内存的堆区重新找一块能够容纳新空间的位置,同时把旧空间的数据拷贝到新空间,然后对旧空间进行释放并返回新空间的起始地址

这里引入一个问题:下面的代码1和代码2哪个更好呢?

//代码1#include #include int main(){int *arr = (int *)malloc(sizeof(int) * 10);arr = (int *)realloc(arr, 1000);free(arr);arr = NULL;return 0;}
//代码2#include #include int main(){int *arr = (int *)malloc(sizeof(int) * 10);int *tmp = (int *)realloc(arr, 1000);if(tmp!=NULL){arr = tmp;}free(arr);arr = NULL;return 0;}

代码2更好。实际上,当我们使用realloc函数调整动态开辟的内存大小的时候,是存在失败的可能的。如果我们直接对指向原空间的指针来进行调整,一旦失败则会返回NULL,指针无法再指向原空间,则原空间就无法被释放,造成内存泄漏

所以在代码2中,我们使用一个tmp指针来进行调整,调整成功后再赋给arr,这样就更保险。

五、常见的动态内存管理的错误

5.1 对NULL指针的解引用操作

void test(){int *arr = (int *)malloc(sizeof(int) * 10);*arr = 20;free(arr);}

如果malloc开辟失败,则arr的值为NULL,再对其解引用就会造成错误

所以使用malloc、calloc、realloc等函数的时候,最好增加一个检测环节避免对NULL指针错误使用

5.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);}

我们用malloc开辟了一块10个sizeof(int)的空间,但是在循环中却访问了第11个位置,属于越界访问,也是错误的做法

5.3 对非动态开辟的内存使用free释放

上面提到过,只有动态开辟的内存才能用free将其释放,如果我们用free释放非动态开辟的内存就会造成错误,如下:

void test(){int i = 0;int *p = &i;free(p);}

5.4 使用free不完全释放动态开辟的内存

void test(){int *p = (int *)malloc(100);p++;free(p);}

如上,p++之后不再指向这块动态开辟的空间的起始地址,所以free函数无法对其完全释放。所以当我们使用free函数释放内存的时候需要注意指针是否指向这块内存的起始位置。

5.5 对一块动态内存重复释放

void test(){int *p = (int *)malloc(100);free(p);free(p);}

对已经被释放的动态内存进行二次释放也是错误的做法

5.6 不释放动态开辟的内存

前面说过当我们动态开辟了一块内存之后,一定要在程序中的某个位置把它free掉,不然就会造成内存泄漏

六、柔性数组

在C99中,结构体的最后一个成员允许是位置大小的数组,这个就叫柔性数组成员

例如:

typedef struct{int i;int a[0];//柔性数组成员} type_a;

当然这么做有些编译器会报错,可以改成

typedef struct{int i;int a[];} type_a;

柔性数组的特点有:

  • 结构体中的柔性数组成员前面必须至少有一个其他成员
  • sizeof返回结构体的大小时,不包括柔性数组成员
  • 包含柔性数组成员的结构体要用malloc进行内存的动态分配,并且分配的内存大小要大于结构体的大小,以应对柔性数组的预期大小

我们可以看看上面那个结构体的大小是多少

可以看到刚好是第一个成员的大小,不包含下面的柔性数组成员

完.