C语言之动态内存管理(动态内存分配+经典笔试题+柔性数组)[建议收藏]

Posted 呆呆兽学编程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言之动态内存管理(动态内存分配+经典笔试题+柔性数组)[建议收藏]相关的知识,希望对你有一定的参考价值。

本篇文章我要给大家梳理一下C语言中的动态内存管理相关知识。其中主要包括如何进行动态内存管理、一些常见的动态内存错误及柔性数组的介绍。
❤️ 博主码云gitee链接:https://gitee.com/byte-binxin ❤️


为什么存在动态内存分配

在此之前,我们基本都是在栈上开辟空间且开辟的空间大小也都是要明确指定的。
例如:

int val = 10;

这个变量的大小是在栈上开辟的,大小是4个字节。缺点大小是固定的

int arr[10] = {0};

这个数组也是在栈上开辟的,大小是40个字节。缺点是数组大小要明确指定。这样会导致空间不能够按需所取
显然,用数组开辟空间大小已经不能满足我们的需求了,这时候就产生了动态内存开辟这一说了。

动态内存管理

malloc和free

malloc原型如下:

void *malloc( size_t size );


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

  • 函数的参数是要申请空间的大小,单位是字节。
  • 如果申请空间成功,则返回空间的指针,否则返回NULL,所以要对函数返回值进行检查。
  • 函数的返回值得类型是void*,所以要根据使用者自己来决定要强转类型。
  • 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。

这个函数要结合free一起使用,动态内存申请的空间要由使用者自己来手动释放,所以C语言就专门提供了一个free函数来释放和回收动态内存空间,函数原型如下:

void free( void *memblock );

使用这个函数需要注意几点:

  • 当参数是NULL时,这个函数什么都不做
  • 参数中指针必须指向动态内存开辟的空间,如果参数 memblock 指向的空间不是动态开辟的,那free函数的行为是未定义的。

举一个malloc和free使用的实例:

#include <stdio.h>
#include <stdlib.h>

int main()
{
	int n = 0;
	scanf("%d", &n);
	int *ptr = (int*)malloc(sizeof(int)* n);//开辟4*n个字节大小的连续空间
	//检查空间是否申请成功
	if (ptr == NULL)
	{
		perror("malloc fail:");
		exit(-1);
	}

	int i = 0;
	for (i = 0; i < n; i++)
	{
		*(ptr + i) = i;
		printf("%d ", *(ptr + i));
	}
	printf("\\n");

	//释放和回收空间
	free(ptr);
	ptr = NULL;

	return 0;
}

看两个测试用例代码运行结果:
第一个:


这个是正常运行。

第二个:


这个是空间申请过大,然后报错,程序提前结束。

calloc

calloc原型如下:

void *calloc( size_t num, size_t size );

  • 这个函数的第一个参数是申请空间的个数,第二个参数是每个空间的大小,单位是字节。
  • 这个函数比mallo函数多了会将开辟好的内存空间中每个字节初始化为0。

看一个实例:

int main()
{
	int *ptr = (int*)calloc(10, sizeof(int));//开辟40个字节大小的连续空间
	//检查空间是否申请成功
	if (ptr == NULL)
	{
		perror("malloc fail:");
		exit(-1);
	}
	
	//释放和回收空间
	free(ptr);
	ptr = NULL;

	return 0;
}

看一下内存:

果真都被初始化为0了。

realloc

realoc函数原型:

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

  • 函数第一个参数是要调整的内存地址,第二个参数是调整后空间的大小。
  • 返回值为调整之后的内存起始位置。
  • 这个函数会将原来内存中的数据移动到新的空间上。
  • realloc调整内存空间一般会有两种情况:
    第一种情况:
    原有空间之后有足够的大的空间。
    第二种情况:
    原有空间之后没有足够大的空间。
    图解:

所以realloc函数在使用时,我们要注意其返回值不能直接用原指针接收,如果动态内存空间申请失败,那么原来那块空间也就找不到了,所以我们要创建一个新的指针变量来接收并检查指针是否为空,这样就能保证空间申请失败时元原空间不丢失,下面我们来看一个实例:

