首页 > 其他分享 >0基础 三个月掌握C语言(15)

0基础 三个月掌握C语言(15)

时间:2024-03-27 14:01:09浏览次数:23  
标签:返回 释放 15 函数 掌握 C语言 内存 空间 指针

动态内存管理

为什么要有动态内存分配

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

int val = 20;  //在栈空间上开辟四个字节

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

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

• 空间开辟⼤⼩是固定的。

• 数组在申明的时候,必须指定数组的⻓度,数组空间⼀旦确定了⼤⼩不能调整

一旦申请好空间 大小便无法调整了

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

C语⾔引⼊了动态内存开辟,让程序员⾃⼰可以申请和释放空间,这样就⽐较灵活了。

但灵活的同时 也会带来一些问题

动态内存管理的4个函数malloc free calloc  realloc

接下来我们来一一学习

malloc:动态内存开辟函数

在使用malloc时 我们需包含一个头文件#include<stdlib.h>

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

如果开辟成功,则返回⼀个指向开辟好空间的指针。

如果开辟失败,则返回⼀个 NULL 指针,因此malloc的返回值⼀定要做检查。

返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使⽤的时候使⽤者⾃⼰来决定。

如果参数 size 为0,malloc的⾏为是标准是未定义的,取决于编译器。

在释放内存后 我们最好让指针arr置为空 原因我们放在下一节知识点内讲解

我们的指针arr指向分配空间的起始地址

free

C语⾔提供了另外⼀个函数free,专⻔是⽤来做动态内存的释放和回收的,函数原型如下:

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

如果参数 ptr 指向的空间不是动态开辟的,那么free函数的⾏为是未定义的。

如果参数 ptr 是NULL指针,则函数什么事都不做。

malloc和free都声明在 stdlib.h 头⽂件中。

传递给free函数的是要释放的内存空间的起始地址(所以如果我们写arr++  再释放空间 这里就会出现问题了)

free只是把开辟的空间的使用权限还给了操作系统 此时我们的指向该空间的指针变成了野指针 所以在释放空间后 要将指针置空

举个例⼦

calloc

C语⾔还提供了⼀个函数叫 calloc , calloc 函数也⽤来动态内存分配。原型如下:

函数的功能是为 num 个⼤⼩为 size 的元素开辟⼀块空间,并且把空间的每个字节初始化为0。

• 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。

举个例⼦:

这里我们就知道了 当我们想给所分配空间的数赋值就用calloc  不想赋初值就可以用malloc

realloc

realloc函数的出现让动态内存管理更加灵活。

有时会我们发现过去申请的空间太⼩了,有时候我们⼜会觉得申请的空间过⼤了,那为了合理的使⽤内存,我们⼀定会对内存的⼤⼩做灵活的调整。那 realloc 函数就可以做到对动态开辟内存⼤⼩的调整。

函数原型如下:

ptr 是要调整的内存地址

size 调整之后新⼤⼩

返回值为调整之后的内存起始位置。

这个函数调整原内存空间⼤⼩的基础上,还会将原来内存中的数据移动到新的空间。

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

情况1:原有空间之后有⾜够⼤的空间

情况2:原有空间之后没有⾜够⼤的空间

情况2:

1.在堆区的内存中找一个新的空间 并且新的空间大小要求满足

2.会将原来空间的数据拷贝一份到新的空间

3.释放旧的空间

4.返回新的内存空间的起始地址

如果调整失败了的话 返回的是NULL

情况1

当是情况1的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发⽣变化。

情况2

当是情况2的时候,原有空间之后没有⾜够多的空间时,扩展的⽅法是:在堆空间上另找⼀个合适⼤⼩的连续空间来使⽤。这样函数返回的是⼀个新的内存地址。

由于上述的两种情况,realloc函数的使⽤就要注意⼀些

代码1--情况1:如果原有空间之后有足够的空间,可以直接将realloc的返回值赋给ptr。

这行代码试图将ptr指向的内存块大小从100字节增加到1000字节。如果原有内存块之后有足够的连续空间,realloc就会扩展原有内存块的大小并返回指向它的指针。如果成功,ptr将指向新的更大的内存块。

代码2--情况2:如果原有空间之后没有足够的空间,realloc可能会移动内存块到另一个有足够空间的位置。因此,在调用realloc时,不应该直接覆盖原来的指针ptr,而应该先将realloc的返回值保存在一个临时指针(如p)中。

这段代码首先定义了一个临时指针p,并将realloc的返回值赋给它。如果realloc返回NULL,则释放操作失败,程序返回1。否则,将p(也就是realloc返回的新指针)赋给ptr,这样ptr就指向了新的内存块。

