目录
1、动态内存分配的重要性
1.0 动态内存管理的概念
动态内存管理,也称为动态内存开辟,是程序在运行时根据需要动态地分配和释放内存空间的过程。
在C语言中,动态内存分配通常通过系统提供的库函数来实现,如malloc、calloc和realloc等; free函数用于释放之前通过malloc、calloc或realloc分配的内存空间。需要注意的是,被释放的内存空间的值将变得不确定,且不应再被访问。
1.1 为何需要动态内存分配
已知:
int a = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
从上面例子看出,上述的开辟空间的方式有两个特点:
• 空间开辟大小固定。
• 数组在申明时必须指定数组的长度,数组空间一旦确定了大小不能调整。
所以C语言引入了动态内存开辟,让程序员自己可以申请和释放空间,就比较灵活。
2、malloc和free
2.0 malloc
函数原型如下:
void* malloc (size_t size);
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
• 如果开辟成功,则返回一个指向开辟好空间的指针。
• 如果开辟失败,则返回一个 NULL 指针,因此malloc的返回值一定要做检查。
• 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
• 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
比如:我们想向内存申请10个整型内存空间,由于我们知道申请空间的数据是整型,所以我们将malloc的返回值进行了强制类型转换,本来,返回值应是void * 类型,但是我们将其强转为了int *
2.0.0 malloc 申请的空间和数组的空间有什么区别
它们在内存分配方式、开辟空间的位置、内存管理和灵活性等方面有所区别。
内存分配方式:• malloc函数动态地在堆(heap)上分配内存。它用于申请一块连续的指定大小的内存块区域,并以void*类型返回分配的内存区域地址。如果分配成功,则返回指向被分配内存的指针;如果分配失败,则返回空指针NULL。
• 数组通常是在栈(stack)上分配的(对于局部数组而言),其大小在编译时就已确定,并且是连续分配的。全局数组或静态数组则是在程序的静态存储区分配空间,与程序的寿命一样长。
灵活性:动态内存大小是可以调整的,更灵活。
2.1 free
当不再需要某个内存空间时,程序应该通过动态内存释放将该空间归还给系统,以便其他部分可以使用。free函数就是专门是用来做动态内存的释放和回收的。
函数原型如下:
void free (void* ptr);
free函数用来释放动态开辟的内存。
• 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
• 如果参数 ptr 是NULL指针,则函数什么事都不做。
⛳总代码:
注意:我们最后通过代码手动释放内存,如果不释放的话,程序结束后也会被操作系统自动回收,但这不是好习惯。而且如果后面还有代码(程序没有结束)的话,可能会造成空间浪费。
#include<stdio.h>
#include<stdlib.h>
int main()
{
// 申请10 个内存你空间
int* p = (int*)malloc(10*sizeof(int));
if (p == NULL)
{
// 空间申请失败
perror("malloc");
return 1;
}
// 申请成功,可以使用这40个字节空间
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i + 1;
}
// 释放
free(p);
p = NULL;
return 0;
}
3. calloc和realloc
3.0 calloc
calloc 函数也用来动态内存分配。
函数原型如下:
void* calloc (size_t num, size_t size);
• 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
• 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全 0。
换句话说,如果您需要一块已清零的内存空间来存储num个大小为size字节的元素,那么使用calloc 是一个方便且高效的选择,因为它同时完成了内存分配和初始化两个步骤。
⛳例子:
int main()
{
// 申请10 个内存空间
/*int* p = (int*)malloc(10 * sizeof(int));*/
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
// 空间申请失败
perror("calloc");
return 1;
}
// 申请成功,可以使用这40个字节空间
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ",p[i]);// *(p+i)
}
free(p);
p = NULL;
return 0;
}
运行结果:
3.1 realloc
• realloc函数的出现让动态内存管理更加灵活。
• 为了更加灵活地管理动态分配的内存大小,当发现原先分配的空间不再满足需求(无论是太小还是过大)时,那 realloc 函数就可以做到对动态开辟内存大小的调整。
函数原型如下:
void* realloc (void* ptr, size_t size);
• ptr 是要调整的内存地址,size 是调整之后的新大小
• 返回值为调整之后的内存起始位置。
• 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
• realloc在调整内存空间的是存在两种情况:
◦ 情况1:原有空间之后有足够大的空间
◦ 情况2:原有空间之后没有足够大的空间
当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
当是情况2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:
(1)realloc 函数直接在内存堆区找一块新的满足大小的空间。
(2)将旧的数据拷贝到新的空间。
(3)释放旧的空间。
(4)返回新的地址。
⛳代码示例:
int main()
{
// 申请10个整型空间
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
perror("calloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ",p[i]);// *(p+i)
}
// 调整空间,希望变为20个整型空间
// 先将realloc函数的返回值放在ptr中,不为NULL,在放回p中
int* ptr = (int*)realloc(p, 20 * sizeof(int));
if (ptr != NULL)
{
p = ptr;
}
// 使用
//...
//释放
free(p);
p = NULL;
return 0;
}
4、动态内存管理中的常见错误
4.0 对NULL指针的解引用操作
void test()
{
int* p = (int*)malloc(INT_MAX);// 值比较大,返回NULL
*p = 20;//如果p的值是NULL,对空指针解引用就会有问题
free(p);
}
4.1 对动态开辟空间的越界访问
int main()
{
// 申请10 个内存空间
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
// 空间申请失败
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 50; i++) // 越界访问
{
*(p + i) = i + 1;
}
free(p);
p = NULL;
return 0;
}
4.2 对非动态开辟内存使用free释放
int main()
{
int a = 10;
int* p = &a;
// ...
free(p);//erro
p = NULL;
}
4.3 使用free释放一块动态开辟内存的一部分
int main()
{
// 申请10 个内存空间
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
// 空间申请失败
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 5; i++)
{
*p = i;
p++;// //p不再指向动态内存的起始位置
}
free(p);
p = NULL;
return 0;
}
4.4 对同一块动态内存多次释放
void test()
{
int *p = (int *)malloc(100);
free(p);
// ...
free(p);//重复释放
}
4.5 动态开辟内存忘记释放(内存泄漏)
在 if (flag) 判断后,如果 flag 为真(在代码中总是为真,因为 flag 被初始化为 1),则 return 语句会导致函数提前返回,而之前分配的内存 p 没有被 free 释放。这会导致内存泄漏。
void test()
{
int flag = 1;
int* p = (int*)malloc(100);
if (p == NULL)
{
return;
}
// 使用
if (flag)
return;
free(p);
p = NULL;
}
int main()
{
test();
// ...
return 0;
}
5、动态内存经典笔试题解析
💡题一:
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;
}
🍊解析:
GetMemory 函数中,p 是一个局部变量,它被初始化为指向 malloc 分配的内存。然而,这个地址仅在这个函数内部有效,因为它是按值传递给 GetMemory 的。一旦 GetMemory 函数结束,这个局部变量 p 将被销毁,而原始指针 str 在 Test 函数中仍然指向 NULL。我们要拷贝“hello world”就要对空指针解引用,程序会崩溃。
✅修正:将 str 的地址传给 p,用char** 来接收,这使得它可以修改 Test 函数中的 str 指针。
void GetMemory(char** p)
{
*p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str);
strcpy(str, "hello world");// ok
printf(str);//ok
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
或者:
调用 GetMemory 函数,并将返回的指针赋给 str;而GetMemory这个函数分配了100字节的内存空间,并将这块内存的地址赋给指针 p并返回这个指针 p;然后 strcpy 函数将字符串 "hello world" 复制到 str 指向的内存中(也就是str 指向的内存足够大的100字节)。
-> 注意的是,我们自己写代码时最好检查一下这里 malloc 是否成功分配了内存。
-> 还有就是这里的printf 直接传递了 str,其实就是拿到这个有效字符串第一个元素地址,也能打印出字符串;但这不是 printf 的标准用法。更安全的做法是使用 %s 格式说明符,如 printf("%s", str) ;
#include<stdio.h>
#include<string.h>
char* GetMemory()
{
char *p = (char*)malloc(100);
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
strcpy(str, "hello world");// ok
printf(str);//ok
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
💡题二:涉及返回栈空间地址问题知识
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
运行Test 函数会有什么样的结果?一堆乱码
🍊解析如下:
GetMemory 函数中定义了一个局部字符数组 p,并返回这个局部数组的地址。然而,当GetMemory 函数返回时,局部变量 p 的作用域结束,其内存被释放(它不再有效,因为栈帧被销毁),但返回的指针 str 仍然指向那个已经无效的内存区域,这时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);
}
🍊解析:示例中分配的内存从未被释放,内存泄漏。
✅更正:
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;
}
💡题四:
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
🍊解析:
在调用 free(str); 之后,str 指向的内存块已经被释放回堆中,无法继续使用了,并且其内容是未定义的。此时,str 已经是野指针了,任何对 str 的解引用(包括尝试写入或读取)都是未定义行为。后面拷贝“world”就是非法访问了。
✅更正:
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
str = NULL;// 将指针置为 NULL,避免野指针
if (str != NULL) // 使用不了了
{
strcpy(str, "world");
printf(str);
}
}
6、柔性数组(Flexible Array Member)
6.0 柔性数组概念
柔性数组也称为变长数组,是C99标准引入的一种特性,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
struct st_type
{
int i;
char c;
int a[0];//柔性数组成员
};
有些编译器会报错无法编译可以改成:
struct st_type
{
int i;
char c;
int a[];//柔性数组成员
};
6.1 柔性数组特点
• 结构中的柔性数组成员前面必须至少一个其他成员。
• sizeof 返回的这种结构大小不包括柔性数组的内存。
• 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
⛳例如:
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
int main()
{
printf("%d\n", sizeof(type_a));//输出的是4
return 0;
}
🍀柔性数组使用:
通过malloc函数为struct S类型的指针ps分配了足够的内存。分配的内存大小是结构体本身的大小(sizeof(struct S))加上一个包含20个整数的数组的大小(20 * sizeof(int))。这种方式确保了结构体实例ps拥有一个足够大的柔性数组a来存储20个整数。
#include <stdio.h>
#include <stdlib.h>
struct S
{
int i;
int a[];//柔性数组成员
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S) + 20 * sizeof(int));
if (ps == NULL)
{
printf("malloc()");
return 1;
}
// 使用这些空间
ps->i = 100;
int n = 0;
for (n = 0; n < 20; n++)
{
ps->a[n] = n + 1;
}
free(ps);
return 0;
}
6.2 柔性数组的优势
⛳第一种方式:
struct S
{
int i;
int a[];//柔性数组成员
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S) + 20 * sizeof(int));
if (ps == NULL)
{
printf("malloc()");
return 1;
}
// 使用这些空间
ps->i = 100;
int n = 0;
for (n = 0; n < 20; n++)
{
ps->a[n] = n + 1;
}
// 调整ps 指向空间的大小
struct S* ptr = (struct S*)realloc(ps,sizeof(struct S) + 40 * sizeof(int));
if (ptr != NULL)
{
ps = ptr;
ptr = NULL;
}
else
return 1;
// 使用
for (n = 0; n < 40; n++)
{
printf("%d ", ps->a[n]);
}
// 释放
free(ps);
ps = NULL;
return 0;
}
打印结果:只有前20元素为整型,后面打印随机值了
⛳第二种方式:
#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;
}
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;
}
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、定义了一个结构体S,它包含一个整型成员n和一个指向整型的指针arr 。
2、通过malloc为结构体S分配内存,并检查是否成功。
3、为整型数组分配了20个元素的内存,并将ps->arr指向这块内存。
4、设置结构体成员n的值为100,并将数组arr的前20个元素初始化为1到20。
5、使用realloc将arr数组的大小从20个元素增加到40个元素。如果realloc成功,它将返回指向新内存块的指针,否则返回NULL。注意,这里通过临时变量tmp来接收realloc的返回值,以防止在realloc失败时丢失对原始内存块的引用。
6、循环遍历并打印调整大小后的数组arr的所有元素
7、首先释放arr指向的内存,然后将ps->arr设置为NULL以防止野指针。接着释放ps指向的结构体内存,并将ps设置为NULL。
方式一和方式二可以完成同样的功能,但是 方式一 的实现有两个好处:
• 方便内存释放。
• 这样有利于访问速度. 连续的内存有益于提高访问速度,也有益于减少内存碎片。
7、C/C++程序内存区域划分总结
如图:
C/C++程序内存分配的几个区域:
1. 栈区(stack):用于存放函数的参数值、局部变量的值等。这部分内存由编译器自动分配和释放,函数的调用过程就是通过栈这种数据结构实现的。
2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
3. 数据段(静态区):(static)存放全局变量、静态数据。程序结束后由系统释放。
4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。这部分内存区域通常是只读的,以防止程序意外地修改其指令。
喜欢记得
⛳ 点赞☀收藏 ⭐ 关注!!
如有不足欢迎评论区指出~