文章目录
- 引言
- malloc函数
- calloc函数
- realloc函数
- free函数-避免内存泄漏
- 常见的动态内存错误
引言
如果我们被问道:如何创建一个可以根据用户需求来开辟大小的数组?
可能有些博友会写出如下代码:
#include <stdio.h>
int main()
{
int n = 0;
scanf("%d", &n);
int arr[n];
return 0;
}
这个代码在C99标准下是可以运行的,但大多数编译器并不支持C99标准,所以这种代码缺乏了跨平台性(可移植性),那么我们有没有办法写出一个既可以满足题目要求,又可以在任何一个编译器下都编译得过去的代码呢?
答案是肯定的。这就和C语言中的动态内存的开辟有关了,动态开辟,即可以按照需求开辟内存的大小。
下面,我向博友们介绍几个操作动态内存的常用函数。
注:下面介绍的几个函数的操作对象都是堆区的内存。
扩展:局部变量存放在内存中的栈区;全局变量、静态变量(static修饰的变量)存放在内存中的静态区(也叫数据段)。
malloc函数
void *malloc( size_t size );
malloc函数的功能是开辟指定字节大小的内存空间,如果开辟成功就返回该空间的首地址,如果开辟失败就返回一个NULL。传参时只需传入需要开辟的字节个数。
假设我们要开辟一个可以存放10个整型的空间:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
//因为malloc函数的返回值为void*,所以需要强制类型转换为对应类型。
if (p == NULL)
{
printf("内存开辟失败\n");
}
else
{
printf("内存开辟成功\n");
//使用...
//使用结束,释放内存(后面介绍)
free(p);
p = NULL;
}
return 0;
}
注:malloc函数开辟好空间后,不对空间内容做任何初始化,所以空间内的数据为随机值。
calloc函数
void *calloc( size_t num, size_t size );
calloc函数的功能也是开辟指定大小的内存空间,如果开辟成功就返回该空间的首地址,如果开辟失败就返回一个NULL。但calloc函数传参时需要传入两个参数(开辟的内存用于存放的元素个数和每个元素的大小)。
calloc函数与malloc函数的用法也是大同小异:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)calloc(10 , sizeof(int));
//开辟一个可以存放10个整型的内存空间
if (p == NULL)
{
printf("内存开辟失败\n");
}
else
{
printf("内存开辟成功\n");
//使用...
//使用结束,释放内存(后面介绍)
free(p);
p = NULL;
}
return 0;
}
注:calloc函数与malloc函数的最大区别在于:calloc函数开辟好内存后会将空间内容中的每一个字节都初始化为0。
realloc函数
void *realloc( void *memblock, size_t size );
realloc函数可以调整已经开辟好的动态内存的大小,第一个参数是需要调整大小的动态内存的首地址,第二个参数是动态内存调整后的新大小。
realloc函数与上面两个函数一样,如果开辟成功便返回开辟好的内存的首地址,开辟失败则返回NULL。
如果我们要将已开辟的动态内存大小做一些调整,我们会这样做:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
printf("内存开辟失败\n");
}
else
{
printf("内存开辟成功\n");
//使用...
//扩展容量
int* ptr = (int*)realloc(p, 100);
//将空间扩展为100个字节大小
if (ptr != NULL)
{
p = ptr;//开辟成功时
//使用...
}
//使用结束,释放内存(后面介绍)
free(p);
p = NULL;
}
return 0;
}
realloc函数调整动态内存大小的时候会有三种情况:
假如我们要将一个大小为50个字节的空间调整为100个字节。
情况一:
此时,realloc函数直接在原空间后方进行扩展,并返回该内存空间首地址(即原来的首地址)。
情况二:
此时,realloc函数会在堆区中重新找一块满足要求的内存空间,把原空间内的数据拷贝到新空间中,并主动将原空间内存释放(即还给操作系统),返回新内存空间的首地址。
情况三:
此时,需扩展的空间后方没有足够的空间可供扩展,并且堆区中也没有符合需要开辟的内存大小的空间。结果就是开辟内存失败,返回一个NULL。
free函数-避免内存泄漏
void free( void *memblock );
free函数的作用就是将malloc、calloc以及realloc函数申请的动态内存空间释放,其释放空间的大小取决于之前申请的内存空间的大小。
其使用方式非常简单,就是在使用完后加上两个语句:
free(p);//p为要释放的代码块的首地址
p = NULL;//必不可少
我们也已经看到了,上面每一个开辟了动态内存的代码,在使用完该动态内存后,都将该内存空间释放了(即还给操作系统)。
如果在使用完动态内存后忘记将其空间释放,便会造成内存泄漏的问题:
1>
内存泄漏(MemoryLeak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。2>
内存泄漏缺陷具有隐蔽性、积累性的特征,比其他内存非法访问错误更难检测。因为内存泄漏的产生原因是内存块未被释放,属于遗漏型缺陷而不是过错型缺陷。此外,内存泄漏通常不会直接产生可观察的错误症状,而是逐渐积累,降低系统整体性能,极端的情况下可能使系统崩溃。
注意:
1> 在释放代码块后,必须将该代码块的首地址改为NULL,否则该指针将变为野指针,我们都知道,野指针非常危险。
2> 如果传入free函数的为空指针(NULL),则free函数什么也不做。
常见的动态内存错误
一、对NULL指针进行解引用操作
我们都知道,不能对NULL指针进行解引用操作,但是如果我们在调用了malloc、calloc以及realloc函数之后,没有检测返回的指针的有效性(即是否为NULL指针),那我们在后面使用该指针的时候就可能会导致对NULL指针进行解引用操作。
二、对动态开辟空间的越界访问
我们需要时刻注意,不能访问未申请的动态内存空间。比如你向动态内存申请了10个字节,那就绝不能访问第11个字节。
三、对非动态开辟的内存使用free释放
free函数只能释放动态开辟的内存空间。
四、使用free释放动态开辟内存的一部分
free函数只能从开辟好的动态内存空间的起始位置开始释放,所以使用free函数释放动态内存时,传入的指针必须是当时开辟内存时返回的指针。
五、对同一块内存多次释放
对同一块动态内存空间只能释放一次。避免这个问题的出现也很简单,我们只要记住在第一次释放完空间后立即将该指针置为NULL即可,因为当传入free函数的指针为NULL指针时,free函数什么也不做(也就不会出现对同一内存多次释放的问题)。
六、动态开辟内存忘记释放(内存泄漏)
一定要做到自己开辟的动态内存自己记得释放,也许你觉得这件事没什么,但当你需要从几十万甚至几百万行代码中找出一个因忘记释放动态内存而造成的内存泄漏问题时,你就会真正知道这件事的重要性。