C语言:高级语言怎样抽象执行逻辑
平时我们做编程的时候,底层 CPU 如何执行指令已经被封装好了,因此你很少会想到把底层和语言编译联系在一起。但从我自己学习各种编程语言的经历看,从这样一个全新视角重新剖析 C 语言,有助于加深你对它的理解。
本节首先要了解 CPU 执行指令的过程,然后再来分析 C 语言的编译过程,掌握 C 语言的重要组成,最后我们再重点学习 C 语言如何对程序以及程序中的指令和数据进行抽象,变成更易于人类理解的语言(代码从这里下载)。
CPU执行指令的过程
CPU执行一条特定指令的详细过程是取指、译码、执行、访存、回写,这是一个非常详细的硬件底层细节,本节将从软件逻辑的角度看CPU执行多条指令的过程。
这个过程描述起来很简单,就是一个循环:
1.以PC寄存器中值为内存地址A,读取内存地址A中的数据
2.CPU把内存地址A中的数据作为指令执行,具体执行过程:取指、译码、执行、访存、回写
3.将PC寄存器中的值更新为内存地址A+(一条指令占用的字节数)
4.回到第一步
上述过程就是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 指令集编写的汇编代码,设置好断点,执行如下:

看到 t0、t1、t2 寄存器中的数据和我们预期的一样。PC 寄存器从 0x10120,一直变化到 0x1012c,每执行一条指令 PC 寄存器的值都要加 4,这是因为每条 RV32I 指令都占用 4 字节的内存空间。
在调试控制台中执行-exec x/16xb 0x10120命令,即可显示从 0x10120 开始的 16 字节内存数据,刚好 4 条指令的数据
下图展示了内存中的情况:

大致逻辑是这样的:最开始,由 CPU 控制单元通过控制总线发出要读取数据的控制信号;接着通过地址总线发送地址信号(当前情况下地址数据来源 PC 寄存器(0x10120));然后通过数据总线传送指令数据 (0x00100313);最后执行单元拿到指令数据开始执行,并增加 PC 寄存器使之指向下一条指令。重复这个过程,内存中的指令就能一条一条地执行了。
C语言编译过程
因为 CPU 始终只认识那些二进制数据,就需要把高级语言转化成为二进制数据,这个转化的过程叫编译过程,完成这个转化的工具软件就叫编译器。
比如C语言编译器编译C语言的过程,先通过下图来理解下这个过程,建立一个整体印象:

现代的 C 预处理器、C 编译器、汇编器、链接器是独立的程序,可以分开独立工作,并不是一个程序完成上图中所有的工作。
因为我们不开发编译器,这里你不需要理解词法、语法是如何分析的,中间代码是怎样优化的。我们要关注的焦点是,从 C 源代码到二进制机器指令数据的转化过程。
C语言的重要组成
想要弄清楚 C 如何跟二进制指令数据转化,首先要清楚 C 语言的重要组成部分
从不同层次抽象,里面的内容是不一样的:从高层次看代码中只有声明和定义,下一层看代码只有函数和变量,变量进一步分解还有不同的类型。
硬背这些分类只会让你晕头转向。接下来我们不妨分析一下,想要让一段 C 语言代码编译通过,需要哪些重要成分和逻辑结构。
在 C 语言中经常容易混淆声明和定义这两个概念,先来看看声明
- 声明是给变量、函数、结构体等命名,表明在程序代码中有该变量、函数、结构体,来看看下图中的代码:

在 declaration.c 程序文件中,声明了一个整型变量,一个结构体变量,一个函数。然后我们编译它,确实能编译成功,这说明在 C 语言文法中仅仅需要有声明就可以,当然空文件也是可以的。声明不会分配内存空间。
需要注意的是,只有声明的代码确实能编译成功,但链接的时候就不一定了,我们这里之所以能链接成功。是因为在其它代码中没有对这些声明进行了引用。
- 定义是具体给变量分配内存空间。这个内存空间可以是初始化的,也可以是没有初始化的、给出具体函数的实现。
具体函数可以是空函数,函数中没有语句什么都不做也可以,唯一必需的就是指明结构体成员。结构体也是变量,只不过结构体是多个变量的组合,同样要分配内存空间,可以初始化也可以不做初始化。
写代码验证一下对不对,如下图所示:

还是在 definition.c 程序文件中,定义了一个整型变量,一个结构体变量,一个函数。我们同样能成功编译它。这说明 C 语言文法中没有声明,只有定义也可以成功编译的,其实 C 语言文法的原则是,声明可以出现很多次,定义有且只能出现一次。声明和定义也可以同时出现。
编译的其中一个过程,就是用某种编程语言的文法来检查所写语言(代码)是否正确。你可以这么理解,语言的文法就是对这种语言的最高抽象,所以我们可以说 C 语言最重要的组成部分就是声明或者定义。
声明或者定义中又包含变量和函数,变量又有指针、数组、结构体,它们又包含各种类型,而函数中包含了各种表达式,各种表达式对变量进行操作。
编译器的语法分析过程,就是这样层层递归推导下去,最终构建出语法树,从而检查语言是否正确无误、是否符合该语言文法的规则定义,都符合编译才能通过。就像你学英文一样,你怎么判断一条英语句子是否正确呢?你会拿主谓宾等等约定俗成的语法去套,如果能套上去,就是正确的。
C语言对程序的抽象

