C语言-动态内存管理+通讯录

目录

1. 程序内存

1.1 程序运行时的内存

1.2 内存开辟的方式 

2. 动态内存函数

2.1 malloc

2.2 free 

2.3 calloc 

2.4 realloc 

3. 常见的动态内存错误

3.1 对NULL指针的解引用操作 

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

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

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

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

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

4. 通讯录


1. 程序内存

1.1 程序运行时的内存

在学习C语言时,我们经常需要为一个变量来分配一块内存空间,不同类型的变量所占用的内存空间也是不一样的,而每一个程序内部都有各种代码和数据需要存储在内存上。因此,了解内存空间的分配对我们理解程序的运行是极为有利的,下图介绍了Linux x86-64运行时的内存映像

C语言-动态内存管理+通讯录_第1张图片

对于一个C程序,操作系统将会针对不同的数据类型在不同的内存区域上开辟空间

  • 静态变量及全局变量

静态变量和全局变量被存储在数据段 ,在程序的整个运行周期都不销毁。其中,如果变量未初始化,则存放在.bss段,初始化的变量存放在.data段

#include 

int a = 10;             //全局变量(初始化)-.bss

int main()
{
	static int a;  //static修饰的静态变量(未初始化)-.data

	return 0;
}
  • 常量 

常量存放在代码段中的.rodata段,对于存放在这一块的常量是不可以被修改的,其状态为只读(read only data),但是注意并不是所有的常量都是放在常量数据段的,其特殊情况如下:

  1. 有些立即数与指令编译在一起直接放在代码段(text段)
  2. 对于字符串常量,编译器会去掉重复的常量,让程序的每个字符串常量只有一份
  3. 有些系统中rodata段是多个进程共享的,目的是为了提高空间的利用率
#include 

int main()
{
	printf("%s", "test");   //这里的字符串test就是一个常量字符串,存放在.rodata中
	return 0;
}
  • 代码 

可执行的代码和部分的只读常量存放在代码段的.text段,它与rodata段的主要不同是,text段是可以执行的,而且不被不同的进程共享

  •  临时变量和函数参数

在内存中有一个部分称为栈区(stack) ,用来保存临时变量以及函数参数,栈区变量的生存周期是在入栈前(函数开始被调用)获得内存空间,而在出栈时(函数结束调用)释放内存空间。值得注意的是,栈的增长是由高地址向地址值增长的,并由栈指针%rsp来维护栈顶的地址

int main()
{
	int a = 10;  //临时变量,存放在栈区

	test(a);     //函数参数,存放在栈区

 	return 0;
}
  • 由内存分配函数申请的空间 

在内存中有一个区域是专门服务于程序的,当程序需要动态地申请内存时,操作系统就会在这个部分开辟空间供程序使用——(heap)是最自由的一种内存,它完全由程序来负责内存的管理,包括什么时候申请,什么时候释放,而且对它的使用也没有什么大小的限制。在C/C++中,用malloc函数和new申请的内存都存在于heap段中

int main()
{
	int* p = (int*)malloc(sizeof(int) * 4);  //用malloc函数申请的4个int类型大小的空间-在堆区

	return 0;
}

1.2 内存开辟的方式 

在C语言中,一般有两种开辟内存的方式

1. 静态开辟内存

int main()
{
	int a = 10;
	float b = 5.0;
	char c[5] = { 0 };
	return 0;
}

类似于这种语句,由于具有给定的数据类型,因此能够直接在内存中分配相应大小的空间来存储数据。比如int a = 10,就在内存中的栈区开辟了4个字节用于存放数据10;char c[5] = {0},在栈区开辟了10个字节的空间。这些空间的开辟有两个特点:

  • 空间开辟大小是固定的
  • 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配

 但这种内存开辟是有一定局限性的,考虑下面一种情况

程序需要不断更新数据,例如使用一个能存放10个整型的数组,在后续需要存入更多的数据,这时无法再去修改源程序,而如果开辟数组时将其长度指定得过大,又会造成内存的浪费,因此我们需要根据实际情况,动态的增长内存空间

2. 动态开辟内存

使用内存分配函数能够实现动态地开辟空间,下面将会介绍动态内存函数的使用 


2. 动态内存函数

2.1 malloc

首先要介绍的第一个函数是malloc,函数定义为

void* malloc (size_t size);

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针 

malloc的参数是一个无符号的size,这个size由程序来指定,其意义是在堆区分配空间的大小,如果size为10,操作系统会为程序在堆区分配一个大小为10字节的空间,并将使用权交给程序

malloc的返回值是一个无类型的指针,它指向分配好的空间的起始地址,通常在使用时需要根据数据类型进行强制转化 

int main()
{
	int* p = (int*)malloc(sizeof(int) * 4);  

    if(NULL != p)
    {
       //使用

    }
	return 0;
}

这个代码说明

  1. 开辟了是4个int类型的大小——16个字节
  2. 需要将指针强制转化成int* 类型的指针再使用
  3. 在使用该内存空间之前,先要判断是否分配成功再使用

注意事项

  • 如果开辟成功,则返回一个指向开辟好空间的指针
  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查
  • 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定 
  • 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器

