C语言之动态内存分配与释放

news/2024/11/15 3:55:23/文章来源:https://www.cnblogs.com/Invinc-Z/p/18400002

C语言之动态内存分配与释放

通用指针类型void

通用类型指针具有以下特点:

类型无关,赋值灵活:由于指针本质上是一个存储内存地址的变量,而内存地址是没有类型的,所以void指针可以存储任意类型数据的地址,指向任意类型对象。无论是整数、浮点数、字符或数组、结构体等类型都可以用void指针指向。表现在代码中就是:可以将任意类型指针(地址)赋值给void指针,这个过程一般是没有风险的。如下列代码:

int a = 42;
void* void_ptr = &a;// void指针转换成其它类型指针,在C++语法中必须加上显式类型转换说明
// 但C语言支持void指针隐式类型转换成各种其它指针类型,所以这个强转语法可加可不加
// float* float_ptr = (float*)void_ptr;// 错误的类型转换
float* float_ptr = void_ptr;// 解引用产生未定义行为
printf("%f\n", *float_ptr);

关于通用指针类型转换成其它指针类型,C语言和C++在语法上会有明显差别:

  • C语言更灵活允许隐式的类型转换,所以强转语法可加可不加。

  • C++则不支持这类隐式类型转换,必须要加上强转的语法,否则编译无法通过。


在C语言中,想要在堆上动态分配内存空间,主要依赖三个函数来完成,它们都声明在头文件<stdlib.h>当中:

  1. malloc
  2. calloc
  3. realloc

内存分配函数malloc

函数全名:memory allocation

函数声明:void* malloc(size_t size);

函数作用:

  • 此函数会在堆空间上分配一片连续的,size个字节大小的内存块

  • 此函数不会对内存块中的数据进行初始化,内存块中的数据是随机未定义的。

函数返回值:

  • 如果分配成功,此函数会返回指向该内存块地址(首字节地址)的指针。注意返回的指针类型是void指针,在操作之前需要进行转换。

  • 如果分配失败,此函数会返回一个空指针(NULL)。

内存泄漏

以往我们创建数组,创建结构体都是在栈上完成的,它们是栈数组、栈结构体,它们的内存空间都是由栈自动管理完成的。

但使用动态内存分配函数创建的数组、结构体都被存储在堆上,栈上存储的只不过是它的指针,如下图所示:

栈上指针指向堆上内存区域-示意图

堆上存储的数据由程序员手动管理生命周期,手动申请内存资源,也需要手动释放内存空间。

内存泄漏是指程序在运行过程中,未能适时释放不再使用的内存区域,导致这部分内存在程序的生命周期内始终无法被重用。

内存泄漏在短时间内可能对程序而言,不是巨大的、致命的风险,但:长时间运行或频繁执行的程序中如果存在内存泄漏,随着时间的推移,被泄漏的内存累积会越来越多,最终可能导致程序运行缓慢甚至崩溃,特别是在内存有限的系统中。

内存释放函数free

为了避免内存泄漏,在确定动态分配的内存不再使用后,要及时调用free函数释放它。

函数声明:void free(void *ptr);

函数参数:必须是堆上申请内存块的地址(首字节地址),不能传递别的指针,否则会引发未定义行为。

函数功能:

  1. free函数并不会修改它所释放的内存区域中存储的任何数据。free 的作用仅仅是告诉操作系统这块内存不再被使用了,可以将其标记为可用状态,以供将来的内存分配请求使用。
  2. 释放后的内存区域中的数据一般仍然会继续存在,直到被下一次的内存分配操作覆盖。当然即便free前的原始数据一直存在未被覆盖,这片内存区域也不再可用了,因为你不知道什么时候数据就会被覆盖掉了。
  3. free函数不会修改传入指针指向的内容,更不会对实参指针本身做任何修改。

free函数调用后,指针指向的内存块就被释放了。但free函数不会改变传入的实参指针本身,所以free后的实参指针就变成了指向一片已释放区域的指针。这就是"悬空指针",悬空指针是野指针的一种特例,使用悬空指针同样会引发未定义行为。

为了避免悬空指针为程序安全带来隐患,推荐在free掉指针指向的内存块后,及时将指针置为空指针。

// 伪代码,p是一个指针类型
p = malloc(...);
free(p);
p = NULL;

