C语言关于动态内存管理的经典笔试题、柔性数组

一、动态内存管理的经典笔试题分析

题目1:

#include<stdio.h>
#include<stdlib.h>
void GetMemory(char* p)
{
	p = (char*)malloc(100);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(str);
	strcpy(str, "hello world");
	printf(str);
}
int main()
{
	Test();
	return 0;
}

请问上面调用Test函数的运行结果是什么?

🍍第一个是程序会崩溃,为什么会崩溃呢?先分析一下上面的代码。首先在Test函数里面创建了一个char*类型的指针变量str。其次又调用了GetMemory这个函数,而且是将str作为参数传给了GetMemory,这个函数里面动态的申请了100个字节的空间,然后就调用完GetMemory这个函数,回到Test函数里;调用strcpy函数,想将"hello world"这个字符串拷贝放到str所指向的内存空间里面。然后再通过printf函数打印str所指向空间里面的字符串,到这里Test函数就调用完毕。

其次这个代码还存在的问题是对于malloc动态申请的内存没有释放。我们知道对于函数的传参分为两种:传值和传地址。上面给函数GetMemory传递的是指针str,是值传参;而对于值传参来说,形参是实参的一份临时拷贝。所以对形参的修改不会影响到实参。当调用GetMemory动态的申请100个字节的空间以后,出了这个函数,局部指针变量p就被销毁了,但是对于malloc动态申请的空间没有被释放,那就一直占用着内存。这就是上面代码的其中一个问题,又因为这里是值传递,所以str的值并没有被修改,str的值还是空指针(NULL),那调用strcpy函数想将字符串"hello world"拷贝放到str所指向的空间里,就不可以了!所以这里对NULL指针的解引用就导致了程序崩溃。

🍳上面的代码无非就是想动态的申请一块空间来存放一个字符串,那要怎么修改上面的代码,让这个程序正确的运行的呢?其实我们就是想修改指针变量str的值嘛!让str成功的指向动态申请的空间的起始地址而已,那就是想形参的修改能影响到实参,那就采用传地址的形式,就可以啦。对一级指针str取地址,那就要用一个二级指针来存放这个一级指针的地址。我们就要对GetMemory函数的定义进行修改:

#include<stdio.h>
#include<stdlib.h>
void GetMemory(char** p)
{
	*p = (char*)malloc(100);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str);
	strcpy(str, "hello world");
	printf(str);
	free(str);
	str = NULL;
}
int main()
{
	Test();
	return 0;
}

程序运行的结果:
在这里插入图片描述由上图可知,程序成功运行,而且我们也不能忘了对动态申请的空间进行释放!

题目2:

#include<stdio.h>
char* GetMemory(void)
{
	char p[] = "hello world";
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}
int main()
{
	Test();
	return 0;
}

程序运行结果:
在这里插入图片描述首先分析一下,为什么打印的结果不是hello world?这里首先调用了Test函数,在函数里创建了一个char*类型的指针变量str并赋值为NULL;其次又在函数里调用了GetMemory函数,在GetMemory函数里创建了一个字符数组p,数组中存放的是"hello world"这个字符串。并将这个数组的首元素地址作为返回值赋给了指针str,最后打印指针str所指向的空间的内容。

🍔这段代码的错误在于在调用完GetMemory函数后,在这个函数里创建的数组就会被销毁,空间会还给操作系统,但是返回了这块空间的地址,那用str接收了这块被销毁空间的地址,再使用指针str去访问这块被销毁的空间,就会出现野指针,非法访问空间。这就是上面代码为什么打印出来乱码的原因。这种错误属于返回栈空间地址的问题。所以返回局部变量的地址,是不行的,很能会造成非法访问。

题目3:

#include<stdio.h>
void GetMemory(char** p, int num)
{
	*p = (char*)malloc(num);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
}
int main()
{
	Test();
	return 0;
}

上面这段代码跟题目1的改正很相似,乍一看好像这段代码没什么问题。但是是有问题的,这段代码唯一的问题就在于没有对动态申请的空间进行释放。这也是常见的动态内存管理中的错误。对动态申请的空间一定要记得释放:
在这里插入图片描述

