用C语言写解释器(二)——表达式求值

声明

为提高教学质量,我所在的学院正在筹划编写C语言教材。《用C语言写解释器》系列文章经整理后将收入书中“综合实验”一章。因此该系列的文章主要阅读对象定为刚学完C语言的学生(不要求有数据结构等其他知识),所以行文比较罗嗦,请勿见怪。本人水平有限,如有描述不恰当或错误之处请不吝赐教!特此声明。

内存管理

既然是表达式求值,自然需要在内存中保存计算结果以及中间值。在《用C语言写解释器(一)》中提过:变量要求是若类型,而 C 语言中的变量是强类型,为实现这个目标就需要定义自己的变量类型,参考代码如下(注释部分指出代码所在的文件名,下同):

  1. // in basic_io.h  
  2. #define MEMORY_SIZE (26)  
  3.   
  4. typedef enum {  
  5.     var_null = 0,  
  6.     var_double,  
  7.     var_string  
  8. } variant_type;  
  9. typedef char STRING[128];  
  10. typedef struct {  
  11.     variant_type type;  
  12.     union {  
  13.         double i;  
  14.         STRING s;  
  15.     };  
  16. } VARIANT;  
  17.   
  18. extern VARIANT memory[MEMORY_SIZE];  
  19.   
  20. // in expression.h  
  21. typedef VARIANT OPERAND;  

程序自带 A-Z 26个可用变量,初始时都处于未赋值(ver_null)状态。所有变量必须先赋值再使用,否则就会报错!至于赋值语句的实现请参见后面语法分析的章节。

操作符

表达式中光有数值不行,还需要有操作符。在《一》中“表达式运算”一节已经给出了解释器需要实现的所有操作符,包括“算术运算”、“关系运算”和“逻辑运算”。下面给出程序中操作符的定义和声明:

  1. // in expression.h  
  2. typedef enum {  
  3.     /* 算数运算 */  
  4.     oper_lparen = 0,    // 左括号  
  5.     oper_rparen,        // 右括号  
  6.     oper_plus,          // 加  
  7.     oper_minus,         // 减  
  8.     oper_multiply,      // 乘  
  9.     oper_divide,        // 除  
  10.     oper_mod,           // 模  
  11.     oper_power,         // 幂  
  12.     oper_positive,      // 正号  
  13.     oper_negative,      // 负号  
  14.     oper_factorial,     // 阶乘  
  15.     /* 关系运算 */  
  16.     oper_lt,            // 小于  
  17.     oper_gt,            // 大于  
  18.     oper_eq,            // 等于  
  19.     oper_ne,            // 不等于  
  20.     oper_le,            // 不大于  
  21.     oper_ge,            // 不小于  
  22.     /* 逻辑运算 */  
  23.     oper_and,           // 且  
  24.     oper_or,            // 或  
  25.     oper_not,           // 非  
  26.     /* 赋值 */  
  27.     oper_assignment,    // 赋值  
  28.     oper_min            // 栈底  
  29. } operator_type;  
  30. typedef enum {  
  31.     left2right,  
  32.     right2left  
  33. } associativity;  
  34. typedef struct {  
  35.     int numbers;        // 操作数  
  36.     int icp;            // 优先级  
  37.     int isp;            // 优先级  
  38.     associativity ass;  // 结合性  
  39.     operator_type oper; // 操作符  
  40. } OPERATOR;  
  41.   
  42. // in expression.c  
  43. static const OPERATOR operators[] = {  
  44.     /* 算数运算 */  
  45.     {2, 17, 1, left2right, oper_lparen},     // 左括号  
  46.     {2, 17, 17, left2right, oper_rparen},    // 右括号  
  47.     {2, 12, 12, left2right, oper_plus},      // 加  
  48.     {2, 12, 12, left2right, oper_minus},     // 减  
  49.     {2, 13, 13, left2right, oper_multiply},  // 乘  
  50.     {2, 13, 13, left2right, oper_divide},    // 除  
  51.     {2, 13, 13, left2right, oper_mod},       // 模  
  52.     {2, 14, 14, left2right, oper_power},     // 幂  
  53.     {1, 16, 15, right2left, oper_positive},  // 正号  
  54.     {1, 16, 15, right2left, oper_negative},  // 负号  
  55.     {1, 16, 15, left2right, oper_factorial}, // 阶乘  
  56.     /* 关系运算 */  
  57.     {2, 10, 10, left2right, oper_lt},        // 小于  
  58.     {2, 10, 10, left2right, oper_gt},        // 大于  
  59.     {2, 9, 9, left2right, oper_eq},          // 等于  
  60.     {2, 9, 9, left2right, oper_ne},          // 不等于  
  61.     {2, 10, 10, left2right, oper_le},        // 不大于  
  62.     {2, 10, 10, left2right, oper_ge},        // 不小于  
  63.     /* 逻辑运算 */  
  64.     {2, 5, 5, left2right, oper_and},         // 且  
  65.     {2, 4, 4, left2right, oper_or},          // 或  
  66.     {1, 15, 15, right2left, oper_not},       // 非  
  67.     /* 赋值 */  
  68.     // BASIC 中赋值语句不属于表达式!  
  69.     {2, 2, 2, right2left, oper_assignment},  // 赋值  
  70.     /* 最小优先级 */  
  71.     {2, 0, 0, right2left, oper_min}          // 栈底  
  72. };  

