C语言——动态内存管理(详解)

news2024/9/22 3:47:49

1.动态内存管理存在的意义

在前面的C语言的学习中,我们已经掌握的空间开辟的方式有如下两种

int i = 0;
//开辟了4字节大小的空间存放i
int arr[5] = {0};
//开辟了20字节的空间存放数组arr

这样开辟空间有两个显著的特点:
1、每次开辟空间的大小都是固定好的。
2、数组在创建时就已经确定好了大小,这样使用起来灵活性不足。
在实际的编程中,对空间的需求不仅仅是以上的情况,当需要的空间大小在程序运行时才能明确的情况下,数组的编译时开辟空间的方式就不能满足了。这时就需要引入一个新的概念,动态内存管理。

2.动态内存函数的介绍

这里介绍的动态内存函数均包含于C标准库中的#include <stdlib.h>

2.1malloc和free

2.1.1malloc函数

malloc是C语言标准库中提供的一个动态内存开辟的函数。

它的声明如下:void* malloc(size_t sz);

malloc函数会向内存申请一块连续可用的空间,并返回指向该空间的起始地址的指针。如果malloc函数申请空间成功,会返回一个指针,该指针用于存放申请空间的起始地址。若malloc函数申请空间失败,将返回空指针NULL。所以在使用malloc函数申请内存空间时,需要对函数的返回值做一个判断。由于malloc函数返回值为 void* ,所以开辟的空间所存数据的类型是由使用者定的,所以具体的类型由使用者自己决定。如果参数部分sz为0的话,malloc函数的是否开辟内存空间,这是C语言标准未定义的,也是有点为难编译器的,这样写是不可取的。

2.1.2free函数

free函数是专门用于释放并回收动态开辟的内存空间的一个库函数。

它的声明如下:void free(void* ptr);

free函数用于释放动态开辟的内存的。如果free参数不是指向动态内存函数开辟的起始空间的指针,那么free的执行结果,也是C语言标准未定义的。在当前作者使用的VS2019环境下会造成程序直接崩溃。若free函数的参数为NULL,那么调用free函数将啥事都不发生。

2.1.3malloc函数、free函数的使用

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

int main()
{
	int* pa = (int*)malloc(40);
	//这里动态开辟40字节的连续空间
	//pa指针指向的是动态开辟空间的起始地址
	if(NULL == pa)//判断是否开辟成功
	{
		return 1;
	}
	int i = 0;
	for(i = 0; i < 10; i++)
	{
		*(pa+i) = i + 1;
	}
	for(i = 0; i < 10; i++)
	{
		printf("%d ",*(pa+i));
	}
	printf("\n");
	free(pa);//使用后释放内存
	pa = NULL;
	//释放后手动将指针置空
	
	return 0;
}

上面代码动态开辟了40字节的连续空间用于存放10个1-10的数。需要注意的是,使用动态开辟函数malloc需要判断是否动态开辟成功。使用free函数释放动态开辟空间内容,需要手动将指针置为NULL,这样可以避免野指针的产生。因为free后,内存空间将还给操作系统,而pa指针依旧指向那块还给操作系统的空间。若有一个糊涂的程序员访问了这块空间,必将导致指针越界访问,导致程序错误。所以,释放动态内存后,请将指针手动置空。

2.2calloc函数

上面介绍了malloc函数,它是用来动态开辟内存空间的。下面要介绍的calloc函数也是用来动态开辟内存空间的。
calloc函数将为num个大小为sz的元素动态开辟空间。

声明如下:void* calloc(size_t num, size_t sz);

calloc函数的返回类型是void*,若动态开辟成功,返回一个指针指向动态开辟空间的起始地址。若开辟失败,则返回一个NULL指针。当然返回的指针类型需要使用者自己来决定。参数部分,第一个参数size_t num,指的是开辟空间的元素个数,size_t sz表示开辟空间的大小,单位是字节。

2.2.1 malloc和calloc的区别

下面将通过代码举例分析malloc和calloc的区别。

int main()
{
	int* pa = (int*)malloc(40);
	if (NULL == pa)
	{
		return 1;
	}

	int* pb = (int*)calloc(10, 10 * sizeof(int));
	if (NULL == pb)
	{
		return 1;
	}

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

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

	free(pa);
	free(pb);
	pa = NULL;
	pb = NULL;
	return 0;
}

在这里插入图片描述
通过上例代码,可知calloc动态开辟内存后,会将num个元素给初始化成0,而malloc并没有初始化这一步骤。所以,当一些不需要初始化所有元素个数的场景下,应使用malloc函数动态开辟空间,这样将是程序的性能更好。反之,则使用calloc函数动态开辟空间。

