【C语言】动态内存管理(C语言的难点与精华,数据结构的前置知识,你真的掌握了吗?)

文章目录

  • 引言
  • 一、为什么要动态内存分配
  • 二、动态内存分配的相关函数
    • 2.1 malloc
    • 2.2 free
    • 2.3 calloc
    • 2.4 realloc
  • 三、常见的动态内存的错误
    • 3.1 对NULL指针的解引用
    • 3.2 对动态内存越界访问
    • 3.3 对非动态内存释放
    • 3.4 对动态内存部分释放
    • 3.5 对动态内存多次释放
    • 3.6 未对动态内存释放(内存泄漏)
  • 四、动态内存经典笔试题分析
    • 4.1 题目一
    • 4.2 题目二
    • 4.3 题目三
    • 4.4 题目四
  • 五、柔性数组
    • 5.1 柔性数组的特点
    • 5.2 柔性数组的使用
  • 六、C/C++中程序内存区域划分

引言

学习专栏

《零基础学C语言》
《数据结构世界》

俗话说的好,要想学好数据结构(数据结构世界,对数据结构感兴趣的小伙伴可以移步),就必须学好以下三方面知识:

  1. 指针
    不允许你还不了解指针的那些事(一)(内存和地址+指针变量+指针运算+野指针+传址调用)
    不允许你还不了解指针的那些事(二)(数组传参的本质+冒泡排序+数组指针+指针数组)
  2. 结构体
    自定义类型:结构体(你真的掌握了内存对齐,位段吗?)
  3. 动态内存管理

前两方面的知识在往期已经详细讲解,今天我们就来学习最后一方面的知识——动态内存管理

一、为什么要动态内存分配

我们已经掌握的内存开辟方式有:

int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间

但是上述的开辟空间的方式有两个特点:

  • 空间开辟大小是固定的。
  • 数组在申明的时候,必须指定数组的长度,数组空间⼀旦确定了大小不能调整

所以,为了能在程序运行中,根据需求灵活地调整空间大小,C语言引入了动态内存管理。

二、动态内存分配的相关函数

2.1 malloc

C语言提供了一个动态内存开辟的函数:

void* malloc (size_t size);

这个函数可以开辟一块连续的内存空间,并返回指向这块空间的指针

  • 如果开辟成功,则返回指向开辟好空间的指针。
  • 如果开辟失败,则返回NULL 指针,因此malloc的返回值一定要做检查。
  • 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
int main()
{
	//动态开辟10个整型空间
	int* a = (int*)malloc(10 * sizeof(int));
	if (a == NULL)//开辟不成功,返回NULL
	{
		perror("malloc fail");
		return 1;
	}
	//开辟成功,使用该空间
	//...
	return 0;
}

注意:malloc(0)——开辟0个字节空间,是标准未定义行为,具体取决于编译器

2.2 free

C语言提供了另外一个函数free,可以动态内存的释放和回收:

void free (void* ptr);

free函数用来释放动态开辟的内存

  • 如果参数 ptr 是指向动态内存空间的指针,则释放该空间
  • 如果参数 ptr 是NULL指针,则不作任何处理。
int main()
{
	//动态开辟10个整型空间
	int* a = (int*)malloc(10 * sizeof(int));
	if (a == NULL)//开辟不成功,返回NULL
	{
		perror("malloc fail");
		return 1;
	}
	//开辟成功,使用该空间
	//使用完毕,释放该空间
	free(a);
	a = NULL;
	return 0;
}

注意:如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的

2.3 calloc

C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。

void* calloc (size_t num, size_t size);

其实,它与malloc很相似,区别在于calloc会将开辟的连续空间,每个字节都初始化为0

int main()
{
	//动态开辟10个整型空间并初始化为0
	int* a = (int*)calloc(10, sizeof(int));
	if (a == NULL)//开辟不成功,返回NULL
	{
		perror("calloc fail");
		return 1;
	}
	//开辟成功,使用该空间
	//使用完毕,释放该空间
	free(a);
	a = NULL;
	return 0;
}

2.4 realloc

realloc函数的出现让动态内存管理更加灵活。
为了合理的运用内存,所以在开辟空间后觉得太大或太小,就可以使用realloc进行调整。

void* realloc (void* ptr, size_t size);

