🌟一、为什么存在动态内存分配
我们已经掌握的内存开辟方式有:
int val = 20;在栈空间上开辟四个字节
char arr[10] = {0};在栈空间上开辟10个字节的连续空间
但是上述的开辟空间的方式有两个特点:
1.空间开辟大小是固定的。
⒉.数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。这时候就只能试试动态存开辟了。
🌟二、动态内存函数的介绍
🌏2.1.malloc函数+free函数
C语言提供了一个动态内存开辟的函数:
void*malloc (size_t size);
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
如果开辟成功,则返回一个指向开辟好空间的指针。
如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
返回值的类型是void,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。*
如果参数size为0,malloc的行为是标准是未定义的,取决于编译器。
C语言提供了另外一个函数free,专门 是用来做动态内存的释放和回收的,函数原型如下:
void free (void* ptr);
free函数用来释放动态开辟的内存。
如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。
如果参数ptr是NULL指针,则函数什么事都不做。
malloc和free都声明在stdlib.h头文件中。
各位不要忘了我们之前学数组和指针的时候学过(p+i)==p[i],所以也可以写成p[i]
#include<stdio.h> #include<errno.h> #include<string.h> #include<stdlib.h> int main() { //申请 //我们放整形时malloc是void*所以强制类型转换赋值给int* p int* p=(int*)malloc(20);//malloc这个函数一旦申请空间失败就会返回一个空指针 if (p == NULL) { printf("%s\n", strerror(errno)); return 1; } //开辟成功 --- 使用 int i = 0; for (i = 0; i < 5; i++) { *(p + i) = i + 1;//为了防止p的地址被改变导致后面释放出错所以我们不用p++而是p+i } for (i = 0; i < 5; i++) { printf("%d", *(p + i)); } //释放 free(p);//free函数并不会让p置为空指针 //free将所开辟的空间释放还给系统 p = NULL;//避免了野指针 return 0; }
我们可以在调试内存中看到我们把所想要放入的值放进去了
注意:malloc申请空间时,里面放的都是随机值
🌏2.2.calloc函数+free函数
C语言还提供了一个函数叫ca1loc,cal1oc函数也用来动态内存分配。原型如下:
void* calloc (size_t num,size_t size);
函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0。
·与函数ma1loc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为全0。
#include<stdio.h> #include<errno.h> #include<stdlib.h> int main() { //开辟 int* p = (int*)calloc(10, sizeof(int)); if (p == NULL) { printf("calloc()---%s\n", strerror(errno)); } //使用 int i = 0; for (i = 0; i < 10; i++) { printf("%d ", p[i]); } //释放 free(p); p = NULL; return 0; }
根据运行结果我们可以看出calloc是由初始化的而malloc是随机值
注意:
calloc和malloc的对比:
1.参数不一样
⒉都是在堆区上申请内存空间,但是malloc不初始化,calloc会初始化为0如果要初始化,就使用calloc不需要初始化,就可以使用malloc
🌏2.3.realloc函数
realloc函数的出现让动态内存管理更加灵活。
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。那rea1loc函数就可以做到对动态开辟内存大小的调整。
函数原型如下:
void* rea1loc (void* ptr,size_t size);
ptr是要调整的内存地址
size调整之后新大小(这里是字节哦~)
返回值为调整之后的内存起始位置。
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
#include<stdio.h> #include<errno.h> #include<stdlib.h> int main() { //开辟 int* p = (int*)malloc(20); if (p == NULL) { printf("%s\n", strerror(errno)); return 1; } //使用 int i = 0; for (i = 0; i < 5; i++) { p[i] = i + 1; } int* ptr = (int*)realloc(p, 40);//这里不能用p接受哦因为realloc返回空指针时,p就被赋值为空指针原来的20个字节的空间都找不到了 if (ptr != NULL) { p = ptr; } else { printf("realloc:%s\n", strerror(errno)); return 1; } for (i = 5; i < 10; i++) { p[i] = i+1; } for (i = 0; i < 10; i++) { printf("%d ", p[i]); } //释放 free(p); p = NULL; return 0; }
🌟三、常见的动态内存错误
🌏3.1.对NULL指针的解引用操作
可能会出现对NULL指针的解引用操作
所以malloc函数的返回值要判断
#include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)malloc(20); int i = 0; for (i = 0; i < 5; i++) { p[i] = i; } free(p); p = NULL; return 0; }
🌏3.2.对动态开辟空间的越界访问
#include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)malloc(20); if (p == NULL) { printf("%s\n", strerror(errno)); return 1; } int i = 0; //malloc开辟了20个字节的空间但是这里要访问的却是40个字节的空间发生了越界访问 for (i = 0; i < 10; i++) { p[i] = i; } free(p); p = NULL; return 0; }
🌏3.3.对非动态开辟内存使用free释放
#include<stdio.h> #include<stdlib.h> int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int* p = arr; free(p); p = NULL; return 0; }
🌏3.4.使用free释放一块动态开辟内存的一部分
#include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)malloc(20); if (p == NULL) { printf("%s\n", strerror(errno)); return 1; } int i = 0; for (i = 0; i < 10; i++) { *p = i + 1; p++; } free(p); p = NULL; return 0; }
🌏3.5.对同一块动态内存多次释放
这种问题可能会出现在一些事情上,假设A程序猿开辟一块空间后释放,但是B程序员并不知道,可能会导致二次释放。所以我们可以将p==NULL置为空指针这样就不会报错啦
#include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)malloc(20); if (p == NULL) { printf("%s\n", strerror(errno)); return 1; } //释放 free(p); p = NULL;//我们释放掉把它置为空指针,p为空指针,这样就不会报错 //释放 free(p); p = NULL; return 0; }
🌏3.6.动态开辟内存忘记释放(内存泄漏)
malloc calloc realloc所申请的空间,如果不想使用,需要free释放如果不使用free释放;程序结束之后,也会由操作系统回收;
如果不使用free释放,程序也不结束就会发生内存泄露
#include<stdio.h> void test() { int* p = (int*)malloc(20); //使用 } int main() { test(); //这个函数一旦返回p就被销毁了,并没有把地址返回来 //出了test函数来到主函数内部,主函数并不知道这个内存在哪里所以并没被释放 return 0; }
🌟四、优化通讯录
🌏4.1.优化前后通讯录的创建
首先创建一个通讯录我们想起之前写过的静态通讯录
typedef struct PeoInfo { char name[MAX_NAME]; int age; char sex[MAX_SEX]; char tele[MAX_TELE]; char addr[MAX_ADDR]; }PeoInfo; typedef struct Contact { PeoInfo data[MAX]; int sz; }Contact;
那我们动态版本的怎么写呢?我们让它满足几点要求:
1.默认能够存放3个人的信息
2.不够的话,每次增加2个人信息
这样的话我们的动态版本不够的话每次需要增加两个人的信息所以就不能是数组形式了不然会出现问题malloc开辟的空间的起始地址交给一个指针维护;因为i要扩容我们要定义一个变量(capacity)来记录容量,还有定义一个变量(sz)来记录通讯录中的有效信息
typedef struct PeoInfo { char name[MAX_NAME]; int age; char sex[MAX_SEX]; char tele[MAX_TELE]; char addr[MAX_ADDR]; }PeoInfo; //静态版本 typedef struct Contact { PeoInfo *data;//不够的话每次需要增加两个人的信息所以这里不能用数组会发生问题 //malloc开辟的空间的起始地址交给一个指针维护 //data指向了存向数据的空间 int sz;//记录通讯录中的有效信息 int capacity;//记录通讯录当前的容量 }Contact;
🌏4.2.优化前后通讯录的初始化
接下来就是初始化啦~对于数组我们初始化使用memset函数把里面的每个元素都设置为0
void InitContact(Contact* pc) { pc->sz=0; //pc->date;//date是一个数组是一块连续的空间,数组名是地址不可以改成0 //所以应该把pc所指向通讯录的date数组里的值改成0 memset(pc->data, 0, sizeof(pc->data)); }
但对于动态内存的初始化,我们需要提前在之前静态的版本上再定义一个最开始默认值放在contact.h,这样方便我们日后更改默认值和扩容量
#define DEFAULT_SZ 3//初始默认值为3 #define INC_SZ 2//每次扩容2个
void InitContact(Contact* pc) { pc->data = (PeoInfo*)malloc(DEFAULT_SZ * sizeof(PeoInfo)); //最开始默认大小,但希望我们日后可以随时改变所以我们定义一个DEFAULT_SZ和INC_SZ(扩容) if (pc == NULL) { printf("通讯录初始化失败:%s\n", strerror(errno)); return; } pc->sz = 0; pc->capacity = DEFAULT_SZ;//默认初始值 }
🌏4.3.优化后通讯录的增加人数
不够就增容,所以我们发现主要和增加有关系,所以我们需要判断一下是否通讯录的人数超过三个是否需要扩容,所以我们分装一个函数来完成这个行为(用realloc来完成)
注意:
这里需要注意不然容易出现bug我们扩容后,一定要pc->data = ptr;不然实际上的data并没有扩容我们超过三个人的信息就会发生越界访问,INC_SZ就是我们最开始在contact.h中定义的扩容值,所以扩容后这个记录capacity当前容量的变量会+=2;我们还要注意一点扩容失败返回0,扩容成功,或者不需要扩容,返回1
int CheckCapacity(Contact* pc) { if (pc->sz == pc->capacity) { PeoInfo*ptr=realloc(pc->data, (pc->capacity + 2) * sizeof(PeoInfo)); if (ptr == NULL) { printf("CheckCapacity:%s\n", strerror(errno)); return 0; } else { pc->data = ptr;//返回值为调整之后的内存起始位置 pc->capacity += INC_SZ; printf("增容成功,当前容量:%d\n", pc->capacity); return 1; } } } void AddContact(Contact* pc) { if(CheckCapacity(pc)==0) { printf("空间不够,扩容失败\n"); return 0; } else { printf("请输入名字>:"); scanf("%s", pc->data[pc->sz].name); printf("请输入年龄>:"); scanf("%d", &(pc->data[pc->sz].age)); printf("请输入性别>:"); scanf("%s", pc->data[pc->sz].sex); printf("请输入电话>:"); scanf("%s", pc->data[pc->sz].tele); printf("请输入地址>:"); scanf("%s", pc->data[pc->sz].addr); pc->sz++; printf("输入完成\n"); } }
🌏4.4.优化后通讯录的退出
前面的修改查找删除我们不需要做更改所以我们来到最后一项对于我们malloc开辟的内存空间最后我们要释放,所以我们要在静态版本的基础上进行修改。我们分装一个DestroyContact函数,然后在contact.h中声明再在contact.c中实现
释放完成后,我们将data置为NULL,capacity置为0,sz置为0
void DestroyContact(Contact* pc) { free(pc->data); pc->data = NULL; pc->capacity = 0; pc->sz = 0; printf("释放内存\n"); }
🌏4.5.完整的通讯录
//contact.h #define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #define MAX 100 #define MAX_NAME 20 #define MAX_SEX 5 #define MAX_TELE 12 #define MAX_ADDR 30 //表示一个人信息 typedef struct PeoInfo { char name[MAX_NAME]; int age; char sex[MAX_SEX]; char tele[MAX_TELE]; char addr[MAX_ADDR]; }PeoInfo; typedef struct Contact { PeoInfo data[MAX]; int sz; }Contact; //初始化通讯录 void InitContact(Contact* pc); //增加指定联系人 void AddContact(Contact* pc); //显示联系人信息 void ShowContact(Contact* pc); //删除联系人信息 //void DelContact(pContact pc); void DelContact(Contact* pc); //查找指定联系人 void SearchContact(Contact* pc); //修改指定联系人信息 void ModifyContact(Contact* pc); //排序联系人 void SortContact(Contact* pc);
//contact.c #define _CRT_SECURE_NO_WARNINGS 1 #include"contact.h" #include<string.h> void InitContact(Contact* pc) { pc->sz=0; //pc->date;//date是一个数组是一块连续的空间,数组名是地址不可以改成0 //所以应该把pc所指向通讯录的date数组里的值改成0 memset(pc->data, 0, sizeof(pc->data)); } void AddContact(Contact* pc) { if (pc->sz == MAX) { printf("通讯录已满,无法增加\n"); return;//在函数里面遇见return就返回了,因为是void所以不需要返回值 } printf("请输入名字>:"); //结构体对象用 . //结构体指针用 -> scanf("%s", pc->data[pc->sz].name);//数组名本来就是一个地址所以不需要进行&操作 printf("请输入年龄>:"); scanf("%d", &(pc->data[pc->sz].age));//年龄是一个变量所以需要&操作 printf("请输入性别>:"); scanf("%s", pc->data[pc->sz].sex ); printf("请输入电话>:"); scanf("%s", pc->data[pc->sz].tele ); printf("请输入地址>:"); scanf("%s", pc->data[pc->sz].addr ); pc->sz++; printf("输入完成\n"); } void ShowContact(const Contact* pc) { int i = 0; //printf("%10s %4s %5s %12s %30s\n", "姓名" "年龄" "性别" "电话" "地址"); //打印标题 printf("%-10s %-4s %-5s %-12s %-30s\n", "姓名", "年龄", "性别", "电话", "地址"); //打印数据 for (i = 0; i < pc->sz; i++) { printf("%-10s %-4d %-5s %-12s %-30s\n", pc->data [i].name, pc->data[i].age, pc->data[i].sex, pc->data[i].tele, pc->data[i].addr); } } static int FindByName(Contact*pc,char name[])//加上static这个函数只能在所在.c文件中使用 { int i = 0; for (i = 0; i < pc->sz; i++) { if (strcmp(pc->data[i].name, name) == 0) { return i;//记录所删除联系人所在位置的下标 break; } } if (i == pc->sz)//这里如果判断pos==0时可能是第一个元素因为i是从0开始了所以可以把pos开始赋值为-1 { return -1; } } void DelContact(Contact* pc) { char name[MAX_NAME] = { 0 }; int i = 0; printf("请输入要删除指定联系人的名字\n"); scanf("%s", &name); if (pc->sz == 0) { printf("通讯录为空,无法删除\n"); } //删除 //1.找到删除的指定联系人 - 位置 (下标) int pos = FindByName(pc, name); if (pos == -1) { printf("要删除的人不存在\n"); } //2.删除 - 删除pos位置上的数据 for (i = pos;i<pc->sz -1; i++) { pc->data[i] = pc->data[i + 1]; } pc->sz--;//元素个数减少 printf("删除成功\n"); } void SearchContact(Contact* pc) { //我们发现查找也需要遍历删除也需要所以为了简洁我们可以把这个遍历的数组分装成一个函数 char name[MAX_NAME] = { 0 }; printf("请输入要查找人的名字\n"); scanf("%s", &name); int pos = FindByName(pc, name); if (pos == -1) { printf("要查找的人不存在\n"); } //找到就打印 printf("%-10s %-4s %-5s %-12s %-30s\n", "姓名", "年龄", "性别", "电话", "地址"); printf("%-10s %-4d %-5s %-12s %-30s\n", pc->data[pos].name, pc->data[pos].age, pc->data[pos].sex, pc->data[pos].tele, pc->data[pos].addr); } void ModifyContact(Contact* pc) { char name[MAX_NAME] = { 0 }; printf("请输入修改指定联系人的名字\n"); scanf("%s", &name); int pos = FindByName(pc, name); if (pos == -1) { printf("要修改的人不存在\n"); } //存在就修改 printf("请输入名字>:"); scanf("%s", pc->data[pos].name); printf("请输入年龄>:"); scanf("%d", &(pc->data[pos].age)); printf("请输入性别>:"); scanf("%s", pc->data[pos].sex); printf("请输入电话>:"); scanf("%s", pc->data[pos].tele); printf("请输入地址>:"); scanf("%s", pc->data[pos].addr); printf("修改成功\n"); } //按照名字排序 int cmp_by_name(const void* e1, const void* e2) { return strcmp(((PeoInfo*)e1)->name, ((PeoInfo*)e2)->name); } void SortContact(Contact* pc) { qsort(pc->data, pc->sz, sizeof(PeoInfo),cmp_by_name); printf("排序成功\n"); }
//test.c #define _CRT_SECURE_NO_WARNINGS 1 #include "contact.h" void meau() { printf("*****************************\n"); printf("****1. add 2. del ****\n"); printf("****3. search 4. modify****\n"); printf("****5. show 6. sort ****\n"); printf("****0. exit ****\n"); printf("*****************************\n"); } enum Option { EXIT, ADD, DEL, SEARCH, MODIFY, SHOW, SORT }; int main() { int input = 0; Contact con; InitContact(&con); do { meau(); printf("请选择:>\n"); scanf("%d", &input); switch (input) { case ADD: AddContact(&con); break; case DEL: DelContact(&con); break; case SEARCH: SearchContact(&con); break; case MODIFY: ModifyContact(&con); break; case SHOW: ShowContact(&con); break; case SORT: SortContact(&con); break; case EXIT: printf("退出通讯录\n"); break; default: printf("选择错误\n"); break; } } while (input); return 0; }
🌟五、一些笔试题
1.调用GetMemory函数的时候,str的传参为值传递,p是str的临时拷贝,所以在GetMemory函数内部讲动态开辟空间的地址存放在p中的时候,不会影响str.所以GetMemory函数返回之后,str中依然是NULL指针。strcpy函数就会调用失败,原因是对NULL的解引用操作,程序会崩溃。
2.GetMemory函数内容malloc申请的空间没有机会释放,造成了内存泄露。
#include<stdio.h> #include<string.h> void GetMemory(char* p)//传值 { p = (char*)malloc(100); } void Test(void) { char* str = NULL; GetMemory(str); strcpy(str,"hello world"); printf(str); } int main() { Test(); return 0; }
返回栈空间地址的问题
GetMemory函数内部创建的数组是临时的,虽然返回了数组的起始地址给了str,但是数组的内存出了
GetMemory函数就被回收了,而str依然保存了数组的起始地址,这时如果使用str,str就是野指针。
#include<stdio.h> char* GetMemory(void) { char p[] = "he1lo wor1d"; return p; } void Test(void) { char* str = NULL; str = GetMemory(); printf(str); } int main() { Test(); return 0; }
😽总结
😽Ending,今天的实现动态内存管理(上)+优化版通讯录+笔试题内容就到此结束啦~如果后续想了解更多,就请关注我吧。