动态内存管理+动态通讯录【C进阶】

文章目录

  • 为什么存在动态内存分配❓
  • 动态内存函数
    • malloc&free
    • calloc
    • realloc
  • ❌常见的动态内存错误❌
  • 练习题
  • C/C++程序的内存开辟
  • 柔性数组
    • 柔性数组的特点
    • 柔性数组的优势
  • :star:动态通讯录:star:
    • 初始化
    • 添加
    • 销毁

为什么存在动态内存分配❓

静态内存分配
当你声明数组时,你必须用一个编译时常量指定数组的长度(c99前)。但是,数组的长度常常在运行时才知道,这是由于它所需要的内存空间取决于输入数据。例如,一个用于计算学生等级和平均分的程序可能需要存储一个班级所有学生的数据,但不同班级的学生数量可能不同。在这些情况下,我们通常采取的方法是声明一个较大的数组,它可以容纳可能出现的最多元素。

这样声明的数组有3个缺陷

  • 这种声明引入了认为的限制,如果使用的长度超过了声明的长度,计算机无法处理这种情况
  • 如果程序使用的元素较少时,这样会导致多余的空间被浪费
  • 如果输入的数据超过了数组的容纳范围,那么程序不得不做出一种合理的相应
int main()
{
	int val = 20;//在栈上开辟4个字节
	int arr[10] = { 0 };//在栈上开辟40个字节的连续空间
	return 0;
}

上述开辟空间的方式有2个特点

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

上述开辟空间的方式称为静态内存开辟

动态内存分配
在计算机科学中, 动态内存分配(Dynamic memory allocation)又称为堆内存分配,是指计算机程序在运行期中分配使用内存。它可以当成是一种分配有限内存资源所有权的方法。
动态分配的内存在被程序员明确释放或被垃圾回收之前一直有效。与静态内存分配的区别在于没有一个固定的生存期。这样被分配的对象称之为有一个“动态生存期”。

动态内存函数

以下动态内存函数均包含头文件stdlib.h
动态内存函数开辟的空间都在堆上

malloc&free

动态内存管理+动态通讯录【C进阶】_第1张图片
malloc的参数是开辟空间的字节数

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

动态内存管理+动态通讯录【C进阶】_第2张图片

  • free函数用来释放通过malloc,calloc,realloc动态开辟的空间
  • 如果指针ptr指向的空间不是上述函数动态开辟出来的,那么行为是未定义的
  • 如果ptr是NULL指针,那么调用free函数什么也不会做
int main()
{
    int i, n;
    char* buffer;

    printf("How long do you want the string? ");
    scanf("%d", &i);

    buffer = (char*)malloc(i + 1);//需要留一个空间给结束符
    if (buffer == NULL) exit(1);  //分配空间失败

    for (n = 0; n < i; n++) buffer[n] = rand() % 26 + 'a';
	buffer[i] = '\0';

	printf("Random string: %s\n", buffer);
	free(buffer);//释放动态开辟的内存

	return 0;
}

calloc

动态内存管理+动态通讯录【C进阶】_第3张图片
calloc函数的第一个参数是要开辟的元素个数,第二个参数是每一个元素的字节数

  • calloc动态开辟一段内存,并且将申请的字节中每一个比特位初始化为0
  • 开辟成功返回开辟的这段空间起始地址
  • 开辟失败返回NULL指针
  • 如果第二个参数为0,返回的结果# 取决于特定的库(不一定返回NULL指针),这个返回的指针不能够被解引用
int main()
{
	int* pa = (int*)calloc(5, sizeof(int)); //申请5个连续存储int的空间并初始化为0
	if (pa!=NULL)
 for (int i = 0; i < 5; i++) printf("%d ", pa[i]);
	free(pa);//释放动态开辟的内存
	return 0;
}

realloc

