动态内存管理是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。
动态内存管理不像数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。
C/C++
定义了4个内存区间:
- 代码区
- 全局变量与静态变量区
- 局部变量区,即
栈
区- 动态存储区,即
堆
(heap)区或自由存储区(free store)。
所有动态存储分配都在堆区中进行。
int a = 20; //在栈空间开辟4个字节内存
char arr[5] = { 0 }; //在栈空间开辟5个字节的连续内存
以上程序使我们常见掌握的内存开辟方式,称为静态的内存分配方式,这种方式有三个特点:
C89
标准要求数组元素个数必须是常量
。
新版本C99
标准允许数组元素个数是变量
。
但实际使用中有时申请开辟的内存空间不足以存储数据,往往还需要手动修改需要的内存空间扩容。为了避免如此繁琐的步骤,使用动态内存分配的方式就可以无需考虑空间不足的情况了。
函数的声明都包含在
头文件中。
void* malloc (size_t size);
这个函数向内存申请一块连续可用的空间,并返回指向这块内存区域的指针。
NULL
空指针,因此malloc
的返回值一定要做检查。void*
,所以malloc
函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。size
为0
,malloc
的行为是标准是未定义的,取决于编译器。malloc函数的使用
int *a = (int *)malloc(10 * sizeof(int));
malloc
只是动态的申请一段连续的内存空间,无论当成int
、char
数组使用都是可以的,因为其返回值是void*
类型,所以只需在函数前进行强制类型转换为要使用的类型即可。int*
)malloc
函数申请空间的单位为字节,所以与使用者希望构建的数组类型息息相关,这时通过元素个数 * 元素类型
的方式计算总字节数。这样的方式申请内存空间的在运行时开辟,空间长度能够比较灵活。10×4=40
)a
的返回值也是int*
,需要与强转的类型相同。此时申请完毕,a
就指向了10 * sizeof(int) = 40
这么多字节的一段连续的内存空间。C
语言提供的free
函数专门是用来做动态内存的释放和回收的。
void free (void* ptr);
ptr
指向的空间不是动态开辟的,那free
函数的行为是未定义的。ptr
是NULL
空指针,则函数什么事都不做。【详见下面特殊情况的讲解】NULL
。free
函数的使用:直接手动释放内存
int *a = (int *)malloc(10 * sizeof(int));
free(a); //直接使用
malloc
但没有free
,申请到的内存没有释放一直被占用,就会造成内存泄漏。int main(){
int *a = (int*)malloc(sizeof(int));
*a = 100;
return 0;
}
此代码中申请到的内存没有进行释放,但是刚申请到内存,main
函数就结束了,说明程序也就结束了,操作系统回收了内存,就不会造成内存泄漏。
所以是否为内存泄漏现象也要取决于程序的生命周期,如果是7×24
小时运行的服务器程序,那么就必然是内存泄漏。
【扩展】:C
语言中要求使用者必须自己保证释放申请到的内存空间。
为了避免内存泄漏问题,后世的许多编程语言引入了新的概念:垃圾回收机制(GC
)
STW
问题:stop the world
)。Q:如何
检测
函数中是否出现了内存泄漏了?
A:单次调用函数可能泄露很少的内存,无法直接发现。但可以多次重复调用
函数,量变引起质变,这时候观察内存使用情况就可以很明显的发现是否出现了内存泄漏。
应对内存泄漏的燃眉之计就是:重新运行代码,泄露的内存区段就全销毁了,全部重新开始写入。但这只是聊胜于无的应对方案,还是需要程序员找到真正造成内存泄露的代码块,并加以修正。
特殊情况
C++
标准规定对空指针进行free是合法的,无事发生。
所以推荐大家在free
申请到一个内存空间后,就把刚刚申请到指向该内存空间的指针设为NULL
:
int *p = NULL;
free(p);
p = NULL; //设为 NULL
void* calloc (size_t num, size_t size);
calloc
与malloc
的区别只在于会进行初始化,calloc
函数会在返回地址之前把申请的num * size
个字节的内存空间置为全0
。num
个大小的size
的元素开辟一块空间,并且把空间的每一个字节初始化为0
。【优点】:每个字节置为0,避免使用到未初始化的随机值的情况。
【缺点】:相比于随机值,多了一步设置为全零的操作,产生多余开销。
realloc
函数的出现让动态内存管理更加灵活。realloc
函数就可以做到对动态开辟内存大小的调整。void* realloc (void* ptr, size_t size);
ptr
是需要调整的内存地址。size
是调整之后的新大小,单位为字节
。realloc
在调整内存内存空间的是存在两种情况:
错误一:以下演示一种经典的内存泄漏的场景
int *p = (int*)malloc(4);
p = (int*)malloc(4);;
free(p);
【错误原因】:
malloc
申请到一块内存空间,假设此连续的内存空间的首地址为0x100
,那么指针变量p
存储的地址就是0x100
。malloc
申请又到一块连续内存空间,假设新申请的连续的内存空间的首地址为0x200
,那么指针变量p
更新之后存储的地址就是0x200
。p
变量进行free
释放空间,则释放的是第二次申请到的内存空间,而第一次申请到的内存空间一直没有释放,就造成了内存泄漏。错误二:以下演示多次free
的场景
int *p = (int*)malloc(4);
free(p); //释放申请到的内存
free(p); //再次释放内存
【错误原因】:
因为释放了向操作系统申请到的内存空间,再对这段没有申请的、没有操作权限的内存空间进行释放,就是未定义行为。造成错误。除非这个指针为空指针NULL
。
错误三:
int a = 10;
int *p = &a;
free(p);
【错误原因】:
因为free
必须搭配malloc
系列函数使用,不能单独使用,如果去释放不是由malloc
系列函数申请到的内存空间,就会出现错误。程序报错。
错误四:
int *p = (int*)malloc(4);
free(p);
*p = 100;
【错误原因】:
申请到的内存释放掉了,再进行赋值造成了内存访问越界,类似于数组访问越界行为,都是未定义行为。程序报错。
*p = 100;
这一语句也与上面的free
后设置为NULL
的操作相呼应,如果出现了此语句,那么就是对空指针解引用,会引起程序崩溃,便于程序员快速找到问题所在。如果没有设置为空,那么出现未定义行为或许不会造成程序崩溃,一错再错,置程序员于万劫不复的境地。所以还是要防微杜渐。
错误五:
int *p = (int*)malloc(100);
p++;
free(p);
【错误原因】:
p
不再指向动态内存的起始位置,如果原地址为0x100
,那么++
之后地址就是0x104
,并不是释放了之后的空间而造成的内存泄漏,而是一个未定义行为。不能对一个已经变更的内存地址进行free
。程序报错。
代码一:
void GetMemory(char *p){
p = (char*)malloc(100);
}
void Test(void){
char *str = NULL;
GetMemory(str);
strcpy(str,"hello world");
printf("%s\n",str);
}
【错误原因】:
free
。p
变量是形式参数,在函数内部对p
如何操作都是不影响实际参数的,实参仍为一个空指针。之后对空指针进行strcpy
就出现了内存访问越界,造成未定义行为。【修改】所以应该改为二级指针进行传参:
void GetMemory(char **p){
*p = (char*)malloc(100);
}
void Test(void){
char *str = NULL;
GetMemory(&str);
strcpy(str,"hello world");
printf("%s\n",str);
free(str); //释放内存
}
代码二:
char *GetMemory(void){
char p[] = "hello world";
return p;
}
void Test(void){
char *str = NULL;
str = GetMemory();
printf("%s\n",str);
}
【错误原因】:
p
的生命周期就结束了,内存释放。在主函数再去访问已释放的内存就也是内存访问越界,造成未定义行为了。"hello world"
这个字面值常量的生命周期是全局的,这个字符串只读不可以修改,要用const
修饰。所以函数结束指针变量p
并没有销毁,可以正确打印出字符串。【修改】:
char *GetMemory(void){
const char *p = "hello world"; //指针变量p
return p;
}
void Test(void){
const char *str = NULL;
str = GetMemory();
printf("%s\n",str);
}
代码三:
void GetMemory2(char **p, int num){
*p = (char*)malloc(num);
}
void Test(void){
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf("%s\n",str);
}
【错误原因】:
malloc
申请内存有可能失败,返回一个空指针,所以要在函数中判定一下返回指针为空的情况。【修改】:
void GetMemory2(char **p, int num){
*p = (char*)malloc(num);
}
void Test(void){
char *str = NULL;
GetMemory(&str, 100);
if(str == NULL){ //判断为空条件
//...
}
strcpy(str, "hello");
printf("%s\n",str);
free(str); //释放内存
}
代码四:
void Test(void){
char *str = (char*)malloc(100);
strcpy(str,"hello");
free(str);
if(str != NULL){
strcpy(str,"world");
printf("%s\n",str);
}
}
【错误原因】:
str
在释放内存后就变成了野指针,再对它进行内容填充就相当于访问一个非法内存,属于未定义行为。结果不可预期,有可能输出希望的结果,也有可能程序闪退,要尽量避免这种行为。内存分配区域图:
stack
):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。CentOS
默认栈的大小为8192KB
=8M
。VS
默认栈的大小不到1M
。CentOS
可通过ulimit -s
指令进行修改,只是修改的空间过大系统就不允许了。heap
):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。static
):存放全局变量、静态数据。程序结束后由系统释放。【这里讨论的栈和堆并非数据结构中的名词】
实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
但是被static
修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁,所以生命周期变长。
- 如图也就解释了可以直接定义数组,为什么还要引入
malloc
:
因为定义的数组是在栈上申请的内存,然而栈相比于malloc
申请空间的堆区,它的空间非常小,有事用户要创建很大的数组就无法正确申请到内存,所以也是动态内存管理函数存在的必要性。
malloc
来申请内存?malloc
。直接定义临时变量
来使用内存?这是C
语言独有的概念,C99
标准中规定结构中最后一个元素允许是位置大小的数组,这就叫做柔性数组成员。
typedef struct st_type {
int i;
int a[0]; //柔性数组成员,0的意思是可以有任意多个元素
//int a[]; 也可写成此方式
}st_type;
int main() {
//创建一个10个单位长度的数组
Test *t = (Test*)malloc(sizeof(int) + sizeof(int) * 10);
//创建一个100个单位长度的数组
//Test *t2 = (Test*)malloc(sizeof(int) + sizeof(int) * 100);
//内存申请完毕,开始赋值
t->i = 10;
for(int i = 0;i < 10;++i){
t->a[i] = i;
}
free(t); //记得释放掉申请的内存空间
return 0;
}
同一个结构体可以定义成不同的长度,这就是柔性数组的功能所在。
那么不使用柔性数组,也可以实现吗?可以,请看代码二:
typedef struct st_type {
int i;
int *a; //定义一个结构体指针 代替柔性数组成员
}Test;
int main(){
Test *t2 = (Test*)malloc(sizeof(Test));
t2->i = 10;
t2->a = (int*)malloc(sizeof(int) * 10); //在这里指针a指向另一个malloc到的内存,大小也可以自己设置
free(t2->a);
free(t2);
return 0;
}
相比之下柔性数组还是优于代码二的,两者区别就是:
free
一次,而代码二需要malloc
两次并free
两次,开销增大。free
操作时先free(t2);
,再进行free(t2->a);
,就会造成错误,所以还需多费精力处理这两者的顺序。sizeof
返回的这种结构大小不包括柔性数组的内存。如果结构中成员为{int i; int a[0];}
,那么结构体sizeof
的结果就是4
。malloc
函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小