learn-tech/专栏/计算机基础实战课/14走进C语言:高级语言怎样抽象执行逻辑?.md
2024-10-16 10:18:29 +08:00

208 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
14 走进C语言高级语言怎样抽象执行逻辑
你好我是LMOS。
前面我们设计了迷你CPU相信你已经对CPU如何执行指令有了一定的了解。
而CPU执行的指令正是由工程师敲出来的高级编程语言产生的比如我们熟知的C、C++、Java等。
平时我们做编程的时候底层CPU如何执行指令已经被封装好了因此你很少会想到把底层和语言编译联系在一起。但从我自己学习各种编程语言的经历看从这样一个全新视角重新剖析C语言有助于加深你对它的理解。
这节课我们首先要了解CPU执行指令的过程然后再来分析C语言的编译过程掌握C语言的重要组成最后我们再重点学习C语言如何对程序以及程序中的指令和数据进行抽象变成更易于人类理解的语言代码从这里下载
CPU执行指令的过程
通过之前的学习我们已经知道了CPU执行一条特定指令的详细过程它们是取指、译码执行、访存、回写。这是一个非常详细的硬件底层细节我们现在再迈一个台阶站高一点从软件逻辑的角度看看CPU执行多条指令的过程。
这个过程描述起来很简单,就是一个循环。为了让你分清每个步骤,我分点列一下:
以PC寄存器中值为内存地址A读取内存地址A中的数据;-
CPU把内存地址A中的数据作为指令执行具体执行过程为取指、译码执行、访存、写回;-
将PC寄存器中的值更新为内存地址A+(一条指令占用的字节数);-
回到第一步。
上述过程就是CPU执行指令的逻辑过程。下面我们动手来写几行代码调试一下观察一下内存的内容和CPU寄存器的变化这样你就更加清楚了。代码如下
.text
.global main
main: # main函数
add t1,zero,1 # x6 = 1
add t2,zero,2 # x7 = 2
add t0,t1,t2 # x5 = x6 + x7
add a0,zero,zero # x10 = 0 相当于main函数中的return 0
ret
这是一段RV32I指令集编写的汇编代码现在你无法完全理解这段代码也没关系能看懂注释就行了。
下面我们一起打开我为你们准备工程代码调试一下。如何用VSCode调试代码我在环境那节课讲过了记不清了可以回顾一下。
这里我们用VSCode打开本课的目录设置好断点按下“F5”键就行了如下所示。
我们看到t0、t1、t2寄存器中的数据和我们预期的一样。PC寄存器从0x10120一直变化到0x1012c每执行一条指令PC寄存器的值都要加4这是因为每条RV32I指令都占用4字节的内存空间。
我们在调试控制台中执行“-exec x/16xb 0x10120”命令即可显示从0x10120开始的16字节内存数据刚好4条指令的数据。我还在文稿里画了一幅图它展示了内存中的情况如下所示
对照示意图我来为你解释一下大致逻辑是这样的最开始由CPU控制单元通过控制总线发出要读取数据的控制信号。接着通过地址总线发送地址信号当前情况下地址数据来源PC寄存器0x10120然后通过数据总线传送指令数据(0x00100313)最后执行单元拿到指令数据开始执行并增加PC寄存器使之指向下一条指令。重复这个过程内存中的指令就能一条一条地执行了。
C语言编译过程
了解了CPU执行逻辑过程之后我们再来看看内存中的指令数据是如何产生的。
其实数据产生的方式有很多,我们可以用手写,也可以用电子编程器。只不过这样的方式,太古老了,也太低效了,更加容易出错。所以人们之后设计汇编语言时,实现了指令符号化,这看似高级了不少,但是依然是低效且难以理解的。
直到后来人们开发出了高级语言进一步抽象形成更容易让人们理解的形式。但是因为CPU始终只认识那些二进制数据就需要把高级语言转化成为二进制数据这个转化的过程叫编译过程完成这个转化的工具软件就叫编译器。
比如下面要讲的C语言编译器编译C语言的过程。我们先通过示意图来理解这一过程建立一个整体印象如下所示
现代的C预处理器、C编译器、汇编器、链接器是独立的程序可以分开独立工作并不是一个程序完成上图中所有的工作。
因为我们不开发编译器这里你不需要理解词法、语法是如何分析的中间代码是怎样优化的。我们要关注的焦点是从C源代码到二进制机器指令数据的转化过程。
C语言的重要组成
想要弄清楚C如何跟二进制指令数据转化首先要清楚C语言的重要组成部分。你可能会说C语言的重要组成当然是C语言代码。这个说法当然没错但代码只是一个统称。从不同层次抽象里面的内容是不一样的从高层次看代码中只有声明和定义下一层看代码只有函数和变量变量进一步分解还有不同的类型。
硬背这些分类只会让你晕头转向。接下来我们不妨分析一下想要让一段C语言代码编译通过需要哪些重要成分和逻辑结构。
我们在C语言中经常容易混淆声明和定义这两个概念我们先来看看声明。
声明是给变量、函数、结构体等命名,表明在程序代码中有该变量、函数、结构体,我们来看看下图中的代码:
我们在declaration.c程序文件中声明了一个整型变量一个结构体变量一个函数。然后我们编译它确实能编译成功这说明在C语言文法中仅仅需要有声明就可以当然空文件也是可以的。声明不会分配内存空间。
这里需要注意的是,只有声明的代码确实能编译成功,但链接的时候就不一定了,我们这里之所以能链接成功。是因为在其它代码中没有对这些声明进行了引用。
下面我们来看看定义,定义是具体给变量分配内存空间。这个内存空间可以是初始化的,也可以是没有初始化的、给出具体函数的实现。
具体函数可以是空函数,函数中没有语句什么都不做也可以,唯一必需的就是指明结构体成员。结构体也是变量,只不过结构体是多个变量的组合,同样要分配内存空间,可以初始化也可以不做初始化。
我们写代码验证一下对不对,如下图所示:
我们还是在definition.c程序文件中定义了一个整型变量一个结构体变量一个函数。我们同样能成功编译它。这说明C语言文法中没有声明只有定义也可以成功编译的其实C语言文法的原则是声明可以出现很多次定义有且只能出现一次。声明和定义也可以同时出现。
现在我来总结一下,其实编译的其中一个过程,就是用某种编程语言的文法来检查所写语言(代码)是否正确。你可以这么理解,语言的文法就是对这种语言的最高抽象,所以我们可以说 C语言最重要的组成部分就是声明或者定义。
声明或者定义中又包含变量和函数,变量又有指针、数组、结构体,它们又包含各种类型,而函数中包含了各种表达式,各种表达式对变量进行操作。
编译器的语法分析过程,就是这样层层递归推导下去,最终构建出语法树,从而检查语言是否正确无误、是否符合该语言文法的规则定义,都符合编译才能通过。就像你学英文一样,你怎么判断一条英语句子是否正确呢?你会拿主谓宾等等约定俗成的语法去套,如果能套上去,就是正确的。
C语言对程序的抽象
前面我们已经从整体上了解了C语言的重要组成可以看出一段程序从语法角度来说就是声明加上定义。
现在我们继续深入了解C语言对程序的抽象平时我们最多是使用设计好的C语言。而现在我们要站在C语言设计者的角度想一想一门既能写程序又更容易让人类理解的语言要怎么设计其实这门语言的“设计过程”就是C语言对机器语言的抽象也就是C语言对程序的抽象。
我特意为你绘制了一幅图我们从C语言的核心语言元素开始了解。这些元素就像英文里的各种固定词型知道了这些元素在C语言中发挥的作用你就找到了理解C语言的钥匙。
我们看到C语言中包含声明和定义可以声明变量和函数由图中绿色箭头指向。也可以定义变量和函数由图中蓝色箭头指向注意定义只能出现一次声明可以出现多次。
我故意安排指针在最前端是因为从C语言特性讲指针能指向任一变量和函数由图中红色箭头指向从另一个角度看指针就是内存能自由寻址读写内存空间但能否读写内存则要看操作系统给的权限指针就是C语言中的“上帝之手”。同时图中黑色线条还表示指针可以有相应的类型并且能参与运算这是我把指针放在比函数更高位置的原因。
需要注意的是,各种类型的变量是可以定义在函数以外的,这些定义在函数以外的变量是全局变量,而定义在函数内部的变量叫局部变量。
如果我们要用C语言完成一个实际功能一定要写一个函数。函数就是C语言中对一段功能代码的抽象。一个函数就是一个执行过程有输入参数也有返回结果根据需要可有可无代码如下所示
void func()
{
return;
}
上面代码中的函数是空函数C语言是允许的当然这样的函数不会完成任何功能。
如果我们要完成点什么功能,就要在函数中写代码语句。代码语句又被抽象成表达式和流程控制。这也是为什么上图中函数下面包含了表达式和流程控制。
接下来,我们写个完成求两数之差、求两数之和的函数,代码如下所示:
int func(int op, int a, int b)
{
if(op < 1) //表达式op < 1
{
return a - b;//表达式a - b
}
return a + b;//表达式a + b
}
上面代码中有三个表达式注释中已经写明了含有三种流程控制if判断分支控制return返回控制还有默认的从上至下的代码顺序你可以把上述代码拿到上图中去套以证明C语言对代码语句的抽象你会发现一套一个准
下面我们继续研究一下表达式从前面的图里可以看到C语言表达式包含了变量和运算符
变量又有各种类型单个变量也是表达式但是运算符不能单独存在变成表达式所以C语言表达式要么是单个变量要么是变量加运算符一起根据运算符的类型不同可以分成运算表达式逻辑表达式赋值表达式等
下面我们使用代码实例来找找感觉如下所示
int sumdata = 0;//全局整型变量sumdata
void func()
{
int i = 1;//局部整型变量i
int *p; //局部整型指针变量p
p = &sumdata;//把sumdata变量的地址赋值给p变量从而指向sumdata变量
while(1)//循环流程控制
{
if(i > 100)
{
break;//跳出循环,流程控制
}
(*p) += i;//相当于sumdata = sumdata + i
i = i + 1;
}
return;
}
上述代码所有的表达式中,涉及了一个全局变量,两个局部变量。其中局部变量中有一个是指针变量,指向全局的变量。包含了更多的流程控制语句,可以明显地看到表达式就是:变量和运算符组合在一起,完成了对变量的操作。而变量代表了数据,最终就能实现对数据的运算。但是变量有各种类型,这些类型只是规范了变量的位宽和大小,下一节我们会有更详细的介绍。
现在我们就可以总结一下C语言是如何抽象程序的如下表所示。
这就是C语言对程序的抽象。到这里今天的课程也到了尾声你是否像我一样想起了那个著名的公式程序=算法+数据结构?
没错C 语言就是函数 + 变量。函数表示算法操作,变量存放数据,即数据结构,合起来就是程序 = 算法 + 数据结构。
C语法的运算符和流程控制可以实现各种算法而各种类型的变量组合起来就能实现各种复杂的数据结构。理解了这些你就抓住了C语言的本质也为后续学习打下了良好的基础。
重点回顾
到这里今天的课程就告一段落了,我来总结一下这节课的重点。
首先我们研究了CPU执行指令过程和C语言编译过程。理解了这两个过程后续的学习就有了良好基础。
接着我们进入到C语言内部从宏观上理解了定义和声明这两个C语言的重要组成部分。声明只是一种说明性质的东西不产生机器指令而定义则是实现会产生对应的机器指令。
最后我们从C语言的核心语言元素入手抽丝剥茧层层解构。C语言由函数构成函数中又包含多条语句语句由流程控制和表达式构成表达式由各种类型的变量和各种运算符构成。这些东西组合在一起就把机器执行的程序抽象成了人类易于掌握和理解的概念——C语言。文稿里我总结的那张图你不妨保存下来作为你学习理解C语言的导航图。
下节课我们继续研究C语言和汇编语言的对应关系敬请期待。
思考题
为什么C语言中为什么要有流程控制
期待你在留言区踊跃发言积极思考有助于你更好地领会课程内容。也推荐你把这节课分享给身边的朋友说不定就能让他进一步掌握C语言了。