int main()
{
	int *ptr = (int*)malloc(10*sizeof(int));//开辟40个字节大小的连续空间
	//检查空间是否申请成功
	if (ptr == NULL)
	{
		perror("malloc fail:");
		exit(-1);
	}

	//扩展40个字节的空间
	//创建一个临时指针变量接收新的空间地址
	int* tmp = (int*)realloc(ptr, 10 * sizeof(int));
	if (tmp == NULL)
	{
		perror("realloc fail:");
		exit(-1);
	}

	ptr = tmp; //扩展成功就把新的空间地址给指向旧的空间的指针变量

	//释放和回收空间
	free(ptr);
	ptr = NULL;

	return 0;
}

常见的动态内存错误

对NULL进行解引用操作

#include <limits.h>

int main()
{
	int* ptr = (int*)malloc(INT_MAX);
	*ptr = 10;
	free(ptr);
	ptr = NULL;
	return 0;
}

指针ptr未检查是否为空,直接使用造成对NULL进行解引用操作程序直接崩溃了。

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

int main()
{
	int* ptr = (int*)malloc(10 * sizeof(int));
	if (ptr == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	int i = 0;
	for (i = 0; i <= 10; i++)
	{
		*(ptr + i) = i;//当i = 10时,指针越界访问,程序崩溃
	}

	free(ptr);
	ptr = NULL;

	return 0;
}

当i = 10时,指针越界访问,程序崩溃。

对非动态开辟的空间进行free释放

int main()
{
	int arr[10] = { 0 };
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		arr[i] = i;
	}
	free(arr);

	return 0;
}


程序直接崩溃了。

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

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

指针p的内容发生了改变,只释放了一部分的内存空间 。程序用运行起来也是直接崩溃。

对同一块动态内存进行多次free释放

int main()
{
	int *p = (int *)malloc(100);
	if (p == NULL)
	{
		perror("malloc fail");
	}
	free(p);
	free(p);
	return 0;
}

程序再一次如我们所想地崩了。

这里就是在告诉我们要养成一个好的习惯,已经释放过的空间要记得置为NULL

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

int main()
{
	int *p = (int *)malloc(100);
	if (p == NULL)
	{
		perror("malloc fail");
	}
	while (1);//程序一直停留在这里不结束,申请的那块空间就不会被释放,这样就导致了内存泄漏
	return 0;
}

程序一直停留在循环那不结束,申请的那块空间就不会被释放,这样就导致了内存泄漏。
我们程序员自己申请的空间一定要自己手动释放,养成一个好的习惯。加入一个服务器一天24小时都在跑,一直申请内存空间但又不是放,程序跑的时间长了,服务器就会崩了,直接结束程序,这是一个很危险的事情。

动态内存相关的几个笔试题

笔试题1

void GetMemory(char *p)
{
	p = (char *)malloc(100);
}
void Test(void)
{
	char *str = NULL;
	GetMemory(str);
	strcpy(str, "hello world");
	printf(str);![请添加图片描述](https://img-blog.csdnimg.cn/a9c5179be36e45c6af526590e896e245.gif)

}

可以先思考一下这里有什么错误,str会打印预期的结果吗?

我们可以发现在Test函数内部,Getmemory中传了一个一级指针过去,GetMemory这个函数也是用以及指针接收这个参数,相当于值传递,也就是说在GetMemory函数内部,给指针p申请一块空间,ptr是得不到的,ptr的内容不会发生任何改变。所以Test函数内部进行字符串拷贝会由于目标字符串空间不足而导致程序崩溃。而且要记得free释放动态内存申请的空间


看运行结果,程序也是直接就崩了。
看一下改善后的代码:

void GetMemory(char** p)
{
	*p = (char *)malloc(100);//修改3
}
void Test(void)
{
	char *str = NULL;
	GetMemory(&str);//修改1
	strcpy(str, "hello world");
	printf(str);
	free(str);//修改2
	str = NULL;
}

再看一次代码运行结果:

笔试题2

char *GetMemory(void)
{
	char p[] = "hello world";
	return p;
}
void Test(void)
{
	char *str = NULL;
	str = GetMemory();
	printf(str);
}

看一下这串代码会发生什么?

我们来一步一步分析一下,首先创建了一个char类型的指针变量,然后用了接收GetMemory函数的返回值。GetMemory函数内部创建了一个字符数组p,返回了首元素地址,与此同时,p指向的空间也被销毁了Test函数内部的str接收了这个地址,其实str就是一个野指针了,因为它指向的空间已经被销毁了,然后打印的时候进行了访问,这就对野指针进行了访问操作,这是一个很危险的动作,会导致程序崩了。

笔试题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);
}

