引入概念
什么是栈区:
栈区用于存储函数的局部变量、函数参数和函数调用的返回地址等信息。栈区的大小在编译时确定,通常比堆区小,由操作系统自动分配和释放。栈区中的内存分配和释放是由编译器自动管理的,遵循后进先出的原则。关于栈区补充
什么是堆区:
堆区是由程序员分配和释放的内存区域。在堆区中分配的内存大小和生存周期由程序员控制。
什么是静态区:
静态区包含两部分内容:全局变量和静态变量。全局变量在程序启动时被分配,并且在整个程序的执行周期内都存在。静态变量是在函数内声明为静态变量的变量,它们在程序的整个生命周期内都存在,但是作用域仅限于声明它们的函数。
这是对于程序员常用的三个区,不同的区域管理着不同数据
正文
什么是动态内存分配?
在C语言中,动态内存分配是通过使用 malloc
、calloc
、realloc
和 free
等库函数来实现的。动态内存分配允许程序在运行时根据需要请求一定数量的内存空间,而不是在编译时就固定分配。
那么为什么我们需要用到堆区。
在写代码的过程中我们通常使用到的是栈区和静态区,明明这样我们也可以正常运行,为什么还需要栈区这种东西。
假设我们需要写一个关于通讯录的程序,通讯录中可以存储1000个人的信息,我们使用什么东西把它们存储起来呢,可能大部分人会想到使用数组,好那么我们用数组试试看
一个人的数据信息包含如下
typedef struct
{
char name[Name_Max];
char sex[SEX_MAX];
int age;
char address[Address_MAX];
int number;
}contacter;
现在用数组创建1000人的数据
编译器报出警告,栈溢出,让我们把数据移入堆中。这时再来分析这个案例,在通讯录中我们要存储一个人的信息是通常我们都是一个一个添加就没有必要一下放进1000人的信息,这时堆区的优势就显现出来了,我们可以在运行时根据自己的需要来开辟空间,而不是像栈区在编译时就确定好空间的大小。
这样我们就可以根据需要来分配内存的大小了,这里简单实现了一个扩容函数
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
typedef struct {
char name[20];
char sex[10];
int age;
char address[20];
int number;
} contacter;
contacter* addInformation(contacter* p, int capacity) {
contacter* ptr = (contacter*)realloc(p, sizeof(contacter) * capacity);
if (ptr != NULL) {
p = ptr;
}
return p; // 返回重新分配后的指针
}
int main() {
contacter* p = (contacter*)malloc(sizeof(contacter));
if (p == NULL) {
printf("申请空间失败");
return 1;
}
int num;
printf("请输入新的容量:");
scanf("%d", &num);
p = addInformation(p, num);
free(p);
p = NULL;
return 0;
}
现在详细讲解关于动态内存分配的几个函数如何使用
malloc函数
malloc
函数的原型如下:
void* malloc(size_t size);
-
size
参数表示要分配的内存空间的字节数。 -
malloc
函数返回一个指向分配的内存空间起始地址的void
类型指针。需要注意的是,返回的指针是void*
类型,因此在使用时需要根据实际情况进行类型转换,如果申请失败malloc会返回一个NULL,所以我们需要判断一下是否返回为NULL,避免对NULL指针操作。
假设我们要申请存储10个整型数据我们可以这样
int* p = (int*)malloc(sizeof(int)*10);
因为malloc会返回一个void*指针我们强制类型转换一下就可以通过这个指针来访问堆区的这片空间,像这样
int main()
{
int* p = (int*)malloc(sizeof(int)*10);
if (p == NULL)
{
return 1;
}
for (int i = 0; i < 10; i++)
{
p[i] = i;//p[i]==*(p+i)
}
for (int i = 0; i < 10; i++)
{
printf("%d", p[i]);
}
return 0;
}
calloc函数
calloc
函数的原型如下:
void* calloc(size_t num, size_t size);
与malloc相比calloc会把申请的空间初始化为0,以及参数不同,calloc有两个参数
num
参数表示要分配的元素个数。size
参数表示每个元素的大小,以字节为单位。
所以当我们申请10个整型空间是这样
int main()
{
int* p = (int*)calloc(10,sizeof(int) );
if (p == NULL)
{
return 1;
}
for (int i = 0; i < 10; i++)
{
printf("%d ",p[i]);
}
//输出:0 0 0 0 0 0 0 0 0 0
free(p);
return 0;
}
realloc
函数
realloc函数可以实现动态增长,就是根据需要在堆区申请一片更大的空间,把原本的空间的值给拷贝过来,然后释放原空间,然后返回一个新空间的地址,就像这样
realloc
函数的原型如下
void* realloc(void* ptr, size_t size);
ptr
参数是一个指向先前分配的内存块的指针。如果ptr
是一个空指针,则realloc
的行为类似于malloc
。size
参数是新的内存块大小,以字节为单位。
与malloc类似只不过多了一个指向先前分配的内存块的指针也就是malloc或calloc所开辟的空间
需要注意的是不能直接把realloc的返回地址直接给我malloc所开辟的空间,因为realloc如果开辟失败会返回一个NULL指针,如果指向malloc所开辟的空间的指针被NULL所覆盖,会导致我访问不到原来的空间也就是内存泄漏,所以在使用realloc之前我们应该重新创建一个变量,判断不为NULL才赋值给我使用的指针变量,像这样
int* p = (int*)calloc(10,sizeof(int) );
if (p == NULL)
{
return 1;
}
int* ptr = (int*)realloc(p, 12 * sizeof(int));
if (ptr != NULL)
{
p = ptr;
}
补充:
如果我们在参数ptr上传进一个null指针就可以达到和malloc一样的效果
free函数
free函数就是释放空间所用的函数,因为堆区和栈区不同我们在使用栈区,空间的分配和释放都是编译器帮我们完成,而堆区不同,既然是手动申请当然也要手动释放。把我们需要释放的空间的地址传过去,free就可以帮我们完成释放的操作.
free
函数的原型如下:
void free(void* ptr);
在使用free函数的函数时我想到一个问题就是free是如何知道需要释放多大的空间,还是说指针之后的空间全部释放,所以找到一篇文章free函数如何知道要释放多大空间,其中简述了free如何得到所需释放空间大小,感兴趣的可以看看
在使用动态内存分配时应该注重的几个点
-
内存泄漏: 动态分配的内存块在使用完毕后必须被及时释放,否则会导致内存泄漏问题。内存泄漏指的是程序分配了一块内存后,却没有释放,导致系统无法再次使用这部分内存。定期检查代码中的内存释放逻辑,避免出现内存泄漏问题。
#include <stdlib.h> int main() { // 未释放分配的内存 int *ptr = (int *)malloc(sizeof(int)); return 0; }
-
野指针和悬挂指针: 动态分配的内存一旦被释放,指向该内存的指针就变成了野指针。进一步地,如果尝试在释放后访问这些指针,就会产生悬挂指针问题。在释放内存后,将指针设置为
NULL
是一个好的习惯,可以避免使用已经释放的内存。 -
#include <stdlib.h> int main() { // 分配并释放内存 int *ptr = (int *)malloc(sizeof(int)); free(ptr); // 访问已释放的内存 *ptr = 10; // 这是一个悬挂指针问题 return 0; }
-
内存访问越界: 动态分配的内存块的大小必须足够容纳程序需要存储的数据,否则可能会发生内存访问越界的问题。越界访问可能会导致数据损坏或者程序崩溃。在分配内存时,要确保分配的内存大小足够存储数据,同时在访问内存时要注意索引的范围。
#include <stdlib.h> int main() { int *ptr = (int *)malloc(5 * sizeof(int)); // 访问越界 for (int i = 0; i < 10; i++) { ptr[i] = i; // 越界访问 } free(ptr); return 0; }
-
不要忘记释放内存: 每次调用
malloc
、calloc
或realloc
分配内存后,都要确保在不再需要时调用free
函数释放内存。否则,动态分配的内存会在程序运行期间一直占用系统资源,直到程序终止。 -
错误处理: 动态内存分配函数(如
malloc
、calloc
、realloc
)在无法分配所需内存时会返回NULL
,因此必须检查返回值,以确保分配成功。如果分配失败,应该及时处理并采取适当的措施,避免程序出现崩溃或不可预料的行为。
动态内存分配比较经典的题目
思考以下代码存在什么问题
第一题:
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;
}
str传给GetMemory函数的时候是值传递,所以GetMemory函数的形参p是str的一份临时拷贝,也就是NULL拷贝给了p。
在GetMemory函数内部动态申请空间的地址,存放在p中,不会影响外边str,所以当GetMemory函数返回之后,str依然是NULL。所以strcpy会失败。当GetMemory函数返回之后,形参p销毁,使得动态开辟的100个字节存在内存泄漏。无法释放。
第二题:
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数组的空间就还给了操作系统
返回的地址是没有实际的意义,如果通过返回的地址,去访问内存就是非法访问内存的
第三题:
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;
}
野指针(非法访问内存),str所指向的空间已经被释放,但是str还指向那片空间,并对其进行访问
柔性数组
柔性数组是C语言中根据动态内存开辟所诞生的一种特殊的结构,允许结构体的最后一个成员是一个数组,且数组的大小可以根据结构体的实际大小动态变化。
//以下是一个简单的示例,演示了柔性数组的用法:
#include <stdio.h>
#include <stdlib.h>
// 定义包含柔性数组的结构体
typedef struct {
int length; // 数组的长度
int data[]; // 柔性数组
} FlexArray;
int main() {
int n = 5; // 数组长度
FlexArray *flex_array;
// 分配结构体和柔性数组的内存空间
flex_array = malloc(sizeof(FlexArray) + n * sizeof(int));
if (flex_array == NULL) {
printf("内存分配失败\n");
return 1;
}
// 设置数组长度
flex_array->length = n;
// 初始化数组元素
for (int i = 0; i < n; i++) {
flex_array->data[i] = i * 2;
}
// 打印数组元素
printf("数组的值:");
for (int i = 0; i < n; i++) {
printf("%d ", flex_array->data[i]);
}
printf("\n");
// 释放内存
free(flex_array);
return 0;
}
这样我就又可以像使用数组一样进行访问,又可以进行动态内存操作
以上就是关于动态内存分配的所以操作及使用,再简略补充关于c/c++内存区域划分
栈区,堆区,静态区,这是相对于程序员所看到的内存划分,c/c++实际划分还要细致化一点如图所示
图片来自B站比特鹏哥视频