2.3realloc函数

有时在申请内存空间时,我们觉得申请的空间多余了。有时在申请内存空间时,我们觉得申请的空间不足了。此时不免需要合理的对申请的空间进行调整,realloc函数便可以对动态开辟空间的大小进行调整。realloc函数使得动态内存管理的方式更加灵活。

函数声明:void* realloc(void* ptr, size_t sz);
ptr参数表示所需要调整的空间的起始地址。 sz参数表示,所需要调整的空间的调整后大小。该函数会返回一个调整后的空间的起始地址。realloc函数会将原本空间的数据,拷贝到调整后的空间内。

2.3.1realloc函数调整的两种情况

情况一:调整前的内存空间后,还有连续可用的内存空间。那么realloc函数会在原空间后面的空间直接进行追加扩容。
情况二:调整前的内存空间后,连续可用的内存空间不足。此时realloc函数会找到一块符合要求的连续空间进行开辟并使用。
在这里插入图片描述

2.3.2realloc函数使用注意事项

基于上面两种情况,我们在使用realloc函数时,应该这么做。

int main()
{
	int* pa = (int*)malloc(40);
	if (NULL == pa)
	{
		return 1;
	}

	int* pb = (int*)realloc(pa, 100);
	if (NULL != pb)//判断是否开辟成功
	{
		pa = pb;
		pb = NULL;//手动置空避免野指针
	}

	free(pa);
	pa = NULL;

	return 0;
}
//错误的例子
int main()
{
	int* pa = (int*)malloc(40);
	if (NULL == pa)
	{
		return 1;
	}

	pa = (int*)realloc(pa, 1000);//可能存在申请失败的问题

	for(int i = 0; i < 10; i++)
	{
	printf("%d ",*(pa+i));
	}
	
	free(pa);
	pa = NULL;

	return 0;
}

如果realloc申请失败,此时,函数返回空指针,那么前面malloc申请的空间的起始地址将被赋为NULL,这必将内存的泄露问题,以及后续的访问权限冲突问题。所以我们应该避免这样使用realloc函数调整空间。

3.常见的动态内存管理的错误案例

3.1对NULL指针进行解引用操作

void test()
{
 int *p = (int *)malloc(INT_MAX/4);
 *p = 20;//对NULL指针进行解引用操作
 free(p);
}

此时malloc函数参数为INT_MAX/4,这将是一个很大很大的数。在大部分的机器下,都无法动态申请到如此庞大的空间。这里malloc函数返回的是NULL。对NULL指针进行解引用操作,会造成访问权限冲突,进而导致程序直接崩溃。

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

int main()
{
 int i = 0;
 int *p = (int *)malloc(10*sizeof(int));
 if(NULL == p)
 {
	 return 1;
 }
 for(i=0; i<=10; i++)
 {
 *(p+i) = i;//当i是10的时候越界访问
 }
 free(p);
 p = NULL;
 reutrn 0;
}

上面在循环体编写时,把控制条件写成了i<=10,这不可避免地导致了越界访问了。这也是一个经典的指针越界形成野指针的问题。我们在编写循环时,应注意控制变量是否会造成越界访问的问题,编译器是无法做出对越界访问的检查的。

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

void test()
{
 int a = 10;
 int *p = &a;
 free(p);//ok?
}

这里我简单从语言层面上描述一下内存空间的分布
在这里插入图片描述
上面代码中的&a,取出的是栈区上的地址。对此地址进行free释放会导致程序直接崩溃。需要注意,free函数只能释放动态开辟内存。

3.4使用free函数释放一块动态内存的一部分

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

由于p++使得p的位置指向起始位置后一个指针变量大小的位置。此时对p++后的p指针进行free释放,那么会导致程序的崩溃。

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

void test()
{
	int* p = (int*)malloc(40);
	if(NULL == p)
	{
		return;
	}
	free(p);
	free(p);//重复释放
}

第一次free释放后,p指向的空间便会还给操作系统。由于没有手动对P置空,此时p依旧存放着p源动态数组起始地址,再次释放便会造成访问权限冲突,导致程序崩溃。所以在free释放空间后,应该手动将指针置空。

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


void test()
{
	int* p = (int*)malloc(100);
	if (NULL == p)
	{
		return;
	}

}

int main()
{
	test();
	return 0;
}

每一次调用test函数,都会想内存申请100字节空间,但是由于test执行完后,函数栈帧就销毁了。此时在test函数内申请的空间由于没有被标记,这回导致内存的泄露。我们使用free函数释放时,要正确合理的进行释放动态内存。

