【C语言——动态内存管理】

一.为什么要有动态内存分配

通过前面的学习我们已经掌握了使用变量和数组来进行内存的开辟

上面所说的这两种内存的开辟方式有两个特点:

  • 空间开辟的大小是固定的。
  • 数组在生命的时候,必须指定数组的长度,数组空间一旦确定了大小就不能再调整。

但是对于空间的的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才知道,那数组编译时开辟空间的方式就不能满足了。

C语言中引入了动态内存开辟,让程序员自己可以申请和释放空间,就比较灵活了。

二.malloc和free

1.malloc

void *malloc(size_t size)

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。

  • 如果开辟成功,则返回一个指向开辟好空间的指针。
  • 如果开辟失败,则返回一个 NULL 指针,因此malloc的返回值一定要检查。
  • 返回值的类型是void*,所以malloc函数并不知道开辟的空间类型,具体在使用的时候使用者自己来决定。
  • 如果参数size_t 为0,malloc的行为标准是未定义的,取决于编译器。

 2.free

该函数是专门用来做动态内存的释放和回收的。

(用来释放动态开辟的内存)

void  free(void*ptr)

  • 如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。
  • 如果ptr的参数是NULL指针,则函数什么事都不做。

注:malloc和free函数都在stdlib.h这个头文件中。

eg:

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int num = 0;
	scanf("%d", num);
	int arr[num] = { 0 };
	int* ptr = NULL;
	ptr = (int*)malloc(num * sizeof(int));
	if (ptr != NULL)
	{
		for (int i = 0; i < num; i++)
		{
			*(ptr + i) = 0;
		}
	}
	free(ptr);//释放ptr所指向的动态内存
	ptr = NULL;//该步骤是有必要的
	return 0;
}

三.calloc和realloc

1.calloc

也是用来动态内存分配的。

void *calloc(size_t num,size_t size)

  • 函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0.
  • 与函数malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为权0.

eg:

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	if (p != NULL)
	{
		for (int i = 0; i < 10; i++)
		{
			printf("%d ", *(p + i));
		}
	}
	free(p);
	p = NULL;
	return 0;
}

所以我们如果要对申请的内存空间的内容初始化,那么可以考虑calloc函数。

2. realloc(涉及动态内存增容)

  • 该函数的出现让动态内存管理更加灵活。
  • 有时候我们会发现过去申请的空间太小了,有时候会觉得过去申请的空间太大了,那么为了合理的使用内存,我们一定会对内存的大小做灵活的调整。

void *realloc(void* ptr,size_t size)

  • ptr是函数的内存地址。
  • 返回值为调整后的内存起始位置。
  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
  • realloc在调整内存空间的是存在两种情况:

(1.)原有的空间之后有足够大的空间。

此时要扩展内存就直接在原有的内存之后追加空间,原来的空间的数据不发生变化。

(2.)原有的空间之后没有足够大的空间。

此时的空间扩展方法是:在堆空间上另找一个合适大小的的连续空间来使用。这样函数返回的是一个新的地址。

eg:

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* ptr = (int*)malloc(100);
	if (ptr == NULL)
	{
		//业务处理
	}
	else
	{
		return 1;
	}
	//扩展容量
	ptr = (int*)realloc(ptr, 1000);//这样写的不适合
	int* p = NULL;//应将realloc的函数的函数返回值放在p中,不为空再放到ptr中
	p = (int*)realloc(ptr, 1000);
	if (p != NULL)
	{
		ptr = p;
	}
	//业务处理
	free(ptr);
	ptr = NULL;
	return 0;
}

PS:忘记释放不再使用的动态内存开辟的空间会造成内存泄漏。

切记,动态内存开辟的空间一定要正确释放!!!

