一.为什么存在动态内存分配?
我们已经掌握的分配内存的方式有:
int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
但是上述的开辟空间的方式有两个特点:
1. 空间开辟大小是固定的。
2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。(即使是变长数组,其数组长度可以由变量指定,但是定义后也不可修改)
但是如果我们开辟了10个空间,等到使用的时候才发现需要100个,或者只需要5个,就会存在空间不够或浪费的情况,这时候就需要我们进行动态内存空间的开辟了。
二.动态内存函数的介绍
下面的四个动态内存函数都需要引入头文件<stdlib.h>
2.1 malloc
用来开辟内存空间:
void* malloc(size_t size);//size表示申请空间的大小,单位是byte
如果开辟成功,则返回一个指向开辟好空间的指针。
如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己
来决定。
如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
2.2 free
用来释放动态开辟的内存空间:
void free(void*ptr);//ptr代表指向要申请空间的指针
free函数用来释放动态开辟的内存。
如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
如果参数 ptr 是NULL指针,则函数什么事都不做。
free函数释放ptr指向的内存空间后,不会将其置空,所以我们需要手动ptr=NULL操作,防止下面再使用ptr指针变成野指针
#include <stdio.h>
#include <stdlib.h>
int main()
{
//代码1
int num = 0;
scanf("%d", &num);
int arr[num] = {0};//定义了一个变长数组
//代码2
int* ptr = NULL;
ptr = (int*)malloc(num*sizeof(int));//必须要将malloc函数的返回值进行强制类型转换
if(NULL != ptr)//判断ptr指针是否为空
{
int i = 0;
for(i=0; i<num; i++)
{
*(ptr+i) = 0;
}
}
else perror("malloc");//如果申请内存空间失败,会返回错误信息malloc: Not enough space
free(ptr);//释放ptr所指向的动态内存
ptr = NULL;//是必要的的,防止下面的代码块对野指针ptr进行操作
return 0;
}
上面的代码中,其实如果我们没有将ptr指向的空间free掉,程序结束时也会将这块空间自动归还给操作系统。但是还是建议自己释放这块空间,否则会发生内存泄漏。(见下文)
2.3 calloc
calloc 函数也用来动态内存分配
void * calloc(size_t num,size_t size);//num表示要申请的元素个数,
//size表示每个元素所占的内存空间的大小
与malloc相比,calloc的区别是在开辟这块空间的时候会自动将每个字节初始化为0
其余的功能与malloc并无二异,如果空间开辟成功会返回指向这片空间的指针,如果开辟失败会返回NULL
来看下面的代码进行验证
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p = (int*)calloc(10, sizeof(int));//calloc的返回值要进行强制类型转换
if(NULL != p)
{
for(int i=0;i<10;i++)
p[i]=i;
}
free(p);
p = NULL;
return 0;
}
所以如果要求对申请的空间进行初始化,使用calloc函数即可
2.4 realloc
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理地使用内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小
的调整。
void* realloc(void*ptr,size_t size);//ptr指向原来的空间,
//size是要重新申请的空间大小,单位是byte
realloc扩展空间时会发生两种情况:
情况一:原有空间之后有足够大的空间
情况二:原有空间之后没有足够大的空间
不管是情况一还是情况二,如果realloc扩容失败,会返回NULL
由于上述的两种情况,realloc函数的使用就要注意一些
举个例子
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *ptr = (int*)malloc(100);
if(ptr != NULL)
{
//...
}
else
{
perror("malloc");
}
//扩展容量
//代码1
ptr = (int*)realloc(ptr, 1000);//这样做是不可以的,如果realloc扩容失败返回NULL,用指向
//原空间的指针接收,会丢失原空间的地址
//更改如下:
int*p = NULL;
p =(int*)realloc(ptr, 1000);
if(p != NULL)
{
ptr = p;
p=NULL;//如果我们下面不再使用p,最好将p置空
}
//...
free(ptr);
return 0;
}
如果realloc要改变的空间容量size比原来空间小的话,原来空间的数据会有丢失
如果给realloc的参数1传的是空指针,那么realloc的作用相当于malloc
int *p=(int*)realloc(NULL,40);
int *p=(int*)malloc(40);
上图中,第一行和第二行的代码功能是一样的
三.常见的动态内存错误
来看看下面的代码你可以挑出来几个错误吧
//代码1
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,对空指针进行解引用操作
free(p);
}
//代码二
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
perror(malloc);
}
for(i=0; i<=10; i++)
{
*(p+i) = i;//当i是10的时候越界访问
}
free(p);
}
//代码三
void test()
{
int a = 10;
int *p = &a;
free(p);//对非动态开辟的内存空间释放
}
//代码四
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置,不能只释放开辟空间的一部分
}
//代码五
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放,如果想避免这个问题,在free(p)之后直接把p置空
//这样free(NULL)等同于没有操作
}
//代码六
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();//出了函数test,局部变量p会自动销毁,那么p指向的内存空间便无法释放
//存在内存泄漏问题
return 0;
}
内存泄漏问题可能对于当前的我们来说并不是很严重的问题,反正程序结束时操作系统都会回收动态申请的内存。但是如果对于一个24小时运营的服务器来说,会产生非常大的内存消耗。所以高质量编程,从现在做起!
四.经典笔试题
请问下面的代码运行test函数会产生什么结果?
void GetMemory(char *p)
{
p = (char *)malloc(100);//出了GetMemory函数,局部变量p会销毁,其空间无法被释放
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");//str在函数GetMemory中进行的是传值操作,给p申请内存空间
//其空间的地址不会放到str里面去,所以strcpy操作不成功
printf(str);
}
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();//出了函数GetMemory,p数组占用的空间会归还给操作系统
//再次访问这片空间,会存在非法访问
printf(str);
}
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);//malloc和free一定要配对使用,否则会出现内存泄漏!
}
void Test()
{
int*p=(int*)malloc(100);
if(1)
return;
free(p);//有时候即使malloc和free成对使用,也会出现内存泄漏问题,因此要多加注意
}
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");//已经将str的空间归还,存在非法访问
printf(str);
}
}
五. C程序的内存开辟
栈区(stack):存放运行函数而分配的局部变量,函数参数,返回数据,返回地址等
堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 (动态内存开辟的空间就是在堆上进行的)
数据段(静态区):(static)存放全局变量、静态数据。程序结束后由系统释放
代码段:存放函数体(类成员函数和全局函数)的二进制代码
所以用static修饰的局部变量会存放在静态区,知道程序结束才会销毁,因此生命周期变长,但是作用域还是局限于它所在的花括号内。
六.柔性数组
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
柔性数组可以用下面两种方式定义
//方法1
typedef struct type1
{
int b;
int a[0];//柔性数组成员
}type1;
//方法二
typedef struct type2
{
int b;
int a[];//柔性数组成员
}type2;
6.1 柔性数组的特点
结构中的柔性数组成员前面必须至少一个其他成员。
sizeof 返回的这种结构大小不包括柔性数组的内存。
包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大
小,以适应柔性数组的预期大小
那么柔性数组该如何使用呢?
type1* p = (type1*)malloc(sizeof(type1) + 10 * sizeof(int));//想要申请含有十个元素的整形数组
for (int i = 0; i < 10; i++)
{
p->a[i] = i + 1;
}
free(p);
当然,如果10个整型空间不够的话,还可以用realloc函数进行扩容
即使给柔性数组分配了内存空间,计算该结构体(变量)的大小时仍然不会把柔性数组计算在内
6.2柔性数组的优势
上面的代码也可以设计为
typedef struct type3
{
int i;
int *pa;
}type3;
type3*p=(type3*)malloc(sizeof(type3));
p->pa=(int*)malloc(10*sizeof(int));
for (int i = 0; i < 10; i++)
{
p->pa[i] = i + 1;
}
//...
free(p->pa);
p->pa=NULL;
free(p);
p=NULL;
我们发现,与上面的代码块相比,柔性数组有两大优势
内存申请和释放次数少
当使用柔性数组进行动态内存开辟时,只需要用一次malloc/free,当使用指针时需要进行两次。因此使用柔性数组会减少产生由于多次申请动态内存产生的内存碎片
空间连续
当使用柔性数组申请空间时,空间是连续存放的,这就提高了访问速度