你也许会问为什么需要 icp(incoming precedence)、isp(in-stack precedence) 两个优先级,现在不用着急,以后会详细解释!

后缀表达式

现在操作数(operand)和操作符(operator)都有了,一个表达式就是由它们组合构成的,我们就统称它们为标记(token)。在程序中定义如下:

  1. // in expression.h  
  2. typedef enum {  
  3.     token_operand = 1,  
  4.     token_operator  
  5. } token_type;  
  6. typedef struct {  
  7.     token_type type;  
  8.     union {  
  9.         OPERAND var;  
  10.         OPERATOR ator;  
  11.     };  
  12. } TOKEN;  
  13. typedef struct tlist {  
  14.     TOKEN token;  
  15.     struct tlist *next;  
  16. } TOKEN_LIST, *PTLIST;  

我们平时习惯将表达式符写作:operand operator operand(比如1+1),这是一个递归的定义,表达式本身也可作为操作数。像这种将操作符放在两个操作数之间的表达式称为中缀表达式,中缀表达式的好处是可读性强,操作数之间泾渭分明(尤其是手写体中)。但它有自身的缺陷:操作符的位置说明不了它在运算的先后问题。例如 1+2×3 中,虽然 + 的位置在 × 之前,但这并不表示先做加运算再做乘运算。为解决这个问题,数学中给操作符分了等级,级别高的操作符先计算(乘号的级别比加号高),并用括号提高操作符优先级。因此上例表达式的值是 7 而不是 (1+2)*3=9。

但对于计算机来说,优先级是一个多余的概念。就像上面提到的,中缀表达式中操作符的顺序没有提供运算先后关系的信息,这就好比用4个字节的空间仅保存1个字节数据——太浪费了!索性将操作符按照运算的先后排序:先计算的排最前面。此时操作符就不适合再放中间了,可以将它移到被操作数的后面:operand operand operator(比如 1 1 +)。上例中 1+2×3 就变化为 1 2 3 × +;(1+2)×3 变化成 1 2 + 3 ×,这种将操作符符放到操作数后面的表达式称为后缀表达式。同理还有将操作符符按照逆序放到操作数的前面的前缀表达式。

无论是前缀表达式还是后缀表达式,它们的优点都是用操作符的顺序来代替优先级,这样就可以舍弃括号等概念,化繁为简。

后缀表达式求值