总结:

  1. 正确传参free函数。free函数需要传入指向动态分配内存块首字节的指针,free之前不妨检查指针是否已被移动。

  2. 在free内存块后,建议立刻将指针设置为NULL。这样可以规避一些常见的问题:

    • 避免了"double free"的风险。对空指针调用free函数是安全的,它不会有任何效果。

    • 减少悬空指针出现的风险。解引用空指针导致程序崩溃,比悬空指针带来的未定义行为要更容易检测和修正。

  3. 慎重改变堆区指针的指向。指向堆区域的指针,如果需要改变它的指向,在改变之前应当考虑指向的内存块是否需要free。

  4. 多函数共同管理同一块内存区域时,应严格遵循单一原则。尤其是,哪个函数用于分配内存,哪个函数用于free释放内存,这两个函数一定要明确单一的职责。

// 在堆上动态分配内存拼接两个字符串
char* dynamic_strcat(const char* prefix, const char* suffix) {
// 计算拼接后字符串的长度int new_str_len = strlen(prefix) + strlen(suffix);
char *new_str = malloc(new_str_len + 1);    // char在各平台上长度都是1,所以不用乘了if (new_str == NULL) {printf("ERROR: malloc failed in dynamic_strcat!\n");exit(1);}
// 长度是精确计算得出的,不用担心越界访问strcat(strcpy(new_str, prefix), suffix);
return new_str;
}int main(void) {char str1[] = "hello";
char str2[] = " world!";char* result_str = dynamic_strcat(str1, str2);  // 注意只要涉及动态内存分配,一律用指针类型。这里不能用数组类型
puts(result_str);// 现在不再使用result字符串了,不要忘记free它
free(result_str);return 0;
}

清零内存分配函数calloc

函数全名:cleared allocation,该函数的最大特点是分配内存空间时会自动初始化0值

函数声明: void* calloc(size_t num, size_t size);

函数参数:

  1. num 表示要分配的元素数量
  2. size 表示每个元素的内存大小

此函数也会在堆空间上分配一片连续的内存空间,但不同的是,它基于元素的个数以及每个元素的大小来进行内存分配,所以calloc常用于在堆上分配数组的内存空间。

函数返回值:返回值在分配成功和失败时,和malloc是一致的。

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>int main(void) {// 分配一个长度为10的整数数组int len = 10;int *arr = calloc(len, sizeof(int));int* p = arr;for (int i = 0; i < len; i++){printf("%d\n", *p++);   // 此时数组中的元素都具有0值,而不是随机未定义的}// 使用完毕,不要忘记freefree(arr);return 0;
}

总得来说,推荐在动态分配数组内存空间时,尤其是需要将内存空间初始化为0值时,使用calloc函数

内存重分配函数realloc

函数全名:reallocation,表示内存重新分配。

函数声明:void* realloc(void* ptr, size_t new_size);

函数参数:

  1. ptr:指向原来已分配内存的内存块。
  2. new_size:新的内存块大小。

函数功能:

该函数根据参数取值的不同,可能表现为malloc或free函数的行为:

  1. 如果ptr指针是一个空指针,那么该函数的行为和malloc一致——分配new_size字节的内存空间,并且返回该内存块的指针。
  2. 如果new_size的取值为0,那么该函数的行为就是free函数,会释放ptr指向的内存块。

如果没有出现上述两种特殊情况,realloc用于重新调整已分配内存块的大小(也就是ptr指针指向的已分配内存块的大小):

  1. 当new_size的取值和已分配的内存块大小一致时,此函数不会做任何操作。
  2. 当new_size的取值比已分配的内存块小时,会在旧内存块的尾部(高地址)截断,被截断抛弃的内存块会被自动释放。
  3. 当new_size的取值比已分配的内存块大时(新内存块比旧内存块大时),会尽可能地尝试原地扩大旧内存块(这样效率高);
  4. 如果无法原地进行扩大,则会在别处申请空间分配new_size大小的新内存块,并将旧内存块中的数据全部复制进去后,将旧内存块自动释放
  5. 不管采用哪种方式扩展旧内存块,新扩展部分的内存区域都不会初始化,仍只具有随机值

函数返回值:

如果realloc函数分配内存空间成功,它会返回指向新内存块的指针,若失败,仍会返回空指针,且不会改变旧内存块。

总之,realloc函数适用于调整已分配内存块的大小,特别是在动态数组或数据结构的大小需要在程序运行时增加或减少时使用。

惯用法:

// p和arr_p指针类型一致
p = realloc(arr_p, new_size);
if (p == NULL){// 分配失败处理
}
// 代码运行到这里,realloc分配内存成功
arr_p = p;

这样写既避免了arr_p成为悬空指针,也不会因为realloc分配失败导致内存泄漏。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

windows下安装kubectl

