目录
一. 动态内存开辟的意义
我们已掌握的内存开辟方法,大多是开辟一块固定大小的内存,如:
int a = 0;
int arr[5] = { 0 };
但是,上述内存开辟的方式有两个局限性
- 只能开辟固定的内存空间大小
- 在定义数组是,必须指明数组的长度,需要在编译阶段为数组分配空间
在程序设计中,我们往往在程序运行阶段才能知道所需内存空间的大小,这时如果还使用上述的方法开辟内存空间,就会出现内存空间浪费或不足的问题,那么,这里就涉及到动态内存开辟的知识。动态内存开辟允许程序在运行时开辟内存,而不是在编译阶段就开辟,这样就可以根据需求,灵活调整开辟内存空间的大小。
二. 动态内存开辟的相关函数详解
2.1 malloc函数和free函数
2.1.1 函数功能及函数原型
malloc函数用于开辟一块指定字节数的内存空间,free函数用于释放动态开辟的内存空间。
malloc函数
函数原型:void *malloc( size_t size );
所需头文件:<stdlib.h>
函数参数:size,动态开辟内存的字节数
函数返回值:返回动态开辟的内存空间的起始位置地址,返回值类型为void*。如果动态内存开辟失败,则函数返回NULL。
free函数
函数原型:void free( void *memblock );
函数功能:释放指针memblock指向的动态开辟的内存空间,将这块空间还给操作系统以便其再次被动态分配。
警告:如果内存池中剩余的可以内存过小或开辟内存过大,都会造成动态内存开辟失败,malloc函数返回空指针NULL。因此,在使用动态内存开辟的空间之前要先进行指针有效性检验,即检验malloc的返回值是否为空指针,如果动态内存开辟失败,那么就应当终止程序。
注意:malloc函数返回值类型为void*,在使用malloc函数返回值时一般要进行强制类型转换。例如:int* ptr = (int*)malloc(10 * sizeof(int));
2.1.2 函数的使用演示
演示代码2.1中动态开辟了10*sizeof(int)个字节的内存空间,将ptr赋值为动态开辟的内存空间的起始位置的地址。经过指针的有效性检验后,将0~9共10个整形数据分别存入动态开辟的内存并打印,之后释放动态内存,将ptr置为空指针。程序运行的结果为:0 1 2 3 4 5 6 7 8 9
演示代码2.1:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* ptr = (int*)malloc(10 * sizeof(int)); //动态开辟10个整形的内存空间
if (NULL == ptr) //检验动态内存开辟是否成功
{
perror("malloc"); //开辟失败就报错
return 1;
}
int n = 10;
int i = 0;
for (i = 0; i < n; i++)
{
*(ptr + i) = i;
printf("%d ", *(ptr + i)); //0 1 2 3 4 5 6 7 8 9
}
free(ptr); //释放动态开辟的内存空间
ptr = NULL;
return 0;
}
警告:动态开辟的内存一定要使用free函数释放掉,否则很可能造成内存泄漏。
什么是内存泄漏?
内存泄漏,指在程序运行过程中,内存池被一点点的榨干,直至没有任何内存可用,如果申请了动态内存空间却不释放,那么这块空间就无法被再次分配,就极有可能造成内存泄漏。如果想要摆脱内存泄漏的困境,就只能重启系统,但重启系统可能会使已完成的工作丢失。
2.2 calloc函数
2.2.1 函数的功能及函数原型
calloc函数与malloc函数类似,都是向系统申请一定字节的动态内存空间。两者的不同在于:malloc函数不会将申请的空间初始化,而calloc函数会将申请到的内存空间的内容初始化为0。
calloc函数
函数原型:void *calloc( size_t num, size_t size );
函数参数:num表示开辟几块内存内存空间,size表示每块内存空间的长度(字节为单位)
函数功能:动态开辟num*size字节的内存空间,并将动态开辟的内存空间初始化为0
函数返回值:若开辟内存成功,返回动态开辟的内存空间的起始位置的地址;若开辟失败,就返回NULL
警告:在使用calloc函数开辟的内存空间之前,也要检验内存开辟是否成功。
2.2.2 函数的使用演示
演示代码2.3用于比较malloc和calloc函数的不同,分别使用malloc和calloc函数开辟5*sizeof(int)个字节的内存空间,用整形指针变量ptr1和ptr2接收。经过指针的有效性检验后,不进行人为的初始化,直接打印两块动态开辟的内存空间的内容。
程序运行结果为:
-842150451 -842150451 -842150451 -842150451 -842150451
0 0 0 0 0
malloc开辟的空间没有被初始化,打印的是内存中默认存储的内容(不同编译环境下可能会有所不同),而calloc将开辟的空间初始化为0,打印结果也全为0。
演示代码2.3:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* ptr1 = (int*)malloc(5 * sizeof(int)); //使用malloc函数动态开辟5个整形的内存空间
int* ptr2 = (int*)calloc(5, sizeof(int)); //使用calloc函数动态开辟5个整形的内存空间
if (NULL == ptr1 || NULL == ptr2) //检验动态内存开辟是否成功
{
perror("malloc"); //开辟失败就报错
return 1;
}
int i = 0;
for (i = 0; i < 5; i++)
{
//打印malloc函数开辟的内存空间里的内容
printf("%d ", *(ptr1 + i));
}
printf("\n");
for (i = 0; i < 5; i++)
{
//打印calloc函数开辟的内存空间里的内容
printf("%d ", *(ptr2 + i));
}
free(ptr1); //释放动态开辟的内存空间
ptr1 = NULL;
free(ptr2);
ptr2 = NULL;
return 0;
}
演示代码2.4使用calloc开辟了5个整形的内存空间,将这5块空间赋值为1~5,并打印。程序运行结果为:0 1 2 3 4。
演示代码2.4:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* ptr = (int*)calloc(5, sizeof(int)); //使用calloc函数动态开辟5个整形的内存空间
if (NULL == ptr) //检查动态内存开辟是成功
{
perror("calloc");
return 1;
}
int i = 0;
for (i = 0; i < 5; i++)
{
*(ptr + i) = i;
printf("%d ", *(ptr + i)); //0 1 2 3 4
}
free(ptr); //释放动态开辟的内存
ptr = NULL;
return 0;
}
2.3 realloc函数
2.3.1 函数功能及函数原型
- realloc函数可用于对已开辟的动态内存的大小进行调整
函数原型:void* realloc(void* memblock, size_t size)
函数参数:memblock -- 之前开辟的内存空间;size -- 新的空间的大小
返回值:指向重新调整的内存空间的起始位置,找不到合适的空间开辟就返回NULL
注意:不要拿指向原先开辟的内存空间起始位置的指针来接收realloc函数的返回值,应当将realloc函数的返回值先存入一个临时的指针变量,检验确定新的内存开辟成功后,在赋给指向原先开辟的内存空间起始位置的指针。
2.3.2 realloc函数的工作原理详解
realloc函数在成功开辟新的内存空间的前提下,可分为三种情况进行讨论:
- 原有空间后面的内存位置足够容纳新增的内存空间
- 原有空间后面的内存空间不足以容纳新增的内存空间
- 新开辟的内存空间大小小于原有内存空间
下面分类进行讨论。
- 当原有空间后面的内存位置足够容纳新增的内存空间时
如图2.1所示,此时直接在原来的空间后面新增内存空间,realloc函数返回原空间的起始位置地址,原有空间存储的内容不发生改变。
![]()
图2.1 原有空间后面的内存空间足够容纳新增空间是realloc函数的工作原理图解
- 当原有空间后面的内存位置不足以容纳新增的内存空间时
这时,函数会再在内存中找一块新的足够容纳新开辟的大小的内存,然后将原先开辟的内存中的内容复制到新开辟的内存中去,并将原先的内存归还给操作系统。函数的返回值为新开辟内存空间起始位置的地址。图解如图2.2所示。
![]()
图2.2 原有空间后面的内存空间足够容纳新增空间是realloc函数的工作原理图解
- 当新开辟的内存空间大小小于原有内存空间时
函数会缩减原有内存间,舍弃原先开辟的内存空间尾部的内容,并返回指向原空间起始位置的指针。
2.3.3 函数的使用演示
演示代码2.5首先利用malloc函数开辟10个整形大小的内存空间,将这块内存空间的内容赋值为1~10并打印,然后,使用realloc函数动态调整这块内存空间的大小为20*sizeof(int),将新开辟内存空间的内容赋值为1~20并打印。
演示代码2.5:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* ptr1 = (int*)malloc(10 * sizeof(int)); //使用malloc函数开辟10个整形的内存空间
if (NULL == ptr1) //检验动态内存开辟是否成功
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
//将malloc函数开辟的内存空间的内容赋值为1~10并打印
*(ptr1 + i) = i + 1;
printf("%d ", *(ptr1 + i)); //1 2 3 4 5 6 7 8 9 10
}
printf("\n");
//将malloc开辟的内存空间大小调整为20*sizeof(int)
//使用临时变量ptr2来接收
int* ptr2 = realloc(ptr1, 20 * sizeof(int));
if (NULL == ptr2) //检验内存大小调整是否成功
{
perror("realloc");
return 1;
}
ptr1 = ptr2; //通过指针有效性检验后将realloc函数新开辟空间的起始位置地址赋给ptr1
for (i = 10; i < 20; i++)
{
*(ptr2 + i) = i + 1;
}
for (i = 0; i < 20; i++)
{
//1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
printf("%d ", *(ptr1 + i));
}
free(ptr1); //释放动态开辟的内存空间
ptr1 = NULL;
ptr2 = NULL;
return 0;
}
三. 动态内存开辟常见的错误详解
3.1 对NULL指针进行解应用
如果内存池中剩余的可用内存过小或动态开辟的内存过大,就会造成动态内存开辟失败。
因此,在使用malloc、calloc函数开辟的内存空间或经realloc函数调整大小的内存空间之前一定要进行判空处理,即检验函数是否返回了空指针。
判空方式:
int main()
{
int* ptr = (int*)malloc(10 * sizeof(int));
if(p == NULL)
{
perror("malloc");
return 1;
}
//使用malloc开辟的空间
......
free(ptr); //切记,动态开辟的内存空间必须释放!
ptr = NULL;
return 0;
}
3.2 越界访问
动态开辟的内存越界访问与数组的越界访问类似,会修改越界位置内存中存放的内容,对程序造成不可预知的后果。如图3.1所示,越界访问程序会。

