C语言——动态内存函数

目录

动态内存函数

1、为什么需要开辟动态内存?

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

 题目2

题目3

题目4

柔性数组

1、柔性数组特点与使用


动态内存函数

1、为什么需要开辟动态内存?

我们已经掌握的内存开辟方式有:

int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间

但是上述的开辟空间的方式有两个特点:

空间开辟大小是固定的。
数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。也就是说当我们在定义变量时并不知道会使用多少的内存,这时候就需要进行动态内存开辟!
上述两种开辟内存方法一个在栈上开辟,一个在堆上开辟。
在C语言<stdlib.h>或<malloc.h>内置的库中有能够进行动态内存开辟的库函数。

2、动态内存函数

2.1 malloc

C语言提供了一个动态内存开辟的函数:

//Allocates memory blocks.

void *malloc( size_t size );

参数size_t表示需要开辟的内存的字节数。该函数会返回开辟好内存的首地址,如果开辟失败返回NULL

比如使用malloc函数开辟拥有10个整型元素的数组,那需要开辟的字节数为40字节。

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

int main()
{
	//使用malloc开辟一个含10个的整型元素数组
	int* arr = NULL;
	int* p = (int*)malloc(sizeof(int) * 10);//为数组开辟内存
	if (p == NULL)
	{
		printf("内存申请失败!\n");
		exit(-1);//内存申请失败,程序没有再进行的必要,直接强制结束程序
	}
	arr = p;//确认内存开辟成功再将此内存交给数组
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		arr[i] = i + 1;//使用这块动态内存
		printf("%d ", arr[i]);
	}
    //使用完动态内存要free
	free(arr);
	arr = NULL;
	return 0;
}

因为malloc函数的返回值类型为void*,所以需要将已经开辟好的内存的首地址强制转换成整型指针类型。

输出结果为:

1 2 3 4 5 6 7 8 9 10
D:\gtee\C-learning-code-and-project\test_928\Debug\test_928.exe (进程 22188)已退出,代码为 0。
按任意键关闭此窗口. . .

2.2 free

对于动态内存开辟的空间,开辟的地址是在堆上的,使用完了是需要返还给操作系统的,C语言中专门有一个回收动态开辟内存的函数——free。当然,程序结束时,会自动释放内存。

//Deallocates or frees a memory block.

void free( void *memblock );

参数void *memblock表示动态开辟内存的首地址,注意这个地址只能是动态开辟内存的首地址,其他的地址都不行!如果传入的地址为NULL,则这个函数什么都不会做。

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

int main()
{
	//使用malloc开辟一个含10个的整型元素数组
	int* arr = (int*)calloc(10, sizeof(int));//为数组开辟内存
	if (arr == NULL)
	{
		printf("内存申请失败!\n");
		exit(-1);//内存申请失败,程序没有再进行的必要,直接强制结束程序
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);
	}
    //动态空间使用完之后,一定要释放掉
	free(arr);
	arr = NULL;//内存释放后,将指针变量置空
	return 0;
}

内存泄漏的危害:
如果动态内存已经使用完了,但不还给操作系统,也就是没有释放内存,就有可能造成内存泄漏的风险。对于其危害,举个栗子,如果在服务器上存在内存泄漏,则可能造成服务器崩溃。因为服务器是一直工作的,一旦存在内存泄漏,使用完的内存不还回去,久而久之,服务器内存被占用的越来越多,终有一天由于内存不足而造成服务器崩溃。

在释放完动态内存空间了之后,需要将指针变量arr赋值NULL。这是因为,释放空间的意思是,把内存空间还给操作系统,也就是把使用内存的权力还给系统,我们用户将不再有使用这块空间的权力。但是,我们在程序中依然保留了动态空间的地址,所以为了数据不泄露,我们需要把指针变量赋值NULL,这样就再也找不到我们开辟过的动态空间了。

2.3 calloc

该函数功能与malloc非常相似,仅仅多了个初始化的功能,就是说在动态内存开辟时,自动将内存中的元素初始化为0

//Allocates an array in memory with elements initialized to 0.

void *calloc( size_t num, size_t size );

参数size_t num表示元素个数,size_t size表示每个元素所占字节数大小。

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

int main()
{
	//使用malloc开辟一个含10个的整型元素数组
	int* arr = (int*)calloc(10, sizeof(int));//为数组开辟内存
	if (arr == NULL)
	{
		printf("内存申请失败!\n");
		exit(-1);//内存申请失败,程序没有再进行的必要,直接强制结束程序
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);
	}
    //动态空间使用完之后,一定要释放掉
	free(arr);
	arr = NULL;//内存释放后,将指针变量置空
	return 0;
}

输出结果:

0 0 0 0 0 0 0 0 0 0
D:\gtee\C-learning-code-and-project\test_928\Debug\test_928.exe (进程 29776)已退出,代码为 0。
按任意键关闭此窗口. . .

2.4 realloc

该函数能够在保留原数据的情况下,对动态申请内存的大小进行调整,通常用来对数组或者链表等数据结构进行扩容。该函数在调整动态内存大小时有以下两个细节:

1、如果原申请内存地址后连续空间大于调整空间大小,则在原地址进行内存调整。

2、如果原申请内存地址后连续空间小于调整空间大小,则在其他内存足够地方进行调整,并将原数据拷贝到新内存和释放原来申请内存的空间。

如果调整失败,返回NULL,调整成功返回新申请内存的首地址。

//Reallocate memory blocks.

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

参数void *memblock表示需要调整空间的首地址(必须为动态开辟的内存空间),参数size_t size表示调整后内存的字节数。

将动态申请的整型数组元素个数调整至20。

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