常⻅的动态内存的错误

对NULL指针的解引⽤操作

要判断malloc的返回值是否为NULL

当然assert也是可以的  assert(p)

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

对⾮动态开辟内存使⽤free释放

使⽤free释放⼀块动态开辟内存的⼀部分

p不再指向所分配的连续空间的起始地址

这里free就会出错

对同⼀块动态内存多次释放

为了避免这一问题 我们要养成在释放空间后 令指针置空

置空后 再释放不会有其他影响

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

在test函数内 p是个局部变量  p指向分配空间的地址 当函数调用结束 p这个局部变量不再存在  但它所指向的内存块并没有得到释放  这时候就发生了内存泄露 最终导致这块内存不能被再次使用 且随着时间的推移 可能会消耗掉系统所有的可用内存

在函数内开辟空间 在函数调用结束后 找不到分配的空间 而这时候我们又不释放空间  我们再想使用这块空间 便无法使用

所以牢记 在不使用这块空间时 记得释放掉该空间

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

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

尽量要做到

1.谁(函数...)申请的空间谁释放

2.如果不能释放 要告诉使用的人 记得释放(不释放掉原先空间 便无法使用)

再给大家看一个牛马代码

动态内存经典笔试题分析

题⽬1:

现在我们对这段代码进行分析

在GetMemory这个函数 其接受一个字符指针 p 作为参数,并试图为它分配100字节的内存。但是,这里有一个重要的问题:在C语言中,函数参数是按值传递的,这意味着 p 是一个局部变量,它只是原始指针的一个副本。p和str并不指向同一地址,所以当在函数内部修改 p 时(如 p = (char*)malloc(100);),它只改变了这个副本(只对p进行了某某操作,str并不会改变),而不是原始的指针。因此,当 GetMemory 函数返回时,原始的 str 指针在 Test 函数中仍然是 NULL

在 Test 函数中,首先定义了一个字符指针 str 并初始化为 NULL。然后调用 GetMemory 函数,试图为 str 分配内存。但由于前面提到的 GetMemory 函数的问题,str 仍然是 NULL。接下来,strcpy(str, "hello world"); 将尝试将字符串 "hello world" 复制到 str 指向的位置,但因为 str 是 NULL,这将导致运行时错误(对NULL的解引用操作会导致程序崩溃)

printf(str); 也会引发问题,因为 printf 函数的正确用法是 printf("%s", str);。即使 str 不是 NULL,直接传递 str 给 printf 也不是标准做法

这段代码的主要问题是 GetMemory 函数无法正确地分配内存给传入的指针。要修复这个问题,你可以使用指针的指针(传址调用)来修改原始的指针。

例如:

当然我们还有另一种改法:

题目2:

要修复这个问题,你有几个选择:

1.使用静态数组:将局部数组改为静态数组,这样它的生命周期就会是整个程序的执行期间。但这种方法有其局限性,比如每次调用GetMemory都会返回同一个数组的地址,且静态变量在多次函数调用之间会保持其值。

2.动态分配内存:使用malloc或calloc在堆上分配内存,并返回这块内存的地址。这样,返回的地址在函数调用结束后仍然有效,直到显式地调用free来释放它。

记得在使用完通过malloc分配的内存后调用free来释放它,避免内存泄漏。

3.使用字符串字面量:直接返回字符串字面量的地址。字符串字面量存储在程序的只读数据段中,它们的生命周期是整个程序的执行期间。

注意,虽然这种方法简单且有效,但返回的指针是指向只读存储区的,所以你不应该试图修改通过这个方法返回的字符串的内容。

在任何情况下,都应该避免返回局部变量的地址,因为这通常会导致程序出现错误或崩溃。

看到这 爱提问的猴子就开口了 在一个函数内部 return n和return &n会有什么影响

看到这 猴子说 这答案不是都为10吗 那这俩的作用一样的

但其实不然 我们return &n中 n为一个局部变量 在函数调用结束 局部变量n的内存便还给操作系统了 此时局部变量n的内存可能会被释放或覆盖

不信 我给大家看一下

这里我们就看到输出的值不在是10了 说明局部变量n的内存被释放或覆盖了

大家如果想了解这一内容 我把这一知识点放在本章结尾 以供大家参考

题目3:

这段代码和我们之前修改的很相像 但我们还需要空间分配和  记得释放

即free(str); str=NULL;

题目4:

在动态内存管理讲完之后 再给大家补充一个新的知识点 我当时也是第一次见

柔性数组

也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。

C99 中,结构中的最后⼀个成员允许是未知⼤⼩的数组,这就叫做『柔性数组』成员。