请看下面的梯等式计算,比较中缀表达式和后缀表达式的求值过程。

  8 × ( 2 + 3 )        8 2 3 + ×
= 8 * 5              = 8 5 ×
= 40                 = 40

后缀表达式的求值方式:从头开始一个标记(token)一个标记地往后扫描,碰到操作数时先放到一个临时的空间里;碰到操作符就从空间里取出最后两个操作数,做相应的运算,然后将结果再次放回空间中。到了最后,空间中就只剩下操作数即运算结果!这个中缀表达式求值类似,只不过中缀表达式操作数取的是前后各一个。下面的代码是程序中后缀表达式求值的节选,其中只包含加法运算,其他运算都是类似的。

  1. // in expression.c  
  2. VARIANT eval ( const char expr[] )  
  3. {  
  4.     // …  
  5.     // 一些变量的定义和声明  
  6.   
  7.     // 将中缀表达式转换成后缀表达式  
  8.     // 转换方法将在后续文章中介绍  
  9.     list = infix2postfix ();  
  10.     while ( list ) {  
  11.         // 取出一个 token  
  12.         p = list;  
  13.         list = list->next;  
  14.   
  15.         // 如果是操作数就保存到 stack 中  
  16.         if ( p->token.type == token_operand ) {  
  17.             p->next = stack;  
  18.             stack = p;  
  19.             continue;  
  20.         }  
  21.   
  22.         // 如果是操作符…  
  23.         switch ( p->token.ator.oper ) {  
  24.         // 加法运算  
  25.         case oper_plus:  
  26.             // 取出 stack 中最末两个操作数  
  27.             op2 = stack;  
  28.             op1 = stack = stack->next;  
  29.   
  30.             if ( op1->token.var.type == var_double &&  
  31.                  op2->token.var.type == var_double ) {  
  32.                 op1->token.var.i += op2->token.var.i;  
  33.             } else {  
  34.                 // 字符串的加法即合并两个字符串  
  35.                 // 如果其中一个是数字则需要先转换为字符串  
  36.                 if ( op1->token.var.type == var_double ) {  
  37.                     sprintf ( s1, ”%g”, op1->token.var.i );  
  38.                 } else {  
  39.                     strcpy ( s1, op1->token.var.s );  
  40.                 }  
  41.                 if ( op2->token.var.type == var_double ) {  
  42.                     sprintf ( s2, ”%g”, op2->token.var.i );  
  43.                 } else {  
  44.                     strcpy ( s2, op2->token.var.s );  
  45.                 }  
  46.                 op1->token.type = var_string;  
  47.                 strcat ( s1, s2 );  
  48.                 strcpy ( op1->token.var.s, s1 );  
  49.             }  
  50.             free ( op2 );  
  51.             break;  
  52.         // …  
  53.         // 其他操作符方法类似  
  54.         default:  
  55.             // 无效操作符处理  
  56.             break;  
  57.         }  
  58.         free ( p );  
  59.     }  
  60.   
  61.     value = stack->token.var;  
  62.     free ( stack );  
  63.   
  64.     // 最后一个元素即表达式的值  
  65.     return value;  
  66. }  

总结

这一篇文章主要介绍了表达式中的操作符、操作数在程序内部的表示方法、后缀表达式的相关知识以及后缀表达式求值的方法。在下一篇文章中将着重介绍如何将中缀表达式转换成后缀表达式,请关注《用C语言写解释器(三)》。


版权声明

请尊重原创作品。转载请保持文章完整性,并以超链接形式注明原始作者“redraiment”和主站点地址,方便其他朋友提问和指正。

联系方式

