动态内存管理/开辟/分配
一、为什么会存在动态内存分配
创建变量的本质是向内存申请空间,我们已经掌握的申请内存空间的方式有两种:
int main()
{
//1.在栈空间上申请4个字节的内存空间
int a = 10;
//2.在栈空间上申请一块40个字节的连续空间
int arr[10] = { 0 };
return 0;
}
上述开辟空间的方式有个问题,当申请的内存空间不够用了或是用了一部分还剩了一部分空间,空间会自动变大或变小吗?很明显是不会的,说明上述两种情况开辟空间的大小是固定的,而且,数组在声明的时候,必须指定数组的长度,数组的空间一旦确定了,那么它的大小是不能调整的,同理,变长数组也是这样的。
C语言提供了动态内存管理,那么为什么会存在动态内存分配呢?
- 变量和数组的方式不够灵活。
- 使用动态内存分配可以自己来维护内存的使用生命周期。
二、malloc和free
1.malloc
malloc()
是C语言提供的一个动态内存开辟的函数。
函数原型如下:
void* malloc(size_t size);
意思是malloc()
会开辟一块大小为size
个字节的连续可用的内存空间,空间开辟成功的话,会返回一个指向这块空间的起始位置的指针;开辟失败,会返回一个空指针,所以函数的返回值一定要检查好。若参数size
为0,malloc()
的行为是标准未定义的,取决于编译器。
2.free
C语言提供了一个函数free()
,是专门用来做动态内存的释放和回收的,函数原型如下:
void free(void* ptr);
free()
函数的参数必须是指向动态内存开辟的空间。
free()
函数的使用是主动释放内存空间后,这块空间就还给了操作系统,操作系统可以把这块空间用到别处。这个过程就像你去图书馆借书,看完了还书,那么这本书还可以借给别人看一样。
程序退出的时候,即使没用free()
,操作系统也会主动收回这块内存空间。
例子: 申请40个字节的空间,用来存放10个整数。
使用动态内存分配都需要这三个步骤:申请内存、使用内存、释放内存。
“使用内存”部分,这块连续空间就像数组一样,可以像访问数组一样,使用指针访问这块空间。
#include <stdio.h>
#include <stdlib.h>
int main()
{
//申请内存
int num = 10;
int* p = (int*)malloc(num * sizeof(int));//(int*)是强制类型转换成int*类型
if (p == NULL)
{
perror("malloc");//失败的话,会打印:malloc:xxxxxx
return 1;//失败返回
}
//使用内存
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i + 1;//p始终指向开辟内存的起始地址,这样p指向的就是一整块内存,最后释放的就是这一整块内存
}
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
//释放内存
free(p);//释放空间这一刻p不知道指向了哪里,此时p就是野指针
p = NULL;//所以要给p赋值为NULL指针
return 0;//正常返回
}
开辟空间失败的例子,p已经是空指针了:
所以需要对返回值做检查。
观察释放动态内存前后动态内存空间的内容的变化:
三、calloc和realloc
1.calloc
函数原型如下:
void* calloc (size_t num, size_t size);
例子:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
perror("calloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
free(p);
p = NULL;
return 0;
}
若使用malloc()
:
对之前的图进行一个补充:
2.realloc
函数原型如下:
void* realloc (void* ptr, size_t size);
realloc()函数有两个功能:
- 调整内存空间的大小。
- 申请内存空间。当ptr为NULL,就和malloc()函数的功能一样了。
realloc()
函数在调整内存空间的大小时有两种情况:情况一: 原有空间之后有足够大的空间。直接在原有内存之后直接追加空间,原来空间的数据不发生变化。
情况二: 原有空间之后没有足够大的空间。扩展的方法是:在堆空间上另找⼀个合适大小的连续空间来使用,将旧空间的数据拷贝到新空间,旧的空间会自动还给操作系统,这样函数返回的是⼀个新的内存地址。
realloc()
函数在调整内存空间的大小时会调整失败的,那么函数返回的是NULL
指针。
#include <stdio.h>
#include <stdlib.h>
int main()
{
//申请内存
int* p = (int*)malloc(5 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
//使用内存
int i = 0;
for (i = 0; i < 5; ++i)
{
*(p + i) = i + 1;
printf("%d ", *(p + i));
}
//内存不够用了:想打印出6到10,所以要调整内存空间
int* ptr = (int*)realloc(p, 10 * sizeof(int));
//realloc()可能调整失败 - 返回NULL指针。直接赋值给p会与前面的p !=NULL矛盾
if (ptr == NULL)
{
perror("realloc");
return 1;
}
else
p = ptr;
//使用调整后的内存空间,这块空间会把旧空间的数据拷贝过来
for (i = 5; i < 10; i++)
{
*(p + i) = i + 1;
printf("%d ", *(p + i));
}
//释放内存
free(p);
p = NULL;
return 0;
}
使用realloc()
函数前:
使用realloc()
函数后:
realloc()
函数申请内存空间:
#include <stdlib.h>
int main()
{
int* p = (int*)realloc(NULL, 10 * sizeof(int));
//相当于int* p = (int*)malloc(10 * sizeof(int));
return 0;
}
四、常见的动态内存的错误
1.对NULL指针的解引用操作
void test()
{
int *p = (int *)malloc(INT_MAX/4);//malloc()开辟失败会返回NULL指针
*p = 20;//如果p的值是NULL,就会有问题
free(p);
p = NULL;
}
2. 对动态开辟空间的越界访问
void test()
{
int i = 0;
int* p = (int*)malloc(10 * sizeof(int));
if (NULL == p)
{
exit(EXIT_FAILURE);
}
for (i = 0; i <= 10; i++)
{
*(p + i) = i;//当i是10的时候越界访问
}
free(p);
p = NULL;
}
3.对非动态开辟内存使用free释放
void test()
{
{
int a = 10;
int* p = &a;
free(p);//err
p = NULL;
}
}
4.使用free释放⼀块动态开辟内存的⼀部分
void test()
{
int* p = (int*)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
p = NULL;
}
5.对同⼀块动态内存多次释放
void test()
{
int* p = (int*)malloc(100);
free(p);
free(p);//重复释放
}
改进:
void test()
{
int* p = (int*)malloc(100);
free(p);
p = NULL;
free(p);//free()的参数是NULL,则free()函数什么都不会做
}
6.动态开辟内存忘记释放(内存泄漏)
void test()
{
int* p = (int*)malloc(100);
if (NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while (1);
}
忘记释放不再使用的动态开辟的空间会造成内存泄漏。
切记:动态开辟的空间⼀定要释放,并且正确释放。
五、动态内存经典笔试题分析
题目一
请问运行Test函数会有什么样的结果?
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
strcpy(str, "hello world");
是对NULL
指针的解引用操作符,这时就是非法访问内存了,程序会崩溃的。
代码改进:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void GetMemory(char** p)
{
*p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf(str);//相当于printf("%s\n", str);或printf("hello world");
//str是存放常量字符串的首元素的地址的,常量字符串与数组一样都是连续的空间,知道了首元素的地址,就会知道整个数组的内容
//释放内存空间
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
这样看函数参数有点多余,进一步优化:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* GetMemory()
{
char *p = (char*)malloc(100);
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
strcpy(str, "hello world");
printf(str);
}
int main()
{
Test();
return 0;
}
最终是指针str
指向这块开辟空间:
题目二
请问运行Test函数会有什么样的结果?
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
结果为:
进行了画图分析后,理应打印出hello world
,但是结果并没有,这里其实是返回栈空间地址的问题。
在《深入理解指针(2)》中讲解了野指针成因,当中有一条就是指针指向的空间释放了,上述代码就是这个意思,数组所在的空间出了函数,这一块的空间就被回收了,即str
是野指针。
题目三
请问运行Test函数会有什么样的结果?
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
会造成内存泄漏,原因是没有对动态内存进行释放。
代码改进:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
//释放内存
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
题目四
请问运行Test函数会有什么样的结果?
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
代码改进:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void Test(void)
{
char* str = (char*)malloc(100);
if (str == NULL)
{
perror("malloc");
return ;
}
strcpy(str, "hello");
free(str);//100B的空间释放了,这时str就是野指针
str = NULL;
if (str != NULL)//假
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
六、柔性数组(flexible array)
C99中,结构中的最后⼀个元素允许是未知大小的数组,这就叫做柔性数组成员。
例如:
struct S1
{
int i;
int a[0];//柔性数组成员
};
struct S2
{
int n;
int a[];//柔性数组成员
};
1.柔性数组的特点
- 结构中的柔性数组成员前面必须至少有⼀个其他成员。
- sizeof()返回的这种结构大小不包括柔性数组的内存。
- 包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
例如:
#include <stdio.h>
struct S
{
int i;
int a[0];//柔性数组成员
};
int main()
{
printf("%zd\n", sizeof(struct S));
return 0;
}
可以发现:sizeof()返回的结构大小中不包括柔性数组的内存。
2.柔性数组的使用
代码1:
#include <stdlib.h>
#include <stdio.h>
struct S
{
int n;
int a[];
};
int main()
{
struct S* ptr = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(int));
if (ptr == NULL)
{
perror("malloc");
return 1;
}
ptr->n = 100;
int i = 0;
for (i = 0; i < 10; i++)
{
ptr->a[i] = i + 1;
printf("%d ", ptr->a[i]);
}
//若觉得数组a的空间不够用了,还可以动态调整
struct S* ps = realloc(ptr, sizeof(struct S) + 20 * sizeof(int));
if (ps == NULL)
{
perror("realloc");
return 1;
}
ptr = ps;
for (i = 10; i < 20; i++)
{
ptr->a[i] = i + 1;
printf("%d ", ptr->a[i]);
}
free(ptr);
ptr = NULL;
return 0;
}
3.柔性数组的优势
代码2:
#include <stdio.h>
#include <stdlib.h>
struct S
{
int n;
int* a;
};
int main()
{
struct S* ptr = (struct S*)malloc(sizeof(struct S));
if (ptr == NULL)
{
perror("malloc");
return 1;
}
ptr->n = 100;
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
ptr->a = p;
int i = 0;
for (i = 0; i < 10; i++)
{
ptr->a[i] = i + 1;
printf("%d ", ptr->a[i]);
}
//空间不够了,扩容
int* p2 = (int*)realloc(ptr->a, 20 * sizeof(int));
if (p2 == NULL)
{
perror("realloc");
return 1;
}
ptr->a = p2;
for (i = 10; i < 20; i++)
{
ptr->a[i] = i + 1;
printf("%d ", ptr->a[i]);
}
free(ptr->a);
ptr->a = NULL;
free(ptr);//若先free掉ptr,那么会找不到ptr->a的
ptr = NULL;
return 0;
}
上述代码1和代码2可以完成同样的功能,但是代码1的实现有两个好处:
- 方便内存释放
- 有利于访问速度