对动态内存的越界访问是一种编程错误,它可以导致程序的不可预测行为和安全漏洞。以下是几个原因:

  1. 内存越界访问可能会覆盖其他变量或数据,导致数据错误或损坏。这样的错误很难调试和修复。

  2. 越界访问可能会导致程序崩溃或产生异常。这会影响程序的稳定性和可靠性。

  3. 越界访问可能会导致程序的安全漏洞,如缓冲区溢出。攻击者可以利用这些漏洞来执行恶意代码或篡改数据。

  4. 越界访问会违反编程语言的内存管理规则。动态内存分配和释放是由编程人员负责管理的,如果出现越界访问,可能导致内存泄漏或资源浪费。

因此,编程时应该避免对动态内存进行越界访问,以确保程序的正确性、稳定性和安全性。

补充:

动态内存创建二维数组的方法如下:

int** createArray(int rows, int cols) {
    int** arr = new int*[rows]; // 创建行指针数组
    for (int i = 0; i < rows; i++) {
        arr[i] = new int[cols]; // 创建每一行的列数组
    }
    return arr;
}

int main() {
    int rows = 3;
    int cols = 4;
    int** arr = createArray(rows, cols);

    // 使用 arr 访问二维数组元素
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            arr[i][j] = i * cols + j; // 赋值
            cout << arr[i][j] << " "; // 输出
        }
        cout << endl;
    }

    // 释放动态内存
    for (int i = 0; i < rows; i++) {
        delete[] arr[i]; // 释放每一行的列数组
    }
    delete[] arr; // 释放行指针数组

    return 0;
}

在这个例子中,createArray 函数接受行数和列数作为参数,创建一个二维数组,并返回指向该数组的指针。然后在 main 函数中,我们使用这个函数创建一个 3 行 4 列的二维数组,并对其进行赋值和输出操作。最后,我们需要手动释放动态分配的内存。

四.柔性数组

柔性数组是一种特殊的数据结构,它允许在数组的末尾动态添加元素。柔性数组通常用于解决数组长度不确定的问题。

在传统的数组中,数组的大小在创建时就需要确定,并且不能动态改变。但是柔性数组允许在创建数组时只指定部分空间,然后在需要的时候动态添加更多的元素。

柔性数组的实现原理是在数组的末尾预留一定的空间,用于存储新添加的元素。当需要添加元素时,可以通过重新分配内存的方式来扩展数组的长度,并将新的元素添加到预留的空间中。

柔性数组的优点是可以节省内存空间,因为它只分配实际需要的空间。而传统的数组在创建时需要分配固定大小的空间,可能造成内存的浪费。

柔性数组在实际应用中常用于动态增长的数据结构,如动态数组、链表等。它能够灵活地适应数据的变化,提供高效的内存管理。

C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员。

例如:

struct st_type
{
	int i;
	int a[0];//柔性数组成员
};

struct st_type
{
	int i;
	int a[];//柔性数组成员
};

柔性数组的特点:

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

例如:

#include <stdio.h>
typedef struct st_type
{
	int i;
	int a[0];
}type_a;
int main()
{
	printf("%d ", sizeof(type_a));//输出的结果为4
}

柔性数组的使用:

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

// 定义包含柔性数组的结构体
struct FlexArray {
  int length;
  int data[]; // 柔性数组
};

int main() {
  int size = 5;
  
  // 分配内存给结构体
  struct FlexArray* flexArray = malloc(sizeof(struct FlexArray) + size * sizeof(int));

  if (flexArray == NULL) {
    printf("Failed to allocate memory.\n");
    return 1;
  }

  flexArray->length = size;

  // 给柔性数组赋值
  for (int i = 0; i < size; i++) {
    flexArray->data[i] = i * 2;
  }

  // 输出柔性数组的值
  for (int i = 0; i < size; i++) {
    printf("%d ", flexArray->data[i]);
  }
  
  // 释放内存
  free(flexArray);

  return 0;
}
 

​

柔性数组的优势:

