【C语言进阶】动态内存与柔性数组:C语言开发者必须知道的陷阱与技巧

📝个人主页🌹:Eternity._
⏩收录专栏⏪:C语言 “ 登神长阶 ”
🤡往期回顾🤡:C语言动态内存管理
🌹🌹期待您的关注 🌹🌹

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

❀C语言动态内存管理

  • 📒1. 常见的动态内存错误
    • 🏞️对NULL指针的解引用操作
    • ⛰️对动态开辟空间的越界访问
    • 🌄对非动态开辟内存使用free释放
    • 🍁使用free释放一块动态开辟内存的一部分
    • 🍂对同一块动态内存多次释放
    • 🌸动态开辟内存忘记释放(内存泄漏)
  • 📚2. 动态内存实战测试
  • 📜3. 柔性数组
    • 🌞特点
    • 🌙使用
    • ⭐优势
  • 📖4. 总结


前言:在C语言的广阔天地中,动态内存管理是一把双刃剑,它既为开发者提供了极大的灵活性和效率,也暗藏着诸多陷阱与挑战。作为C语言编程的基石之一,动态内存分配(如malloc、calloc、realloc等函数的使用)几乎贯穿于每一个复杂程序的设计与实现之中。然而,不恰当的内存管理实践往往会导致内存泄露、越界访问、重复释放等严重问题,进而影响程序的稳定性和安全性

柔性数组(也称为可变长数组或末尾数组)作为C99标准引入的一项特性,为开发者提供了一种在结构体中存储未知大小数据的有效方式。这一特性在处理字符串、动态数组等场景时尤为有用,但同样需要谨慎使用,以避免因误解其工作原理而引入新的问题

本文旨在深入探讨C语言中常见的动态内存错误及其成因,通过实例分析帮助读者理解这些错误的本质,并提供实用的解决方案。同时,本文还将详细介绍柔性数组的概念、工作原理及其在C语言编程中的应用,揭示其背后的设计哲学和潜在陷阱

让我们一同踏上这段探索之旅,揭开C语言动态内存管理与柔性数组的神秘面纱!


📒1. 常见的动态内存错误

在C语言中,动态内存分配是常见且强大的功能,但同时也容易引发各种错误,下面让我们来了解一下这些错误


🏞️对NULL指针的解引用操作

  • 错误描述: 当使用malloc、realloc或calloc等函数动态分配内存时,如果分配失败,这些函数会返回NULL指针。如果不对返回的指针进行检查,直接对其进行解引用操作,将会导致程序崩溃

错误代码示例 (C语言):

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

在这里插入图片描述


  • 解决方案: 在每次动态分配内存后,都应该检查返回的指针是否为NULL。如果是NULL,则表明内存分配失败,应进行相应的错误处理

解决方案示例 (C语言):

#define INT_MAX 0x3f3f3f3f
void test()
{int* p = (int*)malloc(INT_MAX * 4);if (p == NULL){perror("malloc fail");}else{*p = 20;}free(p);
}

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

  • 错误描述: 在动态分配的内存区域之外进行读写操作,即越界访问。这会导致未定义行为,可能破坏程序的稳定性和安全性

错误代码示例 (C语言):

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

在这里插入图片描述

  • 解决方案: 确保对动态分配的内存进行访问时,不要超出其分配的范围。可以通过设置合理的循环条件或使用数组索引来避免越界

解决方案示例 (C语言):