4.经典试题解析

4.1试题一

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

在这里插入图片描述

解析:运行这段代码后,程序会直接崩溃。本题错误为对NULL进行访问操作而导致访问权限冲突。在test函数中,创建字符指针了str。并以参数形式传给GetMemory函数。需要注意的是本次调用为传值调用,对形参部分的修改并不能影响实际的str。此时,程序进入GetMemory函数内部,开辟了100字节的空间并付给了形参p。然而,p开辟的空间随着GetMemory函数的执行结束后被销毁,我们再也无法找到开辟的100字节的空间。此时程序回到test函数中,将"hello world"拷贝到str指向的空间中,并对其进行输出。str指向的是0地址处,所以会造成访问权限的冲突,导致程序崩溃。本题的问题有两个,一、GetMemory函数存在内存泄露问题,二、对NULL指针进行了访问操作。

4.2试题二

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

在这里插入图片描述
解析:运行这段代码,屏幕上会输出一段有随机值组成的字符串内容。首先,test函数内部创建了字符指针str并初始化为空指针,将GetMemory函数返回值赋给str。然后,我们进入GetMemory函数内部,创建字符数组p并将数组内容初始化成"hello world"。问题也就出现在这里,由于是以数组的形式初始化。在函数执行结束后,函数会将空间还给操作系统,那么p数组的值便被销毁成随机值。此时虽然在test函数内得到了p数组首元素地址,但是地址处的内容已经被操作系统初始化成随机值了。这里打印的内容便是一串随机值组成的字符串。故本题错误的地方就是GetMemory函数内部是以数组的形式初始化字符串。如果以字符指针的形式来初始化字符串,那么该字符串将存放在内存中的只读常量区内,并不会随着函数栈帧销毁而销毁。

4.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。但是,这并不代表这段代码没有问题。首先Test内部调用GetMemory函数动态开辟内存空间。本次采取的是传址调用,所以,这里成功开辟了100字节空间。然后开心地进行了打印。可惜忘记了对于动态内存的释放,这会导致内存泄漏问题。这里应该free释放开辟的动态内存。

4.4试题四

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

解析:运行这段代码结果是world,这是一个经典的野指针问题。首先,动态申请了100字节空间并将起始地址赋给str指针。将hello字符串拷贝到str指向的空间中。然后,free释放str指针指向的内容。错误就错在没有在free后手动将str指针置为空指针。这导致了str变成野指针,对野指进行访问操作是一个及其危险的行为。所以我们应当在free释放动态内存后,将指针手动置空避免形成野指针。

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

C/C++程序的内存分配的几个区域:
1、栈区:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
2、堆区:一般由程序员分配如(malloc、calloc等)和释放(free), 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
3、数据段(静态区):存放全局变量、静态数据。程序结束后由系统释放。
4、代码段:存放函数体(类成员函数和全局函数)的二进制代码,常提及的常量字符串就是存放在这里。
在这里插入图片描述

6.柔型数组

也许你从未听过这个概念,下面让我简单介绍一下这个概念。该标准定义于C99标准。所以它在一些不兼容C99环境下可能无法使用。

柔型数组:指的是结构体中最后一个成员为未知大小的数组。但是在这个数组前至少还要有一个成员。否则将无法开辟起始的内存空间。

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

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

两种格式都是柔型数组的定义,具体是用哪一个得具体看编译器。因为在不同编译器下柔型数组的使用还是略有差别的。

6.1柔型数组的特点

#include<stdio.h>

struct S
{
	int i;
	char ch[];
}s;

int main()
{
	printf("%u\n",sizeof(struct s));
	return 0;
}

在这里插入图片描述

从上面代码可以看到,sizeof计算的结构大小是不包含柔型数组的。所以柔型数组前至少需要有一个结构体成员。包含柔性数组的结构体用动态内存开辟函数申请空间时,所申请的动态空间至少要大于成员体的大小,这样才能适应柔型数组。

6.2柔型数组的使用与优点

//代码1
#include<stdio.h>
#include<stdlib.h>

struct S
{
	int i;
	char ch[];
}s;


int main()
{
	char* pc=(char*)malloc(sizeof(s.i) + 10 * sizeof(char));
	if (NULL == pc)
	{
		return 1;
	}
	
	s.i = 20;

	for (int i = 0; i < 10; i++)
	{
		*(pc + i) = 'a';
	}

	for (int i = 0; i < 10; i++)
	{
		printf("%c ", *(pc + i));
	}
	printf("\n");

	free(pc);//释放
	pc = NULL;

	return 0;
}
//代码2
#include<stdio.h>
#include<stdlib.h>
struct S
{
	int i;
	char* pc;
}s;

