C语言:动态内存管理
前言
在还没有接触过动态开辟空间方法时,通常开辟空间的方法为创建一个 变量 或是 开辟一块连续的空间大小的数组 。
这种开辟空间的方式特点为:
- 空间开辟大小是固定的。
- 定义一个数组必须指定数组长度,所需的内存在编译时分配。
但是在程序运行时,对数组的编译时开辟空间需求往往是不能满足的。数组空间开辟大了造成空间浪费,对于空间开辟小了所存储数据得不到很好保存。所以这个时候就得引入动态内存开辟方式了,需要多少开多少,避免供过于求或者求过于供的不足。
在这里简单介绍一下C中内存划分的情况:
动态内存函数
在使用动态内存函数时我们要包含 #include <stdlib.h> 这个头文件
1. 函数 malloc
在C语言库函数中提供了一个开辟动态内存的函数:
malloc这个函数向内存申请一块连续可用的空间,并且返回指向这块空间的指针。
void* malloc (size_t sizeof)
· 如果开辟成功,返回一个指向开辟好空间的指针。
· 如果开辟失败,返回空指针NULL,因此凡是动态开辟的内存都要对其返回值进行检查。
· 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候要根据实际进行数据类型转换。
· 如果参数 size 为0,malloc的行为是标准是未定义的,取决于使用者的编译器。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int num = 0;
scanf("%d", &num);
int* ptr = (int*)malloc(num*sizeof(int)); // 用ptr来存放开辟的内存地址
if(NULL != ptr) // 判断是否开辟成功
{
// 使用开辟的空间
}
return 0;
}
这样一段代码是否存在问题呢?
在前面我们也了解了动态开辟的空间是在堆区的,堆区开辟的空间是需要我们手动还回系统的。没错上面代码就是开辟了动态内存但是没有释放,虽然在这里我们写的整个代码很简单也很容易发现在这里main函数结束后整个程序就结束,开辟的空间也就还回去了。但,这样的想法是否就解决问题了呢?假设要是在一个服务器中,有一个函数负责开辟内存,但是一直开辟内存却没有还回去,内存越用越少,而且服务器中的生命周期也是非常长,那么这样我们也等待服务器生命周期结束然后等程序内存自动还回去吗?这样只会导致内存泄漏,最后整个服务器崩溃是得不偿失的。
那么怎么将空间还给操作系统呢?
2. 函数free
C语言提供了函数 free专门是用来做动态内存的释放和回收的。
void free (void* ptr)
1.如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
2.如果参数 ptr 是NULL指针,则函数什么事都不做
于是上面代码应该完善:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int num = 0;
scanf("%d", &num);
int* ptr = (int*)malloc(num*sizeof(int)); // 用ptr来存放开辟的内存地址
if(NULL != ptr) // 判断是否开辟成功
{
// 使用开辟的空间
}
free(ptr);//释放ptr所指向的动态内存
return 0;
写到这里只能说还不够完善,为什么这样说呢?
ptr指向的空间虽然已经还回去了,但是ptr仍然记录了这块空间的地址,是否造就了野指针?假设这个时候我们已free(ptr),到后面我们忘记了前面已经free过,再次多此一举free(ptr),那么就对同一块动态内存空间进行多次释放这样操作是非法的。
所以通常释放过一块动态内存空间后我们要将指向这块空间的指针置空:ptr = NULL
使用free的注意事项
在使用函数free时我们得注意以下几点:
1.不能对同一块动态内存空间进行多次释放
2. 不能对非动态开辟内存使用free释放
3. 不能使用free释放一块动态开辟内存的一部分
第一点我们前面已近提到过了,下面我们来看看2,3点的问题。
不能对非动态开辟内存使用free释放
我们来看看下面这样一段代码:
int main()
{
int m = 0;
int* p = &m;
free(p);
return 0;
}
运行起来看看效果
程序直接崩溃了,free是用来释放动态开辟内存空间的,动态内存空间在堆区。而我们刚刚创建m变量是在栈区,用操作堆区的函数来释放栈区的空间这显摆的就是瞎扯淡,编译器都不知道怎么继续下去直接卡死,因此我们得杜绝这样的事情发生
不能使用free释放一块动态开辟内存的一部分
我们来看看这样一段代码
int main()
{
int i = 0;
int* ptr = (int*)malloc(sizeof(int)*10);//开辟10个int类型大小动态内存空间
if (ptr == NULL)
{
return 1;
}
for (i = 0; i < 5; i++)
{
*ptr = i;
ptr++;
}
free(ptr);
ptr = NULL;
return 0;
}
运行后的结果:
程序依旧崩溃了,这里又是什么原因呢?
通常使用free的时候,释放的指针都是开辟的动态的起始地址。而现在指针指向的位置发生了改变,指向了动态开辟内存的一部分,找不到起始地址了。这个时候free是从改变位置开始释放的话还是按照10个int大小字节那么后面不属于你的内存也会被释放掉,因此程序会挂掉,这样的问题我们也要杜绝。
3. 函数 calloc
calloc也是C语言提供的一个动态内存开辟函数,函数calloc与函数malloc用法有些许差异,但是可以说是两兄弟。
void* calloc (size_t num, size_t size)
函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
函数calloc与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int i = 0;
int* ptr = (int*)calloc(10, sizeof(int));
if (NULL == ptr)
{
perror("calloc error\n");
return 1; //开辟失败我们直接退出
}
for(i = 0; i < 10; i++)
{
printf("%d ", *(ptr + i));
}
free(ptr); //使用动态开辟的空间都要释放
ptr = NULL;
return 0;
}
那我们再来看看malloc开辟的空间是否有初始化值
#include <stdio.h>
#include <stdlib.h>
int main()
{
int i = 0;
int* ptr2 = (int*)malloc(sizeof(int) * 10);
if (NULL == ptr2)
{
perror("malloc error\n");
return 1; //开辟失败我们直接退出
}
for (i = 0; i < 10; i++)
{
printf("%d ", *(ptr2 + i));
}
free(ptr2); //使用动态开辟的空间都要释放
ptr2 = NULL;
return 0;
}
所以在使用malloc和calloc这两个函数时看使用者是否需要将其空间进行初始化来选择这两个函数。
4. 函数 realloc
前面提到的malloc和calloc这两个函数在堆区开辟空间大小也是固定的,就是说开辟好后不能进行往后扩容。那么怎么对一块空间进行灵活调整呢?下面就来介绍一下函数realloc对动态开辟内存大小是怎么样来调整的。
void* realloc (void* ptr, size_t size)
- ptr是要调整的内存地址
- size调整之后新大小
- 返回值为调整之后的内存起始位置
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间
#include <stdio.h>
#include <stdlib.h>
int main()
{
int i = 0;
int* ptr = (int*)malloc(sizeof(int) * 10);
if (NULL == ptr)
{
perror("malloc error\n");
return 1; //开辟失败我们直接退出
}
//使用ptr开辟空间
{
...
}
//此时我们发现malloc开辟的空间不够用了
ptr = (int*)realloc(ptr, sizeof(int)*20);
//使用扩容空间
{
...
}
free(ptr); //使用动态开辟的空间都要释放
ptr = NULL;
return 0;
}
按照定义用realloc也太简单了些吧!在这里我们得考虑一下realloc重新开辟的空间是否成功了。
函数realloc若是开辟成功则返回这块空间的起始地址,若是开辟失败则返回空指针NULL;
所以在这里可不敢直接将ptr用来直接接收realloc返回值,倘若realloc开辟失败了,那么我们原来malloc开辟空间内容就找不到了。我们要重新创建一个指针变量来接收realloc,防止开辟失败导致内容丢失。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int i = 0;
int* ptr = (int*)malloc(sizeof(int) * 10);
if (NULL == ptr)
{
perror("malloc error\n");
return 1; //开辟失败我们直接退出
}
//使用ptr开辟空间
{
...
}
//此时我们发现malloc开辟的空间不够用了
int* p = (int*)realloc(ptr, sizeof(int)*20);
if(p != NULL)
{
ptr = p;
p = NULL;
}
//使用扩容空间
{
...
}
free(ptr); //使用动态开辟的空间都要释放
ptr = NULL;
return 0;
}
realloc调整空间大小的两种情况
我们都知道动态内存分配开辟都是在堆区,堆区内部空间分布也是我们无法预知的。有的空间很大,有的空间很小,也有的空间被占用。所以realloc在调整空间时就分成两种情况:
情况一:malloc开辟空间的时候在堆区找到的空间很大乃至后面都没有空间被占用,那么realloc就可以直接在其后面进行扩容调整,原来空间的数据不发生变化。
下面以realloc需要调整80字节为例:
情况二:malloc开辟空间的时候在堆区找到的空间很小只够自己开辟使用,此时realloc就不能直接在malloc开辟的后面追加了,需要重新在堆区找一块足够大的内存空间来满足realloc开辟新的空间内存,然后把数据拷贝到这个新开辟的内存空间上,并且返回这个新开辟内存空间的起始地址。
下面以realloc需要调整80字节为例:
realloc当malloc使用
在使用realloc时我们都要传入一个已经有指向动态内存空间地址的指针,当然我们也可以不传。那么不传函数参数空着也不行啊,我们可以放入空指针NULL这样realloc的用法就如同malloc一样了。
int* ptr = (int*)realloc(NULL, sizeof(int)*10);
空指针指向内存肯定是不够我们想要开辟的空间大小,那么realloc就会重新找到新的内存空间进行开辟,这样的功能不就像malloc直接在堆区找到一块动态内存空间吗?所以realloc功能可以是用来调整动态内存大小,也可以是用来直接开辟一块新的动态内存空间。
5. 柔性数组
柔性数组是C99中增添的内容,结构中的最后一个元素允许是未知大小的数组,这个叫做柔性数组成员。
那么为什么叫柔性数组呢?起初听到这个名词也是有点懵,柔这个词应该是可变的意思吧。下面我们就来看看什么是柔性数组:
struct S1
{
int n;
int arr[10]; //确定了结构体最后成员数组元素大小不叫柔性数组
};
struct S2
{
int n;
int arr[]; //这个结构体最后成员数组元素大小未知,为柔性数组
};
有些编译器会跑不过结构体S2,我们得换一种写法
struct S3
{
int n;
int arr[0]; //柔性数组
};
柔性数组的特点:
- 结构中的柔性数组成员前面必须至少一个其他成员
- sizeof 返回的这种结构大小不包括柔性数组的内存
- 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
特点1比较好理解为语法要求,我们来看看特点2和特点3。
struct S3
{
int n;
float num;
int arr[0]; //柔性数组
};
int main()
{
printf("%d",sizeof(struct S3)); // 8
return 0;
}
这里得到的大小为8,刚好大小为前两个结构体成员大小之和。
按照柔性数组特点的定义,我们想要创建一个柔性数组就必须用动态内存分配函数malloc来进行创建。具体用法如下:
struct S3
{
int n;
float num;
int arr[0]; //柔性数组
};
int main()
{
struct S3* ps = (struct S3*)malloc(sizeof(struct S3) + sizeof(int) * 10);
//结构体前两个成员大小加上想要开辟的柔性数组大小
if (NULL == ps)
{
return 1;
}
//...
free(ps);
ps = NULL;
return 0;
}
C语言的动态内存函数就介绍到这里了,感谢大家支持!!!