我的邮箱,欢迎来信([email protected]
我的Blogger(子清行):http://redraiment.blogspot.com/
我的Google Sites(子清行):https://sites.google.com/site/redraiment
我的CSDN博客(梦婷轩):http://blog.csdn.net/redraiment
我的百度空间(梦婷轩):http://hi.baidu.com/redraiment

声明

为提高教学质量,我所在的学院正在筹划编写C语言教材。《用C语言写解释器》系列文章经整理后将收入书中“综合实验”一章。因此该系列的文章主要阅读对象定为刚学完C语言的学生(不要求有数据结构等其他知识),所以行文比较罗嗦,请勿见怪。本人水平有限,如有描述不恰当或错误之处请不吝赐教!特此声明。

内存管理

既然是表达式求值,自然需要在内存中保存计算结果以及中间值。在《用C语言写解释器(一)》中提过:变量要求是若类型,而 C 语言中的变量是强类型,为实现这个目标就需要定义自己的变量类型,参考代码如下(注释部分指出代码所在的文件名,下同):

  1. // in basic_io.h  
  2. #define MEMORY_SIZE (26)  
  3.   
  4. typedef enum {  
  5.     var_null = 0,  
  6.     var_double,  
  7.     var_string  
  8. } variant_type;  
  9. typedef char STRING[128];  
  10. typedef struct {  
  11.     variant_type type;  
  12.     union {  
  13.         double i;  
  14.         STRING s;  
  15.     };  
  16. } VARIANT;  
  17.   
  18. extern VARIANT memory[MEMORY_SIZE];  
  19.   
  20. // in expression.h  
  21. typedef VARIANT OPERAND;  

程序自带 A-Z 26个可用变量,初始时都处于未赋值(ver_null)状态。所有变量必须先赋值再使用,否则就会报错!至于赋值语句的实现请参见后面语法分析的章节。

操作符

表达式中光有数值不行,还需要有操作符。在《一》中“表达式运算”一节已经给出了解释器需要实现的所有操作符,包括“算术运算”、“关系运算”和“逻辑运算”。下面给出程序中操作符的定义和声明:

  1. // in expression.h  
  2. typedef enum {  
  3.     /* 算数运算 */  
  4.     oper_lparen = 0,    // 左括号  
  5.     oper_rparen,        // 右括号  
  6.     oper_plus,          // 加  
  7.     oper_minus,         // 减  
  8.     oper_multiply,      // 乘  
  9.     oper_divide,        // 除  
  10.     oper_mod,           // 模  
  11.     oper_power,         // 幂  
  12.     oper_positive,      // 正号  
  13.     oper_negative,      // 负号  
  14.     oper_factorial,     // 阶乘  
  15.     /* 关系运算 */  
  16.     oper_lt,            // 小于  
  17.     oper_gt,            // 大于  
  18.     oper_eq,            // 等于  
  19.     oper_ne,            // 不等于  
  20.     oper_le,            // 不大于  
  21.     oper_ge,            // 不小于  
  22.     /* 逻辑运算 */  
  23.     oper_and,           // 且  
  24.     oper_or,            // 或  
  25.     oper_not,           // 非  
  26.     /* 赋值 */  
  27.     oper_assignment,    // 赋值  
  28.     oper_min            // 栈底  
  29. } operator_type;  
  30. typedef enum {  
  31.     left2right,  
  32.     right2left  
  33. } associativity;  
  34. typedef struct {  
  35.     int numbers;        // 操作数  
  36.     int icp;            // 优先级  
  37.     int isp;            // 优先级  
  38.     associativity ass;  // 结合性  
  39.     operator_type oper; // 操作符  
  40. } OPERATOR;  
  41.   
  42. // in expression.c  
  43. static const OPERATOR operators[] = {  
  44.     /* 算数运算 */  
  45.     {2, 17, 1, left2right, oper_lparen},     // 左括号  
  46.     {2, 17, 17, left2right, oper_rparen},    // 右括号  
  47.     {2, 12, 12, left2right, oper_plus},      // 加  
  48.     {2, 12, 12, left2right, oper_minus},     // 减  
  49.     {2, 13, 13, left2right, oper_multiply},  // 乘  
  50.     {2, 13, 13, left2right, oper_divide},    // 除  
  51.     {2, 13, 13, left2right, oper_mod},       // 模  
  52.     {2, 14, 14, left2right, oper_power},     // 幂  
  53.     {1, 16, 15, right2left, oper_positive},  // 正号  
  54.     {1, 16, 15, right2left, oper_negative},  // 负号  
  55.     {1, 16, 15, left2right, oper_factorial}, // 阶乘  
  56.     /* 关系运算 */  
  57.     {2, 10, 10, left2right, oper_lt},        // 小于  
  58.     {2, 10, 10, left2right, oper_gt},        // 大于  
  59.     {2, 9, 9, left2right, oper_eq},          // 等于  
  60.     {2, 9, 9, left2right, oper_ne},          // 不等于  
  61.     {2, 10, 10, left2right, oper_le},        // 不大于  
  62.     {2, 10, 10, left2right, oper_ge},        // 不小于  
  63.     /* 逻辑运算 */  
  64.     {2, 5, 5, left2right, oper_and},         // 且  
  65.     {2, 4, 4, left2right, oper_or},          // 或  
  66.     {1, 15, 15, right2left, oper_not},       // 非  
  67.     /* 赋值 */  
  68.     // BASIC 中赋值语句不属于表达式!  
  69.     {2, 2, 2, right2left, oper_assignment},  // 赋值  
  70.     /* 最小优先级 */  
  71.     {2, 0, 0, right2left, oper_min}          // 栈底  
  72. };  

你也许会问为什么需要 icp(incoming precedence)、isp(in-stack precedence) 两个优先级,现在不用着急,以后会详细解释!

后缀表达式

现在操作数(operand)和操作符(operator)都有了,一个表达式就是由它们组合构成的,我们就统称它们为标记(token)。在程序中定义如下:

  1. // in expression.h  
  2. typedef enum {  
  3.     token_operand = 1,  
  4.     token_operator  
  5. } token_type;  
  6. typedef struct {  
  7.     token_type type;  
  8.     union {  
  9.         OPERAND var;  
  10.         OPERATOR ator;  
  11.     };  
  12. } TOKEN;  
  13. typedef struct tlist {  
  14.     TOKEN token;  
  15.     struct tlist *next;  
  16. } TOKEN_LIST, *PTLIST;  

我们平时习惯将表达式符写作:operand operator operand(比如1+1),这是一个递归的定义,表达式本身也可作为操作数。像这种将操作符放在两个操作数之间的表达式称为中缀表达式,中缀表达式的好处是可读性强,操作数之间泾渭分明(尤其是手写体中)。但它有自身的缺陷:操作符的位置说明不了它在运算的先后问题。例如 1+2×3 中,虽然 + 的位置在 × 之前,但这并不表示先做加运算再做乘运算。为解决这个问题,数学中给操作符分了等级,级别高的操作符先计算(乘号的级别比加号高),并用括号提高操作符优先级。因此上例表达式的值是 7 而不是 (1+2)*3=9。

但对于计算机来说,优先级是一个多余的概念。就像上面提到的,中缀表达式中操作符的顺序没有提供运算先后关系的信息,这就好比用4个字节的空间仅保存1个字节数据——太浪费了!索性将操作符按照运算的先后排序:先计算的排最前面。此时操作符就不适合再放中间了,可以将它移到被操作数的后面:operand operand operator(比如 1 1 +)。上例中 1+2×3 就变化为 1 2 3 × +;(1+2)×3 变化成 1 2 + 3 ×,这种将操作符符放到操作数后面的表达式称为后缀表达式。同理还有将操作符符按照逆序放到操作数的前面的前缀表达式。

无论是前缀表达式还是后缀表达式,它们的优点都是用操作符的顺序来代替优先级,这样就可以舍弃括号等概念,化繁为简。

后缀表达式求值

请看下面的梯等式计算,比较中缀表达式和后缀表达式的求值过程。

  8 × ( 2 + 3 )        8 2 3 + ×
= 8 * 5              = 8 5 ×
= 40                 = 40

后缀表达式的求值方式:从头开始一个标记(token)一个标记地往后扫描,碰到操作数时先放到一个临时的空间里;碰到操作符就从空间里取出最后两个操作数,做相应的运算,然后将结果再次放回空间中。到了最后,空间中就只剩下操作数即运算结果!这个中缀表达式求值类似,只不过中缀表达式操作数取的是前后各一个。下面的代码是程序中后缀表达式求值的节选,其中只包含加法运算,其他运算都是类似的。

  1. // in expression.c  
  2. VARIANT eval ( const char expr[] )  
  3. {  
  4.     // …  
  5.     // 一些变量的定义和声明  
  6.   
  7.     // 将中缀表达式转换成后缀表达式  
  8.     // 转换方法将在后续文章中介绍  
  9.     list = infix2postfix ();  
  10.     while ( list ) {  
  11.         // 取出一个 token  
  12.         p = list;  
  13.         list = list->next;  
  14.   
  15.         // 如果是操作数就保存到 stack 中  
  16.         if ( p->token.type == token_operand ) {  
  17.             p->next = stack;  
  18.             stack = p;  
  19.             continue;  
  20.         }  
  21.   
  22.         // 如果是操作符…  
  23.         switch ( p->token.ator.oper ) {  
  24.         // 加法运算  
  25.         case oper_plus:  
  26.             // 取出 stack 中最末两个操作数  
  27.             op2 = stack;  
  28.             op1 = stack = stack->next;  
  29.   
  30.             if ( op1->token.var.type == var_double &&  
  31.                  op2->token.var.type == var_double ) {  
  32.                 op1->token.var.i += op2->token.var.i;  
  33.             } else {  
  34.                 // 字符串的加法即合并两个字符串  
  35.                 // 如果其中一个是数字则需要先转换为字符串  
  36.                 if ( op1->token.var.type == var_double ) {  
  37.                     sprintf ( s1, ”%g”, op1->token.var.i );  
  38.                 } else {  
  39.                     strcpy ( s1, op1->token.var.s );  
  40.                 }  
  41.                 if ( op2->token.var.type == var_double ) {  
  42.                     sprintf ( s2, ”%g”, op2->token.var.i );  
  43.                 } else {  
  44.                     strcpy ( s2, op2->token.var.s );  
  45.                 }  
  46.                 op1->token.type = var_string;  
  47.                 strcat ( s1, s2 );  
  48.                 strcpy ( op1->token.var.s, s1 );  
  49.             }  
  50.             free ( op2 );  
  51.             break;  
  52.         // …  
  53.         // 其他操作符方法类似  
  54.         default:  
  55.             // 无效操作符处理  
  56.             break;  
  57.         }  
  58.         free ( p );  
  59.     }  
  60.   
  61.     value = stack->token.var;  
  62.     free ( stack );  
  63.   
  64.     // 最后一个元素即表达式的值  
  65.     return value;  
  66. }  

总结

这一篇文章主要介绍了表达式中的操作符、操作数在程序内部的表示方法、后缀表达式的相关知识以及后缀表达式求值的方法。在下一篇文章中将着重介绍如何将中缀表达式转换成后缀表达式,请关注《用C语言写解释器(三)》。


版权声明

请尊重原创作品。转载请保持文章完整性,并以超链接形式注明原始作者“redraiment”和主站点地址,方便其他朋友提问和指正。

联系方式

我的邮箱,欢迎来信([email protected]
我的Blogger(子清行):http://redraiment.blogspot.com/
我的Google Sites(子清行):https://sites.google.com/site/redraiment
我的CSDN博客(梦婷轩):http://blog.csdn.net/redraiment
我的百度空间(梦婷轩):http://hi.baidu.com/redraiment

猜你喜欢

转载自blog.csdn.net/zy1049677338/article/details/80669018
今日推荐