动态内存管理
-
动态内存分配的意义
-
动态内存函数介绍
- malloc
- free
- calloc
- realloc
-
常见的动态内存错误
-
几个经典的笔试题
-
柔性数组
动态内存分配的意义
我们已经指到的内存开辟的方式:
//开辟四个字节的空间
int a = 5;
//开辟连续的一个字节的十个空间
char arr[10] = {0};
但是这两种方式开辟的空间有两个特点:
- 空间开辟的大小是固定的
- 数组在申明的时候,必须指定数组的长度,它需要的内存在编译时进行分配。
但是对空间的需求,不仅仅是上述的情况,有的时候我们需要多少的空间是不能直接知道的,需要程序运行开始,或者是用户使用的时才能知道,这时数组在编译时开辟空间的方式就不能满足了。
这时候就可以试试动态内存开辟了。
存储空间的使用
动态内存函数介绍
malloc和free
C语言提供的开辟内存的函数:malloc
void* malloc(size_t size);//malloc函数的声明
函数作用:
这个函数向内存申请一块连续可用的空间,并返回这块空间的指针。
1. 如果开辟成功,返回一个指向开辟好空间的指针。
2. 如果开辟失败,则返回一个NULL指针,所以使用malloc,要对返回值进行检查。
3. 返回值类型是void\* ,即malloc函数不知道开辟空间的类型,在具体使用的时候,强制转化一下指针类型。
4. 如果参数size为0,malloc的行为是标准是未定义的,取决于编译器。
开辟空间后不能让他一直存在吧,这样内存就一直存放着。这时我们还有一个free函数用来释放或回收动态开辟的内存。
void free(void* ptr);
free函数用来释放动态开辟的内存
1. 如果参数ptr指向的空间不是动态开辟的,那么free函数的行为是没有定义的。
2. 如果参数ptr是NULL指针,则函数什么事都不做。
malloc和free都声明在头文件stdlib.h
中。
函数使用实例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main(){
//向内存申请10个整型的空间
int* p = (int *) malloc(10* sizeof(int ));//改成INT_MAX测试就可以执行if部分
if (p == NULL){
//打印错误的原因
printf("%s\n", strerror(errno));
}
else{
//正常使用空间
int i = 0;
for (i = 0; i < 10; ++i) {
*(p+i) = i;
}
for (i = 0; i < 10; ++i) {
printf("%d",*(p+i));
}
}
//当动态申请的空间不再使用的时候就应该还给操作系统。
//回收空间
free(p);
//free掉之后仍然p仍然指向那个内存地址。
p = NULL;//这样更加安全。
return 0;
}
calloc
C语言除了malloc还提供了一个函数叫calloc,calloc函数也用来动态内存分配。
void* calloc(size_t num,size_ size);
- 函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0。
- 与函数malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为全0。
具体使用同malloc和free的使用方式。
int main() {
int *p = (int *) calloc(10, sizeof(int));
if (NULL != p) {
//使用空间
}else{
//打印错误信息
printf("%s\n",strerror(errno));
}
free(p);
p = NULL;
return 0;
}
realloc
realloc函数让动态内存管理更加灵活。
有时我们觉得内存申请小了或者大了,就可以使用realloc函数进行调整。
void* realloc (void* ptr,size_t size);
参数解释:
- ptr是要调整的内存地址
- size是调整时候的新的大小
- 返回值为调整后内存起始位置
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
realloc使用注意事项:
- 如果p指向的空间后面有足够的空间可以追加,则直接追加,返回p
- 如果p指向的空间后面没有足够的内存空间可以追加,则realloc函数会重新找一个新的区域开辟一块满足需求的空间,并且把原来内存中的数据拷贝回来,释放旧的空间,最后返回新开辟的内存空间地址。
- 追加失败,会返回空指针。–不能直接赋值到原来的指针上,防止数据丢失。
内存中情况:
int main() {
int *p = (int *) malloc(20);
if (p == NULL) {
printf("%s\n", strerror(errno));
} else {
int i = 0;
for (i = 0; i < 5; i++) {
*(p + i) = i;
}
}
int *ptr = realloc(p, 4000);
if (ptr != NULL) {
int i = 0;
for (i = 5; i < 10; i++) {
*(ptr + i) = i;
}
for (i = 0; i < 10; i++) {
printf("%d\n", *(p + i));
}
}
//释放空间
//-- 如果出现不够用的情况,另起空间ptr原来的空间在realloc的时候就被释放了
free(ptr);
ptr = NULL;
return 0;
}
常见的动态内存错误
对NULL指针的解引用操作
访问空指针,就是非法访问。
在动态内存分配之后一定要进行返回值的判断。
int main(){
int *p = (int*)malloc(40);
//万一malloc失败了,p就被赋值为NULL
*p = 0;
free(p);
return 0;
}
对非动态开辟内存使用free释放空间
程序崩溃
int main() {
int a = 10;
int *p = &a;
free(p);//可以吗?
}
对动态开辟空间的越界访问
程序崩溃
int main() {
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);
}
使用free释放一块动态开辟内存的一部分空间
释放的时候必须从申请空间的起始位置开始释放。
程序崩溃。
所以一般不对这个起始地址的指针p进行修改性质的运算。
int main() {
int *p = (int *) malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
对同一块动态内存多次释放
int main() {
int *p = (int *) malloc(100);
free(p);
free(p);//重复释放
}
避免方式:free完之后,将p置空。
int main() {
int *p = (int *) malloc(100);
free(p);
p = NULL;
free(p);//无效释放,程序不会崩溃。
}
动态开辟内存后没有释放(内存泄漏)
int main() {
while (1){
malloc(1);
}
}
下面的这种情况就是,出了test函数想释放都没法释放了。
void test() {
int *p = (int *) malloc(100);
if (NULL != p) {
*p = 20;
}
}
int main() {
test();
while (1);
}
忘记释放不再使用的动态开辟的空间会造成内存泄漏。
有些语言就有自己的内存回收机制。
申请动态内存或者使用动态内存时出现的错误,大都会导致程序直接崩溃。
free之后,记得将指针置空。
几个经典的笔试题
题目一
测试函数的执行结果是什么。
void GetMemory(char *p) {
p = (char *) malloc(100);
}
void Test(void) {
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
- 运行代码程序会崩溃
- 程序存在内存泄漏的问题
str以值传递的形式给了p。p是GetMemory函数的形参,只能函数内部有效,等GetMemory函数返回之后,动态开辟内存尚未释放并且无法找到,所以会内存泄漏。
void GetMemory(char *p) {
p = (char *) malloc(100);//2. 内存未释放
}
void Test(void) {
char *str = NULL;
GetMemory(str);//1. 这里传的是str的地址,并不是*str的地址。
strcpy(str, "hello world");//3. 程序是在这里崩溃的,非法访问内存
printf(str);//这个printf没有问题
}
int main(){
Test();
return 0;
}
提示:str是一个变量,向函数传变量,在函数运行的时候是变量的临时拷贝,要想对变量操作必须传地址。但是由于str本身就是一个地址,那在函数GetMemory中必须使用二级指针来接受。
这样是可以运行的。不至于崩溃,但是仍然存在内存泄漏的问题。
修改方式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中创建的变量p在结束函数后就被销毁了,之后由函数返回的内存地址发生了什么是不知道的(有点非法访问的意思)。
解决方式:一、因为函数中变量在函数结束后被销毁,那么延长该变量的生命周期就可以了,也就是在函数中定义变量的时候用static修饰一下。(将变量存在静态区)
二、使用动态内存分配进行定义空间,然后不要在函数中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);
}
int main() {
Test();
return 0;
}
程序可以运行出结果,但是程序的问题是内存泄漏,在使用完str后没有将空间释放掉。
修改错误:
void Test(void) {
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf("%s", str);
//代码修改
free(str);
str = NULL;
}
题目四
下面的测试函数的运行结果是什么?
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;
}
可以运行出预想的结果,但是代码仍然存在问题。free之后继续使用之前开辟的空间的地址,就是非法访问。
注:free释放空间后并不会将内容置空。
修改:
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;
}
C程序的内存开辟
(图片来自于视频截图)
内存分配区域:
- 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
- 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
- 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
- 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
通过这个图,我们就可以更好的理解static关键字修饰局部变量的例子了。
实际上一般的局部变量在栈区分配空间,栈区的特点是在上面创建的变量出了作用域就销毁了。
但是被static修饰的变量存放在数据段(静态区),数据段的特点是在这里创建的变量,直到程序结束才销毁。
所以生命周期变长。
柔型数组
柔型数组:在C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做“柔性数组”成员。
举例说明:
// struct S{
// int n;
// int arr[0];
// };
struct S{
int n;
int arr[];//未知大小的柔性数组成员-数组的大小是可以调整的
};
int main(){
struct S s;
}
柔性数组的特点
- 结构中的柔性数组成员前面必须至少一个其他成员
- sizeof返回的这种结构大小是不包括柔性数组的内存。
- 包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔型数组的预期的大小。
struct S{
int n;
int arr[];//未知大小的柔性数组成员-数组的大小是可以调整的
};
int main(){
struct S s;
printf("%d\n", sizeof(s));//结果是4
}
柔性数组的使用
struct S{
int n;
int arr[];//未知大小的柔性数组成员-数组的大小是可以调整的
};
int main(){
//创建空间
struct S* ps = (struct S*)malloc(sizeof(struct S)+5*sizeof(int));
ps->n=100;
int i = 0;
for (i = 0;i<5;i++){
ps->arr[i] = i;
}
//空间不够了继续开辟
struct S* ps2 = realloc(ps,sizeof(struct S)+10*sizeof(int));
if(ps2!=NULL){
ps = ps2;
}
for (i = 5; i < 10; ++i) {
ps->arr[i] = i;
}
for (i = 0; i < 10; ++i) {
printf("%d ",ps->arr[i]);
}
free(ps);
ps = NULL;
return 0;
}
柔性数组的优势
实现上面同样的功能但是不使用柔性数组
struct S {
int n;
int *arr;//这个指针指向一个动态开辟的空间存放数组
};
int main() {
//申请空间,申请到的是一个int和一个指针的空间
struct S *ps = (struct S *) malloc(sizeof(struct S));
ps->arr = malloc(5 * sizeof(int));
int i = 0;
for (i = 0; i < 5; i++) {
ps->arr[i] = i;
}
for (i = 0; i < 5; ++i) {
printf("%d ", ps->arr[i]);
}
// 调整大小
int *ps2 = realloc(ps->arr, 10 * sizeof(int));
if (ps2 != NULL) {
ps->arr = ps2;
}
for (i = 5; i < 10; ++i) {
ps->arr[i] = i;
}
for (i = 0; i < 10; ++i) {
printf("%d ", ps->arr[i]);
}
//释放空间 z
free(ps->arr);
ps->arr = NULL;
free(ps);
ps = NULL;
return 0;
}
上面的代码我认为可以将ps创建为结构体变量,ps.arr创建为动态申请空间的数组变量。
小笔记:指针变量分配动态空间后,其指向的变量可以直接当数组使用。
对比两种实现方式
第一种的好处:方便内存的释放
-
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
-
连续的内存有益于提高访问的速度,也有益于减少内存碎片。
第二种的好处:有利于访问速度
连续的内存有益于提高访问的速度,也有益于减少内存碎片。
小结
柔性数组是结构体中的最后一个元素。(首先有结构体,然后最后一个0长度的数组),并且这个结构体至少有两个成员。
在结构体中柔性数组成员是不指定数组大小的。
使用柔性数组的目的:
- 想操控一块连续的空间,被当成数组使用。
- 内存释放的时候只需要释放一次。