《C程序设计伴侣》的8.7.3 向main()函数传递数据这一小节中,我们介绍了如何通过main()函数的参数,向程序传递两个数据并计算其和值的简单加法计算器add.exe。这个程序,好用是好用,就是太简单,还停留在幼儿园大班的水平,只能计算两位数的加法。我们现在基本都已经是大学生了,如果还是用这个简陋的加法计算器去向面试官展示我们的编程能力,肯定会遭到他们的笑话。

在看完《C程序设计伴侣》后,我们的编程能力已经今非昔比了。自然,我们也可以利用从这本书中学到的知识(函数,字符串处理等),把这个计算器改进一下,让他成为一个可以计算更多数据更多算符的高级计算器。

我们是怎么计算一个复杂计算式的?我们总是根据要求列出一个计算式,这个计算式中有数字(整数)和对数字进行操作的算符(+,-,*,/四种运算),然后,从左到右依次计算,最后得到结果。比如,我们要计算

1 + 2 – 3 * 4

我们总是先计算3*4得到12,然后计算1+2得到3,最后计算3-12得到结果-9;而如果我们想用程序对这个字符串表示的计算式进行计算,又该如何进行呢?如果这个计算式比较简单,比如,只有1+2,我们倒是可以找出其中的符号和数字字符,然后将数字字符转换为数字进行计算,而如果这个计算式比较复杂,比如这里的1+2-3*4又该如何进行呢?

回想一下,在数学课上老师是怎么教我们的?面对复杂的计算式,我们可以把它拆分成多个不太复杂的计算式,而不太复杂的计算式我们又可以将它拆分成简单的计算式。这种“大事化小,小事化了”的解题思路,刚好切合了我们的递归函数的设计思路。换句话说,他们都是将一个大问题转化为同类型的小问题,逐渐分解,直到最后可以很容易的得到结果。按照这样的递归函数的设计思路,同时结合数学中计算式的结合律(为了符合结合律,我们查找字符串中的运算符时,从字符串的末尾find_last()开始查找,这样可以避免运算顺序改变后更改运算符号。比如,6-3-3,如果我们从字符串的开始查找运算符,首先找到第一个减号,计算式被分为了(6) – (3-3)两部分,这样计算的结果就不正确了,如果我们从末尾开始查找运算符,则分解后得到(6-3) – (3)这样计算结果就是正确的。)另外还需要注意的是,乘除运算的优先级是高于加减运算的,因为函数的递归,实际上是最先计算最里层的函数,所以,我们应该先分解加减运算,将乘除运算放到最里层。

按照上面的思路分析,我们可以把这个更高级的,可以计算计算式字符串的计算器实现如下:

  1. /*
  2. * eval.c
  3. *
  4. * Created on: 2013年11月1日15:21:51
  5. * Author: Bruce
  6. */
  7. #include <string.h>
  8. #include <stdio.h>
  9.  
  10. int find_last(const char* s,char a)
  11. {
  12. int pos = strlen(s);
  13. //从字符串末尾位置开始查找
  14. const char* p = s + pos;
  15. //如果没有到达字符串开始的前一个位置(s-1)
  16. while((s-) != p)
  17. {
  18. //如果蛋清位置的字符就是要查找的字符
  19. if(*p == a)
  20. {
  21. break; //结束查找
  22. }
  23. p--; //变换到下一个位置
  24. pos--;
  25. }
  26. if((s-) != p) //找到字符
  27. {
  28. return pos;
  29. }
  30. else //未找到
  31. {
  32. return -;
  33. }
  34. }
  35. //取得字符串的左半部分
  36. char* left_str(char* s, int pos)
  37. {
  38. s[pos] = '\0';
  39. return s;
  40. }
  41. //取得字符串的右半部分
  42. char* right_str(char* s, int pos)
  43. {
  44. return s + pos + ;
  45. }
  46. //计算字符串计算式s的值
  47. int eval(char* s)
  48. {
  49. int n = ;
  50. //找到最后一个加号
  51. n = find_last(s,'+');
  52. if(- != n)
  53. {
  54. return eval(left_str(s,n)) + eval(right_str(s,n));
  55. }
  56. n = find_last(s,'-');
  57. if(- != n)
  58. {
  59. return eval(left_str(s,n)) - eval(right_str(s,n));
  60. }
  61. n = find_last(s,'*');
  62. if(- != n)
  63. {
  64. return eval(left_str(s,n)) * eval(right_str(s,n));
  65. }
  66. n = find_last(s,'/');
  67. if(- != n)
  68. {
  69. return eval(left_str(s,n)) / eval(right_str(s,n));
  70. }
  71. //当字符串中不包含运算符时,返回这个数字本身
  72. return atoi(s);
  73. }
  74. int main(int argc,char* argv[])
  75. {
  76. //检查参数是否合法
  77. if( != argc)
  78. {
  79. puts("usage: eval 1+2+3");
  80. return ;
  81. }
  82. // 复制从参数得到的计算式字符串
  83. char expr[] = "";
  84. strcpy(expr,argv[]);
  85.  
  86. // 对计算式字符串进行计算,得到结果
  87. int res = eval(expr);
  88. printf("%s = %d",argv[],res);
  89. return ;
  90. }

