C语言第十九课:补充知识——函数栈帧的创建与销毁

news2024/9/22 17:28:57

目录

前言:

一、寄存器:

二、函数栈帧的创建与销毁:

        1.main函数函数栈帧开辟的准备:

        2.main函数函数栈帧的开辟:

        3.Add函数函数栈帧的开辟:

        4.Add函数函数栈帧的销毁:

        5.main函数函数栈帧的销毁:

三、总结:


前言:

        今天是连续爆更的第五天喽,日更的銮同学更博客不易,辛苦各位路过的小伙伴们点点关注点点赞,给我继续爆更的动力!而今天我将对前面函数部分的学习内容的一个补充知识作以讲解:函数栈帧的创建与销毁

        在前面学习函数时,我们会产生很多疑惑,比如:

        · 局部变量是怎么创建的?

        · 为什么局部变量的值是随机值?

        · 函数是怎么传参的?传参的顺序又是怎样的?

        · 形参和实参是什么关系?

        · 函数调用是怎么做的?

        · 函数调用结束后是怎么返回的?

        而以上这些疑惑,都将在今天的知识内容学习后得到解答。并且今天的内容,建议使用的环境是Visual Studio 2013,最好不要使用太高级的编译器,因为我们在学习的过程中需要随时对我们的程序运行过程进行观测,而越是高级的编译器自动优化越优秀封装也更加复杂,随之而来的就是越不容易学习和观察。同时,在不同的编译器下,函数调用过程中栈帧的创建和销毁大体逻辑上是相同的,但细节上是略有差异的,具体细节取决于编译器的实现

一、寄存器:

        在开始了解函数栈帧的创建与销毁之前,我们首先要了解一下计算机CPU内部的重要组成——寄存器

        寄存器是CPU内部用来存放数据的一些小型存储区域,它们是物理的固定的,用来暂时存放参与运算的数据和运算结果。其实寄存器就是一种常用的时序逻辑电路,但这种时序逻辑电路只包含存储电路。寄存器的存储电路是由锁存器触发器构成的,因为一个锁存器或触发器能存储1位二进制数,所以由N个锁存器或触发器可以构成N位寄存器。寄存器是中央处理器内的组成部分。寄存器是有限存储容量的高速存储部件,它们可用来暂存指令、数据和位址

        寄存器最起码具备以下4种功能。

        ①.清除数码:将寄存器里的原有数码清除。

        ②.接收数码:在接收脉冲作用下,将外输入数码存入寄存器中。

        ③.存储数码:在没有新的写入脉冲来之前,寄存器能保存原有数码不变。

        ④.输出数码:在输出脉冲作用下,才通过电路输出数码。

        仅具有以上功能的寄存器称为数码寄存器;有的寄存器还具有移位功能,称为移位寄存器

        寄存器有串行并行两种数码存取方式。将N位二进制数一次存入寄存器或从寄存器中读出的方式称为并行方式。将N位二进制数以每次1位,分成N次存入寄存器并从寄存器读出,这种方式称为串行方式。并行方式只需一个时钟脉冲就可以完成数据操作,工作速度快,但需要N根输入和输出数据线。串行方式要使用几个时钟脉冲完成输入或输出操作,工作速度慢,但只需要一根输入或输出数据线,传输线少,适用于远距离传输

        寄存器分为很多种类型:

        ①.通用寄存器组

        ②.指针和变址寄存器

        ③.段寄存器

        ④.指令指针寄存器

        ⑤.程序状态字寄存器

        在研究函数栈帧时,我们主要研究指针和变址寄存器。而指针和变址寄存器又可以细分为:

        · BP( Base Pointer Register):基址指针寄存器。

        · SP( Stack Pointer Register):堆栈指针寄存器。

        · SI( Source Index Register):源变址寄存器。

        · DI( Destination Index Register):目的变址寄存器。

        而今天我们的学习要关注的便是扩展基址指针寄存器 EBP 扩展堆栈指针寄存器 ESP

        ★EBP:扩展基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈底

        ★ESP:扩展堆栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶

二、函数栈帧的创建与销毁:

        在前面我们说过,每一个函数在进行创建和使用时,都会在内存中创建一块临时空间,并在临时空间内进行函数的处理。今天我们的任务就时更加详细的去认识和了解这一过程。

        每一个函数在创建和使用时,都将会在内存的栈区创建一个临时的空间:

