文章目录
一、 为什么存在动态内存分配
我们已经掌握的内存开辟方式有:
int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
但是上述的开辟空间的方式有两个特点:
空间开辟大小是固定的。
数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况,有时候数组的大小在程序运行的时候才能知道,这是由于它所需要的内存空间取决于输入数据
这种方法的优点是简单,但它有好几个缺点:
这种声明在程序中引入了人为限制,当程序需要的元素数量超过了声明长度,它就无法处理这种情况,要避免这种情况,一种显而易见的办法是把数组声明得足够大,但这种做法恶化了它的第二个缺点,当程序实际需要的元素数量比较少时,巨型数组的绝大部分空间都被浪费了
这时候就只能使用动态内存开辟,所以C语言有了动态内存开辟(动态开辟的空间都是在堆区上的)。
二、动态内存函数的介绍
以下库函数都声明在 <stdlib.h> 头文件中
1. malloc 和 free
C函数库提供了两个函数,malloc 和 free,分别用来执行动态内存分配和释放
malloc 向内存申请一块连续可用的空间,并返回指向这块空间的指针,这块内存此时并没有以任何方式进行初始化
- 函数原型
void* malloc (size_t size);
# void* 函数返回值,申请成功返回指向开辟的空间的指针,申请失败则返回NULL;
# size_t size 参数,指定要开辟多少个字节的空间;
- 注意事项
如果开辟失败,则返回一个NULL指针,因此 malloc 的返回值一定要做检查!
返回值的类型是 void* ,因为 malloc 函数并不知道开辟空间的类型,具体在使用的时候使用者自己
来决定。
如果参数 size 为0,malloc 的行为是标准是未定义的,取决于编译器,所以最好不要这样做!
前面提到,动态开辟的空间都是在堆区上的,在堆区上开辟的空间有一个特点,这一点和栈区是不一样的,那就是堆区上的空间使用完之后不会自己主动释放,而是专门设计了一个释放动态内存的函数:free,需要程序员主动调用这个函数来释放空间;
当然,程序结束后,操作系统是会自动回收动态开辟的内存的(这就是为什么电脑重启能解决99%的问题);但是,在一些公司的大项目中,为了保证用户体验,有的程序是需要7*24小时运行的,就比如腾讯云和阿里云的云服务器;而一旦我们使用动态内存开辟函数开辟空间使用完却忘记释放时,就会造成内存泄露(相当于你向内存申请了一块空间,但是你使用完之后不归还,这样别人也用不了这块空间了,虽然这块空间还存在,但是相当于没有了),这时我们就会发现,随着程序的持续运行,可供我们使用的内存会变得越来越少;
内存泄露是我们进行动态内存管理是最容易犯的错误,需要时刻养成用完就 free 的好习惯。
free 函数原型如下:
void free (void* ptr);
// void* ptr 你要释放的空间的起始地址;
- 函数使用
#include <stdio.h>
int main()
{
int num = 5;
int* ptr = (int*)malloc(num * sizeof(int));
if(NULL != ptr)//判断ptr指针是否为空
{
for(int i=0; i<num; i++)
{
*(ptr+i) = 0;
}
}
free(ptr);//释放ptr所指向的动态内存
ptr = NULL;//指针置空
return 0;
}
- 注意事项
-
如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的
-
如果参数 ptr 是NULL指针,则函数什么事都不做
-
用 free 释放完 ptr 指向的内存后,必须把指针 ptr 置空,防止重复释放已经释放过的内存
2. calloc
- 函数功能
calloc 函数的功能和 malloc 十分相似,都是向堆区申请一块空间并返回空间的起始地址,但是 calloc 函数比 malloc 函数多了一个操作,那就是会将申请的空间里面数据全部初始化为0。
void* calloc(size_t num, size_t size);
// void* 函数返回值,申请成功返回动态开辟的空间的起始地址,申请失败则返回NULL;
// size_t num 函数参数,用于指定要申请的元素个数:
// size_t size 函数参数,用于指定每一个元素的大小(字节为单位);
- 函数使用
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p = (int*)calloc(10, sizeof(int));
if(NULL != p)
{
//使用空间
}
free(p);
p = NULL;
return 0;
}
所以如果我们对申请的内存空间的内容要求初始化,那么可以很方便的使用 calloc 函数来完成任务,不用自己手动初始化
3. realloc
realloc 函数的出现让动态内存管理更加灵活。有时我们会发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理地使用内存,我们可以对内存的大小做灵活的调整,那 realloc 函数就可以做到对动态开辟内存大小的调整
- 函数功能
调整已开辟的动态空间的大小
- 函数原型
void* realloc(void* ptr, size_t size);
// void* 函数返回值,开辟成功返回动态开辟的新空间的起始地址,开辟失败则返回NULL;
// void* ptr 函数参数,表示要调整的空间的起始地址;
// size_t size 函数参数,新空间的大小(字节为单位);
- 函数使用
#include <stdio.h>
int main()
{
int *ptr = (int*)malloc(100);
if(ptr != NULL)
{
//业务处理
}
else
{
exit(EXIT_FAILURE);
}
//扩展容量
//代码1
ptr = (int*)realloc(ptr, 1000);//这样可以吗?(如果申请失败会如何?)
//代码2
int*p = NULL;
p = realloc(ptr, 1000);
if(p != NULL)
{
ptr = p;
}
//业务处理
free(ptr);
ptr = NULL;
return 0;
}
- 注意事项
如果开辟失败,则返回一个NULL指针,因此不能把 realloc 的返回值直接赋给原指针(如代码1),以免丢失原来的空间地址
当 realloc 函数的第一个参数为NULL时,realloc 当作 malloc 函数使用
realloc 函数在调整原内存空间大小的时候,原内存的数据并不会丢失,除了缩小内存块会损失部分数据
realloc在扩大内存空间的时候存在两种情况:
情况1:原有空间的后面有足够大的空间,可以让我们申请,这时扩展内存就在原有内存之后直接添加内存,这块内存原先的数据不会发生变化,新内存也并未进行初始化。
情况2:原有空间的后面没有足够大的空间让我们申请。这时 realloc 函数会在堆空间上另找一个合适大小的连续空间来使用,这样函数返回的是一个新的内存地址;
所以我们在使用 realloc 函数的时候不要直接将重新调整的空间地址直接赋值给源空间地址,而是应该先进行空指针判断,避免开辟新空间失败的同时还将源空间搞丢,造成内存泄漏;
三、常见的动态内存错误
1. 对 NULL 指针的解引用操作
上面提到,malloc、calloc、realloc 这些函数向内存申请空间是有可能会失败的,申请失败函数就会返回空指针,如果我们不对函数的返回值进行判断,而直接对其解引用的话,就会造成程序崩溃;例如:
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
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);
}
3. 对非动态开辟内存使用free释放
free 函数是专门用于释放动态开辟的空间的,如果对非动态开辟的空间进行 free 操作,会造成程序崩溃,例如:
void test()
{
int a = 10;
int *p = &a;
free(p);//ok?
}
4. 使用free释放一块动态开辟内存的一部分
当我们成功开辟一块动态空间并将它交由一个指针变量来管理时,我们可能会在后面的程序中让该指针变量自增,从而让其不再指向该动态空间的起始位置,而是指向中间位置或者结尾,这时我们再对其进行 free 操作时,也会导致程序崩溃,因为free函数必须释放一整块动态内存,而不能释放它的一部分。例如:
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
解决办法:将申请的动态内存交由两个指针变量进行管理,其中一个用于各种操作,另外一个用于记录空间的起始地址
5. 对同一块动态内存多次释放
我们在写程序的时候可能在程序中的某一位置已经对动态内存进行释放了,但是随着后面代码的展开,我们可能忘记了而重复对一块动态内存进行释放,也会导致程序崩溃,例如:
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
解决办法:每次释放掉一块动态内存时,都将相应的指针变量置空,这样即使后面重复释放,free(NULL) 也没有任何影响
6. 动态开辟内存忘记释放(内存泄漏)
void test()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
exit(-1);
}
int flag = 0;
scanf("%d", &flag);
if (flag == 2)
{
//...... --程序逻辑
return; //内存泄漏
}
else
{
//...... --程序逻辑
}
free(p);
p = NULL;
}
以上代码在test函数的末尾对动态开辟的空间进行了释放,还把指针变量p置为了空,但是这个函数还是可能会造成内存泄露,因为当函数从flag == 2 的路径返回时,test函数不会执行 free 函数释放内存,所以说,内存泄漏真的是防不胜防。
**切记:动态开辟的空间一定要释放,并且正确释放 **
四、几个经典的笔试题
笔试题1
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
上面这段程序有三个问题:
在Test函数中调用 GetMemory 函数时,传递的是 str 的值,所以 GetMemory 函数的参数p 只是 str的一份临时拷贝,也是一个空指针,将动态开辟的100个字节交由指针p管理 与 str 没有任何关系;另外,malloc 函数申请空间也有可能失败,没有进行空指针判断。
由于 GetMemory 函数并没有改变str,所以 str 仍为NULL,这时调用 strcpy 函数会导致程序错误,因为strcpy的参数不能为NULL;
代码中并没有对动态开辟的100个字节空间进行free,会导致内存泄漏;
笔试题2
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
上面这段程序有两个问题:
在 GetMemory 函数中,p是一个数组,是在栈区上开辟空间,而不是在堆区上动态开辟的,所以当GetMemory 函数调用完毕后p和字符串都被销毁了,所以 GetMemory 函数并不能使 str 指向一块可用内存;
GetMemory 返回了p的地址,并将其赋值给了 str,但由于 GetMemory 函数调用完毕后其函数栈帧销毁,所以原本属于p的那块空间已经被操作系统回收,而 str 通过返回的地址对那块内存进行访问,就造成了野指针问题,得到的结果是随机值。
笔试题3
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
上面这段程序有两个问题:
虽然这里把 str 的地址传递给了 GetMemory 函数,让其指向了一块动态开辟的空间,但是这里没有对malloc函数的返回值进行检查,当malloc失败的时候还是会产生空指针问题;
程序中没有对 malloc 的空间进行free,造成了内存泄漏;
笔试题4
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
这段程序存在两个问题:
没有对 malloc 的返回值进行空指针检查,使得 strcpy 函数可能执行失败;
在 free 掉动态开辟的内存之后没有把相应的指针置空,导致if条件成立,使 strcpy 函数访问了已经释放的内存,造成野指针问题;
五、C/C++程序的内存开辟
C/C++程序内存分配的几个区域:
栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
堆区(heap):就是通过 new、malloc、realloc 分配的内存块,一般由程序员分配释放(free), 若程序员不释放,程序结束时可能由操作系统回收 。分配方式类似于链表。
数据段(静态区)(static)存放全局变量、静态数据,程序结束后由系统释放。
代码段:存放函数体(类成员函数和全局函数)的二进制代码。
常量区:常量存储在这里,不允许修改。
有了这幅图,我们就可以更好的理解 static 关键字修饰局部变量的例子:
实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
但是被 static 修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序
结束才销毁,所以被 static 修饰的变量生命周期变长。