【C语言】动态内存管理和柔性数组

前言

我们知道数据的存储有静态存储和动态存储两种方式,各有各的优点,前面我们所学的都是静态存储,今天我们来学习一下如何进行动态的存储。

一、动态内存分配的意义

我们知道,我们定义一个变量或数组的时候,是在栈区或者静态区开辟内存空间的。

#include<stdio.h>
int num = 100;//全局变量:静态区
int arr[20];
static int st = 1;//静态全局变量:静态区

int main()
{
	int count = 0;//局部变量:栈区
	int a[10];
	static se = 10;//静态局部变量:静态区
}

全局变量和静态变量是在静态区开辟空间的 ;
局部变量和函数的形参是在栈区开辟空间的;

在这里插入图片描述

栈区(stack): 在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。

堆区(heap): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分
配方式类似于链表。

数据段(静态区) (static): 存放全局变量、静态数据。程序结束后由系统释放。

普通的局部变量存储在栈区,出了作用域就销毁;
但是被static修饰的变量存储在静态区(数据段),生命周期变长,直到程序结束才销毁。


比如:

char ch = 'a';		//栈上开辟1个字节空间
int arr[100] = { 0 };//栈上开辟40个字节的连续空间

可以发现这种开辟空间的方式有两个特点:

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

这样的话,如果我们arr数组只有10个数据要存储,剩下的90个整型空间就浪费掉了;如果有200个数据要存储,那么数组的空间又不够;不仅如此,有时候我们需要的空间大小在程序运行的时候才能知道;这些情况下,我们就需要用到动态内存开辟。

动态内存开辟是在程序执行的过程中根据程序的需要 动态地分配或者回收存储空间。

动态开辟是在堆区进行的。

二、动态内存函数

以下这几个动态内存函数的头文件都是 stdlib.h

1.malloc

void* malloc(size_t size);

功能:向内存申请一块连续可用的空间,并返回指向这块空间的指针。

注意:

1.空间开辟成功,则返回一个指向这块空间的指针;
2.空间开辟失败,则返回一个空指针NULL
3.返回值的类型是void*, 程序员自己决定返回类型;
4.如果参数size为0,这种情况是C语言没有定义的,能不能开辟成功取决于编译器。

malloc函数用法会和free函数一起介绍

2.free

void free(void* ptr);

功能:用来释放动态开辟的内存空间(与三种动态开辟函数搭配使用)。

注意:

1.如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的;
2.如果参数 ptr 是NULL指针,则函数什么事都不做;
3.动态开辟的空间使用完后记得使用free释放掉,还给操作系统;
4.释放完动态开辟的内存空间后,ptr一定要置NULL。这是因为free之后那块开辟的空间已经释放掉不存在了,但ptr仍指向那块空间,所以手动置NULL防止后续非法访问。

malloc和free的搭配使用:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	//int arr[10] //栈上开辟10个整型空间
	int* p = (int*)malloc(10 * sizeof(int));//堆上开辟10个整型空间
	if (NULL == p)
	{
		perror("malloc");//打印错误信息
		return 1;//直接返回,后面程序不再执行
	}
	
	for (int i = 0; i < 10; i++)
		p[i] = i;
	for (int i = 0; i < 10; i++)
		printf("%d ", p[i]);

	free(p);//释放空间
	p = NULL;//手动置为空指针
	return 0;
}

3.calloc

void *calloc(size_t num, size_t size);

功能:为 num 个大小为 size 的元素开辟一块空间,并把空间的每个字节初始化为 0,并返回一个指向这块空间的指针。

与malloc相比calloc多了一个参数,会把申请空间的每个字节初始化为0;
calloc分配内存空间失败也会返回一个空指针NULL。

int* p1 = (int*)malloc(10 * sizeof(int));//开辟40个空间
int* p2 = (int*)calloc(10, sizeof(int));//开辟10个大小为int的空间也即40个空间

calloc用法:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	if (NULL == p)
	{
		perror("calloc");//打印错误信息
		return 1;//直接返回,后面程序不再执行
	}
	
	for (int i = 0; i < 10; i++)
		p[i] = i;
	for (int i = 0; i < 10; i++)
		printf("%d ", p[i]);

	free(p);//释放空间
	p = NULL;//手动置为空指针
	return 0;
}

