编译[2]执行一行四则运算

1,408 阅读18分钟

概述

上一篇开场简单描述了一下我对编译的看法,现在要开始动手做点东西。

在亲自实现词法和语法分析器之前,先利用工具帮个忙。

利用LexYACC生成编译器,该编译器能解析计算四则运算。这里实现的效果是输入一个四则运算,回车后输出结果,因为是一边输入一边解析执行,所以应该叫解析器更合适。

对应代码放在github库上的calculator分支: calculator

词法分析

词法分析读入输入字符串,按顺序解析其中包含的词,每次调用词法分析器返回的内容叫token,token包类型(词的类型),以及命中的字符串具体是什么(又叫属性)。后续的语法分析通过token的类型组成语法结构,属性影响最终的语义。

对于每一种类型的token,都定义匹配的逻辑,匹配逻辑就用正则表达式描述,词法分析器一边读入字符,一边跟这些正则匹配,确认匹配某个正则,就确认了匹配的token的类型,返回的token包含了类型以及实际匹配的字符串。

Lex

Lex定义token匹配规则,完整代码文件在这里


%{
......
#include "nl.h"
#include "y.tab.h"
......
%}

%%
"+" return ADD;
"-" return SUB;
"*" return MUL;
"/" return DIV;
"%" return MOD;
"(" return LP;
")" return RP;
"\n" return CR;
([1-9][0-9]*)|"0" {
    Expression *expression = nl_alloc_expression(INT_EXPRESSION);
    sscanf(yytext, "%d", &expression->u.int_value);
    yylval.expression = expression;
    return INT_LITERAL;
}
[0-9]+\.[0-9]+ {
    Expression *expression = nl_alloc_expression(DOUBLE_EXPRESSION);
    sscanf(yytext, "%lf", &expression->u.double_value);
    yylval.expression = expression;
    return DOUBLE_LITERAL;
}
[ \t] ;
. {
    printf("lexical error with unexpected charactor %s\n", yytext);
    exit(1);
}
%%

顶部引入了类型文件nl.h

而词法分析返回的token类型定义在y.tab.h文件,该文件是YACC生成语法分析器时生成的,后面会介绍语法分析。

命中某个标点符号,比如'+'号,就返回ADD这个常量,常量代表类型,类型的定义可以在Lex文件第一部分,也可以在YACC文件那边定义,现在采用的是后者。

匹配整数的代码加上注释说明如下(另外匹配浮点数的正则类似)

// 匹配整数的正则,要么是0,要么是非0开头后续跟0到9的数字
([1-9][0-9]*)|"0" {
    // 在另外的C文件定义的表达式类型,以及创建表达式的方法,方法传进去的参数指明了要创建的表达式是整数表达式
    Expression *expression = nl_alloc_expression(INT_EXPRESSION);
    // 匹配命中的字符串存放在yytext这个外部变量中,这里就是把匹配到的整数字符串转换成整数,再赋值给表达式的值
    sscanf(yytext, "%d", &expression->u.int_value);
    // 把创建的表达式对象存起来,YACC那边运行时可以拿到
    yylval.expression = expression;
    // 返回token类型,YACC构造语法逻辑时要用
    return INT_LITERAL;
}

还有匹配空白字符的逻辑,这里就是匹配空格和tab之后什么都不做,直接忽略,至于换行符在前面加了匹配逻辑。

[ \t] ;

最后还有一个匹配逻辑是一个点号'.',点号在正则里面表示任意字符,当前面的匹配都没有命中,最后命中任意字符都判定为错误,所以会打印一个错误提示,并终止程序。

总的来说,目前词法分析会试图去匹配四则运算符,求模,整数,浮点数,小括号这些类型的token。

类型定义

在介绍语法分析之前,先介绍类型和方法逻辑,因为语法分析时一边分析一边需要执行逻辑代码。

类型定义完整文件在这里

这里要实现的是无类型语言,并且在四则运算中,整数和浮点数是区别对待的,虽然也可以把所有数字都当做浮点数处理,但本文并不打算这样做,而是区分整数和浮点数两种类型。所以这里定义值类型枚举(ValueType),同时定义值数据类型结构体(NL_Value),NL_Value可以存放值的类型以及具体值是什么,具体值放在联合体u里面,如果值类型是整数,就放在int_value属性中,如果值类型是浮点数,就放在double_value中。

