前言:我们知道C语言是一门接触底层的语言,其核心用法之一就是对内存的操作,本篇将就详细介绍C语言中是如何灵活开辟内存空间的以及如何管理使用这些空间等等。
一.为什么要引入动态内存管理 ?
在C语言中我们目前已经掌握两种开辟内存空间的方式:
1.int data=10;//在栈(stack)空间上开辟4个字节
2.char arr[10]={0};//在栈空间上开辟10个字节的连续空间
这样的空间开辟方式有两个特点:
- 空间开辟的大小是固定的,无法修改。
- 数组在申请空间的时候,必须指定或间接指定数组的长度,数组空间一旦确定了大小不能调整。
但显然这两种空间开辟方式不能满足我们对空间灵活处理的需求,试想一个案例,例如我们要做一个客户端app,其中需要收集用户信息,但我们事先是无法预料到用户群体的个数的,这样以来我们就不能事先指定一个固定长度的空间,而是需要一种更为灵活的空间开辟方式,以适应现实需求。C语言引入了动态内存开辟的方式,使得程序员自己就可以申请和释放空间,就比较灵活了。
二.内存的基本划分
在开始讲解动态内存管理之前,我们首先需要搞懂内存是如何划分区域的,只有对内存分区搞懂,我们才能知道数据存储在内存的哪个区域,加深我们的理解。
一个由C/C++编译的程序占用的内存通常分为以下几个部分:栈区,堆区,全局区(静态区),文字常量区,代码区。
这里我们主要了解栈区,堆区和全局区(静态区):
栈区(stack)
由编译器自动分配释放,主要存储局部变量值,函数参数。堆区(heap)
允许程序在运行时动态地申请某个大小的内存空间, 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事。全局区(静态区)
存放全局变量和静态变量。
具体图示如下:
上图过于复杂的话也可以参考下面这个简要示意图:
这里我们只是粗浅地了解了一下内存中各个区域的作用,实际上远比介绍的复杂的多,为避免冗杂,这里我们还不深入作探讨 ,感兴趣的小伙伴可以自行了解。如图我们不难发现今天我们学习的动态内存管理开辟的空间不同于我们之前的局部变量,函数参数,它是开辟在堆上的。堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于静态区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时可能会由操作系统回收。
三.动态内存管理的具体实现
3.1malloc和free
- malloc
malloc是C语言提供的一个动态内存开辟的函数:
void* malloc (size_t size);
此函数向内存(堆区)申请一块连续可用的空间 ,并返回指向这块空间的指针。
- 若开辟成功,则返回一个指向开辟好的空间的指针;
- 若开辟失败,则返回空指针NULL,由于并不是每次都能成功开辟空间,因此对于malloc函数的返回值一定要作检查;
- malloc函数类型为void*,所以malloc函数并不知道开辟空间的具体类型,实际在使用的时候需要程序员自己决定;
- 参数size的单位为字节,表示开辟空间大小,若size为0,malloc的行为是标准是未定义的,取决于编译器,最好不要发生这种情况。
假设要开辟一个20字节的空间,如何实现呢?如下:
#include<stdio.h>
#include <stdlib.h>
int main()
{
//开辟空间:20 个字节 - 存放5个整型
int* ptr = (int*)malloc(20);
//检查返回值
if (ptr == NULL)
{
perror("malloc");
return 1;//非正常情况程序结束
}
//使用空间
int i = 0;
for (i = 0; i < 5; i++)
{
*(p + i) = i + 1;
//注意不能是*p++=i+1;
//因为这样会改变p指向的位置,则返回值指向的空间就不是开辟的空间地址了
}
//释放内存
free(p);//传递给free函数的是要释放的内存空间的起始地址
p = NULL;//使用完毕要置空
return 0;
}
对于malloc开辟空间时并不能保证每一次都能成功开辟,存在以下三种情况:
对于情况1空间足够则直接正常开辟空间,对于情况2也有自己的解决方式:我愿称之为另辟蹊径,具体实现如下:
情况3则是无法开辟空间,返回一个空指针NULL。
2.free
上文提及过,动态申请完的空间使用完一定要释放。所以C语言提供了另外一个函数,专门是用来做动态内存的释放和回收的,函数原型如下:
void free (void* ptr);
- 若参数ptr指向的空间不是动态开辟的,则free函数的行为是未定义的;
- 若ptr为空指针NULL,则该函数什么也不做。
注意:malloc和free都声明在头文件stdlib.h中 。
具体使用如上述案例,其中已经使用了free函数。总之就一句话:有借必有还,再借才不难。
3.2calloc和realloc
- calloc
C语言还提供了一个函数calloc,calloc函数也课用于动态内存分配,原型如下:
void* calloc (size_t num,size_t size);
- 该函数的功能是 是为num个大小的size的元素开辟一块空间,并且把空间的每个字节都初始化为0;
- 该函数与malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为全0.
举个例子:
#include<stdio.h>
#include<stdlib.h>
int main()
{
//申请一个存储5个整型的20个字节的空间
int* p = (int*)calloc(5, sizeof(int));
if (p != NULL)
{
int i = 0;
for (i = 0;i < 5;i++)
{
printf("%d ", *(p + i));
}
}
free(p);
p = NULL;
return 0;
}
输出结果:
0 0 0 0 0
由上可知,如果我们对申请的内存空间的内容要求初始化,那么使用calloc可以很方便地完成任务。
2.realloc
- 时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们⼀定会对内存的大小做灵活的调整。那realloc 函数就可以做到对动态开辟内存大小的调整。
- realloc函数可以实现对动态开辟内存空间的二次调整,使得动态内存管理更加灵活。
函数原型如下:
void* realloc (void* ptr,size_t size);
- ptr 是要调整的内存地址;
- size是调整之后内存空间的新大小;
- 返回值为调整之后的内存的起始位置;
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间;
- realloc函数在调整空间存在两种情况:
情况一:原有空间后有足够大的空间
情况二:原有空间后没有足够大的空间
对于malloc并非每次开辟空间都是一帆风顺的,realloc亦是如此,但是realloc对这种情况有着自己的处理方式:
情况1:当是情况1的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
情况2:当是情况2的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找⼀个合适大小的连续空间来使用。这样函数返回的是⼀个新的内存地址。
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* ptr = (int*)malloc(100);
if (ptr != NULL)
{
//业务处理
}
else
{
perror(malloc);
return 1;
}
//扩充容量
int* p = NULL;//确保容量成功扩充
p = realloc(ptr, 1000);
if (p != NULL)
{
ptr = p;
}
//业务处理
free(ptr);
return 0;
}
简单总结一下:其实malloc,calloc,realloc申请的空间在不想使用的时候可以用free进行释放,如果没有用free释放,在程序运行结束的时候也会由OS(操作系统)回收的。
但是还是要尽量做到:1.谁(函数)申请的空间谁释放;2.如果不能及时释放,要告诉后来使用的人要记得释放。
四.柔性数组
很少有人会介绍柔性数组(flexible array)这个概念,但它确实是存在的,C99中,结构体中最后一个元素允许是未知大小的数组,这就叫做柔性数组成员。具体结构如下:
struct type
{
int i;
int a[0];//柔性数组成员
};
若是部分编译器报错可以改成:
struct type
{
int i;
int a[];//柔性数组成员
};
4.1柔性数组的特点
- 结构体中柔性数组成员前面必须至少有一个其他类型成员;
- sizeof返回的这种结构大小不包括柔性数组的内存;
- 包含柔性数组成员的结构用malloc函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
eg:
#include<stdio.h>
#include<stdlib.h>
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
int main()
{
printf("%zd\n", sizeof(type_a));//4
return 0;
}
4.2柔性数组的使用
示例:
#include<stdio.h>
#include<stdlib.h>
//代码1
typedef struct st_type
{
int n;
int a[0];//柔性数组成员
}type_a;
int main()
{
int i = 0;
type_a* p = (type_a*)malloc(sizeof(type_a) + 100 * sizeof(int));
if (p == NULL)//检验一下
{
perror(malloc);
return 1;
}
p->n = 100;
for (i = 0;i < 100;i++)
{
p->a[i] = i;
}
//扩容
type_a* ptr = (type_a*)realloc(p, sizeof(type_a) + 5 * sizeof(int));
if (ptr != NULL)
{
p = ptr;
}
//业务处理...
free(p);
p=NULL;
return 0;
}
这样柔性数组成员a,相当于获得了100个整型元素的连续空间。
4.3柔性数组的优势
上述代码还有如下第二种实现方式:
#include<stdio.h>
#include<stdlib.h>
//代码2
struct S
{
int n;
int* arr;
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S));
if (ps == NULL)
{
return 1;
}
ps->arr = (int*)malloc(5 * sizeof(int));
if (ps->arr == NULL)
{
return 1;
}
//使用
ps->n = 100;
int i = 0;
for (i = 0;i < 5;i++)
{
ps->arr[i] = i;
}
//调整数组大小
int* ptr = (int*)realloc(ps->arr, 10 * sizeof(int));
if (ptr != NULL)
{
ps->arr = ptr;
}
//使用
//...
//释放
free(ps->arr);
ps->arr=NULL;
free(ps);
ps=NULL;
return 0;
}
上述两种代码都可以完成同样的功能,但是方式1的实现有2个好处:
第一个好处:方便内存释放
如果我们的代码是在⼀个给别⼈⽤的函数中,你在⾥⾯做了⼆次内存分配,并把整个结构体返回给用户。用户调⽤free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返回给用户⼀个结构体指针,⽤⼾做⼀次free就可以把所有的内存也给释放掉。一句话来说,malloc和free越少越好。
第二个好处:有利于访问速度
连续的内存有益于提⾼访问速度,也有益于减少内存碎⽚。
以上便是对动态内存管理一些基本概念和简易使用作以介绍,之后将以本篇为基础带大家认识到动态内存开辟中容易犯的一些问题以及经典面试题,望大家多多关注,提出宝贵意见,同时再次希望屏幕前的你能有所收获。
标签:malloc,进阶,int,free,C语言,内存,动态内存,空间,开辟 From: https://blog.csdn.net/2301_80827065/article/details/137430495