4.realloc

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

功能:使得动态内存管理更加灵活。用于重新调整之前调用 malloc 或 calloc 所分配的 ptr 所指向的内存块的大小,可以对动态开辟的内存进行大小的调整。

注意:

1.ptr 为指针要调整的内存地址。
2.size 为调整之后的新大小
3.返回值为调整之后的内存起始位置,请求失败则返回空指针。
4.realloc 函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。

realloc函数在调整内存空间的时候存在三种情况:

1.原有空间的后面足够大的空间,会直接在原有空间之后直接追加空间,原来空间的数据不发生变化。
2.原有空间的后面没有足够的空间,会在在堆空间上另找一个合适大小的连续空间来使用,并且返回一个指向这块新空间的地址。
3.开辟空间失败则返回空指针NULL。

realloc用法:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* arr = (int*)calloc(10, sizeof(int));//申请10个整型空间,并初始化0
	if (NULL == arr)
	{
		perror("calloc");//打印错误信息
		return 1;//直接返回,后面程序不再执行
	}
	
	int* p = (int*)realloc(arr, 50 * sizeof(int));//重新调整为50个整型空间
	if (NULL == p)
	{
		perror("realloc");
		return 1;
	}
	arr = p;//如果开辟成功,arr指向新开辟的空间,arr之前保存的内容不变

	/*
	对arr进行操作
		*/

	free(arr);//释放空间
	arr = NULL;//手动置为空指针
	return 0;
}

三、常见的动态内存错误

1.对NULL指针的解引用操作

动态内存开辟之后先判断是否为NULL再使用,否则会出现问题

int* p = (int*)malloc(100000000000);
*p = 10;//如果p开辟失败是NULL呢,所以先判断再使用,像上面一样

解决方法: 对动态开辟空间的函数的返回值进行判断

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

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	if (NULL == p)
	{
		perror("calloc");
		return 1;
	}
	for (int i = 0; i < 20; i++)//p只开辟了10个整型空间,无法访问20个
		p[i] = i;

	free(p);
	p = NULL;
	return 0;
}

解决方法: 对边界进行检查,不要越界访问不属于自己开辟的空间

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

int main()
{
	int a = 100;
	int* p = &a;
	free(p);
	p = NULL;
	return 0;
}

发生错误:

在这里插入图片描述

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

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	if (NULL == p)
	{
		perror("calloc");
		return 1;
	}
	for (int i = 0; i < 10; i++)
	{
		*p = i;
		p++;//p不再指向空间的起始位置
	}
	free(p);//内存泄露
	p = NULL;
	return 0;
}

p不再指向空间起始位置,此时free掉p只释放了p后面的位置,空间不能完全释放,会造成内存泄露。

发生错误:

在这里插入图片描述

解决方法: 创建临时变量保存空间的起始位置或者不改变p所指向的位置。

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

当我们写代码的时候可能忘了这块空间已经释放,就有可能后面会重复释放。

程序崩溃:

在这里插入图片描述

程序正常:

在这里插入图片描述

解决方法: 空间释放完及时置空NULL,这样即使后面再次释放,free函数什么也不会做。

6.动态开辟内存忘记释放导致内存泄漏

#include<stdio.h>
#include<stdlib.h>
void test()
{
	int* p = (int*)malloc(100);
	if (NULL == p)
	{
		return 0;
	}
	for (int i = 0; i < 10; i++)
		p[i] = i;
}
int main()
{
	test();
	return 0;
}

如果在函数内部开辟空间,没有及时释放,则后面也无法再释放,因为出了函数之后p销毁,找不到开辟空间的起始地址,想释放也释放不了,造成内存泄漏。同样在主函数内部忘记释放也会造成内存泄露。

内存泄漏: 指的是在程序运行过程中,分配的内存空间没有被正确释放或回收的现象。

内存泄漏的危害:当程序中存在内存泄漏时,每次执行该部分代码都会分配新的内存空间,但是却没有释放之前分配的内存空间,导致程序使用的内存空间不断增加,最终可能导致内存耗尽,程序崩溃或运行变慢。

解决方法: 动态开辟空间使用完之后一定要及时free释放。

