提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、为什么要进行动态内存分配
- 二、实现动态内存分配的三种库函数
- (一)、malloc函数
- (二)、calloc函数
- (三)、realloc函数
- (四)、free函数
- 三、常见的动态内存的错误
- 四、有关动态内存分配的经典题型分析
- 五、柔性数组的介绍(前面没有见到过)
- (一)、柔性数组的概念
- (二)、柔性数组的特点
- (三)、柔性数组的使用与优势
- 六、C语言中内存区域的划分
- 总结
前言
提示:这里可以添加本文要记录的大概内容:
提示:以下是本篇文章正文内容,下面案例可供参考
本文主要介绍C语言中的动态内存管理相关内容,它是后面学习数据结构的基础,至关重要。在本文中首先会介绍为什么要使用动态内存,它有什么好处,然后介绍三种实现动态内存分配的库函数(malloc、calloc、realloc);而后介绍在动态内存分配中常常出现的几种问题;紧接着分析几道有关动态内存分配的习题,再者介绍一下柔性数组(属于新概念型),最后讲一下C语言中程序内存区域的划分
一、为什么要进行动态内存分配
- 我们在学习动态内存分配之前,我们之前用的内存开辟方式都是非动态内存开辟,它们都是在栈区开辟的内存空间
int val=20;//在栈空间上开辟四个字节的空间
char arr[10]={0};///在栈空间上开辟10个字节的连续空间
上述在栈区开辟的空间有两个特点:
一是空间开辟大小是固定的;
二是数组在声明的时候,必须指定数组的长度,数组空间一旦确定了,大小就不能调整了。
- 但是我们对于空间的需求,不仅仅是上述的情况,有时候我们需要的空间大小要根据程序允许需要才知道使用多大的空间,那么数组的编译时开辟空间的方式就不能满足了,故而我们引入了动态开辟,让我们自己可以申请和释放空间,形式就比较灵活了。
二、实现动态内存分配的三种库函数
(一)、malloc函数
头文件是<stdlib.h>
- 函数原型:
void* malloc (size_t size);
- 关于此函数注意以下几点:
这个函数是向内存申请一块连续可用的空间,并返回这块空间的指针;
size的单位是字节;
如果开辟成功,则返回一个开辟好的空间的指针;
如果开辟失败,则返回一个NULL指针,因此我们在使用malloc函数后一定要进行检查是否开辟成功;
该函数的返回值类型是void*,所以我们用malloc函数后要根据需要进行强制类型转换成我们所需要的类型;
如果size为0,malloc行为是未定义的,取决于编译器。
#include<stdlib.h>
#include<string.h>
void test()
{
int*p=(int*)malloc(20);//动态申请空间
//判断是否申请成功
if(p==NULL)
{
perror("malloc");//告诉大家是malloc那一步出的问题
return ;
(二)、calloc函数
头文件是<stdlib.h>
- 函数原型:
void* calloc (size_t num, size_t size);
- 关于此函数注意以下几点:
函数的功能是为num个字节为size的元素开辟一块空间,并把空间的每个字节初始化为0;
与函数malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化全为0
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p = (int*)calloc(10, sizeof(int));
if(NULL != p)
{
int i = 0;
for(i=0; i<10; i++)
{
printf("%d ", *(p+i));
}
}
free(p);
p = NULL;
return 0;
}
运行结果如下:
故而我们如果要对申请的内存空间要求初始化的时候,我们可以调用calloc函数来实现。
(三)、realloc函数
头文件是<stdlib.h>
- 函数原型:
void* realloc (void* ptr, size_t size);
- 关于此函数注意以下几点:
其一:realloc函数的出现让动态内存管理更加灵活啦,有时候我们会发现申请的空间太小了,或者申请的空间太大了,那么未来合理的使用内存,我们一定会对内存的大小进行一定的调整,realloc函数可以实现对动态开辟的内存大小进行调整;
其二:上述函数原型中,ptr是要调整内存的起始地址,size是调整之后的大小,返回值是调整之后内存的起始位置;
其三:这个函数在调整原来内存空间大小的基础上,还有将原来内存中的数据移动到新的空间里去; - realloc在调整内存空间的时候存在以下两种调整情况
其一:原有空间之后足够大;
#include<stdlib.h>
int main()
{
int*ptr=(int*)malloc(20);
if(ptr==NULL)
{
return 1;
}
int*tmp=(int*)realloc(ptr,40);
return 0;
}
我们对ptr指针进行扩容处理
ptr后面的空间足够大,我们可以直接进行扩容,这样原来空间的数据不发生变化。
其二:原有空间之后没有足够大的空间
遇到这种情况我们就要注意了!!原有空间之后没有足够大的空间,它的扩容方法为:在堆空间上另外找一个合适大小的空间来使用,这样函数返回的是一个新的内存地址,这还涉及到将原有的数据粘贴到新空间的问题
我们在写代码的时候最好重新定义一个指针将扩容后的结果放到新的指针里,这样是为了避免扩容失败后原来的数据丢失
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int*ptr=(int*)malloc(20);
if(ptr==NULL)
{
return 1;
}
int*p=NULL;//重新定义一个变量
p=(int*)realloc(ptr,1000);
if(p==NULL)
{
malloc("realloc");//显示错误原因
return 1;
}
ptr=p;
free(ptr);
ptr=NULL;
}
(四)、free函数
头文件是<stdlib.h>
只要涉及使用到以上三中动态内存分配函数,当我们不用到我们申请的内存空间后,一定要通过free函数释放掉, 并将指针置空(避免出现野指针问题)(养成好习惯)
- 函数原型:
void free(void*ptr);
- 关于free函数注意以下几点:
如果参数ptr指向的空间不是动态开辟的,那么free函数的行为是未定义的;
如果参数ptr是NULL指针,那么函数相当于什么也没做。
#include <stdio.h>
#include <stdlib.h>
#include<string.h>
int main()
{
int num = 0;
scanf("%d", &num);
int arr[num] = {0};
int* ptr = NULL;
ptr = (int*)malloc(num*sizeof(int));
if(ptr==NULL)
{
perror("malloc");
return 0;
}
int i = 0;
for(i=0; i<num; i++)
{
*(ptr+i) = 0;
}
free(ptr);//释放ptr所指向的动态内存
ptr=NULL;//避免野指针问题。
return 0;
}
三、常见的动态内存的错误
- 对NULL指针的解引用操作
void test()
{
int*p=(int*)malloc(20);
*p=20;//如果p动态内存没有开辟成功就会出现对空指针的解引用问题
free(p);
p=NULL;
}
所以我们在对p进行解引用前,一定要判断一下是否内存开辟成功了,如果开辟成功的话,才能进行接下来的一系列操作,如果没有成功,应该立即结束程序
修改如下:
void test()
{
int*p=(int*)malloc(20);
if(p==NULL)
{
perror("malloc");//告诉大家是malloc那一步出的问题
return ;
}
*p=20;//如果p动态内存没有开辟成功就会出现对空指针的解引用问题
free(p);
p=NULL;
return ;
}
这样就完美啦,不会出现对空指针进行解引用的问题。
- 对动态开辟空间的越界访问
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
perror("malloc");
return;
}
for(i=0; i<=10; i++)
{
*(p+i) = i;//当i等于10的时候很显然出现了越界访问。
}
free(p);
p=NULL;//避免出现野指针问题
}
对于以上代码,我们将p指针所指向的空间开辟成40个字节大小,但是我们下面用for进行遍历赋值的时候,当i等于10的时候,我们越界访问了未开辟的空间,这就出现了问题。
我们只能访问到i=9。
- 对非动态开辟内存使用free释放,程序会出现卡死的现象,是比较严重的问题,一定要避免
#include<stdio.h>
#include<stdlib.h>
void test()
{
int a=10;
int*p=&a;
free(p);
}
int main()
{
test();
}
很显然局部变量a是存在于栈区的,我们free只能释放堆区(动态开辟的内存),不能释放栈区(非动态开辟的内存)的内存,否则会出现程序卡死的现象
- 使用free释放一块动态开辟内存的一部分
#include<stdio.h>
#include<stdlib.h>
void test()
{
int *p = (int *)malloc(100);
if(p==NULL)
{
perror("malloc");
return ;
}
p++;
free(p);//p不再指向内存的起始位置
}
int main()
{
test();
}
这里我们对p指针进行了自加加运算后,p就不再指向动态内存的起始位置,这样我们释放的时候就指释放了动态开辟内存的一部分,这一定会出现程序卡死的。所以我们再进行指针操作的时候一定要保存动态内存的起始位置。为了方便我们后续的内存释放,修改代码如下:
#include<stdio.h>
#include<stdlib.h>
void test()
{
int *p = (int *)malloc(100);
if(p==NULL)
{
perror("malloc");
return ;
}
int*j=p;//记录动态内存开辟的起始位置。
p++;
free(j);//p不再指向内存的起始位置
}
int main()
{
test();
}
- 对同一块动态内存多次释放
#include<stdio.h>
#include<stdlib.h>
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);
}
int main()
{
test();
}
我们对动态开辟的p指针进行两次内存释放,这会出现程序卡死的现象,如果我们在第一次释放后,将p置为空指针,那么再释放多少次都不会报错
#include<stdio.h>
#include<stdlib.h>
void test()
{
int *p = (int *)malloc(100);
free(p);
p=NULL;
free(p);
}
int main()
{
test();
}
但是这种操作没有什么必要,我们只需记住,不能对同一块内存空间进行多次释放即可
- 动态开辟内存忘记释放(造成内存泄漏)
#include<stdio.h>
#include<stdlib.h>
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
}
在这个程序中,我们调用完test函数后,在test函数里面动态开辟的内存空间,没有得到释放,这样就会造成内存泄漏的问题,切记:动态内存开辟的空间一定要释放,并且正确释放,修改如下:
#include<stdio.h>
#include<stdlib.h>
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
free(p);
p=NULL;//注意释放完要置空,避免出现野指针的问题
}
int main()
{
test();
}
四、有关动态内存分配的经典题型分析
- 练习1:分析以下代码有什么问题
#include<stdio.h>
#include<stdlib.h>
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");//这里存在对NULL指针进行解引用,程序会崩溃,而且还存在内存泄露
printf(str);
}
int main()
{
test();
return 0;
}
这个代码存在大问题,运行的时候出现了崩溃的现象,主要存在两处内存错误,第一个是对空指针进行了解引用操作,第二个是没有释放动态开辟的内存空间,最主要的原因是我们在调用GetMemory函数的时候,没有传指针的地址,这样是无法给str进行内存分配的,修改方法有两种
法1:使用二级指针方法:
#include<stdio.h>
#include<stdlib.h>
void GetMemory(char** p)
{
*p = (char*)malloc(100);
}
void test(void)
{
char* str = NULL;
GetMemory(&str);//传入的是指针str的地址
strcpy(str, "hello world");
printf(str);
free(str);//防止内存泄漏
str = NULL;//避免野指针问题
}
int main()
{
test();
return 0;
}
运行结果如下:
在调用GetMemory函数的时候我们传的是str指针的地址,在接收的时候我们用的是二级指针这样就可以有效的对str进行内存分配,使用完毕后,我们free掉str,并把str进行置空操作,这样有效的避免了内存泄漏和野指针问题。
法2:使用return返回的方法:
#include<stdio.h>
#include<stdlib.h>
char* GetMemory()
{
char*p = (char*)malloc(100);
return p;
}
void test(void)
{
char* str = NULL;
str=GetMemory();
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main()
{
test();
return 0;
}
运行结果如下:
在这里我们重新定义了GetMemory函数的返回类型,改为char*类型,这样我就可以用str来接收,间接性的对str进行了动态内存分配,最后将str给free掉,并将str置空,避免了内存泄漏与野指针问题。
- 练习2:分析以下代码有什么问题:
#include<stdio.h>
#include<stdlib.h>
char* GetMemory(void)
{
char p[] = "hello world";//在栈区,这块空间只在函数运行时候才分配
return p;
}
void test()
{
char* str = NULL;
str = GetMemory();//野指针问题,虽然放着数组首元素地址,但是不能使用这块空间
printf(str);
}
int main()
{
test();
return 0;
}
运行结果如下:
这段代码咱们预期结构是hello world,但是却打印出来乱码,这里存在着一个大问题——野指针问题。我们调用GetMemory函数的时候,给数组p分配的空间是栈区的空间,它的特点是出了GetMemory函数后,这块内存会自动地返还给操作系统,我们返回地只是这块空间地起始地址,暂存放在str指针中而我们接下里要对str进行访问(越界访问),这就是野指针问题(指针指向了一块不属于自己地空间),所以打印出来地东西是未知的,随机的,因为我们访问的不是我们能掌握的空间。
故而,返回栈空间地址的问题,一定会引起野指针的问题,但返回变量的值确实可以的(因为这个值会放到寄存器中临时存储,不会销毁)
#include<stdio.h>
int test()
{
int a=10;
return a;
}
int main()
{
int n=test();
printf("%d\n",n);
return 0;
}
在这里我们调用完test函数后,a是个局部变量,它地内存空间会自动地返回操作系统,但是它地值会被放在寄存器中存储起来,这时候,我们就可以将寄存器地值赋给n变量。
而我们返回a地址就会出问题;
#include<stdio.h>
int* test()
{
int a=10;
return &a;
}
int main()
{
int* n=test();
printf("%d\n",*n);
return 0;
}
这里虽然也是打印出10来,但是这是巧合,
如果 我们在打印之前再打印一个hehe,这样地话就会显现出问题来
#include<stdio.h>
int* test()
{
int a=10;
return &a;
}
int main()
{
int* n=test();
printf("hehe\n");
printf("%d\n",*n);
return 0;
}
这时候值就变成了5,因为变量a在出test函数后它所指向地空间就被释放掉了,n指针只保存了起始地址,但是它指向地空间是未知地,所以进行解引用后打印出来地东西也是未知的(典型地野指针问题)!!
- 练习3:分析以下代码有什么问题:
#include<stdio.h>
#include<stdlib.h>
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
int main()
{
test();
return 0;
}
这段代码分析起来与练习1类似。都是运用了二级指针来对str进行动态内存分配,但是此代码最大的问题就是忘记释放掉开辟出来地动态内存空间,存在内存泄漏地问题。修改如下:
#include<stdio.h>
#include<stdlib.h>
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
free(str);//避免内存泄漏
str=NULL;//避免野指针问题
}
int main()
{
test();
return 0;
}
- 练习4:分析以下代码有什么问题:
#include<stdio.h>
#include<stdlib.h>
void test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);//野指针问题,free完一定要置为空指针,避免野指针问题,防止出现野指针问题
if (str != NULL)
{
strcpy(str, "world");//形成非法访问,野指针问题
printf(str);
}
}
int main()
{
test();
return 0;
}
这段代码存在两个主要问题:
其一:造成野指针问题,进行了非法访问,我们将str指针释放掉后,没有将str指针置空,造成了对访问不属于自己空间地行为(野指针问题)。
其二:没有对malloc开辟出来的而空间进行判断是否为空:
修改后的代码如下:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void test(void)
{
char* str = (char*)malloc(100);
if(str==NULL)
{
perror("malloc");
return ;
}
strcpy(str, "hello");
free(str);//野指针问题,free完一定要置为空指针,避免野指针问题,防止出现野指针问题
str=NULL;
if (str != NULL)
{
strcpy(str, "world");//形成非法访问,野指针问题
printf(str);
}
}
int main()
{
test();
return 0;
}
五、柔性数组的介绍(前面没有见到过)
(一)、柔性数组的概念
- 在C99中,结构体的最后一个成员允许是未知大小的数组,这就叫做”柔性数组“成员
- 它具体有两种定义形式:
其一:
struct s1
{
int n;
char c;
double d;
int arr[];//在结构体中,最后一个成员,未知大小,称为柔性数组
};
其二:
struct s2
{
int n;
char c;
double d;
int arr[0];//这里也是未知大小的数组!
};
(二)、柔性数组的特点
- 结构中的柔性数组成员前面必须至少有一个其他成员;
- sizeof返回的这种结构大小不包括柔性数组的内存
#include<stdio.h>
typedef struct st_type
{
int i;
int a[0];//柔性数组
}type_a;
int main()
{
printf("%zd\n",sizeof(type_a));
}
运行结果如下:
很显然一个int类型就占据4个字节,在利用sizeof计算结构体内存大小的时候,没把柔性数组所占的内存大小算进去。
- 包含柔性数组的成员的结构用malloc()函数进行内存的动态分配,并且分配的内存大小应该大于结构的大小,以适应柔性数组的预期大小!
(三)、柔性数组的使用与优势
- 柔性数组的使用代码如下:
代码1:
#include <stdio.h>
#include <stdlib.h>
typedef struct st_type
{
int i;
int a[0];//柔性数组
}type_a;
int main()
{
int i = 0;
type_a* p=(type_a*)malloc(sizeof(type_a)+100*sizeof(int));//分配内存空间的时候,额外分配内存空间,以适应柔性数组的预期大小。
p->i = 100;
//对柔性数组进行赋值使用
for(i=0; i<100; i++)
{
p->a[i] = i;
}
free(p);
p=NULL;
return 0;
}
当然我们也可以利用指针型成员来代替柔性数组,达到同样的效果,不过要进行两次动态内存分配,释放的时候也应该两次释放。代码如下:
代码2:
#include <stdio.h>
#include <stdlib.h>
typedef struct st_type
{
int i;
int *p_a;//指针型成员
}type_a;
int main()
{
type_a *p = (type_a *)malloc(sizeof(type_a));//直接给结构体开辟空间,没有额外开辟空间。
//对结构体成员进行操作:
p->i = 100;
p->p_a = (int *)malloc(p->i*sizeof(int));//为指针成员进行空间的开辟,类同于对柔性数组进行内存空间的开辟
for(i=0; i<100; i++)
{
p->p_a[i] = i;
}
for(i=0; i<100; i++)
{
p->p_a[i] = i;
}
//对开辟的空间进行内存释放,注意释放顺序由里向外释放,否则会造成指针地址的丢失
free(p->p_a);
p->p_a=NULL;//避免野指针问题
free(p);
p=NULL;//避免野指针问题
return 0;
}
- 两段代码可以达到同样的效果,但是我们会优先选择代码1,即利用柔性数组来进行操作,这就涉及到了使用柔性数组的优势,它有两个好处:
其一:方便内存的释放。如果我们的代码是在⼀个给别⼈⽤的函数中,你在⾥⾯做了⼆次内存分配,并把整个结构体返回给⽤⼾。⽤⼾调⽤free可以释放结构体,但是⽤⼾并不知道这个结构体内的成员也需要free,所以你不能指望⽤⼾来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返回给⽤⼾⼀个结构体指针,⽤⼾做⼀次free就可以把所有的内存也给释放掉。
其二:有利于访问速度。连续的内存有益于提高访问速度,也有益于减少内存碎片。
六、C语言中内存区域的划分
由上面图可以知道,在C/C++程序中我们计算机中的内存空间被分成了六个区域,分别为内核空间、栈区、内存映射段、堆区、数据段(静态区)、代码段。我们结合上面代码分别介绍这六个区域分别存储什么内容。
- 内核空间:这块内存空间,是留给操作系统的,用户所写的代码不能在这段空间里面读写于存储。
- 栈区(stack):在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内存容量有限。栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。例如上面代码中的
所定义的局部变量都存放在栈区中。
-
内存映射段:主要涉及到文件映射、动态库、匿名映射相关内容的存储,这块后面写操作系统类文章时再详细书写,在这做一下了解。
-
堆区(heap):⼀般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配⽅式类似于链表。换言之我们本文所讲的动态内存申请都是从堆区里面申请的,它常用malloc、calloc、realloc函数进行申请,使用完后,我们要用free函数手动释放,以免造成内存泄漏问题。如果没有释放,那么再程序最终结束的时候会被OS回收。
上述代码中的用malloc、calloc、realloc,申请的在堆区中存放的
存在于堆区。 -
数据段(静态区):(static)存放全局变量、静态数据。程序结束后由系统释放,上述代码中的
全局变量与用static修饰的变量存放于数据段中。 -
代码段:存放函数体(类成员函数和全局函数)的⼆进制代码。
代码总的常量字符串,或者进入计算器中转换成的二进制代码都存放于代码段中。
总结
本文主要介绍C语言中的动态内存管理相关内容。在本文中首先会介绍为什么要使用动态内存,它有什么好处,然后介绍三种实现动态内存分配的库函数(malloc、calloc、realloc);而后介绍在动态内存分配中常常出现的几种问题;紧接着分析几道有关动态内存分配的习题,再者介绍一下柔性数组(属于新概念型),最后讲一下C语言中程序内存区域的划分。