动态内存管理+动态通讯录【C进阶】_第4张图片
realloc函数让动态内存管理更灵活,我们可以通过realloc对动态开辟的内存进行更改(扩大/缩小)
realloc函数的第一个参数是待扩容内存起始地址,第二个参数是扩容后的字节数

  • 如果扩容成功,返回扩容内存后的首地址(可能与之前的地址不一样)
  • 如果扩容失败,返回NULL指针
  • 如果第一个参数为NULL指针,则realloc行为和malloc行文一样
  • 如果size参数为0,
    对于c90/c++98,ptr指向的空间被free并且返回NULL指针
    对于c99/c++11,返回值取决于特定的库实现:它可能是空指针或其他不应被取消引用的位置。
  1. 当原空间后面有连续足够大的空间时,realloc返回原空间起始地址
  2. 当原空间后面没有足够大的空间时

动态内存管理+动态通讯录【C进阶】_第5张图片
对于情况一:直接返回已知空间的起始地址

对于情况二:

  1. realloc寻找可以存放下的空间
  2. 将原来的在已知空间中的数据拷贝到可用空间中
  3. free已知空间
  4. 返回目标空间的起始地址
int main()
{
	int input, n;
	int count = 0;
	int* numbers = NULL;
	int* more_numbers = NULL;

	do {
		printf("Enter an integer value (0 to end): ");
		scanf("%d", &input);
		count++;

		more_numbers = (int*)realloc(numbers, count * sizeof(int));//没输入一个数申请一个多空间

		if (more_numbers != NULL) 
		{
			numbers = more_numbers;
			numbers[count - 1] = input;
		}
		else perror("realloc失败->\n");
	} while (input != 0);

	printf("Numbers entered: ");
	for (n = 0; n < count; n++) printf("%d ", numbers[n]);
	free(numbers);

	return 0;
}

从这个例子中可以很好的体现动态开辟的优势,即使我事先不知道用户要输入多少个数(甚至用户也不知道),我也可以将用户输入的数字记录下来

注意:调用realloc时最后用临时变量接受返回值,不然返回值如果为NULL时原来已知空间中的地址找不到了,因为原本记录那个地址的变量现在变成了NULL

❌常见的动态内存错误❌

对NULL指针解引用

//1.对空指针解引用
int main()
{
	int* p = (int*)malloc(INT_MAX);
	*p = 20;
	free(p);
	return 0;
}

动态内存管理+动态通讯录【C进阶】_第6张图片

malloc开辟失败返回空指针,没有检查是否开辟成功就直接解引用,有可能解引用空指针,程序崩溃

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

//2.对动态开辟的内存进行越界访问
int main()
{
	int* p = (int*)malloc(sizeof(int) * 5);
	assert(p);
	for (int i = 0; i <= 5; i++)
	{
		p[i] = i + 1;
	}	
	return 0;

p指向的是5个int的空间,但是当i等于5时,程序访问了未定义的位置,这样会越界访问,越界访问有可能会时程序崩溃,有可能不会(不代表问题不严重)

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

int main()
{
	int	a = 10;
	int* p = a;
	free(p);
	return 0;
}

动态内存管理+动态通讯录【C进阶】_第7张图片

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

//4.使用free释放动态开辟内存的一部分
int main()
{
	int* p = (int*)malloc(sizeof(int) * 5);
	free(++p);
	return 0;
}

对用一块内存多次释放

//5.对同一块内存多次释放
int main()
{
	int* p = (int*)malloc(sizeof(int) * 5);
	free(p);//释放一次后p指向的空间不在是动态开辟的了
	free(p);//此时释放非动态开辟的内存报错
	return 0;
}

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

void test()
{
	int* p = (int*)malloc(100 * sizeof(int));
}
int main()
{
	while (1)
	{
		test();
	}
	return 0;
}

动态内存管理+动态通讯录【C进阶】_第8张图片
当内存大量泄漏时,程序会吃掉大部分内存,这样操作系统没有过多的内存分配给其他应用,因此内存泄漏可能会导致操作系统崩溃

切记:动态开辟的内存一定要释放,并且保证正确释放
为了避免这种情况,我们应该保证自己申请的内存自己释放,自己不释放的应当写文档告诉别人来释放

练习题

//1.下面程序会打印什么?
void GetMemory(char* p)
{
	p = (char*)malloc(100);
}

int main()
{
	char* str = NULL;
	GetMemory(str);
	strcpy(str, "hello world");
	printf(str);
}

动态内存管理+动态通讯录【C进阶】_第9张图片
可以通过函数返回开辟的地址和传二级指针修正程序

//修正1
char* GetMemory()
{
	char* tmp = (char*)malloc(100);
	return tmp == NULL ? NULL : tmp;
}

int main()
{
	char* str = GetMemory();
	strcpy(str, "Hello world");
	printf(str);
	free(str);//释放动态分配的内存
	str = NULL;
	return 0;
}
//修正2
void GetMemory(char** pp)
{
	*pp = (char*)malloc(100);
}

int main()
{
	char* str = NULL;
	GetMemory(&str);
	strcpy(str, "Hello world");
	printf(str);
	free(str);
	str = NULL;
	return 0;
}
//2.输出结果是什么?
char* GetMemory(void)
{
	char p[] = "hello world";
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}

动态内存管理+动态通讯录【C进阶】_第10张图片

//3.输出什么
void GetMemory(char** p, int num)
{
	*p = (char*)malloc(num);
}
int main()
{
	char* str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
	return 0;
}

动态内存管理+动态通讯录【C进阶】_第11张图片

//4.输出什么?
int main()
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}

