大家好!我是保护小周,本期为大家带来超详细的C语言——动态内存管理,论动态内存在C++/C中的重要性,学好动态内存约等于看穿了C语言~确定不来看看嘛~
快来跟我一起学习吧!
一、为什么存在动态内存分配
当前我们知道的内存的使用方式:
(1)创建一个变量
创建一个变量就一定会开辟空间,例如:int sum=0; 如果sum变量在函数内部创建的,这个时候它是局部变量,又或者你定义的的sum是全局变量,无论是全局变量还是局部变量,作为整型的sum变量都会向内存申请4个字节的空间,无非就是局部变量是在内存的栈区上申请的,全局变量是在内存的静态区上申请的。
(2)创建一个数组
创建一个局部范围的数组,是在栈区上申请的,同样如果创建的是一个全局范围的数组,也是静态区上申请的。
我们来画个图换一种思维理解:
编辑
两种方式:一种是一次申请一个变量的空间,另一种是一次申请一片连续的空间。
上述两种开辟空间的方式有两个特点:
1.空间开辟的大小是固定的。
2.数组在申明的时候,必须指定数组的长度,所需内存在编译时分配。
那么是不是有了这两种常见的使用内存的方式就够了?
举个例子:我们采用一个结构体描述学生信息,学号、姓名、性别、年龄等信息;然后我们需要定义一个结构体类型变量,或者结构体类型数组来存储信息,如果我们采用一个数组存储一个班级的学生信息,我们当然可以根据一个班级里的人数来定义数组的大小,这时候如果班级有的同学转走了,我们把他的信息处理掉之后,表面上是删除了,实际上属于他原来的空间一直存在,是不是造成了一定程度上空间的浪费,又或者是增加了一些新的同学,数组的大小是固定的,满了之后我们又该如何处理,增加这些新同学的信息呢?有的人这样想,我们把数组的大小定义的大一些,一点点的空间浪费可以接受的,那么如果我采用这个结构体类型的数组存放全校同学的信息,这个误差就非常大了。
#include<stdio.h> //学生学习成绩类型定义 struct grade { int Chinese;//语文 int math; //数学 int English;//英语 }; //学生信息类型定义 struct student //定义一个student的结构体类型 { char number[12];//学号 char name[12]; //姓名 char sex[5]; //性别 int age; //年龄 char tele[12]; //电话 char addr[20]; //地址 struct grade report;//成绩单 }; int main() { //定义一个student类型的数组data; struct student data[50];//data可以存放50个同学的信息 //如果实际录入信息为30,就造成空间上的浪费 //如果实际需录入信息60,那我们data的空间不够 return 0; }
如果大家在结构体方面有什么不明白的可以观看博主的另一篇博客:结构体(初阶)
使用数组的方式有他的局限性,应该怎么办?动态内存分配,等空间满了之后自动的在堆区上开辟我们理想大小的空间,用来存储。如果我们删除某个同学的信息之后,可以执行释放掉用于存储被删除同学信息的空间,这样是不是非常方便?
二、动态内存函数介绍
动态内存函数的在头文件<stdlib.h>
2.1 malloc()和free()
C语言提供了一个动态内存开辟的函数:
void* malloc (size_t size);
这个函数可以向内存申请一块连续可用的空间,并返回指向这块从堆区上开辟的空间的指针。
(1)如果空间开辟成功,则返回一个指向开辟好空间起始位置的指针
(2)如果空间开辟失败,则返回一个值为NULL的指针,我们可以利用这一点来判断malloc()是否成功开辟空间
(3)返回值的类型是void *,所以malloc()函数不知道开辟空间的类型,类型需要我们自己来决定
这个时候我们就需要根据自己的需要进行强类型转换。
C语言提供了一个free()函数,专门是用来做动态内存的释放和回收的。
free()函数调用形式如下:
void free(void*ptr);
free()函数用来释放动态开辟的空间。
(1)如果参数 ptr 指向的空间不是动态开辟的,free()函数的行为是未定义的
(2)如果参数 ptr 是NULL指针,则函数什么事也不做
我们举一个简单的例子来看看怎么使用malloc()函数和free()函数;
#include<stdio.h> #include<stdlib.h> int main() { //定义一个指针 int* p=NULL; //指针p指向malloc()函数返回的空间的起始位置 p=(int*)malloc(10*sizeof(int));//向内存的栈区申请10个整型的空间 if (p==NULL)//判断申请空间是否成功 { //打印错误提示 perror("malloc"); return;//中止程序 } else//正常使用空间 { int i = 0; for (i=0;i<10;i++)//给申请的空间赋值 { *(p + i) = i; } //打印数据 for (i=0;i<10;i++) { printf("%d ",*(p+i)); } } //当动态申请的空间不再使用 // 就应该还给操作系统 //释放空间 free(p); //如果我们不释放不用的空间的话,如果程序一直不结束,就会造成空间的浪费 p = NULL;//断开p与动态开辟空间的联系 return 0;//程序在结束时也可以释放掉空间 }
程序功能展示:
编辑
使用malloc()函数动态开辟结构体student类型的空间,然后我们输入信息和打印信息:
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<string.h> #define max 2//开辟多大的空间 //学生学习成绩类型定义 struct grade { int Chinese;//语文 int math; //数学 int English;//英语 }; //学生信息类型定义 struct student //定义一个student的结构体类型 { char number[12];//学号 char name[12]; //姓名 char sex[5]; //性别 int age; //年龄 char tele[12]; //电话 char addr[20]; //地址 struct grade report;//成绩单 }; //添加学生信息 void add(struct student* ps)//ps指向newnode动态开辟的空间 { int i; for (i=0;i<max;i++)//输入次数 { printf("请输入学号:>"); scanf("%s",(ps+i)->number); printf("请输入姓名:>"); scanf("%s", (ps + i)->name); printf("请输入性别:>"); scanf("%s", (ps + i)->sex); printf("请输入年龄:>"); scanf("%d", &((ps + i)->age));//变量加& printf("请输入联系电话:>"); scanf("%s", (ps + i)->tele); printf("请输入地址:>"); scanf("%s", (ps + i)->addr); printf("请输入语文成绩:>"); scanf("%d", &((ps + i)->report.Chinese));//变量加& printf("请输入数学成绩:>"); scanf("%d", &((ps + i)->report.math));//变量加& printf("请输入英语成绩:>"); scanf("%d", &((ps + i)->report.English));//变量加& printf("\n添加成功!\n"); } if (i == max) { printf("空间已满!\n"); } } //打印学生信息 void print(struct student* ps) { //打印标题 printf("%-5s\t%-5s\t%-5s\t%-5s\t%-12s\t%-20s\t%-5s\t%-5s\t%-5s\n", "学号","姓名", "年龄", "性别", "联系电话", "地址","语文","数学","英语"); int i; for (i = 0; i < max; i++) { //打印信息 printf("%-5s\t%-5s\t%-5s\t%-5d\t%-12s\t%-20s\t%-5d\t%-5d\t%-5d\n", (ps + i)->number, (ps + i)->name, (ps + i)->sex, (ps + i)->age, (ps + i)->tele, (ps + i)->addr, (ps + i)->report.Chinese, (ps + i)->report.math, (ps + i)->report.English); } } int main() { //定义一个student类型的指针变量newnode; struct student *newnode=(struct student*)malloc(max*sizeof(struct student));//max=3; //在内存堆区上开辟2个student类型的空间 if (newnode==NULL)//判断内存有没有开辟成功 { //提示动态内存开辟失败 perror("malloc"); exit(-1);//中止程序 } else//正常使用动态开辟的空间 { //添加学生信息 add(newnode); //打印学生信息 print(newnode); //释放空间 free(newnode); newnode = NULL; } return 0; }
程序功能展示:
2.2 calloc()
C语言还提供了一个函数叫calloc(),calloc()函数也可以用来动态内存分配。
calloc()函数调用形式如下:
void* calloc(size_t num,size_t size);
(1)函数的功能是为num个大小的size的元素开辟一块空间,并且把空间的每个字节初始化为0。
(2)与函数malloc的区别在于 calloc()函数会在返回地址前把申请的空间每个字节初始化为0。
如果我们对动态开辟的空间的内容要求初始化,那么可以优先使用calloc()函数,非常方便!
我们上代码看看calloc()函数在实际中怎么运用,以及与malloc()函数的对比:
#include<stdio.h> #include<stdlib.h> #include<errno.h> int main() { //malloc() int* q = (int*)malloc(10*sizeof(int)); //calloc() int *p=(int*)calloc(10,sizeof(int));//两个函数的参数对比 if (p == NULL&&q==NULL)//判断有没有开辟成功 { //如果开辟失败提示错误原因 printf("%s\n",strerror(errno)); } else//申请成功 { int i = 0; for (i = 0; i < 10; i++) { printf("%d ",*(p + i));//打印calloc()函数开辟的空间 } printf("\n"); for (i = 0; i < 10; i++) { printf("%d ", *(q + i));//打印malloc()函数开辟的空间 } } //释放空间 //free函数是用来释放动态开辟的空间的 free(p); p = NULL; return 0; }
calloc()函数会把开辟的空间初始化为0然后返回,而malloc()函数不会初始化,直接返回,所以大家在抉择这两个函数的时候,根据自己的需求来即可,各有千秋。
2.3 realloc()
realloc()函数让动态内存开辟更加灵活。
有时候我们会发现以往申请的空间太小,有时候我们觉得申请的空间太大,为了合理的使用内存,我们可以对动态开辟的内存的大小进行调整,使用的就是realloc()函数。
realloc()函数调用形式如下:
void *realloc(void*ptr,size_t size);
(1)ptr 里是要调整的动态开辟内存的地址
(2)size 调整之后的大小
(3)返回值为调整之后的内存起始位置
#include<stdio.h> #include<stdlib.h> #include<errno.h> int main() { //开辟10个整型空间 int* p = (int*)malloc(sizeof(int) * 10); if (p==NULL)//判断是否开辟成功 { printf("%s\n", strerror(errno)); } else//开辟成功 { int i =0; for (i=0;i<10;i++) { *(p + i) = i; printf("%d ",*(p+i)); } } printf("\n"); // 我们使用malloc()函数开辟了40个字节的空间 //假设这40个字节不能再满足我们的使用了 //我们需要60个字节的空间,存储0~14的数据 //这里就可以使用realloc()函数调整动态开辟空间的大小 int i=0; //增容 int *p2=(int*)realloc(p,60); if (p2 == NULL) { //报错 perror("realloc"); } else { //这里我们可以p=p2;让p重新指向新开辟的空间,然后利用p进行操作,最后再free(p);即可 //给增容后的空间赋值 for (i=10;i<15;i++) { *(p2 + i) = i; } for (i = 0; i < 15; i++) { //打印增容后的所有数据 printf("%d ", *(p2 + i)); if ((i + 1) % 5 == 0)//5个数据一换行 { printf("\n"); } } } //释放空间 free(p); free(p2); p2=NULL; p=NULL; }
使用realloc()函数的注意事项
编辑
(1)如果p指向的空间之后有足够的空间可以追加,则直接追加,然后返回p
(2)如果p指向的空间之后没有足够的内存空间可以追加,则realloc()函数会重新找一个新的内存区域(堆区)开辟一块满足需求的空间,并且把原来的数据拷贝过来,释放原来的内存空间。
(3)最后再返回新开辟的内存空间地址,这个时候我们需要一个新的变量来接收realloc()函数的返回值
给大家留一点作业好吧!
使用realloc()函数给动态开辟的结构体student类型的空间newnode增容
嘿嘿!加油哦!
在此我们动态内存函数已经介绍完毕:malloc,calloc,mealloc,free,一定要多练,多敲代码,才能更好的理解, 如果有什么不懂得地方可以下方评论,我看到后一定会第一时间回复。
三、常见的动态内存错误
3.1 对NULL指针的解引用操作
3.2 对动态内存的越界访问
3.3 对非动态内存的free()释放
3.4 使用free()释放一块动态开辟内存的一部分
3.5 对同一块动态开辟的空间多次释放
那么我们怎么去避免这种多次释放动态开辟的空间这种现象:
首先我们要做到谁(程序员)申请的空间,就由谁来负责回收,如何防止我们自己多次释放呢?
那我们可以在释放后,将指针置为NULL,要随手养成良好的代码习惯。
//释放 free(p); p=NULL;//置为空 free(p);//这样多次释放就没有问题了
3.6 对动态开辟的空间忘记释放
while(1) { malloc(sizeof(int)); }
以此代码为例:我们一直循环的向内存申请空间,一次申请4个字节,虽然4个字节不多,但是内存是有限的,我们一直不回收的话,申请的内存就会一直被占用,等内存被消耗待尽,服务就会断掉,很多的服务器不是轻易关闭。只有程序结束时,或者服务器重启时申请的空间才能自动释放掉,就像是你去图书馆一天借一本书,你看完了以后,你也不还回去,那么别人是不是也无法阅读,是不是资源的一种浪费,就非得是执法人员的去抄你的家,才能把拿走的书还回去。那我们何不自己还回去呢?
忘记释放不再使用的动态开辟的空间会造成内存的泄露。
切记:动态开辟的空间一定要释放,并且要正确的释放。
以上就是6条常见的动态内存开辟的错误
四、经典笔试题
4.1 请问以下程序有那些错误
//4.1 void GetMemory(char* p) { p= (char*)malloc(100); } void Test(void) { char* str = NULL; GetMemory(str); strcpy(str,"helloc world"); printf(str); } int main() { Test(); return 0; }
1.运行代码程序会出现崩溃的现象
2.程序存在内存泄漏的问题
- str以值传递的形势给p
- p是GetMemory函数的形参,只能函数内部有效
- 等GetMemory函数返回之后,动态开辟的内存尚未释放,并且无法找到,所以会造成内存泄漏
改正方法1:
改正方法2:
4.2 请问运行Test()函数会有什么结果
//4.2 char* GetMemory(void) { char p[] = "hello world"; return p; } void Test(void) { char* str = NULL; str = GetMemory(); printf(str); } int main() { Test(); return 0; }
1.p[];作为GetMemory()函数的局部变量,变量和数组是在内存的栈区上申请的,当函数结束时,数组p会被销毁还给操作系统,所以指针str无法接收数组p的地址,如果是动态开辟的是在内存的堆区上,需要我们free()释放,或者是程序结束运行才会被释放。
该程序存在非法访问内存的错误,因为数组p在GetMemory()函数结束时就已经被销毁了,输出随机值
改正方式:定义一个全局变量 在内存的静态区
char* GetMemory(void) { static char p[] = "hello world"; return p; }
4.3 请问运行Test()函数会有什么结果
void GetMemory(char** p, int num) { *p = (char*)malloc(num); } void Test(voud) { char* str = NULL; GetMemory(&str,100); strcpy(str,"hello"); printf(str); } int main() { Test(); return 0; }
忘记释放动态开辟的空间,导致内存泄漏了
free(str); str=NULL;
4.4 请问运行Test()函数会有什么结果
void Test(void) { char* str = (char*)malloc(100); strcpy(str,"hello"); free(str); if (str != NULL) { strcpy(str, "world"); printf(str); } } int main() { Test(); return 0; }
1.打印world
那么程序有什么错误呢?
举个例子:我们去酒店,开了100平的一个房间,再把行李"hello"放到房间里,第二天早上起来,把房退掉了(free),退掉之后你又强行把行李"world"放进房间,str已经成为了“野指针”,这样就造成了非法访问内存!
怎么改?
void Test(void) { char* str = (char*)malloc(100); strcpy(str,"hello"); free(str);//free()释放str指向的空间后,并不会把str置为NULL str=NULL;//需要我们手动将它置为NULL //这样就避免了非法访问内存 if (str != NULL) { strcpy(str, "world"); printf(str); } } int main() { Test(); return 0; }
C/C++ 程序内存分配的几个区域:
1.栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限,栈区主要存放运行函数而分配的局部变量,函数参数,返回数据,返回地址等。
2.堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时由OS回收。
3.数据段(静态区)(static)存放全局变量,静态变量。程序结束后由系统释放。
4.代码段:存放函数体(类成员函数和全局函数)的二进制代码。
五、柔性数组
也许你从来没有听说过柔性数组(flexible array)这个概念,但是它时真实存在的,C99中,结构体中的最后一个元素允许是未知大小的数组,这就叫做【柔性数组】成员。
我们举个例子:
//常规定义 /*struct S { int i; int arr[10]; };*/ //柔性数组 struct S { int i; int arr[];//未知大小的-柔性数组成员-这里数组的大小是可以调整的 //正常情况下数组在申明的时候,必须指定数组的长度 }; int main() { struct S s; printf("%d\n",sizeof(s));//观察结构体s的大小 return 0; }
我们发现S类型的结构体只有4个字节的大小,整型变量 i 就占了4个字节,所以我们定义的arr[ ],不占空间大小,所以柔性数组成员定义的时候不占含有柔性数组成员的结构体的空间,是未知大小的。
我们如何使用柔性数组呢?
以上就是在向申请空间时定义柔性数组成员的大小。
我们看看给arr[ ]数组开辟的空间能否正常使用:
现在我们提一个要求就是,把arr[ ]空间变成40个字节试一试:
//柔性数组 struct S { int i; int arr[];//未知大小的-柔性数组成员-这里数组的大小是可以调整的 //正常情况下数组在申明的时候,必须指定数组的长度 }; int main() { struct S* ps = (struct S*)malloc(sizeof(struct S)+5*sizeof(int)); ps->i = 10; int j = 0; for (j=0;j<5;j++) { ps->arr[j] = j;//0 1 2 3 4 printf("%d ",ps->arr[j]); } struct S*pps=(struct S*)realloc(ps,10*sizeof(int)+sizeof(int)); //判断是否开辟成功 if (pps != NULL) { ps = pps; } //继续给增容后的arr数组赋值 for (j = 5; j < 10; j++) { ps->arr[j] = j; printf("%d ", ps->arr[j]); } //动态开辟的空间需要释放 free(ps); ps = NULL; return 0; }
我们会发现,定义结构体成员的时候我们没有指定arr[ ]的大小,而后我们通过malloc()给数组arr开辟空间,可以正常使用,我们还通过realloc()给数组arr增容,增容的空间也可以正常使用。
我们再使用一种容易理解的办法理解一下什么是柔性数组:
//柔性数组 struct S { int i; int *arr; }; int main() { //给S类型的结构体指针变量开辟空间 struct S* ps = (struct S*)malloc(sizeof(struct S)); //给S类型的结构体指针变量的arr成员开辟空间 ps->arr =(int*)malloc(sizeof(int)*5);//正常情况下我们需要判断是否开辟成功 int j = 0; //使用开辟的空间 for (j = 0; j < 5; j++) { ps->arr[j] = j;//0 1 2 3 4 printf("%d ",ps->arr[j]); } //调整空间大小并使用 int* pss = (int*)realloc(ps->arr,10*sizeof(int)); if (pss != NULL) { ps->arr = pss; } for (j = 5; j < 10; j++) { ps->arr[j] = j; printf("%d ",ps->arr[j]); } //释放 free(ps->arr); ps->arr = NULL; free(ps); ps = NULL; return 0; }
画个图理解一下结构:
总结:我们在使用柔性数组的时候,sizeof(struct S) 计算S类型结构体大小时候 ,并没有包含柔性数组成员arr[ ],我们在malloc()动态开辟的时候在后面也加上我们想为arr[ ]开辟大小,一次性开辟成功,然后给到指针 ps 管理。
struct S* ps = (struct S*)malloc(sizeof(struct S)+5*sizeof(int));
感谢每一个观看这篇文章的朋友,如果你看到了这段话,相信你已经对C语言的动态内存有了一个初步的认识,这里我们也已经接近尾声了,是时候说拜拜啦!
怎么会拜拜嘞,更多精彩敬请期待:保护小周ღ *★,°*:.☆( ̄▽ ̄)/$:*.°★*
本期收录于博主的专栏——C语言,适用于编程初学者,感兴趣的朋友们可以订阅,查看其它“C语言基础知识”。C语言_保护小周ღ的博客-CSDN博客
遇见了你,所有的星星都落在了我的头上……