C语言--动态内存管理
动态内存管理
1. 为什么要有动态内存分配
当前向内存中申请空间存放数据的两种方式如下:
int val = 20; // 在栈空间上开辟四个字节
char arr[10] = {0}; // 在栈空间上开辟40个字节的连续空间
但是上述的开辟空间的方式有两个特点:
- 空间开辟大小是固定的。
- 数组在声明的时候,必须指定数组的长度,数组空间一旦确定了大小不能调整。
有时候,我们需要的空间大小在程序运行的时候才能知道,这时候数组的编译时开辟空间的方式就不能满足需求。
C语言引入了动态内存开辟,让程序员自己可以申请和释放空间,灵活性更强。
2. malloc和free
2.1 malloc
C语言提供了一个动态内存开辟的函数:
void* malloc(size_t size);
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个
NULL
指针,因此malloc的返回值一定要做检查。 - 返回值的类型是
void*
,所以malloc函数并不知道开辟空间的类型,具体在使用的时候由使用者自行决定。 - 如果参数
size
为0,malloc的行为是未定义的,取决于编译器。参数的单位是字节。
举个例子:
#include <stdio.h>
#include <stdlib.h> //包含malloc函数的头文件
int main()
{
//申请10个整型空间
int* p = (int*)malloc(10 * sizeof(int));
//void*类型的指针放进一个int*类型的变量中去需要将类型强制转换
if(p == NULL)
{
//空间开辟失败
perror("malloc") //perror会主动将错误信息追加在malloc后面打印出来
return 1; //异常返回,组织程序继续运行
}
//空间开辟成功,可以使用这40个字节的空间
int i = 0
for (i = 0; i < 10; i++)
{
*(p + i) = i + 1; //类似于数组的使用方法一样,向空间中添加0~9的数字
}
return 0;
}
注意:
- malloc申请的空间和数组的空间有什么区别?
- malloc申请的空间是动态内存,大小是可以调节的。
- 开辟空间的位置不一样。
- 栈区:局部变量,局部数组,函数形参
- 堆区:动态内存开辟的空间,包括malloc,free,calloc,realloc等函数开辟的空间
- 静态区:全局变量,static修饰的静态变量
开辟空间失败的代码示例:
//在x86的环境下
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(INT_MAX);
if(p == NULL)
{
//空间开辟失败
perror("malloc") //perror会主动将错误信息追加在malloc后面打印出来
return 1; //异常返回,组织程序继续运行
}
//空间开辟成功,可以使用这40个字节的空间
return 0;
}
运行结果:
malloc:Not enough spac
最后在利用malloc函数开辟的空间,并将空间使用完之后需要将开辟的空间进行释放,需要利用到下面的函数free。
2.2 free
C语言提供了另一个函数free,专门用于释放动态内存。函数原型如下:
void free(void* ptr);
free函数用于释放动态开辟的内存。
- 如果参数
ptr
指向的空间不是动态开辟的,那free函数的行为是未定义的,不能利用free函数进行释放。 - 如果参数
ptr
是NULL指针,则函数什么事都不做。
malloc和free都声明在stdlib.h
这个头文件中
举个例子:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int num = 0;
scanf("%d", &num);
int arr[num] = {0};
int* ptr = NULL;
ptr = (int*)malloc(num*sizeof(int));
if(NULL != ptr)//判断ptr指针是否为空
{
int i = 0;
for(i=0; i<num; i++)
{
*(ptr+i) = 0;
}
}
free(ptr);//释放ptr所指向的动态内存
ptr = NULL;//是否有必要? 有必要!
return 0;
}
注意:
- 在利用free函数释放完空间之后,是否还有必要再将ptr赋值为空指针?
这是有必要的!
如果不加 **ptr = NULL;**这一句,会造成虽然利用free函数释放了ptr的空间,但是此时ptr中的值仍为开辟空间的首地址,但是此地址已经归还给操作系统,此时ptr指向的空间不属于当前程序,但是仍可以找到这个空间,所以此时的ptr是野指针。
在加上 **ptr = NULL;**这一句的话,就可以保证此时ptr已经不记得之前开辟的空间,也不是野指针了。
- 以上程序和直接定义一个arr[10] 的数组存放0~9的数有什么区别?
因为数组是定义在函数中的,当函数执行完之后,对应数组开辟的空间会自动被收回。其生命周期是相当固定的。
而malloc函数开辟的空间是需要手动利用free函数进行释放的,所以对于内存空间的控制是更加灵活的。
如果不利用free函数手动释放内存,在程序结束之后也会被操作系统自动回收。但是不建议开辟空间之后等着操作系统自动回收。
malloc函数和free函数最好成对使用。
3. calloc和realloc
3.1 calloc
C语言还提供了一个函数叫 calloc
,它也用于动态内存分配。原型如下:
void* calloc(size_t num, size_t size);
- 该函数为
num
个大小为size
的元素开辟一块空间,并且将空间的每个字节初始化为0。 - 与
malloc
的区别在于,calloc
会在返回地址之前把申请的空间的每个字节初始化为全0。
举个例子:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p = (int*)calloc(10, sizeof(int));
if(NULL != p) //开辟成功
{
//使用空间
int i = 0;
for(i=0; i<10; i++)
{
printf("%d ", *(p+i)/*p[i]*/);
//打印这十个空间中的内容
}
}
else //开辟失败
{
perror("calloc"); //打印错误原因
return 1;
}
free(p); //calloc也是在堆区申请空间,所以使用完之后需要释放空间
p = NULL; //防止变成野指针
return 0;
}
输出结果:
0 0 0 0 0 0 0 0 0 0
所以如果对申请的内存空间内容要求初始化,可以很方便的使用calloc函数来完成。
注意:
那么malloc开辟的空间中是否初始化了内容呢?
通过监视内存中的值可以知道,malloc函数开辟的空间中初始时其中的值都是cd,而cd代表的含义是内存中的一些随机值,打印出来的话显示的数据是没有办法预测的。
3.2 realloc
realloc
函数使得动态内存管理更加灵活。- 当发现过去申请的空间太小,或者需要更大的空间时,
realloc
可以调整内存的大小。
函数原型如下:
void* realloc(void* ptr, size_t size);
- ptr是要调整的内存地址
- size调整之后新大小
- 返回值为调整之后的内存起始位置。
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
- realloc在调整内存空间的是存在两种情况:
- 情况1:原有空间之后有足够大的空间可以追加
- 情况2:原有空间之后没有足够大的空间无法继续追加
情况1
当是情况1的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
情况2
当是情况2的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。
再重新找到一块连续的空间之后,会将原来的旧的数据拷贝到新的空间中去,并将旧的空间释放,最后返回新的地址。
由于上述的两种情况,realloc函数的使用就要注意一些。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *ptr = (int*)malloc(100);
if(ptr != NULL)
{
//业务处理
}
else
{
return 1;
}
//扩展容量
//代码1 - 直接将realloc的返回值放到ptr中
ptr = (int*)realloc(ptr, 1000);//这样可以吗?(如果申请失败会如何?)
//代码2 - 先将realloc函数的返回值放在p中,不为NULL,在放ptr中
int*p = NULL;
p = realloc(ptr, 1000);
if(p != NULL)
注意:
代码1并不推荐,因为如果realloc函数调整空间失败,原来开辟的空间也会失效,所以需要创建一个新的指针用于接收调整之后的空间。
也就是代码2的操作方式。
补充:
realloc函数不仅可以调整空间,还可以用于申请空间。
realloc (NULL, 40) = malloc (40);
4. 常见的动态内存的错误
4.1 对NULL指针的解引用操作
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20; // 如果p的值是NULL,就会有问题
free(p);
}
注意:
第四行中,因为p有可能是空指针,所以可能出现空指针的解引用操作。
因此以后需要对malloc函数的返回值进行if判断,即可解决此类错误。
4.2 对动态开辟空间的越界访问
void test()
{
int i = 0;
int *p = (int *)malloc(10 * sizeof(int));
if(NULL == p)
{
exit(EXIT_FAILURE);
}
for(i = 0; i <= 40; i++)
{
*(p + i) = i; // 越界访问
}
free(p);
}
4.3 对非动态开辟内存使用free释放
void test()
{
int a = 10;
int *p = &a;
free(p); // 错误:不是动态分配的内存
}
4.4 使用free释放一块动态开辟内存的一部分
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
4.5 对同一块动态内存多次释放
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
4.6 动态开辟内存忘记释放(内存泄漏)
//代码1
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}
忘记释放不再使用的动态开辟的空间会造成内存泄漏。
切记:动态开辟的空间一定要释放并且要正确释放。
//代码2
void test()
{
int flag = 1;
int*p = (int*)malloc(100);
if(p == NULL)
{
//···
return 1;
}
//使用
if(flag)
return;
free(p);
p = NULL;
}
int main()
{
test();
//···
return 0;
}
注意:
代码2中虽然写了free函数用于释放内存,但是由于test函数的实现,会在主程序全部执行完一遍后再释放内存,这种情况也会造成内存泄漏。所以在程序的编写中,需要严格思考代码逻辑防止出现这种由于代码逻辑问题出现的内存泄漏现象。
5. 动态内存经典笔试题分析
5.1 题目1:
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;
}
请问运行Test函数会有怎样的结果?
运行结果:①对NULL指针进行解引用操作,程序崩溃。②未对动态内存空间进行释放,内存泄漏。
解释:因为函数的形参是实参的一份拷贝,这里的GetMemory函数是将开辟新空间的地址传给了形参p,但是在Test函数的后续使用中还是对实参进行操作,然而此时的形参中的值还是NULL,所以会造成程序的崩溃。同时申请的内存也没有进行释放操作,会导致内存泄漏。
修改方案:
**方案一:**本质还是将函数之间的值传递,改为地址传递即可。
void GetMemory(char **p) //二级指针接收
{
*p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str); //将str的地址传到GetMemory函数中去
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL; //释放空间
}
int main()
{
Test();
return 0;
}
**方案二:**还可以通过函数返回值的方法,对形参操作后,将形参的值返回给实参已达到操作实参的目的。
char* GetMemory()
{
char* p = (char *)malloc(100);
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL; //释放空间
}
int main()
{
Test();
return 0;
}
5.2 题目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;
}
请问运行Test函数会有怎样的结果?
**运行结果:**乱码(烫烫烫烫烫烫烫···)
解释:按照代码运行顺序,首先由main函数进入,在进入Test函数,Test函数中首先创建指针变量str放入NULL,在进入GetMemory函数,在GetMemory函数中创建局部数组放入hello world,再将数组首元素地址返回至str中,此时str中寄存放着数组局部数组p的首元素地址,内存中确实存在着这块空间,但是在后续代码中是否可以继续使用这块空间就取决于是否有操作这块空间的权限,所以当代码执行完GetMemory函数之后,局部数组p这块空间的使用权限就会被收回,也就是说此时str中的地址尽管可以找到但是没有权限使用,此时的str因为其中的地址已经不属于程序也就成为了野指针。而这块空间中的内容也是没有办法预知的。
上述代码本质是因为字符数组p是临时的,进入函数创建离开函数销毁,这种局部数组是放在栈区的,所以这类问题也叫返回栈空间(临时空间)地址的问题,这种返回栈空间的可以用于返回变量或者一些具体的值,但是不可以返回地址。
**解决方案:**手动开辟空间放入“hello world”,并将动态内存空间首地址返回即可。
char *GetMemory(void)
{
char* p = (char *)malloc(15);
strcpy(p, "hello world");
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
5.3 题目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);
}
int main()
{
Test();
return 0;
}
请问运行Test函数会有怎样的结果?
**运行结果:**可以正常打印“hello”,但是涉及内存泄漏的问题
**解释:**在Test函数中创建指针变量str,并将str的地址传递给GetMemory函数并用二级指针p接收,再在GetMemory函数中对p解引用获取到了str的地址,并在其地址后面开辟了100个内存空间大小的空间,后续再在这块空间放入hello是可以正常放入并打印出来的,但是该代码因为动态开辟的空间并没有释放,所以涉及内存泄漏的问题。
**修改方案:**在打印完hello之后,调用free函数释放内存
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;
}
5.4 题目4:
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;
}
请问运行Test函数会有怎样的结果?请问运行Test函数会有怎样的结果?
**运行结果:**程序崩溃
**解释:**在第8行中对已经释放的空间继续操作,造成非法访问。因为调用玩free函数之后,str已经变成野指针了。
**修改方案:**将free(str);语句转移到程序最后执行,虽然可以解决语法问题但是这种修改方案的逻辑是不自洽的,因为同样是strcpy函数放入字符串,为什么后面放入world需要进行判断。所以这里需要改变的并不是free(str);语句的位置,而是在后面加上str = NULL;即可。
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
str = NULL;
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
6. 柔性数组
C99中,结构体中最后一个元素允许是未知大小的数组,这就叫做【柔性数组】成员
例如:
struct st_type
{
int i;
int a[0];//柔性数组成员 数组大小为0即为未知大小的数组
};
有些编译器会报错无法编译可以改成:
struct st_type
{
int i;
int a[];//柔性数组成员
};
6.1 柔性数组的特点
- 结构中的柔性数组成员前面必须至少有一个其他成员。
sizeof
返回的这种结构大小不包括柔性数组的内存。- 包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存大于结构的大小,以适应柔性数组的预期大小。
例如:
typedef struct S
{
int i;
int arr[0];//柔性数组成员
};
int main()
{
printf("%d\n", sizeof(struct S));//输出的是4
return 0;
}
struct S
{
int n;
int arr[];
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S) + 20*sizeof(int));
//开辟大小84个字节,大于结构体大小可以用于柔性数组内容的分配
//这里的ps既可以访问变量n,还可以访问数组arr
if(ps == NULL)//开辟失败
{
perror("malloc()");
return 1;
}
//开辟成功,正常使用这些空间
return 0;
}
6.2 柔性数组的使用
//代码1
#include <stdio.h>
#include <stdlib.h>
struct S
{
int n;
int arr[];
};
int main()
{
int i = 0;
struct S* ps = (struct S*)malloc(sizeof(struct S) + 100 * sizeof(int));
if(ps == NULL) //判断
{
perror("malloc()");
return 1;
}
//使用这些空间
ps->n = 100;
int i = 0;
for (i = 0; i < 20; i++)
{
ps->arr[i] = i + 1;
}
//调整ps指向空间的大小
struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 40 * sizeof(int));
if (ptr != NULL) //判断
{
ps = ptr;
ptr = NULL; //保证可以继续使用变量ps继续动态空间的维护
}
else//开辟失败
{
return 1;
}
//使用调整后的空间
for (i = 0; i < 40; i++)
{
printf("%d ", ps->arr[i]);
}
//释放空间
free(ps);
ps = NULL;
return 0;
}
这样的柔性数组成员a,相当于获得了100个整型元素的连续空间。
并且后续还可以再利用realloc函数对这块空间继续调整,实现动态变化。
6.3 柔性数字的优势
上述结构也可以设计为下面的结构,也能完成同样的效果。
//代码2
#include <stdio.h>
#include <stdlib.h>
struct S
{
int n;
int* arr;
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S));
if (ps == NULL) //判断
{
perror("malloc");
return 1;
}
int* tmp = (int*)malloc(20*sizeof(int));
if (tmp != NULL)
{
ps->arr = tmp; //保证继续用arr来维护这块空间
}
else
{
return 1;
}
//使用这块空间
ps->n = 100;
int i = 0;
//给arr中的20个元素赋值为1~20
for (i = 0; i < 20; i++)
{
ps->arr[i] = i + 1;
}
//调整空间
tmp = (int*)realloc(ps->arr, 40*sizeof(int));
if (tmp != NULL)
{
ps->arr = tmp; //保证继续用arr来维护这块空间
}
else
{
perror("realloc");
return 1;
}
//使用调整后的空间
for (i = 0; i < 40; i++)
{
printf("%d ", ps->arr[i]);
}
//释放空间
free(ps->arr);//先释放后开辟的
ps->arr = NULL;
free(ps);
ps = NULL;
return 0;
}
上述代码1
和代码2
可以完成同样的功能,但是方法1
的的实现有两个好处:
第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第二个好处是:这样有利于访问速度.
连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,个人觉得也没多高了,反正你跑不了要用做偏移量的加法来寻址)
7. 总结C/C++中程序内存区域划分
C/C++程序内存分配的几个区域:
- 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。(局部变量、形式参数···)
《函数栈帧的创建和销毁》
- 堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS(操作系统)回收。分配方式类似于链表。(动态申请的内存)
- 数据段(静态区):(static)存放全局变量、静态数据。程序结束后由系统释放。(全局变量、静态变量···)
- 代码段:存放函数体(类成员函数和全局函数)的二进别代码。不能被修改。