1. 为什么要有动态内存分配
相信同学们通过前面的学习知道,在C语言中有以下的内存开辟⽅式:
比如:①创建一个变量
int a = 10;//在栈空间上申请四个字节的空间
创建一个数组
②一个数组是一块连续的内存空间
int arr[10] = {0};//在栈空间上开辟10个字节的连续空间
但是上述的开辟空间的⽅式有两个特点:
• 空间开辟大小是固定的。
• 数组在申明的时候,必须指定数组的⻓度,数组空间⼀旦确定了⼤⼩不能调整
举一个具体的例子:
int arr[100] = {0};
这里给了我们一个能够存放100个整型的连续内存空间,那么如果我们有50个整型类型的数据要储存,那么就要浪费剩余的内存空间,如果我们有200个整型类型的数据要存储,那么arr数组的空间又不够我们存放数据。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小
在程序运⾏的时候才能知道,那数组的编译时开辟空间的⽅式就不能满⾜了。
于是C语言引⼊了动态内存开辟,让程序员⾃⼰可以申请和释放空间,就⽐较灵活了。
那么本期博客就给同学们学习在动态开辟上所用到的几个函数
2. malloc和free
2.1 malloc
C语⾔提供了⼀个动态内存开辟的函数:
函数原型:malloc - C++ Reference (cplusplus.com)
void* malloc (size_t size);
参数:
size -- 内存块的大小,以字节为单位,类型是无符号整型(size_t)
功能:这个函数向内存申请⼀块连续可⽤的空间,并返回指向这块空间的指针。
对于返回类型也做了介绍:
从函数原型也能看出:返回值的类型是 void* ,
所以malloc函数并不知道开辟空间的类型,具体在使⽤的时候使⽤者⾃⼰来决定。
另外如果malloc函数开辟失败的话,则返回⼀个 NULL 指针。
因此malloc的返回值⼀定要做检查!
另外注意:如果参数 size 为0,malloc的⾏为是标准是未定义的,取决于编译器。
2.2 free
C语言提供了另外⼀个函数free,专门是⽤来做动态内存的释放和回收的,函数原型如下:
free - C++ Reference (cplusplus.com)
void free (void* ptr);//传过去是要释放的空间的起始地址
free函数⽤来释放动态开辟的内存。
• 如果参数 ptr 指向的空间不是动态开辟的,那free函数的⾏为是未定义的。
• 如果参数 ptr 是NULL指针,则函数什么事都不做。
malloc和free都声明在 stdlib.h 头⽂件中。
这里补充一点:
如果malloc函数开辟失败的话,我们特别想知道malloc开辟失败的原因是什么,
我们可以使用库函数 perror ,这个函数会直接打印出malloc开辟失败所对应错误信息。
我们一起看一下这个函数原型:perror - C++ Reference (cplusplus.com)
void perror ( const char * str );
功能:打印错误信息
参数:
str -- 是一个字符串,包含了一个自定义消息,将显示在原本的错误消息之前。
返回值:无返回值
举个例⼦:
我们来看看malloc 开辟空间的具体使用方法:
#include <stdio.h>
#include <stdlib.h>int main()
{//开辟内存int* ptr = (int*)malloc(40);int* p = ptr;if (p == NULL){perror("malloc");return 1;}//内存操作int i = 0;for (i = 0; i < 10; i++){*(p + i) = i;}//打印for (i = 0; i < 10; i++){printf("%d ", *(p + i));}//内存释放free(ptr);ptr = NULL;return 0;
}
代码解释如下:
#include <stdio.h>
#include <stdlib.h>int main()
{int* ptr = (int*)malloc(40);//malloc 函数会返回所申请的内存块的起始地址,返回类型为void*//将其强制类型转换为int*意思是所申请的这块空间之后将以整数对其进行访问操作int* p = ptr;//因为malloc要返回申请空间的起始地址,所以通常不直接对ptr操作,而是另外创建//一个指针存放ptr,后续对p进行操作if (p == NULL){//malloc函数在申请内存空间时,若空间大小不够,会返回一个空指针//此时内存空间就会申请失败perror("malloc:");//打印程序错误信息,双引号内的内容由自己定//这里perror函数刚好可以验证空间是否申请成功return 1;//主函数中return 1表示程序异常结束}//代码走到这里,说明malloc成功开辟空间,我们在空间里赋值,//通过循环将 0 到 9 依次存储到分配的内存空间中。//这里使用了我们学习过指针偏移的方式 *(p + i) 来进行赋值。int i = 0;for (i = 0; i < 10; i++)//申请的空间大小为40个字节,转换为整形即10个整形{*(p + i) = i;}//同样通过指针偏移的方式读取并打印出存储在内存中的值。for (i = 0; i < 10; i++){printf("%d ", *(p + i));}free(ptr);//申请的这块内存空间使用结束后,需要释放这片空间,free函数的参数为申请空间的起始地址ptr = NULL;//空间释放后,ptr仍然指向空间内的一个有效地址,为避免后续对其使用造成野指针的情况return 0;
}
运行结果:
3. calloc
C语⾔还提供了⼀个函数叫 calloc , calloc 函数也⽤来动态内存分配。原型如下:
calloc - C++ Reference (cplusplus.com)
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));//calloc函数与malloc一样都是用于申请内存空间的,不同之处在于,calloc会对申请的空间进行初始化//申请的空间会被全部初始化为0,其余地方的使用与malloc一样if (p == NULL){perror("calloc:");return 1;}for (int i = 0; i < 10; i++){printf("%d ", *(p + i));}free(p);p = NULL;return 0;
}
运行结果:
4.realloc(内存扩容)
realloc函数的出现让动态内存管理更加灵活。
有时我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整。realloc 函数就可以做到对动态开辟内存大小的调整。
函数原型如下:
realloc - C++ Reference (cplusplus.com)
void* realloc (void* ptr, size_t size);
• ptr 是要调整的内存空间的起始地址
• size 调整之后新⼤⼩
• 返回值为调整之后的内存空间的起始位置。
• 这个函数调整原内存空间⼤⼩的基础上,还会将原来内存中的数据移动到新的空间。realloc函数在调整空间时存在两种情况:
- 情况一:原有空间后有足够大的空间
- 情况二:原有空间后没有足够大的空间
情况1
当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发⽣变化。
情况2
当是情况2 的时候,原有空间之后没有⾜够多的空间时,扩展的⽅法是:在堆空间上另找⼀个合适⼤⼩的连续空间来使⽤。这样函数返回的是⼀个新的内存地址。
由于上述的两种情况,因此我们在使用realloc函数时需要多加注意。
举个例子:
#include<stdio.h>
#include<stdlib.h>int main()
{int* p = (int*)malloc(40);if (p == NULL){perror("malloc:");return 1;}int i = 0;for (i = 0; i < 10; i++){*(p + i) = i;}//空间不够,希望能放20个元素,考虑扩容int* ptr = (int*)realloc(p, 80);//realloc函数为内存扩容函数//因为realloc也会扩容失败,当扩容失败时,realloc会返回一个空指针//如果直接将realloc的返回值给之前开辟空间时的p,那么当返回值为NULL,即扩容失败时,//这时候p被赋值为空指针,原来那片开辟的空间就会丢失,造成内存泄漏if (ptr != NULL){p = ptr;}//扩容成功,开始使用//不再使用,就释放free(p);p = NULL;return 0;
}
5. 常⻅的动态内存的错误
5.1 对NULL指针的解引⽤操作
举个例子:
大家看看下面这个代码,大家认为这个程序有没有问题?
#include <stdio.h>
#include <stdlib.h>int main()
{int* p = (int*)malloc(INT_MAX);*p = 20;free(p);
}
当我们调试发现,程序会崩溃。
原因是这样的:
首先,使用
malloc(INT_MAX)
来分配内存是一种不太常见且不太安全的做法。INT_MAX
通常是一个非常大的值,可能会超出系统的可用内存,导致分配失败。在执行
*p = 20;
这一行时,如果p
的值是NULL
(即内存分配失败),那么这将导致未定义的行为,可能会引发程序崩溃、数据损坏或其他难以预测的错误。例如,如果系统内存不足,
malloc
可能无法成功分配这么大的内存空间,此时p
就会是NULL
。当尝试对NULL
指针进行解引用并赋值时,就像上述代码中那样,很可能导致程序异常终止。另外,即使内存分配成功,在使用完后通过
free(p);
释放了分配的内存,以避免内存泄漏。但要确保在释放之前对内存的操作都是合法和正确的,避免因错误的操作导致程序出现问题。总之,在实际编程中,应根据实际需求合理地分配内存大小,并妥善处理内存分配可能失败的情况。
5.2 对动态开辟空间的越界访问
void test()
{int* p = (int*)malloc(10 * sizeof(int));if (p != NULL){for (int i = 0; i <= 10; i++){*(p + i) = i + 1; //当i是10的时候越界访问}}free(p);p = NULL;
}
这段代码存在问题。
在循环中:
for (int i = 0; i <= 10; i++) {*(p + i) = i + 1; }
由于分配的内存大小为
10 * sizeof(int)
,所以有效的索引范围应该是0
到9
。当i
等于10
时,对*(p + 10)
进行赋值会导致越界访问。
5.3 对非动态开辟内存使用free释放
void test()
{int a = 10;int *p = &a;free(p);//ok?
}
在这段代码中,
free(p)
是不正确的操作。
p
指向的是一个普通的局部变量a
,而不是通过malloc
、calloc
或realloc
等动态内存分配函数分配的内存空间。对非动态分配的内存使用
free
会导致未定义的行为,可能会使程序崩溃或产生不可预测的结果。例如,程序可能会在执行这一行时突然终止,或者后续的代码执行出现异常。
正确的做法是只对通过动态内存分配获取的指针使用
free
进行内存释放。
5.4 使⽤free释放⼀块动态开辟内存的⼀部分
int main()
{int* p = (int*)malloc(100); // 申请100个字节大小的空间if (p == NULL){return 1;}for (int i = 0; i < 10; i++){*p = i + 1;p++; }free(p);p = NULL;return 0;
}
这段代码存在错误。
在循环中使用
p++
会导致p
不再指向最初动态分配的内存的起始位置。当执行free(p)
时,由于p
已经不再指向正确的起始位置,这将导致未定义的行为,可能会引发程序错误甚至崩溃。
5.5 对同⼀块动态内存多次释放
void test()
{int *p = (int *)malloc(100);free(p);free(p);
}
这段代码存在错误。
在
test
函数中,对已经释放的内存指针p
再次调用free
函数,这是不合法的操作。当第一次调用
free(p)
时,所分配的内存已经被归还给系统,p
所指向的内存不再有效。再次调用free(p)
会导致未定义的行为,可能会使程序崩溃或产生不可预期的结果。例如,可能会导致内存管理的混乱,影响到其他正在使用的内存区域,或者在某些情况下导致程序直接异常终止。
为了避免这种错误,应该确保对同一个动态分配的内存指针只调用一次
free
函数。
如下面这个代码:
void test()
{int *p = (int *)malloc(100);free(p);free(p);
}
这段代码存在错误。
在
test
函数中,对已经释放的内存指针p
再次调用free
函数,这是不合法的操作。当第一次调用
free(p)
时,所分配的内存已经被归还给系统,p
所指向的内存不再有效。再次调用free(p)
会导致未定义的行为,可能会使程序崩溃或产生不可预期的结果。例如,可能会导致内存管理的混乱,影响到其他正在使用的内存区域,或者在某些情况下导致程序直接异常终止。
为了避免这种错误,应该确保对同一个动态分配的内存指针只调用一次
free
函数。
5.6 动态开辟内存忘记释放(内存泄漏)
void test()
{int *p = (int *)malloc(100);if(NULL != p){*p = 20;}
} int main()
{test();while(1);
}
这段代码存在潜在的问题。
在
test
函数中,虽然分配了 100 个字节的内存,但并没有指定这 100 个字节如何使用。直接使用
*p = 20;
只是给p
所指向的内存的第一个int
大小的位置赋值为 20 。如果后续的代码期望按照特定的方式使用这 100 个字节,例如将其当作一个包含多个
int
元素的数组,那么这种简单的赋值可能无法满足需求。此外,如果在后续的代码中不再对这块内存进行其他操作,并且在程序结束时也没有使用
free(p)
释放内存,就会导致内存泄漏。
⚠️ 忘记释放不再使⽤的动态开辟的空间会造成内存泄漏。
切记:动态开辟的空间⼀定要释放,并且正确释放。
6. 动态内存经典笔试题分析
6.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 函数会有什么样的结果?
我们运行结果发现,最终程序崩溃了
我们分析一下这个代码究竟出现了什么问题了
- 内存非法访问:我们知道传值调用时,形参只是实参的临时拷贝,对形参的改变无法影响实参,这时str仍是空指针,而strcpy拷贝会对空指针进行解引用操作,对NULL指针解引用会出错!
2. 内存泄漏:在GetMemory()函数内部动态申请了100字节的空间,因为p随着函数结束而被销毁,所以已经再也找不到该空间,会造成内存泄漏。
改正方法:
- 我们要想改变str就需要传址调用,而str本身就是个指针变量,传指针变量的地址需要二级指针来接收。
- 使用完之后必须释放内存。
void GetMemory(char** p)
{*p = (char*)malloc(100);
}void Test(void)
{char* str = NULL;GetMemory(&str);strcpy(str, "hello world");printf(str);// 释放free(str);str = NULL;
}
6.2 题目二
#include <stdio.h>
#include <stdlib.h>char* GetMemory()
{char p[] = "hello world";return p;
}void test()
{char* str = NULL;str = GetMemory();printf(str);
}int main()
{test();return 0;
}
大家可以思考一下,上述代码有没有问题?
运行结果,发现打印的是这样乱码的结果
同学们会感到很疑惑,奇怪,为什么是这样的结果呢?
别急,我们来分析一下:
针对上述问题,我们可以对代码进行如下修改:
① 返回 p ,让 str 接收:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>// 修改返回类型为char*
char* GetMemory(char *p)
{p = (char*)malloc(100);return p; // 将p带回来
}void Test()
{char *str = NULL;str = GetMemory(str); // 用str接收,此时str指向刚才开辟的空间strcpy(str, "hello world"); // 此时copy就没有问题了printf(str);// 用完之后记得free,就可以解决内存泄露问题free(str);str = NULL; // 还要将str置为空指针
}int main()
{Test();return 0;
}
② 将值传递改为址传递:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>// 用char**接收
void GetMemory(char** p)
{*p = (char*)malloc(100);
}void Test()
{char* str = NULL;GetMemory(&str); // 把str地址传递strcpy(str, "hello world");printf(str);// 记得free释放,就可以避免内存泄露问题free(str);str = NULL; // 还要将str置为空指针
}int main()
{Test();return 0;
}
经过我们的修改,我们运行代码,hello world 就打印出来了
6.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进行传址调用,在GetMemory函数里面申请100个字节大小的空间,将hello拷贝到str所指向的空间中,但是使用之后并没有使用free函数进行释放,导致内存泄漏。
解决:申请的空间使用完之后要使用free函数进行释放,并将str置为空指针。
6.4 题⽬4:
void Test(void)
{char *str = (char *) malloc(100);strcpy(str, "hello");free(str);if(str != NULL){strcpy(str, "world");printf(str);}
}
请问运⾏Test 函数会有什么样的结果?
分析:Test函数里面str申请了100个字节的空间,将hello拷贝到str所指向的空间中,就直接用free释放掉了,导致str成了野指针,之前将hello拷贝到str中,所以str一定不是空指针,
因为 free(str); 之后 str 成为野指针, if(str != NULL) 语句不起作⽤。
6. 柔性数组
同学们可能从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。
C99 中,结构中的最后⼀个元素允许是未知⼤⼩的数组,这就叫做『柔性数组』成员。
例如:
typedef struct st_type
{int i;int a[0];//柔性数组成员
}type_a;
有些编译器会报错⽆法编译可以改成:
typedef struct st_type
{int i;int a[];//柔性数组成员
}type_a;
6.1 柔性数组的特点:
• 结构中的柔性数组成员前⾯必须⾄少⼀个其他成员。
• sizeof 返回的这种结构大小不包括柔性数组的内存。
• 包含柔性数组成员的结构⽤malloc ()函数进⾏内存的动态分配,
并且分配的内存应该⼤于结构的⼤⼩,以适应柔性数组的预期⼤⼩。
⚠️ps:除了malloc函数,realloc、calloc等动态内存开辟的函数也需要类似的操作
例如:
typedef struct st_type
{int i;int a[0];//柔性数组成员
}type_a;int main()
{printf("%d\n", sizeof(type_a));return 0;
}
输出结果是4
说明:sizeof 返回的这种结构大小不包括柔性数组的大小
6.2 柔性数组的使⽤
比如说我现在要数组a里面有10个元素,现在进行malloc一下
示例如下:
#include<string.h>
#include<errno.h>
struct st_type
{int i;//4字节int a[0];//柔性数组成员,也可以写int a[];
};
int main()
{//假设我现在需要a里有10个元素struct st_type*ps=(struct st_type*)malloc(sizeof(struct st_type) + 10 * sizeof(int));if (ps == NULL)//由于空间可能不够开辟导致malloc开辟失败,开辟失败会返回空指针{printf("%s\n", strerror(errno));return -1;//程序出问题后,跳出程序}//开辟成功int j = 0;for (j = 0;j < 10;j++){ps->a[j] = j;}for (j = 0;j < 10;j++){printf("%d ", ps->a[j]);//打印0-9}printf("\n");//如果想继续用柔性数组a进行打印//比如现在a里只有10个元素,我用完10个了,我还要继续来10个,用realloc追加struct st_type*ptr=realloc(ps, sizeof(struct st_type) + 20 * sizeof(int));//ps:realloc第二个参数是调整后的整体大小if (ptr == NULL){printf("扩容失败\n");return -1;}else{ps = ptr;}//扩容成功int k = 0;for (k = 10;k < 20;k++){ps->a[k] = k;}for (j = 0;j < 20;j++){printf("%d ", ps->a[j]);//打印0-19}//释放空间free(ps);ps = NULL;return 0;
}
我们这里需要数组a里有10个元素,那我们malloc的时候要对结构体里的整形i先开辟4个字节,然后为整形数组a再开辟40个字节,然后malloc函数返回开辟空间的起始地址,赋给truct st_type * 类型的ps指针。
malloc(sizeof(struct st_type) + 10 * sizeof(int))这个操作等价于struct st_type类型创建一个变量所占空间,只不过是用malloc来开辟
你改变数组a大小,追加空间时,realloc(ps, sizeof(struct st_type) + 20 * sizeof(int)),realloc的第一个参数仍然是ps,因为你当时是用malloc一次开辟出的一块空间,你是不能单独调整数组a的空间的
6.3 柔性数组的优点
柔性数组就是对一块空间实现动态开辟嘛,那我们之前也讲过指针来动态内存开辟,我们来看一段代码来对比一下这两种方法:
//用指针也可以做到a指向的空间动态变化
struct st_type
{int i;//4字节int *a;//4字节,这里计算结构体大小恰好是8字节
};
int main()
{struct st_type*ps = (struct st_type*)malloc(sizeof(struct st_type));ps->i = 100;ps->a = (int*)malloc(10 * sizeof(int));//a指向40个字节的空间,该空间由int*进行管理int j = 0;for (j = 0;j < 10;j++){ps->a[j] = j;//a[j]=*(a+j)}for (j = 0;j < 10;j++){printf("%d", ps->a[j]);}//a指向的空间不够了,希望调整大小int *ptr = (int*)realloc(ps->a, 20 * sizeof(int));if (ptr == NULL){printf("扩容失败");return -1;}else{ps->a = ptr;}//使用...//释放free(ps->a);ps->a = NULL;free(ps);ps = NULL;
}
这里需要注意的是,在释放空间时,你要先释放指针a指向的空间,然后释放结构体指针
如上图,我们结构体指针ps开辟一块空间,空间里存放整形i和整形指针a,a又malloc(后续如果需要还可以realloc追加)一块空间,如果你先释放掉ps,a就没了,你就没法找到a指向的那块空间了。
这里对比柔性数组,柔性数组和上述的指针都可以实现一块空间大小的调整,
但是柔性数组有两个好处:
第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。以上,如果我把结构体的内存及其成员要的内存一次性分配好,并返回给用户一个结构体指针,用户做一次free就可以把所有内存都释放掉,并且不用考虑前面说的释放的顺序。
第二个好处是:加快访问速度
连续的内存有益于提高访问速度,也有益于减少内存碎片。
ps:内存碎片如下图
橙色部分表示malloc开辟的空间
操作系统给我们一块内存,我们在进行malloc时,不一定就是一块连着一块的,
上图的空白部分就是内存碎片,有些类似我们在生活裁剪布料时,剪下来的一些剩余的边角料一样
扩展阅读:同学们有兴趣了解柔性数组的更多内容,可以阅读下面这篇文章~
C语言结构体里的成员数组和指针 | 酷 壳 - CoolShell
7. 总结C/C++中程序内存区域划分
C/C++程序,对于内存分配了如下几个区域:
这里我们简单了解一下:
1. 栈区(stack):在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内存容量有限。 栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。
2. 堆区(heap):⼀般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配⽅式类似于链表。
3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
4. 代码段:存放函数体(类成员函数和全局函数)的⼆进制代码
补充阅读:陈浩大佬在14年发布的一篇关于成员数组的博客 C语言结构体里的成员数组和指针
8. 总结
内存管理是一项非常重要的任务。动态内存管理是指在程序运行时分配和释放内存的过程。通过动态内存管理,我们可以根据需要分配适当的内存空间,并在不再需要时释放它。这使得程序更加灵活,并能够处理各种大小和形状的数据。
以上就是动态内存管理的所有内容了~~~
如果对你的学习有所帮助,别忘了收藏和点赞,有疑问随时可以在评论区骚扰我呦~