下载kubectl的二进制文件 地址 https://github.com/kubernetes/kubernetes/tree/master/CHANGELOG 根据 k8s 集群的版本,下载对应的客户端 kubectl 工具 例如: 我的集群是1.30.1 找到 1.30的markdown文件然后找到1.30.1的客户端二进制文件然后根据自己硬件下载对应的压缩包 我…

R-Adapter:零样本模型微调新突破,提升鲁棒性与泛化能力 | ECCV 2024

大规模图像-文本预训练模型实现了零样本分类,并在不同数据分布下提供了一致的准确性。然而,这些模型在下游任务中通常需要微调优化,这会降低对于超出分布范围的数据的泛化能力,并需要大量的计算资源。论文提出新颖的Robust Adapter(R-Adapter),可以在微调零样本模型用于…

Kubernetes-etcd备份恢复

目录使用etcdctl备份与恢复简介集群信息etcdctl安装下载安装同步到其他节点配置环境变量查看集群状态查看所有key查看指定key备份所有节点创建备份目录备份etcd数据恢复删除资源所有master节点停止etcd所有master节点备份原有数据master01恢复master02恢复master03恢复所有节点…

使用 Android Lua Helper 在VSCODE中调试安卓Lua应用

一、VSCODE 与 Android Lua Helper 的功能特点 Visual Studio Code(VSCODE)是一款功能强大的代码编辑器,它以其高度可定制的界面、强大的扩展生态系统、流畅的性能表现以及对众多编程语言的天然支持而备受开发者青睐。在众多的开发场景中,VSCODE 都展现出了卓越的性能和灵活…

【git】No user exists for uid 1000 fatal: 无法读取远程仓库。

一、问题今天拉取代码时,突然报错 二、处理过程 1、检查用户信息 $ id -u1000用户的id是对的 那是因为什么呢2、重启终端 发现还是不行3、重启电脑 发现还是不行三、解决方案 最终重新生效环境变量 $ source ~/.bashrc然后重新执行拉取更新 $ git fetchremote: Enumerating ob…

最罕见的性格,在程序员里却最常见?它就是。。

当然,性格测试也不绝对准确,毕竟谁还没有一点儿 “逆反” 心理呢?大家好,我是程序员鱼皮。不知道大家有没有了解过 MBTI?这是目前特别流行的人格测试,可以帮助你更了解自己。 MBTI 将人格分为 4 个维度,每个维度有 2 个选项:比如在精力来源这点,就分为内倾和外倾(俗称…

etcd集群新增节点和移除节点

etcd集群新增节点和删除节点 现在的集群信息是: 新增节点1、下载etcd二进制包wget https://github.com/etcd-io/etcd/releases/download/v3.5.15/etcd-v3.5.15-linux-amd64.tar.gz2、创建etcd目录mkdir -p /data/etcd/{data,ssl,bin}3、解压并移动etcd命令至etcd目录tar zxf …

My SQL 列转行操作

原表结构如下,我们可以发现,“日运输量”和“车次”是在同一张表中相互独立的两个字段,即独立的两列数据,下面,我将系统中的测试数据以及代码全部放出来,以解释列转行的操作方法 原表数据库查询代码:1 SELECT 2 yzrq AS 运作日期, 3 DATE_FORMAT( yzrq, %Y-%m )…

VS中 调试逐语句和逐过程的区别

逐过程(F10)在遇到函数时,会把函数从整体上看做一条语句,不会进入函数内部;逐语句(F11)在遇到函数时,认为函数由多条语句构成,会进入函数内部。

使用 nuxi init 创建全新 Nuxt 项目

title: 使用 nuxi init 创建全新 Nuxt 项目 date: 2024/9/6 updated: 2024/9/6 author: cmdragon excerpt: 摘要:本文介绍了如何使用nuxi init命令创建全新的Nuxt.js项目,包括安装所需环境、命令使用方法、指定模板、强制克隆、启动开发服务器等步骤,并提供了完整的项目初…

KUnit:设备模拟重定向

设备模拟 有些驱动文件是需要device的,所以KUnit提供了一些设备模拟的方法,并且还提供了总线来管理设备的生命周期。 下面先以clock device模拟举例(drivers/clk/clk_test.c)首先用一个struct来模拟这个clk设备。其中clk_hw是clk的描述,rate相当于模拟设备的波特率寄存器s…

IDA 动态调试初步学习

题目 https://files.buuoj.cn/files/985826f5dda0d8665ed997a49321dd88/attachment.zip 1C这个值太小导致加密失败,所以考虑动态调试修改1C为更大的值选择调试F2下一些断点找到1C在内存中的位置F9开始调试先F7单步,观察右下角的Stack view,内存中出现1C先选中,然后按F2,然后修改…