动态内存管理
动态内存分配
C程序内存区域划分
C程序将数据存储在不同的内存区域, 内存被划分为栈区,堆区,数据段(静态区),代码段
-
栈区: 存储函数的局部变量,局部变量在进入函数时开辟栈空间,函数结束时自动释放.
栈内存分配在指令集中有定义,效率很高,但是分配的内存容量有限 -
堆区: 动态内存是在堆区上分配的,程序员操作,不手动回收会一直等到程序结束系统才回收
-
数据段(静态区): 全局变量,加上static关键字的静态变量,直到程序结束才销毁
-
代码段: 存放函数体的二进制代码,常量字符串也存储在此处
#include <stdio.h>
#include <stdlib.h>
int e = 50;
int f = 60;
int main()
{
printf("局部变量\n");
int a = 10;
int b = 20;
printf("%p\n",&a);
printf("%p\n",&b);
printf("静态变量\n");
static int c = 30;
static int d = 40;
printf("%p\n", &c);
printf("%p\n", &d);
printf("全局变量\n");
printf("%p\n", &e);
printf("%p\n", &f);
printf("动态内存开辟\n");
int *p1 = (int*)malloc(sizeof(int));
int *p2 = (int*)malloc(sizeof(int));
printf("%p\n", &p1);
printf("%p\n", &p2);
printf("常量字符串\n");
printf("%p\n","hello world");
printf("%p\n","welcome");
return 0;
}
输出结果:
局部变量
0x16d8ae9c8
0x16d8ae9c4
静态变量
0x102558008
0x10255800c
全局变量
0x102558000
0x102558004
动态内存开辟
0x16d8ae9b8
0x16d8ae9b0
常量字符串
0x102553f93
0x102553f9f
从此段程序的输出结果可以看出, 位于同一个内存区域的变量地址相近, 而位于不同内存区域的变量地址相差较远
而且我们仔细观察之后还能发现, 栈和堆的内存地址较接近,数据段和代码段的内存地址较接近,与上图的位置关系大致相符
动态内存分配的作用
栈区开辟空间只有普通变量和定义数组两种方式, 其长度都是固定的, 但是有许多的场景是要求我们根据外界输入来决定数组/开辟空间的大小,编写程序时是不知道的
所以我们在此场景下便要使用堆区的内存,在堆区上动态开辟内存空间
注意: 与栈空间初始为随机值不同,堆区上的内存的初始值是0
动态内存函数
动态内存开辟函数malloc
void* malloc (size_t size);//单位为字节 结合sizeof(类型)使用
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
如果开辟成功,则返回一个指向开辟好空间的指针。 (void* 类型)
如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。 (如果不进行检查,解引用时会有严重错误)
返回值的类型是 void* ,需要强制转化指针类型
size为0时,程序错误
动态内存释放/回收函数free
void free (void* ptr);
free函数用来释放动态开辟的内存。
如果参数 ptr 指向的空间不是动态开辟的,程序错误。
如果参数 ptr 是NULL指针,则函数什么事都不做。
malloc和free都声明在 stdlib.h 头文件中。
指定类型开辟内存calloc
void* calloc (size_t num, size_t size);//要开辟几个空间 每个空间大小是多少个字节
函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。(适用于对申请的内存空间初始化的场景)
调整动态内存realloc
void* realloc (void* ptr, size_t size);//单位:字节
可以对已分配的空间重新调整,通过size参数指定调整后的字节数为多少,返回调整后的空间地址
内部实现时会有两种情况:
- 如果原本的空间后面的内存足够用于调整, 是直接在现有内存后面扩展
- 如果原本的空间后面不足以用于调整, 则在堆空间上另找一块区域作为调整后空间, 返回新的内存地址
接受之后需要判断 是否申请失败, 如果是申请失败应该马上报错 否则如果将p指针直接指向ptr可能会造成内存泄露,一直无法释放p原来指向的内存空间
动态内存分配错误
- 开辟失败,返回NULL,但还是解引用了(不常见)
- 动态开辟空间越界访问(注意)
- 对非动态开辟内存使用free函数(不常见)
- 使用free函数只释放一部分内存(比如p+2)(不常见)
- 忘记释放内存--》内存泄露(特别注意!!!)
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}
函数出来之后p变量被销毁 但对应的内存空间未被释放 导致之后再也没有指针指向这段空间 无法进行回收 导致内存泄露
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
这段程序会出错, str还是NULL, p是形参 是str的临时拷贝 只是p指向了这段空间 并不是str指向这段空间, 函数结束后p会销毁, 会造成内存泄露
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
字符数组在函数的栈空间上创建, 随着函数的结束销毁, 所以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);
}
传递过去的是指针的地址,所以可以正常的将p返回回来,还是指向函数内开辟的堆内存,正常打印hello,但是忘记对开辟的堆空间进行释放
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
过早对开辟的内存回收, str变成了野指针
柔性数组
C99 中,结构中的最后一个元素允许是未知大小的数组, 这就是柔性数组成员
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
前面最少有一个其他成员,sizeof返回结构的大小不包括柔性数组,如果用malloc进行内存分配,分配的大小要大于结构的大小,适应柔性数组的预期大小
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);
变相实现了变长数组,此处长度为100
也可以用malloc进行100个int元素的内存分配
但是用柔性数组的方法只用malloc一次,而第二种方法需要malloc两次(第一次结构,第二次数组,free需要多一次,风险高)
另外,malloc两次内存是不连续的,连续的内存访问速度更高,减少内存碎片(内存利用率更高)