3.3 使用free函数释放非动态开辟的内存空间
释放非动态开辟的内存空间,程序会崩溃
演示代码3.1:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int arr[10] = { 0 };
int* p = arr;
free(p); //arr不是动态开辟的内存空间,程序会崩溃
p = NULL;
return 0;
}
3.4 仅使用free释放动态开辟的内存的一部分
在演示代码3.2中,对指针p解应用的同时p进行了自加操作,这是p便不再指向使用malloc函数动态开辟的内存的起始位置,这是free(p),仅释放了一部分动态开辟的内存,而余下一部分并没有还给操作系统,依旧有很高的内存泄漏风险。
演示代码3.2:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(5 * sizeof(int));
if (NULL == p)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 3; i++)
{
//p++会使p指向的位置发生改变
*p++ = i;
}
//此时free(p)只释放了一部分动态开辟的内存空间
free(p);
p = NULL;
return 0;
}
3.5 对同一块动态开辟的内存多次释放
演示代码3.5:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* ptr = (int*)malloc(5 * sizeof(int));
if (ptr == NULL)
{
return 1;
}
free(ptr);
free(ptr); //对malloc函数开辟的内存空间第二次释放,报错
return 0;
}
显然,稍有水平的程序员都不会连着写两个free释放同一块动态开辟的内存空间,那么,为什么还要将对同一块内存动态开辟的内存多次释放归为一种常见的错误呢? 这是因为在程序设计时,可能对一个指向动态开辟空间的指针进行多次复制,而前面可能对其中的某一个复制进行过free释放操作,从而后面再对其他的复制free时造成对同一块动态开辟的内存多次释放。
注意:free(NULL)语句不执行任何操作,在释放内存后将指针置为NULL也是避免多次释放动态开辟的内存的一种方法。
3.6 忘记释放动态开辟的内存
可能引发内存泄漏的问题。 若出现内存泄漏问题就只能重启系统来解决,但这可能会使前面的工作丢失。
四. 柔性数组
4.1 柔性数组的定义
在C99标准中,允许结构中的最后一个元素是未知大小的数组,这个不知道大小的数组就是柔性数组。
如:
typedef struct st_type
{
int i;
//a[0]就是柔性数组成员
//有些编译器不支持写为a[0],那么就写为a[]
int a[0];
}type_a;
4.2 柔性数组的特点
- 结构中的柔性数组成员前面必须至少有一个其他成员
- sizeof返回这种包含柔性数组的结构的大小时不包含柔性数组的大小
- 包含柔性数组的成员的结构应当使用malloc函数进行动态内存开辟,并且分配内存的大小应该大于结构的大小,以适应柔性数组的大小。
4.3 柔性数组的使用演示
演示代码4.1:
#include<stdio.h>
#include<stdlib.h>
//定义包含柔性数组成员的结构,并将该结构类型重定义为st_type
typedef struct
{
int n;
int arr[0];
}st_type;
int main()
{
//为包含柔性数组的结构动态开辟内存
//动态开辟的内存大小不应低于sizeof(st_type)
//柔性数组成员a的大小为10*sizeof(int)
st_type* ps = (st_type*)malloc(sizeof(st_type) + 10 * sizeof(int));
if (NULL == ps)
{
perror("malloc");
return 1;
}
ps->n = 10;
int i = 0;
//将柔性数组的每个元素依次赋值为0~9并打印
for (i = 0; i < ps->n; i++)
{
*(ps->arr + i) = i;
printf("%d ", *(ps->arr + i)); //0 1 2 3 4 5 6 7 8 9
}
free(ps); //释放动态开辟的内存空间
ps = NULL;
return 0;
}
全文结束,感谢大家的阅读,敬请批评指正。