为什么存在动态内存分配
int main() { int arr[20] = {0};//整形数组开辟20个元素,就是80个字节。 return 0; }
1.空间开辟大小是固定的
2.数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
如果我们开辟的空间不够,那么进行修改会比较麻烦。如果开辟的空间较大,那么所占据的空间又会很大。那么有没有一种方法可以用多少空间就开辟多大的内存呢?
这个时候就有了动态内存开辟。
malloc
动态内存函数的头文件 <stdlib.h>
void* malloc (size_t size)
申请内存块,
size
就是我们要申请的字节大小。当然,我们的内存是有限的,不是想要申请多少内存就可以申请多少内存。
返回值
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
- 返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
开创的空间放回地址是void*类型,使用的时候记得要强制类型转换。
所以我们在使用malloc函数时,要检测返回值p是否是空指针。
#include <string.h> #include <errno.h> #include <stdlib.h> #include<stdio.h> int main() { int arr[10] = { 0 }; //动态内存开辟 int* p = (int*)malloc(40); if (p == NULL) //检测返回的p是不是空指针。 { printf("%s\n", strerror(errno)); return 1; } //使用动态内存 int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i; } for (i = 0; i < 10; i++) { printf("%d ", *(p + i)); } //释放 free(p); //常常搭配malloc、calloc、realloc使用,后面会讲到 p = NULL; return 0; }
动态内存空间位置
动态内存函数是在堆区开辟内存空间的。我们一般的局部变量、形式参数都是存放在栈区。
内存泄漏问题
什么是内存泄漏?
我们创建的局部变量,数据会在函数结束时释放。
动态内存空间存放临时使用的数据,这些数据不必等到函数结束时释放,而是需要时随时开辟,不需要时,随时释放。动态开辟的内存使用完是要进行释放的,如果不对内存进行释放,那么开辟的动态内存就会被之前的数据占据,这部分的内存就无法使用,相当于丢失了内存。因此我们把这类问题叫做内存泄漏。
下面的代码如果吧进行释放,那么就会一直占据内存空间。
通过释放动态内存,内存可以被重新调用
最后我们加了一步,p = NULL,p的内存空间已经被释放了还给操作系统了,但是p还是原来的地址,我们通过p就会使用到一个已经释放的内存,这就会导致野指针问题。为了避免这种情况,我们将p的地址去掉就可以了。
free
void free (void* ptr)
free函数是专门用来释放动态开辟的内存。
- 如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。
- 如果参数ptr 是NULL指针,则函数什么事都不做。
free函数只能释放动态内存开辟的空间,如果释放其他空间,就会报错。
#include <stdlib.h> int main() { int a = 0; int* p = &a; free(p); p = NULL; return 0; }
calloc
void* calloc (size_t num, size_t size)
num
开辟空间元素的个数
size
空间中每个元素的大小。
- 函数的功能为num个大小为size 的元素开辟一块空间,并且把空间的每个字节初始化为0
- calloc和malloc的区别在于calloc在放回地址之前把申请的空间的每个字节初始化为全0。
下面的例子就是解释:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> int main() { int* p = (int*)calloc(10, sizeof(int)); if (p == NULL) { printf("%s\n", strerror(errno)); } //打印 int i = 0; for (i = 0; i < 10; i++) { printf("%d ", *(p + i)); } //释放 free(p); p = NULL; return 0; }
realloc
void* realloc (void* ptr, size_t size)
ptr
要调整的空间的起始位置。
size
- realloc函数的出现让动态内存管理更加灵活。
- 有时我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间豁达了,那为了合理的内存,我们一定对内存的大小做灵活的调整。那realloc函数可以做到对动态开辟内存大小的调整。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> int main() { int* p = (int*)malloc(40); if (p == NULL) { printf("%s\n", strerror(errno)); } //使用 int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i + 1; } //扩容 int* ptr = realloc(p, 80); if (ptr != NULL) //这里不直接将新的地址赋给p,是因为realloc有可能开辟失败返回空指针。后面详细讲 { p = ptr; } for(i=0;i<10;i++) { printf("%d ", *(p + i)); } //释放 free(p); p = NULL; return 0; }
realloc如何开辟动态内存空间
realloc在调整内存空间的是存在两种情况。
第一种情况,原空间之后有足够大的空间。当要进行调整的内存空间后面有多余的40个字节空间,那么就可以直接开辟向后面开辟40个字节的空间,然后放回起始位置的地址,这里指的是0的地址。
第二种情况,原空间之后没有足够大的空间。要调整的内存空间后面不足以存放40个字节的空间,那么就要重新找到一个新的地址(可以存放80个字节)开辟,开辟成功后返回起始位置的地址。
注意:如果需要开辟的空间过大,是会开辟失败的,开辟失败,realloc返回空指针,所以要检查空指针。
这里把realloc开辟的空间换成8000,来实现第二种情况。
动态版本的通讯录
动态:
要实现通讯录容量的动态化,要实现两个功能
1.通讯录默认能存放3个人的信息
2.如果空间不够了,就增加空间,每次增加2个人的空间
静态:
原来的通讯录的信息由结构体组成的数组,来存放信息,设置的是100人的信息。
1.当没有100人的信息时,会造成空间上的浪费。
2.当超过100人的信息时,又无法自动扩容,如果想要扩容要手动改变最大存放的空间。
原通讯录的代码:
有兴趣可以学习通讯录的实现http://t.csdnimg.cn/UbT9I
存放数据的改变
首先把存放联系人信息的结构体给大家看看
typedef struct PeoInfo { char name[MAX_NAME]; int age; char sex[MAX_SEX]; char tele[MAX_TELE]; char addr[MAX_ADDR]; }PeoInfo;
原来存放联系人的信息是通过数组data[100],这样的数组的空间是固定的,是一开始设置的最大容量。
typedef struct Contact { PeoInfo data[MAX]; int count;//记录当前通讯录中实际人数的个数 }Contact;
不以数组的形式进行存放,以数组改成地址,但是地址也可以像数组一样访问数据。data是联系人的起始地址,如果想要访问第二个人,就是*(data+1),等于data[1]。
count是当前使用了多少个联系人的空间.
capacity记录的是现在存放联系人的容量是多大。因为当每次容量不够时,就增加空间,每次增加2个人的空间。也就是每次count == capacity的时候,capacity就要加2.
typedef struct Contact { PeoInfo* data; int count;//记录当前通讯录中实际人数的个数 int capacity; }Contact;
1.初始化的改变
assert函数,用来检验空指针,如果为空指针,就会报错。
参数pc,是创立的struct Contact结构体变量的地址,这里是传址调用,作用就是改变原来的数据。
原通讯录是存放100个联系人的数组,将100个联系人的数据初始化为0。
void InitContact(Contact* pc) { assert(pc); pc->count = 0; memset(pc->data, 0, sizeof(pc->data)); }
动态通讯录要实现开辟3个联系人的空间,并将它们进行初始化。 既要开辟空间,又要进行初始化,我们想到calloc函数。
malloc是单纯地开辟空间,realloc是既开辟空间,并进行初始化。malloc和realloc地区别就在于是否对开辟的空间初始化。
开辟3个联系人空间,calloc进行开辟,将地址传给pc->data.将记录联系人的容量传给capacity.
int InitContact(Contact* pc) { assert(pc); pc->count = 0; pc->data =(int*) calloc(3, sizeof(PeoInfo)); if (pc == NULL) { printf("InitContact::%s\n", strerror(errno)); return 1; } pc->capacity = 3; return 0; }
2.增加联系人
参数pc,是创立的struct Contact结构体变量的地址,这里是传址调用,作用就是改变原来的数据。
动态内存管理,通过pc->data[count]可以进行数据的输入。最重要的是实现通讯录容量的动态化。
count表示已经使用的联系人数量,capacity表示联系人的总容量。
当count == capacity时,就要动态开辟内存,对容量进行增容。
CheckCapacity为自定义增容函数,我们要实现增容的功能。
realloc重新开辟内存块,实现内存的动态化。realloc返回的起始地址不能直接传给data,因为动态内存的开辟有可能失败,失败传回空指针。
capacity增加2,最后提示增容成功。
void CheckCapacity(Contact* pc) { if (pc->count == pc->capacity); { PeoInfo* ptr = (PeoInfo*)realloc(pc->data, (pc->capacity + 2) * sizeof(PeoInfo)); if (ptr == NULL) { printf("AddContact::%s\n", strerror(errno)); return 1; } else { pc->data = ptr; pc->capacity += 2; printf("增容成功\n"); } } } void AddContact(Contact* pc) { assert(pc); //增容 CheckCapacity(pc); printf("请输入名字:》"); scanf("%s", pc->data[pc->count].name); printf("请输入年龄:》"); scanf("%d", &(pc->data[pc->count].age)); printf("请输入性别:>"); scanf("%s", pc->data[pc->count].sex); printf("请输入电话:>"); scanf("%s", pc->data[pc->count].tele); printf("请输入地址:>"); scanf("%s", pc->data[pc->count].addr); pc->count++; printf("增加成功\n"); }
将整体的代码呈现在文章末尾。
常见动态内存错误
1.对NULL空指针进行解引用
如果开辟的空间过大,malloc有可能开辟失败,开辟失败就会返回空指针。如果直接对p进行解引用,就会产生问题。
#include <stdlib.h> int main() { int* p = (int*)malloc(40); *p = 20; return 0; }
正确解决方法:
在开辟动态内存后对p进行检验,是否为空指针。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> int main() { int* p = (int*)malloc(40); if (p == NULL) { printf("%s\n", strerror(errno)); return 1; } *p = 20; free(p); p = NULL; return 0; }
2.对动态开辟空间的越界访问
我们只开辟了10个字节的空间,但是访问,从0到10,10算进去的话就是11个元素,这里访问越界了,就会出问题。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> int main() { int* p = (int*)malloc(40); if (p == NULL) { printf("%s\n", strerror(errno)); return 1; } //使用 int i = 0; for (i = 0; i <= 10;i++) { p[i] = i; } free(p); p = NULL; return 0; }
正确解决方法:
一定要注意我们开辟的空间是否和访问的空间是一样的。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> int main() { int* p = (int*)malloc(40); if (p == NULL) { printf("%s\n", strerror(errno)); return 1; } //使用 int i = 0; for (i = 0; i < 10;i++) { p[i] = i; } free(p); p = NULL; return 0; }
3.对非动态开辟的内存使用free释放
free只能够释放动态开辟的内存,不能够随意去使用函数。
#include <stdlib.h> int main() { int a = 10; int* p = &a; //.... free(p); p = NULL; return 0; }
4.使用free释放动态开辟内存的一部分
free做不到释放动态内存的一部分,如果要释放要将整个动态内存进行释放。
#include <stdlib.h> int main() { int* p = (int*)malloc(40); if (p = NULL) { return 1; } int i = 0; for (i = 0; i < 10; i++) { *p = i; p++; } free(p); p = NULL; return 0; }
正确解决方法:
不改变p的位置,对p进行释放。使用p的话,通过:
*(p+i) = i;
5.对同一块动态内存多次释放
free的二次使用,第二次使用的动态内存空间已经还给操作系统了,但是还能对p进行操作,就是野指针问题。
#include <stdlib.h> int main() { int* p = (int*)malloc(40); free(p); //.... free(p); return 0; }
正确解决方法:
避免free的二次使用,或者将p转化为空指针。
#include <stdlib.h> int main() { int* p = (int*)malloc(40); free(p); p = NULL; free(p); return 0; }
6.动态开辟内存忘记释放(内存释放)
看看下面的例子:
如果flag = 5的话,那么后面free函数就会跳过,动态开辟的内存就不能释放。
#include <stdio.h> #include <stdlib.h> void test() { int* p = (int*)malloc(100); int flag = 0; scanf("%d", &flag); if (flag == 5) { return; } free(p); p = NULL; } int main() { test(); return 0; }
动态通讯录的实现
contact.h
头文件:用来对函数的声明
#pragma once #include <string.h> #include <stdio.h> #include <assert.h> #include <stdlib.h> #define DEFAULT_SZ 3 #define INC_SZ 2 #define MAX 100 #define MAX_NAME 20 #define MAX_SEX 10 #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; int count;//记录当前通讯录中实际人数的个数 int capacity; }Contact; //初始化通讯录 int InitContact(Contact* pc); //销毁通讯录 void DestroyContact(Contact* pc); //增加联系人到通讯录 void AddContact(Contact* pc); //打印通讯录中信息 void ShowContact(const Contact* pc); //删除联系人的信息 void DelContact(Contact* pc); //查找指定联系人 void SeachContact(Contact* pc); //修改指定联系人 void ModifyContact(Contact* pc); //排序通讯录中的内容 //按照名字来排序 void SortContact(Contact* pc);
contact.c
源文件:函数的实现和定义
#define _CRT_SECURE_NO_WARNINGS #include "contact.h" //动态版本 int InitContact(Contact* pc) { assert(pc); pc->count = 0; pc->data =(int*) calloc(DEFAULT_SZ, sizeof(PeoInfo)); if (pc == NULL) { printf("InitContact::%s\n", strerror(errno)); return 1; } pc->capacity = DEFAULT_SZ; return 0; } void DestroyContact(Contact* pc) { assert(pc); free(pc->data); pc->data = NULL; } void CheckCapacity(Contact* pc) { if (pc->count == pc->capacity); { PeoInfo* ptr = (PeoInfo*)realloc(pc->data, (pc->capacity + INC_SZ) * sizeof(PeoInfo)); if (ptr == NULL) { printf("AddContact::%s\n", strerror(errno)); return 1; } else { pc->data = ptr; pc->capacity += INC_SZ; printf("增容成功\n"); } } } void AddContact(Contact* pc) { assert(pc); //增容 CheckCapacity(pc); printf("请输入名字:》"); scanf("%s", pc->data[pc->count].name); printf("请输入年龄:》"); scanf("%d", &(pc->data[pc->count].age)); printf("请输入性别:>"); scanf("%s", pc->data[pc->count].sex); printf("请输入电话:>"); scanf("%s", pc->data[pc->count].tele); printf("请输入地址:>"); scanf("%s", pc->data[pc->count].addr); pc->count++; printf("增加成功\n"); } void ShowContact(const Contact* pc) { assert(pc); int i = 0; //一个汉字是两个字符 printf("%-20s\t%-5s\t%-5s\t%-12s\t%-30s\n", "名字", "年龄", "性别", "电话", "地址"); for (i = 0; i < pc->count; i++) { printf("%-20s\t%-5d\t%-5s\t%-12s\t%-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[]) { assert(pc); int i = 0; for (i = 0; i < pc->count; i++) { if (0 == strcmp(pc->data[i].name, name)) { return i; } } return -1; } void DelContact(Contact* pc) { char name[MAX_NAME] = { 0 }; assert(pc); int i = 0; if (pc->count == 0) { printf("通讯录为空,没有信息可以删除\n"); return; } printf("请输入要删除人的名字:>"); scanf("%s", name); //删除 //1.查找 int pos = FindByName(pc, name); if (pos == -1) { printf("要删除的人不存在\n"); return; } //2.删除 for (i = pos; i < pc->count; i++) { pc->data[i] = pc->data[i + 1]; } pc->count--; } void SeachContact(Contact* pc) { assert(pc); char name[MAX_NAME] = { 0 }; printf("请输入需要查找的联系人的名字:>"); scanf("%s", name); //1.查找 int pos = FindByName(pc, name); if (pos == -1) { printf("要查找的人不存在\n"); return; } //2.打印 printf("%-20s\t%-5s\t%-5s\t%-12s\t%-30s\n", "名字", "年龄", "性别", "电话", "地址"); printf("%-20s\t%-5d\t%-5s\t%-12s\t%-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) { assert(pc); char name[MAX_NAME] = { 0 }; printf("请输入需要查找的联系人的名字:>"); scanf("%s", name); //1.查找 int pos = FindByName(pc, name); if (pos == -1) { printf("要查找的人不存在\n"); return; } printf("要修改人的信息已经找到,接下来进行修改\n"); //2.修改 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_peo_by_name(const void* e1, const void* e2) { return strcmp(((PeoInfo*)e1)->name, ((PeoInfo*)e2)->name); } //按照名字来排序 void SortContact(Contact* pc) { assert(pc); qsort(pc->data, pc->count, sizeof(PeoInfo), cmp_peo_by_name); printf("排序成功\n"); }
test.c
通讯录主脉络:
#define _CRT_SECURE_NO_WARNINGS #include "contact.h" void menu() { 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"); } int main() { int input = 0; Contact con; //初始化通讯录:模块化初始化 InitContact(&con);//只能传地址,进行修改 do { menu(); printf("请选择:》"); scanf("%d", &input); switch (input) { case 1: AddContact(&con); break; case 2: DelContact(&con); break; case 3: SeachContact(&con); break; case 4: ModifyContact(&con); break; case 5: ShowContact(&con); break; case 6: SortContact(&con); break; case 0: DestroyContact(&con); printf("退出通讯录\n"); break; default: printf("选择错误\n"); } } while (input); return 0; }