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

news2024/11/15 20:01:07

📝个人主页🌹: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.coloradmin.cn/o/2144019.html

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

相关文章

数据结构-链式二叉树-四种遍历

博客主页&#xff1a;【夜泉_ly】 本文专栏&#xff1a;【数据结构】 欢迎点赞&#x1f44d;收藏⭐关注❤️ 数据结构-链式二叉树-四种遍历 1.前言2.前、中、后序遍历2.1前序遍历2.1中、后序遍历 3.层序遍历3.1递归实现3.2队列实现关于在Pop之后为什么还能用tmp访问节点&#x…

Docker学习笔记(四)单主机网络

简介 Docker从容器中抽象除出了底层的主机连接网络&#xff0c;使得程序不用关心运行时的环境。连接到Docker网络的容器将获得唯一的地址&#xff0c;其他连接到同一Docker网络的容器也可以根据该IP找到目标容器并发送消息。   但是容器内运行的软件没法方便的确定主机IP地址…

第二期: 第二节 , 裸机编程 , gpio

1 首先就是 看原理图&#xff1a; 这里有两个 &#xff2c;&#xff25;&#xff24; 核心板的原理图。 可以看到 是这个脚。 &#xff12; 然后就是 查看数据手册。 从 数据手册可以看出 &#xff0c;一共有这么多的 gpio 组&#xff0c; 但是这些 组 是有复用的&#xf…

非常实用的桌面日历 你桌面上的备忘录和提醒工具

在快节奏的现代生活中&#xff0c;时间管理成为了每个人不可或缺的技能。随着数字化时代的到来&#xff0c;我们虽然拥有了智能手机、平板电脑以及各类时间管理应用&#xff0c;但那份传统而温馨的桌面日历&#xff0c;却依然以其独特的魅力&#xff0c;在无数人的工作台上占据…

油耳拿什么清理比较好?好用的无线可视挖耳勺推荐

油耳的朋友通常都是用棉签来掏耳。这种方式是很不安全的。因为使用棉签戳破耳道和棉絮掉落在耳道中而引起感染的新闻不在少数。在使用过程中更加建议大家可视挖耳勺来清理会更好。不仅清晰度得干净而且安全会更高。但最近这几年我发现可视挖耳勺市面上不合格产品很多&#xff0…

澳元/美元价格:进一步上涨看向美联储

澳元/美元在0.6700关口附近波动不定。美元因美国经济数据强劲而重新获得上行动力。接下来&#xff0c;澳大利亚将公布西太平洋领先指数。 美元的再度走强使风险敏感资产承压&#xff0c;澳元/美元周二维持在0.6700关口上方的小幅区间内。尽管美元反弹&#xff0c;澳元仍成功维…

关于STM32项目面试题02:ADC与DAC篇(输入部分NTC、AV:0-5V、AI:4-20mA和DAC的两个引脚)

博客的风格是&#xff1a;答案一定不能在问题的后面&#xff0c;要自己想、自己背&#xff1b;回答都是最精简、最精简、最精简&#xff0c;可能就几个字&#xff0c;你要自己自信的展开。 面试官01&#xff1a;什么是模数转换/ADC&#xff1f;说说模数转换的流程&#xff1f; …

STM32F407 - 01

嵌入式概述 什么是嵌入式?嵌入式是以应用为中心,以计算机技术为基础 硬件可裁剪 适用于对体积 可靠性 功耗 性能等方面有着严格的专用计算机系统 简单来说 除了处理桌面PC和服务器之外所有的控制类设备都是嵌入式. 通用计算机和专用计算机的区别 两者的区别在于技术的发展和…

Python编码系列—Python组合模式:构建灵活的对象组合

&#x1f31f;&#x1f31f; 欢迎来到我的技术小筑&#xff0c;一个专为技术探索者打造的交流空间。在这里&#xff0c;我们不仅分享代码的智慧&#xff0c;还探讨技术的深度与广度。无论您是资深开发者还是技术新手&#xff0c;这里都有一片属于您的天空。让我们在知识的海洋中…

质量与数量的博弈!大模型数据建设