看到 C 语言中,包含声明和定义,可以声明变量和函数,由图中绿色箭头指向。也可以定义变量和函数,由图中蓝色箭头指向,注意定义只能出现一次,声明可以出现多次。
故意安排指针在最前端,是因为从 C 语言特性讲,指针能指向任一变量和函数,由图中红色箭头指向;从另一个角度看,指针就是内存,能自由寻址读写内存空间,但能否读写内存则要看操作系统给的权限,指针就是 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语言:高级语言怎样抽象执行逻辑的更多相关文章
- scala(二) Future执行逻辑解读
在scala中是没有原生线程的,其底层使用的是java的Thread机制.但是在scala中对java Thread进行了封装,实现了更便于操作线程的Future. 官方文档: Futures pro ...
- defer、return、返回值,这三者的执行逻辑
defer.return.返回值,这三者的执行逻辑是: return 最先执行,return 负责将结果写入返回值中:接着defer执行,可能修改返回值:最后函数携带当前返回值退出.
- ETL-kettle 核心执行逻辑
一.大数据下的ETL工具是否还使用Kettle kettle 作为通用的ETL工具,非常成熟,应用也很广泛,这里主要讲一下 目前我们如何使用kettle的? 在进行大数据处理时,ETL也是大数据处理的 ...
- Springboot中mybatis执行逻辑源码分析
Springboot中mybatis执行逻辑源码分析 在上一篇springboot整合mybatis源码分析已经讲了我们的Mapper接口,userMapper是通过MapperProxy实现的一个动 ...
- [C语言]进阶|数据类型: 整数, 浮点, 逻辑, 类型转换和条件运算
--------------------------------------------------------------------------------- [C语言的类型] 1. 整型(都分为 ...
- Fabric架构:抽象的逻辑架构与实际的运行时架构
Fabric从1.X开始,在扩展性及安全性上面有了大大的提升,且新增了诸多的新特性: 多通道:支持多通道,提高隔离安全性. 可拔插的组件:支持共识组件.权限管理组件等可拔插功能. 账本数据可被存储为多 ...
- 并发包的线程池第一篇--ThreadPoolExecutor执行逻辑
学习这个很长时间了一直没有去做个总结,现在大致总结一下并发包的线程池. 首先,任何代码都是解决问题的,线程池解决什么问题? 如果我们不用线程池,每次需要跑一个线程的时候自己new一个,会导致几个问题: ...
- MVC+Ef项目(4) 抽象业务逻辑层BLL层
接下来,我们就要到业务逻辑层了,简单的说,业务逻辑层就是调用Repository(可以看做是DAL数据库访问层) 先来看看项目的架构 我们现在就开始来做BLL层. 同样,先编写 UserInfoS ...
- Python语言基础04-构造程序逻辑
本文收录在Python从入门到精通系列文章系列 学完前面的几个章节后,博主觉得有必要在这里带大家做一些练习来巩固之前所学的知识,虽然迄今为止我们学习的内容只是Python的冰山一角,但是这些内容已经足 ...
- 使用CyclicBarrier+线程池,按总页数分批次开多线程执行逻辑
通过CyclicBarrier+线程池的方式,同步的方式分页分批次并发高效处理逻辑,将总页数分成多个批次并发执行每页逻辑,每个批次处理DO_MAX_SIZE个页,每个批次等待DO_MAX_SIZE个页 ...
随机推荐
- LCD显示器的接口协议
简介LCD的接口有多种,常用的LCD的连接方式有如下几种:MCU(MPU)模式,RGB模式,SPI模式,VSYNC模式,MDDI模式,DSI模式,MIPI模式,LVDS模式,TTL模式,EDP模式. ...
- SM系列国密算法
其中SM1.SM4.SM7.祖冲之密码(ZUC)是对称算法:SM2.SM9是非对称算法:SM3是哈希算法.目前,这些算法已广泛应用于各个领域中,期待有一天会有采用国密算法的区块链应用出现. 一.SM1 ...
- 导出和导入word样式模板
对于自己配置过之后常用的word样式可以导出作为样式模板, 可以重复使用. 举例说一下哪些是常用的word样式, 例如: (常见的): 中文的内容的样式, 中英文分别设置不同的样式 (比较高级的)多级 ...
- Netty基础—4.NIO的使用简介
大纲 1.Buffer缓冲区 2.Channel通道 3.BIO编程 4.伪异步IO编程 5.改造程序以支持长连接 6.NIO三大核心组件 7.NIO服务端的创建流程 8.NIO客户端的创建流程 9. ...
- STM32 学习方法
前言 学习知识要掌握有效的学习方法,学习技术也是一样,本篇分享关于我学习 STM32 后总结的学习方法. 推荐的学习方法 系统学习 在网上购买一款开发板,使用开发板+开发板配套视频教程+开发板配套源码 ...
- dcgm-exporter:Collect Switch Metrics和Collect Link Metrics
在 DCGM(Data Center GPU Manager)中,"Collect Switch Metrics" 和 "Collect Link Metrics&quo ...
- linux configure: error: no acceptable C compiler found in $PATH
前言 在 Linux 上安装 pgsql时,执行 ./configure --prefix=/usr/local/pgsql 报错,同以下: [root@instance-0qymp8uo postg ...
- 快速排序(NB)
博客地址:https://www.cnblogs.com/zylyehuo/ # _*_coding:utf-8_*_ def partition(li, left, right): tmp = li ...
- docx4j转换pdf样式问题~Java Libreoffice转换pdf
背景 本篇文章主要是介绍我在使用docx4j过程中遇到的问题,并最终如何通过Libreoffice来实现pdf的转换. 问题 在使用docx4j转换pdf过程中发现word文档中表格.加粗样式无法实现 ...
- Delphi Richedit代码语法加亮显示
procedure CodeColors(Form : TForm;Style : String; RichE : TRichedit;InVisible : Boolean); const // s ...