文章目录
- 前言
- 一、动态内存管理的意义
- 二、malloc和free
- 三、calloc和realloc
- [四、 常见的动态内存的错误](#四、 常见的动态内存的错误)
- 五、C语言中程序内存区域划分
前言
malloc、calloc、realloc 和 free这些动态内存函数都声明在 stdlib.h 头文件中。
一、动态内存管理的意义
二~三、malloc和calloc函数的对比;malloc、calloc、realloc函数与free结合使用;realloc函数在调整内存空间时的不同情况
四、常见的动态内存的错误
五、C语言中程序内存区域划分(栈区、堆区、静态区和代码段)
一、动态内存管理的意义
在未学习动态内存的知识之前,我们一般会使用以下方式来在内存中开辟空间:
int val = 20;//在内存空间上开辟四个字节
char arr[10] = {0};//在内存空间上开辟10个字节的连续空间
但是上述的开辟空间的方式有两个特点:
• 空间开辟大小是固定的。
• 数组在申明的时候,必须指定数组的长度,数组空间一旦确定了大小不能调整。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知
道,那数组在编译时就开辟好空间的方式就不能满足要求。
于是C语言引入了动态内存开辟,让程序员自己可以申请和释放空间,就比较灵活了。
二、malloc和free
1.malloc
C语言提供了⼀个动态内存开辟的函数,函数原型如下:
void * malloc (size_t size);
这个函数向内存申请⼀块连续可用的空间,并返回指向这块空间的指针。
• 如果开辟成功,则返回⼀个指向开辟好空间的指针。
• 如果开辟失败,则返回⼀个 NULL 指针,因此malloc的返回值⼀定要做检查。
• 返回值的类型是 void * ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
• 如果参数 size 为 0,malloc的行为是标准是未定义的,取决于编译器。
malloc函数的使用示例:
int* p = (int*)malloc(10*sizeof(int));
//用malloc函数动态开辟一块大小为10个整形(40个字节)的内存空间
//如果开辟成功,返回一个指向开辟好空间的指针,指针的类型是void*
//我们想用int*类型的指针p来接收返回值,所以将返回的void*类型指针强制类型转换成int*
2.free
C语言提供了另外⼀个函数free,是专门用来做动态内存的释放和回收的,函数原型如下:
void free (void * ptr);
free函数是专门用来释放动态开辟的内存的。
• 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
• 如果参数 ptr 是NULL指针,则free函数不进行任何操作,程序也不会报错。
free函数的使用示例:
free(p); //释放指针变量p所指向的已分配的动态空间
注:动态开辟了一块空间,但使用完这块空间之后忘记使用free函数释放这块空间,这并不代表这块内存空间就不回收了,当程序退出的时候,系统会自动回收这块内存空间。
但是为了提高代码的运行效率,最好动态开辟的空间使用完之后就立刻用free函数释放掉,并且正确释放,否则在代码运行过程中会造成内存空间被浪费的情况。
示例(malloc函数是要和free函数组合使用的):
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
//用malloc函数动态开辟一块大小为10个整形(40个字节)的内存空间
int* p = (int*)malloc(10 * sizeof(int));
//检查malloc函数返回的地址是否有效(不是空指针才有效)
if (p == NULL)
{
perror("malloc");//打印错误信息
exit(1);//exit()函数的作用是提前退出程序,只要括号内数字不为0都表示异常退出
}
//使用动态开辟的空间
int i = 0;
for (i = 0; i < 10; i++)
{
p[i] = i;//相当于 *(p+i)=i;
printf("%d ", p[i]);
}
printf("\n");
//回收动态开辟的空间
free(p);
p = NULL;//p指向的空间已被释放,交还内存,所以要将p置为NULL,防止其访问不属于该程序的内存
return 0;
}
三、calloc和realloc
1.calloc
C语言还提供了⼀个函数叫 calloc , calloc 函数也用来动态内存分配,函数原型如下:
void * calloc (size_t num, size_t size);
• 函数的功能是为 num 个大小为 size 的元素开辟⼀块空间,并且把空间的每个字节初始化为0。
• 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
calloc函数的使用示例:
int* p = (int*)calloc( 10, sizeof(int));
//用calloc函数动态开辟一块大小为10个整形(40个字节)的内存空间
示例(malloc和calloc函数动态开辟空间时的区别):
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p1 = (int*)malloc(10 * sizeof(int));//malloc不会将动态开辟的空间中的数据初始化
int* p2 = (int*)calloc(10, sizeof(int));//calloc在返回地址之前把申请的空间的每个字节初始化为全0
free(p1);
free(p2);
p1 = p2 = NULL;
return 0;
}
2.realloc
realloc函数的出现让动态内存管理更加灵活。
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们⼀定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小进行调整。
函数原型如下:
void * realloc (void * ptr, size_t size);
• ptr 是要调整的内存地址
• size 是调整之后的新大小
• 返回值为调整之后的内存起始位置。
• 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
realloc函数的使用示例:
int* p = (int*)calloc(10, sizeof(int));
//想将p指向的动态空间加大到20个int的空间,用realloc函数调整空间
int* ptr = (int*)realloc(p, 20*sizeof(int));
realloc在调整内存空间的时候是存在两种情况的:
(1) 情况一:原有空间之后有足够大的空间
(2) 情况二:原有空间之后没有足够大的空间
示例(研究realloc调整内存空间时的两种不同情况):
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
//用calloc函数动态开辟一块大小为10个整形(40个字节)的内存空间
int* p = (int*)calloc(10, sizeof(int));
//检查calloc函数返回的地址是否有效(不是空指针才有效)
if (p == NULL)
{
perror("calloc");
exit(1);
}
//使用动态开辟的空间
int i = 0;
for (i = 0; i < 10; i++)
{
p[i] = i;
}
//想将p指向的动态空间加大到20个int的空间,用realloc函数调整空间
int* ptr = (int*)realloc(p, 20 * sizeof(int));
//检查空间调整是否成功,当ptr!=NULL,证明调整成功,再将调整之后的地址赋给p
if (ptr != NULL)//当ptr==NULL,证明调整失败,就不能把ptr赋给p,否则会丢失之前已开辟空间的地址
{
p = ptr;
}
//回收动态开辟的空间
free(p);
p = NULL;//p指向的空间已被释放,交还内存,所以要将p置为NULL,防止其访问不属于该程序的内存
return 0;
}
情况一(原有空间之后有足够大的空间):
原有空间之后有足够大的空间,要扩展内存就直接在原有空间之后追加空间,原来空间的数据不发生变化。
情况二(原有空间之后没有足够大的空间):
原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样realloc函数返回的是⼀个新的内存地址。
(原有空间中的数据会移动到新开辟的空间,然后原有空间会被释放还给系统)
四、 常见的动态内存的错误
1.对NULL指针的解引用操作
c
int main()
{
int* p = (int*)malloc(INT_MAX*10);
*p = 20;//如果p的值是NULL,对NULL进行解引用就会报错
free(p);
return 0;
}
C/C++中的 <limits.h> 头文件中定义:
#define INT_MAX 2147483647
INT_MAX为 2^31-1 ,即 2147483647 ;
当malloc要开辟的动态空间过大的时候,动态空间的开辟可能会失败,开辟失败就会返回⼀个 NULL 指针。
以上代码中,我们想用malloc开辟一个(INT_MAX*10)个字节的动态空间,这个空间过于庞大,所以开辟失败,返回的是NULL,而对NULL进行解引用是会报错的。所以我们要对上述代码进行改进,在使用malloc开辟了动态空间之后,对malloc返回的指针进行检查,如下:
c
int main()
{
int* p = (int*)malloc(INT_MAX * 10);
//检查malloc函数返回的地址是否有效(不是空指针才有效)
if (p == NULL)
{
perror("malloc");//打印错误信息
exit(1);//exit()函数的作用是提前退出程序,只要括号内数字不为0都表示异常退出
}
*p = 20;
free(p);
return 0;
}
如果p为NULL,证明malloc函数开辟空间失败,我们使用perror函数打印错误信息,可以看到错误原因正是:"没有充足空间",然后用exit函数提前退出程序,因为空间开辟都失败了,后续对开辟的动态空间进行的操作就无需进行了。
2.对动态开辟空间的越界访问
c
int main()
{
int i = 0;
int* p = (int*)malloc(10 * sizeof(int));//动态开辟了10个整形的内存空间
if (p == NULL)
{
perror("malloc");
exit(1);
}
for (i = 0; i <= 10; i++)
{
*(p + i) = i;//循环进行了11次,当访问第11个整形空间时越界访问,程序报错
}
free(p);
p = NULL;
return 0;
}
3.对非动态开辟内存使用free释放
c
int main()
{
int a = 10;
int* p = &a;
free(p);//free只能释放malloc、calloc和realloc动态开辟的空间
return 0;//对非动态开辟的内存使用free会报错
}
4.使用free释放⼀块动态开辟内存的⼀部分
c
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
exit(1);
}
p++;//p不再指向动态开辟空间的起始位置
free(p);//试图用free释放一块动态开辟空间的一部分,程序报错
p = NULL;
return 0;
}
5.对同⼀块动态内存多次释放
c
int main()
{
int* p = (int*)malloc(100);
if (p == NULL)
{
perror("malloc");
exit(1);
}
free(p);
free(p);//对同⼀块动态内存多次释放,程序报错
return 0;
}
6.动态开辟内存忘记释放(内存泄漏)
c
void test()
{
int* p = (int*)malloc(100);//动态开辟了一个100字节的空间之后,p中存储了该空间的地址,
if (p == NULL) //test()函数调用结束后,指向该空间的指针p被销毁,
{ //后续再也找不到此空间,动态空间占用了内存,却无法被使用,
perror("malloc"); //内存被浪费,导致了内存泄漏
exit(1);
}
*p = 20;
}
int main()
{
test();
int arr[100] = { 0 };
int i = 0;
for (i = 0; i < 100; i++)
{
arr[i] = i;
}
return 0;
}
忘记释放不再使用的动态开辟的空间会造成内存泄漏。
切记:动态开辟的空间使用完之后⼀定要释放,并且正确释放。
五、C语言中程序内存区域划分
C程序运行时,操作系统将内存划分为以上图示中的4个区域:
1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
2. 堆区(heap):一般由程序员(使用malloc、calloc、realloc和free函数)分配释放, 若程序员不释放,程序结束时由操作系统回收 。
3. 静态区(数据段):存放全局变量、静态数据。程序结束后由系统释放。
4. 代码段:存放函数体的二进制代码。