例如:

上面两种方式是就不同的编译器来说

因为有些编译器支持第一种写法 有些支持第二种写法

柔性数组的特点:

结构体中的柔性数组成员前⾯必须⾄少⼀个其他成员。

sizeof 返回的这种结构体⼤⼩不包括柔性数组的内存。

包含柔性数组成员的结构⽤malloc ()函数进⾏内存的动态分配,并且分配的内存应该⼤于结构的⼤⼩,以适应柔性数组的预期⼤⼩。

例如:

这里我们就可以知道 假如柔性数组成员前面没有其他成员 sizeof返回的这种结构体大小就为0了 所以结构体中的柔性数组成员前⾯必须⾄少⼀个其他成员。

这个结构体ps(通过malloc)所得到的空间为4+20=24个字节

我们可以通过realloc来改变我们分配空间的大小 就不久很柔性了吗 收放自如

柔性数组 我们不就是让该数组可大可小吗

我们也可以有其他的方式来进行该操作

柔性数组的优势

当然这两种方式相比 还是柔性数组更好一点 因为柔性数组只需要一次malloc 一次free 不容易出错

而且malloc空间的次数越多 内存碎片(空间与空间的间隙)越多 内存的利用率就越低

柔性数组中分配的空间是连续的 连续的空间有益于提高访问速度 也有益于减少内存碎片 而第二种方式是先给结构体分配空间 再给结构体成员arr分配空间

所以柔性数组方便内存的释放 有利于访问速度

总结C/C++中程序内存区域划分

栈区:存放局部变量 函数参数

堆区:动态内存申请

数据段也就是静态区

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

1.栈区(stack):在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内存容量有限。 栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。

《函数栈帧的创建和销毁》

2. 堆区(heap):⼀般由程序员分配释放, 若程序员不释放,程序结束时可能由操作系统回收 。分配⽅式类似于链表。

3. 数据段(静态区):(static)存放全局变量、静态数据。程序结束后由系统释放。

4. 代码段:存放函数体(类成员函数和全局函数)的⼆进制代码。

在C语言中,return n 和 return &n 之间的区别涉及到返回值的数据类型和所指向的内存区域。

return n:

当你在函数中返回 n 时,你实际上返回的是变量 n 的值。这里的 n 必须是一个具有确定值的表达式,并且它的类型必须与函数的返回类型相匹配或兼容。
如果 n 是一个基本数据类型(如 int, float, char 等),那么返回的是这个值的副本。这意味着函数外部的调用者获得的是这个值的一个拷贝,而不是原始变量本身。
如果 n 是一个复杂的数据类型(如结构体或联合),那么返回的可能是一个这些类型的副本,但这取决于具体的实现和上下文。对于大型的数据结构,通常建议通过指针或引用传递,以避免不必要的复制开销。
需要注意的是,如果 n 是一个局部变量(定义在函数内部的变量),在函数返回后,这个局部变量占用的内存可能会被释放或覆盖,所以返回它的值通常没有问题,但尝试返回它的地址(如 &n)则是不安全的。
return &n:

当你返回 &n 时,你返回的是变量 n 的地址(即指向 n 的指针)。这意味着调用者现在拥有一个指向 n 的指针,可以通过这个指针来访问或修改 n 的值。
如果 n 是一个局部变量,那么返回它的地址是不安全的,因为当函数返回后,局部变量 n 的内存可能会被释放或覆盖。如果调用者尝试通过这个指针来访问或修改 n 的值,可能会导致未定义的行为,包括程序崩溃或数据损坏。
如果 n 是一个全局变量或静态变量,那么返回它的地址通常是安全的,因为这些变量的生命周期贯穿整个程序的执行过程。
返回指针时,函数的返回类型应该是一个指针类型,例如 int*、float* 或自定义数据类型的指针。
总结:

return n 返回的是变量 n 的值,而 return &n 返回的是变量 n 的地址。
返回局部变量的值通常是安全的,但返回局部变量的地址通常是不安全的。
函数的返回类型应该与返回值的类型相匹配或兼容。
在实际编程中,要谨慎处理指针和地址,确保不会发生悬挂指针(dangling pointer)或野指针(wild pointer)等问题,这些问题可能导致程序的不稳定或难以调试的错误。

标签:返回,释放,15,函数,掌握,C语言,内存,空间,指针
From: https://blog.csdn.net/L2770789195/article/details/136976441

