如何从C中的字符串获取和评估表达式
char *str = "2*8-5+6";
这应该在评估后给出结果为17 。
自己试试。你可以使用堆栈数据结构来评估这个字符串这里是实现(它在c ++中) 堆栈数据结构用于字符串计算的参考
你必须自己做,C 没有提供任何方法来做到这一点。C是一种非常低级的语言。最简单的方法是找到一个库,或者如果不存在,请使用 lex + yacc 创建您自己的解释器。
一个快速的谷歌建议如下:
你应该试试TinyExpr。它是一个可以添加到项目中的单个 C 源代码文件(没有依赖项)。
使用它来解决您的问题只是:
#include <stdio.h>
#include "tinyexpr.h"
int main()
{
double result = te_interp("2*8-5+6", 0);
printf("Result: %f\n", result);
return 0;
}
那将打印出:Result: 17
C 没有标准eval()
函数。
有很多库和其他工具可以做到这一点。
但是,如果您想学习如何自己编写表达式求值器,这可能会非常容易。这不是微不足道的:它实际上是一个非常深入的理论问题,因为您基本上是在编写一个微型解析器,可能构建在一个微型词法分析器上,就像一个真正的编译器一样。
编写解析器的一种直接方法涉及一种称为递归下降的技术。编写递归下降解析器与解决大问题或难题的另一种伟大技术有很多共同之处,即将大而难的问题分解成更小且更容易的子问题。
所以让我们看看我们能想出什么。我们将编写一个函数,该函数int eval(const char * expr)
接受一个包含表达式的字符串,并返回计算int
结果。但首先让我们编写一个小的主程序来测试它。我们将读取用户使用 输入的一行文本fgets
,将其传递给我们的expr()
函数,然后打印结果。
#include <stdio.h>
int eval(const char *expr);
int main()
{
char line[100];
while(1) {
printf("Expression? ");
if(fgets(line, sizeof line, stdin) == NULL) break;
printf(" -> %d\n", eval(line));
}
}
所以现在我们开始写eval()
。第一个问题是,在解析字符串时,我们如何跟踪读取字符串的距离?一个简单(虽然有点神秘)的方法是传递一个指向下一个字符的指针的指针。这样任何函数都可以在字符串中向前(或偶尔向后)移动。所以我们的eval()
函数几乎什么都不做,除了获取指向要解析的字符串的指针的地址,导致char **
我们刚刚决定需要,并调用一个函数evalexpr()
来完成这项工作。(但别担心,我没有拖延;一会儿我们就会开始做一些有趣的事情。)
int evalexpr(const char **);
int eval(const char *expr)
{
return evalexpr(&expr);
}
所以现在是时候写了evalexpr()
,这将开始做一些实际的工作。它的工作是对表达式进行第一个顶级解析。它将寻找一系列被添加或减去的“术语”。所以它想得到一个或多个子表达式,它们之间有+
或-
运算符。也就是说,它将处理如下表达式
1 + 2
或者
1 + 2 - 3
或者
1 + 2 - 3 + 4
或者它可以读取单个表达式,例如
1
或者任何被添加或减去的术语都可以是一个更复杂的子表达式,因此它也能够(间接地)处理诸如
2*3 + 4*5 - 9/3
但底线是它想要获取一个表达式,然后可能是 a +
or-
后跟另一个子表达式,然后可能是 a +
or-
后跟另一个子表达式,依此类推,只要它一直看到 a +
or -
。这是代码。由于它是将表达式的附加“项”相加,因此它通过调用函数来获取子表达式evalterm()
。它还需要查找+
and-
运算符,它通过调用函数来完成此操作gettok()
。有时它会看到除+
or以外的运算符-
,但这些不是它要处理的工作,所以如果它看到其中一个,它会“取消”它并返回,因为它已经完成了。所有这些函数都传递指针到指针p
因为正如我之前所说,这就是所有这些函数在解析字符串时如何跟踪它们在字符串中移动的方式。
int evalterm(const char **);
int gettok(const char **, int *);
void ungettok(int, const char **);
int evalexpr(const char **p)
{
int r = evalterm(p);
while(1) {
int op = gettok(p, NULL);
switch(op) {
case '+': r += evalterm(p); break;
case '-': r -= evalterm(p); break;
default: ungettok(op, p); return r;
}
}
}
这是一些非常密集的代码,仔细观察它并说服自己它正在做我描述的事情。它调用evalterm()
一次,以获取第一个子表达式,并将结果分配给局部变量r
。然后它进入一个潜在的无限循环,因为它可以处理任意数量的加减项。在循环内部,它获取表达式中的下一个运算符,并使用它来决定要做什么。(不要担心第二个,NULL
参数gettok
; 我们会在一分钟内解决这个问题。)
如果它看到 a +
,它会获取另一个子表达式(另一个术语)并将其添加到运行总和中。类似地,如果它看到 a -
,它会得到另一个项并将其从运行总和中减去。如果它得到其他任何东西,这意味着它已经完成,所以它“取消”它不想处理的运算符,并返回运行总和——这实际上是它评估的值。(该return
语句还打破了“无限”循环。)
在这一点上,你可能会感到“好吧,这开始有意义了”和“等一下,你在这里玩得又快又松,这永远行不通,是吗?” 但它会起作用,正如我们将看到的。
我们需要的下一个函数是收集要通过 evalexpr() 一起添加(和减去)的“项”或子表达式。该功能是evalterm()
,它最终非常相似 -非常相似 - evalexpr
。*
它的工作是收集一系列由and/or连接的一个或多个子表达式/
,并将它们相乘和相除。此时,我们将把这些子表达式称为“primaries”。这是代码:
int evalpri(const char **);
int evalterm(const char **p)
{
int r = evalpri(p);
while(1) {
int op = gettok(p, NULL);
switch(op) {
case '*': r *= evalpri(p); break;
case '/': r /= evalpri(p); break;
default: ungettok(op, p); return r;
}
}
}
这里实际上没有什么可说的了,因为 的结构evalterm
最终与 evalexpr完全一样,除了它用*
and做事/
,它调用evalpri
获取/评估它的子表达式。
那么现在让我们来看看evalpri
。它的工作是计算表达式的三个最低级别但优先级最高的元素:实际数字、带括号的子表达式和一元运算-
符。
int evalpri(const char **p)
{
int v;
int op = gettok(p, &v);
switch(op) {
case '1': return v;
case '-': return -evalpri(p);
case '(':
v = evalexpr(p);
op = gettok(p, NULL);
if(op != ')') {
fprintf(stderr, "missing ')'\n");
ungettok(op, p);
}
return v;
}
}
首先要做的是调用gettok
我们在evalexpr
and中使用的相同函数evalterm
。但现在是时候多说一点了。它实际上是我们的小解析器使用的词法分析器。词法分析器返回原始“令牌”。标记是编程语言的基本句法元素。标记可以是单个字符,例如+
or -
,也可以是多字符实体。整数常量 like123
被认为是单个标记。在 C 中,其他标记是关键字,如while
,和标识符printf
,以及多字符运算符,如<=
and ++
。(不过,我们的小表情评估器没有这些。)
所以gettok
必须返回两件事。首先,它必须返回它找到的令牌类型的代码。对于单字符标记+
,-
我们会说代码就是字符。对于数字常量(即任何数字常量),我们会说它gettok
会返回字符1
。但是我们需要一些方法来知道数字常量的值是什么,正如你可能已经猜到的,这就是gettok
函数的第二个指针参数的用途。当gettok
返回1
时表示一个数字常量,并且如果调用者传递一个指向一个int
值的指针,gettok
将在那里填充整数值。(我们将看到gettok
一会儿起作用。)
无论如何,通过这种解释gettok
,我们可以理解evalpri
。它获取一个标记,传递一个局部变量的地址v
,如果需要,可以在其中返回标记的值。如果标记是一个1
指示整数常量,我们只需返回该整数常量的值。如果标记是 a -
,这是一个一元减号,所以我们得到另一个子表达式,取反并返回它。最后,如果标记是 a (
,我们得到另一个完整的表达式,并返回它的值,检查以确保它)
后面还有另一个标记。而且,您可能会注意到,在括号内,我们递归调用顶层evalexpr
函数来获取子表达式,因为显然我们希望允许任何子表达式,即使是包含低优先级运算符的子表达式,例如+
and -
,在括号内。
我们几乎完成了。接下来我们可以看看gettok
。正如我提到的,gettok
是词法分析器,检查单个字符并从中构造完整的标记。我们现在终于开始了解如何使用传递的指针到指针p
。
#include <stdlib.h>
#include <ctype.h>
void skipwhite(const char **);
int gettok(const char **p, int *vp)
{
skipwhite(p);
char c = **p;
if(isdigit(c)) {
char *p2;
int v = strtoul(*p, &p2, 0);
*p = p2;
if(vp) *vp = v;
return '1';
}
(*p)++;
return c;
}
表达式可以包含任意空格,这些空格会被忽略,因此我们使用辅助函数跳过它skipwhite
。现在我们看看下一个角色。 p
是一个指向那个字符的指针,所以这个字符本身就是**p
. 如果是数字,我们调用strtoul
转换它。 strtoul
有用地返回一个指向它扫描的数字后面的字符的指针,因此我们使用它来更新p
. vp
我们用为我们计算的实际值填充传递的指针strtoul
,并返回1
指示整数常量的代码。
否则——如果下一个字符不是数字——它是一个普通字符,大概是一个类似+
or-
或类似标点符号的操作符(
)
,所以我们简单地返回这个字符,然后递增*p
以记录我们已经使用它的事实。正确地“递增”p
有点棘手:它是一个指向指针的指针,我们想要递增指向的指针。如果我们写p++
或者*p++
它会增加指针p
,那么我们需要(*p)++
说它是我们想要增加的指向指针。(另见C 常见问题 4.3。)
还有两个实用函数,然后我们就完成了。这是skipwhite
:
void skipwhite(const char **p)
{
while(isspace(**p))
(*p)++;
}
这只是跳过零个或多个空格字符,由isspace
函数 from确定<ctype.h>
。(同样,我们注意记住这p
是一个指针对指针。)
最后,我们来到ungettok
. 这是递归下降解析器(或者实际上是几乎任何解析器)的一个标志,它必须在输入中“向前看”,根据下一个标记做出决定。然而,有时它决定它毕竟还没有准备好处理下一个标记,所以它想把它留在输入中,以便解析器的其他部分稍后处理。
可以说,填充输入“回到输入流”可能很棘手。这个实现ungettok
很简单,但绝对不完美:
void ungettok(int op, const char **p)
{
(*p)--;
}
它甚至不看它被要求放回的令牌;它只是将指针向上备份 1。如果(但仅当)它被要求取消获取的令牌实际上是最近获得的令牌,并且它不是整数常量令牌时,这将起作用。事实上,对于编写的程序,只要它解析的表达式格式正确,情况总是如此。可以编写一个更复杂的版本gettok
来明确检查这些假设,如果需要的话,它可以备份多字符标记(例如整数常量),但是这篇文章比我写的要长得多故意的,所以我暂时不担心这个。
但如果你还在我身边,我们就完了!如果您还没有,我鼓励您将我提供的所有代码复制到您友好的社区 C 编译器中,然后尝试一下。例如,您会发现1 + 2 * 3
给出 7(而不是 9),因为解析器“知道”这一点*
并且/
具有比+
and更高的优先级-
。就像在真正的编译器中一样,您可以使用括号覆盖默认优先级:(1 + 2) * 3
给出 9。从左到右的关联性也有效:1 - 2 - 3
是 -4,而不是 +2。它也处理大量复杂的,也许令人惊讶(但合法)的情况:(((((5)))))
评估为 5,----4
评估为 4(它被解析为“负负负负四”,因为我们的简化解析器没有 C'--
然而,这个解析器确实有一个很大的限制:它的错误处理很糟糕。它会处理合法的表达式,但是对于非法的表达式,它要么做一些奇怪的事情,要么就忽略这个问题。例如,它会简单地忽略它无法识别或没有预料到的任何尾随垃圾——表达式4 + 5 x
,4 + 5 %
和4 + 5 )
所有计算结果都为 9。
尽管有点像“玩具”,但这也是一个非常真实的解析器,正如我们所见,它可以解析很多真实的表达式。通过研究这段代码,您可以了解很多关于如何解析表达式(以及如何编写编译器)的知识。(一个脚注:递归下降不是编写解析器的唯一方法,实际上真正的编译器通常会使用相当复杂的技术。)
您甚至可能想尝试扩展此代码,以处理其他运算符或其他“主要”(例如可设置变量)。事实上,曾几何时,我从类似这样的东西开始,并将其一直扩展到一个实际的 C 解释器。