现在,我们就可以用这个更高级的计算器计算最开始的那个计算式了:

F:\code>gcc -o eval.exe 35.c

F:\code>eval 1+2-3*4
1+2-3*4 = -9

我们使用递归函数的方法,计算了一个简单字符串计算式的值。这种方法简单是简单,可是却有一个漏洞,那就是他无法计算带有括号的,改变了运算顺序的计算式。比如,他无法计算

1+(2-3)*4

这个简单表达式的值。如果遇到了计算式中有括号(这是很常见的),又该如何计算呢?

这个问题,实际上是编译原理中经典的一个问题,只要是计算机专业的同学,在学习编译原理的时候,几乎都会遇到,在网络上搜索一下,发现这实际上就是这门课程的一个作业题目,还有同学在网上问如何如何解决这道题目呢。

  1. 【问题描述】 设计一个实现表达式求值的演示程序。 【基本要求】 当用户输入一个合法的算术表达式后,能够返回正确的结果。能够计算的运算符包括:加、减、乘、除、括号;
  1. 能够计算的操作数要求在实数范围内;对于异常表达式能给出错误提示。
  1. 【测试数据】 1)请输入您所求的表达式 3*(7-2)+5 多项式的结果是: 20 2)请输入您所求的表达式 3.154*(12+18)-23 多项式的结果是: 71.62 【实现提示】 1首先置操作数栈为空栈,表达式起始符#为运算符栈的栈底元素; 2依次扫描表达式中每个字符,若是操作数则进OPND栈;若是运算符,
  1. 则和OPTR栈的栈顶运算符比较优先权后作相应操作,直至整个表达式求值完毕。 3先做一个适合个位的+-*/运算, 其次就要考虑到对n位和小数点的运算。

这应该算是编译原理中最常见的一个题目了。所谓求人不如求己。只要我们掌握了编译原理的基础原理,掌握了C++相关的基本知识(特别是STL中stack容器的使用(参见《我的第一本C++书》 ),如果只是学了C语言(参考《C程序设计伴侣》 ),需要知道栈的基本操作,可以自己实现一个栈),在按照这里的实现提示,就可以很轻松地自己解决这个问题。