2.2 free 

C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数定义为

void free (void* ptr);

free函数是用来回收动态分配的内存空间的 ,参数ptr是指向动态分配空间首地址的指针

一般来说,在程序结束后,操作系统会自动释放在之前为程序动态开辟的内存空间,但如果程序出错或,会导致操作系统无法将内存空间收回,造成内存泄漏 ,因此,当内存分配的空间不再使用时,程序员使用free函数主动释放动态分配的空间是一种很好的编码习惯

int main()
{
	int* p = (int*)malloc(sizeof(int) * 4);

	free(p);   //释放掉动态申请的空间p
    
    p = NULL;

	return 0;
}

注意事项 

  • 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的
  • 如果参数 ptr 是NULL指针,则函数什么事都不做 
  • 在free空间之后,将指针置空是很好的编码习惯,防止造成野指针的错误

2.3 calloc 

C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。函数定义为

void* calloc (size_t num, size_t size);
  • 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0
  • 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0

如果需要对开辟的空间进行初始化,通常使用calloc函数 

2.4 realloc 

realloc函数能够在malloc和colloc函数动态开辟的空间的基础上,接着开辟空间,函数定义为

void* realloc (void* ptr, size_t size);

参数ptr是之前就动态开辟好的内存空间的起始地址,参数size是接着要开辟空间的大小。返回值是改变后的空间的起始地址,下面会详细讲述

考虑下面的情况

  • 使用malloc分配100个字节的空间,空间过大产生浪费,可以使用realloc将空间调整到10个字节,并将原地址返回
  • 使用malloc分配100个字节的空间,空间过小不够存放数据,可以使用realloc将空间调整到200个字节,这个调整基于原地址空间后面有100个字节,那么将原地址返回
  • 同样是上面的情况,原地址空间后面连续的空间不足100个字节,realloc函数重新在内存中找到一块大小足够的新空间并开辟,将原空间释放,返回新的内存地址
int main()
{
	int* p = (int*)calloc(4, sizeof(int));

	if (p != NULL)
	{
		p = (int*)realloc(p, sizeof(int) * 5);
	}

	return 0;
}

上述代码先动态分配16个字节的空间,之后使用realloc在p后面分配20个字节的空间,因此这一块连续空间的大小为36个字节 


3. 常见的动态内存错误

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)
	{
		exit(EXIT_FAILURE);
	}
	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);    //报错
}

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

void test()
{
	int* p = (int*)malloc(100);
	p++;
	free(p);         //p不再指向动态内存的起始位置
}

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

void test()
{
	int* p = (int*)malloc(100);
	free(p);
	free(p);//重复释放
}

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

void test()
{
	int* p = (int*)malloc(100);
	if (NULL != p)
	{
		*p = 20;
	}
}

int main()
{
	test();
	while (1);
}


4. 通讯录

使用内存的动态分配,实现动态版本的通讯录,代码如下:

contact.h
#pragma once
#include 
#include 
#include 
#include 


//类型的声明

#define MAX 1000
#define NAME_MAX 20
#define SEX_MAX 5
#define TELE_MAX 12
#define ADDR_MAX 30
#define DEFAULT_SZ 3

//定义一个结构体, 包含联系人信息
typedef struct PeoInfo
{
	char name[NAME_MAX];
	char sex[SEX_MAX];
	int age;
	char tele[TELE_MAX];
	char addr[ADDR_MAX];
}PeoInfo;

//定义一个结构体, 作为通讯录(静态的数组版本)
//typedef struct Contact
//{
//	PeoInfo* data[MAX];  //可以存放1000个人的信息
//	int sz;             //记录通讯录中已经保存的信息个数
//}Contact;

//动态版本
typedef struct Contact
{
	PeoInfo* data;      //指向动态分配的空间
	int sz;             //记录通讯录中已经保存的信息个数
	int capacity;       //记录通讯录当前的最大容量 
}Contact;

//函数的声明

//初始化通讯录
void InitContact(Contact* pc);

//销毁通讯录
void DestoryContact(Contact* pc);

//增容的函数实现
void CheckCapacity(Contact* pc);

//增加联系人的信息
void AddContact(Contact* pc);

//打印通讯录中的信息
void PrintContact(const Contact* pc);

//删除指定联系人
void DelContact(Contact* pc);

//查找指定联系人
void SearchContact(const Contact* pc);

//修改指定联系人的信息
void ModifyContact(Contact* pc);

//排序联系人信息
void SortConcatc(Contact* pc);

//清空通讯录
void EmptyContact(Contact* pc);
contact.c
#include "contact.h"

//初始化通讯录的函数实现
void InitContact(Contact* pc)
{
	assert(pc);

	pc->sz = 0;
	pc->capacity = DEFAULT_SZ;
	pc->data = (PeoInfo*)malloc(pc->capacity * sizeof(PeoInfo));

	//如果malloc失败
	if (pc->data == NULL)
	{
		perror("InitContact::malloc");
		return;
	}
	
	memset(pc->data, 0, pc->capacity * sizeof(PeoInfo));
}

