【C语言】万字讲解函数栈帧的创建与销毁

目录

前言

一、什么是函数栈帧?

二、理解函数栈帧能解决什么问题呢

三、函数栈帧的创建和销毁解析

3.1 什么是栈?

3.2 认识相关寄存器和汇编指令

3.3 剖析函数栈帧的创建和销毁

3.3.1 esp寄存器与ebp寄存器的重要性

3.3.2 函数的调用堆栈

3.3.3 准备环境

3.3.4 转到反汇编

3.3.5 函数栈帧的创建

3.3.6 执行有效代码

3.3.7 回答理解函数栈帧能解决什么问题中的六个问题

总结


前言

函数为C语言中最基本的一个单位,但它是如何创建的您了解吗?它又是如何传参的呢?又是如何返回的呢?本文章将深入底层,详细讲解关于函数栈帧的创建与销毁一系列知识,看完本文,您将收获匪浅。


一、什么是函数栈帧?

我们在写C语言代码的时候,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。 那函数是如何调用的?函数的返回值又是如何待会的?函数参数是如何传递的?这些问题都和函数栈帧有关系。

函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间是用来存放:

  • 函数参数和函数返回值
  • 临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)
  • 保存上下文信息(包括在函数调用前后需要保持不变的寄存器)。

简单理解就是创建函数时,会在栈区创建一块空间,而这块空间正是函数栈帧。


二、理解函数栈帧能解决什么问题呢

理解函数栈帧有什么用呢?只要理解了函数栈帧的创建和销毁,以下问题就能够很好的理解了:

  1. 局部变量是如何创建的?
  2. 为什么局部变量不初始化内容是随机的?
  3. 函数调用时参数时如何传递的?
  4. 传参的顺序是怎样的?
  5. 函数的形参和实参分别是怎样实例化的?
  6. 函数的返回值是如何带回的?

函数栈帧的知识是偏向底层的,当理解透彻后,对理解变量的存储、静态变量的创建、动态内存的申请与销毁等等知识点有很大的帮助!


三、函数栈帧的创建和销毁解析

讲解函数栈帧之前,需要了解一些预备知识点。

3.1 什么是栈?

栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函 数,没有局部变量,也就没有我们如今看到的所有的计算机语言。 在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈中(入栈,push),也可 以将已经压入栈中的数据弹出(出栈,pop),但是栈这个容器必须遵守一条规则:先入栈的数据后出 栈(First In Last Out, FIFO)。就像叠成一叠的术,先叠上去的书在最下面,因此要最后才能取出。 在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据 从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。 在经典的操作系统中,栈总是向下增长(由高地址向低地址)的。 在我们常见的i386或者x86-64下,栈顶由成为 esp 的寄存器进行定位的。

栈最主要的特性:先进后出。这里简单介绍,如想了解栈相关知识点,可以看《超详细之实现栈》


3.2 认识相关寄存器和汇编指令

在函数栈帧的创建与销毁过程中,涉及到了寄存器和汇编指令知识点。

寄存器的功能是存储二进制代码,它是由具有存储功能的触发器组合起来构成的。一个触发器可以存储1位二进制代码,故存放n位二进制代码的寄存器,需用n个触发器来构成。 [1]

按照功能的不同,可将寄存器分为基本寄存器和移位寄存器两大类。基本寄存器只能并行送入数据,也只能并行输出。移位寄存器中的数据可以在移位脉冲作用下依次逐位右移或左移,数据既可以并行输入、并行输出,也可以串行输入、串行输出,还可以并行输入、串行输出,或串行输入、并行输出,十分灵活,用途也很广。详细可见百度百科:寄存器

汇编指令是汇编语言中使用的一些操作符和助记符,还包括一些伪指令(如assume,end),汇编指令同机器指令一一对应。每一种CPU都有自己的汇编指令集。 [1]

计算机是通过执行指令来处理数据的,为了指出数据的来源、操作结果的去向及所执行的操作,一条指令一般包含操作码和操作数两部分。详细可见百度百科:汇编指令

相关寄存器:

函数栈帧创建与销毁相关寄存器
eax通用寄存器,保留临时数据,常用于返回值
ebx通用寄存器,保留临时数据
ebp栈底寄存器
esp栈顶寄存器
eip指令寄存器,保存当前指令的下一条指令的地址

