#1024程序员节|用代码,改变世界#

本篇博客主要介绍了C语言程序内部的内存开辟.动态内存分布 动态内存函数malloc calloc realloc free的使用 常见的动态内存错误.以及柔性数组的概念与使用
学会动态内存管理将不再局限于使用静态的空间,对内存空间的理解和使用将更进一层楼~

C语言动态内存管理

  • 一.认识C语言程序的内存开辟
  • 二.什么是动态内存
  • 三.为什么会存在动态内存分布
  • 四.动态内存函数的介绍
    • 1.malloc动态内存开辟函数
    • 2.calloc动态内存开辟函数
    • 3.realloc动态内存分配函数
    • 4.free释放动态内存空间函数
  • 五.常见的动态内存错误
    • 1.对free后指针的解引用操作
    • 2.对动态开辟空间的越界访问
    • 3.对非动态开辟内存使用free释放
    • 4.使用free释放一块动态开辟内存的一部分
    • 5.对同一块动态内存多次释放
    • 6.动态开辟内存忘记释放(内存泄漏)!!!
    • 7.在死循环里动态开辟内存空间
  • 六.柔性数组
    • 1.柔性数组的特点
    • 2.柔性数组的开辟方式
    • 3.柔性数组的使用
    • 4.柔性数组的优势
  • 七. 动态内存管理总结

一.认识C语言程序的内存开辟