看一下这串代码会发生什么?

这里其实就是发生了内存泄漏。动态内存开辟的空间没有释放。GetMemory函数内部开辟了100个字节大小的空间给了str,str指向的空间在Test函数内部使用后没有及时释放。造成了内存泄漏

笔试题4

void Test(void)
{
	char *str = (char *)malloc(100);
	strcpy(str, "hello");
	free(str);
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}

str指针申请了一块大小为100个字节的空间,然后进行字符串拷贝,"hello"被拷贝到str指向的空间中,然后str指向的空间被free释放和回收了,由于没有将str置为NULL,str就变成了一个野指针。下面有再一次的对str进行使用,也就造成了野指针的访问,程序同样会崩,这里又再一次告诫我们,被free释放和回收后的指针一定要置空,你面造成不必要的麻烦。

柔性数组

什么是柔性数组?

在C99中,结构体最后一个元素是一个未知大小的数组,这就叫做柔性数组成员
例如:

//第一种
typedef struct st_type
{
	int i;
	int a[0];//柔性数组成员
}type_a;

//第二种
typedef struct st_type
{
	int i;
	int a[];//柔性数组成员
}type_a;

以上两种哪一种不报错就使用哪一种,因为这是根据编译器来选择的。

柔性数组的特点

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

柔性数组的使用

柔性数组实现方式:

typedef struct st
{
	int i;
	int arr[];//柔性数组成员
}st;

int main()
{
	st* p = (st*)malloc(sizeof(st)+sizeof(int)* 10);
	if (p == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	//开始使用
	p->i = 100;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		p->arr[i] = i;
	}

	//如果不够,继续扩展
	st* tmp = (st*)realloc(p, sizeof(st)+sizeof(int)* 20);
	if (tmp == NULL)
	{
		perror("realloc fail");
		exit(-1);
	}
	else
	{
		p = tmp;
	}
	for (i = 10; i < 20; i++)
	{
		p->arr[i] = i;
	}

	//释放回收空间
	free(p);
	p = NULL;

	return 0;
}

图解:

来看一种指针实现上述效果:

typedef struct st
{
	int i;
	int* ptr;
}st;

int main()
{
	st* p = (st*)malloc(sizeof(st));//先开辟一个结构体大小的空间
	if (p == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	
	p->i = 100;
	p->ptr = (int*)malloc(sizeof(int)* p->i);
	if (p->ptr == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	int i = 0;
	//使用
	for (i = 0; i < p->i; i++)
	{
		p->ptr[i] = i;
	}

	//扩展空间
	int* tmp = (int*)realloc(p->ptr, sizeof(int)* (p->i+10));
	if (tmp == NULL)
	{
		perror("realloc fail");
		exit(-1);
	}
	else
	{
		p->ptr = tmp;
	}
	for (i = 10; i < 20; i++)
	{
		p->ptr[i] = i;
	}

	//释放空间
	free(p->ptr);
	p->ptr = NULL;
	free(p);
	p = NULL;

	return 0;
}

图解:

柔性数组的优势

对比上面两种方法,柔性数组有两个优势:
第一个优势:第一种方法只需要释放一次内存空间,方便内存释放,但第二种方法要进行两次free,不方便释放。
第二个优势:由于空间连续,有利于内存访问,所以访问速度快。连续的内存有益于提高访问速度,也有益于减少内存碎片。内存碎片化会导致右下空间不可用。所以柔性数组还是比较有优势的。

总结

本片博客就先介绍到这,同时也是为了自己巩固知识。大家可以互相学习。欢迎大家点赞支持和指正~

以上是关于C语言之动态内存管理(动态内存分配+经典笔试题+柔性数组)[建议收藏]的主要内容,如果未能解决你的问题,请参考以下文章

几道经典动态内存分配笔试题!楼下大爷做完直呼就这?(题目+答案+详解)C语言

动态内存分配

动态内存管理(动态内存函数的介绍,c/c++经典笔试题,柔性数组)

动态内存管理(动态内存函数的介绍,c/c++经典笔试题,柔性数组)

动态内存管理详解(动态内存函数介绍 + 常见动态内存错误 + 经典笔试题)

⭐️欢度国庆-共约C语言进阶⭐️ 动态内存管理+柔性数组 建议收藏