柔性数组是一种特殊的数据结构,它允许在数组的最后一个元素之后继续添加新的元素,而不需要修改数组的大小。柔性数组的优势主要体现在以下几个方面:

  1. 节省空间:由于柔性数组的大小可以根据需要动态增长,因此可以节省内存空间。在使用传统的固定大小数组时,如果数组大小预设过大,就会浪费大量的内存空间;如果数组大小预设过小,可能会导致数组溢出。柔性数组可以根据实际需要动态分配内存,避免了这个问题。

  2. 简化操作:由于柔性数组可以在尾部追加新的元素,而不需要修改原有的元素位置,因此添加或删除元素的操作相对简单。而在传统的固定大小数组中,插入或删除元素需要移动其他元素,操作复杂度较高。

  3. 提高效率:柔性数组的动态分配内存操作相对高效,因为内存可以在堆上连续分配,无需重复开辟新的存储空间。而传统的固定大小数组,如果需要扩展大小,可能需要重新分配一块更大的内存空间,并将原有的元素复制到新的空间中,效率较低。

以下是一个使用柔性数组的代码示例:

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

typedef struct {
    int size; // 数组当前大小
    int capacity; // 数组总容量
    int* array; // 指向柔性数组的指针
} FlexArray;

FlexArray* createFlexArray(int capacity) {
    FlexArray* flexArray = malloc(sizeof(FlexArray));
    flexArray->size = 0;
    flexArray->capacity = capacity;
    flexArray->array = malloc(capacity * sizeof(int));
    return flexArray;
}

void append(FlexArray* flexArray, int value) {
    if (flexArray->size == flexArray->capacity) {
        // 若数组容量已满,重新分配内存空间
        flexArray->capacity *= 2;
        int* newArray = realloc(flexArray->array, flexArray->capacity * sizeof(int));
        if (newArray == NULL) {
            printf("内存分配失败!\n");
            return;
        }
        flexArray->array = newArray;
    }
    flexArray->array[flexArray->size++] = value;
}

void printArray(FlexArray* flexArray) {
    for (int i = 0; i < flexArray->size; i++) {
        printf("%d ", flexArray->array[i]);
    }
    printf("\n");
}

int main() {
    FlexArray* flexArray = createFlexArray(5);
    append(flexArray, 1);
    append(flexArray, 2);
    append(flexArray, 3);
    append(flexArray, 4);
    append(flexArray, 5);
    append(flexArray, 6);
    append(flexArray, 7);
    append(flexArray, 8);
    printArray(flexArray);
    free(flexArray->array);
    free(flexArray);
    return 0;
}

此代码示例展示了如何使用柔性数组来创建一个动态增长的数组。首先创建了一个 FlexArray 结构体,并初始化了数组的大小和容量,然后通过 createFlexArray 函数分配内存空间。append 函数用于向柔性数组追加新的元素,当数组容量不足时会动态分配更多的内存空间。printArray 函数用于打印数组中的所有元素。在 main 函数中,通过调用这些函数来演示柔性数组的使用。最后,释放内存空间,避免内存泄漏。

柔性数组的劣势: 

柔性数组(Flexible Array)是一种在内存中动态分配空间的数据结构,可以根据需要调整数组的大小。它的劣势包括以下几个方面:

  1. 内存浪费:由于柔性数组的大小是在运行时确定的,因此需要分配一定的额外空间来存储未使用的部分。这样会导致内存的浪费,在大规模应用中可能会成为一个问题。

  2. 不支持直接访问:由于柔性数组的大小是动态变化的,因此无法通过下标直接访问某个元素,而需要使用指针来进行访问。这增加了代码的复杂度,并且容易引入指针相关的错误。

  3. 可能导致内存碎片化:由于柔性数组的大小是动态变化的,当数组大小减小时,可能会导致内存产生碎片。这会影响内存的利用率,可能导致系统的性能下降。

代码示例:

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

struct FlexArray {
    int length;
    int arr[];  // 柔性数组
};

int main() {
    int size = 5;
    struct FlexArray* flexArray = malloc(sizeof(struct FlexArray) + sizeof(int) * size);

    flexArray->length = size;
    for (int i = 0; i < flexArray->length; i++) {
        flexArray->arr[i] = i;
    }

    for (int i = 0; i < flexArray->length; i++) {
        printf("%d ", flexArray->arr[i]);
    }

    free(flexArray);
    return 0;
}

