前言:
本篇博客中所提到的函数需包含头文件 stdlib.h。
一.动态内存分配
在我们写代码时,经常会向内存申请空间,但是那时我们申请的空间是固定的,有时候空间被申请后发现并没有使用这么多的空间。
举个列子:
int main()
{
int i = 10; //固定的向内存申请4个字节的空间
int arr[10];//固定的向内存申请40个字节的连续空间
return 0;
}
上述开辟空间的方式有两个特点
1. 空间开辟大小是固定的
2. 数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。
于是c语言就向我们提供了可以实现动态内存开辟的函数,可以让我们自己来决定开辟空间上的大小。
二.动态内存分配函数
在介绍动态内存分配函数前,我们先来简单了解一下内存的划分。动态内存分配函数是在内存中的堆区开辟空间,我们可以将内存简单划分为几个区域。
1.malloc和free
1.1malloc函数介绍
void* malloc (size_t size);
malloc 函数会在堆区上向内存申请一块连续可用的空间(size个字节),并返回指向这块空间的指针。
-
如果开辟成功,则返回一个指向开辟好空间的指针。
-
如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查
-
返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
-
如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
-
malloc申请的空间不会进行初始化
1.2free函数的介绍
void free (void* ptr);
在堆区申请的空间使用完之后应该主动释放,free函数便是专门用来释放我们在堆区上申请的空间.
虽然当我们没有主动释放的时候,在程序结束的时候也会被释放,但是如果程序一直运行,那么这块空间将不会被释放,将会造成内存泄漏。
- free函数是专门用来做动态内存的释放和回收的,如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
- free函数释放空间之后并不会主动置空。
- 如果参数 ptr 是NULL指针,则函数什么事都不做。
1.3malloc函数的使用
#include <stdlib.h>
int main()
{
//申请40个字节的空间用来存放整型
//此时返回的是void*的指针,使用时还需要转换
//void* p = malloc(40);
//在申请空间的同时,转换成我们需要的类型
int* p = (int*)malloc(40);
//判断是否申请成功,防止野指针的出现
if (NULL == p)
{
perror("malloc:");
return 1;
}
//存放1—10
int i = 0;
for (i = 0; i < 10; i++)
{
p[i] = i + 1;
}
//打印
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
//释放申请的内存
free(p);
//置空,防止非法访问
p = NULL;
return 0;
}
- 注意点
在代码的最后我们对申请空间进行了置空的操作,为什么要进行置空的操作?
在调试窗口输入p,(逗号)10,就可以以p指针的视角向后观察10个元素
此时我们已经将申请的空间进行了释放,我们发现此时p依然存放着那块空间的起始地址,如果不对p进行置空,下次有人再去使用p的时候就会造成非法访问。
2.calloc
2.1calloc函数的介绍
void* calloc (size_t num, size_t size);
calloc 函数也用来用来进行动态内存分配的,它和malloc函数的区别如下:
- 函数 realloc和malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
相同点:
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个NULL指针,也需要进行判断。
2.2calloc函数的使用
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (NULL == p)
{
perror("calloc:");
return 1;
}
//打印
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
free(p);
p = NULL;
return 0;
}
关于malloc和calloc在效率上的区别:
- malloc的效率比calloc高,因为malloc申请空间的时候并不会对空间进行初始化。
- calloc的效率比malloc低,因为calloc在申请空间的同时进行了初始化。
3.relloc
3.1.relloc函数的介绍
void* realloc (void* ptr, size_t size);
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。
ptr指向由前面malloc,calloc或者realloc开辟的空间,size是调整之后的空间大小。
注意点:
- ptr 是要调整的内存地址
- 如果传的是空指针,realloc和malloc相同,开辟一块空间,返回空间的起始地址。
- size 是调整之后新大小
- 返回值为调整之后的内存起始位置。
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
- realoc函数扩容失败会返回NULL;
3.2realloc函数的工作原理
3.3realloc函数的使用
int main()
{
int* p = (int*)malloc(5 * sizeof(int));
if (NULL == p)
{
perror("malloc:");
return 1;
}
int i = 0;
for (i = 0; i < 5; i++)
{
p[i] = 1;
}
//空间不够,增加5个整型的空间
//若用p接收时扩容失败,将丢失p指向空间的数据
int* ptr = (int*)realloc(p, 10 * sizeof(int));
if (NULL == ptr)
{
perror("realoc:");
return 1;
}
p = ptr;
ptr = NULL;
for (i = 0; i < 5; i++)
{
printf("%d ", p[i]);
}
free(p);
p = NULL;
return 0;
}
三.常见的动态内存错误
3.1 对NULL指针的解引用操作
int main()
{
int* p = (int*)malloc(5 * sizeof(int));
//未判断是否为空,就直接使用
//if (NULL == p)
//{
// perror("malloc:");
// return 1;
//}
int i = 0;
for (i = 0; i < 5; i++)
{
//如果p是空指针,就会有问题
p[i] = 1;
}
free(p);
p = NULL;
return 0;
}
3.2 对动态开辟空间的越界访问
int main()
{
int* p = (int*)malloc(5 * sizeof(int));
if (NULL == p)
{
perror("malloc:");
return 1;
}
int i = 0;
//越界访问
for (i = 0; i < 10; i++)
{
p[i] = 1;
}
free(p);
p = NULL;
return 0;
}
3.3 对非动态开辟内存使用free释放
int main()
{
int i = 10;
int* p = &i;
free(p);
p = NULL;
return 0;
}
3.4使用free释放一块动态开辟内存的一部分
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
3.5对同一块动态内存多次释放
void test()
{
int *p = (int *)malloc(100);
free(p);
//p = NULL; //(及时置空,即使再次释放也不会出现问题)
free(p);//重复释放,出现问题
}
3.6 动态开辟内存忘记释放(内存泄漏)
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
//此时忘记释放空间
//此时想释放空间也不行(没有传回来空间的起始地址)
return 0;
}
解决方法:
void test()
{
int* p = (int*)malloc(100);
if (NULL != p)
{
*p = 20;
}
//使用完之后进行释放
free(p);
p = NULL;
}
int main()
{
test();
return 0;
}
注意:
若需要返回空间,应该写好注释,记得释放
注释不仅能提醒我们自己。也可以提醒使用代码的其他人。
//函数内部malloc申请空间,返回空间的起始地址,记得释放
int* test()
{
int* p = (int*)malloc(100);
if (NULL != p)
{
*p = 20;
}
return p;
}
int main()
{
int* ret = test();
free(ret);
ret = NULL;
return 0;
}
四.经典的笔试题
4.1内存泄漏and非法访问
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str); //传值调用
strcpy(str, "hello world");
printf(str);
}
int main()
{
Test();
return 0;
}
存在的问题:
-
当str传给p的时候,p是str的一份临时拷贝,有自己独立的空间,当GetMemory函数内部申请了空间后,地址放在p中,此时str依旧是空指针,当GetMemor函数结束后,未返回申请空间的首地址,同时销毁变量p,此时已经无法在对申请的空间进行释放,造成内存泄漏。
-
strcpy拷贝的时候,传的是空指针,造成非法访问。
解决方法:
void GetMemory(char** p)
{
*p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str); //传址调用
if (NULL == str)//判断是否为空
{
perror("GetMemory:");
return;
}
strcpy(str, "hello world");
printf(str);
free(str); //释放
str = NULL; //置空
}
int main()
{
Test();
return 0;
}
4.2返回栈空间地址的问题
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void) {
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
存在的问题:
- 在GetMemory函数内创建字符串数组,返回字符串数组的起始地址,虽然起始地址被返回,但是申请存放字符串数组的空间在GetMemory函数结束时已经还给操作系统,当我们在利用返回的地址打印时(随机值),形成非法访问。
解决方法:
char* GetMemory(void)
{
//1.static修饰
//static char p[] = "hello world";
//2.常量字符串
char* p = "hello world";
return p;
}
void Test(void) {
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
注意点:
- 应该避免返回局部变量的地址。
- 需要返回局部变量的地址时,可以使用static修饰局部变量。
4.3非法访问
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
存在的问题:
- 申请空间后并未判断,若申请失败返回NULL,在使用strcpy函数,将造成非法访问。
- 释放空间后,申请的空间还给操作系统,并未及时置空,此时str中依旧存放着起始地址,但是str已经变成了野指针。
- 对野指针进行strcpy函数的拷贝,造成非法访问。
解决方法:
void Test(void)
{
char* str = (char*)malloc(100);
if (NULL != str)
{
strcpy(str, "hello");
}
free(str);
str = NULL;
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}