realloc调整成功,会返回调整后指向这块空间的指针

  • ptr 是调整前空间的指针
  • size 是调整后的新大小
  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。

realloc在调整内存空间的是存在两种情况:

  • 情况1:原有空间之后有足够大的空间

这个时候,realloc会直接在原有空间后面直接追加开辟。

  • 情况2:原有空间之后没有足够大的空间

这个时候,realloc就会再找一块足够大空间,一次性开辟完,再将原数据拷贝过去。

int main()
{
	//动态开辟10个整型空间
	int* a = (int*)malloc(10 * sizeof(int));
	if (a == NULL)//开辟不成功,返回NULL
	{
		perror("malloc fail");
		return 1;
	}
	//调整空间,扩大为20个整型空间
	int* tmp = (int*)realloc(a, 20 * sizeof(int));
	if (tmp == NULL)//开辟不成功,返回NULL
	{
		perror("realloc fail");
		return 1;
	}
	//调整成功,将空间地址给a
	a = tmp;
	//使用空间...
	free(a);
	a = NULL;
	return 0;
}

注意

  • 用临时变量tmp来接受realloc调整后的空间地址
  • 因为如果用a接收,万一调整失败返回NULL,还会丢掉原本空间的地址

三、常见的动态内存的错误

3.1 对NULL指针的解引用

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

3.2 对动态内存越界访问

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

3.3 对非动态内存释放

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

3.4 对动态内存部分释放

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

3.5 对动态内存多次释放

void test()
{
	int *p = (int *)malloc(100);
	free(p);
	free(p);//重复释放
}

3.6 未对动态内存释放(内存泄漏)

因为程序终止会回收所有空间,所以这里用死循环来模拟程序运行中不释放动态内存,会导致内存泄漏。

int main()
{
	int* a = (int*)malloc(10 * sizeof(int));
	while(1);
	return 0;
}

总结动态开辟的内存一定要释放,并且要正确释放

四、动态内存经典笔试题分析

请先自行思考哦,不要立马看解析~

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

4.1 题目一

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

结果:

  • 程序崩溃,内存泄漏

解析:

  • GetMemory函数中,动态开辟了100个字节空间。但是p是一个形参,形参是实参的一份临时拷贝,对形参的影响无法改变实参,所以str还是NULL。
  • 在运行strcpy函数时,因为要拷贝的目的地是NULL,所以程序崩溃。
  • 同时,动态开辟的空间未释放,还导致了内存泄漏。

4.2 题目二

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

结果:

  • 有可能打印出乱码

解析:

  • GetMemory函数中,开辟了p数组。p是局部变量,出了函数作用域就会销毁(空间被回收)。
  • 所以,str接收p的地址时,此时已经指向一块被回收的空间。
  • 打印该空间的内容,是否为乱码,取决于该空间的内容是否被其余数据覆盖

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);
}

结果:

  • 内存泄漏,有可能程序崩溃

解析:

  • 这次进行传址调用,二级指针解引用,确实能改变外部str
  • 但是接收到空间地址后,未进行检查,如果开辟失败,为NULL,则strcpy时依旧会程序崩溃
  • 同时未对动态内存释放,造成内存泄漏

4.4 题目四

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

结果:

  • 后果未知

解析:

  • 假设能正常开辟动态内存
  • strcpy拷贝完hello,并free进行释放
  • 因为free不会自动将str置为NULL,所以以下判断不起作用,“拦不住”str这个野指针
  • 最后strcpy将world拷贝到已被释放的空间,再进行打印(对已被释放的动态内存进行操作,后果未知,十分危险,很严重!)

五、柔性数组

也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。
C99 中,结构中的最后⼀个元素允许是未知大小的数组,这就叫做『柔性数组』成员。

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

有些编译器会报错无法编译可以改成:

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

5.1 柔性数组的特点

  • 结构中的柔性数组成员前面必须至少一个其他成员
  • sizeof 返回的这种结构大小不包括柔性数组的内存
  • 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
typedef struct st_type
{
	int i;
	int a[0];//柔性数组成员
}type_a;
int main()
{
	printf("%d\n", sizeof(type_a));//输出的是4
	return 0;
}

5.2 柔性数组的使用

