欢迎来到白刘的领域 Miracle_86.-CSDN博客
系列专栏 C语言知识
先赞后看,已成习惯
创作不易,多多支持!
在这个波澜壮阔的内存地产世界中,malloc、free、calloc和realloc四位主角,共同演绎着一场场精彩绝伦的楼盘开发与交易大戏。
目录
一、为什么要有动态内存分配
二、malloc和free
2.1 malloc —— 购买土地
2.2 free —— 出售土地
三、calloc和realloc
3.1 calloc —— 批量购买并初始化土地
3.2 realloc —— 调整土地大小
四、常见的动态内存错误
4.1 对NULL指针解引用
4.2 对动态内存开辟空间的越界访问
4.3 对非动态开辟内存进行free释放
4.4 使用free释放动态开辟内存的一部分
4.5 对同一块动态内存多次释放
4.6 忘记释放(内存泄漏)
五、柔性数组
5.1 柔性数组的特点
5.2 柔性数组的使用
六、总结C/C++中程序内存区域划分
一、为什么要有动态内存分配
我们已经掌握的内存开辟方法有:
//变量
int val = 20;//在栈空间上开辟四个字节//数组
char arr[10] = { 0 };//在栈空间上开辟10个字节的连续空间
但是上述的开辟方法有两个缺点:
1. 开辟的空间大小是有限的。
2. 数组在开辟的时候,必须声明数组的长度,数组空间一旦确定大小就不能调整。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。
所以在C语言中,我们引入了动态内存开辟,可以让程序员自己申请和释放空间,就比较灵活了。
二、malloc和free
如果我们将内存比作地产,那malloc和free就可以非常恰当地比作:购买土地和出售土地。
2.1 malloc —— 购买土地
C语言中提供了一个动态内存开辟的函数:
void* malloc(size_t size);
malloc函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
1. 如果开辟成功,返回一个指向开辟好空间的指针。
2. 如果开辟失败,返回NULL指针。因此malloc的返回值一定要检查。
3. 返回值类型为void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候程序员自己确定。
4. 如果参数size为0,malloc的行为是标准未定义的,取决于编译器。
2.2 free —— 出售土地
C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:
void free(void* ptr);
free函数用来释放动态开辟的内存。
1. 如果参数ptr指向的空间不是动态开辟的,那free的行为是未定义的。
2. 如果参数ptr是NULL指针,则什么也不做。
malloc和free都包含在<stdlib.h>头文件中。
eg:
#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;
}
首先,定义了一个整数变量num
并初始化为0。然后使用scanf
函数从标准输入读取一个整数,并存储在num
中。然后,声明了一个长度为num
的整数数组arr
,并将其所有元素初始化为0。注意,在C99标准之前,这种变长数组(VLA)是不被允许的。但在C99及之后的版本中,这是合法的。变长数组在之前我们也有所讲过:
C语言中的百宝箱——数组(2)-CSDN博客
然后我们定义了一个整数指针ptr
并初始化为NULL
。 之后使用malloc
函数动态分配了num
个整数大小的内存,并将返回的指针赋值给ptr。if语句
首先检查ptr
是否为NULL
,以确保内存分配成功。如果成功,则使用一个循环将动态分配的内存的每一个位置初始化为0。紧接着我们使用free
函数释放了ptr
所指向的内存,以避免内存泄漏。
最后我们为什么将ptr设置为空指针,因为此时它是个野指针!如果我们接下来对其操作,将造成严重的后果,再一个就是提高了代码可读性,设置为空指针,提示这块内存已经被释放了。
三、calloc和realloc
calloc和realloc也可以有一个比较恰当的比喻:批量购买并且初始化土地(在土地上盖房子)和调整土地大小。
3.1 calloc —— 批量购买并初始化土地
C语言中还有一个函数,也是用来动态内存分配,它就是calloc。原型如下:
void* calloc(size_t num, size_t size);
1. 函数的功能是为num个大小为size的元素开辟一块空间,并且把每块空间都初始化为0。
2. 与malloc的区别就是:calloc会在返回地址之前,将申请空间的每个字节都初始化为0。
eg:
#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));}}free(p);p = NULL;return 0;
}
运行结果:
所以如果我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。
3.2 realloc —— 调整土地大小
realloc的出现,让动态内存管理更加灵活。
有的时候,我们会觉得申请的空间太小了,有的时候又觉得太大了,那为了合理的内存,我们一定会对内存的大小做灵活的调整,而realloc的作用就是可以做到对动态开辟内存的大小做调整。
函数原型:
void* realloc(void* ptr, size_t size);
1. ptr是要调整的地址,size是调整后的新大小。
2. 返回值为调整之后的内存起始位置。
3. 这个函数在调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
realloc在调整内存空间时存在两种情况:
情况1:原有空间之后有足够大的空间。
情况2:原有空间之后没有足够大的空间。
当是情况1的时候,要扩展内存就直接在原有内存之后直接追加空间,原来空间的数据不发生变化。
当是情况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){ptr = p;}//业务处理free(ptr);return 0;
}
代码1:
这种直接使用realloc
的方式是可行的,但需要注意以下几点:
返回值检查:如果realloc
函数调用失败,它会返回NULL
。这时,原来的内存块(由ptr
指向)也不会被释放,所以需要确保在将realloc
的返回值赋给ptr
之前检查其返回值是否为NULL
。
内存泄漏:如果realloc
失败并返回NULL
,而我们又没有保存原来的ptr
的值,那么将失去对原始内存块的引用,从而导致内存泄漏。
代码2:
这种方式更加安全,因为它首先创建了一个新的指针p
来保存realloc
的返回值。如果realloc
成功,p
将指向新的内存块,然后你可以安全地将p
的值赋给ptr
。如果realloc
失败,p
将为NULL
,但ptr
仍然指向原来的内存块,因此不会发生内存泄漏。
由于上述代码,我们就不得不简单介绍几个常见的动态内存的错误。
四、常见的动态内存错误
4.1 对NULL指针解引用
void test()
{int* p = (int*)malloc(INT_MAX / 4);*p = 20;//如果p的值是NULL,就会有问题free(p);
}
4.2 对动态内存开辟空间的越界访问
void test()
{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);
}
4.3 对非动态开辟内存进行free释放
void test()
{int a = 10;int* p = &a;free(p);//ok?
}
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 忘记释放(内存泄漏)
void test()
{int* p = (int*)malloc(100);if (NULL != p){*p = 20;}
}
int main()
{test();while (1);
}
五、柔性数组
也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。
C99中,结构体中的最后一个元素允许是未知大小的数组,这就叫做“柔性数组”成员。
eg:
typedef struct st_type
{int i;int a[0];//柔性数组成员
}type_a;
有的编译器可能会报错,可以改成:
typedef struct st_type
{int i;int a[];//柔性数组成员
}type_a;
5.1 柔性数组的特点
1. 结构体中的柔性数组前面至少有一个成员。
2. sizeof返回结构体时不包括柔性数组的大小。
3. 包含柔性数组的结构体用malloc函数进行动态开辟,并且分配的内存大小应该大于结构体的大小,以适应柔性数组的大小。
5.2 柔性数组的使用
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // 定义一个包含柔性数组成员的结构体
typedef struct { int count; double data[]; // 柔性数组成员
} FlexArray; int main() { int array_size = 10; // 假设我们想要一个大小为10的数组 size_t struct_size = sizeof(FlexArray) - sizeof(double[0]); // 计算结构体的固定部分大小 size_t total_size = struct_size + sizeof(double) * array_size; // 计算总大小 // 使用malloc分配内存 FlexArray *p = (FlexArray *)malloc(total_size); if (p == NULL) { perror("Memory allocation failed"); return EXIT_FAILURE; } // 初始化结构体 p->count = array_size; for (int i = 0; i < array_size; ++i) { p->data[i] = i * 1.0; // 假设我们为数组填充一些值 } // 使用结构体... for (int i = 0; i < p->count; ++i) { printf("%f\n", p->data[i]); } // 释放内存 free(p); return 0;
}
在这个例子中,我们首先计算了结构体的固定部分大小(不包括柔性数组成员),然后加上柔性数组所需的大小,计算出总大小。malloc
函数被用来分配所需的总内存大小。
注意,我们在计算结构体固定部分大小时使用了sizeof(double[0])
,这是为了确保在计算时不包括柔性数组成员。这个技巧依赖于sizeof
对于数组类型返回的是数组的总大小,即使数组的大小是0。
另外,在使用柔性数组成员时,要确保不要试图对结构体使用sizeof
来获取完整大小,因为这会返回不包含柔性数组成员的大小。总是根据你的需要动态地计算并分配内存。
最后,别忘了在使用完分配的内存后调用free
函数来释放它,以避免内存泄漏。
六、总结C/C++中程序内存区域划分
代码区(Code Area 或 Text Area):
- 也称为文本段或代码段,它存放程序执行的二进制代码,包括机器指令。这部分内存是只读的,以防止程序意外地修改了它的指令。
- 编译后的机器码(CPU执行的指令)就放在这一部分内存中。
全局/静态存储区(Global/Static Storage Area):
- 全局变量和静态变量的存储区域。全局变量包括在函数外部定义的变量,而静态变量包括在函数内部使用
static
关键字定义的变量以及全局静态变量。 - 这部分内存的生命周期是整个程序的执行期间。
堆区(Heap Area):
- 动态内存分配的区域,通常使用
malloc
、calloc
、realloc
在C中分配内存,或者在C++中使用new
操作符分配。 - 程序员负责在不再需要时释放这部分内存,否则会导致内存泄漏。
栈区(Stack Area):
- 由编译器自动分配和释放,存放函数的参数值、局部变量等。其操作方式类似于数据结构中的栈。
- 每次函数调用时,都会在栈上为其分配一块内存,用于存储函数的局部变量等。当函数返回时,这块内存会被自动释放。