//销毁通讯录的函数实现
void DestoryContact(Contact* pc)
{
	free(pc->data);
	pc->data = NULL;
	pc->sz = 0;
	pc->capacity = 0;
	printf("销毁成功\n");
}


//增容的函数实现
void CheckCapacity(Contact* pc)
{
	if (pc->sz == pc->capacity)
	{
		PeoInfo* tmp = (PeoInfo*)realloc(pc->data, (pc->capacity + 2) * sizeof(PeoInfo)); //每次增容两个
		if (tmp != NULL)
		{
			pc->data = tmp;
		}
		else
		{
			perror("CheckCapacity::realloc");
		}
		pc->capacity += 2;
		printf("增容成功\n");
	}
}


//增加联系人的函数实现
void AddContact(Contact* pc)
{
	assert(pc);

	CheckCapacity(pc); //每次要添加联系人, 先检查容量是否已满

	//录入信息
	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 PrintContact(const Contact* pc)
{
	assert(pc);

	int i = 0;
	printf("%-20s %-5s %-5s %-12s %-30s\n", "姓名", "年龄", "性别", "电话", "地址");
	for (int i = 0;i < pc->sz;i++)
	{
		printf("%-20s %-5d %-5s %-12s %-30s\n", pc->data[i].name, pc->data[i].age, pc->data[i].sex, pc->data[i].tele, pc->data[i].addr);
	}
}

//通讯录中查找的函数实现
int FindByName(const Contact* pc, char name[])
{
	assert(pc);

	int i = 0;
	for (i = 0;i < pc->sz;i++)
	{
		if (0 == strcmp(pc->data[i].name, name))
		{
			return i;
		}
	}

	return -1;
}

//删除通讯录信息的函数实现
void DelContact(Contact* pc)
{
	assert(pc);

	//无有效信息
	if (pc->sz == 0)
	{
		printf("通讯录已空, 无法删除\n");
		return;
	}

	//删除联系人的函数实现
	char name[NAME_MAX] = { 0 };
	printf("请输入要删除人的名字:>");
	scanf("%s", name);

	int pos = FindByName(pc, name);

	//1.无效名字
	if (pos == -1)
	{
		printf("要删除的人不存在\n");
		return;
	}

	//2.删除
	int j = 0;
	for (j = pos;j < pc->sz - 1;j++)
	{
		pc->data[j] = pc->data[j + 1];
	}

	pc->sz--;
	printf("删除成功\n");
}

//查找联系人的函数实现
void SearchContact(const Contact* pc)
{
	assert(pc);

	char name[NAME_MAX] = { 0 };
	printf("请输入要查找人的名字:>");
	scanf("%s", name);

	int pos = FindByName(pc, name);
	if (pos == -1)
	{
		printf("要查找的人不存在\n");
		return;
	}

	//找到了, 打印pos位置上联系人的信息
	printf("%-20s %-5s %-5s %-12s %-30s\n", "姓名", "年龄", "性别", "电话", "地址");
	printf("%-20s %-5d %-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)
{
	assert(pc);

	char name[NAME_MAX] = { 0 };
	printf("请输入要修改信息人的名字:>");
	scanf("%s", name);

	int pos = FindByName(pc, name);
	if (pos == -1)
	{
		printf("要修改信息人不存在\n");
		return;
	}

	//修改pos位置上联系人的信息
	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");
}

//排序联系人信息的函数实现
void SortConcatc(Contact* pc)
{
	assert(pc);

	//按照姓氏的首字母排序

	for (int i = 0;i < pc->sz - 1;i++)
	{
		for (int j = 0;j < pc->sz - 1 - i;j++)
		{
			if (strcmp(pc->data[j].name, pc->data[j + 1].name) > 0)
			{
				PeoInfo tmp = pc->data[j];
				pc->data[j] = pc->data[j + 1];
				pc->data[j + 1] = tmp;
			}
		}
	}

	printf("排序完成\n");
}

//清空通讯录的函数实现
void EmptyContact(Contact* pc)
{
	assert(pc);

	pc->sz = 0;
	printf("清空成功\n");
}
test.c
#include "contact.h"

enum Option
{
	EXIT,
	ADD,
	DEL,
	SEARCH,
	MODIFY,
	SORT,
	PRINT,
	EMPTY
};

void menu()
{
	printf("*************************\n");
	printf("*** 1.add    2.del    ***\n");
	printf("*** 3.search 4.modify ***\n");
	printf("*** 5.sort   6.print  ***\n");
	printf("*** 7.empty  0.exit   ***\n");
	printf("*************************\n");
}

void test()
{
	int input = 0; //记录用户输入

	//创建通讯录
	Contact con;

	//初始化通讯录
	InitContact(&con);

	do
	{
		menu();
		printf("请选择:>");
		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 SORT:
			SortConcatc(&con);
			break;
		case PRINT:
			PrintContact(&con);
			break;
		case EMPTY:
			EmptyContact(&con);
			break;
		case EXIT:
			DestoryContact(&con);
			printf("退出通讯录\n");
			break;
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);
}

int main()
{
	test();
	return 0;
}

你可能感兴趣的:(C语言,c语言)