题目4:

#include<stdio.h>
void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}
int main()
{
	Test();
	return 0;
}

上面这段代码又存在什么问题呢?首先调用函数Test,在函数里动态的开辟了100个字节的空间,并将这块空间的起始地址赋给了char*类型的指针变量str,紧接着调用strcpy函数,将字符串"hello"拷贝放进这块空间里,然后释放了这块动态开辟的空间。紧接着判断str是不是空指针,不是,就再一次调用strcpy函数,将字符串"world"拷贝放进指针str所指向的空间里,然后打印str所指向空间里的内容。

上面这段代码存在的问题在于在动态的开辟了100个字节的空间后,就直接使用指针str了,没有判断malloc是否开辟空间成功,其次是,在释放了这块动态开辟的空间后,这块空间就还给了操作系统,但是str还指向着这块空间,也就是str并不是空指针,紧接着又用这个str去访问这块被释放的空间,就已经造成非法访问空间了。所以在上面的代码中,第一次调用strcpy函数,将"hello"这个字符串存进动态开辟的空间里了,在释放了这块空间后,又调用strcpy函数,又将"world"这个字符串非法的存进了这块已释放的空间中,也即"world"将"hello"覆盖了,所以打印出来的结果就会是world:
在这里插入图片描述改正这段代码,就是要在释放动态开辟空间后,将指向这块空间的指针也置为空,以免后面又使用这个指针非法访问空间。str置为空以后,那后面的if语句也就没有了作用。

二、柔性数组

1.什么是柔性数组?

🍉1. 柔性数组(flexible array)也称为变长数组,是一种动态数组的实现方式。
🍊2.与普通数组不同的是,柔性数组在定义时不需要明确指定数组大小,在程序运行时可以动态地分配和扩展数组大小。[引用]
🍋3.在c99版本中,结构体中的最后一个成员允许是未知大小的数组,这个数组就叫做『柔性数组』成员。

例如:

struct tag_name1
{
	char c;
	int arr[]; //柔性数组
};
struct tag_name2
{
	int i;
	char c;
	short arr[0]; //柔性数组
};

上面两个结构体中的最后一个成员都是柔性数组,第二个结构体的最后一个数组成员的[ ]中是0,意思是他没有指定元素个数,也就没有指定数组大小,也是柔性数组。上面两种形式的柔性数组可能在不同的编译器上会支持不同的形式。

2.柔性数组的特点

结构体中的柔性数组成员前面必须至少有一个其他成员。
sizeof返回的这种结构体大小不会包括柔性数组的内存。
包含柔性数组成员的结构体用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小

对于柔性数组在结构体中,必须是最后一个成员,而且在柔性数组的前面最少还要有一个结构体成员,这是柔性数组的必须条件。其次由于柔性数组没有指定大小,所以在计算含有柔性数组的结构体大小时,是不会包含柔性数组成员的:

#include<stdio.h>
struct tag_name
{
	int i;
	int arr[]; //柔性数组
};
int main()
{
	printf("%zd\n", sizeof(struct tag_name));
	return 0;
}

程序运行结果:
在这里插入图片描述

3.柔性数组的使用

对于柔性数组特点的最后一特点,也是最重要的一个特点,包含柔性数组的结构体要用malloc函数来进行动态内存分配,并且分配的空间大小应该要大于结构体的大小,也就是通过动态内存分配多出来的空间就是给这个柔性数组的。看下面一段代码:

#include<stdio.h>
#include<stdlib.h>
struct S
{
	int i;
	int arr[];//柔性数组
};
int main()
{
	struct S* p = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	p->i = 10;
	int j = 0;
	for (j = 0; j < 5; j++)
	{
		p->arr[j] = j;
	}
	//打印结构中的内容
	printf("%d ", p->i);
	for (j = 0; j < 5; j++)
	{
		printf("%d ", p->arr[j]);
	}
	free(p);
	p = NULL;
	return 0;
}