动态内存管理+动态通讯录【C进阶】_第12张图片

C/C++程序的内存开辟

动态内存管理+动态通讯录【C进阶】_第13张图片

看见这个图,相比就会大致了解C/C++的内存区域规划了吧hhh~

柔性数组

想必不少人一开始听到这个词比较陌生,什么叫做柔性数组?变长数组我知道,数组的长度可以是变量嘛,柔性数组是什么鬼嘛‍
柔性数组和变长数组都是在C99中加上的
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。

柔性数组的特点

1.柔性数组不占结构体的大小
动态内存管理+动态通讯录【C进阶】_第14张图片
2.柔性数组只能是结构体中最后一个成员并且结构体除柔性数组外至少有一个其他成员

//定义柔性数组的两种错误写法
struct flexible_array
{
	int arr[];
};

struct flexible_array
{
	int arr[];
	int i;
};

3.包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小

//malloc分配结构的大小,分配的空间必须大于结构的大小
typedef struct flexible_array
{
	int i;
	int arr[];
}flexible_array;
int main()
{
	//给柔性数组分配10个int大小
	flexible_array* pa =
	(flexible_array*)malloc(sizeof(flexible_array) + sizeof(int) * 10);
	assert(pa);
	for (size_t i = 0; i < 10; i++)  pa->arr[i] = i;
	for (size_t i = 0; i < 10; i++) printf("%d ", pa->arr[i]);
	free(pa);
	return 0;
}

更改数组的大小

int main()
{
	flexible_array* pa =
	(flexible_array*)malloc(sizeof(flexible_array) + sizeof(int) * 10);//给柔性数组分配10个int大小
	assert(pa);
	for (size_t i = 0; i < 10; i++)  pa->arr[i] = i;
	for (size_t i = 0; i < 10; i++) printf("%d ", pa->arr[i]);
	puts("");
	//数组扩容成20个int大小
	flexible_array* tmp = (flexible_array*)realloc
	(pa, sizeof(flexible_array) + sizeof(int) * 20);
	assert(tmp);
	pa = tmp;
	for (size_t i = 10; i < 20; i++) pa->arr[i] = i;
	for (size_t i = 0; i < 20; i++) printf("%d ", pa->arr[i]);
	puts("");
	free(pa);
	return 0;
}

柔性数组的优势

上述代码可以写成