typedef enum {
    INT_VALUE = 1,
    DOUBLE_VAULE
} ValueType;

typedef struct {
    ValueType type;
    union {
        int int_value;
        double double_value;
    } u;
} NL_Value;

定义表达式数据类型,表达式的类型放在枚举(ExpressionType)中,整数和浮点数也是一种表达式,另外加减乘除和求模是另外的表达式类型,ExpressionType中最后一个枚举值是一个占位符。表达式类型结构体Expression_tag记录了表达式类型(type)以及该表达式的具体值放在联合体,根据具体类型决定放在联合体的哪个属性中。因为解析四则运算比较简单,所以可以在解析过程中边解析边算出结果,所以表达式具体值只记录结果,整数值或浮点数值。

typedef enum {
    INT_EXPRESSION = 1,
    DOUBLE_EXPRESSION,
    ADD_EXPRESSION,
    SUB_EXPRESSION,
    MUL_EXPRESSION,
    DIV_EXPRESSION,
    MOD_EXPRESSION,
    EXPRESSION_TYPE_PLUS
} ExpressionType;

struct Expression_tag {
    ExpressionType type;
    union {
        int int_value;
        double double_value;
    } u;
};

接着定义生成表达式的方法(放在create.c文件)以及计算表达式值的方法(放在eval.c文件),语法分析过程中一边分析一边生成相应的表达式,就是调用了我们定义的方法。

/* create.c */
Expression *nl_alloc_expression(ExpressionType type);
Expression *nl_create_minus_expression(Expression *exp);
Expression *nl_create_binary_expression(ExpressionType type, Expression *left, Expression *right);

/* eval.c */
NL_Value nl_eval_binary_expression(ExpressionType operator, Expression *left, Expression *right);
NL_Value nl_eval_expression(Expression *exp);
void nl_print_value(NL_Value *v);

方法定义

create.c文件,完整文件在这里

顶部引入头文件,这里重点引入了上面定义类型的文件nl.h

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "nl.h"

定义创建表达式的方法,其实就是为Expression类型分配空间,分配空间用C库函数malloc,用表达式类型指针指向分配的空间,设置类型,返回指针。

Expression *
nl_alloc_expression(ExpressionType type) {
    Expression *exp;
    exp = malloc(sizeof(Expression));
    exp->type = type;

    return exp;
}

定义一个方法把值转换成表达式,目前只有整数和浮点数两种值,直接声明一个表达式结构体,这里不是声明指针,直接声明结构体的话会自动分配空间,然后根据值类型设置表达式类型,以及设置对应值,最后返回表达式结构体。

static Expression
convert_value_to_expression(NL_Value *v) {
    Expression exp;

    if (v->type == INT_VALUE) {
        exp.type = INT_EXPRESSION;
        exp.u.int_value = v->u.int_value;
    } else if (v->type == DOUBLE_VAULE) {
        exp.type = DOUBLE_EXPRESSION;
        exp.u.double_value = v->u.double_value;
    } else {
        printf("[runtime error] convert value with unexpected type:%d\n", v->type);
        exit(1);
    }
    return exp;
}

定义创建二元表达式的方法,传入类型表明是哪种二元操作,还要传入左值和右值,左右值都是表达式,因为也有整数和浮点数表达式。这里调用了计算二元表达式值得方法(nl_eval_binary_expression),该方法定义在eval.c文件,在nl.h有声明,返回计算结果是值类型,然后调用上面定义的convert_value_to_expression方法把值转成表达式,放在left指针,这里纯粹是复用了left指针,赋值给left时,前面加了星号*left,这是C语言用法,表示取指针指向的值,而这里是赋值,因为convert_value_to_expression返回的是结构体,而不是指针。创建完二元表达式结构体之后返回指针。

Expression *
nl_create_binary_expression(ExpressionType type, Expression *left, Expression *right) {
    NL_Value v;
    v = nl_eval_binary_expression(type, left, right);

    *left = convert_value_to_expression(&v);
    return left;
}

还要定义创建一元负操作表达式的方法,传进去的是表达式,其实就是简单判断是整数还是浮点数,再直接把对应值取负后存回去,最后返回传进来的表达式指针。这里是因为所有表达式都是边解析边计算的,所以所有表达式肯定都是某个数值结果。但之后遇到更复杂情况时,左右表达式就不是简单的数值,就不能直接计算。

