目录
前言:动态内存管理,嗯~,一个高大上的名字,其实也不是很难(支付宝到账一桌头发),咳咳,那在这节中我们主要习什么呢?首先我们会学习如何使用动态内存开辟函数在内存中(堆区)中开辟内存空间,然后在前一章自定义类型中结构体的基础下,引入柔性数组的概念。
引入
为什么存在动态内存分配
关于内存分配,我们现在只会在内存的栈空间上开辟内存,例如
int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
以上这种内存开辟的方式主要有两个特点
- 在空间内存上开辟的空间大小是固定的。
- 在声明数组时,必须要指定数组的长度,它所需要的内存在编译时分配。不能通过给变量赋值的方式指定数组
长度,但这不是绝对的,因为有的编译器支持变长数组,变长数组就支持给变量赋值的方式指定数组长度,不建议
使用,因为绝大多数编译器不支持
这种比较固定的内存开辟方式,其实有时候并不能满足我们的需求,例如:给一个已知长度的数组随机赋值,设计
一个函数返回一个包含其中所有奇数的数表。此时仅仅用普通的方法就很困难,不仅仅是上述的情况。有时候我们
需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了,这时动态内存开辟就十
分有优势。
动态内存函数的介绍
malloc函数声明
void* malloc (size_t size);
功能:这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
传参特点:
1.如果开辟成功,则返回一个指向开辟好空间的指针。
2.如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
3.返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
4.如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
malloc函数使用
#include <stdio.h>
int main()
{
int* ptr = NULL;
ptr = (int*)malloc(num*sizeof(int));
if(NULL != ptr)//判断ptr指针是否为空
{
int i = 0;
for(i=0; i<num; i++)
{
*(ptr+i) = 0;
}
}
free(ptr);//释放ptr所指向的动态内存(后面介绍)
ptr = NULL;
return 0;
}
free函数声明
void free (void* ptr);
功能:free,专门是用来做动态内存的释放和回收的,即释放在内存已堆区中开辟的内存
传参特点:
1.如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
2.如果参数 ptr 是NULL指针,则函数什么事都不做。
3.一般对动态内存进行空间释放后,要将指针赋值为空指针NULL,防止造成野指针
free函数的使用
#include <stdio.h>
int main()
{
int* ptr = NULL;
ptr = (int*)malloc(num*sizeof(int));
if(NULL != ptr)//判断ptr指针是否为空
{
;
}
free(ptr);//释放已开辟的内存
ptr = NULL;
return 0;
}
calloc函数的声明
void* calloc (size_t num, size_t size);
- num代表开辟元素个数, size代表该元素类型的字节大小
2.函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
如果函数开辟空间失败的情况,则返回一个空指针NULL。
3.与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
calloc函数的使用
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* arr = calloc(4, sizeof(int));//申请4个整形空间
int i;
for (i = 0; i < 4; i++)
{
arr[i] += i;
printf("%d ", arr[i]);
}
free(arr);//释放堆上申请的空间
arr=NULL;
return 0;
}
realloc函数
前面的两种动态内存空间开辟函数很明显都有一个共同的缺点,就是空间一旦申请成功,则无法改变空间大小
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时
候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小
的调整。
void* realloc (void* ptr, size_t size);
1.ptr 是要调整的内存地址
2.size 调整之后新的大小
3.返回值为调整之后的内存起始位置。
4.这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间。
realloc函数开辟空间的两种情况
由于realloc可以对堆上已开辟的空间进行空间的扩容或减容,所以此时就必须考虑已开辟的空间后是否有足够的空间进行操作,即两种情况,第一种:已开辟空间后有足够的空间,第二种:已开辟空间后没有足够空间。
第一种
第二种
这种情况较为特殊,所以他的空间开辟方式是从新在堆中寻找一块足够的空间,然后返回一个新的指针
realloc函数的使用
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* arr = (int*)calloc(4, sizeof(int));//申请4个整形空间
if (arr == NULL)
{
perror(malloc);//打印空间开辟错误信息
}
int i;
//给已开辟的4字节整形空间赋值
for (i = 0; i < 4; i++)
{
arr[i] += i;
printf("%d ", arr[i]);
}
printf("\n");
realloc(arr, (4 + 2) * sizeof(int));
if (arr == NULL)
{
perror(calloc);//打印空间扩容错误信息
}
//给扩容后整形空间赋值
for (i = 0; i < 6; i++)
{
arr[i] = i;
printf("%d ", arr[i]);
}
free(arr);//释放堆上申请的空间
return 0;
}
以上就是动态内存开辟函数的简单介绍和使用,其实内容不多,但是我们在使用的过程中往往会出现很多不必要的小错误,那接下来就简单罗列几种使用动态内存开辟时常见的错误,其实你看到这里就已经可以退出这篇文章了,因为你一定不会犯错误的,呃呃呃呃呃~~
使用动态内存的常见错误
1.对NULL空指针解引用
我们在使用函数进行空间开辟的时,常常容易忽略空间开辟是否成功,如果开辟成功则返回有效的空间地址,反之失败的话就会返回空指针,这时如果不检查指针的有效性,则可能会造成对NULL指针的非法解引用
void test()
{
int *p = (int *)malloc(sizeof(int));
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
小结:
所以为了避免出现这种错误,我们必须要在申请空间后,检查申请是否成功。
2.对动态开辟的空间越界访问
越界访问是什么意思呢?比方说下面这种情况
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
perror(malloc);
}
//开辟了10个整形空间,但循环进行了11次
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);
}
这里的p指向的空间是在堆栈上开辟的,并不是在堆上开辟的空间,所以非堆上的空间进行空间释放是错误的。
情况二:
使用free释放一块动态开辟内存的一部分,简单来说就是在使用动态空间时,指针指向的位置可能在使用的过程中发生偏移,即不再指向空间的初始位置。如下面这种使用方式
void test()
{
int *p = (int *)malloc(100);
if(p==NULL)
{
perror(malloc);
}
p++;//指针指向的位置发生偏移
free(p);//p不再指向动态内存的起始位置
}
小结:
避免发生这种错误其实解决方法也不是很困难,只要想办法将指针从新指向空间起初位置,例如可以创建一个临时指针将初始地址储存起来,方便其空间释放。
4.对同一片空间多次释放
这种情况其实在我们写代码的过程中也十分容易发生,比如我们在写一个较为长的代码时,往往可能会忘记我们已经释放过一次,从而造成空间的重复释放。
5.忘记对动态空间的释放
这种情况造成的后果是比较严重的,即会造成内存泄露,以至于在整个程序中,这片空间无法使用,如下面这种情况
void test()
{
//此空间
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();//函数调用结束后空间销毁
while(1);
}
这种情况往往是因为动态内存是在调用函数时进行开辟的,当调用函数结束时,如果不是放空间或返回空间地址,那么接下来就无法对这片空间使用或者释放,从而造成内存泄漏。
柔性数组
柔性数组顾名思义就是柔软的数组~咳咳,我在放屁,那什么是柔性数组呢?
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
例如
typedef struct s
{
int i;
int a[];//柔性数组成员
}s;
柔性数组的特点
1.结构中的柔性数组成员前面必须至少一个其他成员。
2.sizeof 返回的这种结构大小不包括柔性数组的内存。
3.包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大
小,以适应柔性数组的预期大小。(至少满足除柔性数组外其他成员组成的结构体大小)。
柔性数组的使用
柔性数组最大的特点就是结构体中数组大小可以由自己需求决定
typedef struct s
{
int i;
int a[];//柔性数组成员
}s;
//代码1
int i = 0;
s*p = (s*)malloc(sizeof(s)+100*sizeof(int));
//业务处理
p->i = 100;
for(i=0; i<100; i++) {
p->a[i] = i; }
free(p);
使用柔性数组的好处
其实如果没有柔性数组我们也可以实现同样的效果,那为什么还要有柔性数组这一概念呢?请看下面两种做法
一般做法
//代码2
typedef struct st_type
{
int i;
int *p_a;
}type_a;
type_a *p = (type_a *)malloc(sizeof(type_a));
p->i = 100; p->p_a = (int *)malloc(p->i*sizeof(int));
//业务处理
for(i=0; i<100; i++) {
p->p_a[i] = i; }
//释放空间
free(p->p_a);
p->p_a = NULL;
free(p);
p = NULL;
柔性数组做法:
//代码1
int i = 0;
type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
//业务处理
p->i = 100;
for(i=0; i<100; i++) {
p->a[i] = i; }
free(p);
相对于第二种方法很明显第一种方法有一不足的地方就是进行了多次空间申请,回顾上方使用动态开辟的常见错误可知的如果进行多次空间申请,之后空间释放也不方便,也容易忘记释放空间,重复释放空间的概率就会很大,除了容易造成发生错误之外,多次进行空间申请还会造成一些内存碎片。使用柔性数组优点详情见下
第一种优点:
方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第二种优点
这样有利于访问速度.
连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得也没多高了,反正 你跑不了要用做偏移的
小结:这篇文章我们主要学会动态内存函数的基本使用与注意事项,了解为什么柔性数组的存在意义,好啦这篇文章就讲到这里,谢谢您的耐心观看