栈区(低地址)


main函数的函数栈帧
 
栈区(高地址)

        此处提到的高地址与低地址都是相对的,而内存在进行使用时总是习惯像向枪械的弹匣压入子弹一般,将数据依次压入,即优先使用高地址后使用低地址

        例如我们尝试在VS 2013中观察下面代码的堆栈调用

#define _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>

int Add(int x, int y)
{
	int z = x + y;
	return z;
}

int main()
{
	int a = 10;
	int b = 20;
	int c = Add(a, b);
	printf("%d + %d = %d\n", a, b, c);

	return 0;
}

        此时就在内存中的栈区为main函数分配出了一块空间用于进行函数处理,而在分配空间时,main函数的函数栈帧的维护就交由寄存器 EBP 与 ESP 来完成。同时我们可以观察到,main函数也是被我们看不到的其它函数(__tmainCRTStartup/mainCRTStartup)所调用的

        并且在main函数中的Add函数也在栈区分配了自己的空间

栈区(低地址)

Add函数的函数栈帧

栈区

main函数的函数栈帧

栈区(高地址)

        同样的,函数在开辟空间时,也遵循栈区先使用高地址再使用低地址的原则,先在高地址处为先执行的main函数分配出空间再为后使用的Add函数在低地址处分配空间

        我们将程序进入调试,并转入反汇编,通过汇编代码来查看其处理过程:

        这里为了方便我们查看,我将汇编代码复制过来进行研究。其中压栈(push)指给栈顶放置一个元素出栈(pop)指从栈顶删除一个元素

int main()
{
001C18B0  push        ebp  
001C18B1  mov         ebp,esp  
001C18B3  sub         esp,0E4h  
001C18B9  push        ebx  
001C18BA  push        esi  
001C18BB  push        edi  
001C18BC  lea         edi,[ebp-24h]  
001C18BF  mov         ecx,9  
001C18C4  mov         eax,0CCCCCCCCh  
001C18C9  rep stos    dword ptr es:[edi]  
001C18CB  mov         ecx,1CC008h  
001C18D0  call        001C131B  
	int a = 10;
001C18D5  mov         dword ptr [ebp-8],0Ah  
	int b = 20;
001C18DC  mov         dword ptr [ebp-14h],14h  
	int c = Add(a, b);
001C18E3  mov         eax,dword ptr [ebp-14h]  
001C18E6  push        eax  
001C18E7  mov         ecx,dword ptr [ebp-8]  
001C18EA  push        ecx  
001C18EB  call        001C10B4  
001C18F0  add         esp,8  
001C18F3  mov         dword ptr [ebp-20h],eax  
	printf("%d + %d = %d\n", a, b, c);
001C18F6  mov         eax,dword ptr [ebp-20h]  
001C18F9  push        eax  
001C18FA  mov         ecx,dword ptr [ebp-14h]  
001C18FD  push        ecx  
001C18FE  mov         edx,dword ptr [ebp-8]  
001C1901  push        edx  
001C1902  push        1C7B30h  
001C1907  call        001C10D2  
001C190C  add         esp,10h  

	return 0;
001C190F  xor         eax,eax  
}
001C1911  pop         edi  
001C1912  pop         esi  
001C1913  pop         ebx  
001C1914  add         esp,0E4h  
001C191A  cmp         ebp,esp  
001C191C  call        001C1244  
001C1921  mov         esp,ebp  
001C1923  pop         ebp  
001C1924  ret

        1.main函数函数栈帧开辟的准备:

        我们看到,在main函数一开始,第一行首当其冲的就是压栈操作

001C18B0  push        ebp

        它的意义是,将维护完上一个空间的寄存器 EBP进行回收,并用于(push)main函数分配空间的压栈维护。紧接着由于 EBP 的重新指向寄存器 ESP 也进行回收并指向(mov) EBP 之后

001C18B1  mov         ebp,esp

        再接下来,ESP继续向后指向,而向后移动的距离,就是main函数所申请的空间大小

001C18B3  sub         esp,0E4h
//0E4h为十六进制数

        接下来再次在 ESP 后压(压栈)上三个元素。在这里我们无需关心这三个元素,这与我们此处的讲解无关,后面它们也会自行弹出:

