1.前言:
对于很多参加竞赛或者平时在线OJ的同学来说,当大家使用C语言来书写代码的时候经常会遇到这样的情况:
题目要求数组的长度可变,并且要求输入一个整型当做数组的长度,我第一次遇到这个要求的时候是这样处理的:
int a=0;
scanf(“%d”,&a);
int arr1[a]={0};
正当我信心满满的提交答案时,发现编译器这样写时不通过的,但是从语法上来看没错啊?而且C99之后也支持C语言的变长数组的概念,但我们毕竟不是OJ的创始人,过不去的时候不能硬着头皮非要一条路走到黑,所以我们要另辟蹊径,这个时候,我们就需要动态内存管理来创建一个控制长度的数组了,所以,接下来我将说一说何为动态内存管理。
2.关于动态内存的概念理解:
首先我们先来理解一张图:
当我们写下一个程序的时候,其本质实际上是直接划分内存里面的空间,将空间分成有规律的几块,其具体要求如下:
- 栈区(stack):在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时
这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内
存容量有限。 栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。 - 堆区(heap):⼀般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配⽅式类似于链表。
- 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
- 代码段:存放函数体(类成员函数和全局函数)的⼆进制代码。
我们平时开辟的整型,浮点型,结构体型甚至是数组开辟好之后都是在栈区直接开辟的,但我们今天要说的动态内存是在堆区开辟的,堆区正如上面所说,是专门给程序员开辟和释放的灵活可变的空间。
3.动态内存使用的基本方式和函数介绍:
1.基本函数:malloc calloc realloc free
就像文件操作一样,动态内存也强调其使用的步骤,其最关键的在于使用了动态内存后一定要及时释放,即使用free函数。
1.free函数:
其基本格式如下:free函数是用来释放掉我们所开辟的内存的
void free (void ptr);*
注意!!:free只是释放内存,但它不会将ptr指针自动放置成空指针,在指针章节我们说过,不及时置空指针,变成野指针是很麻烦的,所以我们使用完free后一定要自己手动置空指针,常见的方式为:
free(str1);
str1=NULL;
2.malloc函数:(引用头文件#include<stdlib.h>)
malloc函数即最基础的动态开辟的函数,它的作用是在堆区申请一块空间并让程序员使用,基本格式如下:
void malloc (size_t size);*
它的参数是要申请的堆区空间的大小,返回值是申请的这块动态内存的指针,我们注意到为void*类型的,对于这种类型的接收,我们只需要按我们所需的强转其指针即可。如同文件操作一样,使用动态开辟的函数使用后同样也要检验,其基本格式如下为例:
int*arr1=(int*)malloc(10*sizeof(int)); if(arr1==NULL) { perror(:malloc failed"); exit(-1);/return 1; }
这便是我动态开辟的一个10个元素的数组,对于开辟失败的返回值,使用eixt(-1)和return 1都可以,其目的主要是跟return 0区分开,让程序员知道程序到底是因为什么结束的。后面的calloc realloc都遵循这个检验的步骤。
3.calloc函数:(#include<stdlib.h>)
calloc函数的用法与malloc大致相同,唯一不同的是它不仅开辟空间,还会将开辟的空间的每一个元素设置为0,基本格式如下:
void calloc (size_t num, size_t size);*
calloc的第一个参数为要开辟空间的元素个数,第二个为每一个元素的大小,所以calloc在开辟动态数组方面更加方便,但似乎不适合用来开辟结构体,它的返回情况和malloc一样。检验步骤同理。
4.realloc函数:(#include<stdlib.h>)
realloc的作用为调整由malloc和calloc开辟的空间的大小,使得其在调整动态内存的大小方面可以起到自动调整的作用,这对于使用顺序表以至于构建通讯录都是很方便的,格式如下:
void* realloc (void* ptr, size_t size);
它的第一个参数为用malloc realloc开辟的动态内存的地址,第二个参数为要调整的动态内存的大小**(注意这里不是累加的调整,是设定为多大就调整为多大)**,它的返回值为开辟的这块空间的地址,注意,我没有说返回值一定是原来开辟的地址,而是调整后的地址,这是由于堆区的开辟本身为碎片化的而不是有顺序的进行,realloc的调整倘若不影响其他动态空间,它返回的是原地址,但假如开辟的空间过大与其他冲突了,realloc就会重新找到一块地方开辟,同时将原来的那一块空间释放掉,这个时候它传过来的指针就是新地址的指针了。所以我在这里强调,一定不要把这个返回值单纯的看作一个源地址的指针,你就理解为这个空间的地址指针,你用之前的动态开辟的指针接收也就可以,用新的指针接收也可以。
realloc的特殊用法:当realloc对应的指针参数没有开辟动态空间的时候,此时使用realloc,它的用法跟malloc一样,直接为这个指针开辟一个对应的空间,并且返回这块的地址。
如下:
int*arr1=(int*)realloc(NULL,10*sizeof(int));//此时,realloc的作用与malloc作用相同 if(arr1==NULL) { perror("realloc failed"); exit(-1);/return 1; }
那么,我们动态内存开辟的基本格式是什么呢?
void*arr1=(void*)malloc()/ realloc()/ calloc();//开辟内存 检验过程 { } 使用过程 { } free(arr1);//内存释放过程 arr1=NULL;
使用动态内存开辟的时候,这几个步骤至关重要不能省略。
4.动态内存使用的常见错误总结:
1.对NULL指针的解引⽤操作
例如:
void test() { int *p = (int *)malloc(INT_MAX/4); *p = 20;//如果p的值是NULL,就会有问题 free(p); }
2.对动态开辟空间的越界访问
例如:
void test() { int i = 0; int *p = (int *)malloc(10*sizeof(int)); if(NULL == p) { exit(EXIT_FAILURE); } for(i=0; i<=10; i++) { *(p+i) = i;//当i是10的时候越界访问 } free(p); }
3.对⾮动态开辟内存使⽤free释放
例如:
void test() { int a = 10; int *p = &a; free(p);//ok? }
4.使⽤free释放⼀块动态开辟内存的⼀部分
例如:
void test() { int *p = (int *)malloc(100); p++; free(p);//p不再指向动态内存的起始位置 }
5.对同⼀块动态内存多次释放
例如:
void test() { int *p = (int *)malloc(100); free(p); free(p);//重复释放 }
6.动态开辟内存忘记释放(内存泄漏):忘记释放不再使⽤的动态开辟的空间会造成内存泄漏。
切记:动态开辟的空间⼀定要释放,并且正确释放。!!!!!!!且要手动置空
例如:
void test() { int *p = (int *)malloc(100); if(NULL != p) { *p = 20; } } int main() { test(); while(1); }
5.柔性数组:
回到前言中的问题,我们现在已经知道,利用动态开辟是可以操控数组长度的,但是C99中提到了这样一个概念,柔性数组,即在一个结构体内部包含多个成员变量,但最后一个成员是一个大小未知的数组,这就是柔性数组。
例如:
c typedef struct st_type { int i; int a[];//柔性数组成员 }type_a; 有时候在a的中括号里面写0也是可以的,但我本人建议按照定义来不写大小比较好理解。
1.柔性数组的特点:
• 结构中的柔性数组成员前⾯必须⾄少⼀个其他成员。
**• sizeof 返回的这种结构⼤⼩不包括柔性数组的内存。**你可以理解为它实际上是附加在结构体内部的一种特殊的结构,虽然在结构体里但开辟的空间是独立的
• 包含柔性数组成员的结构⽤malloc ()函数进⾏内存的动态分配,并且分配的内存应该⼤于结构的⼤
⼩,以适应柔性数组的预期⼤⼩。
2.柔性数组的使用:
柔性数组同样也是利用动态内存函数来开辟的,但特殊的是,它的大小是要独立于动态内存开辟的结构体而开辟的,具体如下:
type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
如上图,我们独立于结构体之外单独为柔性数组开辟100个元素的空间,而不是算在一起开辟,这样开辟有利于我们理清结构体和柔性数组的大小。
倘若不用柔性数组,正常开辟也可以,如下:
type_a *p = (type_a *)malloc(sizeof(type_a)); p->i = 100; p->p_a = (int *)malloc(p->i*sizeof(int));
所以我们说说柔性数组的好处:
1.第⼀个好处是:⽅便内存释放
如果我们的代码是在⼀个给别⼈⽤的函数中,你在⾥⾯做了⼆次内存分配,并把整个结构体返回给⽤
⼾。⽤⼾调⽤free可以释放结构体,但是⽤⼾并不知道这个结构体内的成员也需要free,所以你不能
指望⽤⼾来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返
回给⽤⼾⼀个结构体指针,⽤⼾做⼀次free就可以把所有的内存也给释放掉。通俗来讲就是,简单清晰,干净利落。
2.第⼆个好处是:这样有利于访问速度.
连续的内存有益于提⾼访问速度,也有益于减少内存碎⽚。(其实,我个⼈觉得也没多⾼了,反正你
跑不了要⽤做偏移量的加法来寻址)。注意,堆区大量使用动态开辟并不是一件好事,这样会导致堆区的空间碎片化,大量空间无法使用。
6.总结:
以上就是动态内存开辟的全部内容的,C语言之所以不适合竞赛,就在于它不能像C++那样使用vector调整长度,只能动态开辟数组来使用,但是动态开辟依旧是一个十分好用的操作,对于数据结构的链表和顺序表的节点创建和大小调整很重要,所以还是建议大家能熟练掌握动态内存开辟的内容。