C/C++程序内存分配的几个区域:

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结 束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是 分配的内存容量有限。
    栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
  2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分 配方式类似于链表。
  3. 数据段(静态区static存放全局变量、静态数据。程序结束后由系统释放。
  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码

实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序
结束才销毁
所以生命周期变长。

二.什么是动态内存

动态内存是相对静态内存而言的。所谓动态和静态就是指内存的分配方式。动态内存是指在堆上分配的内存,而静态内存是指在栈上分配的内存。

前面所写的程序大多数都是在栈上分配的,比如局部变量、形参、函数调用等。栈上分配的内存是由系统分配和释放的,空间有限,在复合语句或函数运行结束后就会被系统自动释放。而堆上分配的内存是由程序员通过编程自己手动分配和释放的,空间很大,存储自由。

三.为什么会存在动态内存分布

int a=10;  //在栈空间申请4字节空间int arr[10]={0}; //在栈空间申请40个字节空间

我们现在已经掌握了在栈区申请开辟空间,
而在栈区申请空间有以下特点:
1.栈区开辟的空间即定义的变量是局部变量,出了所在的局部范围后生命周期会结束,在局部范围里时变量会一直存在…
2.栈区开辟的空间大小是固定的,一旦开辟后大小无法发生变化…
3.在栈区开辟数组空间,数组长度必须是确定的,在编译时为其分配固定长度的内存空间

但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。这时候就只能试试动态存开辟了

四.动态内存函数的介绍

动态内存即可以使内存动态变化,当我们不确定数组长度具体是多少时 (数组长度小时,存在数据溢出不够存放的问题,而数组长度大时,存在占用可用空间太多的问题),
实际运行中很少能够准确使用到合适的数组长度,而用动态内存可以根据实际编译情况为数组动态开辟合适的空间
而开辟动态内存需要用到c语言提供的动态内存函数…
而使用这些库函数都需要包含头文件 #include

1.malloc动态内存开辟函数

malloc动态内存开辟函数右边是官方文档->

malloc 返回类型是void *
形参类型是size_t
作用为,调用malloc函数时实参为一个具体整数n表示在堆区开辟n个字节的空间,开辟成功返回开辟的内存空间的起始地址,开辟失败返回一个NULL指针,(开辟失败可能存在堆区空间占满的情况)
返回的指针是空类型的,因为开辟空间时编译器不确定开辟后的空间是什么类型的,最后返回空类型指针
通过强制类型转换为对应类型的指针,让编译器知道你对开辟的空间以什么类型进行访问,可将空间按你强转的类型进行访问操作
而malloc开辟的空间开辟后空间里的值是不确定的,需要自己设置值

#include#includeint main(){int *p=(int*)malloc(40);if (p == NULL){printf("开辟空间失败\n");return 1;}int arr[10];int i = 0;for (i = 0; i < 10; i++){*(p + i) = i;arr[i] = i;printf("%d %d\n", p[i],arr[i]);}return 0;}}

上面代码通过使用malloc在堆区开辟了40个字节空间(经过判断返回的指针是否为NULL如果是则表示开辟失败则结束程序),每4个内存单元为一个整形空间, 在栈区申请开辟了一个有10个元素的整形数组
二者的区别是一个在堆区开辟空间,一个在栈区开辟空间,本质上都是一段用于存放10个整形的地址连续的内存空间
最后通过循环输入值,输入值得出的结果都相同…

2.calloc动态内存开辟函数

右边是calloc库函数官方文档->calloc
calloc函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
与函数 malloc 的区别在于参数表示num个size字节空间而malloc参数为多大的字节空间
calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。

calloc和malloc对比:
malloc 只有一个参数假如为40表示开辟40个字节空间
colloc有两个参数(10,4)表示开辟10个4字节的空间即10*4为40个字节空间,二者最后开辟的空间字节个数是一样的
malloc开辟的空间最后不会初始化 是随机值
calloc开辟的空间每个内存单元都会初始化为0…

#include#includeint main(){//返回的是void* 可以强转类型后赋给对应类型变量也可以直接赋值最后都会根据变量的类型自身转换为该类型赋值int* p = calloc(10, sizeof(int));int* l = malloc(40);int i = 0;for (i = 0; i < 10; i++){printf("%d ", *(p + i));printf("%d \n", *(l + i));}return 0;}

可以看到calloc 和malloc都开辟了40个字节空间,最后都以整形输出10个整形元素 calloc里为10个0 而malloc是随机值

3.realloc动态内存分配函数

右边为realloc动态内存分配函数官方文档->

realloc库函数返回类型是void*
第一个形式参数ptr为void*类型,接受的是要重新开辟的空间的起始地址
第二个参数是size_t 表示无符号整数 表示要重新开辟的空间大小
如果第一个参数接受的是NULL则表示在堆区开辟第二参数表示的字节个数的空间等同于malloc

realloc主要作用就是是原先开辟的空间动态减少或者动态增长,达到动态变化的效果
使原来已有的空间减少时传递的字节个数比原来空间小,此时会减少原来空间,减少的空间数据被丢失…最后返回当前被减少后的内存空间的指针…

realloc是原来空间增加传递的字节个数比原来空间大,此时有两种情况
当给当前空间增长时,后面能容纳增长的区域,直接在当前空间后面加要增长的字节空间个数,返回当前空间起始地址,即原空间起始地址
当给当前空间增长时.后面有其他空间存在或者后面已经到了堆区范围,此时会重新在堆区找一块能容纳增长后所有字节大小的区域开辟一块空间,将原来空间的数据拷贝到当前空间并释放掉原来空间,返回当前被开辟的空间起始地址

#include#includeint main(){int* p = (int*)realloc(NULL, 20);if (p == NULL){printf("开辟失败\n");return 1;}int i = 0;for (i = 0; i < 5; i++){p[i] = i;printf("%d ", p[i]);}printf("\n");p = (int*)realloc(p, 12);   //重新开辟12个字节空间 表示在原来空间上减少后面8字节空间for (i = 0; i < 3; i++){p[i] = i;printf("%d ", p[i]);}printf("\n");int* tmp = (int*)realloc(p, 40);  //重新开辟40个字节空间,在原来空间基础上动态增长20个字节空间可能开辟失败会返回NULL ,先用一个临时变量接受if (tmp == NULL){printf("增容失败\n");return 1;}p = tmp;for (i = 0; i < 10; i++){p[i] = i;printf("%d ", p[i]);}return 0;}

上面为测试realloc一个参数为NULL指针时,相当于malloc函数开辟后面20个字节空间
将返回的起始地址转换为整形指针,即将20个字节空间看成5个整形空间,分布赋值后输出
用realloc(p,12)表示重新开辟12个字节,等价于将p指针指向的动态内存空间动态减少后面8字节空间,得到12个字节空间,后面8字节空间数据丢失,前面的仍保留,实现了动态减少的效果

realloc(p,40)表示重新开辟40个字节空间,等价于都p指针指向的动态内存空间往后增长到40个字节空间,可能出现增长时容量已满增容失败返回NULL
此时先用一个临时指针变量接受返回的指针,经过判断不是NULL指针后将其赋值给p
此时p指针得到的是指向40个字节空间的指针,增长前空间的数据仍然会被保留,实现了动态增长的效果

4.free释放动态内存空间函数

右边数free库函数的官方文档->free

free函数用来释放动态开辟的内存。
如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的.
如果参数 ptr 是NULL指针,则函数什么事都不做。

free 形参类型为void* 表示接受的是一个指向需要释放的动态空间的指针,
返回类型是void表示无返回值

free是一个很重要的函数, 我们知道在栈区申请的局部空间,在出其局部范围时空间会被释放,
而堆区申请的空间,只有在整个程序结束时才会被释放,而往往有时候我们只需要临时开辟空间,用完就不再使用了,而不使用这个空间但是仍然在程序里就会浪费不必要的空间
需要做到在程序运行时释放空间,就要用到free函数

#include#includevoid print(){int* p = (int*)malloc(40);int i = 0;for (i = 0; i < 10; i++){p[i] = i;printf("%d ", p[i]);}}int main(){print();return 0;}

当我们在一个自定义函数里使用动态内存函数在堆区申请空间进行某些操作时,如上面代码使用malloc在堆区申请40个字节空间然后当做十个整形元素赋值并遍历打印
执行完这个函数后,显然这些空间我们已经不需要再使用了,而它并不会像数组一样出了这个函数空间被释放

堆区申请的空间要在整个程序结束后才会被自动释放还给操作系统,在上面代码运行完函数后最后结束程序会释放空间
但实际以后编写的程序都是在服务器之类上运行,而服务器是二十四小时不间断运行的,不会结束程序,而随着程序运行实现某些功能不停在堆区申请空间而释放不了,最后会造成空间浪费很多会导致服务器崩溃

要使程序在运行过程中释放已经申请开辟了的且不需要使用的堆区空间就需要用到free函数

#include#includevoid print(){int* p = (int*)malloc(40);if (p == NULL){perror("malloc");   //perror打印错误信息函数 return 1;}int i = 0;for (i = 0; i < 10; i++){p[i] = i;printf("%d ", p[i]);}free(p);    //p为指向堆区开辟的空间的起始地址 传参调用free函数释放这个开辟的空间p = NULL;   //释放后p里的指针指向的未知空间是野指针 为了避免对野指针解引用将p置为NULL}int main(){print();return 0;}

上面代码加了free函数,参数是指向堆区申请开辟的空间的起始地址,作用是将传递的指针指向堆区的空间释放还给操作系统,此时就实现了在程序运行中释放掉堆区申请开辟的空间
注意:p指向的已开辟堆区空间被释放后,p指针此时指向的一块未知空间是一个野指针,为了避免再对p野指针解引用操作将p置为NULL

五.常见的动态内存错误

在堆区申请开辟动态内存比栈区的静态内存更为灵活,但是使用它需要更谨慎,使用不当会出现很多内存错误,要合理使用并管理动态内存,得认识常见的动态内存错误

1.对free后指针的解引用操作

void test(){int *p = (int *)malloc(INT_MAX/4);free(p);  //free后p指向的空间被释放*p = 20;//此时p指针属于野指针,解引用赋值就会有问题

申请一块动态内存得到指向该内存的指针 后,通过指针free释放掉这个内存空间后,这个指针变为了野指针,此时不小心再对野指针解引用操作是错误的…
free后不能再对free后的空间的指针进行解引用操作

2.对动态开辟空间的越界访问

void test(){int i = 0;int *p = (int *)malloc(10*sizeof(int));if(NULL == p){   perror("test");   return 1;}for(i=0; i<=10; i++){*(p+i) = i;//当i是10的时候越界访问}free(p);}

上面代码开辟了40个字节的空间,分为10个4字节的整形空间访问,但通过循环访问时i为10访问到了第11个整形空间,属于越界访问,是错误的…
访问开辟的空间要注意不要发生越界访问

3.对非动态开辟内存使用free释放

void test(){int a = 10;int *p = &a;free(p);//ok" />

上面是没有运行时的截图,先打开任务管理器ALT+Ctrl+Delete查看内存情况是正常的
下面冒着死机风险运行了程序

可以看到才运行了一会儿,电脑内存就已经使用率到了94%!!!,可以看到,不free空间,又频繁动态开辟空间的后果,最后可能会导致电脑死机!!

六.柔性数组

也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。

1.柔性数组的特点

结构体中的柔性数组成员的前面必须至少要有一个其他成员。
sizeof 返回的这种结构大小不包括柔性数组的内存。
包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

2.柔性数组的开辟方式

typedef struct st_type{int i;int a[];//柔性数组成员}type_a;

有些编译器会报错则可以改成:

typedef struct st_type{int i;int a[0];//柔性数组成员}type_a;

根据柔性数组特点可以看出这种开辟方法一开始数组是没有元素个数即的,使用sizeof 求出来的也是柔性数组以外的成员大小

#includestruct S{int i;int arr[];};int main(){printf("%d", sizeof(struct S));//输出结果是什么return 0;}

根据特点,结构体最后一个成员变量为int arr[]未指定数组元素个数是一个柔性数组,此时求struct S的大小是除柔性数组以外的其他成员变量(再考虑内存对齐),最后结构大小为4字节

3.柔性数组的使用

柔性数组是没有元素个数的,而它的特性就是可以通过动态内存函数使其类似于获得自身的元素个数即内存空间,也可以通过重置动态内存函数改变其后面访问元素个数,实现数组空间动态增长!!!

#include#includestruct S{int i;int arr[];};int main(){struct S *p=(struct S*)malloc(sizeof(struct S) + sizeof(int) * 10);if (p == NULL){perror("malloc");return 1;}printf("%d ", sizeof(*p));//输出结果是什么p->i = 10;int n = 0;for (n = 0; n < 10; n++){p->arr[n] = n;printf("%d ", p->arr[n]);}free(p);p = NULL;return 0;}

上面代码使用了柔性数组,最后输出结果为4 0 1 2 3 4 5 6 7 8 9
先使用malloc开辟了(4+40)个空间,开辟成功后返回起始地址,将地址转换完struct S * 结构体类型指针,而这个结构体里有int 类型和柔性数组成员变量,因为柔性数组开始大小为0,所以开始的struct S* 指针是指向动态内存空间起始4个字节的整形空间的指针,将其赋值给结构体指针变量p
对其解引用访问了还是4个字节整形空间,sizeof求类型大小结果为4

前面四个字节的整形空间是struct s的也就是第一个成员变量int a的,后面开辟40个字节空间并不会浪费掉,这些空间是属于能被柔性数组int arr[]访问的空间,虽然柔性数组不算类型字节但是可以看做是一个指针访问后面的空间 即40个字节为10个整形空间类似于分配给柔性数组的空间即给了柔性数组10个元素,通过arr对应下标可以访问这十个整形空间

柔性数组没有指定元素个数但是在后面有可用的空间时可以根据数组类型将后面开辟的空间当成内部元素访问后面的空间

4.柔性数组的优势

上面代码中的整形柔性数组就类似于一个整形指针,同样也可以写成下面这种形式

#include#includestruct S{int a;int* arr;  //整形指针变量代替柔性数组};int main(){struct S* p = (struct S*)malloc(sizeof(struct S));p->a = 10;p->arr = (int*)malloc(40);if (p->arr == NULL){perror("malloc");return 1;}int i = 0;for (i = 0; i < 10; i++){p->arr[i] = i;printf("%d ", p->arr[i]);}   //释放空间free(p->arr);p->arr=NULL;free(p);p = NULL;return 0;}

可以看到最后代码输出结果是一样的,虽然这种写法和柔性数组实现效果一样,但是柔性数组的实现有两个好处

好处一:

可以看到代码一malloc开辟了两次内存空间,最后释放了两次空间
而柔性数组只需要开辟一次使用完后也只释放一次

如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。
用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
因此使用柔性数组动态内存开辟和使用后的释放更方便

好处二:

连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得也没多高了,反正你少不了要用做偏移量的加法来寻址)
使用柔性数组有益于提高访问速度

七. 动态内存管理总结

本文主要介绍了动态内存函数malloc calloc realloc free 以及使用动态内存函数需要了解和避免出现的常见错误和柔性数组的概念与使用,
先以熟悉为主 学会使用动态内存,对内存空间的掌握将更进一步,不再局限于静态内存的使用,
具体的代码实操练习在下一篇博客:用动态内存将静态学生信息管理改变为动态内存增长版…