为什么要有动态内存分配
我们之前已经学过了为变量开辟内存的方法了,比如:
int a = 0;
int arr[12]={0";
在编程中,首先来看两个场景:
第一种情况:初始化阶段,我们声明了一个整型变量a,并为之分配了4个字节的内存空间。这一步操作是在编译时期就确定了所需内存的大小。
第二种情况:同样在初始化时,我们定义了一个长度为12的整型数组arr,系统会一次性为其分配48个字节的连续内存空间,其大小也是在编译期间就能明确得知。
然而,在实际开发中,有时候我们会遇到这样的问题:对于某些数据结构或变量,我们无法预先知道到底需要多大的存储空间,或者在程序运行过程中,它们所需的内存空间可能会不断变化和增长。在这种情况下,静态地预先分配固定大小的内存空间显然无法满足需求。
这就引出了动态内存管理的概念。动态内存管理允许我们在程序运行时,根据实际需要动态地申请、分配和释放内存空间。通过使用诸如malloc、calloc、realloc等函数,我们可以灵活地管理内存,从而适应那些对内存大小有动态需求的数据结构和变量。这样不仅提高了内存的利用率,也极大地增强了程序的灵活性与扩展性。
malloc和free
malloc

这是C语言提供的一个动态内存分配的函数malloc,它可以对内存进行动态分配,参数部分为需要申请的空间大小,类型为size_t,
使用这个函数时,会申请一块新的内存空间,并返回这块内存空间的起始地址
若开辟成功,则返回该内存空间的起始地址
若开辟失败,则返回NULL,因此在使用malloc的时候一定要进行检查
如果size值为0,此时malloc是未定义的,取决于编译器
返回值的类型为void*,这就意味着我们在使用的时候需要根据具体的情况进行类型转换
free
free是用来释放内存空间的函数

在动态内存管理中,当一块先前通过malloc、calloc等函数申请的内存空间不再需要时,可以通过free函数来释放并回收这块内存。这样做的目的在于有效地管理和利用系统资源,确保有足够的内存空间供后续的内存分配请求使用。
针对上述机制,若ptr是一个指向先前已分配内存区域的指针,正确调用free(ptr)将会释放ptr所指向的那部分内存。但如果ptr并未指向有效且已分配的内存区域,那么执行free(ptr)将会导致未定义行为的发生,这意味着程序可能产生不可预测的结果,甚至可能导致程序崩溃。
另外,当ptr的值为NULL(即空指针)时,调用free(ptr)函数是安全的,此时free函数不会进行任何实质性的操作,仅会简单地返回,不做任何内存释放处理。
最后,请注意free(ptr)函数并不会改变ptr指针自身的值,即使释放了它所指向的内存块,ptr仍会保持原样,继续指向原来的内存地址。为了避免潜在的问题,实践中通常建议在释放内存后将ptr设为NULL,以示该指针不再指向有效的内存区域。
例子
动态内存函数都包含在stdlib.h这个头文件里,下面是一个具体的例子
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
int main()
{
int num = 0;
scanf("%d", &num);
int* ptr = NULL;//定义一个ptr变量并赋值为空指针
// 动态为ptr指针分配可以存储num个整数的内存空间
ptr = (int*)malloc(num * sizeof(int));
//判断是否分配成功
if (ptr != NULL)
{
// 若内存分配成功,则初始化ptr指向的内存区域为零
for (int i = 0; i < num; i++)
{
*(ptr + i) = 0;
printf("%d ", *(ptr+i));
}
}
//释放ptr所指向的空间
free(ptr);
// 为了防止野指针错误,将ptr重新赋值为空指针
ptr = NULL;
return 0;
}
calloc和realloc
calloc
除了malloc之外,C语言还提供了另外一个用于动态内存分配的函数calloc

从官方的定义来看该函数的作用是分配内存并将申请的内存空间全部换成0
和malloc一样,该函数可以让用户自定义分配的空间的类型
下面通过一个例子来看这个函数的使用:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
int main()
{
//需要赋值的个数
int num = 0;
scanf("%d", &num);
//定义一个ptr指针,类型为int,置为空指针
int* ptr = NULL;
//为ptr指针分配内存
ptr = (int*)calloc(num, sizeof(int));
//判断是否内存分配成功
if (ptr != NULL)
{
//若分配成功则循环打印ptr的内容
for (int i = 0; i < num; i++)
{
printf("%d ", *(ptr + i));
}
}
//释放和回收ptr指针
free(ptr);
//为了防止野指针的出现,将ptr置为空指针
ptr = NULL;
return 0;
}
realloc
realloc的出现让动态内存管理更加灵活
有的时候我们觉得申请的空间太小了,不够用;有的时候我们觉得申请的空间太大了,用不完,此时我们就可以使用realloc函数来对已有的空间进行重新分配

改变由ptr指向的内存空间大小
提到重新分配内存,这里就会出现两种情况
1.后续的空间足以存放新申请的空间

2.后续的空间不足以存放新申请的空间

在动态内存管理中,当我们需要调整已分配内存区域的大小以容纳更多或更少的数据时,`realloc`函数便发挥了关键作用。当现有内存区域不足以存放新增申请的空间时,`realloc`函数不仅能够尝试获取一块更大的连续内存空间,而且更重要的是,它具备自动迁移数据的能力。也就是说,`realloc`在找到一块满足新大小要求的新内存区域之后,会将原有内存区域中的所有数据完整无损地复制到新分配的空间内,这一过程确保了程序状态的延续性和完整性。
简而言之,`realloc`函数提供了一种便捷而安全的方式来调整动态分配内存的大小,无论增大或减小,都能妥善处理原有数据,减轻了程序员手动迁移数据的负担,极大地提升了内存管理的灵活性和效率。同时,值得注意的是,尽管`realloc`在大多数情况下能很好地完成内存重分配和数据迁移,但依然存在失败的可能性(如系统内存不足),因此在使用后应验证其返回值,确保内存重分配操作的成功执行。
使用realloc的注意事项
鉴于上述两种情况的存在,我们在使用realloc函数的时候就要注意考虑是否能够分配成功的问题
下面是一个例子:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
int main()
{
int arr[3] = { 1,2,3 };
int* ptr;
ptr = &arr;
ptr = (int*)realloc((int*)ptr, sizeof(int) * 5);
if (ptr != NULL)
{
//业务处理
}
else
{
return 1;
}
//定义一个新的指针暂时存放ptr的内容
int* p = NULL;
p = realloc(ptr, sizeof(int) * 5);
//如果开辟成功,则将p的值赋给ptr
if (p != NULL)
{
ptr = p;
}
free(ptr);
free(p);
return 0;
}
常见的动态内存错误
对NULL指针的解引用操作
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
对动态开辟空间的越界访问
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* ptr = NULL;
ptr = (int*)malloc(sizeof(int) * 10);
if (ptr != NULL)
{
for (int i = 0; i < 11; i++)
{
*(ptr + i) = i;//当i=10的时候就越界访问了,因为开辟的内存空间只有10个int,而这里有了11个
}
}
return 0;
}
对非动态开辟的内存使用free
int main()
{
int a = 0;
int* ptr = &a;
free(ptr);//没有对ptr进行动态内存开辟,但对其使用了free函数
ptr = NULL;
return 0;
}
使用free释放动态开辟内存的某一部分
int main()
{
int* ptr = NULL;
ptr = (int*)malloc(sizeof(int) * 5);
if (ptr != 0)
{
for (int i = 0; i < 5; i++)
{
*(ptr + i) = 0;
ptr++;//此时ptr指针向后移动,指向的空间不再是之前完整的空间
}
}
free(ptr);
ptr = NULL;
return 0;
}
对一块动态内存多次释放
int main()
{
int a = 0;
int* ptr = &a;
free(ptr);//对ptr指针多次释放
free(ptr);
ptr = NULL;
return 0;
}
动态内存开辟忘记释放
int main()
{
int a = 0;
int* ptr = &a;
//忘记释放ptr指针
ptr = NULL;
return 0;
}
内存泄漏
内存泄漏是指在计算机程序运行过程中,程序动态分配了一块内存,但在使用完毕后没有释放回系统,使得这部分内存再也无法被程序重新利用,随着程序运行时间的增长,未释放的内存累积起来,最终可能导致系统可用内存越来越少,直至耗尽,严重影响程序乃至整个系统的性能和稳定性。
#include <stdio.h>
#include <stdlib.h>
void createLeak() {
int* data = (int*)malloc(sizeof(int)); // 分配一个int类型的内存空间
*data = 42; // 假设给分配的内存赋值
// 忘记释放内存
// free(data);
}
int main() {
while (true) { // 循环模拟长时间运行
createLeak();
}
return 0;
}
内存泄露的影响
-
资源浪费:程序占用的内存逐渐增多,导致系统可用内存减少,即使这些内存对程序本身已无用处。
-
性能下降:随着可用内存减少,系统可能频繁触发内存交换(Swap),将内存数据写入硬盘交换文件,大大降低程序和系统的运行速度。
-
程序异常:严重的内存泄漏最终可能导致程序因为无法再分配新的内存而崩溃,或者操作系统由于内存压力过大而导致响应缓慢或系统不稳定。
-
服务中断:对于长期运行的服务程序来说,内存泄漏可能引发服务意外终止,必须重启才能恢复,影响用户体验和服务质量。
-
难以调试:内存泄漏通常不会立即表现出明显的问题,而是在运行一段时间后才显现,这使得定位和修复内存泄漏成为一项具有挑战性的任务。
动态内存经典笔试题分析
第一题
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
这段代码的运行结果是未定义行为,很可能会导致程序崩溃。原因在于,`GetMemory`函数虽然为字符指针`p`分配了100个字节的内存,但是这个分配的内存地址并没有传递回主函数`Test`中`str`变量。
在`GetMemory`函数内部,`p`被重新指向了`malloc`分配的内存地址,但原始的`str`变量(在`Test`函数中)并没有被更新,它仍然保持着`NULL`值。接下来,在`strcpy`函数试图将字符串"hello world"复制到`str`指向的内存位置时,由于`str`依然是`NULL`,程序会试图向地址为0的内存区域写入数据,这是典型的未定义行为,可能导致段错误(Segmentation Fault)或其他不可预知的后果。正确的做法是让`GetMemory`函数返回分配的内存地址,或者直接在`Test`函数中进行内存分配。
因此我们可以做如下的修改
void GetMemory(char *p)
{
p = (char *)malloc(100);
return p;
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
将p的地址返回就可以了
第二题
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
这段代码的错误在于当GetMemory函数结束之后,p的作用域也结束了,因此p被回收,无法将地址返回给str
第三题
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
这段代码的逻辑确实是可以运作的,它是通过以下步骤实现动态内存分配并确保字符串正确存储的:
首先,在Test函数中声明一个指向字符的指针str并初始化为NULL。
随后,Test函数调用GetMemory函数并将str的地址(即一个指向字符指针的指针,亦称为二级指针)作为参数传递进去。在GetMemory函数内部,接受到的二级指针p被用来间接访问str变量,通过解引用*p为str动态分配了指定大小(此处为100字节)的内存空间。
一旦内存成功分配,str将在Test函数外部被正确初始化,于是后续的strcpy函数可将字符串"hello"复制到str指向的内存区域,最后通过printf函数输出字符串内容
第四题
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
柔性数组
什么是柔性数组
也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。
C99 中,结构中的最后⼀个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
例如:
struct A
{
int i;
int a[];
};
数组a就是一个柔性数组,因为我们不知道数组a的具体大小是多少
柔性数组的特点
1.结构体中的柔性数组前面必须包含至少一个其他成员
2.sizeof返回包含柔性数组的结构体的大小时不包含柔性数组的大小,因为大小未知
3.包含柔性数组的结构体再对柔性数组使用malloc进行动态内存分配的时候,分配的大小应该大于结构体其他成员的大小以适应柔性数组的预期大小
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
int main()
{
printf("%d\n", sizeof(type_a));//输出的是4
return 0;
}
柔性数组的使用
#include <stdio.h>
#include <stdlib.h>
int main()
{
int i = 0;
type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
//业务处理
p->i = 100;
for(i=0; i<100; i++)
{
p->a[i] = i;
}
free(p);
return 0;
}
总结
C/C++程序内存分配的⼏个区域:
1. 栈区(stack):在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时 这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内 存容量有限。 栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。
2. 堆区(heap):⼀般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配⽅ 式类似于链表。
3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。 4. 代码段:存放函数体(类成员函数和全局函数)的⼆进制代码。


414

被折叠的 条评论
为什么被折叠?