在上述代码中,我们定义了一个包含柔性数组的结构体 FlexArray。使用 malloc 函数分配内存时,我们通过 sizeof 运算符计算了结构体的大小,并加上了柔性数组的大小。然后,我们可以通过指针访问柔性数组,并使用它存储数据。最后,务必记得使用 free 函数释放内存,以避免内存泄漏。

关注博主,优质内容不断更新!!!

如有错误,还望指出! 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

逆滤波器的推导与实现

设滤波器为&#xff0c;逆滤波器为 根据滤波器和逆滤波器的定义 对上式做傅里叶变换 对上式做逆傅里叶变换可得&#xff0c; 好了&#xff0c;逆滤波器的公式推导完了&#xff0c;但是实际计算时大多数时候这样是算不出来的&#xff0c;除非像扫频或粉噪这样的全频带信号才行&…

C盘越用越大?教你如何科学管理C盘空间

前言&#xff1a; 如图&#xff0c;左边是我多开的E5电脑&#xff0c;装的是LTSC2019_210707F多开封装版&#xff0c;C盘占用8.5GB&#xff0c;右边是我平常打游戏写代码的电脑&#xff0c;装的是Win11 22H2&#xff0c;C盘占用30GB。两台电脑都关闭了休眠&#xff0c;C盘的虚拟…

logisim 图解超前进位加法器原理解释

鄙人是视频作者&#xff0c;文件在视频简介的网盘链接。 找规律图解超前进位加法器与原理解释_哔哩哔哩_bilibili 一句话就是“把能导致进位到这个位置的情况全都穷举一遍。” 穷举情况看图中算式。 视频讲解比较啰嗦。

LLM padding left or right

参考博客&#xff1a; 大部分的大模型(LLM)采用左填充(left-padding)的原因 注&#xff1a;文章主要内容参考以上博客&#xff0c;及其评论区&#xff0c;如有侵权&#xff0c;联系删除。 最近在看大模型相关内容的时候&#xff0c;突然想到我实习时候一直一知半解的问题&…

经典网络解读—IResNet

论文&#xff1a;Improved Residual Networks for Image and Video Recognition&#xff08;2020.4&#xff09; 作者&#xff1a;Ionut Cosmin Duta, Li Liu, Fan Zhu, Ling Shao 链接&#xff1a;https://arxiv.org/abs/2004.04989 代码&#xff1a;https://github.com/iduta…

Ubuntu22.04.4 - 网络配置 - 笔记

一、设置固定ip 1、cd /etc/netplan 查看文件夹下的配置文件 我这里叫 00-installer-config.yaml 2、sudo nano /etc/netplan/00-installer-config.yaml 完成配置后&#xff0c;按下Ctrl O保存更改&#xff0c;然后按下Ctrl X退出nano编辑器。 3、sudo netplan apply 4、ip …

C++ 继承(一)

一、继承的概念 继承是面向对象编程中的一个重要概念&#xff0c;它指的是一个类&#xff08;子类&#xff09;可以从另一个类&#xff08;父类&#xff09;继承属性和方法。子类继承父类的属性和方法后&#xff0c;可以直接使用这些属性和方法&#xff0c;同时也可以在子类中…

springboot+vue全栈开发【2.前端准备工作篇】

目录 前言准备工作Vue框架介绍MVVM模式 快速入门导入vue在vscode创建一个页面 前言 hi&#xff0c;这个系列是我自学开发的笔记&#xff0c;适合具有一定编程基础&#xff08;html、css那些基础知识要会&#xff01;&#xff09;的同学&#xff0c;有问题及时指正&#xff01;…

语雀如何显示 Markdown 语法

正常的文章链接 https://www.yuque.com/TesterRoad/t554s28/eds3pfeffefw12x94wu8rwer8o 访问后是文章&#xff0c;无法复制 markdown 的内容 在链接后增加参数 /markdown?plaintrue&linebreakfalse&anchorfalse 直接显示代码