int main()
{
	struct S* ps = (struct S*)malloc(sizeof(s));
	ps->i = 100;

	ps->pc = (char*)malloc(10 * sizeof(char));
	int j = 0;
	for (j = 0; j < 10; j++)
	{
		*(ps->pc + j) = 'a';
	}

	for (j = 0; j < 10; j++)
	{
		printf("%c ", *(ps->pc + j));
	}
	free(ps->pc);//释放
	s.pc = NULL;
	free(ps);//释放
	ps = NULL;
	return 0;
}

上述代码1和代码2的功能完全一致。但是代码1的优势有以下两点:
1、动态内存管理操作量较少,不容易出错。人难免出错,1次动态内存申请和一次动态内存释放可以有效减少失误概率,也方便掌控。
2、柔性数组可以有效减少内存碎片,使得内存的利用效率更加的高。
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/357611.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

K8S集群将Docker切换到Containerd

文章目录1. 开启节点维护1.1 将节点设置成不可调度1.2 驱逐节点上的 Pod1.3 停止相关服务2. 升级到 containerd2.1 安装 containerd2.2 调整 containerd 配置2.3 修改 kubelet 启动配置参数3. 重启节点服务4. 验证升级后的节点5. 容器管理工具5.1 容器管理命令行工具对比5.2 cr…

【JavaScript】ES6模块化与异步编程高级用法

一、ES6模块化 1、回顾&#xff1a;node.js遵循了ComminJS的模块化规范。 其中&#xff1a; 导入其他模块使用require()方法模块对外共享成员使用module.exports对象 模块化的好处&#xff1a; 遵守同样的模块化规范写代码&#xff0c;降低了沟通成本&#xff0c;极大方便了…

使用 ApiPost进行WebService 调试,就是这样简单

文章目录5.1 新建一个 HTTP 接口5.2 添加请求Body5.3 设置请求Header5.4 发送请求Apipost 可以用于调试 WebService 请求。具体步骤如下&#xff1a;5.1 新建一个 HTTP 接口 新建一个 HTTP接口&#xff0c;URL 部分填写 endpoint&#xff0c;请求方式选择 POST。以下 URL 为示…

SpringBoot自动装配的原理

前言 在开发SpringBoot项目时&#xff0c;当我们引入spring-boot-starter-xxx依赖后&#xff0c;想要使用依赖中的bean&#xff0c;直接就用Autowired拿来用了&#xff0c;不需要用xml或者注解的方式把它先注入到Spring容器中。这就是自动装配的特性&#xff0c;本文来讲述Spri…

Spring Bean循环依赖

解决SpringBean循环依赖为什么需要3级缓存&#xff1f;回答&#xff1a;1级Map保存单例bean。2级Map 为了保证产生循环引用问题时&#xff0c;每次查询早期引用对象&#xff0c;都拿到同一个对象。3级Map保存ObjectFactory对象。数据结构1级Map singletonObjects2级Map earlySi…

CMake option选项使用方式及注意事项

CMAKE官网 &#x1f358; 在复习 CMake 的时候&#xff0c;使用了 option 功能&#xff0c;发现修改了参数的值之后&#xff0c;和未修改的效果一样&#xff0c;然后不断的查找 option 的使用方法&#xff0c;最后发现并非 option 使用方式而错误&#xff0c;而是 option 第一…

SpringCloudAlibaba-分布式事务Seata

一、介绍官网&#xff1a;http://seata.io/zh-cn/index.html TC (Transaction Coordinator) - 事务协调者维护全局和分支事务的状态&#xff0c;驱动全局事务提交或回滚。TM (Transaction Manager) - 事务管理器定义全局事务的范围&#xff1a;开始全局事务、提交或回滚全局事务…

Mac Appium iOS自动化测试环境搭建教程

目录Appium环境搭建Mac iOS环境搭建Appium基础Appium进阶环境搭建安装brewCopyruby -e "$(curl -fsSL https://raw.github.com/mxcl/homebrew/go)"安装javaCopybrew install java安装python3 及相关包Copybrew install python3 pip install selenium pip install app…

实现8086虚拟机(四)——mov 和 jmp 指令解码

文章目录mov 指令解码jmp 指令解码这篇文章举例来讲讲 mov 指令和 jmp 指令解码函数的实现&#xff0c;其他的指令解码函数都与这些类似。mov 指令解码 以 mov 指令中的一类&#xff1a;寄存器/内存 到/从 寄存器&#xff0c;来详细说明解码函数的实现。 机器指令格式如下&am…