int main()
{
	int i = 0;
	type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
	//使用空间
	p->i = 100;
	for(i=0; i<100; i++)
	{
		p->a[i] = i;
	}
	free(p);
	return 0;
}

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

六、C/C++中程序内存区域划分

C/C++程序内存分配的几个区域:

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
  2. 堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。
  3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

看到这里了还不给博主扣个:
⛳️ 点赞☀️收藏 ⭐️ 关注!
💛 💙 💜 ❤️ 💚💓 💗 💕 💞 💘 💖
拜托拜托这个真的很重要!
你们的点赞就是博主更新最大的动力!
有问题可以评论或者私信呢秒回哦。

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

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

相关文章

Unity中Shader URP的安装与设置

文章目录 前言一、URP安装1、Window -> Project Manager -> 搜索 Render 二、URP设置1、创建一个URP配置文件2、渲染管线的修改&#xff08;当为空时&#xff0c;使用的是 BuildIn Render Pipeline&#xff09;3、这时我们新建一个对象。使用的材质球默认使用 URP 默认Sh…

【JAVA面向对象练习】---第四天

数据求和类 定义一个类Demo,其中定义一个求两个数据和的方法&#xff0c;定义一个测试了Test&#xff0c;进行测试。 package com.fuhai.day04;public class Sum {private double a;private double b;public double getA() {return a;}public void setA(double a) {this.a a…

AE-制作绚丽的图形通道

目录 1.新建合成命名为四边形 2.在合成中新建纯色层命名为tao 3.在纯色层上添加RG Trapcode –>Tao 效果&#xff0c;设置Segment参数 4. 在合成中添加摄像机 5.设置Tao Repeat Paths 相关参数&#xff0c;并调整摄像机的位置 6.设置Tao的 Material & Lighting 等…

系列一、Linux中安装MySQL

一、Linux中安装MySQL 1.1、下载MySQL安装包 官网&#xff1a;https://dev.mysql.com/downloads/file/?id523327 我分享的&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/188_9RnBYlWVzFb_UJH5aaQ?pwdyyds 提取码&#xff1a;yyds 1.2、上传至/opt目录 & 解压…

【每日一题】统计区间中的整数数目

文章目录 Tag题目来源解题思路方法一&#xff1a;平衡二叉搜索树 写在最后 Tag 【平衡二叉搜索树】【设计类】【2023-12-16】 题目来源 2276. 统计区间中的整数数目 解题思路 方法一&#xff1a;平衡二叉搜索树 思路 用一棵平衡二叉搜索树维护插入的区间&#xff0c;树中的…

【Qt开发流程】之网络编程:`HTTP`和`FTP`的高级网络操作

概述 Qt Network模块提供了可以编写TCP/IP客户端和服务器的类。它提供了较低层次的类&#xff0c;如QTcpSocket、QTcpServer和QUdpSocket&#xff0c;来代表低层次网络概念&#xff0c;以及高级层次类&#xff0c;如QNetworkRequest、QNetworkReply和QNetworkAccessManager&am…

CSS对文本的简单修饰

CSS格式&#xff1a; 格式一&#xff1a;在head中的style标签范围内。 < style> 在style内的只写名字不写 &#xff1a; < > 选择器 { 属性的名称 &#xff1a; 样式&#xff1b; 属性的名称&#xff1a;样式&#xff1b; } < style> style中的注释用/* *…

前端如何设置模板参数

1.背景&#xff1a; 最近接到一个需求&#xff0c;在一个类似chatGpt的聊天工具中&#xff0c;要在对话框中设置模板&#xff0c;后端提供了很多模板参数&#xff0c;然后要求将后端返回的特殊字符转成按钮&#xff0c;编辑完成后在相应的位置拼接成字符串。 2.效果&#xff1a…

关于嵌入式开发的一些信息汇总:嵌入式C开发人员、嵌入式系统Linux

关于嵌入式开发的一些信息汇总&#xff1a;嵌入式C开发人员、嵌入式系统Linux 1 关于嵌入式 C 开发人员1.1 嵌入式 C 开发人员必须具备的一些基本技能是&#xff1a;1.2 嵌入式C开发的应用案例 2 如何学习用于嵌入式系统的 Linux2.1 如何学习Linux2.1.1 第一步&#xff1a;创建…

