用c语言手搓一个500+行的类c语言解释器: 给编程初学者的编译器教程(2)- 简介和设计

简介: 通常我们说的 “编译器” 是一种计算机程序,负责把一种编程语言编写的源码转换成另外一种计算机代码,后者往往是以二进制的形式被称为目标代码(object code)。这个转换的过程通常的目的是生成可执行的程序。而解释器是一种计算机程序,它直接执行由编程语言或脚本语言编写的代码,它并不会把源代码预编译成机器码,而是一行一行地分析源代码并且直接执行,相对编译器而言可能效率较为低下,但实现也相对简单,并且容易在不同的机器上进行移植(比如x86和mips指令集的机器)。

用c语言手搓一个500+行的类c语言解释器: 给编程初学者的编译器教程(2)- 简介和设计

项目github地址及源码:
https://github.com/yunwei37/tryC

需要了解的一些基本概念

编译器和解释器的区别不同

通常我们说的 “编译器” 是一种计算机程序,负责把一种编程语言编写的源码转换成另外一种计算机代码,后者往往是以二进制的形式被称为目标代码(object code)。这个转换的过程通常的目的是生成可执行的程序。

而解释器是一种计算机程序,它直接执行由编程语言或脚本语言编写的代码,它并不会把源代码预编译成机器码,而是一行一行地分析源代码并且直接执行,相对编译器而言可能效率较为低下,但实现也相对简单,并且容易在不同的机器上进行移植(比如x86和mips指令集的机器)。

先来看看通常的编译器是如何实现的:

编译器从源码翻译为目标代码大致需要这样几个步骤,每个步骤都依赖于上一个步骤的结果:

  1. 词法分析:

    编译器对源程序进行阅读,并将字符序列,也就是源代码中一个个符号收集到称作记号(token)的单元中;比如:
    
    num = 123.4;
这样个赋值语句中,变量num算是一个token,“=”符号算是一个token,“123.4”算是一个token;每个token有自己的类别和属性,比如“123.4”的类别是数字,属性(值)是123.4
  1. 语法分析:

    语法分析指将词法分析得到的标记流(token)进行分析,组成事先定义好的有意义的语句,这与自然语言中句子的语法分析类似。通常可以用抽象语法树表示语法分析的结果,比如赋值语句:
    
    num = 123.4 * 3;
可以用这样一个抽象语法树来表示:
    graph TD
    = --> num
    = --> *
    * --> 123.4
    * --> 3
  1. 语义分析:

    程序的语义就是它的“意思”,程序的语义确定程序的运行方式。语义分析阶段通常包括声明和类型检查、计算需要的一些属性值等等。编译器在这个阶段中通常会维护一个叫做“符号表”的东西,保存变量的值、属性和名称。同样以
    
    num = 123.4 * 3;
    
    为例,假如我们是第一次在这里遇见“num”,就将num的名称字符串“num” 和当前计算出来的初始值370.2插入符号表中,当下次再遇见num时。我们就知道它是一个数字,已经初始化完毕,并且当前值是370.2;
    
  2. 目标代码生成:

    在语义分析之后,我们就可以将语法分析和语义分析的结果(通常是抽象语法树)转换成可执行的目标代码。
    

解释器与编译器仅在代码生成阶段有区别,而在前三个阶段如词法分析、语法分析、语义分析基本是一样的。

当然,已经有许多工具可以帮助我们处理阶段1和2,如 flex 用于词法分析,bison 用于语法分析;但它们的功能都过于强大,屏蔽了许多实现上的细节,对于学习构建编译器帮助不大,所以我们要完全手写这些功能。

(实际上完成一个可以跑起来的解释器并不难,而且还是一件很有成就感的事,不是嘛?)

tryC编译器的设计:

从上面可以看出,我们的tryC解释器需要这三个模块:

  1. 词法分析
  2. 语法分析
  3. 语义分析和解释执行

需要这两个数据结构(用来在阶段之间保存或传递值):

  1. token,用来在词法分析和语法分析之间传递标记;
  2. 符号表,保存语义分析阶段遇见的变量值,使用一个数组存储;