Expression *
nl_create_minus_expression(Expression *exp) {
    if (exp->type == INT_EXPRESSION) {
        exp->u.int_value = -exp->u.int_value;
    } else if (exp->type == DOUBLE_EXPRESSION) {
        exp->u.double_value = -exp->u.double_value;
    }
    return exp;
}

create.c就只定义了这几个方法。

eval.c文件,完整文件在这里

先引入头文件,我们定义的类型文件是关键。

#include "nl.h"
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

计算某种值类型表达式的值,其实就是声明值类型结构体,赋值对应type,再赋值传进来的值,这里是为了产生对应值的一个数据结构。

static NL_Value
eval_int_expression(int value) {
    NL_Value v;
    v.type = INT_VALUE;
    v.u.int_value = value;
    return v;
}

static NL_Value
eval_double_expression(double value) {
    NL_Value v;
    v.type = DOUBLE_VAULE;
    v.u.double_value = value;
    return v;
}

另外再定义一个方法,包装了上面两种方法,其实就是穿进去表达式,判断是整数还是浮点数类型,从而调用上面两者之一处理

static NL_Value
eval_expression(Expression *exp) {
    NL_Value v;
    switch (exp->type) {
        case INT_EXPRESSION: {
            v = eval_int_expression(exp->u.int_value);
            break;
        }
        case DOUBLE_EXPRESSION: {
            v = eval_double_expression(exp->u.double_value);
            break;
        }
        case ADD_EXPRESSION:
        case SUB_EXPRESSION:
        case MUL_EXPRESSION:
        case DIV_EXPRESSION:
        case MOD_EXPRESSION:
        case EXPRESSION_TYPE_PLUS:
        default: {
            printf("[runtime error] eval expression with unexpected type:%d\n", exp->type);
            exit(1);
        }
    }
    return v;
}

计算二元操作值得方法,分成两个,整数和浮点数分开,最后传进去的指针用于存放结果,调用方给出这个指针,计算完就可以从这个指针拿到结果。判断操作符类型,决定具体做什么计算。这里的操作类型只能是某种二元操作表达式,除此外其他类型都是异常,遇到异常类型会打印出错误信息。计算整数和浮点数的二元操作,大致相同,最后求模的方法有点差异。

static void
eval_binary_int(ExpressionType operator, int left, int right, NL_Value *result) {
    result->type = INT_VALUE;
    
    switch (operator) {
        case ADD_EXPRESSION: {
            result->u.int_value = left + right;
            break;
        }
        case SUB_EXPRESSION: {
            result->u.int_value = left - right;
            break;
        }
        case MUL_EXPRESSION: {
            result->u.int_value = left * right;
            break;
        }
        case DIV_EXPRESSION: {
            result->u.int_value = left / right;
            break;
        }
        case MOD_EXPRESSION: {
            result->u.int_value = left % right;
            break;
        }
        case INT_EXPRESSION:
        case DOUBLE_EXPRESSION:
        case EXPRESSION_TYPE_PLUS:
        default: {
            printf("[runtime error] eval binary int with unexpected type:%d\n", operator);
            exit(1);
        }
    }
}

static void
eval_binary_double(ExpressionType operator, double left, double right, NL_Value *result) {
    result->type = DOUBLE_VAULE;
    
    switch (operator) {
        case ADD_EXPRESSION: {
            result->u.double_value = left + right;
            break;
        }
        case SUB_EXPRESSION: {
            result->u.double_value = left - right;
            break;
        }
        case MUL_EXPRESSION: {
            result->u.double_value = left * right;
            break;
        }
        case DIV_EXPRESSION: {
            result->u.double_value = left / right;
            break;
        }
        case MOD_EXPRESSION: {
            result->u.double_value = fmod(left, right);
            break;
        }
        case INT_EXPRESSION:
        case DOUBLE_EXPRESSION:
        case EXPRESSION_TYPE_PLUS:
        default: {
            printf("[runtime error] eval binary int with unexpected type:%d\n", operator);
            exit(1);
        }
    }
}

再定义二元表达式计算方法,其实就是对上面两种二元方法做了封装,通过判断传进来的表达式类型选择合适方法。只要左表达式或右表达式有一个是浮点数表达式,就把另一个整数表达式也转换成浮点数,然后调用浮点数二元计算方法计算。