ros2 RVIZ2 不显示urdf模型

ros2 RVIZ2 不显示urdf模型 我的情况是 &#xff1a; 没有如何报错但是不显示 Description Topic 手动写上 /robot_description

python使用tkinter和ttkbootstrap制作UI界面(二)

这次讲解UI界面常用的主键&#xff0c;延续上文的框架进行编写&#xff0c;原界面如下&#xff1a; Combobox组件应用&#xff08;下拉框&#xff09; """Combobox组件"""global comvalue_operatorcomvalue_operator tk.StringVar()value_ope…

就业班 第三阶段(nginx) 2401--4.19 day3 nginx3

二、企业 keepalived 高可用项目实战 1、Keepalived VRRP 介绍 keepalived是什么keepalived是集群管理中保证集群高可用的一个服务软件&#xff0c;用来防止单点故障。 ​ keepalived工作原理keepalived是以VRRP协议为实现基础的&#xff0c;VRRP全称Virtual Router Redundan…

黑马python-python基础语法

1.注释&#xff1a; 单行注释&#xff1a;#注释内容 多行注释&#xff1a; """ 第一行 第二行 第三行 """ 或 第一行 第二行 第三行 2.定义变量 变量名值 变量名满足标识符命名规则即可 3.标识符命名规则&#xff1a; 有数组、字母、下划线组成…

欢乐钓鱼大师加速、暴击内置脚本,直接安装

无需手机root,安装软件即可使用&#xff0c;仅限安卓。 网盘自动获取 链接&#xff1a;https://pan.baidu.com/s/1lpzKPim76qettahxvxtjaQ?pwd0b8x 提取码&#xff1a;0b8x

从零开始学习Linux(4)----yum和vim

1.Linux软件包管理器yum Linux中我们要进行工具/指令/程序&#xff0c;安装&#xff0c;检查卸载等&#xff0c;需要yum的软件 安装软件的方式&#xff1a; 源代码安装---交叉编译的工具rpm包直接安装yum/apt-get yum是我们Linux预装的一个指令&#xff0c;搜索&#xff0c;下…

claude3国内注册

claude3国内注册 Claude 3 作为大型语言模型的强大之处在于其先进的算法设计和大规模训练数据的应用&#xff0c;能够执行复杂和多样化的任务。以下是 Claude 3 主要的强项&#xff1a; 接近人类的理解能力&#xff1a;Claude 3 能够更加深入地理解文本的含义&#xff0c;包括…

外贸企业邮箱有什么用?如何选择适合的外贸企业邮箱?

外贸公司每天都需要与各个国家的客户打交道&#xff0c;通过邮箱聊天、谈合作。由于语言、文化差异&#xff0c;一个小错误可能会致使业务失败和数据泄漏风险。做为外贸企业的重要沟通工具&#xff0c;企业电子邮件的功效是显而易见的。那样&#xff0c;外贸企业邮箱有什么用&a…

【在本机上部署安装禅道详细操作步骤2024】

1、进入禅道官网&#xff0c;选择开源版进行下载&#xff1a;禅道下载 - 禅道开源项目管理软件 2、根据自身电脑环境选择合适的版本&#xff0c;此处是windows版本&#xff1a; 3、双击打开下载好的.exe安装包-选择安装目录-【Extract】-然后就等着安装完成就行了 4、安装完成…

JavaSE高阶篇-反射

第一部分、Junit单元测试 1&#xff09;介绍 1.概述:Junit是一个单元测试框架,在一定程度上可以代替main方法,可以单独去执行一个方法,测试该方法是否能跑通,但是Junit是第三方工具,所以使用之前需要导入jar包 2&#xff09;Junit的基本使用&#xff08;重点啊&#xff09; 1.…

【jinja2】模板渲染

HTML文件 return render_template(index.html)h1: 一级标题 变粗变大(狗头 <