相关汇编命令:

函数栈帧创建与销毁相关汇编命令
mov数据转移指令
push数据入栈,同时esp栈顶寄存器也要发生改变
pop数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
sub减法命令
add加法命令
call函数调用,1. 压入返回地址 2. 转入目标函数
jump通过修改eip,转入目标函数,进行调用
ret恢复返回地址,压入eip,类似pop eip命令

3.3 剖析函数栈帧的创建和销毁

3.3.1 esp寄存器与ebp寄存器的重要性

  • esp:栈顶指针。
  • ebp栈底指针。
  • 每一次函数调用,都要为本次函数调用开辟空间,就是函数栈帧的空间。
  • 要理解清楚函数栈帧,就要理解ebp与esp寄存器。
  • 这块空间的维护使用了2个寄存器:esp和ebp,ebp存储的是栈底的地址,ebp存储的是栈顶的地址。
  • ebp寄存器与esp寄存器中存放的是地址,这两个寄存器共同维护函数栈帧空间。

如图所示:

关于栈区的特点:

  • 栈区的分配习惯是先使用高地址,再使用低地址。
  • 栈区往往是从高地址开始,往上使用空间。

注意:函数栈帧的创建和销毁过程,在不同的编译器上实现的方式大同小异,主要掌握的是实现过程。本文将以VS2019为例。


3.3.2 函数的调用堆栈

演示代码:

#include <stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 3;
int b = 5;
int ret = 0;
ret = Add(a, b);
printf("%d\n", ret);
return 0;
}

在VS2019编译器上进行调试(F10进入调试),我们可以调用函数堆栈看看,如下图:

函数调用堆栈是反馈函数调用逻辑的,那我们可以清晰的观察到, main 函数调用之前,是由invoke_main 函数来调用main函数。这也解释了为什么main函数总是有return 0了,因为main函数也是被其它函数调用的,这里不做过多讲解,只需要知道每个函数都会有自己的栈帧。


3.3.3 准备环境

为了让我们研究函数栈帧的过程足够清晰,不要太多干扰,我们可以关闭下面的选项,让汇编代码中排 除一些编译器附加的代码:


3.3.4 转到反汇编

观察函数栈帧需要再返汇编内观看,里边用到了寄存器与汇编指令。

为了方便观看地址,我们鼠标右键,将显示外部符号取消勾选。

int main()
{
//函数栈帧的创建
002E18B0  push        ebp  
002E18B1  mov         ebp,esp  
002E18B3  sub         esp,0E4h  
002E18B9  push        ebx  
002E18BA  push        esi  
002E18BB  push        edi  
002E18BC  lea         edi,[ebp-24h]  
002E18BF  mov         ecx,9  
002E18C4  mov         eax,0CCCCCCCCh  
002E18C9  rep stos    dword ptr es:[edi]  
//main函数中的核心代码int a = 3;
002E18D5  mov         dword ptr [ebp-8],3  int b = 5;
002E18DC  mov         dword ptr [ebp-14h],5  int ret = 0;
002E18E3  mov         dword ptr [ebp-20h],0  ret = Add(a, b);
002E18EA  mov         eax,dword ptr [ebp-14h]  
002E18ED  push        eax  
002E18EE  mov         ecx,dword ptr [ebp-8]  
002E18F1  push        ecx  
002E18F2  call        002E10B4  
002E18F7  add         esp,8  
002E18FA  mov         dword ptr [ebp-20h],eax  printf("%d\n", ret);
002E18FD  mov         eax,dword ptr [ebp-20h]  
002E1900  push        eax  
002E1901  push        2E7B30h  
002E1906  call        002E10D2  
002E190B  add         esp,8  return 0;
002E190E  xor         eax,eax  
}

以上汇编代码为main函数这个函数栈帧中一系列操作,其实可以分成两部分:

  • 函数栈帧的创建(每个函数都有这部)
  • 执行有效代码

3.3.5 函数栈帧的创建

我们先来看看第一部分,如何创建函数栈帧。我们知道main函数也是被其它函数调用的,前文提到一个函数栈帧是被esp和ebp进行维护的,此时esp与ebp还在维护着invoke_main函数栈帧,我们看看图:

接下来,开始为main函数创建栈帧,我们逐条分析:

002E18B0   push     ebp  