001C18B9  push        ebx  
001C18BA  push        esi  
001C18BB  push        edi 

        接下来我们看到,在 edi 中存入了一个地址,而我们经过内存的查看发现,这个地址恰恰是main函数真正使用的内存栈空间的结束地址

001C18BC  lea         edi,[ebp-24h]

        再接下来,我们看到将 9 个 dword 数据(每个 double word 数据占四个字节)全部改为0CCCCCCCCh,即将mian函数实际占用的空间内数据全部改为0CCCCCCCCh:

001C18BF  mov         ecx,9  
001C18C4  mov         eax,0CCCCCCCCh  
001C18C9  rep stos    dword ptr es:[edi] 

        截至到这里,为main函数的函数栈帧的开辟所作的准备工作就全部结束了

        2.main函数函数栈帧的开辟:

        然后正式进入了程序,首先定义变量a并将其值初始化为10,即将十六进制数 0AH (10)放入(mov)地址 [ebp-8] 中。并且我们在上面知道了,EBP所指向的是main函数的起始位置并从该位置起向后的四个字节(一个整型变量的大小)分配给变量 a 使用:

	int a = 10;
001C18D5  mov         dword ptr [ebp-8],0Ah

        同样的,变量b与c的创建也是相同的原理

	int b = 20;
001C18DC  mov         dword ptr [ebp-14h],14h
	int c = Add(a, b);
001C18E3  mov         eax,dword ptr [ebp-14h]

        再往后我们看到程序来到了我们的函数调用处,而在这里我们首先进行的便是函数的传参。而在传参时,首先进行的是将两个参数a、b中后压入的变量 b 的值,即20,放入(mov)寄存器 eax 中

001C18E3  mov         eax,dword ptr [ebp-14h]

        在将变量b的值存入寄存器eax后将eax进行压栈操作压至栈顶

001C18E6  push        eax

        并对之后压入的变量a执行同样的操作

001C18E7  mov         ecx,dword ptr [ebp-8]
001C18EA  push        ecx

        接着执行了call指令,并且我们通过内存查看,发现call指令将它下一条指令的地址压在了变量之后,并且这个地址不是没有用处的,相反该地址极其重要,我们的程序是按照语句顺序进行执行的,在这里调用过Add函数并在该函数指执行完成之后要回到这个位置并继续向后顺序执行此时这里存储的地址在Add函数执行完成回归主函数时就十分重要了,并且接下来才真正进入到了Add函数之中

001C18EB  call        001C10B4
001C18F0  add         esp,8

        3.Add函数函数栈帧的开辟:

        接着在执行Add函数时,执行的流程与上面main函数的执行流程完全相同:

int Add(int x, int y)
{
001C1770  push        ebp  
001C1771  mov         ebp,esp  
001C1773  sub         esp,0CCh  
001C1779  push        ebx  
001C177A  push        esi  
001C177B  push        edi  
001C177C  lea         edi,[ebp-0Ch]  
001C177F  mov         ecx,3  
001C1784  mov         eax,0CCCCCCCCh  
001C1789  rep stos    dword ptr es:[edi]  
001C178B  mov         ecx,1CC008h  
001C1790  call        001C131B  
	int z = x + y;
001C1795  mov         eax,dword ptr [ebp+8]  
001C1798  add         eax,dword ptr [ebp+0Ch]  
001C179B  mov         dword ptr [ebp-8],eax  
	return z;
001C179E  mov         eax,dword ptr [ebp-8]  
}
001C17A1  pop         edi  
001C17A2  pop         esi  
001C17A3  pop         ebx  
001C17A4  add         esp,0CCh  
001C17AA  cmp         ebp,esp  
001C17AC  call        001C1244  
001C17B1  mov         esp,ebp  
001C17B3  pop         ebp  
001C17B4  ret

        并且我们在其中也看到,实际在进行操作时,操作的并不是变量a与变量b的实际地址内的数据,而是在上面进行函数调用传参时传递过来的寄存器 eax 与 ecx 中所存储的形式参数

001C1795  mov         eax,dword ptr [ebp+8]
001C1798  add         eax,dword ptr [ebp+0Ch]
001C179B  mov         dword ptr [ebp-8],eax

        如此我们再来回忆,在我们即将但还没有调用函数之前,我们就已经将参数传递到了寄存器分配的临时空间内,并在之后真正进行函数调用时,操作的是寄存器中所存储的数据。因此我们说函数在进行传值调用是操作的是main函数变量的一份临时拷贝的说法完全正确!

        并在Add函数进行了计算之后,将值赋给了变量 z

	int z = x + y;