typedef struct flexible_array
{
	int i;
	int* arr;
}flexible_array;
int main()
{
	
	flexible_array* pa = (flexible_array*)malloc
	(sizeof(flexible_array)); 
	pa->arr = (int*)malloc(sizeof(int) * 10);
	for (size_t i = 0; i < 10; i++) pa->arr[i] = i;
	for (size_t i = 0; i < 10; i++) printf("%d ", pa->arr[i]);
	puts("");
	//
	flexible_array* tmp = (flexible_array*)realloc
	(pa->arr, sizeof(int) * 20);
	assert(tmp);
	pa->arr = tmp;
	for (size_t i = 10; i < 20; i++) pa->arr[i] = i;
	for (size_t i = 0; i < 20; i++) printf("%d ", pa->arr[i]);
	puts("");
	//先释放arr
	free(pa->arr);
	free(pa);
	return 0;
}

对比上述两种写法
动态内存管理+动态通讯录【C进阶】_第15张图片

  • 第一个好处是:方便内存释放
    如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给
    用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
  • 第二个好处:这样有利于访问速度.
    连续的内存有益于提高访问速度(计算机在访问当前地址时会默认把当前地址相邻的数据加载到寄存器中),也有益于减少内存碎片(使用柔性数组只调用一次malloc,而指针写法调用了两次malloc)

⭐️动态通讯录⭐️

在了解动态内存函数之后,我们可以将之前的静态通讯录改进为动态通讯录,不会静态通讯录看这里静态通讯录
首先明确一点:静态通讯录是通过一个大小固定的数组来存放联系人的信息,而数组的大小每次都是固定的,所以通讯录的长度不能更改,动态通讯录就是通讯录的容量可以更改,这样的好处有:1. 存多少人的信息就开辟多少空间,不够了随时可以扩容,2. 动态开辟的空间是在堆上的,堆上的空间比栈上的大,所以存储的联系人也比静态通讯录存储的多
将数组换成指针变量即可实现动态通讯录并且需要增加变量capacity来存储当前通讯录的最大容量

动态通讯录只有在初始化、增加联系人、销毁时和静态通讯录不一样

初始化

//动态通讯录
typedef struct Contact
{
	PeoInfo* data;//data是一个指针,指向动态开辟的内存
	int sz;		 //当前联系人个数
	int capacity;//当前最大容量
}Contact;

//动态通讯录初始化
void InitContact(Contact* con)
{
	con->data = NULL;
	con->capacity = FITST_NUM;//一开始的最大容量
	con->sz = 0;
}

添加

添加联系人之前需要检查一下当前最大容量是否满了,如果满了需要扩容

//检查容量是否满了
void CheckContact(Contact* con)
{
	if (con->capacity == con->sz || con->sz == 0)
	{
		PeoInfo* pc =
			(PeoInfo*)realloc(con->data, MAGNIFICATION * sizeof(con->capacity));
		if (NULL == pc)
		{
			perror("扩容失败\n");
			return;
		}
		con->capacity *= MAGNIFICATION;//容量扩成当前最大容量的两倍
		con->data = pc;
		printf("扩容成功!当前容量最大容量%d\n", con->capacity);
	}
	return;
}

//动态通讯录添加 
void AddContact(Contact* con)
{
	CheckContact(con);
	printf("请输入你需要添加人的姓名:");
	scanf("%s", con->data[con->sz].name);
	printf("请输出你需要添加人的年龄:");
	scanf("%d", &(con->data[con->sz].age));
	printf("请输入你需要添加人的性别:");
	scanf("%s", con->data[con->sz].sex);
	printf("请输入你需要添加人的电话:");
	scanf("%s", con->data[con->sz].tele);
	printf("请输入你需要添加人的地址:");
	scanf("%s", con->data[con->sz].address);
	con->sz++;
	printf("添加成功!\n");
}

销毁

//动态通讯录销毁
void DestroyContact(Contact* con)
{
	printf("你确定要清空通讯录吗?(YES/NO)\n");
	char selection[MAX] = { 0 };
	scanf("%s", selection);
	fflush(stdin);
	if (strcmp(selection, "YES") != 0) return;
	free(con->data);
	printf("清空成功!\n");
}


你可能感兴趣的:(c语言,数据结构,算法)