你可以先尝试自己解决这个问题,也可以参考下面的实现,整个算法的思路在注视中。

  1. /*
  2. * eval2.cpp
  3. *
  4. * Created on: 2013年11月2日13:39:31
  5. * Author: Bruce
  6. */
  7.  
  8. #include <iostream>
  9. #include <string>
  10. #include <cctype>
  11. #include <stack>
  12. using namespace std;
  13.  
  14. //返回两个 操作符之间的优先级关系
  15. char cmp(char a,char b)
  16. {
  17. switch(a)
  18. {
  19. case '#': //'#'优先级最低
  20. return ('#' == b)? '=' : '<';
  21. break;
  22.  
  23. case '-':
  24. case '+': //'+' '-'的优先级小于'*' '/' '('
  25. {
  26. if('*' == b || '/' == b || '(' == b)
  27. {
  28. return '<';
  29. }
  30. else
  31. {
  32. return '>';
  33. }
  34. }
  35. break;
  36.  
  37. case '*':
  38. case '/': // '*''/'的优先级小于'('而大于其他
  39. {
  40. return ('('==b)?'<':'>';
  41. }
  42. break;
  43.  
  44. case '(': // '('的优先级等于')'
  45. return (')'==b)?'=':'<';
  46. break;
  47. default: // 不支持的操作符,抛出异常
  48. throw "error:unkown operator";
  49. }
  50. }
  51.  
  52. // 用操作符对两个操作数进行操作,返回结果
  53. int calc(int a, int b, char op)
  54. {
  55. switch(op)
  56. {
  57. case '+':
  58. return a+b;
  59. case '-':
  60. return a-b;
  61. case '*':
  62. return a*b;
  63. case '/':
  64. if( == b) // 特殊处理除数为0
  65. throw "error: the divisor shoud not be negtive.";
  66. else
  67. return a/b;
  68. default:
  69. throw "error: unknown operator.";
  70. }
  71. }
  72.  
  73. // 判断当前字符是否是操作符
  74. bool isoptr(char c)
  75. {
  76. // 合法的操作符列表
  77. static string optrs("+-*/()#");
  78. // 如果在列表中无法找到
  79. if(optrs.find(c) == string::npos)
  80. {
  81. if(isdigit(c))
  82. return false; //不是算符
  83. else // 不支持的操作符
  84. throw "error: unknown char.";
  85. }
  86. return true; // 是操作符
  87. }
  88.  
  89. //求计算式e的值
  90. int eval(string e)
  91. {
  92. e += "#"; //添加一个#表示表达式结束
  93. stack<int> opnd; //操作数栈
  94. stack<char> optr; //操作符栈
  95. optr.push('#'); //在操作符栈添加'#'表示开始
  96. int i = ; //计算式的起始扫描位置
  97. int num = ; //从表达式中提取数字
  98.  
  99. //这个字符解析计算式
  100. //直到表达式没有遇到结束符'#'
  101. //或者操作符栈中还有操作符
  102. while(e[i] != '#' || optr.top() != '#')
  103. {
  104. //判断当前字符是否是操作符
  105. if(!isoptr(e[i]))
  106. {
  107. // 不是操作符,则是操作数
  108. // 利用循环从计算式中提取数字
  109. num = ;
  110. // 逐个字符向后遍历,直到遇到操作符为止
  111. while(!isoptr(e[i]))
  112. {
  113. num *= ; // 将已经提取的数字向前移动一位
  114. num += e[i] - ''; // 加上当前数字,
  115. ++i;
  116. }
  117. //将操作数压入操作数栈
  118. opnd.push(num);
  119. }
  120. else // 如果当前字符是操作符
  121. {
  122. // 比较当前操作符与操作符栈顶操作符的优先级
  123. // 根据优先级采取不同策略
  124. if(optr.empty())
  125. {
  126. throw "error: optr is empty";
  127. }
  128. switch(cmp(optr.top(),e[i]))
  129. {
  130. // 栈顶操作符优先级低,暂不计算,新操作符入栈
  131. // 比如在1+2中,操作符栈中最开始的#和+比较,
  132. // #小于+,所以不执行计算,+直接压入操作符栈
  133. case '<': // 小于
  134. optr.push(e[i]);
  135. ++i; // 解析下一个字符
  136. break;
  137.  
  138. // 优先级相等,说明')'遇到了'(',
  139. // 或者是'#'遇到了'#',
  140. // 那么')'或'#'出栈,新符号不入栈
  141. case '=':
  142. optr.pop();
  143. ++i;
  144. break;
  145.  
  146. // 栈顶运算符优先级高,暂停输入,计算
  147. // 比如,1+2#末尾的#,当他与此时栈顶+比较
  148. // +的优先级大于#,从操作数栈中取两个数1和2,
  149. // 同时取出操作符栈顶的+进行计算
  150. case '>':
  151. // 取出两个数计算结果
  152. if(opnd.empty())
  153. {
  154. throw "error: opnd is empty.";
  155. }
  156. // 从操作数栈中取第一个数
  157. int a = opnd.top(); opnd.pop();
  158. if(opnd.empty())
  159. {
  160. throw "error: opnd is empty.";
  161. }
  162. // 取第二个数
  163. int b = opnd.top(); opnd.pop();
  164.  
  165. // 这里要注意a,b的顺序,a先出栈,也就是后入栈,说明是操作符
  166. // 之后的操作数,所以这里应该是b op a
  167. // 将计算结果压入操作数栈,作为新的操作数
  168. opnd.push(calc(b, a, optr.top())); //注意这里a和b的顺序
  169. // 已经计算过的操作符出栈
  170. optr.pop();
  171. // 注意,这里没有进行++i,
  172. // 而是直接再次对当前操作符进行处理
  173. break;
  174. }
  175. }
  176. }
  177. return opnd.top();
  178. }
  179.  
  180. int main()
  181. {
  182. string expr; // 计算式
  183. while(true)
  184. {
  185. cout<<"please input the expression. 'end' for exit"<<endl;
  186. cin>>expr;
  187. if("end" == expr)
  188. break;
  189. try
  190. {
  191. int res = eval(expr);
  192. cout<<expr<<" = "<<res<<endl;
  193. }
  194. catch (const char* err)
  195. {
  196. cout<<err<<endl;
  197. }
  198. }
  199. return ;
  200. }