总结:

1.根据需要开辟空间,避免空间浪费或者空间不足;
2.动态内存函数malloc、calloc、realloc一定要和free搭配使用,避免空间泄露;
3.空间开辟完一定要先判断再使用,避免对空指针解引用;
4.空间free完一定要置空,避免再次使用导致程序崩溃。

四、动态内存开辟练习题

练习一:

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

结果:这段代码运行会造成程序崩溃和内存泄露。
结果分析:

1.str是以值传递的方式传参(值传递并不会影响实参),p是str的临时拷贝,有自己独立的空间,GetMemory函数内部申请的空间放在p中,str仍是NULL,所以strcpy函数使用的时候造成了非法访问,程序会崩溃。
2.GetMemory函数内部申请开辟了空间,但没有及时释放,造成内存泄漏。
3.没有对开辟的空间地址进行判断,容易对空指针解引用。

改正:
将值传递改为址传递,参数用二级指针接收;空间开辟完判断是否为NULL;空间使用完及时释放。

#include<stdio.h>
#include<stdlib.h>
void GetMemory(char** p)
{
	*p = (char*)malloc(100);
}
void test(void)
{
	char* str = NULL;
	GetMemory(&str);
	if (str == NULL)
	{
		printf("空间开辟失败\n");
		return 1;
	}
	strcpy(str, "hello world");
	printf(str);
	free(str);
	str = NULL;
}
int main()
{
	test();
	return 0;
}

练习二:

#include<stdio.h>
#include<stdlib.h>
char* GetMemory(void)
{
	char p[] = "hello world";
	return p;
}
void test(void)
{
	char* str = NULL;
	str = GetMemory();
	if (str == NULL)
	{
		printf("空间开辟失败\n");
		return 1;
	}
	printf(str);
}
int main()
{
	test();
	return 0;
}

结果:非法访问内存,打印乱码
结果分析:

1.局部变量p出了函数自动销毁,p所指空间的可能被使用,也可能没有被使用,我们不知道,所以会打印乱码随机值;
2.p不是动态开辟的空间,所以不用free。

改正:
只需修改GetMemory函数即可,使用static修饰变量或者常量字符串即可,这两种都不会随着函数返回而销毁;

char* GetMemory(void)
{
	//static char p[] = "hello world";//p存放在静态区
	char* p = "hello world";//常量字符串也存放在静态区
	return p;
}

练习三:

#include<stdio.h>
#include<stdlib.h>
void test(void)
{
	char* str = (char*)malloc(100);
	if (str == NULL)
	{
		printf("空间开辟失败\n");
		return 1;
	}
	
	strcpy(str, "hello");
	free(str);
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}
int main()
{
	test();
	return 0;
}

结果:打印world,但是造成非法访问
结果分析:

str指向的空间已经释放掉还给操作系统了,我们没有权限继续访问,但是str没有置空是一个野指针,虽然程序可以正常运行打印结果,但是这样的写法是错误的,会造成非法访问内存。

五、柔性数组

柔性数组的定义和特点

柔性数组是C99 引入的一个新特性,即结构体中的最后一个元素可以是未知大小的数组,并且要求这样的结构体至少包含一个其他类型的成员。

struct S
{
	int n;
	char str[];//或者char str[0](部分编译器会报错) 都表示数组大小是未知的
};
printf("%d\n", sizeof(struct S));//输出4

柔性数组的特点

1.结构中的柔性数组成员前面必须至少一个其他成员。
2.柔性数组存在于结构体内,但是不占结构体的大小。
3.包含柔性数组成员的结构用malloc() 函数进行内存的动态分配,并且分配的内存应该大于结构的大 小,以适应柔性数组的预期大小。

柔性数组的使用和优点

在堆上给结构体分配内存,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小。

#include<stdio.h>
#include<stdlib.h>
struct S
{
	int n;
	char str[];
};
int main()
{
	//柔性数组str获得了20个char的空间
	struct S* p = (struct S*)malloc(sizeof(struct S) + 20 * sizeof(char));
	if (NULL == p)
	{
		printf("malloc fail\n");
		return 1;
	}
	
	p->n = 250;
	for (int i = 0; i < 20; i++)
	{
		p->str[i] = 'a' + i;
	}
	free(p);
	return 0;
}

