前言
文章的标题是动态内存管理,那什么是动态内存管理?为什么有动态内存管理呢?
回顾一下以前学的知识,我们已经掌握的开辟内存的方式有以下几种:
char ch = 'a';
int val = 10;
float f_val = 12.8f;
int arr[20] = { 0 };
//....
上述开辟内存的方式有几个弊端:
- 开辟空间的大小是固定的
- 数组在声明的时候,必须指定数组的长度,数组空间⼀旦确定了大小就不能调整了
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组在编译时就开辟空间的方式就不能满⾜了。
C语言引⼊了动态内存开辟,让程序员自己可以申请和释放空间,就比较灵活了。
1.malloc
这个函数的功能是:向内存申请一块连续的内存空间,然后返回一个指向该空间的起始地址的指针。
- 如果空间
开辟成功
,则返回一个指向该空间起始地址的指针。 - 如果空间
开辟失败
,则返回一个NULL指针。因此,函数的返回值需要做检查 - 开辟的空间不会被初始化。
- 返回值的类型是void*,所以该函数并不知道所开辟空间的类型,需要空间的使用者自己决定。
- 当该函数的参数是0是,返回值是未定义的,取决于编译器。
- 头文件stdlib.h
使用malloc函数申请40个字节的空间
当函数的参数是0时,vs2019中是随机值
既然空间是程序员申请的,那么最后由谁释放呢?
- 程序员自己释放。
- 程序结束,操作系统回收
下面就来介绍如何释放
2.free
C语言提供了另外⼀个函数free,专⻔是⽤来做动态内存的释放和回收的,函数原型如下:
- 该函数用来释放由malloc、calloc、realloc申请的空间的
- 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
- 如果参数 ptr 是NULL指针,则函数什么事都不做。
- 该函数不会改变指针ptr的值,因此为了避免野指针的出现,要将其及时置为NULL。
3.calloc
该函数的功能跟malloc差不多,区别在于:
- 该函数的参数有两个,一个是有几个元素,一个是每个元素的大小。
- 该函数会将申请的空间初始化为全0。
4.realloc
realloc函数的出现让动态内存管理更加灵活。
有时会我们发现过去申请的空间太小了,有时候我们⼜会觉得申请的空间过⼤了,那为了合理的使用内存,我们⼀定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。
- ptr 是要调整的内存地址
- size 调整之后新大小
- 返回值为调整之后的内存起始位置。
- 这个函数调整原内存空间大小的基础上,还会
将原来内存中的数据移动到新的空间
。 - realloc在调整内存空间的是存在两种情况:
- 情况1:原有空间之后有足够大的空间
- 情况2:原有空间之后没有足够大的空间
- 情况1
当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。- 情况2
当是情况2的时候,原有空间之后没有⾜够多的空间时,扩展的⽅法是:在堆空间上另找⼀个合适⼤⼩连续空间来使⽤。这样函数返回的是⼀个新的内存地址。
既然该函数有两种特殊情况,那我们在使用时也应该更加注意:
下面的代码有没有问题呢?
int main()
{
//1. 先申请一部分空间
int* p = (int*)malloc(20);
if (NULL == p)
{
perror("malloc");
return 1;
}
//2.扩容空间
p = (int*)realloc(p, 40);
if (NULL == p)
{
perror("realloc");
return 1;
}
//3.使用
//.......
free(p);
p = NULL;
return 0;
}
假如realloc开辟空间失败了,那么它会返回一个空指针,p就是空指针了。
好家伙,没给我扩容成功,我以前的数据也没了,真可以!
所以在接收realloc的返回值时,我们应该使用你一个临时变量接收,判断临时变量是不是空指针,然后再对p进行操作。
所以,应该这样写:
#include<stdlib.h>
int main()
{
//1. 先申请一部分空间
int* p = (int*)malloc(20);
if (NULL == p)
{
perror("malloc");
return 1;
}
//2.扩容空间
int* tmp = NULL;
tmp = (int*)realloc(p, 40);
//先判断realloc的返回值
if (tmp != NULL)
{
p = tmp;
tmp = NULL;
}
//3.使用
//....
free(p);
p = NULL;
return 0;
}
5.动态内存常见错误
- 对NULL指针的解引用操作
- 对动态开辟空间的越界访问
- 对非动态开辟内存使用free释放
- 使用free释放⼀块动态开辟内存的⼀部分
- 对同⼀块动态内存多次释放
- 动态开辟内存忘记释放(内存泄漏)
6.动态内存经典笔试题分析
- 题目一
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
此处是值传递,并不会为str真正开辟空间,str还是NULL。
- 题目二
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
p变量生命周期为GetMemory函数内,函数结束,变量销毁。使用一个已经被销毁的变量的地址,结果是不可预测的
3.题目三
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
str指向的空间被释放,那么str就是野指针了,但是str还是指向那块内存空间;向str中拷贝world,那就是非法访问了。
7.柔性数组
- 什么是柔性数组
也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。C99 中,结构中的最后⼀个元素
允许是未知大小的数组,这就叫做『柔性数组』成员。
struct s
{
int i;
char ch;
int a[]; //int a[0]
};
其中a就是一个柔性数组。
- 柔性数组的特点
- 结构中的柔性数组成员前⾯ 必须⾄少⼀个其他成员。
- sizeof 返回的这种结构大小不包括柔性数组的内存。
- 包含柔性数组成员的结构体用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小。
- 柔性数组的使用
如果我们不使用柔性数组,那么如何实现上述代码呢?
虽然不适用柔性数组也能达到目的,但使用柔性数组有两个好处:
- 方便内存的释放
如果我们的代码是在⼀个给别⼈⽤的函数中,你在⾥⾯做了⼆次内存分配,并把整个结构体返回给⽤⼾。⽤⼾调⽤free可以释放结构体,但是⽤⼾并不知道这个结构体内的成员也需要free,所以你不能指望⽤⼾来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返回给⽤⼾⼀个结构体指针,⽤⼾做⼀次free就可以把所有的内存也给释放掉。
- 有利于访问速度
连续的内存有益于提⾼访问速度,也有益于减少内存碎⽚。
8.C/C++中的内存区域划分
- 栈区
栈区(stack):在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内存容量有限。栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。
- 堆区
堆区(heap):⼀般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配⽅方式似于链表。
- 静态区
数据段(静态区):存放全局变量、static修饰的变量。程序结束后由系统释放。
- 代码段
代码段:存放函数体(类成员函数和全局函数)的⼆进制代码。文章来源:https://www.toymoban.com/news/detail-774355.html
本次的分享就到这里啦!如有错误请纠正。文章来源地址https://www.toymoban.com/news/detail-774355.html
到了这里,关于【C语言:动态内存管理】的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!