NL_Value
nl_eval_binary_expression(ExpressionType operator, Expression *left, Expression *right) {
    NL_Value left_val;
    NL_Value right_val;
    NL_Value result;
    left_val = eval_expression(left);
    right_val = eval_expression(right);

    if (left_val.type == INT_VALUE && right_val.type == INT_VALUE) {
        eval_binary_int(operator, left_val.u.int_value, right_val.u.int_value, &result);
    } else if (left_val.type == DOUBLE_VAULE && right_val.type == DOUBLE_VAULE) {
        eval_binary_double(operator, left_val.u.double_value, right_val.u.double_value, &result);
    } else if (left_val.type == INT_VALUE && right_val.type == DOUBLE_VAULE) {
        left_val.u.double_value = left_val.u.int_value;
        eval_binary_double(operator, left_val.u.double_value, right_val.u.double_value, &result);
    } else if (left_val.type == DOUBLE_VAULE && right_val.type == INT_VALUE) {
        right_val.u.double_value = right_val.u.int_value;
        eval_binary_double(operator, left_val.u.double_value, right_val.u.double_value, &result);
    } else {
        printf("[runtime error] eval binary expression with unexpected type, left:%d, right:%d\n", left_val.type, right_val.type);
        exit(1);
    }

    return result;
}

又定义一个对外使用的计算表达式值得方法,其实就是调用了上面定义好的方法。

NL_Value
nl_eval_expression(Expression *exp) {
    return eval_expression(exp);
}

还定义了一个打印方法,用来打印某个值的结果。

void
nl_print_value(NL_Value *v) {
    if (v->type == INT_VALUE) {
        printf("--> %d\n", v->u.int_value);
    } else if (v->type == DOUBLE_VAULE) {
        printf("--> %lf\n", v->u.double_value);
    }
}

就这些。

YACC

接着讲语法分析。以YACC能读懂的方式写下语法规则,让YACC读入规则文件后生成语法分析器。

完整的文件在这里

YACC文件分成三部分,先说第一部分

引入必要的C文件,声明必要的方法,C语言里面,调用方法之前要先声明,声明必须在调用之前,定义可以写在调用后面。C语言没有像JavaScript那样的变量或方法提升的行为。定义类型的文件nl.h还是需要的,声明了yylex方法,该方法在Lex生成的词法分析器C文件里面定义,该方法就是用来返回token的。另外声明的yyerror方法在语法分析出错时调用,在YACC文件第三部分定义。

%{
#include <stdio.h>
#include <stdlib.h>
#include "nl.h"
int yylex();
int yyerror(char const *str);
%}

定义类型,用于指派给一些token或非终结符,表明解析到实例时,该实例的值是什么类型。其实光这样说是很难让人明白的,直接看下面的实际例子吧。这里定义了expression类型,这只是一个类型名字,而它的实际类型是Expression指针,在nl.h里面定义。

%union {
    Expression *expression;
}

定义一些token,其实是token的类型,用常量的方式表示,比如这里定义了常量ADD,表示一种token类型,匹配什么样的字符时返回这个类型的token在词法分析那边定义,那边定义了命中加号('+')时返回ADD类型的token。第一行都是些没有值的token类型,第二行表示整数和浮点数两种类型的token,他们返回的值是expression类型的。

%token ADD SUB MUL DIV LP RP MOD CR
%token <expression> INT_LITERAL DOUBLE_LITERAL

再定义非终结符,非终结符就是由其他非终结符和token组成的元素,当然,其它非终结符也是由非终结符和token组成,但所有非终结符往下推导,最终都是由token组成。定义非终结符用%type开头,他们返回的值类型是expression。可能不能理解非终结符是什么,往后看可能就明白了,其实就是一些语法组织中一类元素的代号。

一行双百分号表示第一部分结束。

%type <expression> primary_expression mult_expression add_expression expression
%%

看第二部分,定义各个产生式,也组成了语法结构

定义表达式列表(expression_list),它可以是单个表达式(expression),也可以是另一个表达式列表后面跟一个表达式。

expression_list
    : expression
    | expression_list expression
    ;

比如,有这样一个表达式

3 + 6;

它是一个表达式列表,它也是单个表达式

再比如,有两个表达式

4 - 19 - 4;

23 + 45 * 34;

把第一个表达式看做一个表达式列表(expression_list),第二个表达式看做单个表达式(expression),这样expression_list后面跟一个expression,组成了一个新的expression_list