在了解过这些之后,我们先来大概看看代码的基本结构:

(从上往下在代码中依次对应,“...”表示省略的相关代码,在后续文章中会详细讲解)

  • 数据结构的声明部分:token类型、符号表结构:
#include <stdio.h>
...

typedef struct symStruct {  
    int type;                
    char name[MAXNAMESIZE];    
    double value;             
    ..........
} symbol;
symbol symtab[SYMTABSIZE];          // 符号表
int symPointer = 0;             

char* src, * old_src;               // 当前分析的源代码位置指针

// tokens 的枚举类型
enum {
    Num = 128, Char, Str, Array, Func,
    ........
};

// token 的表示形式
int token;                      // current token type
union tokenValue {
    symbol* ptr;               
    double val;                 
} token_val;
  • 词法分析的两个函数:
// 获取输入流中的下一个记号:
void next() {
    char* last_pos;

    while (token = *src) {
        ++src;
        if(token == AAA ){
            .....
        }else if(token == BBB ){
            .....
        }
    }
}

// 匹配一个记号,并获取下一个token:
void match(int tk) {
    if (token == tk) {
        next();
    }
    else {          // 遇到了一个错误
        exit(-1);
    }
}
  • 语法分析和语义分析,以及执行阶段:使用递归下降法实现(后面会再提到什么是递归下降法啦)

// 计算表达式的值:
double expression(){}
double factor(){}
double term(){}

// 计算布尔表达式的值:
int boolOR();
int boolAND();
int boolexp();

// 执行一个语句;
double statement();

// 执行一个函数:
double function();
  • main() 函数,代码的入口,并

int main(int argc, char** argv)
{   
    // 往符号表里面添加关键词
    int i, fd;
    src = "array func else if return while print puts read";
    for (i = Array; i <= Read; ++i) {
        next();
        symtab[symPointer -1].type = i;
    }

    src = old_src = (char*)malloc(POOLSIZE); // 分配空间

    ....

    fd = open(*argv, 0);        // 打开读取文件

    read(fd, src, POOLSIZE - 1);

    src[i] = 0; 
    close(fd);
    next();
    while (token != 0) {        // 一条一条语句执行
        statement();
    }
    return 0;
}

重要概念

  • 编译器/解释器
  • 词法分析
  • 语法分析
  • 语义分析
  • token
  • 符号表

可参照github源码查看(如果觉得写得还行麻烦您帮我点个star哦)
https://github.com/yunwei37/tryC