对比上下两段代码

struct S
{
	int n;
	char* str;//这里不一样!!!
};
int main()
{
	struct S* p = (struct S*)malloc(sizeof(struct S));
	if (NULL == p)
	{
		printf("malloc p fail\n");
		return 1;
	}

	p->n = 250;
	p->str = (char*)malloc(20 * sizeof(char));
	if (NULL == p->str)
	{
		printf("malloc p->str fail\n");
		return 1;
	}

	for (int i = 0; i < 20; i++)
	{
		p->str[i] = 'a' + i;
	}
	//释放顺序千万不要搞反,先释放结构体成员,再释放结构体
	free(p->str);
	p->str = NULL;
	free(p);
	p = NULL;
	return 0;
}

这两段代码实现的功能是一样的,但是有些不同之处,第一段代码一次性分配足够空间;第二段代码是先给结构体分配空间,再给结构体成员分配空间,这两块空间并不是连续的,并且最后需要free两次,置空两次。

第一段代码更好,优点如下:

1.方便内存释放。代码一只需要释放一次,而代码二需要释放两次,并且结构体成员容易忘记释放。
2.访问速度更快。连续的内存有益于提高访问速度,也有益于减少内存碎片。动态开辟空间的次数越多,产生的内存碎片就越多,就会降低内存的使用率。

关于动态内存管理和柔性数组的内容总结到这里就结束了,就感谢您的观看和支持!
后面我会基于动态内存再写一个通讯录小程序,希望大家多多支持!

  • 23
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
东南亚位于我国倡导推进的“一带一路”海陆交汇地带,作为当今全球发展最为迅速的地区之一,近年来区域内生产总值实现了显著且稳定的增长。根据东盟主要经济体公布的最新数据,印度尼西亚2023年国内生产总值(GDP)增长5.05%;越南2023年经济增长5.05%;马来西亚2023年经济增速为3.7%;泰国2023年经济增长1.9%;新加坡2023年经济增长1.1%;柬埔寨2023年经济增速预计为5.6%。 东盟国家在“一带一路”沿线国家中的总体GDP经济规模、贸易总额与国外直接投资均为最大,因此有着举足轻重的地位和作用。当前,东盟与中国已互相成为双方最大的交易伙伴。中国-东盟贸易总额已从2013年的443亿元增长至 2023年合计超逾6.4万亿元,占中国外贸总值的15.4%。在过去20余年中,东盟国家不断在全球多变的格局里面临挑战并寻求机遇。2023东盟国家主要经济体受到国内消费、国外投资、货币政策、旅游业复苏、和大宗商品出口价企稳等方面的提振,经济显现出稳步增长态势和强韧性的潜能。 本调研报告旨在深度挖掘东南亚市场的增长潜力与发展机会,分析东南亚市场竞争态势、销售模式、客户偏好、整体市场营商环境,为国内企业出海开展业务提供客观参考意见。 本文核心内容: 市场空间:全球行业市场空间、东南亚市场发展空间。 竞争态势:全球份额,东南亚市场企业份额。 销售模式:东南亚市场销售模式、本地代理商 客户情况:东南亚本地客户及偏好分析 营商环境:东南亚营商环境分析 本文纳入的企业包括国外及印尼本土企业,以及相关上下游企业等,部分名单 QYResearch是全球知名的大型咨询公司,行业涵盖各高科技行业产业链细分市场,横跨如半导体产业链(半导体设备及零部件、半导体材料、集成电路、制造、封测、分立器件、传感器、光电器件)、光伏产业链(设备、硅料/硅片、电池片、组件、辅料支架、逆变器、电站终端)、新能源汽车产业链(动力电池及材料、电驱电控、汽车半导体/电子、整车、充电桩)、通信产业链(通信系统设备、终端设备、电子元器件、射频前端、光模块、4G/5G/6G、宽带、IoT、数字经济、AI)、先进材料产业链(金属材料、高分子材料、陶瓷材料、纳米材料等)、机械制造产业链(数控机床、工程机械、电气机械、3C自动化、工业机器人、激光、工控、无人机)、食品药品、医疗器械、农业等。邮箱:market@qyresearch.com

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值