1. 为什么存在动态内存分配
我们已经掌握的内存开辟方式有:
int val = 20;//在栈空间上开辟四个字节 char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
但是上述的开辟空间的方式有两个特点:
1. 空间开辟大小是固定的。
2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。
有时候我们需要的空间大小在程序运行的时候才能知道,
那数组的编译时开辟空间的方式就不能满足了。
这时候就只能试试动态存分配了。
所谓动态内存分配(Dynamic Memory Allocation) 就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配不像数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。
2. 动态内存函数的介绍
以下四个动态内存函数头文件都是:stdlib.h
2.1 malloc和free
C语言提供了一个动态内存开辟的函数:void* malloc (size_t size);
size是要开辟的总字节数
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
① 如果开辟成功,则返回一个指向开辟好空间的指针。
② 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
③ 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,
具体在使用的时候使用者自己决定。(所以接收时要强制转换)
④ 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
(有的笔试题:malloc(0)是允许的,也会返回一个指针,只是没有空间所以不可使用而已。)
C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:
void free (void* ptr);
free函数用来释放动态开辟的内存。
① 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
② 如果参数 ptr 是NULL指针,则函数什么事都不做。
注意事项:
① 使用完动态开辟的内存之后一定要记得使用 free 函数释放所开辟的内存空间。
② 使用指针指向动态开辟的内存,使用完并 free 之后一定要记得将其置为空指针。
举个例子:
#include <stdio.h> #include <stdlib.h> int main(void) { // 假设开辟10个整型空间 int arr[10]; // 在栈区上开辟 // 动态内存开辟 int* p = (int*)malloc(10 * sizeof(int)); // 开辟10个大小为int(40字节)的空间 // 使用这些空间的时候 if (p == NULL) { perror("main"); // main: 错误信息 return 0; } // 使用 int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i;// p[i]=i;也行 } for (i = 0; i < 10; i++) { printf("%d ", p[i]); } // 回收空间 free(p); p = NULL; // 需要手动置为空指针 return 0; }
为什么 free 之后,一定要把 p 置为空指针?
解析:因为 free 之后那块开辟的内存空间已经不在了,
free的功能只是把开辟的空间回收掉,但是 p 仍然还指向那块内存空间的起始位置,
这时p就是一个野指针。所以我们需要使用 p = NULL ;手动把他置成空指针。
为什么 malloc 前面要进行强制类型转换呢?
int* p = (int*)malloc(10*sizeof(int));
解析:为了和 int* p 类型相呼应,所以要进行强制类型转换。你可以试着把强转删掉,
其实也不会有什么问题。但是因为有些编译器要求强转,所以最好进行一下强转,避免不必要的麻烦。
2.2 calloc
C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。原型如下:
void* calloc (size_t num, size_t size);
calloc 函数的功能实为 num 个大小为 size 的元素开辟一块空间,并把空间的每个字节初始化为 0 ,和malloc一样:
① 如果开辟成功,则返回一个指向开辟好空间的指针。
② 如果开辟失败,则返回一个NULL指针,因此calloc的返回值也要做检查。
③ 返回值的类型是 void* ,所以calloc函数并不知道开辟空间的类型,
具体在使用的时候使用者自己决定。(所以接收时要强制转换)
④ 如果参数 size 为0,calloc的行为是标准是未定义的,取决于编译器。
(这里的size是一个num的字节)
和malloc的区别:
① malloc 只有一个参数,而 calloc 有两个参数,分别为元素的个数和元素的大小。
② 与函数 malloc 的区别在于 calloc 会在返回地址前把申请的空间的每个字节初始化为 0 。
#include <stdio.h> #include <stdlib.h> int main() { // calloc int* p = (int*)calloc(10, sizeof(int)); // 开辟10个大小为int的空间,40 if (p == NULL) { perror("main"); return 0; } int i = 0; for (i = 0; i < 10; i++) { printf("%d ", *(p + i)); //这里打印了十个0,换成malloc开辟的就打印十个一样的随机值 } free(p); p = NULL; return 0; }
所以如何我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。
2.3 realloc
realloc函数的出现让动态内存管理更加灵活。
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,
那为了合理的使用内存,我们一定会对内存的大小做灵活的调整。
那 realloc 函数就可以做到对动态开辟内存大小的调整。
函数原型如下:
void* realloc (void* ptr, size_t size);
① ptr 为指针要调整的内存地址。
② size 为调整之后的新大小。
③ 返回值为调整之后的内存起始位置,请求失败则返回空指针。
④ realloc 函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
(有的笔试题里:realloc函数可以调整动态申请内存的大小,可大可小)
realloc 函数在调整内存空间时存在的三种情况:
情况1:原有空间之后有足够大的空间。
情况2:原有空间之后没有足够大的空间。
情况3:realloc 有可能找不到合适的空间来调整大小。
情况1:当原有空间之后没有足够大的空间时,直接在原有内存之后直接追加空间,
原来空间的数组不发生变化。
情况2:当原有空间之后没有足够大的空间时,会在堆空间上另找一个合适大小的连续的空间来使用。
函数的返回值将是一个新的内存地址。
情况3:如果找不到合适的空间,就会返回一个空指针。
代码演示:realloc 调整内存大小
#include <stdio.h> #include <stdlib.h> int main() { int* p = (int*)calloc(10, sizeof(int)); if (p == NULL) { perror("main"); return 0; } for (int i = 0; i < 10; i++) { *(p + i) = 5; } // 此时,这里需要p指向的空间更大,需要20个int的空间 // realloc 调整空间 //p = (int*)realloc(p, 20 * sizeof(int)); // 调整为20个int的大小的空间 /*刚才提到的第三种情况,如果 realloc 找不到合适的空间,就会返回空指针。 我们想让它增容,他却存在返回空指针的危险,然后我们就找不到原来的空间了 解决方案:不要拿指针直接接收 realloc,可以使用临时指针判断一下。*/ int* tmp = (int*)realloc(p, 20 * sizeof(int));// 调整为20个int的大小的空间 // 如果ptmp不等于空指针,再把p交付给它 if (tmp != NULL) { p = tmp; } free(p); // 释放 p = NULL; // 置空 }
有趣的是,其实你可以把 realloc 当 malloc 用:
// 在要调整的内存地址部分,传入NULL: int* p = (int*)realloc(NULL, 40); // 这里功能类似于malloc,就是直接在堆区开辟40个字节
插入练习:把上篇通讯录改成动态的
思考后发现,我们只需要先该存放联系人的结构体,再改初始化通讯录函数和增加联系人函数,其它函数都可以不用动,最后在选0退出程序之前写一个销毁通讯录函数,释放开辟的空间就行了。
以下是基于上篇最后的代码改的:
test.c:
//通讯录-静态版本 //1.通讯录中能够存放1000个人的信息 //每个人的信息: //名字+年龄+性别+电话+地址 //2. 增加人的信息 //3. 删除指定人的信息 //4. 修改指定人的信息 //5. 查找指定人的信息 //6. 排序通讯录的信息 // //版本2: //动态增长的版本 //1.通讯录初始化后,能存放3个人的信息 //2.当我们空间的存放满的时候,我们增加2个信息 //3+2+2+2+... #include"contact.h" void menu() { printf("****************************************\n"); printf("****** 1.add 2.del ********\n"); printf("****** 3.search 4.modify ********\n"); printf("****** 5.sort 6.print ********\n"); printf("****** 0.exit ********\n"); printf("****************************************\n"); } enum option { EXIT, ADD, DEL, SEARCH, MODIFY, SORT, PRINTF }; int main() { int input = 0; //创建通讯录 info(信息) contact con; //初始化通讯录函数 init_contact(&con); do { menu(); printf("请选择:"); scanf("%d", &input); switch (input) { case ADD: add_contact(&con); break; case DEL: del_contact(&con); break; case SEARCH: find_contact(&con); break; case MODIFY: modify_contact(&con); break; case SORT: sort_contact(&con); break; case PRINTF: print_contact(&con); break; case EXIT: destroy_contact(&con); printf("退出程序\n"); break; default: printf("选择错误,重新选择\n"); break; } } while (input); return 0; }
contact.h
#include<stdio.h> #include<string.h> #include<stdlib.h>//qsort,perror,动态内存开辟函数 #define MAX_NAME 20 #define MAX_SEX 10 #define MAX_TELE 20 #define MAX_ADDR 30 #define DEFAULT_SZ 3 //default 默认 #define INC_SZ 2 //sz增量 //类型的定义 typedef struct peoinfo { char name[MAX_NAME]; char sex[MAX_SEX]; int age; char tele[MAX_TELE]; char addr[MAX_ADDR]; }peoinfo; 静态版本通讯录 //typedef struct contact //{ // peoinfo data[MAX];//存放添加进来的人的信息 // int sz;//记录的是当前通讯录中有效信息的个数 //}contact; //动态版本通讯录 typedef struct contact { peoinfo* data;//指向动态开辟的空间,存放添加进来的人的信息 int sz;//记录的是当前通讯录中有效信息的个数 int capacity;//记录当前通讯录的最大容量,方便增容 }contact; //初始化通讯录函数 void init_contact(contact* p); //增加联系人函数 void add_contact(contact* p); //销毁联系人函数 void destroy_contact(contact* p); //打印联系人函数 void print_contact(const contact* p); //删除联系人函数 void del_contact(contact* p); //查找联系人函数 void find_contact(const contact* p); //修改联系人函数 void modify_contact(contact* p); //排序联系人函数 void sort_contact(contact* p);
contact.c
#include"contact.h" 初始化通讯录函数 静态版本 //void init_contact(contact* p) //{ // p->sz = 0; // memset(p->data, 0, sizeof(p->data)); //} //初始化通讯录函数 动态版本 void init_contact(contact* p) { p->data = (peoinfo*)calloc(DEFAULT_SZ, sizeof(peoinfo)); if (p->data == NULL) { perror("init_contact"); return; } p->sz = 0;//初始化后默认是0 p->capacity = DEFAULT_SZ; } 增加联系人函数 静态版本 //void add_contact(contact* p) //{ // if (p->sz == MAX) // { // printf("通讯录已满,无法添加"); // return; // } // printf("请输入要添加人的姓名:"); // scanf("%s", p->data[p->sz].name); // printf("请输入要添加人的性别:"); // scanf("%s", p->data[p->sz].sex); // printf("请输入要添加人的年龄:"); // scanf("%d", &p->data[p->sz].age);//只用年龄不是数组,要取地址 // printf("请输入要添加人的电话:"); // scanf("%s", p->data[p->sz].tele); // printf("请输入要添加人的住址:"); // scanf("%s", p->data[p->sz].addr); // // p->sz++; // printf("添加成功\n"); //} //增加联系人函数 动态版本 void add_contact(contact* p) { if (p->sz == p->capacity) { peoinfo* tmp = (peoinfo*)realloc(p->data, (p->capacity + INC_SZ) * sizeof(peoinfo)); if (tmp == NULL) { perror("add_contact"); printf("增容失败\n"); return; } p->data = tmp; p->capacity += INC_SZ; printf("增容成功\n"); } printf("请输入要添加人的姓名:"); scanf("%s", p->data[p->sz].name); printf("请输入要添加人的性别:"); scanf("%s", p->data[p->sz].sex); printf("请输入要添加人的年龄:"); scanf("%d", &p->data[p->sz].age);//只用年龄不是数组,要取地址 printf("请输入要添加人的电话:"); scanf("%s", p->data[p->sz].tele); printf("请输入要添加人的住址:"); scanf("%s", p->data[p->sz].addr); p->sz++; printf("添加成功\n"); } //销毁联系人函数 void destroy_contact(contact* p) { free(p->data); p->data = NULL; p->sz = 0; p->capacity = 0; } //打印联系人函数 void print_contact(const contact* p) { if (p->sz == 0) { printf("通讯录为空,无法打印\n"); } else { printf("%-10s %-10s %-10s %-15s %-10s\n", "姓名", "性别", "年龄", "电话", "住址"); for (int i = 0;i < p->sz;i++) { printf("%-10s %-10s %-10d %-15s %-10s\n", p->data[i].name, p->data[i].sex, p->data[i].age, p->data[i].tele, p->data[i].addr); } } } //查找函数的一部分,删除和修改也要用,只放在这就行 static int find_by_name(contact* p, char name[]) { for (int i = 0;i < p->sz;i++) { if (strcmp(p->data[i].name,name)==0) { return i; } } return -1; } //删除联系人函数 void del_contact(contact* p) { if (p->sz == 0) { printf("通讯录为空,无法删除\n"); } else { char name[MAX_NAME] = { 0 }; printf("请输入要删除人的名字:"); scanf("%s", name); int pos = find_by_name(p, name); if (pos == -1) { printf("要删除的人不存在\n"); } else { for (int i = pos;i < p->sz - 1;i++) { p->data[i] = p->data[i + 1]; } p->sz--; printf("删除成功\n"); } } } //查找联系人函数 void find_contact(const contact* p) { if (p->sz == 0) { printf("通讯录为空,无法查找\n"); } else { char name[MAX_NAME] = { 0 }; printf("请输入要查找人的名字:"); scanf("%s", name); int pos = find_by_name(p, name); if (pos == -1) { printf("要查找的人不存在\n"); } else { printf("%-10s %-10s %-10s %-15s %-10s\n", "姓名", "性别", "年龄", "电话", "住址"); printf("%-10s %-10s %-10d %-15s %-10s\n", p->data[pos].name, p->data[pos].sex, p->data[pos].age, p->data[pos].tele, p->data[pos].addr); } } } //修改联系人函数 void modify_contact(contact* p) { if (p->sz == 0) { printf("通讯录为空,无法查找\n"); } else { char name[MAX_NAME] = { 0 }; printf("请输入要修改人的姓名:"); scanf("%s", name); int pos = find_by_name(p, name); if (pos == -1) { printf("要修改的人不存在\n"); } else { printf("以下是输入要修改人的新信息:\n"); printf("请输入姓名:"); scanf("%s", p->data[pos].name); printf("请输入性别:"); scanf("%s", p->data[pos].sex); printf("请输入年龄:"); scanf("%d", &p->data[pos].age); printf("请输入电话:"); scanf("%s", p->data[pos].tele); printf("请输入住址:"); scanf("%s", p->data[pos].addr); printf("修改成功\n"); } } } //排序联系人函数 int cmp(void* e1, void* e2) { return strcmp(((peoinfo*)e1)->name, ((peoinfo*)e2)->name); } void sort_contact(contact* p) { if (p->sz == 0) { printf("通讯录为空,无法排序\n"); } else { qsort(p->data, p->sz, sizeof(peoinfo), cmp); printf("按照姓名排序成功\n"); } } void sort_contact2(contact* p)//冒泡,比较麻烦,就用qsort了 { if (p->sz == 0) { printf("通讯录为空,无法排序\n"); } else { for (int i = 0; i < p->sz; i++) { for (int j = 0; j < p->sz - i - 1; j++) { if (strcmp((p->data[j].name), (p->data[j + 1].name)) > 0) { peoinfo data = p->data[j]; p->data[j] = p->data[j + 1]; p->data[j + 1] = data; } } } printf("按照姓名排序成功\n"); } }
完成静态或动态通讯录后想到怎么能把内容保存下次用呢?
这时就要用到后面学的文件操作,(放到文件只是学C语言的一种临时方法,后面学得更多会放到数据库中)
C语言进阶⑰(动态内存管理)四个动态内存函数+动态通讯录+柔性数组_malloc+free(中):https://developer.aliyun.com/article/1513205