C语言动态内存管理
C语言中提供了很多数据类型,有基本数据类型、指针类型、自定义类型和空类型。我们可以根据需要选择合适的数据类型使用,当需要较大空间时我们可以选择数组、结构体等类型。但这些并不能够满足编程需求,例如在不知道具体所需内存大小的情况下直接定义变量分配内存就有可能造成内存浪费。在C语言中,动态内存管理就可以很好的解决这一问题。使用动态内存管理函数,程序员可以根据需求随时向系统分配申请空间,当这块空间用完时在及时释放归还给系统。
1.为什么存在动态内存分配
当不知道需要多大内存或者需要动态申请内存空间时,可以使用动态内存分配解决一次性分配太多内存造成内存浪费的问题。
2.动态内存函数的介绍
动态内存函数使用时,都需要包含头文件。动态内存函数的头文件为:#include<stdlib.h>或者#include<malloc.h>
malloc()和free()
void* malloc (size_t size);
Allocates a block of size bytes of memory, returning a pointer to the beginning of the block.If size is zero, the return value depends on the particular library implementation (it may or may not be a null pointer), but the returned pointer shall not be dereferenced.
//向内存申请size个字节的空间,并且返回这块空间的首地址。如果所给参数为0,则返回值取决于编译器。
void free (void* ptr);
A block of memory previously allocated by a call to malloc, calloc or realloc is deallocated, making it available again for further allocations.
//释放由malloc、calloc、realloc函数申请的空间
注意:如果申请空间失败,会返回NULL,所以使用malloc函数时必须要检查是否申请成功。而使用malloc函数时,具体要申请的空间的类型是由使用者自己决定的。
注意:由于malloc函数返回值为void*类型,在使用时需要根据自己所申请的类型进行强制类型转换
举个例子说明一下malloc函数和free函数的使用:
从图中,我们可以看到,给ptr申请了五个int型的空间并且都赋初值为0,从监视窗口就可以看到我们申请的五个空间已经全部被赋值为0,而第6个是随机值。使用完后,将空间进行释放。那么,释放完成后有必要将指针置空吗?
如果不置空:
没有将指针置空,我们可以看到五个int型空间的值已经是随机值,不在是我们赋的0.但是,仔细观察我们可以看到,虽然ptr已经被释放,但是ptr依然指向之前分配的那块空间的地址。这样就会存在这样一种可能:系统将这块空间分配给其他程序,但ptr还指向这块空间,如果错误使用ptr就会造成这块空间被污染,导致一些难以预料的后果。如果置空,ptr不在指向这块空间,就不会造成这个问题。
calloc函数
void* calloc (size_t num, size_t size);
calloc函数的功能
函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
举个例子:
int main()
{
int *p = calloc(10, sizeof(int));
if(NULL != p)
{
//使用空间
}
free(p);
p=NULL;
return 0;
}
ralloc函数
void* realloc (void* ptr, size_t size);
//ptr为要调整的空间的地址,size为要调整的大小
realloc函数的出现让动态内存分配变得更加灵活,realloc的函数的作用是对动态分配好的内存空间的大小进行调整。当我们一次性开辟的内存不够时,可以利用这个函数进行二次分配,最主要的是realloc函数还会将原来的内存空间中的值进行复制。realloc函数的使用有以下几种情况:
注意:realloc函数必须是对malloc函数或者calloc函数申请的空间进行修改
realloc函数使用演示:
#include<stdio.h>
#include<stdlib.h>
int main()
{
char* ptr = (char*)calloc(5,sizeof(char));
if (ptr != NULL)
{
realloc(ptr,8);
}
free(ptr);
ptr = NULL;
return 0;
}
3.常见的动态内存错吴
对空指针的解引用操作
对空指针进行解引用操作可能会出现下面问题:
可能会出现空指针解引用的示例:
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
上面代码没有对使用malloc函数申请的空间进行判断,如果申请空间失败,就会返回空指针,这时在进行解引用就会出现问题。
对动态开辟空间的越界访问
动态开辟空间的越界访问是非法访问指在开辟的空间的范围外的空间。如下代码:
//动态开辟空间的越界访问
int main()
{
char* ptr = (char*)malloc(10);
if (ptr == NULL)
{
printf("开辟空间失败\n");
exit(-1);
}
ptr[11] = 20;
free(ptr);
return 0;
}
虽然说动态开辟空间的越界访问不一定会报错,但实际写代码时是坚决不允许对空间越界访问的。
对非动态开辟的内存使用free释放
使用free释放对动态开辟的内存的一部分
int main()
{
char* ptr = (char*)malloc(10);
ptr++;//此时ptr不在是动态开辟内存的起始地址
free(ptr);
return 0;
}
对同一块内存释放多次
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
对动态开辟的内存忘记释放
动态开辟的内存忘记释放会造成内存泄露,因此动态开辟的内存一定要使用free进行释放,并且正确释放(释放多次、释放一部分等都是不正确的释放)
4.柔性数组
C99 中,结构中的最 后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。例如:
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
柔性数组的特点:
结构中的柔性数组成员前面必须至少一个其他成员。
sizeof 返回的这种结构大小不包括柔性数组的内存。
包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应 柔性数组的预期大小。
5.动态内存开辟的经典笔试题
笔试题1
我们发现,这个程序并没有输出“hello word”这是为什么呢?因为在使用GetMemory函数时,参数传的是一个指针变量,而不是指针变量的地址,因此并不会修改ptr中的内容。正确代码如下:
‘
笔试题2
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
执行上述代码,会输出一些随机值。这是因为p是局部变量,在GetMemory函数执行完后p就会被释放,再次访问就会输出一些随机值。
笔试题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);
}
这个程序的云顶结果是:hello。没有任何问题,这个程序是对上面两个程序的综合考察,这里既不存在局部变量问题也不存在传地址问题。
笔试题4
int main()
{
char* p = (char*)malloc(100);
strcpy(p,"hello");
free(p);
if (p != NULL)
{
printf("%s\n",p);
}
return 0;
}
实际上这个代码是有问题的,首先这个代码对已经释放的空间再次访问。因为,前面我们讲过,释放后如果指针不置空指针还将指向这块空间,再次访问会继续返回这块空间中的值,也就是说这个程序会输出一个随机值。
动态内存管理的一个使用误区:检查下面程序存在的问题
char * test()
{
char* str = (char*)calloc(10, sizeof(char));
free(str);
return str;
}
int main()
{
char* p = test();
return 0;
}
结果分析:
观察上图可以发现,程序执行到最后一步时指针p指向了test函数中的局部变量str的地址,而且str已经不存在。str不存在是因为,str是局部变量,当test函数执行完时,str就会归还给内存。之所以指针p能指向str,是因为在str被归还给内存之前我们将str的地址返回给了指针p(这里所说的归还不同于动态开辟的内存释放后的归还)。但是,我们发现calloc开辟的空间应该都被初始化为0了,但是p的内容全都为随机值,这是为什么呢?
前边,我们讲过动态开辟的内存都要进行释放,否则会造成一系列问题。这里,在将str返回前已经将str释放,看似没问题,实则返回的是一块已经被释放的地址。那么,局部变量动态开辟的内存需要返回的情况下该如何进行释放呢?
观察下面代码:
我们发现,这里test 函数调用完后str就被归还给内存,而p在释放之前得到的内存中的值也全为0,释放之后会变成随机值。刚好满足我们的要求,既对动态开辟的内存进行了释放也满足了我们的使用需求。
结论:局部变量使用动态内存管理申请的空间,如果在其他函数中需要使用大该空间,可以不直接释放该局部变量,等到使用完后在利用指向该空间的地址的指针进行释放。例如:这列的str可以不直接使用str进行释放,使用完后利用指针p进行释放。