1 为什么存在动态内存分配
我们已经掌握的内存开辟方式有:
int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
但是上面的两种开辟空间的方式都有两个特点:
- 空间开辟的大小是固定的。
- 数组在声明的时候必须指定数组的长度,他所需的内存在编译的时候分配。
对于空间的需求,不仅仅上面的两种情况,如有时候我们需要的空间是在程序运行的时候才知道,不能满足我们的需求,所以这个时候只能试试动态内存的开辟了。
2 动态内存函数的介绍
2.1 malloc和free
void *malloc( size_t size );
这个函数向内存申请一块连续的可用的空间,并返回指向这个空间的指针。
- 如果开辟成功,返回指向开辟好空间的指针
- 如果开辟失败,返回一个NULL的指针,因此使用malloc函数的时候要判断指针是否为空
- 返回值的类型是void * ,所以malloc函数并不知道开辟空间的类型,具体的类型是由使用者自己来决定的。
- 如果参数size为0,malloc的行为是标准未定义的,取决于编译器。
光说不练假把式,下面我们通过代码来探索:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
int main()
{
//申请一块空间,用来存放10个整型
int* p = (int*)malloc(10 * sizeof(int));
//使用malloc开辟的空间需要进判断
if (p == NULL)
{
perror("malloc");
return 1;
}
//使用:用开辟的空间,来存放0-9的数字。
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
printf("%d ", p[i]);
}
printf("\n");
//释放空间
free(p);
p=NULL;
return 0;
}
现在这里开辟了空间,那是怎么样释放的呢?
malloc开辟的空间主要由两种释放方式:
1.free 释放------主动
2.程序退出后 ,malloc申请的空间会被操作系统回收------被动
正常情况下,谁申请的空间谁去释放,万一自己不释放,也要交代给别人,记得释放。
这里就该free函数登场了
void free( void *memblock );
用来释放动态开辟的内存。
- 如果参数memblock指向的空间不是动态开辟的,那free函数的行为是未定义的。
- 如果参数memblock是NULL指针,那么free函数什么事都不做。
使用方式如同上面代码。
这里可能就有人要问了,都使用free函数来释放动态开辟的空间了,为什么还要把它赋值为空。
是这样的free函数把开辟的动态空间释放了,归还于内存了,但是指针P还是指向开辟空间的起始地址,如果不小心使用了指针P,就会造成非法访问,形成野指针。
2.2 calloc
也是用来开辟内存空间的。
void* calloc (size_t num, size_t size);
- 函数的功能是num个大小为size的元素 开辟一块空间,并且把空间的每个字节初始化为0;
- 与函数malloc的区别只在于calloc函数会返回地址之前把之前申请的每个空间的每个字符初始化为全0;
可以看到变量0x00C0AD38的地址里面全部都初始化为0;
calloc函数和malloc函数有什么区别呢?这里只能说差不多。
calloc(10,sizeof(int ));
malloc(10*sizeof(int ));
上面都是开辟10个int类型的空间,只是开辟的参数由一点区别,前者是分开的,后者是合并的。除了是参数的区别,calloc函数申请好空间后或者使用完后,会将空间初始化为0,但是malloc函数不会初始化。
int main()
{
//申请一块空间,用来存放10个整型
int* p = (int*)calloc(10 , sizeof(int));
//使用malloc开辟的空间需要进判断
if (p == NULL)
{
perror("calloc");
return 1;
}
//使用:用开辟的空间,来存放0-9的数字。
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
printf("%d ", p[i]);
}
printf("\n");
return 0;
}
2.3 realloc
realloc 函数的出现,让动态内存的管理变得更加的灵活,因为有些时候我们会发现过去申请的空间太小了,有些时候又太大了,为了合理的管理 内存,我们就可以通过realloc函数来对开辟的内存大小进行灵活的调整,那realloc函数是如何做到的呢?我们先来看看realloc函数的定义 。
void *realloc( void *memblock, size_t size );
这里介绍一下这里的参数:
- memblock 是要调整的内存地址
- size 是调整之后新大小
- 返回值是 调整之后的内存的起始地址
- 这个函数会在原内存调整时候判断,如果后面的内存空间大小不够,就会寻找新的新地址,然后将原来内存中的数据移到新的空间,并返回新空间的起始地址。
realloc 函数在调整内存空间的时候分为两种情况:
1.原空间之后有足够大的空间。
这时如果想要扩展内存,就直接在原内存的后面直接追加空间,原空间的数据不发生变化。
2.原空间之后没有足够大的空间。
原空间后面没有足够大的空间,扩展的方法是:在堆空间上寻找新的合适大小连续空间来使用,释放旧的空间并返回新的内存地址。
来看realloc的具体使用,假设上面空间不够用,改为20个int空间:
//空间不够,希望调整空间为 20个整型的地址
int* ret = realloc(p, 20 * sizeof(int));
//这里会有小伙伴问,为什么不用上面的P,这里要说的是:realloc开辟空间也会失败,
// 失败返回的是NULL,并把原来的
//数据为NULL,导致数据丢失,所以在使用的时候也要进行判断。
if (ret != NULL)
{
p = ret;
}
return 0;
}
这里 会有同学问,realloc里面第一个参数传的是 NULL,怎么班呢?
这里就等价于:
int *p=(int *)realloc(NULL,40);// ==malloc(40)
realloc 函数也有malloc 的功能。
下面我们进行几个经典的笔试题
笔试题
题目一:
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
int mian()
{
test();
return 0;
}
程序的运行如图所示:先创建了指针 str并把他赋值为NULL,接着调用GetMemory函数,创建临时变量P 然后对其开辟空间,函数GetMemory调用结束,但是P空间没有释放,接着使用strcpy函数,但是str还是空指针,没有接到p开辟的地址,接着对NULL进行解引用,程序崩溃。
题目二:
void *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str =GetMemory();
printf(str);
}
我们创建str指针,并赋值为NULL。调用GetMemory函数,在里面创建了字符数组P存储了hello world。这时候p指向的是h的地址,然后返回p的地址,数组P被销毁。str收到0xfff40的地址。这时候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);
}
内存泄漏。这里 能打印,但是没有释放开辟的内存,导致内存泄漏。
题目四:
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
我们来看上面的这个代码:
创建str指针,并开辟100个字节的空间。然后调用strcpy函数把hello复制到str开辟的空间中,然后free释放开辟的空间,但是str任然指向开辟空间的起始地址,不为NULL。进行if判断,想要把world复制到str指向的空间。注意此时str是野指针,对野指针进行操作,非法访问内存。因为free函数 把开辟的空间返还给了操作系统。