001C1795  mov         eax,dword ptr [ebp+8]

        在计算完成后,Add函数将要按照问我们的要求返回数据。它的做法是将计算出的结果z的值,放入寄存器 eax 之中,这么做的原因是,我们也都知道一旦函数执行完成所有Add函数的空间将被销毁并回收,但寄存器不会被销毁或回收,于是我们通过使用全局的寄存器,才可以实现将Add函数的结果返回给我们的主程序:

	return z;
001C179E  mov         eax,dword ptr [ebp-8]

        4.Add函数函数栈帧的销毁:

        接下来函数Add函数的函数栈帧便会开始销毁,首先将同样放在栈顶的三个元素由低地址到高地址依次弹出

001C17A1  pop         edi
001C17A2  pop         esi
001C17A3  pop         ebx
001C17A4  add         esp,0CCh

        接着将指向Add函数空间栈顶的 ESP 重新指向 EBP,Add函数函数空间被销毁并回收,即Add函数的函数栈帧被销毁

001C17AA  cmp         ebp,esp

        再接下来,继续弹出此时的栈顶 EBP,此时的 EBP 一经弹出,便找回并指向之前保存在栈顶的main函数的函数栈帧。而此时,前来维护Add函数函数栈帧的 EBP 与 ESP 也得到了释放又回到了main函数中继续维护main函数的函数栈帧

001C17B3  pop         ebp

         最后,执行最后一条 ret 指令,而此时 ret 指令要回到的,又恰恰是此前 call 指令存储在EBP弹出后变为栈顶的那个地址

001C17B4  ret

        接着我们就回到了主函数中,并且此时,存储在此时的栈顶的我们在进行函数传参时所使用的临时变量也被全部弹出

001C18F0  add         esp,8  

         接着再把我们前面存储在全局寄存器中eax中的返回值赋给变量c

001C18F3  mov         dword ptr [ebp-20h],eax

        至此,函数Add的生命彻底结束,成功的回到了主函数之中

        5.main函数函数栈帧的销毁:

        再接下来main函数函数栈帧的销毁过程,与Add函数完全一致,在此我们也就不再做过多的阐述,各位小伙伴们可以对照这上面Add函数的销毁过程,自己尝试着去分析接下来main函数函数栈帧的销毁过程:

	printf("%d + %d = %d\n", a, b, c);
001C18F6  mov         eax,dword ptr [ebp-20h]  
001C18F9  push        eax  
001C18FA  mov         ecx,dword ptr [ebp-14h]  
001C18FD  push        ecx  
001C18FE  mov         edx,dword ptr [ebp-8]  
001C1901  push        edx  
001C1902  push        1C7B30h  
001C1907  call        001C10D2  
001C190C  add         esp,10h  

	return 0;
001C190F  xor         eax,eax  
}
001C1911  pop         edi  
001C1912  pop         esi  
001C1913  pop         ebx  
001C1914  add         esp,0E4h  
001C191A  cmp         ebp,esp  
001C191C  call        001C1244  
001C1921  mov         esp,ebp  
001C1923  pop         ebp  
001C1924  ret

三、总结:

        到这里我们关于函数栈帧的讲解也就全部结束了。函数栈帧的部分略显得有些晦涩难懂,在这里对其进行讲解的意义在于帮助各位小伙伴们从更深层次去了解和理解函数的运作过程。函数作为我们极其常用的代码组成,而函数栈帧又作为函数部分的补充知识,希望各位先伙伴们能够多看几遍,仔细揣摩,认真思考,努力理解,争取掌握。

        希望今天的学习能够增加各位小伙伴们对于函数栈帧的理解,和对函数运作过程的认识,帮助小伙伴们尽可能的避免在使用函数时可能遇到的问题。没有拼尽全力,就别推脱运气不好;没有竭尽所能,就别抱怨命运不公;很多时候我们只是看到别人光鲜亮丽的一面,而忽略了别人背后的付出!

        新人初来乍到,辛苦各位小伙伴们动动小手,三连走一走 ~ ~ ~  最后,本文仍有许多不足之处,欢迎各位看官老爷随时私信批评指正!

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

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