目录
相关文章
|
2月前
|
编译器 C语言
C语言编译器为什么能够用C语言编写?
C语言编译器为什么能够用C语言编写?
47 9
|
25天前
|
存储 编译器 C语言
【C语言】数据类型全解析:编程效率提升的秘诀
在C语言中,合理选择和使用数据类型是编程的关键。通过深入理解基本数据类型和派生数据类型,掌握类型限定符和扩展技巧,可以编写出高效、稳定、可维护的代码。无论是在普通应用还是嵌入式系统中,数据类型的合理使用都能显著提升程序的性能和可靠性。
41 8
|
28天前
|
C语言
C语言编程中,错误处理至关重要,能提升程序的健壮性和可靠性
C语言编程中,错误处理至关重要,能提升程序的健壮性和可靠性。本文探讨了C语言中的错误类型(如语法错误、运行时错误)、基本处理方法(如返回值、全局变量、自定义异常处理)、常见策略(如检查返回值、设置标志位、记录错误信息)及错误处理函数(如perror、strerror)。强调了不忽略错误、保持处理一致性及避免过度处理的重要性,并通过文件操作和网络编程实例展示了错误处理的应用。
64 4
|
3月前
|
存储 编译器 C语言
C语言存储类详解
在 C 语言中,存储类定义了变量的生命周期、作用域和可见性。主要包括:`auto`(默认存储类,块级作用域),`register`(建议存储在寄存器中,作用域同 `auto`,不可取地址),`static`(生命周期贯穿整个程序,局部静态变量在函数间保持值,全局静态变量限于本文件),`extern`(声明变量在其他文件中定义,允许跨文件访问)。此外,`typedef` 用于定义新数据类型名称,提升代码可读性。 示例代码展示了不同存储类变量的使用方式,通过两次调用 `function()` 函数,观察静态变量 `b` 的变化。合理选择存储类可以优化程序性能和内存使用。
170 82
|
2月前
|
NoSQL C语言 索引
十二个C语言新手编程时常犯的错误及解决方式
C语言初学者常遇错误包括语法错误、未初始化变量、数组越界、指针错误、函数声明与定义不匹配、忘记包含头文件、格式化字符串错误、忘记返回值、内存泄漏、逻辑错误、字符串未正确终止及递归无退出条件。解决方法涉及仔细检查代码、初始化变量、确保索引有效、正确使用指针与格式化字符串、包含必要头文件、使用调试工具跟踪逻辑、避免内存泄漏及确保递归有基准情况。利用调试器、编写注释及查阅资料也有助于提高编程效率。避免这些错误可使代码更稳定、高效。
477 12
|
3月前
|
消息中间件 Unix Linux
C语言 多进程编程(五)消息队列
本文介绍了Linux系统中多进程通信之消息队列的使用方法。首先通过`ftok()`函数生成消息队列的唯一ID,然后使用`msgget()`创建消息队列,并通过`msgctl()`进行操作,如删除队列。接着,通过`msgsnd()`函数发送消息到消息队列,使用`msgrcv()`函数从队列中接收消息。文章提供了详细的函数原型、参数说明及示例代码,帮助读者理解和应用消息队列进行进程间通信。
|
3月前
|
缓存 Linux C语言
C语言 多进程编程(六)共享内存
本文介绍了Linux系统下的多进程通信机制——共享内存的使用方法。首先详细讲解了如何通过`shmget()`函数创建共享内存,并提供了示例代码。接着介绍了如何利用`shmctl()`函数删除共享内存。随后,文章解释了共享内存映射的概念及其实现方法,包括使用`shmat()`函数进行映射以及使用`shmdt()`函数解除映射,并给出了相应的示例代码。最后,展示了如何在共享内存中读写数据的具体操作流程。
|
3月前
|
Linux C语言
C语言 多进程编程(七)信号量
本文档详细介绍了进程间通信中的信号量机制。首先解释了资源竞争、临界资源和临界区的概念,并重点阐述了信号量如何解决这些问题。信号量作为一种协调共享资源访问的机制,包括互斥和同步两方面。文档还详细描述了无名信号量的初始化、等待、释放及销毁等操作,并提供了相应的 C 语言示例代码。此外,还介绍了如何创建信号量集合、初始化信号量以及信号量的操作方法。最后,通过实际示例展示了信号量在进程互斥和同步中的应用,包括如何使用信号量避免资源竞争,并实现了父子进程间的同步输出。附带的 `sem.h` 和 `sem.c` 文件提供了信号量操作的具体实现。
|
C语言
用C语言写解释器(五)——其他一些东西
写完解释器之后 这一篇文章我只想和大家侃侃编程语言的事情,不会被放到书中。因此可以天南地北地扯淡,不用像前几篇一样畏首畏尾的了。 经过前面几篇文章的讨论,已经把用纯 C 语言来实现一个解释器的方法介绍完了。但那些是写给我校 C 语言初学者看的,并不只是你,我得也觉得很不过瘾 ^_^。因此准备继续深入学习编译原理等课程,希望有志同道合的朋友和我一起交流! 富饶的语言(工具) 在前几篇文章中一直在
1484 0
|
25天前
|
存储 C语言 开发者
【C语言】字符串操作函数详解
这些字符串操作函数在C语言中提供了强大的功能,帮助开发者有效地处理字符串数据。通过对每个函数的详细讲解、示例代码和表格说明,可以更好地理解如何使用这些函数进行各种字符串操作。如果在实际编程中遇到特定的字符串处理需求,可以参考这些函数和示例,灵活运用。
50 10