openGauss学习笔记-155 openGauss 数据库运维-备份与恢复-导出数据-使用gs_dump和gs_dumpall命令导出数据-概述

文章目录 openGauss学习笔记-155 openGauss 数据库运维-备份与恢复-导出数据-使用gs_dump和gs_dumpall命令导出数据-概述155.1 概述155.2 注意事项 openGauss学习笔记-155 openGauss 数据库运维-备份与恢复-导出数据-使用gs_dump和gs_dumpall命令导出数据-概述 155.1 概述 op…

邮件服务下载安装详细步骤、汉化、配置

Foxmail for Mac 下载地址&#xff1a;Download - hMailServer - Free open source email server for Microsoft Windows 教程地址 hMailServer安装使用教程 - 诸子流 - 博客园 (cnblogs.com) 设置密码为:dzqdb123 设置好端口 添加账号密码 (9条消息) hMailServer 配置DKIM…

IO流学习

IO流:存储和读取数据的解决方案 import java.io.FileOutputStream; import java.io.IOException;public class Test {public static void main(String[] args) throws IOException {//1.创建对象//写出 输入流 OutputStream//本地文件fileFileOutputStream fos new FileOutputS…

TrustGeo代码理解(七)preprocess.py

代码链接:https://github.com/ICDM-UESTC/TrustGeo 一、导入各种模块和数据库 # Load data and IP clusteringimport math import random import pandas as pd import numpy as np import argparse from sklearn import preprocessing from lib.utils import MaxMinScaler …

列表优先于数组

在Java中&#xff0c;列表&#xff08;List&#xff09;通常优于数组&#xff0c;因为列表提供了更灵活的操作和动态调整大小的能力。下面是一个例子&#xff0c;展示了为什么在某些情况下使用列表比数组更好&#xff1a; import java.util.ArrayList; import java.util.List;…

AtCoder ABC周赛2023 12/10 (Sun) D题题解

目录 原题截图&#xff1a; 题目大意&#xff1a; 主要思路&#xff1a; 注&#xff1a; 代码&#xff1a; 原题截图&#xff1a; 题目大意&#xff1a; 给定两个 的矩阵 和 。 你每次可以交换矩阵 的相邻两行中的所有元素或是交换两列中的所有元素。 请问要使 变换至…

python封装执行cmd命令的方法

一、前置说明 在自动化时&#xff0c;经常需要使用命令行工具与系统进行交互&#xff0c;因此可以使用python封装一个执行cmd命令的方法。 二、代码实现 import subprocess import timefrom common.exception import RunCMDError from common.logger import loggerclass Cmd…

使用Matlab实现声音信号处理

利用Matlab软件对声音信号进行读取、放音、存储 先去下载一个声音文件&#xff1b;使用这个代码即可 clear; clc; [y, Fs] audioread(xxx.wav); plot(y); y y(:, 1); spectrogram(y); sound(y, Fs); % player audioplayer(y, Fs);y1 diff(y(:, 1)); subplot(2, 1, 1); pl…

【Spark精讲】Spark五种JOIN策略

目录 三种通用JOIN策略原理 Hash Join 散列连接 原理详解 Sort Merge Join 排序合并连接 Nested Loop 嵌套循环连接 影响JOIN操作的因素 数据集的大小 JOIN的条件 JOIN的类型 Spark中JOIN执行的5种策略 Shuffle Hash Join Broadcast Hash Join Sort Merge Join C…

关于MySQL的bigint问题

MySQL的bigint(8)能存多大数值&#xff1f; MySQL的BIGINT(8)可以存储的数值范围是从-9,223,372,036,854,775,808到9,223,372,036,854,775,807。这是因为BIGINT数据类型在MySQL中使用8字节进行存储&#xff0c;每个字节有8位&#xff0c;所以总共可以表示2^64个不同的整数。 …

亚太地区是Aleo下一个重点市场!ZK技术将重塑区块链世界!

4 年&#xff0c;3 亿美元&#xff0c;基于 ZK 的隐私公链&#xff0c;是 Aleo 最直观的三个标签。区块链的致富效应&#xff0c;已经让传统金融蠢蠢欲动&#xff0c;想参与Aleo私募和头矿的朋友请于文末添加微信。 对于Aleo副总裁兼业务发展主管Joanna Zeng来说&#xff0c;近…
最新文章