质量与数量的博弈&#xff01;大模型数据建设 前言大模型数据建设 前言 大数据和人工智能&#xff08;DataAI&#xff09;技术正以惊人的速度改变着我们的生活和工作方式。大模型数据建设作为人工智能领域的核心组成部分&#xff0c;其重要性日益凸显。 大模型数据建设涉及到海…

硬件工程师笔试面试——电机

目录 18、电机 18.1 基础 电机原理图 电机实物图 18.1.1 概念 18.1.2 电机的一些基本分类和特点 18.2 相关问题 18.2.1 不同类型的电机在实际应用中有哪些具体的优势和劣势 18.2.2 在设计一个电机系统时,我应该如何考虑电机的选型和配置? 18.2.3 对于需要频繁启停的…

【C++篇】C++类与对象深度解析(三):类的默认成员函数详解

文章目录 【C篇】C类与对象深度解析&#xff08;三&#xff09;前言4. 运算符重载基本概念4.1 运算符重载的基本概念4.2 重载运算符的规则4.3 成员函数重载运算符4.4 运算符重载的优先级与结合性4.5 运算符重载中的限制与特殊情况4.5.1 不能创建新的操作符4.5.2 无法重载的运算…

TypeScript入门 (一)数据类型与运算符

引言 大家好&#xff0c;我是GISer Liu&#x1f601;&#xff0c;一名热爱AI技术的GIS开发者。本系列文章是我跟随DataWhale 2024年9月学习赛的TypeScript学习总结文档。希望通过我的知识点总结&#xff0c;能够帮助你更好地学习TypeScript。&#x1f495;&#x1f495;&#x…

【漏洞复现】科荣AIO UtilServlet 代码执行漏洞

免责声明&#xff1a; 本文内容旨在提供有关特定漏洞或安全漏洞的信息&#xff0c;以帮助用户更好地了解可能存在的风险。公布此类信息的目的在于促进网络安全意识和技术进步&#xff0c;并非出于任何恶意目的。阅读者应该明白&#xff0c;在利用本文提到的漏洞信息或进行相关测…

学习实践: Procdump获取本地远控信息

看到一篇文章是通过Procdump获取ToDesk远控信息的&#xff0c;于是实操了一下&#xff1b;顺便也测试了一下向日葵远控&#xff0c;发现都是适用的。该方法对于肉鸡提权有一定价值。 1、获取ToDesk远控信息 测试版本&#xff1a;ToDesk v4.7.4.3 测试工具&#xff1a;procdump、…

【Java EE】文件IO

Author&#xff1a;MTingle major:人工智能 --------------------------------------- Build your hopes like a tower! 目录 一、文件是什么&#xff1f; 二、针对文件系统操作的API 1.文件路径&#xff0c;文件名&#xff0c;文件是否存在 2. 创建文件 3.删除文件&#…

推动公平学习与身份归一化的视网膜神经疾病数据集

人工智能咨询培训老师叶梓 转载标明出处 在机器学习领域&#xff0c;公平性&#xff08;也称为公正性或平等性&#xff09;是一个日益受到关注的话题&#xff0c;它对于社会的福祉至关重要。然而&#xff0c;目前缺乏专门用于公平学习且包含成像数据的公共医学数据集&#xff…

vs2022配置opencv==4.9.0(C++)

1、下载opencv-windows [opencv4.9.0下载地址](Releases - OpenCV) 下载4.9.0版本&#xff1a; 双击下载好的opencv-4.9.0-windows.exe&#xff0c;选择安装路径&#xff1a; 2、配置opencv环境 &#xff08;1&#xff09;添加系统环境变量 此电脑–>右键“属性”–…

小红书治愈插画副业,猛猛涨粉上万+,每天只用5分钟

在这个数字化时代&#xff0c;AI技术正以前所未有的速度改变着我们的生活和工作。特别是在内容创作领域&#xff0c;AI的运用已经催生了一种全新的创作模式。一幅幅温馨可爱的治愈插画如同清流&#xff0c;不仅契合当下年轻人生活的状态&#xff0c;更成为许多人表达生活态度、…

「iOS」push与present

iOS学习 前言push与poppresent与dismiss使用dismiss弹出多级PresentedViewController 与 PresentingViewController区别 总结 前言 在此前就学习过视图的push与present。与之对应的退出方法为pop与dismiss。这里进行一次总结。 push与pop pushViewController 是通过导航控制器…