push压栈,将ebp压栈,简单理解就是拷贝一份ebp,放到栈顶,此时栈顶存储着栈底的地址,push后esp的指向也会改变,看图:

002E18B1   mov   ebp,esp 

mov移动,相当于赋值操作,将esp的值赋给ebp,ebp一开始是指向栈底的,赋值后,ebp指向栈顶,即esp指向的位置,看图:

002E18B3   sub   esp,0E4h 

sub减操作,将esp减去0E4h,0E4h是一个8进制数字,如想知道具体值,可以打开监视查看,这条指令的意思就是将esp指向位置减0E4h,就是改变esp位置,esp会向上移动,因为栈区下面是高地址,上边是低地址,具体移动到了哪个位置,我们可以通过内存窗口看,下面是具体的图解:

此时esp与ebp各自指向的地址之间,就是共同维护的新一块空间。当然,这块空间有多大是由编译器决定的,我们也不知道,这块空间也可以说是编译器为某函数预开辟的空间。

002E18B9   push      ebx  
002E18BA   push      esi  
002E18BB   push      edi 

接下来,会进行三次push,也就是压栈三次,分别将ebx、esi、edi压栈。


接下来的4条指令,作用是为函数栈帧初始化:

  • 先把ebp-24h的地址,放在edi中
  • 把9放在ecx中
  • 把0xCCCCCCCC放在eax中
  • 将从ebp-24h到ebp这一段的内存的每个字节都初始化为0xCC
002E18BC   lea    edi,[ebp-24h] 

lea(load effecitve address 加载有效地址),将后面这个地址加载到edi中,其实就是将edi的值改为(ebp-24h)。其实在刚刚sub操作时,将esp-0E4h,让esp向上走,那时候esp的指向和ebp是一样的,sub后指向了新的地址,此时让edi指向(ebp-24h)的值,那此时ebi的值为在3次push之前esp指向的位置了.

002E18BF   mov   ecx,9

mov移动,将9赋值给ecx。

002E18C4   mov   eax,0CCCCCCCCh

mov操作,将0cccccccch赋值给eax。

002E18C9  rep stos   dword ptr es:[edi]

关键一步,这条指令的意思是将edi以下9次或ecx次的dword(double word,1个word为2字节,double word为4字节)每次初始化4个字节,全部改为0cccccccch的内容,到ebp结束。

以上操作后,此时为该函数的函数栈帧就开辟好了,接下来开始执行有效代码了。


3.3.6 执行有效代码

int a = 3;
int b = 5;
int ret = 0;
ret = Add(a, b);
printf("%d\n", ret);
return 0;
int a = 3;
002E18D5  mov  dword ptr [ebp-8],3 

mov移动,意思为将3赋值给[ebp-8],dword为4字节,意思就是将3放在[ebp-8]4字节位置,[ebp-8]其实就是栈底向上8字节的位置,该位置用来存储3,该位置也是为局部变量a开辟的空间。

此时也能解是局部变量是如何创建的,为什么局部变量创建的时候建议初始化了。如果不初始化,那a的值就为0cccccccch,这就是为什么会打印出烫烫烫.....的原因了。

int b = 5;
002E18DC  mov  dword ptr [ebp-14h],5

同理,与上一条指令的操作思路是一样的。还需要注意的是,在哪个地方为局部变量开辟空间是不一定的,这由编译器决定。

int ret = 0;
002E18E3  mov   dword ptr [ebp-20h],0


很多人都不清楚,函数是如何传参的,接下来4条指令,就是函数的传参过程:

res = Add(a,b);
002E18EA  mov  eax,dword ptr [ebp-14h]

mov操作,将[ebp-14h]的值赋给eax,[ebp-14h]是什么呢?就是b所在的空间中的5。

002E18ED  push   eax

push压栈,将eax压栈。

002E18EE   mov  ecx,dword ptr [ebp-8]

mov操作,将[ebp-8]的值赋给ecx,[ebp-8]就是a所在的空间里的3。

002E18F1  push  ecx

push压栈,将ecx压栈。

eax与ecx分别存储了b与a的值,并且进行压栈操作,注意:此时还没有调用函数Add,那也就是说,函数的传参其实是在调用之前完成的,而形参是实参的一份临时拷贝这句话也能理解了,eax与[ebp-14h]、ecx与[ebp-8] 均不在一块空间,并且eax与ecx存储的值分别为b与a的值,这就是临时拷贝。关于函数传参,是从右到左执行的


