深挖C语言:动态内存管理
1.为什么要有动态内存分配
我们已经掌握的开辟方式:
int val = 20;// 在栈空间上开辟四个字节
char arr[10] = { 0 };//在栈空间上开辟10个连续的空间
上述开辟空间由两个特点:
1. 空间开辟的大小固定。
2. 数组在声明的时候,必须指定数组长度,数组空间一旦确定大小就不能调整。
C语言引入了动态内存开辟,让程序员自己可以申请和释放空间。
2. malloc 和 free 函数
2.1 malloc函数
#include<stdlib.h>
void* malloc( size_t size );
功能:向内存的堆区申请一块连续可用的空间,并返回指向这块空间的起始地址。
参数:
size:要分配的内存块的字节数。
返回值:
开辟成功:返回这块空间的起始地址。
开辟失败:返回一个NULL指针,因此一定要检查malloc的返回值。
注:
1. 返回值的类型是 void* ,具体在使用的时候需要我们强转为我吗需要的类型。
2. 如果参数 size 为 0 ,malloc 的行为是标准未定义的,取决于编译器。
2.2 free函数
#include<stdlib.h>
void free( void* ptr );
功能:释放先前动态分配函数(malloc、calloc、realloc)申请的内存空间。
参数:
ptr:指向要释放内存的指针。
注:
1. 如果参数 ptr 指向的不是动态开辟的,那 free 函数的行为是未定义的。
2. 如果参数 ptr 是NULL指针,则函数不运行。
代码演示:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int num;
scanf("%d",&num);
int arr[num];
int* ptr =(int*)malloc(num * sizeof(int));
if(ptr != NULL)
{
for(int i = 0;i < num;i++)
{
*(ptr + i) = 0;
}else
{
perror("malloc");
retrun 1;
}
free(ptr);
ptr = NULL;
return 0;
}
3.calloc 和 realloc 函数
3.1 calloc函数
#include<stdlib.h>
void* calloc( size_t num , size_t size );
功能:将 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为 0.
(与malloc函数的区别在于,会把每个字节初始化为0)
代码示例:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)calloc(10,sizeof(int));
if(p != NULL)
{
for(int i = 0;i < 10;i++)
{
printf("%d ",*(p + i);
}
free(p);
p = NULL;
return 0;
}
所以如果我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。
3.2 realloc函数
#include<stdlib.h>
void* realloc( void* ptr , size_t size );
功能:重新调整之前分配的内存块大小,它可以在不丢失原有数据的情况下,扩大或缩小动态分配的内存块。
参数:
ptr:指向要调整的内存空间的起始地址,如果 ptr 为NULL,realloc函数的功能类似于malloc函数。
size:调整后的内存块大小,单位字节。
返回值:
成功:返回新内存块的起始地址(可能与原地址不同)。
失败:返回NULL,原内存块保持不变。
注意事项:
realloc在调整内存空间大小的时候,存在两种情况:
情况1:原有空间后面又足够大的空间,要扩展的空间就直接在原来内存后面追加空间,原来空间的数据不发生变化,最终返回的地址还是旧地址。
情况2:原有空间之后没有太多空间,会在内存堆区寻找新的满足要求的空间,返回新的起始地址,事件过程如下:
(1)寻找新的空间
(2)将旧空间的数据拷贝到新空间,保证数据不丢失
(3)释放旧空间,返回新空间的起始地址

代码演示:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* ptr = (int*)malloc(100);
if(ptr != NULL)
{
*(ptr + i) = i;
}else{
perror("malloc");
retrun 1;
}
//代码1,直接放到原指针中
ptr = (int*)realloc(ptr,1000);
//代码2,先放到其他指针中进行判断
int* p = realloc(ptr,1000);
if( p != NULL )
{
ptr = p;
p = NULL;
}
free(ptr);
ptr = NULL;
return 0;
}
4.常见的动态内存的错误
4.1 对NULL指针的解引用操作
void test()
{
int* p = (int*)malloc(INT_MAX/4);
*p = 20;// 如果指针p是NULL,就会报错
free(p);
p = NULL;
}
4.2 对动态开辟空间的越界访问
void test()
{
int* p = (int*)malloc(10 * sizeof(int));
if(p == NULL)
{
return 1;
}
for(int i = 0;i <= 10;i++)
{
*(p + i) = i;//当i为10时越界访问
}
free(p);
p = NULL;
}
4.3 对非动态开辟内存使用free释放空间
void test()
{
int a = 10;
int* p = &a;
free(p);// 报错
}
4.4 使用free释放一块动态开辟内存的一部分
void test()
{
int* p = (int*)malloc(100);
p++;
free(p);// p不在指向动态内存的起始空间,空间未完全释放。
}
4.5 对同一块动态内存多次释放
void test()
{
int* p = (int*)malloc(10);
if(p == NULL)
{
return 1;
}
free(p);
free(p);//重复释放
}
4.6 动态开辟内存忘记释放(内存泄漏)
void test()
{
int* p = (int*)malloc(100);
if(p == NULL)
{
rturn 1;
}else
{
*p = 20;
}
}
int main()
{
test();
return 0;
}
动态开辟内存一定要释放,并且正确释放。
5.动态内存管理经典笔试题分析
5.1 题目1:
#include <stdio.h>
#include <stdlib.h>
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为传值调用,str并没有接收到开辟空间的起始地址,依旧为 NULL。
5.2 题目2:
#include <stdio.h>
#include <stdlib.h>
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf("%s\n",str);
}
int main()
{
Test();
return 0;
}
问题:'p'是函数里的临时数组,函数一结束就被系统回收了,把它的地址返回去,外面拿到的地址已经无效。
改法:用 'malloc' 分配内存并返回,或者直接返回字符串常量 "hello world"。
5.3 题目3:
#include <stdio.h>
#include <stdlib.h>
#include<string.h>
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf("%s\n",str);
}
int main()
{
Test();
return 0;
}
问题:没检查 'malloc' 是否成功、没用 'free'释放空间。
5.4 题目4:
#include <stdio.h>
#include <stdlib.h>
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf("%s\n",str);
}
}
int main()
{
Test();
return 0;
}
问题:free(str) 之后没有把 str 置为 NULL,导致 if( str != NULL) 判断为真,接着对已释放的内存执行 strcpy 和 printf,这是 "释放后使用" 的未定义行为。
6.柔性数组
在C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组。
struct Example
{
int len; // 其他成员
int data[]; // 柔性数组,不占结构体本身的空间
};
柔性数组特点:
1. 前面必须至少有一个其他成员。2. 只能作为结构体最后一个成员。
3. 声明时不写数组大小,语法为 type name[ ] 或 type name[0](0长度时GCC扩展,非标准,有些编译器写第二个语法时会报错)
4.柔性数组不占用结构体的内存大小,sizeof(struct Example)只包含前面的成员内存大小。
5. 包含柔性数组的结构体用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小。
代码演示:
#include<stdio.h>
#include<stdlib.h>
struct Stu
{
int age;
char name[];
}s;
int main()
{
printf("%d\n",sizeof(s));
return 0;
}
柔性数组的使用:
#include<stdio.h>
#include<stdlib.h>
struct Stu
{
int age;
char name[];
};
int main()
{
// 柔性数组获得10字节空间
struct Stu* p = (struct Stu*)malloc(sizeof(s) + sizeof(char) * 10);
for(int i = 0;i < 10;i++)
{
p->name[i] = i;
}
free(p);
return 0;
}
7. 总结C/C++中程序内存区域划分

C/C++程序内存分配的几个区域:
1. 栈区:在执行函数时,函数内局部变量的存储单元都可以在栈区上创建,函数执行结束时,这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率高,但是分配的内存容量有限。栈区主要存放运行函数中的局部变量、函数参数、返回数据、返回地址等。
2. 堆区:一般由程序员分配释放,若程序员不释放,程序结束时可能由OS(操作系统)回收,分配方式类似于链表。
3. 数据段(静态区):(static)存放全局变量、静态数据。程序结束后由系统释放。
4. 代码段:存放函数体(类成员函数和全局变量)的二进制代码。
最后,感谢阅读,欢迎大家交流与指正。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)