int main()
{
	//使用malloc开辟一个含10个的整型元素数组
	int* arr = (int*)calloc(10, sizeof(int));//为数组开辟内存
	if (arr == NULL)
	{
		printf("内存申请失败!\n");
		exit(-1);//内存申请失败,程序没有再进行的必要,直接强制结束程序
	}
    int* ptr = realloc(arr,sizeof(int) * 20);
	if (ptr == NULL)
	{
		printf("内存申请失败!\n");
		exit(-1);//内存申请失败,程序没有再进行的必要,直接强制结束程序
	}
    arr = ptr;
	for (int i = 0; i < 20; i++)
	{
		arr[i] = i + 1;
		printf("%d ", arr[i]);
	}
    //动态空间使用完之后,一定要释放掉
	free(arr);
	arr = NULL;//内存释放后,将指针变量置空
	return 0;
}

输出结果为:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
D:\gtee\C-learning-code-and-project\test_928\Debug\test_928.exe (进程 7520)已退出,代码为 0。
按任意键关闭此窗口. . .

3、常见的动态内存错误

3.1、对NULL指针的解引用操作

错误示范

void test()
{
	int* p = (int*)malloc(INT_MAX / 4);
	*p = 20;//如果p的值是NULL,就会有问题
	free(p);
}

改正

void test()
{
	int* p = (int*)malloc(INT_MAX / 4);
	if (p == NULL)
	{
		printf("内存申请失败!\n");
		exit(-1);//强制结束程序
	}
	*p = 20;//如果p的值是NULL,就会有问题
	free(p);
}

使用动态内存函数开辟空间,一定要进行判断,是否成功开辟动态空间,开辟失败会返回NULL空指针,这会影响到我们后面的程序。

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

改正

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);//对非动态开辟的内存释放是错误的,程序会崩溃
}

改正

void test()
{
	int* a = (int*)malloc(sizeof(int));
	if (a == NULL)
	{
		exit(-1);//强制结束程序
	}
	*a = 10;
	int* p = a;
	free(p);//对非动态开辟的内存释放是错误的,程序会崩溃
	p = NULL;
	a = NULL;
}

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

错误示范

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

改正

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

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

错误示范

void test()
{
	int* p = (int*)malloc(100);
	free(p);
	free(p);//重复释放,程序崩溃
}

改正

void test()
{
	int* p = (int*)malloc(100);
	free(p);
}

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

错误示范

void test()
{
	int* p = (int*)malloc(100);
	if (NULL != p)
	{
		*p = 20;
	}
}
int main()
{
	test();
	while (1);//内存忘记示范,内存泄漏,程序崩溃
}

改正

void test()
{
	int* p = (int*)malloc(100);
	if (NULL != p)
	{
		*p = 20;
	}
	free(p);
	p = NULL;//好习惯
}
int main()
{
	test();
	while (1);
}

4、有关动态内存的经典笔试题

题目1

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

这个程序会崩溃。原因是,

1、str传给p的时候,是值传递,p是str的临时拷贝,所以当malloc开辟的空间起始地址放在p中的时候,不会影响str。str依然为NULL。

2、当str是NULL,strcpy想把hello world拷贝到str指向的空间时,程序就崩溃了。因为NULL指针指向的空间是不能直接访问的。

 题目2

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

p为GetMemory函数内部的局部变量,该函数运行完后,其栈帧被销毁,在函数外得到返回的地址并访问属于非法访问,打印该地址的字符串,如果该空间没有被覆盖,能够打印hello world,否则打印随机值。调用printf函数是有可能覆盖该地址的,所以极大概率打印的是随机值。

输出结果为

烫烫烫烫烫烫烫烫8
D:\gtee\C-learning-code-and-project\test_928\Debug\test_928.exe (进程 23592)已退出,代码为 0。
按任意键关闭此窗口. . .

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

该程序虽然会输出hello,但是是存在内存泄漏的,因为最后并没有释放申请的内存。

题目4

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

输出world将一个动态申请的空间释放,传入的指针变量是不会置空的,会成为一个野指针,所以我们要养成一个好习惯:释放一个空间,应将其传入的指针置空!

柔性数组

1、柔性数组特点与使用

也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。

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

有些编译器会报错无法编译可以改成:

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

柔性数组的特点:

1、结构中的柔性数组成员前面必须至少一个其他成员。

2、 sizeof 返回的这种结构大小不包括柔性数组的内存。

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

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

int main()
{
	printf("%d\n", sizeof(type_a));//输出的是4
	return 0;
}

柔性数组的使用

方法1

int main()
{
	int i = 0;
	type_a* p = (type_a*)malloc(sizeof(type_a) + 100 * sizeof(int));

	//业务需求代码段

	p->i = 100;
	for (i = 0; i < 100; i++) {
		p->a[i] = i;
	}
	free(p);
	return 0;
}

方法2

typedef struct st_type
{
	int i;
	int* p_a;
}type_a;

int main()
{
	type_a* p = (type_a*)malloc(sizeof(type_a));
	p->i = 100;
	p->p_a = (int*)malloc(p->i * sizeof(int));
	
	//业务需求代码段
	
	int i = 0;
	for (i = 0; i < 100; i++) {
		p->p_a[i] = i;
	}
	//释放空间
	free(p->p_a);
	p->p_a = NULL;
	free(p);
	p = NULL;
	return 0;
}

上述 代码1 和 代码2 可以完成同样的功能,但是 方法1 的实现有两个好处:

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

第二个好处是:这样有利于访问速度. 连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得也没多高了,反正你跑不了要用做偏移量的加法来寻址)

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值