C语言--动态内存管理(随用随开辟,可真香)

 哦吼!今天的干货来了哦家人们,冲冲冲,有错误还请大家帮忙斧正一下哈哈。好了,安全带系好,准备发车了!

目录

一,为什么有动态内存管理

二,动态内存函数

2.1 malloc和free

2.2 calloc

2.3,realloc

 三,动态内存常见错误

3.1 对NULL指针的解引用操作

3.2 对动态开辟空间的越界访问

3.3 对非动态开辟内存使用free释放 

3.4 使用free释放一块动态开辟内存的一部分

3.5 对同一块动态内存多次释放

3.6 动态开辟内存忘记释放(内存泄漏)

4. 经典笔试题

4.1 题目1:

4.2 题目2: 

4.3 题目3:

4.4 题目4:

5. 通信录升级(动态增长版)

5.1,contact.h

5.2,contact.c

5.3,test.c


一,为什么有动态内存管理

通常情况下我们是如下开辟空间的:

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;
}

好了,今天的分享就这么多了,如果大家觉得博主还写的不错的话,还请帮忙点点咱咯,十分感谢呢!

猜你喜欢

转载自blog.csdn.net/qq_61688804/article/details/123859310