文章目录
- 前言
- 为什么存在动态内存分配?
- `malloc` 和 `free`
- 1.malloc
- 2.free
- 3.使用
- `calloc`
- `realloc`
- 常见的动态内存错误
- 1.对NULL指针的解引用操作
- 2.对动态开辟空间的越界访问
- 3.对非动态开辟内存使用free释放
- 4.使用free释放一块动态开辟内存的一部分
- 5.对同一块动态内存多次释放
- 6.动态开辟内存忘记释放(内存泄漏)
- 写在最后
前言
- 动态内存管理函数可以说很好用,但是有些小危险。
- 所谓动态内存分配,就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。 动态内存分配不像 数组 等 静态内存 分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。
- 动态内存函数的头文件都是:
<stdlib.h>
为什么存在动态内存分配?
我们已经掌握的内存开辟方式有:
int val = 20; //在栈空间上开辟四个字节
char arr[10] = {0}; //在栈空间上开辟10个字节的连续空间
但是上述的开辟空间的方式有两个特点:
- 空间开辟大小是固定的。
- 数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配。但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。这时候就只能试试动态存开辟了。
此外:在后面的通讯录的完整实现,以及数据结构的完整实现大都是需要动态内存来实现的。
malloc
和 free
1.malloc
malloc
是C语言提供的一个内存开辟函数,该函数的参数如下:
返回值:
- 这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
- 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
- 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
malloc
开辟的内存空间都是每有初始化的,观察内存如下:
2.free
C语言提供了另外一个函数free
,专门是用来做动态内存的释放和回收的,函数参数如下:
free
函数用来释放动态开辟的内存。- 如果参数
ptr
指向的空间不是动态开辟的,那free
函数的行为是未定义的。 - 如果参数
ptr
是NULL
指针,则函数什么事都不做。
注意:任何只要是开辟的动态内存空间(堆上的),都要free
释放返还给操作系统。
3.使用
malloc
与free
是要共同使用的,有malloc
开辟空间就一定要有free
释放空间,通过上面的函数介绍,接下来结合使用。
例如,这里动态开辟一个能够存放10
个整型的数组:
#include <stdio.h>
#include <stdlib.h> // 对应头文件
int main()
{
// 因为返回的是void*,最好强转以下
int* tmp = (int*)malloc(sizeof(int) * 10); // 也可以直接放一个40(要40字节)
// 一定要检查开辟成功没有
if (tmp == NULL)
{
perror("malloc fail"); // 这里打印错误“开辟失败”
exit(-1); // 这里可以理解为直接退出程序
}
// 开辟没问题,进行以下操作
// 给开辟的数组赋值
for (int i = 0; i < 10; ++i)
{
tmp[i] = i + 1;
}
// 打印
for (int i = 0; i < 10; ++i)
{
printf("%d ", tmp[i]);
}
// 操作完后一定要释放空间
// 传递指向那段空间起始位置的指针
free(tmp);
// 释放后要把该指针置为空,不然后面一不小心又使用该指针找到那块空间,属于非法访问了
tmp = NULL;
return 0;
}
如果后面不释放,虽然现在的机器大都会自动返还给操作系统,但是出于严谨和安全,一定要记得free
,不然会造成内存泄露问题,这是很严重的。
calloc
calloc
也是动态内存分配函数
// 例如这里开辟一个有十个整型元素的数组
int* arr = (int*)calloc(10, sizeof(int));
通过上面的介绍,可以发现,calloc
的功能与malloc
几乎相同,其有两点不同之处:
calloc
与malloc
的函数参数不同;calloc
开辟的空间会将全部元素初始化0
,而malloc
则是随机值。
如:
#include <stdio.h>
#include <stdlib.h> // 对应头文件
int main()
{
// 个数 一个元素的大小
int* tmp = calloc(10, sizeof(int));
// 一定要检查开辟成功没有
if (tmp == NULL)
{
perror("calloc fail"); // 这里打印错误“开辟失败”
exit(-1); // 这里可以理解为直接退出程序
}
// 开辟没问题,进行以下操作
// 给开辟的数组赋值
for (int i = 0; i < 10; ++i)
{
tmp[i] = i + 1;
}
// 打印
for (int i = 0; i < 10; ++i)
{
printf("%d ", tmp[i]);
}
// 操作完后一定要释放空间
// 传递指向那段空间起始位置的指针
free(tmp);
// 释放后要把该指针置为空,不然后面一不小心又使用该指针找到那块空间,属于非法访问了
tmp = NULL;
return 0;
}
所以如何我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。
realloc
realloc函数的出现让动态内存管理更加灵活。
- 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候管理内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的灵活调整。
基础点:
ptr
是要调整的内存地址。size
调整之后新大小。- 返回值为调整之后的内存起始位置。
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间。
- realloc在调整内存空间的是存在两种情况:
情况1
: 内存中原有的空间之后有足够的空间来存放重新开辟的新大小的空间,这时直接在原有的空间之后追加空间。
情况2
: 内存中原有的空间之后没有足够的空间来存放重新开辟的新大小的空间,这时在堆上另找一个合适大小的连续空间来使用。
那么我们如何来写代码呢?
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* tmp = (int*)malloc(100);
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
//扩展容量
//代码1
tmp = (int*)realloc(tmp, 1000);//这样可以吗?(如果申请失败会如何?)
///
//代码2
int* p = realloc(tmp, 1000);
if (p == NULL)
{
perror("realloc fail");
exit(-1);
}
tmp = p;
// 释放
free(tmp);
tmp = NULL;
return 0;
}
- 上面有两种写法,代码
1
跟代码2
。
- 分析代码1:如果重新开辟的空间开辟成功,并且是在原空间上做修改,那么这是可行的;如果原空间后面没有足够空间来开辟,另寻找到一份空间来存放,此时的地址空间的起始地址发生了改变,如果空间申请失败,而此时又将该空间的起始地址给了原有的指针变量tmp,这时原有空间就找不到了,并且会出现错误,所以还是不严谨的;
- 分析代码2:代码2是先将重新开辟的空间的起始地址交给一个临时变量,在判断这份空间的有效性,最后才赋值给原有的指针变量,这样做才是最安全且不会亏损原有空间的,所以,根据代码2的严谨性强的特点,以后realloc一定要写代码2这种样式。
有了
realloc
buff的加持,我们想让数组变他就嘚变,哈哈哈
常见的动态内存错误
1.对NULL指针的解引用操作
void test()
{
int *p = (int *)malloc(INT_MAX/4);
// 这里没有判断是否开辟成功
*p = 20; //如果p的值是NULL,就会有问题
free(p);
p = NULL;
}
2.对动态开辟空间的越界访问
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
exit(-1);
}
for(i = 0; i <= 10; i++)
{
*(p+i) = i;//当i是10的时候越界访问
}
free(p);
p = NULL;
}
当i
为10
就越界访问了,越界访问的后果就不用多说了把(哈哈哈哈哈,非法闯入)。
3.对非动态开辟内存使用free释放
void test()
{
int a = 10;
int *p = &a;
free(p); //ok?
p = NULL;
}
free是不能释放除动态开辟的内存以外的内存的,只适用于堆上。
4.使用free释放一块动态开辟内存的一部分
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
p = NULL;
}
free这样子释放相当于拦腰截断,会存在内存泄漏的问题。
5.对同一块动态内存多次释放
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
p = NULL;
}
对同一块空间多次释放,这当然是不行的。
6.动态开辟内存忘记释放(内存泄漏)
这样是绝对不行的,内存泄漏迟早会吃光你的内存。
例如:
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
// p指向的动态内存空间没有释放,虽然p变量销毁了,但申请的空间还在
return 0;
}
写在最后
动态内存分配是不是很容易就学会了,接下来就可以 ”肆无忌惮“ 的玩弄 ”数组“ 了,不过要小心内存泄漏噢!
感谢阅读本小白的博客,错误的地方请严厉指出噢!