前言
为什么存在动态内存的分配呢?我们可以思考我们写过的代码,当我们创建数组时,创建了很多个空间,但是有些空间特别长时间都不会用到,所以这一现象就造成了空间的浪费。因此我们就引出了动态内存分配这个概念。
动态内存函数的介绍
一、malloc
1、函数malloc()
void* malloc (size_t size);
函数说明:这个函数向内存申请了size个字节的连续空间,并返回指向这块空间的指针。
2、函数介绍
<1>如果函数开辟空间成功,那么函数返回指向该空间的指针。
<2>如果函数开辟空间失败,那么函数返回NULL。(因此一定要检查返回的是不是空指针)
<3>返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体使用的时候需要使用者自己决定类型。
<4>如果参数size为0,malloc的行为是标准未定义的,取决于编译器。
二、free
1、函数free()
void free(void* ptr);
函数说明:这个函数的功能是将动态内存进行释放。
2、函数介绍
<1>如果参数ptr指向的空间不是动态开辟的,那么free函数的行为是未定义的。
<2>如果参数ptr为空指针,那么函数什么也不用做。
<3>与动态内存开辟函数是成对出现的。
代码:
#include <stdio.h>
int main()
{
int num = 0;
scanf("%d", &num);
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;
}
三、calloc
1、函数calloc()
void* calloc(size_t num,size_t size);
函数说明:此函数也用于动态内存开辟,返回值是指向所开辟空间的指针。
2、函数介绍
<1>函数的功能是为num个大小为size的元素开辟一块空间,并把空间的每个字节初始化为0
<2>与函数malloc相比,malloc不会将所开辟的空间初始化为0,而calloc会将所开辟的空间初始化为0.
代码:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p = calloc(10, sizeof(int));
if (NULL != p)
{
for (int i = 0; i < 10; i++)
printf("%d ",*(p+i));
}
free(p);
p = NULL;
return 0;
}
四、realloc
1、函数realloc()
void* realloc(void*ptr,size_t size);
函数说明:有时申请的空间太大,有时申请的空间太小,所以为了灵活的改变所申请的空间就需要realloc函数。
2、函数介绍
<1>ptr是要调整的内存地址
<2>size是调整之后新大小
<3>返回值为调整之后的内存起始位置。
<4>realloc在调整内存空间时存在三种情况。
①在原内存空间的基础上往后继续增加空间,返回值为原本开辟的空间的指针。(红色空间为原内存空间,需要增加6个字节的空间)
②原内存空间后面没有充足的空间可以增加时,realloc会在后面找个位置开辟新空间,并且将原内存空间的值拷贝到新开辟的内存空间,同时释放原内存空间,返回值为指向新开辟的内存空间的指针。(红色空间为原内存空间,需要增加6个字节的空间)
③内存中没有充足的空间可以增加需要的空间时,原内存空间大小维持不变,返回值为NULL。(红色空间为原内存空间,需要增加6个字节的空间)
<5>如果realloc的第一个参数为空指针,那么可以起到和malloc一样的效果。
代码:
int main()
{
int *ptr = malloc(100);
if (ptr != NULL)
{
//
}
else
{
return;
}
//ptr = realloc(ptr, 1000);//直接将返回值放到ptr中是存在缺陷的,因为如果realloc返回NULL,那么原本的数据也将丢失。
int*p = NULL;
p = realloc(ptr, 1000);
if (p != NULL)
{
ptr = p;
}
free(ptr);
return 0;
}
常见的动态内存错误
1、对NULL指针的解引用操作
如:如果需要动态开辟的内存太大,而内存空间不足,就会返回NULL造成错误。
void test()
{
int* p = (int*)malloc(INT_MAX);
*p = 20;
free(p);
}
2、对动态开辟空间的越界访问
如:
void test()
{
int i = 0;
int *p = (int*)malloc(10 * sizeof(int));
if (NULL == p)
{
return;
}
for (int i = 0; i <= 10; i++)
{
*(p + i) = i;//当i=10时,越界访问了
}
free(p);
}
3、对非动态开辟内存使用free释放
如:
void test()
{
int a = 10;
int *p = &a;
free(p);
}
4、使用free释放动态内存只释放掉一部分,造成内存泄漏。
如:
void test()
{
int* p = (int*)malloc(100);
p++;
free(p);//因为p不再指向内存的起始位置,所以释放p时,有一部分空间未被释放
}
5、对同一块动态内存多次释放。
如:
void test()
{
int* p = (int*)malloc(100);
free(p);
free(p);
}
6、动态开辟内存忘记释放。(会造成内存泄漏)
如:动态内存的释放有两种方式①手动释放②程序结束
void test()
{
int* p = (int*)malloc(100);
if (NULL! = p)
{
*p = 20;
}
}
int main()
{
test();
while (1);
return 0;
}
经典例题分析
1、
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
分析:将NULL传到函数GetMemory建立的局部变量p中,然后,给p动态分配空间,然后GetMemory函数结束,局部变量p销毁,str中仍是NULL,且给p动态分配的空间还为销毁,所以将“hello world”拷贝到NULL中是错误的。
总结:此程序有两个错误,其一动态分配的空间未释放,造成了内存泄漏。其二将字符串拷贝到空指针指向的空间,造成错误。
代码修改:
<1>
void GetMemory(char *p)
{
p = (char *)malloc(100);
return p;
}
void Test(void)
{
char *str = NULL;
str=GetMemory(str);
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
<2>
void GetMemory(char** p)
{
*p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
2、
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
分析:当程序执行到进入GetMemory函数时,在函数内部创建了数组,然后将数组首元素的地址返回放到了str中,但是,当GetMemory函数执行完毕后,函数内部创建的数组销毁了,因此返回的地址没有任何的意义。
3、
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
分析:此程序仅有一个问题,也是最为致命的问题,就是开辟了动态内存,用完之后没有释放。
4、
void Test(void)
{
char *str = (char *)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
分析:此代码使用完动态内存之后释放了,但是并未将str置成空指针,因此,还会继续往下执行程序,下面的程序就是将一个字符串拷贝到NULL指向的空间,因此造成了错误。
柔性数组
1、柔性数组:C99中,结构中的最后一个元素允许是未知大小的数组,这个数组就叫做柔性数组。
2、柔性数组的表示:
<1>
typedef struct st_type
{
int i;
int a[0];
}type_a;
<2>
typedef struct st_type
{
int i;
int a[];
}type_a;
有的编译器支持第一种,有的编译器支持第二种,有的两者都可以。
3、柔性数组的特点:
<1>结构中的柔性数组成员前面必须至少有一个其他成员。
<2>sizeof求这种结构体的大小时,返回的大小不包括柔性数组的大小。
<3>包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
对<2>进行解释的代码:
typedef struct st_type
{
int i;
int a[0];
}type_a;
int main()
{
printf("%d\n",sizeof(type_a));//结果为4
return 0;
}
4、柔性数组使用的代码:
#include<stdio.h>
typedef struct st_type
{
int i;
int a[0];
}type_a;
int main()
{
type_a* p = (type_a*)malloc(sizeof(type_a)+10*sizeof(int));
p->i = 1;
for (int j = 0; j < 10; j++)
scanf("%d",&p->a[j]);
for (int k = 0; k < 10; k++)
printf("%d ",p->a[k]);
return 0;
}
5、柔性数组的优势
柔性数组的使用中介绍的代码也可以设计为如下形式
#include<stdio.h>
typedef struct st_type
{
int i;
int* a;
}type_a;
int main()
{
//将结构体建立在栈区
/*type_a p;
p.i = 1;
p.a = (int*)malloc(10 * sizeof(int));
for (int j = 0; j < 10; j++)
scanf("%d", &p.a[j]);
for (int k = 0; k < 10; k++)
printf("%d ", p.a[k]);*/
//将结构体建立在堆区
type_a* p = (type_a*)malloc(sizeof(type_a));
p->i = 1;
p->a = (int*)malloc(10 * sizeof(int));
for (int j = 0; j < 10; j++)
scanf("%d",&p->a[j]);
for (int k = 0; k < 10; k++)
printf("%d ",p->a[k]);
return 0;
}
但是两个代码相比较,柔性数组的使用有明显的两个好处。
<1>方便内存释放。
如果我们的代码是在一个给别人的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
<2>这样有利于访问速度。
连续的内存有益于提高访问速度,也有益于减少内存碎片。