接下来,就准备进行函数调用了。

002E18F2  call   002E10B4

call指令是要执行函数调用逻辑的,在执行call指令之前先会把call指令的下一条指令的地址进行压栈操作,这个操作是为了解决当函数调用结束后要回到call指令的下一条指令的地方,继续往后执行。(按F11跳转)

002E10B4  jmp   002E1770 

这里不用过多关注,再按一次F11,就进入Add函数了。

int Add(int x, int y)
{
//函数栈帧的创建
002E1770  push        ebp  
002E1771  mov         ebp,esp  
002E1773  sub         esp,0CCh  
002E1779  push        ebx  
002E177A  push        esi  
002E177B  push        edi  
002E177C  lea         edi,[ebp-0Ch]  
002E177F  mov         ecx,3  
002E1784  mov         eax,0CCCCCCCCh  
002E1789  rep stos    dword ptr es:[edi]  
//执行有效代码int z = 0;
002E1795  mov         dword ptr [ebp-8],0  z = x + y;
002E179C  mov         eax,dword ptr [ebp+8]  
002E179F  add         eax,dword ptr [ebp+0Ch]  
002E17A2  mov         dword ptr [ebp-8],eax  return z;
002E17A5  mov         eax,dword ptr [ebp-8]  
}
//函数销毁并回到main函数内,销毁函数内部的局部变量
002E17A8  pop         edi  
002E17A9  pop         esi  
002E17AA  pop         ebx   
002E17B8  mov         esp,ebp  
002E17BA  pop         ebp  
002E17BB  ret

此时的逻辑跟创建main函数栈帧逻基本辑相同,但要分为三部分,前两部分与main函数逻辑相同,在执行有效代码部分,如何使用形参也是重点,第三部分为函数销毁、如何将返回值带回、如何回到main函数内、如何销毁形参。下面进行解析:

//Add函数栈帧的创建
002E1770  push        ebp  
002E1771  mov         ebp,esp  
002E1773  sub         esp,0CCh  
002E1779  push        ebx  
002E177A  push        esi  
002E177B  push        edi  
002E177C  lea         edi,[ebp-0Ch]  
002E177F  mov         ecx,3  
002E1784  mov         eax,0CCCCCCCCh  
002E1789  rep stos    dword ptr es:[edi]  

第一部分为创建Add函数栈帧指令,因为上文解释过了,这里不进行过多解释了,看看图解吧:


第二部分是执行有效代码,这里需要关注形参的使用。

int z = 0;
002E1795  mov  dword ptr [ebp-8],0 

与上文逻辑相同,为z开辟一个空间。不过多解释。

z = x + y;
002E179C  mov   eax,dword ptr [ebp+8]  
002E179F  add   eax,dword ptr [ebp+0Ch]  
002E17A2  mov   dword ptr [ebp-8],eax

以上3条指令的作用是使用形参x,y,并且相加赋给z。一条条解释吧:

002E179C  mov   eax,dword ptr [ebp+8]

mov操作,将[ebp+8]的值赋给eax,[ebp+8]是什么呢?看看下图:

一块内存空间就为4字节,那ebp+8的话,就是ebp向下8字节,那就是ecx内存空间,也就是a所在的空间,看到这里相信聪明的您明白了吧。函数内的形参并不会在它的函数栈帧中分配一块空间,而是当要使用时,寻找之前压栈的ecx与eax,所有严格上来说,函数的形参是创建在主调函数栈帧里的。

002E179F  add   eax,dword ptr [ebp+0Ch]

add增加指令,其实就是找到[ebp+0Ch],然后eax + [ebp+0Ch],而[ebp+0Ch]的值也就是eax空间,b的值了,找到后进行相加,也就对应着:x + y了。

这里有个点,就是当寻找形参时,是从左向右执行的,这点与函数传参是不同。

002E17A2  mov   dword ptr [ebp-8],eax

mov操作,将eax的值赋给[ebp-8],逻辑跟上文创建局部变量相同,eax的值就是刚刚相加后的值,[ebp-8]就是Add函数栈帧内的一处空间,为z开辟了一处空间,这块空间就是[ebp-8],里面的内容是eax,就是形参相加后的值。


