本篇将详细的介绍在C语言中的动态内存管理,其中包括为什么要有动态内存分配,已经对应的动态内存函数:malloc、realloc、calloc以及free,这些函数的作用以及这些函数的用法都会详细给出。还会提出在常见的动态内存错误。给出一些和动态内存相关的试题。然后还会介绍柔性数组的概念及用法,以及使用柔性数组的优势。
可提高旁边的目录快速找到自己想看的部分。
除了动态内存的分配,我们还有直接分配内存,有直接申请一个字节(char),4个字节(int)……,还有分配一块内存——数组,如下:
#include
int main() {
//变量类型分配空间
int n = 0;
char c = 'q';
//数组分配空间
int arr[10] = { 0 };
return 0;
}
但是对于这样的内存分配,存在一个缺陷,那就是分配的内存已经固定死了,当我想要给这个数组(变量)在多分配一些内存时,我们只能通过在源代码中修改,在编译时并不能修改 。
即: 空间开辟的大小是固定的。
数组在申明的时候,必须指定数组的长度,数组空间一旦确定了大小就不能在调整。
所以在C语言中引入了动态内存开辟,让我们在编译时也可以申请更多的空间。
首先分析的是这个malloc函数,给出cplusplus网站对于该函数的解释:
由上图可知,使用malloc函数需要包含头文件stdlib.h,其中引入的参数为size_t类型的参数,表明需要申请空间的大小,单位为字节,所以malloc函数的作用就是申请分配内存。malloc函数的用法和注意事项有以下几个点:
1.如果内存开辟成功,那么会返回一共指向开辟好空间的一个指针;
2.如果开辟失败,则返回一个NULL指针,所以对于malloc函数的返回值,我们需要做检查;
3.返回值的类型为void*,表明malloc函数并不知道开辟空间的类型,在具体使用时,由程序员自己来决定(强制类型转换);
4.如果size为0,malloc的行为是标准未定义的,取决于编译器。
以上的几点,将在下面详细解释:
1.开辟成功,返回一个指向开辟好空间的一个指针(地址):
由上图所示,我们先将指针赋值NULL,然后在用malloc函数申请内存空间之后,将分配好的内存空间的首地址赋值给指针变量。
2.开辟失败,返回NULL
当我们在内存中一下申请很多字节的内存时,内存分配不了这么多,返回一个空指针。所以我们在动态内存的分配中,很有可能分配的内存失败,所以我们一般还是需要分配的动态内存进行检查,检查分配的空间是否成功,防止我们对NULL指针的使用。
3.void*指针变量,强制转化。
对于上述对于malloc函数的说明可知,malloc返回的指针类型为void*类型,对于void*类型的指针变量,我们既不可以引用也不可以解引用,所以需要将这个指针类型进行强制转换,才可以进行使用,如下:
对于上述已经从动态内存中分配的空间,我们申请了,当然也需要将内存还回去,这时候我们就需要用free函数将分配好的内存空间进行释放。
在对free函数的讲解前,我们先来看看动态分配的内存空间是从哪里分配的空间,如下图:
如上图所示,内存空间一共分为栈区、堆区以及静态区,其中对于栈区,存储局部变量和函数参数,堆区则存储malloc、calloc、realloc申请的内存,静态区存储全局变量和静态变量。
当程序结束时,操作系统会回收整个进程所占用的内存,包括申请在堆区的内存(也就是malloc、realloc、calloc函数的申请的空间),因此申请的内存会被自动释放回收。
但是尽管在程序结束时会自动释放堆区内存,但是也存在内存泄漏的情况,申请的内存可能一直存在于栈区中,导致内存资源的浪费。
所以为了防止内存泄漏的问题,我们最好使用free函数来手动释放申请的内存,并且将被释放的指针变量置为NULL。
以下为free函数的详细解释:
free函数的返回值为void,函数参数为void*,即可以为任意类型的指针变量。但是要注意,free释放的为动态开辟的内存,如果释放的指针变量指向的内存不是动态开辟的,那么对于free函数的行为是未定义的。当free释放的指针为NULL指针,那么free函数什么都不用做。
如上所示,当我们释放了ptr的内存,但是该指针还是指向原来的地址,说明该指针还能找到那个源地址,但是源地址的信息被释放了, 那么目前这个ptr指针就是一个野指针,所以我们最好将其置为NULL。
接下来我们讲解这个calloc函数,给出cplusplus官网的解释:
该函数返回值同样为void*,函数参数size为每个元素的大小,num为元素的个数,所以分配的内存空间为num*size个字节,分配完之后,将所有字节都初始化为0。这个函数的作用和malloc函数其实非常相似,不同的是,malloc函数并不会将分配的内存初始化。以下总结calloc函数的要点:
1.函数的功能是为num个大小为size的元素开辟一块空间,并把空间的每个字节初始化为0。
2.与函数malloc的区别只在于calloc会返回地址前把申请空间的每个字初始化为全0。
由上图所示,打印出来的值都是0。
以下给出realloc函数在cplusplus网站的定义:
由上图所示,所得realloc函数的介绍及用法:
realloc函数的作用是将原动态分配的内存再次重新分配内存,可以将内存变小也可以变大。realloc函数返回的类型仍为void*,没有具体要求返回的类型,说明和以上的内存分配函数基本一致,返回类型利用强制转换由自己决定。以下将详细讲解realloc函数的要点与细节。
1.传入参数之一ptr:是要调整内存的地址;
2.size为调整之后的新大小;
3.返回值为调整后的内存起始位置;
4.这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新分配的内存空间;
5.realloc在调整内存空间时存在两种情况:情况一:原空间之后存在足够大的空间、情况二:原空间之后没有足够大的空间。
给出以下realloc函数的实例应用:
如上图的两种的使用realloc函数的实例,第一种是使用一个新的指针接收realloc函数返回的参数,但是对于第二种realloc函数用法,是使用原ptr接收来自realloc函数返回的参数。注意:只有第一种用法是对的,对于第二种用法存在安全隐患,用原指针接收新的地址存在的安全隐患就是:当realloc函数申请内存失败之后返回的指针为NULL,但是这时修改了原指针,所以原指针指向的内存地址也没有了,也就是即可能会申请失败,还会导致原地址内容丢失。
对于以上的解释,当realloc返回的函数为失败时,返回的内容为NULL。但是当分配成功时,一共存在两种情况,如下:
如上所示,一共有两种分配内存的方式,第一种:当原地址后面分配的地址不够分配的新地址长度时,会重新分配一块地址内存,赋值给指针,同时会把原地址的内容拷贝过来。第二种: 当原地址后面分配的地址够分配的新地址长度时,会直接在原地址内存的后面分配新的内存。
其实对于realloc函数,也可以使用成malloc函数,也就是,realloc函数的功能包含malloc函数的功能,如下:
int main() {
int* p = (int*)realloc(NULL, 40);
if (p == NULL) {
printf("%s\n", strerror(errno);
return 1;
}
free(p);
p=NULL;
return 0;
}
将原地址置为NULL,那么在使用realloc函数时,会直接在内存中随机分配内存,然后分配给p指针。
以下举出的例子只是举出一些较为常见的例子,但仍然存在许多的错误情况,在此不做出一一列举。
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
如上代码,申请的空间为整型最大值的四分之一,分配的内存空间过大,很可能分配失败,返回NULL指针,此时p指向的地址为NULL,*p——对NULL指针的解引用,错误的写法。
void test() {
int i = 0;
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL) {
exit(1);
}
for (i = 0; i <= 10; i++) {
*(p + i) = i;
}
free(p);
}
对以上函数的调用,存在越界访问的问题,访问到了第十一个整型的位置,而初始时,只分配了10个整型的空间。
void test() {
int a = 10;
int* p = &a;
free(p);
}
直接释放局部变量,会导致程序崩溃。
void test() {
int* p = (int*)malloc(100);
p++;
free(p);
}
对于此情况,我们将动态开辟内存所指向的指针+1,导致在p为起始位置的往后100个字节,有4个字节不是动态开辟的内存,会导致程序崩溃。
void test() {
int* p = (int*)malloc(100);
free(p);
//...一系列操作
free(p);
}
在写程序时,很可能因为忘记进行了什么样的操作之后,继续进行了该操作,当free多次同一块地址时,第二次free时,free的就不在是动态开辟的内存了,所以会导致程序崩溃。
void test() {
int* p = (int*)malloc(100);
if (p != NULL) {
*p = 20;
//...一系列操作
}
}
如上,对分配的内存进行一系列操作之后,并未进行释放空间,虽然程序并不会报错,但这很可能导致内存泄露。 注意:对于所有动态内存分配的内存,在结束程序前都要释放内存,防止内存泄漏。
以下将详细解析有关动态内存的相关题目,加深对以上知识的理解。
检查以下代码存在什么问题?
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
该函数表面上是一个给str指针分配内存,然后将hello world拷贝到已经分配好的内存之中,然后打印出hello world。但是对于该函数真的可以运行吗?以下是运行结果:
对于该test函数,运行出的结果并没有打印出hello world,存在以下几个问题:
1.当我们将指针str传入GerMemory函数时,GerMemory只是将str的值拷贝了一份,并没有拿到str指针的地址,对于拷贝的值分配内存后,当离开这个函数,这个分配的内存和拷贝的值也已经找不到了。(简单来说就是GerMemory函数的参数是一级指针,应该用二级指针来接收一级指针的地址,所以GerMemory函数的参数采用的实际是传值,而不是传址)。
2.既然str没有拿到内存空间,那么strcpy就是对NULL指针的解引用,程序崩溃。
3.对于准备接收的动态内存在结束时并没有释放,以及将该指针置为NULL。
4.没有对动态分配的内存进行判断是否内存分配成功。
将代码修改为以下代码即可成功:
void GetMemory(char** p)
{
*p = (char*)malloc(100);
if(*p==NULL){
assert(*p); //断言,若为NULL,直接结束整个代码
}
}
void Test(void)
{
char* str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main() {
Test();
return 0;
}
检查以下代码存在什么问题?
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
该函数表面上的作用是用GetMemory函数生成一个字符数组,然后将字符数组的地址传给str,最后将str打印出来,以下是运行结果:
最后打出来的形式却是以乱码的形式打印出来。
该函数存在的问题就是:GetMemory函数是在栈区开辟的空间(free部分有讲),当我们离开该函数的时候,栈区里的内容也已经被计算机所处理了,不在是原来的hello world值,而GetMemory还将p的地址返回,这是的p指针也已经是一个野指针了,所以对于一个野指针的打印,肯定会乱码。
以下代码存在什么问题?
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
以上函数表面上看着已经不存在什么错误了,GetMemory的参数也是二级指针,对于strcpy中也没有对NULL指针的解引用,当我们运行时也确实可以打印出hello,但是真正的问题在于,没有对分配的内存进行判断是否分配成功,并且忽略了动态内存最后的处理:释放内存。
以下代码存在什么问题?
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
起始对于该函数存在的问题是显而易见的:
1.没有判断内存是否分配成功就用strcpy函数。
2.free掉str的内存之后,又对str指针用strcpy函数拷贝world。
对于该函数的运行结果为:
为什么会是这样的结果:首先对str指针分配的拷贝了一个hello字符串,然后又将str的内存free掉,但是此时str仍然指向的是原来分配的地址,因为free之后没有将str置为NULL,所以str不为NULL,进入判断语句,strcpy可以找到该地址,也可以拷贝,但是这是一种危险的行为,存在越界访问。
对于柔性数组,顾名思义就是有弹性(柔性)的数组,这个数组的成员个数我们可以改变,但是对于柔性数组的格式也存在要求,如下:
struct soft_arr {
int i;
int a[];//柔性数组成员
//也可以将a[ ]写成a[0]
};
柔性数组的特点:
1.结构体的柔性数组成员前面必须至少存在一个其他类型的成员。
2.如果用sizeof返回这种结构体的大小,计算出的结果不包括柔性数组的成员。
3. 包含柔性数组成员的结构用动态内存malloc(calloc、realloc)进行动态内存分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期⼤⼩。
如上图,计算出来的结果只有4个字节,相当于只计算了int类型的内存大小。
对于柔性数组的初始化以及使用如下:
struct soft_arr {
int i;
int a[];
//也可以将a[ ]写成a[0]
};
int main() {
struct soft_arr* p = (struct soft_arr*)malloc(sizeof(struct soft_arr) + 10 * sizeof(int));
if (p == NULL) {
perror("malloc:");
return 1;
}
p->i = 0;
int i = 0;
for (i = 0; i < 10; i++) {
*(p->a + i) = i;
}
//...other oprations
//需要更多内存时
struct soft_arr* tmp = (struct soft_arr*)malloc(sizeof(struct soft_arr) + 15 * sizeof(int));
if (tmp == NULL) {
perror("realloc");
}
p = tmp;
//...other oprations
free(p);
p = NULL;
return 0;
}
对于柔性数组的内存分配我们使用内存分配函数进行内存分配,首先我们需要将柔性数组原有的内存大小分配给指针p,然后多出来的内存分配其实就是对柔性数组的成员内存进行分配。当我们需要更多的内存时,我们也可以使用realloc函数调节内存。
先给出两组代码,实现的功能一致,但是写法和原理存在区别:
代码一:
struct soft_arr {
int i;
int a[];
//也可以将a[ ]写成a[0]
};
int main() {
struct soft_arr* p = (struct soft_arr*)malloc(sizeof(struct soft_arr) + 10 * sizeof(int));
if (p == NULL) {
perror("malloc:");
return 1;
}
p->i = 0;
int i = 0;
for (i = 0; i < 10; i++) {
*(p->a + i) = i;
}
//...other oprations
//需要更多内存时
struct soft_arr* tmp = (struct soft_arr*)malloc(sizeof(struct soft_arr) + 15 * sizeof(int));
if (tmp == NULL) {
perror("realloc");
}
p = tmp;
//...other oprations
free(p);
p = NULL;
return 0;
}
代码二:
struct hard_arr {
int i;
int* a;
};
int main() {
struct hard_arr* p = (struct hard_arr*)malloc(sizeof(struct hard_arr));
if (p == NULL) {
perror("malloc:");
return 1;
}
p->i = 0;
p->a = (int*)malloc(10 * sizeof(int));
if (p->a == NULL) {
perror("malloc:");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++) {
*(p->a + i) = i;
}
//...other oprations
//需要更多内存时
int* tmp = (int*)malloc(sizeof(15 * sizeof(int)));
if (tmp == NULL) {
perror("realloc");
}
p = tmp;
//...other oprations
free(p->a);
p->a = NULL;
free(p);
p = NULL;
return 0;
}
对于以上两个代码实现的功能一模一样,但是他们又存在什么区别呢?通过以下图例给出解释:
如上图所示,第一块是柔性数组分配的空间形式,第二块是代码二对应的内存分配方式。第一块是直接分配一块连续的内存空间。第二块则是将结构体先申请一块空间(包含整型 i ,指针*a),然后在对指针a随机分配一块新的内存空间。这两种内存分配方式,第一种相对于第二种一共有两个好处:
1.方便内存释放,对于代码一我们只用了一次malloc函数,说明在释放时也只需要free一次,而对于代码二,我们分配了两次的内存,先分配结构体的内存,然后分配结构体内整型指针的内存,那么我们在释放内存的时候也要先将结构体内整型指针的内存释放,然后释放结构体的内存。
2.利于增加访问速度,连续的内存有益于提高访问的速度,同时也有益于减少内存碎片。
内存碎片:
在内存中用malloc分配的内存空间,会在内存中随机分配一块地址,地址与地址之间的小空间就是内存碎片,这些内存碎片通常很难在被利用,所以malloc次数越多,对于空间的利用率越小。
提升访问速度:
以上这个图是CPU在计算机上读取的顺序显示图,首先会从寄存器中读,寄存器没有则向下读取,以此类推,但是在读取数据时存在空间局部性,也就是读取到一个数据会接着沿着这个数据的周围读取数据,对于代码一中的数据类型,一次性就可以将所有的数据读取完。
在c/c++中对于代码中对应变量的存储区域如下图
内核空间:该空间是留给计算机操作系统内核的,我们写出的代码以及其他需要内存的变量都不会存储在这。
代码段:用于存储可执行代码,计算机在执行代码前需要将代码存储转化为二进制序列,其中二进制序列就是存储这一代码段,还有常量字符串这一类的只读常量。
栈区:函数内局部变量的存储单元在栈区存储创建,当函数执行结束时,这些存储单元会自动被释放掉,还会存储函数参数、返回数据、返回地址等。
堆区:用于存储动态开辟的内存变量,程序结束时,系统会自动回收内存,但也很有可能存在内存泄漏的问题。
数据段:数据段就是静态区,可用于存储全局变量,以及静态数据(不管是全局的静态数据还是局部的静态变量,程序结束之后由系统自动释放掉。