程序运行结果:
在这里插入图片描述上面使用malloc来为结构体动态的开辟空间:
在这里插入图片描述由上图,蓝色框里计算的就是结构体的大小,它并不包含柔性数组,而后面的绿色框里的大小就是为柔性数组开辟的空间,两部分加起来才是我们需要开辟的总空间。我们还学过一个动态内存管理的函数叫realloc,这个函数可以对已经动态开辟的空间大小进行调整,以适应我们的需要,这样对于柔性数组的大小就可以更改,柔性的含义也就体现在这里。所以为什么要把柔性数组放在结构体的最后一个成员处,就为了放便对这个数组的大小进行调整,而不影响到前面成员在结构体中的存储。
在这里插入图片描述由上图,如果我们要调整刚刚动态开辟的空间,就用realloc函数调整,蓝色框里还是结构体的大小,橙色框里就是更改以后的柔性数组的新大小。两部分加起来才是调整以后的总大小。当然,只要是动态开辟的空间,就不要忘记对其返回值进行检查,以免动态开辟空间失败,最后也是不要忘记用完这块空间后,要用free释放回收。

🍏🍏🍏当然,还有一种形式,也可以达到跟上面柔性数组一样的效果。就是我们在设计结构体时,最后一个成员我们不放柔性数组,我们改成指针变量:
在这里插入图片描述我们可以先为这个结构体动态的开辟一块空间,然后再动态的开辟一块空间,将这个空间的起始地址赋给结构体的最后一个成员,也就是赋值给arr这个指针变量。后面如果要更改空间大小,那就直接改arr指针所指向的空间大小即可。这样也能达到柔性的特点:

#include<stdio.h>
#include<stdlib.h>
struct S
{
	int n;
	int* arr;
};
int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S));
	if (ps == NULL)
	{
		return 1;
	}
	ps->arr = (int*)malloc(5 * sizeof(int));
	if (ps->arr == NULL)
	{
		return 1;
	}
	ps->n = 10;
	int i = 0;
	//使用空间
	for (i = 0; i < 5; i++)
	{
		ps->arr[i] = i;
	}
	//调整空间大小
	int* ptr = (int*)realloc(ps->arr, 10 * sizeof(int));
	if (ptr != NULL)
	{
		ps->arr = ptr;
	}
	//使用空间……
	//释放空间
	free(ps->arr);
	ps->arr = NULL;
	free(ps);
	ps = NULL;
	return 0;
}

在这里插入图片描述🧀🧀🧀这种形式的结构体也能达到柔性的特点,让结构体的最后一个成员是指针变量,让这个指针也指向一块动态开辟的空间,到后面通过更改这个指针所指向的空间大小即可。当然要记得检查指针是否为空,后面使用完空间,也要记得释放空间。这里释放空间是要先释放arr所指向的空间,再释放ps所指向的空间,顺序不能乱。因为ps指针指向的结构体中包含了arr指针,如果先释放ps指向的空间,那这个结构体的空间就销毁了,包括存放的指针变量arr,那后面就找不到arr这个指针了,更找不到arr所指向的空间。所以顺序不能乱。

上面两种方式:一种是结构体中包含柔性数组,另一种是结构体中包含指针。那这两种方式那种更好呢?答案是第一种。因为第二种方式,动态的申请了两次空间,在后面就可能会忘记要进行两次释放内存。

🍓第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉
🍑第二个好处是:这样有利于访问速度
连续的内存有益于提高访问速度,也有益于减少内存碎片

对于上面说的内存碎片有两种类型:外部碎片和内部碎片。
🏈① 外部碎片:是指由于动态内存分配和释放过程中,导致剩余的未分配内存块被零散占据,无法满足大块内存的需求。虽然总的空闲内存足够,但无法分配连续的内存空间。
🏉② 内部碎片:是指已经分配给进程的内存块中,存在着未被充分利用的空间。例如,当为一个固定大小的数据结构分配内存,但实际使用的空间小于分配的大小时,就会产生内部碎片。[引用]
在这里插入图片描述

🍇🍇🍇结束啦!!!
在这里插入图片描述

  • 21
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

米饭「」

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值