继续执行,接下来是return返回语句,那函数中怎么带回返回值呢?

return z;
002E17A5  mov   eax,dword ptr [ebp-8]

我们知道,在函数返回后,也代表这个函数结束,进行销毁,而进行销毁,那z的值不也销毁了吗?那我们怎么带回去呢?

mov指令,将[ebp-8]的值赋给eax,这里非常巧妙,[ebp-8]为z,而eax是什么呢?eax是一个寄存器,寄存器是独立于内存之外的,不会随着函数的结束而销毁,因此将返回值放到eax中,等回到主调函数再拿出来。


此时Add函数内的有效代码全部执行完毕,但并还没有结束,接下来将进行函数栈帧的销毁、返回到主调函数。

002E17A8  pop     edi  
002E17A9  pop     esi  
002E17AA  pop     ebx

还记得在创建函数栈帧的时候push了3个寄存器到栈顶吗?而此时,又进行3次pop,将edi、esi、ebx依次弹出,esp的指向也改变。

002E17B8  mov    esp,ebp  
002E17BA  pop    ebp 

进行mov操作,将ebp的值赋给esp,此时esp指向ebp位置。

pop操作,将栈顶元素弹出并赋值,放到ebp中,而正好,此时的栈顶正好是刚开始执行的第一次指令push ebp,而此时栈顶的值就是上一个函数栈帧的栈底,因此ebp此时指向上一个函数的栈底位置,这就是精妙之处,因为上一个函数的栈底位置很难找到,那在创建函数栈帧时,就会push ebp,将上一个函数的栈底地址压栈,栈顶放置栈底的地址,方便回来的时候找到栈底,此时,因为pop了,esp与ebp执行均改变,esp执行下一个,ebp被pop赋值,而pop的位置正好存放了栈底的值,因此ebp又指向栈底了,现在esp与ebp又维护这main函数。

002E17BB  ret

最后ret指令,首先是从栈顶弹出一个值,此时栈顶的值就是call指 令下一条指令的地址,然后直接跳转到call指令下一条指令的地址处,继续往下执行。此时就真正的回到了main函数栈帧内了。这就能解释之前为什么将call指令的下一条指令的地址存储起来了,就是为了当调用函数结束后,返回后能继续执行代码。

此时我们观察上图,发现形参还没有销毁,因此说明,形参不是随着函数栈帧的结束而销毁的,而是要回到主调函数栈帧内,再进行销毁。

002E18F7  add   esp,8

add操作,让esp+8,就是让esp向下走8字节。


此时,函数调用结束,要继续执行main函数有效代码部分了。

002E18FA  mov   dword ptr [ebp-20h],eax

mov操作,将eax的值赋给[ebp-20h],eax里的值是什么呢?里面就是Add函数的返回值,而[ebp-20]所处的空间为res的空间,这条指令就是接受返回值。

printf("%d\n", ret);
002E18FD  mov         eax,dword ptr [ebp-20h]  
002E1900  push        eax  
002E1901  push        2E7B30h  
002E1906  call        002E10D2  
002E190B  add         esp,8  
return 0;
002E190E  xor         eax,eax

最后就是打印和return返回了。这里不再解释了。函数栈帧的创建与销毁这个流程也将结束。


3.3.7 回答理解函数栈帧能解决什么问题中的六个问题

问题1:局部变量是如何创建的?

在创建初始化完函数栈帧后,通过栈底指针ebp-某个数,这个数是由编译器决定的,从而得到一块内存空间,mov指令后,这块空间就是属于局部变量的空间。

问题2:为什么局部变量不初始化内容是随机的?

在创建函数栈帧时,会有4条指令用于初始化函数栈帧的内存空间,每个内存中会存放0cccccccch这个值,而如果在创建局部变量时不进行初始化,那分配的空间中的内容就为0cccccccch。

问题3:函数调用时参数是如何传递的?

对于参数是如何传递的,也就是如何进行函数传参问题,在调用函数之前,会将()内的参数从右到左依次进行push压栈操作,此时参数处于主调函数的栈帧之间,并且存储了实参的值,内存空间与实参不同。

问题4:传参的顺序是怎样的?

函数传参的顺序是从右到左执行的。

问题5:函数的形参和实参分别是怎样实例化的?

