动态内存管理
为什么存在动态内存管理
学习过C语言基本语法的同学应该都熟练掌握了静态内存的开辟方式,例如:
int val1 = 20; // 在静态区开辟一个4字节的空间
char arr1[10] = {0}; //在静态区开辟10个字节的连续空间
int main()
{
int val2 = 20; // 在栈空间开辟一个4字节的空间
char arr2[10] = {0}; // 在栈空间上开辟10个字节的连续空间
}
上述案例中静态开辟内存的方式有2个特点:
- 开辟的空间大小是固定的
- 如果是为数组开辟空间,在声明时必须指定数组长度,它所需要的空间在编译时就会分配。
但是对于空间的需求,不仅仅是上述的情况,有时候我们需要开辟的空间大小只有在程序运行的时候才能知道,那么编译时开辟空间的方式就不能满足了,这时候就需要用到动态内存开辟。
备注:
C语言的C99标准支持了数组的动态创建,但是很多编译器并没有支持该标准,所以使用变量创建数组的代码在跨平台移植或者调用等场景下,可能会出现问题。
int main() { int n = 0; scanf("%d\n", &n); int arr1[10]; // 静态数组创建 int arr2[n]; // 动态数组创建(仅C99标准支持,很多编译器没有支持该标准,可移植性差) return 0; }
动态内存函数介绍
malloc
头文件:#include <stdlib.h>
语法:void* malloc(sizeof_t sizeof);
说明:这个函数向内存申请一篇连续可用的内存空间,并返回指向这片内存空间的指针。
- 参数size是需要开辟的空间大小,单位是字节。
- 如果开辟成功,则返回执行开辟空间的首地址。
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
- 返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体类型由使用者自己决定。
- 如果size为0,malloc的行为是未定义的,不同编译器可能会有不同的表现(要尽量避免这种行为)。
案例:
向内存申请10个int类型的空间
#include <stdio.h>
#inclued <stdlib.h> // 引入malloc函数头文件
#include <string.h>
#inclued <erron.h>
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if(NULL = p)
{
printf("%d\n", strerror(erron)); // 打印C语言错误码
}
return 0;
}
在上述案例中,我们向内存申请了10个int类型的空间,但是使用结束后没有还给操作系统,这可能会导致内存泄漏。那么怎么将mallco申请的内存空间还给操作系统哪,就需要用到free函数
free
头文件:#include <stdlib.h>
语法:void free(void* ptr);
说明:free函数用来释放动态开辟的内存
- 如果参数ptr指向的空间不是动态开辟的,那么free函数的行为是未定义的 ,不同编译器可能有不同的表现(要尽量避免这种行为)。
- 如果ptr是NULL指针,则函数什么事也不做。
- 使用free函数释放了参数ptr指向的内存空间后,ptr还是指向该内存空间,需要让ptr指向NULL(易错)。
案例:
使用free释放malloc开辟的空间
#include <stdio.h>
#inclued <stdlib.h>
#include <string.h>
#inclued <erron.h>
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if(NULL = p)
{
printf("%d\n", strerror(erron)); // 打印C语言错误码
}
free(p); // 当申请的内存空间不再使用时,需要还给操作系统
// *p = 10; // 如果申释放空间后,P不指向NULL,就有可能继续访问这块已经被释放的内存空间
p = NULL;
return 0;
}
在上述案例中,我们向内存申请了10个int类型的空间,但是使用结束后没有还给操作系统,这可能会导致内存泄漏。那么怎么将mallco申请的内存空间还给操作系统哪,就需要用到free函数
calloc
头文件:#include <stdlib.h>
语法:void* calloc(size_t num, sizeof_t size);
参数:
size_t num
元素的个数sizeof_t size
每个元素占用字节大小- 返回值
void*
,返回的是开辟空间的首地址
说明:calloc跟malloc函数的能力相似,都可以用来动态开辟内存
- callo函数的功能是为num个大小为size的元素开辟一块连续的内存空间,并把空间的每个字节初始化为0。
- 于函数malloc的区别只在于calloc会在返回地址前,把申请的内存空间的每个字节初始化为0。
案例:
向内存申请10个int类型的空间
#include <stdio.h>
#inclued <stdlib.h>
#include <string.h>
#inclued <erron.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if(NULL = p)
{
printf("%d\n", strerror(erron)); // 打印C语言错误码
}
free(p); // 当申请的内存空间不再使用时,需要还给操作系统
// *p = 10; // 如果申释放空间后,P不指向NULL,就有可能继续访问这块已经被释放的内存空间
p = NULL;
return 0;
}
使用calloc申请的内存空间,释放时也需要使用free函数
realloc
有时候我们会发现malloc或calloc函数申请的空间太大了,有时候我们又觉的申请的空间太小了。为了合理的使用内存,有时候会对内存大小进行灵活调整,那么reallo函数就可以做到对动态开辟的内存进行大小调整。
头文件:#include <stdlib.h>
语法:void* realloc(void* ptr, size_t size);
参数:
ptr
是要调整的内存地址size
是调整后的内存地址大小,单位字节- 返回值
void*
,返回的是调整后的内存起始地址
说明:
- realloc追加空间时有两种情况:
- 当已经申请的内存空间后面有足够的空闲空间可以追加,realloc会向操作系统申请追加这片空闲内存空间的使用权限,并返回已经申请的内存空间的首地址。
- 当已经申请的内存空间后面的空闲空间不足以追加时,realloc会向操作系统重新申请一片足够大的内存空间,并将原内存空间中的数据拷贝到新的空间中,释放旧内存,最后返回新申请内存空间的首地址。
案例:
思考以下代码段2次输出结果输出的p1地址是否相同?
#include <stdio.h>
#inclued <stdlib.h>
#include <string.h>
#inclued <erron.h>
int main()
{
int* p1 = (int*)malloc(10 * sizeof(int));
if(NULL = p1)
{
printf("%d\n", strerror(erron));
}else
{
printf("%p\n", p1); // 第一次打印p1地址
}
p1 = (int*)realloc(p1, 1000 * sizeof(int));
if(NULL = p1) // 注意:realloc函数也可能申请内存扩容失败,记得要对返回值判空
{
printf("%d\n", strerror(erron));
}else
{
printf("%p\n", p1); // 第二次打印p1地址
}
free(p1);
p1 = NULL;
return 0;
}
答案:不确定
2次输出结果输出的p1地址是否相同取决于使用realloc函数扩容时,申请的内存空间后面有足够的空闲空间可以追加。如果有足够空间,则输出结果相同。如果没有足够的内存空间,则输出结果不相同。
常见的动态内存错误
对NULL指针的解引用错误
使用malloc、calloc、realloc函数后,未对返回值判空就进行操作。
int main
{
int p = (int*)malloc(sizeof(int));
*p = 0; // 万一malloc申请内存失败了,p会被赋值为NULL,直接使用p就会有空指针风险
return 0;
}
对动态开辟空间的越界访问
即使是动态内存开辟,也不能访问未向操作系统申请的内存空间。
int main
{
int p = (int*)malloc(sizeof(int));
*(p+1) = 0; // 指针越界
return 0;
}
对非动态开辟的空间使用free释放
对非使用malloc、calloc、realloc函数申请的内存空间,进行了free操作。
int main()
{
int a = 10;
int* p = &a;
free(p); // 非动态开辟的空间,是由操作系统进行管理的,不能使用free进行释放
p = NULL;
return 0;
}
使用free释放动态开辟内存的一部分
free函数只能一次释放malloc、calloc、realloc函数申请的整片内存空间,不能只释放其中的一部分
int main()
{
int p = (int*)malloc(10 * sizeof(int));
free(++p); // 传递给free函数的不是动态开辟空间的首地址,不能使用free进行释放
return 0;
}
对同一块动态内存进行多次释放
避免对同一块动态开辟的内存空间进行多次free,一般遵循以下原则:
- 谁申请谁回收
int main()
{
int p = (int*)malloc(10 * sizeof(int));
free(p);
free(p);
p = NULL;
return 0;
}
动态开辟的内存空间忘记释放(内存泄漏)
动态申请内存空间后,不进行回收。导致内存泄漏,轻则程序性能下降,重则内存溢出程序崩溃。
int main()
{
while(1)
{
malloc(1); // 只开辟不回收,内存泄漏。
}
return 0;
}
动态内存经典面试题
-
判断以下代码段是否能正常输出,如果可以,输出结果是什么?
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是GetMemory函数的形参,只在函数的内部有效。等GetMemory函数执行结束后,p就会被销毁,p指向的动态开辟的内存空间并未被释放,且此时并没有指针指向该段动态开辟的内存空间,所以会造成内存泄漏。
代码改造:对上述代码进行改造,解决程序崩溃及内存泄漏问题
-
改造方式1:将str的值传递,改为地址传递
void GetMemory(char** p) // 2. 双重指针接收指向地址的指针 { *p = (char*)malloc(100); // 3. 解引用,改变指针指向 } void Test(void) { char* str = NULL; GetMemory(&str); // 1. 值传递改为地址传递 strcpy(str, "hello world"); printf(str); free(str); // 4. 动态开辟的空间,使用结束后记得释放,并将指针指向NULL str = NULL; } int main() { Test(); return 0; }
-
改造方式2:将GetMemory函数中申请的动态内存地址作为返回值,赋值给str
void* GetMemory(char* p) // 1. GetMemory函数返回申请的动态内存地址 { p = (char*)malloc(100); return p; } void Test(void) { char* str = NULL; str = GetMemory(str); // 2. str接收动态开辟内存空间的地址 strcpy(str, "hello world"); printf(str); free(str); str = NULL } int main() { Test(); return 0; }
-
请问运行以下代码段中Test函数会有什么样的结果?
char* GetMemory(void) { char p[] = "hello world"; return p; } void Test(void) { char* str = NULL; str = GetMemory(); printf(str); }
答案:程序崩溃或者打印随机值
原因:经典的返回栈空间的地址问题
解析:GetMemory方法中定义的数组p是函数内部变量,是在栈空间中定义的,GetMemory函数执行结束后,对应的栈空间就会被销毁。在被销毁前,GetMemory函数返回了p的值,并且被Test函数捕获到并且进行了调用,结果是未可知的。不同的编译器会有不同的表现,有些编译器会直接崩溃,有些编译器可能会越界访问已经被回收的内存地址。
代码改造:对上述代码进行改造,解决程序崩溃或打印随机值问题
-
改造方式1:将变量p声明为静态变量,静态区中的变量函数执行结束不会被销毁,那么它的值就是可以被返回的。
char* GetMemory(void) { static char p[] = "hello world"; // static修饰的变量是存在静态区的,函数结束不会被销毁 return p; } void Test(void) { char* str = NULL; str = GetMemory(); printf(str); }
-
改造方式2:将变量p指向的空间使用动态内存开辟方式,动态开辟的内存在堆区,函数结束不会被销毁。
char* GetMemory(void) { char* p = (char*)malloc(100 * sizeof(char)); strcpy(p, "hello worl"); return p; } void Test(void) { char* str = NULL; str = GetMemory(); printf(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); }
答案:正常输出hello
解析:在调用GetMemory函数时,使用了地址传递的方式,在GetMemory函数中,将str指针指向了一片malloc动态开辟的内存空间,所以在Test函数中可以正常的对这片申请的内存空时使用。
缺陷:malloc函数申请的内存空间使用结束后没有进行释放,造成了内存泄漏。
-
请问运行以下代码段中Test函数会有什么样的结果?
void Test(void) { char* str = (char*)malloc(100); strcpy(str, "hello"); free(str); if(str != NULL) { strcpy(str, "world"); prtinf(str); } }
答案:篡改了已经还给操作系统的栈区空间,结果难以预料,非常危险。因为free之后,str已经变成了野指针。
改进:使用free释放动态开辟的内存空间后,应该即可将str指向NULL。
柔性数组
什么是柔性数组
在C99标准中,结构体中的最后一个元素允许是一个未知大小的数组,这就叫做“柔性数组”成员。例如:
typedef struct st_type
{
int i;
int arr[0]; // 未知大小的柔性数组成员
}type_a;
补充:arr后面的[ ]中不填写值也可以,填0也可以。
柔性数组的特点
- 结构体中的柔性数组成员前面必须至少包含一个其他成员。
- sizeof返回的这种结构大小不包含柔性数组的内存。
- 包含柔性数组成员的结构体使用malloc函数进行内存的动态分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小。
带柔性数组的结构体大小
定义:
- 操作系统不会在编译阶段为柔性数组开辟内存空间,所以带柔性数组的结构体大小,就是除柔性数组外,结构体大小的总和。
案例:
-
思考以下代码段的输出结果:
struct S1 { int i; int arr[0]; // 未知大小的柔性数组成员 }; int main() { struct S1 s; printf("%d\n", sizeof(S1)); return 0; }
答案:输出结果为4,因为擦操作系统不会为柔性数组开辟内存空间。
柔性数组的使用
既然操作系统不会为柔性数组开辟内存空间,那么就需要程序员来动态申请。
案例
struct S1
{
int i;
int arr[0]; // 未知大小的柔性数组成员
};
int main()
{
// 为柔性数组arr申请长度为5个元素的空间
struct S1* s = (struct S1*)malloc(sizeof(S1) + 5 * sizeof(int));
for(int i = 0;i < 5; i++)
{
s->arr[i] = i;
}
// 为柔性数组arr申请追加长度为6个元素的空间
struct S1* tempS = (struct S1*)realloc(s, sizeof(S1) + 6 * sizeof(int));
if(NULL != tempS)
{
s = tempS;
}
s->arr[5] = 5;
free(s);
s = NULL;
return 0;
}
使用非柔性数组方式
思考一个问题,在没有柔性数组的时代,是怎么让一个结构体成员包含未知大小的数组的?
案例:
struct S1
{
int i;
int* arr; // 未知大小的柔性数组成员
};
int main()
{
// 为结构体申请内存空间
struct S1* s = (struct S1*)malloc(sizeof(S1));
// 为结构体中的指针申请一片连续内存空间,用于存储5个整型元素。
s->arr = (int*)malloc(5 * sizeof(int));
// 调整结构体中的指针指向数组的大小
int* ptr = (int*)realloc( s->arr, 6 * sizeof(int));
if(NULL != ptr)
{
s->arr = ptr;
}
// 使用结束,释放内存
free(s->arr);
s->arr = NULL;
free(s);
s = NULL;
return 0;
}
思考:想一下,先
free(s)
,再free(s->arr)
可以吗,会有什么后果?
柔性数组的优点
-
方便内存释放
在编码过程中,malloc、calloc、realloc等动态开辟内存的函数调用次数越多,就需要对应调用更多次的free。如果我们的结构体所占空间是动态申请的,结构体中的元素所占空间也是动态申请的,那么使用结束就需要逐级向操作系统归还内存,这种情况下,既需要多次free,又对free的先后顺序有严格限制,很容易代码编写失误造成内存泄漏。
而使用柔性数组,能够降低动态开辟内存的次数。
-
有利于访存
根据段/页式存储和局部性原理,连续的内存空间更容易被加载到cache中,有益于提高CPU访存速度,也有溢于减少内存碎片。