相关文章

  • 【独立开发前线】Vol.20 一个写信网站,每月115万访问量,年收入超百万
    今天给大家分享的案例网站是:FutureMe它的网址是:FutureMe:WriteaLettertoyourFutureSelf这个网站的主要作用就是给未来的自己写封信,网站设计的非常简洁,首页就可以直接输入信件的内容;写好后,可以定时在未来的某一天将信发给自己。信件可以选择私密或公开,公开的话其实......
  • "已知知识" 指的是我们已经掌握并了解的信息、事实或理论,它们是经过验证和确认的,可以
    "已知知识"指的是我们已经掌握并了解的信息、事实或理论,它们是经过验证和确认的,可以被人们普遍接受和相信的。这些知识可以来源于科学研究、教育、传统经验等途径,是我们日常生活和工作中依赖的基础。而"未知知识探索"则指的是对那些我们尚未了解或者尚未完全掌握的领域、问题......
  • Codeforces Round 915 (Div. 2) D
    CyclicMEX题面翻译对于一个长为\(n\)的排列\(p\),定义其权值为\(\sum_{i=1}^n\operatorname{mex}_{j=1}^ip_j\),也就是\(p_1\simp_i\)中没有出现过的最小自然数的和。然后你可以对这个排列进行移位操作,问最大权值。题目描述Foranarray$a$,defineitscostas$......
  • C语言-实现文件操作
    0.前言:    我们知道下载东西,电脑上就会有各种的文件夹及文件里面的内容,那么文件里面的数据怎么通过编写程序来帮我们获取呢,这些文件又是怎么创建的呢?C语言给我们提供了一些可以操作文件的函数。这里我只列举了一部分操作文件的函数,使用这些函数需要引入头文件<stdlib.......
  • Xilinx ZYNQ 7000+Vivado2015.2系列(四)之GPIO的三种方式:MIO、EMIO、AXI_GPIO
    前言:ZYNQ7000有三种GPIO:MIO,EMIO,AXI_GPIOMIO是固定管脚的,属于PS,使用时不消耗PL资源;EMIO通过PL扩展,使用时需要分配管脚,使用时消耗PL管脚资源;AXI_GPIO是封装好的IP核,PS通过M_AXI_GPIO接口控制PL部分实现IO,使用时消耗管脚资源和逻辑资源。使用的板子是zc702。1.MIO方式Zynq7000......
  • Xilinx ZYNQ 7000+Vivado2015.2系列(三)之HelloWorld实验(最小系统)(纯PS)
    前言:使用的板子是zc702。用Vivado的IP核搭建最小系统,包括ARM核(CPUxc7z020),DDR3(4×256M),一个UART串口(MiniUSB转串口),纯PS,通过串口打印出HelloWorld,工程虽小,五脏俱全,算是一种朝圣。配置要和板子对应,大家注意修改。操作步骤:硬件部分1.新建Vivado工程选择芯片型号xc7z020clg484_1......
  • Xilinx ZYNQ 7000+Vivado2015.2系列(二)之奇数分频和逻辑分析仪(ILA)的使用
    前言:偶数分频容易得到:N倍偶数分频,可以通过由待分频的时钟触发计数器计数,当计数器从0计数到N/2-1时,输出时钟进行翻转,并给计数器一个复位信号,使得下一个时钟从零开始计数。以此循环下去。奇数分频如何得到呢?第一部分 奇数分频奇数分频方法:N倍奇数分频,首先进行上升沿触发进行......
  • 轻松掌握:使用 API 接口自动缩短网址的秘诀
    在互联网的世界里,网址缩短已经成为了一种时尚和必要。长而复杂的网址不仅难以记忆,还可能让人望而却步。但是,现在有了API接口,我们可以轻松地将网址自动缩短,让分享变得更加简单和高效!本文将以具体例子详细介绍如何使用API接口实现网址缩短。首先,让我们来了解一下什么是API......
  • Xilinx ZYNQ 7000+Vivado2015.2系列(一)之流水灯(纯PL)
    原文链接:https://blog.csdn.net/u014485485/article/details/78056980前言:学习Xilinx的ZYNQ7000系列,用的板子是zc702(注意不是zedboard),SOC型号是xc7z020。虽然设计思路一样,但不同的套件引脚和io标准是有区别的,zc702评估板的的外观图如下,可以对照下自己的板子:作为入门体验,本设......
  • C语言实现游戏——三子棋
    三子棋是一种民间传统游戏,又叫九宫棋、井字棋等。游戏分为双方对战,双方依次在9宫格棋盘上摆放棋子,率先将自己的三个棋子走成一条线就视为胜利,而对方就算输了,但是三子棋在很多时候会出现和棋的局面。今天我们就来用C语言来实现一下这个游戏游戏分解:本文采用分文件编写的模式,实......