产生式都是递归概念的。

再定义表达式的产生式,表达式可以是一个加法表达式后面跟回车,也可以是不跟回车的加法表达式,但如果是遇到跟回车的加法表达式时,需要执行大括号里面的逻辑。

expression
    : add_expression CR
    {
        NL_Value v = nl_eval_expression($1);
        nl_print_value(&v);
    }
    | add_expression
    ;

大括号的逻辑里,用到nl_eval_expression方法,该方法之前介绍过,定义在eval.c文件,在nl.h也声明了。$1表示产生式体中第一个元素(add_expression)返回的值,第一部分定义%type的时候指明了add_expression返回的类型是expression,实际类型是Expression指针,而Expressionnl.h里面有定义。

nl_eval_expression计算表达式的值,返回NL_Value类型的值,然后调用nl_print_value方法打印出来。

定义add_expression的产生式,它可以是单个乘法表达式(mult_expression),也可以是另一个加法表达式(add_expression)加上(ADD)一个乘法表达式(mult_expression),还可以是另一个加法表达式(add_expression)减去(SUB)一个乘法表达式(mult_expression)

add_expression
    : mult_expression
    | add_expression ADD mult_expression
    {
        $$ = nl_create_binary_expression(ADD_EXPRESSION, $1, $3);
    }
    | add_expression SUB mult_expression
    {
        $$ = nl_create_binary_expression(SUB_EXPRESSION, $1, $3);
    }
    ;

之所以这样设计,是为了让乘法有更高的计算优先级,当一个表达式包含加减法和乘除法时,整体看作是一个加法表达式,而里面的乘除法表达式肯定要先计算,构成乘法表达式(mult_expression)才能满足加法表达式定义的结构。这里的定义也表明了单个乘法表达式也是一个加法表达式。

当遇到包含加号或减号的加法表达式时,就会调用nl_create_binary_expression方法,创建一个加法表达式,$1$3表示产生式体中第一个元素(add_expression)和第三个元素(mult_expression),他们都是返回表达式类型的值。而$$表示最终这一整个加法表达式的值,也是一个Expression指针。

可能有人觉得不好理解,等介绍完全部产生式之后举个例子。

定义乘法表达式,乘法表达式可以是一个基础表达式,或者是另一个乘法表达式乘以(或除以/或模)另一个乘法表达式

mult_expression
    : primary_expression
    | mult_expression MUL primary_expression
    {
        $$ = nl_create_binary_expression(MUL_EXPRESSION, $1, $3);
    }
    | mult_expression DIV primary_expression
    {
        $$ = nl_create_binary_expression(DIV_EXPRESSION, $1, $3);
    }
    | mult_expression MOD primary_expression
    {
        $$ = nl_create_binary_expression(MOD_EXPRESSION, $1, $3);
    }
    ;

基础表达式,可以是另一个基础表达式取负值,可以是括号包裹另一个表达式,可以是一个整数或浮点数。

primary_expression
    : SUB primary_expression
    {
        $$ = nl_create_minus_expression($2);
    }
    | LP expression RP
    {
        $$ = $2;
    }
    | INT_LITERAL
    | DOUBLE_LITERAL
    ;

产生式就这些,下面举例尝试说明

比如这个表达式

3

解析是自上而下的,3属于整数,会被识别为INT_LITERALINT_LITERAL属于primary_expression,而根据产生式定义primary_expression属于mult_expression,同理mult_expression属于add_expression,一路属于到expression_list

比如这个表达式

3 + 2

3可以一路属于到add_expression,同理,2属于mult_expression,它们之间有个加号,就满足了add_expression ADD mult_expression这个结构,触发定义的逻辑执行,创建加法表达式结构体变量并返回。

比如这个表达式

3 + 3 + 4

前面的3 + 3就像上一个例子,会最终返回一个加法表达式add_expression,然后4属于mult_expression,两者相加,又满足了add_expression ADD mult_expression结构,又触发创建加法表达式。

比如这个表达式

5 + 4 * 8

5属于add_expression,遇到4之后发现它后面是个乘号,加法表达式本身不能处理乘号,只能由乘法表达式处理,所以当遇到后面是一个乘号时,5 + 4不会单独组成add_expression,而是会继续看乘号后面的内容,发现是一个8,而4可以属于mult_expression,8属于primary_expression,两者加上中间的乘号满足mult_expression MUL primary_expression,会生成并返回mult_expression,前面的5属于add_expression,这样5加上后面的乘法表达式,又构成一个add_expression,又触发add_expression的计算。

