哦吼!今天的干货来了哦家人们,冲冲冲,有错误还请大家帮忙斧正一下哈哈。好了,安全带系好,准备发车了!
目录
一,为什么有动态内存管理
通常情况下我们是如下开辟空间的:
int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
但是上述的开辟空间的方式有两个特点:
1. 空间开辟大小是固定的。
2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
在某些情况下,空间的大小是需要实时开辟的,大小也是不一定的,所以这时候就需要用到动态内存的开辟。动态内存开辟,都是在对堆区上的空间进行操作。
二,动态内存函数
2.1 malloc和free
void* malloc (size_t size);//申请空间,以字节为单位
- 这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
- 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己 来决定。
- 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器(没有明确规定你错还是对,但是最好不要这么用,不知道后果)
既然能开辟空间,那怎么释放呢?
C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:
void free (void* ptr);
- free函数用来释放动态开辟的内存。
- 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
- 如果参数 ptr 是NULL指针,则函数什么事都不做。
使用示例:
#include<stdio.h>
#include<stdlib.h>
int main() {
int* ptr = (int*)malloc(40);
if (ptr == NULL) {
perror("malloc");
return 1;
}
for (int i = 0;i < 10;i++) {
*(ptr + i) = i;
}
free(ptr);
ptr = NULL;
return 0;
}
注意:最后在free掉指定空间时,只是把ptr指向的空间释放了,但是这个指针变量还是存在的,也就是说ptr任然还保存着那个地址,但是空间被释放了,也即是ptr这个时候是一个野指针,所以最后还需要把ptr赋值为NULL,否则有可能会造成非法访问。
那么,可能有人会问了,我们不free会怎么样?
当我们不去手动释放malloc申请的空间的时候,那么程序结束的时候系统会自动回收。但如果你不手动释放,程序还是一直运行的,那结果就是内存泄漏。
2.2 calloc
void* calloc (size_t num, size_t size);
num是代表的是元素的个数,size代表的是每一个元素的空间大小。
- 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
- 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
使用示例:
int main() {
int* pc = (int*)calloc(10, sizeof(int));
if (pc == NULL) {
perror("calloc");
return 1;
}
for (int i = 0;i < 10;i++) {
printf("%d ", *(pc + i));
}
free(pc);
pc = NULL;
return 0;
}
2.3,realloc
void* realloc (void* ptr, size_t size);
//ptr代表你要扩容的那块空间的起始地址,size是你扩容后的新大小,返回的是你扩容后的空间的起始地址
realloc扩容会存在两种情况:
第一种情况就是:在原来的空间后面,就有足够的空间进行扩容,系统会直接在原来的基础之上扩容到你指定的大小,并且返回扩容后的空间的起始地址,在这种情况下,起始地址是没有变化的。
第二种情况就是:在原来的空间后面,没有足够的空间扩容,得新找空间,这个时候其实就不像是扩容了,而是会直接开辟一块你指定的大小的空间,然后返回你新开辟的这块空间的地址,与此同时,系统会自动将原来空间中有的值拷贝过来放到新空间中,原来的空间会被直接free掉。
realloc还有一个特别要注意的点:
三,动态内存常见错误
3.1 对NULL指针的解引用操作
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
解决方法就是在使用之前一定要对指针先判定是否为空指针,不然对空指针解引用就是非法访问内存。
3.2 对动态开辟空间的越界访问
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
return;
}
for(i=0; i<=10; i++)
{
*(p+i) = i;//当i是10的时候越界访问
}
free(p);
}
和静态开辟的空间一样,动态申请的空间也是不能越界访问的,你申请了多大,最后就只能访问那么大,所以在写程序的时候一定要注意好边界的问题。
3.3 对非动态开辟内存使用free释放
void test()
{
int a = 10;
int *p = &a;
free(p);//ok?
}
对于非动态开辟的空间,你不能用free去释放,因为free是释放动态开辟的空间的
3.4 使用free释放一块动态开辟内存的一部分
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
此时p不是指向的起始地址,所以你释放的只是动态开辟的空间的一部分,但这是不允许的。
3.5 对同一块动态内存多次释放
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
此时指针p中任然存储着开辟的空间的地址,只不过空间被释放了,但是对于一块空间,释放一次就够了,不能重复释放。
3.6 动态开辟内存忘记释放(内存泄漏)
void test()
{
int *p = (int *)malloc(100);//指针p是一个局部指针,所以出了test函数就会被销毁,那这块动态开辟的空间的地址也就没人知道了。
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}
对于动态开辟的空间,如果你不手动释放,那就得等到程序结束系统自己回收,但是如果程序一直执行,你也不手动释放,那就造成了内存泄漏。
所以,最后总结一下,使用malloc,calloc,realloc一般都适合free配合使用的,但是即使配合使用也不一定保证就不会出问题,因为还有逻辑问题,有可能你写了free但是程序执行不到free。所以,写的时候一定要谨慎,不然可就真的和那句话说的似的,一个程序员要写bug,谁也拦不住!
4. 经典笔试题
4.1 题目1:
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
解析:
4.2 题目2:
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
解析:
由这个题,我们可以推出一系列的这类问题,也就是返回栈空间地址的问题,因为在栈上创建的一般都是局部变量以及函数参数,都只是在局部范围内临时作用,出了范围就会被回收,但是一旦你返回了栈空间变量的地址,那你使用的时候也就会出现野指针的问题。
4.3 题目3:
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
str在这段代码里面是传址调用,这里是会正常输出hello的,但也有一些小的瑕疵,就是在test函数里面最好最后free一下str,并且在GetMemory函数里面也没有对指针p判断是否为NULL。
4.4 题目4:
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
解析:
5. 通信录升级(动态增长版)
在上次总结的通信录里面,通信录的大小是固定的,也就是人数是定下来的,而当我们在写了动态内存管理后,是不是就可以升级为动态增长版,假设初始情况下最多存3人,满了就扩容,一直这样循环,实现一个动态增长的效果,那咱话不多说,直接上代码!
5.1,contact.h
#pragma once
#include<string.h>
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<Windows.h>
//头文件包含函数的声明 与 类型的声明
//宏定义将后面的一些具体数字进行替换,是代码后期便于更改维护
#define size_max 3
#define name_max 20
#define sex_max 3
#define phone_max 12
#define address_max 30
//联系人具体信息结构体的声明
typedef struct perinfo {
char name[name_max];
int age;
char sex[sex_max];
char phone[phone_max];
char address[address_max];
}perinfo;
//动态版
//通信录结构体的声明
typedef struct contact {
perinfo* date;
int sz;
int lim_size;
}contact;
//枚举类型的声明,因为case后面的数字不能很好的直接与功能挂上钩,所以对于我们的选择可以定义一个枚举类型,然后将case后的数字换成具体的功能的名字
//增加代码的可读性
enum option {
EXIT,
ADD,
DEL,
SER,
MOD,
SORT,
PRINT,
};
//函数声明
//初始化函数
void init(contact* pc);
//销毁函数
void DELcontac(contact* pc);
//添加函数
void ADDinfo(contact* pc);
//查找函数(此查找函数只是单纯找到人返回下标)
int ser_byname(const contact* pc);//只是查找,不会对指针指向的内容进行修改,用const修饰一下
//删除函数
void DELinfo(contact* pc);
//打印函数
void PRINTinfo(const contact* pc);
//查找函数
void SERinfo(const contact* pc);
//修改函数
void MODinfo(contact* pc);
//排序函数
void SORTinfo(contact* pc);
//比较函数
int comparestu(const void* e1, const void* e2);
5.2,contact.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "contact.h"
//函数实现
//初始化函数实现
void init(contact *pc) {
assert(pc);
pc->lim_size = size_max;//size_max是最初通信录能存储的最大容量
pc->sz = 0;
pc->date = (perinfo*)malloc(pc->lim_size * sizeof(perinfo));//malloc申请空间,将空间地址传给联系人的结构体指针
if (pc->date == NULL) {
perror("init::malloc");//如果malloc返回空指针则是开辟失败
return;
}
memset(pc->date, 0, pc->lim_size * sizeof(perinfo));//将开辟的空间初始化为0
}
//销毁通信录
void DELcontact(contact* pc) {
free(pc->date);
pc->date = NULL;
pc->lim_size = 0;
pc->sz = 0;
printf("已销毁!\n");
}
//添加函数
void check_capacity(contact* pc) {
if (pc->sz == pc->lim_size) {
perinfo* tmp = (perinfo*)realloc(pc->date, (pc->lim_size + 2) * sizeof(perinfo));
if (tmp != NULL) {
printf("扩容成功!\n");
pc->date = tmp;
pc->lim_size += 2;
}
else {
perror("check_capacity::realloc");
}
}
}
//注意,因为静态版用的是数组,所以保留了很多[]的访问方式,但是对于动态版而言也是适用的,因为动态版使用的是指针,下标就相当于地址偏移量
//所以[]的方式本质就是加上偏移量并解引用
void ADDinfo(contact* pc) {
assert(pc);
check_capacity(pc);//容量检查函数,满了就增容
printf("请输入联系人姓名>>\n");
scanf("%s", pc->date[pc->sz].name);
printf("请输入联系人年龄>>\n");
scanf("%d", &(pc->date[pc->sz].age));//这里要注意,因为其他属性都是用数组存储的,所以数组名就是地址,而age是需要取地址才行的
printf("请输入联系人性别>>\n");
scanf("%s", pc->date[pc->sz].sex);
printf("请输入联系人电话>>\n");
scanf("%s", pc->date[pc->sz].phone);
printf("请输入联系人地址>>\n");
scanf("%s", pc->date[pc->sz].address);
pc->sz++;
printf("添加成功!\n");
return;
}
//查找函数
int ser_byname(const contact* pc,char* sername) {
assert(pc);
for (int i = 0;i < pc->sz;i++) {
if (0 == strcmp(pc->date[i].name, sername)) {
return i;//找到了就返回下标
}
}
return -1;//最终找不到就返回0
}
//删除函数
void DELinfo(contact* pc) {
assert(pc);
if (pc->sz == 0) {
printf("通信录已经为空,无法删除!");
return;
}
//1,先找到
char delname[name_max] = { 0 };
printf("请输入你要删除的联系人的名字>>\n");
scanf("%s", &delname);
int ret = ser_byname(pc,delname);
//2,删除
for (int j = ret;j < (pc->sz - 1);j++) {
pc->date[j] = pc->date[j + 1];
}
pc->sz--;
printf("删除成功!\n");
}
//打印函数
void PRINTinfo(const contact* pc) {
assert(pc);
printf("%-10s %-10s %-10s %-12s %-20s\n", "姓名", "年龄", "性别", "电话", "地址");
for (int i = 0;i < pc->sz;i++) {
printf("%-10s %-10d %-10s %-12s %-20s\n", pc->date[i].name, pc->date[i].age, pc->date[i].sex, pc->date[i].phone, pc->date[i].address);
}
}
//查找函数
void SERinfo(const contact* pc) {
assert(pc);
//1,先找到
char sername[name_max] = { 0 };
printf("请输入你要查找的联系人的名字>>\n");
scanf("%s", &sername);
int ret = ser_byname(pc, sername);//利用之前的查找函数拿到下标
//2,输出
if (-1 == ret) {
printf("对不起,查无此人!\n");
return;
}
else {
printf("已找到如下相关信息>>\n");
printf("%-10s %-10s %-10s %-12s %-20s\n", "姓名", "年龄", "性别", "电话", "地址");
printf("%-10s %-10d %-10s %-12s %-20s\n", pc->date[ret].name, pc->date[ret].age, pc->date[ret].sex, pc->date[ret].phone, pc->date[ret].address);
}
return;
}
//修改函数
void MODinfo(contact* pc) {
assert(pc);
//1,先找到
char modname[name_max] = { 0 };
printf("请输入你要修改的联系人的人的名字>>\n");
scanf("%s", &modname);
int ret = ser_byname(pc, modname);//利用之前的查找函数拿到下标
//2,修改
if (-1 == ret) {
printf("对不起,无法修改,此通信录下无此人!\n");
return;
}
else {
printf("请输入修改后的联系人姓名>>\n");
scanf("%s", pc->date[ret].name);
printf("请输入修改后的联系人年龄>>\n");
scanf("%d", &(pc->date[ret].age));
printf("请输入修改后的联系人性别>>\n");
scanf("%s", pc->date[ret].sex);
printf("请输入修改后的联系人电话>>\n");
scanf("%s", pc->date[ret].phone);
printf("请输入修改后的联系人地址>>\n");
scanf("%s", pc->date[ret].address);
printf("修改成功!\n");
}
return;
}
//自定义比较函数
int comparestu(const void* e1,const void* e2) {
return ((struct perinfo*)e1)->age - ((struct perinfo*)e2)->age;//以年龄作为比较对象
}
//排序函数
void SORTinfo(contact* pc) {
//这里的排序我们可以用qsort来实现
qsort(pc->date, pc->sz, sizeof(pc->date[0]), comparestu);
printf("排序完毕,可使用打印查看!\n");
}
5.3,test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "contact.h"
void menu() {
printf("**** 通信录系统 ****\n");
printf("****1,ADD 2,DEL ****\n");
printf("****3,SER 4,MOD ****\n");
printf("****5,SORT 6,PRINT****\n");
printf("****0,EXIT ****\n");
printf("************************\n");
}
void test() {
int input = 0;
contact con;//创建通信录对象
init(&con);//初始化这个通信录
do {
menu();
printf("请进行选择>>");
scanf("%d", &input);
switch (input) {
case ADD:
ADDinfo(&con);
Sleep(3000);
system("cls");
break;
case DEL:
//要删除,得先找到你想删除的人,所以删除函数里面得有一个查找函数
DELinfo(&con);
Sleep(3000);//控制3秒会清屏一次,让屏幕输出看起来不冗杂
system("cls");
break;
case SER:
SERinfo(&con);
Sleep(3000);
system("cls");
break;
case MOD:
MODinfo(&con);
Sleep(3000);
system("cls");
break;
case SORT:
SORTinfo(&con);//这里排序我们就按照年龄大小来排序
Sleep(3000);
system("cls");
break;
case PRINT:
PRINTinfo(&con);
Sleep(3000);
system("cls");
break;
case EXIT:
DELcontact(&con);//退出就销毁通信录,也就是销毁申请的空间
printf("退出通信录!\n");
break;
default:
printf("输入有误,请重新输入!\n");
break;
}
} while (input);
}
int main() {
test();
return 0;
}
好了,今天的分享就这么多了,如果大家觉得博主还写的不错的话,还请帮忙点点咱咯,十分感谢呢!