相关文章

Linux多线程

Linux多线程 文章目录Linux多线程1.Linux线程概念1.1 什么是线程1.2 线程的执行流1.3 线程的优缺点1.4 线程的异常与用途2.进程与线程的区别总结3.多进程与多线程的应用场景4.线程控制4.1 进程ID与线程ID4.2 线程创建(pthread_create)4.3 线程终止(pthread_exit、pthread_cance…

Unity计算着色器 01

序 计算着色器&#xff0c;是什么&#xff1f; Unity中ComputeShader的基础介绍与使用 - 知乎 (zhihu.com) 好像是并行计算的一个东西。再具体的&#xff0c;看不懂了。 并行计算&#xff0c;显卡&#xff1f; 那看来得先了解显卡&#xff0c;铺垫一下&#xff0c;再计划…

算法前缀和

一 和为 k 的子数组 1.1 题目描述 给定一个整数数组和一个整数 k &#xff0c;请找到该数组中和为 k 的连续子数组的个数。 示例 1&#xff1a; 输入:nums [1,1,1], k 2 输出: 2 解释: 此题 [1,1] 与 [1,1] 为两种不同的情况 示例 2&#xff1a; 输入:nums [1,2,3], k…

由浅入深,详解 LiveData 的那些事

引言 关于LiveData,在2022尾声的今天&#xff0c;从事 Android 开发的小伙伴一定不会陌生。相应的&#xff0c;关于 LiveData 解析与使用的文章更是数不胜数&#xff0c;其中不乏优秀的创作者&#xff0c;在众多的文章以及前辈面前&#xff0c;本篇也不敢妄谈能写的多么深入,易…

python使用Flask,Redis和Celery的异步任务

介绍 随着Web应用程序的发展和使用的增加&#xff0c;用例也变得多样化。我们现在正在建设和使用网站来执行比以往任何时候都更复杂的任务。其中一些任务可以进行处理&#xff0c;并将反馈立即转发给用户&#xff0c;而其他任务则需要稍后进行进一步处理和结果转发。越来越多地…

冯诺依曼体系各硬件工作原理解析

文章目录计算机结构体系来源冯诺依曼体系结构主存储器的基本组成运算器的基本组成控制器的基本组成计算机的工作过程总结计算机结构体系来源 1946年,美国发明了世界上第一台计算机ENIAC,可用于比较快速的数据计算,但是其运算速度却受到了人工数据的输入速度的限制,为此我们现在…

NNDL实验 优化算法3D轨迹 鱼书例题3D版

这张图在网络上很流行。代码源自&#xff1a; 深度学习入门&#xff1a;基于Python的理论与实现 (ituring.com.cn) 2D版讲解&#xff1a;NNDL 作业11&#xff1a;优化算法比较 调整学习率等超参数&#xff0c;观察动画&#xff0c;可以加深对各种算法的理解。 配合实验的模型…

南方农业杂志南方农业杂志社南方农业编辑部2022年第19期目录

遗传育种 峨眉黑鸡遗传多样性及群体遗传结构分析 袁霞;刘方庆;文陇英;徐婧;廖光祥;王强胜;王湘; 1-7 栽培与植保《南方农业》投稿&#xff1a;cn7kantougao163.com 井窖式移栽烤烟前期地上部和地下部生长规律拟合分析 温明霞;郭发文;冯小芽;王军;刘京;彭剑涛;廉云; 8-1…

从进程的角度来看JVM的内存分布

JVM(下面JVM都是指代HotSpot)本质上是运行在操作系统上的一个C程序&#xff0c;本文会从这个角度来构建对于JVM内存的完整视角&#xff0c;以HotSpot这个JVM实现运行在Linux操作系统上进行分析&#xff0c;在分析的过程中会解释清楚一些不太好理解的概念&#xff0c;诸如堆外内…

双十二哪些数码好物值得入手?盘点双十二最值得入手的数码好物

双十二快到了&#xff0c;相信很多人像我一样想趁着年末入手数码产品&#xff0c;但又不知道什么值得入手。最近也听到很多人问&#xff0c;针对这个问题&#xff0c;我来给大家盘点双十二最值得入手的数码好物&#xff0c;有需要的可以当个参考。 一、南卡小音舱蓝牙耳机 推…

怎样批量查询网站是否被搜狗收录?批量查询网站搜狗收录的详细教程

怎样批量查询网站是否被搜狗收录&#xff1f;批量查询网站搜狗收录的详细教程 批量查询网站搜狗收录的的具体操作&#xff1a; 第一步、打开站长工具 第二步、添加需要查询的网站域名 第三步、勾选要查询的功能&#xff08;勾选搜狗是否收录和搜狗总收录) 第四步、提交查询 第…

手把手教你在Ubuntu22.04 上安装 Vivado、HLS、Vitis 2022.2版本

文章目录1 Vivado22.2 和 HLS 22.2 安装下载安装包执行.bin文件开始安装命令配置启动问题2 Vitis 22.2 安装3 卸载Xilinx我是 雪天鱼&#xff0c;一名FPGA爱好者&#xff0c;研究方向是FPGA架构探索和数字IC设计。欢迎来关注我的B站账号&#xff0c;我将定期更新IC设计教程。 …

手撕红黑树 | 变色+旋转你真的明白了吗?【超用心超详细图文解释 | 一篇学会Red_Black_Tree】

说在前面 我们也很久没有更新数据结构系列了&#xff0c;半年前博主重新深入学习了红黑树这个数据结构&#xff0c;一直想更新呈现给大家&#xff0c;最近也一直没有时间&#xff0c;今天红黑树它来了&#xff01; 博主为了这篇博客&#xff0c;做了很多准备&#xff0c;试了…

怎么修复老照片?给你推荐这几个修复方法

相信大家的家里都有老照片吧&#xff0c;那在你们翻看这些老照片的时候&#xff0c;有没有发现有些老照片变得有些破旧、泛黄、模糊等情况呢&#xff1f;看到这些情况&#xff0c;大家是不是会很心疼呢&#xff1f;因为这些老照片都充满了各种各样的回忆&#xff0c;根本拍不出…

Docker查看容器的初始启动命令参数的常见几种方式

1.在使用docker容器的过程中&#xff0c;经常需要查看容器启动的命令来看当时启动容器时候所用的参数&#xff0c;如果时间不是很久或者通过history命令就可以很容易的想起或查看到命令&#xff0c;一旦时间过了很久或history被清空那么就无法获取命令&#xff0c;如下所示dock…

cengbox2靶机(hydra爆破、公私钥免密登录)

环境准备 靶机链接&#xff1a;百度网盘 请输入提取码 提取码&#xff1a;zdpr 虚拟机网络链接模式&#xff1a;桥接模式 攻击机系统&#xff1a;kali linux 2021.1 信息收集 1.arp-scan -l探测目标靶机ip 2.nmap -p- -A -T4 192.168.1.107 探测目标靶机开放端口和服务 …

Docker基本使用

1、centos7安装docker engine 参考文档&#xff1a;https://docs.docker.com/engine/install/centos/ &#xff08;1&#xff09;卸载之前的docker sudo yum remove docker \docker-client \docker-client-latest \docker-common \docker-latest \docker-latest-logrotate \…

Spring——AOP

Spring中的可插拔组件技术 Spring AOP Spring AOP——Aspect Oriented Programming 面向切面编程AOP 的做法是将通用的、与业务无关的功能抽象封装为切面层切面可配置在目标方法执行前后&#xff0c;做到即插即用 不修改源码对程序功能进行拓展 AoP的关键概念 Spring AoP 与A…

栈与队列3:有效的括号

主要是我自己刷题的一些记录过程。如果有错可以指出哦&#xff0c;大家一起进步。 转载代码随想录 原文链接&#xff1a; 代码随想录 leetcode链接&#xff1a;20. 有效的括号 题目&#xff1a; 给定一个只包括 ‘(’&#xff0c;‘)’&#xff0c;‘{’&#xff0c;‘}’&am…

怎样才能批量查询网站的谷歌PR权重?把手教你批量查询网站谷歌PR权重值

谷歌PR是Google排名运算法则&#xff08;排名公式&#xff09;的一部分&#xff0c;用来标识网页的等级/重要性。在计算网站排名时&#xff0c;PageRank会将网站的外部链接数考虑进去。一个网站的外部链接数越多其PR值就越高&#xff1b;外部链接站点的级别越高&#xff0c;网站…