比如这个表达式

4 * 5 * (1 + 6)

4 * 5会构成mult_expression,因为5后面跟乘号不影响前面4 * 5先组合,然后遇到括号,根据定义,它会认为遇到了primary_expression,并试图在遇到右括号之前构建一个expression,然后它会像从头解析一样最终解析出了1 + 6是一个add_expression,而add_expression也属于expression,加上左右括号构成primary_expression,因为前面的4 * 5属于mult_expression,所以又符合了mult_expression MUL primary_expression结构,然后触发生成逻辑,接着可以一路往上属于。

yacc文件还有最后一部分,这里就定义了yyerror方法,第一部分声明过了,这里定义,在语法分析报错时执行该方法

%%
int yyerror(char const *str) {
    extern char *yytext;
    fprintf(stderr, "parse error near %s\n", yytext);
    return 0;
}

生成词法分析器和语法分析器代码文件

我用的是mac,安装yacclex这两个工具,也是两个新命令。

如果用windows,要安装另外两个工具,bisonflex,查一下怎么安装就行,指令跟yacclex是差不多的。

先用yacc做编译

yacc -dv nl.y

编译了nl.y文件后,生成y.tab.hy.tab.c文件,而写lex文件nl.l时,开头就引入了y.tab.h文件。

接着用lex编译nl.l

lex nl.l

它会生成lex.yy.c文件

这些生成的C文件,里面就包含了词法和语法分析的功能,也包含了语法分析的方法。

编译执行

结合我们定义的方法,以及词法和语法分析方法,我们需要写一个C的入口文件main.c,引入必要头文件,main方法是必须的。完整文件在这里

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv) {
    extern int yyparse(void);
    extern FILE *yyin;

    yyin = stdin;
    if (yyparse()) {
        fprintf(stderr, "Error ! \n");
        exit(1);
    }
}

main方法里面声明了外部方法yyparse和外部指定输入的变量yyin,把yyin赋值了stdin,表示读入的是标准输入的内容,基本就是键盘输入的内容。

接着的if判断里面调用yyparse方法,该方法就会执行整个语法分析,这包括了读入输入的字符,做词法分析,做语法分析,语法分析过程中满足某些产生式逻辑时,执行我们定义的逻辑。而我们定义的逻辑包括了创建几种表达式,以及在遇到带回车的表达式时输出计算结果。

接着是调用gcc编译所有我们写的文件以及yacclex生成的文件。我写了一个Makefile文件(完整文件在这里)只要一个make命令就能执行编译,生成一个可执行的nl文件。

直接用指令执行它,他会等待你的输入,每当输入一个四则运算表达式,按回车,就会打印结果,可以继续输入下一个,比如输入下面的表达式

4 + 8 + 9 * 4 * (4+4*3+(3)+ -4)

它能输出正确结果,注意这个表达式最后是加上一个负四,目前是合法了。另外值得注意的是,做二元运算时,如果其中一个值是浮点数,则另一个整数也会先转成浮点数再计算,另外,浮点数在词法分析时确定,有小数点的都是浮点数,目前的词法定义中,像这样(45.)小数点后没有数字的格式是错误的。

下面补一个本地操作的录屏,因为mac电脑不在身边,用的是windows上的ubuntu,并且用的不是yacclex,而是bisonflex,但其实就是换个命令而已,在Makefile上换一下就好,lex直接换成flex,而yacc换成bison,但因为bison生成的文件不是固定文件名,而是根据输入文件名而定,所以这里写死了输出文件名跟yacc一致,编译的时候有个warning,应该是该C的标准版本不支持打印时用%lf,但问题不大。最后编译后会生成一些.o文件,这是不必须的,yacc生成y.tab.cy.tab.h,而lex的文件中也有引入y.tab.h。其实用Makefile就是为了把几个编译步骤写在一起,先编译语法,再编译词法,再集合所有依赖的c文件编译出一个可执行文件。

命令运行四则运算.gif

结束

这篇结束,下一篇稍微修改一下输入方式,以及解释表达式做小调整,就实现了一门四则运算语言。

进入下一篇