函数的实参实例化在传参时压栈操作后,与相对应的局部变量建立了联系,相当于临时拷贝,这是实参进行了实例化。而函数的形参实例化是当要使用该形参时,会通过栈底指针ebp+某个值找到调用函数前push压栈后所对应的空间,使用里边的值,这就是形参的实例化。

问题6:函数的返回值是如何带回的?

当函数执行到return返回语句时,会将返回值赋值给eax寄存器,因为寄存器不会随着函数的结束而销毁,当回到主调函数时,再进行使用eax寄存器里的值。


总结

这就是函数栈帧的创建与销毁相关知识点,希望这篇文章对您有帮助,如果有帮助,可以点个赞,关个注,后续还会出更多的干货,希望大家多多支持!!!❤❤❤❤

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

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

相关文章

SAP MRP-MD01与MRP LIVE-MD01N简介

自从SAP推出HANA以后,无论在做项目还是在面试的时候都会遇到一个问题,就是MRP和MRP LIVE 有什么区别。通常顾问都知道MRPLIVE是运行在内存中的,运行效率会优于传统的MRP。经历了很多家的公司都是HANA的系统,基本都很少会用到MRP LIVE,百分之98%都还是在用传统的MRP在跑物料…

利用 Python 开发手机 App 实战

Python语言虽然很万能&#xff0c;但用它来开发app还是显得有点不对路&#xff0c;因此用Python开发的app应当是作为编码练习、或者自娱自乐所用&#xff0c;加上目前这方面的模块还不是特别成熟&#xff0c;bug比较多&#xff0c;总而言之&#xff0c;劝君莫轻入。 准备工作 …

c++的学习之路:24、 二叉搜索树概念

摘要 本章主要是讲一下二叉搜索树的实现 目录 摘要 一、二叉搜索树概念 二、 二叉搜索树操作 1、二叉搜索树的查找 2、二叉搜索树的插入 3、二叉搜索树的删除 三、二叉搜索树的实现 1、插入 2、中序遍历 3、删除 4、查找 四、二叉搜索树的递归实现 1、插入 2、删…

Leetcode刷题之合并两个有序数组

Leetcode刷题之合并两个有序数组 一、题目描述二、题目解析 一、题目描述 给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2&#xff0c;另有两个整数 m 和 n &#xff0c;分别表示 nums1 和 nums2 中的元素数目。 请你 合并 nums2 到 nums1 中&#xff0c;使合并后的数…

去哪网拿去花不能提现,只能用于透支消费,那么拿去花提现是怎么实现呢?

去哪网拿去花不能提现&#xff0c;只能用于透支消费&#xff0c;那么拿去花提现是怎么实现呢&#xff1f; 申请携程拿去花之后&#xff0c;有一些人就会想着把钱提现出来拿去用。一般来说&#xff0c;他们都是通过线下门店来提现拿去花&#xff0c;拿去花允许用户先消费后付款&…

Python文件操作大全

1 文件操作 1.1 文件打开与关闭 1.1.1 打开文件 在Python中&#xff0c;你可以使用 open() 函数来打开文件。以下是一个简单的例子&#xff1a; # 打开文件&#xff08;默认为只读模式&#xff09; file_path example.txt with open(file_path, r) as file:# 执行文件操作…

LeetCode-二叉树修剪

每日一题 今天遇到的题比较简单&#xff0c;是一道二叉树的题。 题目要求 给定一个二叉树 根节点 root &#xff0c;树的每个节点的值要么是 0&#xff0c;要么是 1。请剪除该二叉树中所有节点的值为 0 的子树。 节点 node 的子树为 node 本身&#xff0c;以及所有 node 的…

appium2报错:Failed to create session. ‘automationName‘ can‘t be blank

1、问题概述&#xff1f; 今天在window环境中安装了appium2.5.2版本&#xff0c;通过appium inspector连接真机的时候报错如下&#xff1a; Failed to create session. automationName cant be blank 原因分析&#xff1a;这是因为appium2的比appium1有了很大的改进&#xff…

Linux 指令之文件

1.开发背景 记录 linux 下对文件操作的指令 2.开发需求 记录常用的文件操作指令 3.开发环境 linux 操作系统&#xff0c;如果不支持需要查看是否存在对应的可执行文件 4.实现步骤 4.1 查找字符串 查找指定目录下包含指定的字符串 grep -rn "Timer frequency" .…