void test()
{int i = 0;int* p = (int*)malloc(10 * sizeof(int));if (NULL == p){exit(3);}for (i = 0; i < 10; i++){*(p + i) = i; //当i是10的时候越界访问,所以不要超出最大范围}free(p);
}

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

  • 错误描述: 尝试使用free函数释放非动态分配的内存,如栈上分配的内存或全局/静态变量。这会导致未定义行为,因为free函数只适用于通过malloc、realloc或calloc等函数动态分配的内存

错误代码示例 (C语言):

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

在这里插入图片描述

  • 解决方案: 确保只使用free函数释放动态分配的内存。对于栈上分配的内存或全局/静态变量,不需要也不应该使用free函数进行释放

解决方案示例 (C语言):

void test()
{int a = 10;int* p = &a;
}

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

  • 错误描述: 在动态分配的内存块中,只对其中一部分进行访问后,就尝试使用free函数释放整个内存块。然而,如果在访问过程中修改了指向内存块起始位置的指针,那么free函数将无法正确释放整个内存块

错误代码示例 (C语言):

void test()
{int* p = (int*)malloc(100);p++;free(p);//p不再指向动态内存的起始位置
}
  • 解决方案: 在调用free函数之前,确保指针仍然指向动态分配的内存块的起始位置。如果需要在内存块中移动指针,可以在调用free之前将指针重新指向起始位置,或者避免在需要释放内存之前修改指针

解决方案示例 (C语言):

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

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

  • 错误描述: 对同一块动态分配的内存进行多次free操作。这会导致未定义行为,因为一旦内存被释放,其对应的指针就变成了悬空指针(dangling pointer),再次对悬空指针进行free操作是危险的

错误代码示例 (C语言):

void test()
{int* p = (int*)malloc(100);free(p);free(p);//重复释放
}
  • 解决方案: 确保每块动态分配的内存只被释放一次。在释放内存后,将指针置为NULL,以避免再次对其进行释放操作

解决方案示例 (C语言):

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

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

  • 错误描述: 在程序中动态分配了内存,但在不再需要这些内存时忘记了释放它们。这会导致内存泄漏,即程序占用的内存量不断增加,最终可能导致系统资源耗尽

解决方案示例 (C语言):

void test()
{int* p = (int*)malloc(100);if (NULL != p){*p = 20;}
}
  • 解决方案: 在程序中及时释放不再需要的动态分配的内存。可以通过在适当的位置调用free函数来实现。同时,也要注意在程序结束前释放所有动态分配的内存,以避免内存泄漏

解决方案示例 (C语言):

void test()
{int* p = (int*)malloc(100);if (NULL != p){*p = 20;}free(p);
}

切记:动态开辟的空间一定要释放,并且正确释放


📚2. 动态内存实战测试

动态内存实战测试是确保你的C语言程序在处理动态内存时既安全又高效的重要手段,现在让我来带领你们巩固动态内存知识


请问运行Test 函数会有什么样的结果?

题目1:

#include <stdlib.h>  
#include <string.h>  void GetMemory(char* p)
{p = (char*)malloc(100);
}void Test(void)
{char* str = NULL;GetMemory(str);strcpy(str, "hello world");printf(str);
}

结果:程序崩溃,因为 str 是 NULL

存在问题:

  • 指针传递问题:
    GetMemory 函数中,p 是一个指向 char 的指针的局部变量。当你执行 p = (char *)malloc(100); 时,你实际上是在为 p 分配了一个新的内存地址,但这个新地址仅对 GetMemory 函数内的 p 指针有效。一旦GetMemory 函数返回,这个新的内存地址就会丢失,因为 GetMemory 函数是通过值传递接收的 str 指针(即 str 的一个拷贝),而 str 本身在 Test 函数中并未被修改
  • 内存泄漏:
    由于 GetMemory 中的 p 指针在函数返回后被销毁,但它指向的内存并没有被释放(即没有调用 free),这会导致内存泄漏
  • 未定义行为:
    在 Test 函数中,strcpy(str, “hello world”); 尝试将字符串 “hello world” 复制到 str 指向的地址。但由于 str 在 GetMemory 函数调用后仍然是 NULL,这个操作会尝试写入一个空指针,导致未定义行为

修改后代码 (C语言):

#include <stdlib.h>  
#include <string.h>  void GetMemory(char** p) 
{*p = (char*)malloc(100);
}void Test(void) {char* str = NULL;GetMemory(&str);if (str != NULL){strcpy(str, "hello world");printf(str);free(str); // 释放分配的内存  }
}

题目2:

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

结果:程序崩溃,因为 p在出了GetMemory函数之后,p占用的内存会自己释放,str就不确定了

存在问题:

作用域:

  • 局部数组 p 的生命周期仅限于 GetMemory 函数的执行期间。一旦 GetMemory 函数返回,p 数组所占用的内存就会被释放(在栈上),因此返回的指针将指向一个不再有效的内存区域

修改后代码 (C语言):

#include <stdlib.h>  char* GetMemory(void) {  // 使用 malloc 分配足够的内存来存储 "hello world" 字符串和结尾的空字符 '\0'  char* p = (char*)malloc(12); // "hello world" 加上 '\0' 共计 12 个字符  if (p != NULL) {  strcpy(p, "hello world"); // 将 "hello world" 复制到新分配的内存中  }  return p;  
}  void Test(void) {  char* str = GetMemory();  if (str != NULL) {  printf(str); // 正确使用 printf 格式化字符串  free(str); // 释放之前分配的内存  }  
}

题目3:

#include <stdlib.h>  void GetMemory(char** p, int num)
{*p = (char*)malloc(num);
}
void Test(void)
{char* str = NULL;GetMemory(&str, 100);strcpy(str, "hello");printf(str);
}

结果:程序虽然能正常运行,当时存在内存泄漏的问题

存在问题:

  • 由于未释放分配的内存,还存在内存泄漏的问题,应该在不再需要分配的内存时,使用 free 函数来释放它

修改后代码 (C语言):

#include <stdlib.h>  void GetMemory(char** p, int num)
{*p = (char*)malloc(num);
}void Test(void)  
{  char* str = NULL;  GetMemory(&str, 100);  if (str != NULL) {  strcpy(str, "hello");  printf(str);  free(str); // 释放内存  str = NULL; // 防止野指针  }  
}

题目4:

#include <stdlib.h>  void Test(void)
{char* str = (char*)malloc(100);strcpy(str, "hello");free(str);if (str != NULL){strcpy(str, "world");printf(str);}
}

结果:程序崩溃

存在问题:

  • 未定义行为:
    当执行 free(str); 后,str 指针的值(即内存地址)本身并没有改变,但它现在指向的内存块已经不再是您的程序可以安全访问的

修改后代码 (C语言):

#include <stdlib.h>  void Test(void)  
{  char* str = (char*)malloc(100);  if (str != NULL) {  strcpy(str, "hello");  printf("%s\n", str);  free(str);  str = NULL; // 防止野指针,但此时不应再使用str  }  // 注意:不要在这里或之后尝试使用str,因为它已经指向了无效的内存,// 如果想继续使用就必须重新分配内存  
}

📜3. 柔性数组

柔性数组(Flexible Array)是C语言中一种特殊的数据结构,它允许在结构体中定义一个长度可变的数组。这种技术为程序员提供了更灵活的内存管理方式,特别适用于那些需要在运行时确定数组大小的情况

定义与原理:

  • 柔性数组通常是在结构体的最后一个成员位置声明一个长度为0的数组(或称为柔性数组成员)。尽管数组的长度被声明为0,但它实际上并不占用任何内存空间,因为数组名本身不占空间,它只是一个偏移量。然而,这个数组的存在允许我们在结构体之后紧接着分配一块连续的内存区域,用于存储数组的实际数据。这样,结构体和数组就形成了一个连续的内存块,便于管理和释放

🌞特点

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

代码示例 (C++):

typedef struct st_type
{int i;int a[0];//柔性数组成员
}type_a;
printf("%d\n", sizeof(type_a));//输出的是4

🌙使用

代码示例 (C++):

typedef struct pxt
{int num;int a[0];//柔性数组成员
}pxt;int main()
{int i = 0;pxt* p = (pxt*)malloc(sizeof(pxt) + 100 * sizeof(int));//业务处理p->num = 100;for (i = 0; i < 100; i++){p->a[i] = i;}for (i = 0; i < 100; i++){printf("%d ", p[i]);}free(p);return 0;
}

这样柔性数组成员a,相当于获得了100个整型元素的连续空间


⭐优势

柔性数组也可以使用一下方法完成上面的业务,但是上面的方法优于下面这种,上述只需要做一次free就可以释放所有的内存,我们以学习的目的了解一下第二种方式

typedef struct pxt
{int num;int *p_a;//柔性数组成员
}pxt;int main()
{int i = 0;pxt* p = (pxt*)malloc(sizeof(pxt) + 100 * sizeof(int));//业务处理p->num = 100;p->p_a = (pxt*)malloc(p->num * sizeof(int));for (i = 0; i < 100; i++){p->p_a[i] = i;}for (i = 0; i < 100; i++){printf("%d ", p->p_a[i]);}free(p->p_a);p->p_a = NULL;free(p);p = NULL;return 0;
}

柔性数组的优点:

  • 灵活性: 允许在运行时动态确定数组的大小,满足不同的数据存储需求
  • 内存管理方便: 由于结构体和数组是连续分配的,因此可以一次性申请和释放内存,减少了内存碎片化的风险,提高了内存管理的效率
  • 设计简约: 简化了代码结构,提高了程序的可读性和可维护性

📖4. 总结

在深入探讨了C语言中常见的动态内存错误及柔性数组的应用后,我们不难发现,动态内存管理是C语言编程中不可或缺但又极具挑战性的一部分。它要求开发者不仅要有扎实的编程基础,还需要具备严谨的逻辑思维和细致入微的调试能力

我们了解了内存泄露、野指针、重复释放等动态内存错误的成因及防范策略,这些错误看似简单,实则可能对程序的稳定性和安全性造成严重影响。因此,在日常编程中,我们必须时刻保持警惕,遵循最佳实践,确保每一块分配的内存都能得到妥善管理

同时,柔性数组作为C99标准引入的一项实用特性,为我们提供了一种在结构体中灵活存储未知大小数据的方法。然而,柔性数组的使用也需谨慎,必须明确其工作原理和限制条件,避免误用或滥用导致的问题

总的来说,C语言的动态内存管理和柔性数组是相辅相成的两个概念。它们为开发者提供了强大的工具来构建高效、灵活的程序,但同时也要求开发者具备高度的责任感和严谨性。希望本文能够为读者在学习C语言动态内存管理和柔性数组的过程中提供一些有益的参考和启示,帮助大家更好地掌握这些关键技能,编写出更加稳定、安全、高效的C语言程序。让我们在未来的编程道路上继续探索、学习、进步!

在这里插入图片描述

希望本文能够为你提供有益的参考和启示,让我们一起在编程的道路上不断前行!
谢谢大家支持本篇到这里就结束了,祝大家天天开心!

在这里插入图片描述

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

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

相关文章

数据结构入门学习(全是干货)——树(中)

数据结构入门学习&#xff08;全是干货&#xff09;——树&#xff08;中&#xff09; 1 二叉搜索树&#xff08;Binary Search Tree&#xff0c;简称 BST&#xff09; 1.1 二叉搜索树及查找 二叉搜索树&#xff08;Binary Search Tree, BST&#xff09; 是一种特殊的二叉树…

【Delphi】实现接收系统拖动文件

在 Delphi 中&#xff0c;可以通过以下步骤来实现将文件夹中的文件拖动到 Form 上&#xff0c;并在拖动时显示文件类型的光标。我们可以利用 VCL 中的 Drag and Drop 机制来处理拖动操作&#xff0c;以及自定义光标显示。 以下是详细的步骤和代码示例&#xff1a; 实现步骤&a…

力扣之181.超过经理收入的员工

文章目录 1. 181.超过经理收入的员工1.1 题干1.2 准备数据1.3 题解1.4 结果截图 1. 181.超过经理收入的员工 1.1 题干 表&#xff1a;Employee -------------------- | Column Name | Type | -------------------- | id | int | | name | varchar | | salary | int | | mana…

在设计开发中,如何提高网站的用户体验?

在网站设计开发中&#xff0c;提高用户体验是至关重要的。良好的用户体验不仅能提升用户的满意度和忠诚度&#xff0c;还能增加转化率和用户留存率。以下是一些有效的方法和策略&#xff1a; 优化页面加载速度 减少HTTP请求&#xff1a;合并CSS和JavaScript文件以减少HTTP请求…

『 Linux 』HTTP(一)

文章目录 域名URLURLEncode和URLDecodeHTTP的请求HTTP的响应请求与响应的获取简单的Web服务器 域名 任何客户端在需要访问一个服务端时都需要一个IP和端口号,而当一个浏览器去访问一个网页时通常更多使用的是域名而不是IP:port的方式, www.baidu.com这是百度的域名; 实际上当浏…

MySQL高阶1777-每家商店的产品价格

题目 找出每种产品在各个商店中的价格。 可以以 任何顺序 输出结果。 准备数据 create database csdn; use csdn;Create table If Not Exists Products (product_id int, store ENUM(store1, store2, store3), price int); Truncate table Products; insert into Products …

音视频入门基础:AAC专题(8)——FFmpeg源码中计算AAC裸流AVStream的time_base的实现

一、引言 本文讲解FFmpeg源码对AAC裸流行解复用&#xff08;解封装&#xff09;时&#xff0c;其AVStream的time_base是怎样被计算出来的。 二、FFmpeg源码中计算AAC裸流AVStream的time_base的实现 FFmpeg对AAC裸流进行解复用&#xff08;解封装&#xff09;时&#xff0c;其…

通过FUXA在ARMxy边缘计算网关上实现生产优化

在当今工业4.0时代&#xff0c;智能制造的需求日益增长&#xff0c;企业迫切需要通过数字化转型来提高生产效率、降低成本并增强市场竞争力。ARMxy系列的BL340工业级ARM控制器&#xff0c;凭借其强大的处理能力和灵活的配置选项&#xff0c;成为实现生产优化的重要基础。 一、…

io多路复用:epoll水平触发(LT)和边沿触发(ET)的区别和优缺点

在进行ET模式的正式分析之前&#xff0c;我们来举个例子简单地了解下ET和LT: 假设我们通过fork函数创建了父子两个进程&#xff0c;并通过匿名管道来通信&#xff0c;在子进程中&#xff0c;我们一次向管道写入10个字符数据&#xff0c;为"aaaa\nbbbb\n";每隔5s写入…

SmartX 分布式存储产品全新升级,支持文件存储能力与纠删码机制

近日&#xff0c;SmartX 正式发布了 SMTX ZBS 5.6 版本&#xff0c;通过引入对文件存储的支持能力&#xff0c;可作为企业统一存储平台&#xff0c;为大规模虚拟化、私有云、容器等环境提供高可靠、高可用、高性能、易扩展的企业级分布式块存储和分布式文件存储服务。该版本还引…

2024 年至今回顾:The Sandbox 创作者的历程及下一步展望

2024 年上半年是 The Sandbox 令人振奋的旅程&#xff01;从激动人心的里程碑、丰厚的奖励到创新的功能&#xff0c;我们见证了来自充满活力的社区的惊人创造力。 作为平台的生命线&#xff0c;我们致力于帮助创作者发光发热。让我们深入了解过去六个月中最激动人心的时刻和更…

OpenHarmony(鸿蒙南向开发)——标准系统方案之瑞芯微RK3566移植案例(下)

往期知识点记录&#xff1a; 鸿蒙&#xff08;HarmonyOS&#xff09;应用层开发&#xff08;北向&#xff09;知识点汇总 鸿蒙&#xff08;OpenHarmony&#xff09;南向开发保姆级知识点汇总~ OpenHarmony&#xff08;鸿蒙南向开发&#xff09;——轻量系统STM32F407芯片移植案…

MySQL聚合统计和内置函数

【数据库】MySQL聚合统计 王笃笃-CSDN博客https://blog.csdn.net/wangduduniubi?typeblog显示平均工资低于2000的部门和它的平均工资 mysql> select deptno,avg(sal) deptavg from emp group by deptno; --------------------- | deptno | deptavg | --------------…

基于树表的查找

二叉排序树 相关概念 存储结构 查找 具体实现 算法分析 插入 创建 删除 无孩子 有一个孩子 有左右孩子 示例 平衡二叉树 概念 示例 插入调整 类型 由插入结点在失衡结点的位置来定&#xff1a;插入结点C为失衡结点A的左子树的左孩子&#xff0c;因此为LL型。 原则 即选择中…

10.第二阶段x86游戏实战2-反编译自己的程序加深堆栈的理解

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 本次游戏没法给 内容参考于&#xff1a;微尘网络安全 工具下载&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1rEEJnt85npn7N38Ai0_F2Q?pwd6tw3 提…

[C++进阶[六]]list的相关接口模拟实现

1.前言 本章重点 在list模拟实现的过程中&#xff0c;主要是感受list的迭代器的相关实现&#xff0c;这是本节的重点和难点。 2.list接口的大致框架 list是一个双向循环链表&#xff0c;所以在实现list之前&#xff0c;要先构建一个节点类 template <class T> struct L…

Java 中 List 常用类和数据结构详解及案例示范

1. 引言 在 Java 开发中&#xff0c;List 是最常用的数据结构接口之一&#xff0c;它用于存储有序的元素集合&#xff0c;并允许通过索引进行随机访问。电商系统中&#xff0c;如购物车、订单列表和商品目录等功能都依赖 List 进行数据管理。选择适当的 List 实现类能够显著提…

C++STL~~priority_queue

文章目录 容器适配器一、priority_queue的概念二、priority_queue的使用三、priority_queue的练习四、仿函数五、总结 容器适配器 什么是适配器 适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结)&#xff0c;该种模式是将…

交流电力控制电路之交流调功电路、交流电力电子开关

目录 一、交流调功电路 二、交流电力电子开关 交流调压电路可看&#xff1a;交流调压电路 交流调压电路、交流调功电路和交流电力开关的异同点&#xff1a; 一、交流调功电路 交流调功电路用于调节电力设备的功率输出&#xff0c;通过改变电路中电压、电流的有效值&#xff…