联想M7268激光打印机开机红绿灯双闪报错不打印

故障现象: 一台联想M7268激光打印机开机后电源键、复印键一起双闪,电源键闪红灯、复印键闪绿灯; 检测维修: 根据闪灯故障判断如果无卡纸异常情况下可能是激光器故障,因为以前曾经维修过一台一模一样的机器故障基本相同,先打开机器吧,把硒鼓拿出来先看看有没有卡纸,进纸…

php小程序餐馆点餐订餐外卖系统

目录 1 绪论 1 1.1课题背景 1 1.2课题研究现状 1 1.3初步设计方法与实施方案 2 1.4本文研究内容 2 2 系统开发环境 4 2.2MyEclipse环境配置 4 2.3 B/S结构简介 4 2.4MySQL数据库 5 3 系统分析 6 3.1系统可行性分析 6 3.1.1经济可行性 6 3.1.2技术可行性 6 3.1.3运行可行性 6 …

c++11 标准模板(STL)(std::unordered_set)(二)

定义于头文件 <unordered_set> template< class Key, class Hash std::hash<Key>, class KeyEqual std::equal_to<Key>, class Allocator std::allocator<Key> > class unordered_set;(1)(C11 起)namespace pmr { templ…

python中的for循环以及枚举函数enumerate()

一、可迭代的对象&#xff08;iteratle_object&#xff09; python中可以使用for循环进行迭代的对象大致有以下几种类型&#xff1a; String(字符串)List(列表)Tuple(元组)Dictionary(字典)range()内置函数返回的对象 二、for循环迭代示例 1. 依次输出字符串"python&q…

printk浅析

内核printk原理介绍 - 知乎 (zhihu.com)34.Linux-printk分析、使用prink调试驱动 (bbsmax.com)【原创】计算机自制操作系统(Linux篇)五&#xff1a;内核开发之万丈高楼从地起---printk(理清pintf/vprintf&#xff1b;sprintf/vsprintf &#xff1b;fprintf/vfprintf) - 知乎 (z…

自抗扰控制ADRC之扩张观测器

目录 前言 1. 被控对象(被观测对象) 2.非线性观测器 2.1仿真分析 2.2仿真模型 2.3仿真结果 3.线性观测器 3.1仿真模型 3.2仿真结果 4.总结和学习问题 前言 什么叫观测器&#xff1f;为什么该类观测称为扩张观测器&#xff1f; &#xff1a;观测器可以理解为所观测…

组合数学原理与例题

目录 一、前言 二、计数原理 1、加法原理 2、分割立方体&#xff08;lanqiaoOJ题号1620&#xff09; 3、乘法原理 4、挑选子串&#xff08;lanqiaoOJ题号1621&#xff09; 5、糊涂人寄信&#xff08;lanqiaoOJ题号1622&#xff09; 6、战斗吧N皇后&#xff08;lanqiaoO…

依次判断数组1对中的每个元素是否小于等于数组2中对应位置的每个元素numpy.less_equal()

【小白从小学Python、C、Java】【计算机等级考试500强双证书】 【Python-数据分析】 依次判断数组1对中的每个元素是否 小于等于数组2中对应位置的每个元素 numpy.less_equal() [太阳]选择题 以下错误的一项是? import numpy as np a np.array([1,2,3]) b np.array([1,3,2]) …

kubernetes 核心技术-Pod(1)

概述&#xff1a; 首先要知道 Pod 不是容器&#xff01; 一、 基本概念 Pod 是 k8s 系统中可以创建和管理的最小单元。k8s 不会直接处理容器&#xff0c;而是podpod 包含多个容器(一组容器的集合)一个pod中容器共享网络命名空间pod是短暂的(生命周期) 二、Pod存在的意义 创建…

数据结构与算法总结整理(超级全的哦!)

数据结构与算法基础大O表示法时间复杂度大O表示法时间复杂度排序&#xff1a;最坏时间复杂度时间复杂度的几条基本计算规则内存工作原理什么是内存内存主要分为三种存储器随机存储器&#xff08;RAM&#xff09;只读存储器&#xff08;ROM&#xff09;高速缓存&#xff08;Cach…

玄子Share-BCSP助学手册-JAVA开发

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b2gPyAnt-1676810001349)(./assets/%E7%8E%84%E5%AD%90Share%E4%B8%89%E7%89%88.jpg)] 玄子Share-BCSP助学手册-JAVA开发 前言&#xff1a; 此文为玄子&#xff0c;复习BCSP一二期后整理的文章&#x…