现在,这个计算器已经足够高级了,他可以计算加减乘除和括号,也能够对异常情况进行处理。比如一开始的那个计算式:

F:\code>eval
please input the expression.  ‘end’ for exit

1+(2-3)*4

1+(2-3)*4 = -3

please input the expression.  ‘end’ for exit

end

唯一的遗憾是目前他只支持整数,而题目的要求是实数范围内,不过不要紧,只要我们看明白了整个算法的思路和过程,自然可以轻松将其扩展到实数范围。即使是老师险恶地要求扩展到支持其他运算,比如乘方开方等,我们自己也能搞定,再也不用到处求爷爷告奶奶了。

这个例子也再次证明了毛主席的那句话:

只有自己动手,才能丰衣足食!

本文在创作过程中参考了以下两篇文章,特此鸣谢。

转自:http://www.howzhi.com/course/3387/lesson/43244

如何计算一个字符串表示的计算式的值?——C_递归算法实现的更多相关文章

  1. python面试题之如何计算一个字符串的长度

    在我们想计算长度的字符串上调用函数len()即可 >>> len('hhhhhhhhjg') 10 所属网站分类: 面试经典 > python 作者:外星人入侵 链接:http ...

  2. 判断一个字符串str不为空的方法

    1.str == null; 2."".equals(str); 3.str.length 4.str.isEmpty(); 注意:length是属性,一般集合类对象拥有的属性,取 ...

  3. 如何识别一个字符串是否Json格式

    前言: 距离上一篇文章,又过去一个多月了,近些时间,工作依旧很忙碌,除了管理方面的事,代码方面主要折腾三个事: 1:开发框架(一整套基于配置型的开发体系框架) 2:CYQ.Data 数据层框架(持续的 ...

  4. 字符串混淆技术应用 设计一个字符串混淆程序 可混淆.NET程序集中的字符串

    关于字符串的研究,目前已经有两篇. 原理篇:字符串混淆技术在.NET程序保护中的应用及如何解密被混淆的字符串  实践篇:字符串反混淆实战 Dotfuscator 4.9 字符串加密技术应对策略 今天来 ...

  5. php查找字符串首次出现的位置 判断字符串是否在另一个字符串中

    strpos - 查找字符串首次出现的位置 说明 int strpos ( string $haystack , mixed $needle [, int $offset = 0 ] ) 返回 nee ...

  6. js中解析json对象:JSON.parse()用于从一个字符串中解析出json对象, JSON.stringify()用于从一个对象解析出字符串。

    JSON.parse()用于从一个字符串中解析出json对象. var str = '{"name":"huangxiaojian","age&quo ...

  7. divmod(a,b)函数是实现a除以b,然后返回商与余数的元组、eval可以执行一个字符串形式的表达式、exec语句用来执行储存在字符串或文件中的Python语句

    #!/usr/bin/env python a = 10/3 print(a) #divmod计算商与余数 r = divmod(10001,20) print(r) #eval可以执行一个字符串形式 ...

  8. Java中将一个字符串传入数组的几种方法

    String Str="abnckdjgdag"; char a[]=new char[Str.length()]; -------------------方法1 用于取出字符串的 ...

  9. 有一字符串,包含n个字符。写一函数,将此字符串中从第m个字符开始的全部字符复制成为另一个字符串。

    [提交][状态][讨论版] 题目描述 有一字符串,包含n个字符.写一函数,将此字符串中从第m个字符开始的全部字符复制成为另一个字符串. 输入 数字n 一行字符串 数字m 输出 从m开始的子串 样例输入 ...

随机推荐

  1. 浅谈dynamic的简单使用用法

    今天看了博客园里面的dynamic用法,我犹豫从来没接触过,今天恶补了一下,把我对dynamic的认识分享了出来,大家一起学习. Visual C# 2010 引入了一个新类型 dynamic. 该类 ...

  2. 002--VS C++ 获取鼠标坐标并显示在窗口上

    //--------------------------------------------MyPaint() 函数------------------------------------------ ...

  3. TFT LCD 参数详解

    我的板子设置HCLK=100M因此CLKVAL= int(HCLK/(VCLK*2)-1),其中VCLK即上图的DCLK=6.4M, CLKVAL="int"(100/12.8-1 ...

  4. VIM实用基本操作技巧

    文本编辑器有很多,图形模式下有gedit.kwrite等编辑器,文本模式下的编辑器有vi.vim(vi的增强版本)和nano.vi和vim是Linux系统中最常用的编辑器.有人曾这样的说过在世界上有三 ...

  5. 2014103《JAVA程序设计》第一周学习总结

    本周,在刻苦看了三天课本之后,终于对JAVA这门课程有了一定的认识.了解了JAVA的前世今生,JAVA的三大平台:Java SE.Java EE与Java ME.其中Java SE又可分为四个主要的部 ...

  6. public、protect、private在父类子类中使用

    先贴出一张,直观的.估计大家都见过的关于public.protect.private的范围图 作用域 当前类 同一package 子孙类 其他package public     T         ...

  7. 关于BaseAdapter的使用及优化心得(一)

    对于Android程序员来说,BaseAdapter肯定不会陌生,灵活而优雅是BaseAdapter最大的特点.开发者可以通过构造BaseAdapter并搭载到ListView或者GridView这类 ...

  8. android开发 根据Uri获取真实路径

    Uri uri = data.getData(); String[] proj = { MediaStore.Images.Media.DATA }; Cursor actualimagecursor ...

  9. 前端之JavaScript第一天学习(1)-JavaScript 简介

    javaScript 是世界上最流行的编程语言. 这门语言可用于 HTML 和 web,更可广泛用于服务器.PC.笔记本电脑.平板电脑和智能手机等设备. JavaScript 是脚本语言 JavaSc ...

  10. MySQL中求年龄

    时间函数: 1.curdate() --- 当前系统日期 调取: select curdate() 2.curtime() --- 当前系统时间 调取: select curtime() 3.now( ...