python中如何求阶乘

第一种、利用functools工具处理 import functools result (lambda k: functools.reduce(int.__mul__, range(1, k 1), 1))(5) print(result)第二种、普通的循环 x 1 y int(input("请输入要计算的数:")) for i in range(1, y 1):x x * i print(x) 第三种、利用…

美格智能出席紫光展锐第三届泛金融支付生态论坛,引领智慧金融变革向新

4月16日&#xff0c;以“融智创新&#xff0c;共塑支付产业新生态”为主题的紫光展锐第三届泛金融支付生态论坛在福州举办&#xff0c;来自金融服务机构、分析师机构、终端厂商、模组厂商等行业各领域生态伙伴汇聚一堂&#xff0c;探讨金融支付产业的机遇与挑战。作为紫光展锐重…

浮点数的存储方式、bf16和fp16的区别

目录 1. 小数的二进制转换2. 浮点数的二进制转换3. 浮点数的存储3.1 以fp32为例3.2 规约形式与非规约形式 4. 各种类型的浮点数5. BF16和FP16的区别Ref 1. 小数的二进制转换 十进制小数转换成二进制小数采用「乘2取整&#xff0c;顺序排列」法。具体做法是&#xff1a;用 2 2…

数据结构复杂度

算法的时间复杂度 常对幂指阶 小练习1 小练习2

【实战】Dubbo应用可观测性升级指南与踩坑记录

应用从dubbo-3.1.*升级到dubbo-*:3.2.*最新稳定版本&#xff0c;提升dubbo应用的可观测性和度量数据准确性。 1. dubbo版本发布说明(可不关注) dubbo版本发布 https://github.com/apache/dubbo/releases 【升级兼容性】3.1 升级到 3.2 2. 应用修改点 注意&#xff1a;Sprin…

qutip,一个高级的 Python 量子力学研究库!

目录 前言 安装 特性 基本功能 量子态的创建和操作 量子态的测量 示例代码 动力学模拟 高级功能 退相干和噪声模拟 控制和优化 量子信息学工具 实际应用场景 量子态演化研究 量子计算机模拟 量子纠错协议 总结 前言 大家好&#xff0c;今天为大家分享一个高级的 Pytho…

机器学习理论入门---线性回归从理论到实践

线性回归是机器学习里面最简单也是最常用的算法&#xff0c;理解了线性回归的推导之后对于后续的学习有很大帮助&#xff0c;所以我决定从这里开始深入学习相关的机器学习模型。 本篇首先从矩阵求导开始切入&#xff0c;然后介绍一次线性回归的推导&#xff0c;再到代码实现。本…

酒店餐厅装水离子雾化壁炉前和装后对比

酒店餐厅装水离子雾化壁炉前和装后的对比可以体现出餐厅氛围和客户体验的显著改变&#xff1a; 装前&#xff1a; 普通的氛围&#xff1a;餐厅可能显得比较普通&#xff0c;缺乏特色或独特的装饰元素。 视觉上缺乏焦点&#xff1a;餐厅空间可能显得相对平淡&#xff0c;缺乏…

压缩感知(ISTA-Net论文)学习笔记

压缩感知&#xff08;ISTA-Net论文&#xff09;学习笔记 第一天&#xff0c;主要查找相关视频和笔记&#xff0c;补全预备知识 【nabla算子】与梯度、散度、旋度_哔哩哔哩_bilibili 近端梯度(Proximal Gradient)下降算法的过程以及理解|ISTA算法|LASSO问题_哔哩哔哩_bilibil…

华为ensp中静态路由和默认路由的原理及配置

作者主页&#xff1a;点击&#xff01; ENSP专栏&#xff1a;点击&#xff01; 创作时间&#xff1a;2024年4月17日17点37分 默认路由 [Router] ip route-static <目的网络> <目的网络掩码> <下一跳地址>默认路由的作用是将无法匹配路由表中其他路由表项的…

【行业前沿】制造业的数字化转型如何做?

随着科技的迅速发展&#xff0c;数字化转型已经成为制造型企业提高竞争力的关键因素。它可以帮助制造型企业&#xff0c;在产品优化设计、材料采购、生产流程方面实现精细化管理&#xff1b;提升上下游协同生产能力&#xff0c;提高生产效率、降低生产成本、优化产品质量&#…