一、为什么存在动态内存管理
- 堆区能够申请大块内存。
- 数组开辟空间实在程序编译时进行的,且申请空间是固定的,而有些情况下,只有在程序运行时才能知道所需内存的大小。
- malloc、calloc等都是函数,所以对空间的申请是在程序运行阶段进行的。
二、动态内存函数
malloc和free
malloc是C提供的动态内存开辟的函数
函数的参数是一个无符号整型,表示要开辟的空间的字节数;函数的返回值是void*类型,指向开辟的空间的首地址,malloc只进行空间开辟,而不进行类型检查,所以使用时要进行强制类型转换。形式如下:
- 指针自身 = (指针类型*)malloc(sizeof(指针类型)*数据数量)
int *p=NULL;
unsigned int size=10;
p=(int *)malloc(sizeof(int)*10);
开辟失败时会返回NULL,所以使用malloc函数必须判断是否申请成功。
free函数用来释放动态开辟的内存
这里的第四点,free并没有改变指针原来的指向,空间被free以后,指针仍然指向原来的地方,只是取消了指针内部的地址和堆空间的关系。
malloc和free有几点需要注意:
- 申请空间时必须整体申请,释放时也必须整体释放。
- 如果申请内存没有释放,会造成内存泄漏。
- 申请空间时,实际申请到的空间会大一些,大出来的部分保存本次申请的“元信息”(属性信息,包含本次申请的大小等),即内存cookie信息(所以每次释放空间能准确释放)。
calloc
calloc与malloc功能相似,区别是会在申请空间时将空间内容按字节初始化为0。
函数的两个参数都是无符号整型,第一个参数表示要申请的元素个数,第二个参数表示每个元素的大小,所以最终申请的空间大小是num*size个字节。
calloc的使用:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<Windows.h>
#include<stdlib.h>
int main()
{
int* p = calloc(5, sizeof(int));
if (NULL != p)//判断是否申请成功
{
for (int i = 0; i < 5; i++)
{
printf("%d ", p[i]);
}
}
free(p);//释放空间
system("pause");
return 0;
}
realloc
realloc函数用于调整已经申请的内存空间的大小
第一个参数ptr是已申请的空间的指针,第二个参数size是调整之后的内存空间的大小。
有几点需要注意
- 由于堆空间必须是连续的空间,所以如果与原有空间连续的后面没有足够大的空间,那么realloc后的返回值不再是之前的地址。
- 使用realloc函数要用一个临时指针保存旧空间的地址,如果之间将realloc结果赋值给旧的指针,当realloc申请失败会返回NULL,导致旧的空间找不到,造成内存泄漏。
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<Windows.h>
#include<stdlib.h>
int main()
{
int* ptr = malloc(sizeof(int)*5);
if(NULL==ptr)
{
exit(EXIT_FAILURE);
}
//代码1
//ptr = realloc(ptr, 10);//将原来申请的空间扩展为10个字节
//代码2
int* p = NULL;//定义临时变量
p = realloc(ptr, 10);//用临时变量保存realloc的返回值
if (p != NULL)
{
ptr = p;
}
free(ptr);//释放空间
system("pause");
return 0;
}
代码1是常见的错误写法,将realloc的值直接赋给原来空间的指针。
动态内存试题
题目1
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
这个程序本意是将str传给函数GetMemory,为其动态开辟内存,但是此处忽略了一点,str本身是一个指针变量,要在函数内操作这个指针变量的内容要给函数传入其地址,即二级指针。与一般变量相同,要在函数内改变变量的值,需要传值传参,否则改变的只是临时拷贝的变量。
正确写法:
void GetMemory(char** p)
//用二级指针接收参数
{
*p = (char*)malloc(100);//
}
void Test(void)
{
char* str = NULL;
GetMemory(&str);//传str的地址,才能在GetMemory函数内改变其值
strcpy(str, "hello world");
puts(str);
}
除了二级指针方案外,还可以将函数返回值设为char*类型,开辟空间后将空间地址作为返回值。
总结一句话:函数传参时,要修改谁的内容,就传谁的地址。
题目2
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
p是局部变量,在GetMemory函数被调用结束后,其形成的栈帧会被释放掉,而对于返回值,函数数调用方在栈上额外开辟了一片空间,并将这块空间的一部分作为传递返回值的临时对象,这个程序中,指针p被临时保存在额外的空间,但是其指向的数组内容已将不存在了。
计算机在释放空间时,不会将空间的内容清空,只是这些空间的内容已经无效,可以被重新写入,所以最终str拿到的返回值依然指向原来的位置,但是该位置已经被调用printf函数形成的栈帧所覆盖,导致最终打印结果是乱码。
题目三
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
本题与题目一相似,这里为GetMemory传入的是str的地址,所以可以改变str指针,让其指向开辟的堆内存。这里存在的问题是,没有判断是否申请成功,且程序执行完没有释放空间。
题目四
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
因为free函数的作用是解除指针与堆空间的关系,并不会将指针清空,所以此时指针str依然不为空,这里会将"world"拷贝到str(准确的说法是将"world"首元素的地址拷贝给变量str,且会拷贝’\0’)所以打印结果是"world"。
三、柔性数组
**定义:**结构体中的最后一个元素允许是未知大小的数组,这就叫作柔性数组成员。如:
typedef struct st
{
int i;
int a[0];
}st;
或另一种写法:
typedef struct st
{
int i;
int a[];
}st;
特点:
- 结构中的柔性数组前面必须至少有一个成员(否则柔性数组便没有了意义,不如直接使用malloc开辟空间)。
- 柔性数组成员不影响结构体的大小,但是会影响内存对其。
- 柔性数组的数组名是起内存标识作用的一种占位符。
使用:
#include<stdio.h>
#include<Windows.h>
#include<stdlib.h>
#include<assert.h>
typedef struct st
{
int i;
int a[0];//柔性数组成员
}st;
int main()
{
st* p = (st*)malloc(sizeof(st) + 10 * sizeof(int));//动态内存开辟
if (NULL == p)
{
assert(1);//判断是否申请成功
}
p->i = 10;
//使用开辟的空间
for (int j = 0; j < 10; j++)
{
p->a[j] = j;
}
for (int j = 0; j < 10; j++)
{
printf("%d\n", p->a[j]);
}
free(p);//释放堆空间
system("pause");
return 0;
}
这个程序使用了柔性数组,在堆区开辟了空间并赋值使用,通过柔性数组下标可以访问到其后面开辟的所有空间。
有些时候,我们为了在结构体中有一个变长数组,也可以在结构体中定义一个指针,让这个指针指向动态开辟的内存:
#include<stdio.h>
#include<Windows.h>
#include<stdlib.h>
#include<assert.h>
typedef struct st
{
int i;
int* p;
}st;
int main()
{
int *p = (int*)malloc(sizeof(int) * 10);
if (NULL == p)
{
assert(1);//判断是否申请成功
}
st mem = { 10,p };//初始化了一个结构体变量mem
for (int j = 0; j < 10; j++)
{
p[j] = j;
}
for (int j = 0; j < 10; j++)
{
printf("%d\n", p[j]);
}
free(p);//释放堆空间
system("pause");
return 0;
}
相比之下,使用柔性数组更加方便,而且由于结构体是封装的类型,其内部元素容易被忽略,可能使用后会忘记释放内存,导致内存泄漏。另一方面,柔性数组使用的是连续的堆空间,有利于提高访问速度。