如何从C中的字符串获取和评估表达式

时间:2013-01-23 04:51:39

标签: c string

如何从 C

中的字符串中获取和评估表达式
char *str = "2*8-5+6";

这应该在评估后给出 17 的结果。

4 个答案:

答案 0 :(得分:3)

亲自尝试。你可以使用堆栈数据结构来评估这个字符串这里是对实现的引用(在c ++中) stack data structre for string calcualtion

答案 1 :(得分:2)

你必须自己做,C没有提供任何方法来做到这一点。 C是一种非常低级的语言。最简单的方法是找到一个可以执行此操作的库,或者如果不存在则使用lex + yacc创建自己的解释器。

快速谷歌建议如下:

答案 2 :(得分:1)

您应该尝试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

答案 3 :(得分:0)

C 没有标准的 eval() 函数。

有很多库和其他工具可以做到这一点。

但是,如果您想学习如何自己编写表达式求值器,那会非常容易。这不是微不足道:它实际上是一个非常深入的理论问题,因为您基本上是在编写一个微型解析器,可能构建在微型词法分析器上,就像真正的编译器一样。

编写解析器的一种直接方法涉及一种称为 recursive descent 的技术。编写递归下降解析器与另一种解决大问题或困难问题的伟大技术有很多共同之处,即将大问题分解成更小的、更容易的子问题。

让我们看看我们能想出什么。我们将编写一个函数 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 **,并调用函数 {{1 }} 做这项工作。 (但别担心,我不会拖延;稍后我们将开始做一些有趣的事情。)

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 + 后跟另一个子表达式,然后可能是一个 -+由另一个子表达式,依此类推,只要它一直看到 -+。这是代码。由于它将表达式的附加“项”相加,因此它通过调用函数 - 来获取子表达式。它还需要查找 evalterm()+ 运算符,它通过调用函数 - 来完成此操作。有时它会看到除 gettok()+ 之外的运算符,但这些不是它的工作要处理,所以如果它看到其中之一,它会“取消获取”它并返回,因为它已经完成。所有这些函数都传递指向指针 - 的指针,因为正如我之前所说,所有这些函数都是这样在解析字符串时跟踪它们如何在字符串中移动的。

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,它会得到另一个子表达式(另一个术语)并将其添加到运行总和中。类似地,如果它看到一个 +,它会得到另一个项并从运行总和中减去它。如果它得到其他任何东西,这意味着它已经完成,因此它“取消”它不想处理的运算符,并返回运行总和 - 这实际上是它评估的值。 (- 语句也打破了“无限”循环。)

在这一点上,您可能会感到“好吧,这开始有意义了”和“等一下,您在这里玩得很快而且很松散,这永远行不通,是吗?” ?”但它会奏效,我们将看到。

我们需要的下一个函数是收集要通过 evalexpr() 相加(和相减)的“项”或子表达式。该函数是return,它最终与evalterm()非常相似——非常相似——。它的工作是收集一系列由 evalexpr 和/或 * 连接的一个或多个子表达式,并将它们相乘和相除。在这一点上,我们将把这些子表达式称为“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; } } } 的结构最终与 evalexpr 完全 一样,只是它使用 evalterm 和 {{1} },并调用 * 来获取/评估其子表达式。

那么现在让我们看看 /。它的工作是计算表达式的三个最低级别但优先级最高的元素:实际数字、带括号的子表达式和一元 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 函数。但现在是时候多说一点了。它实际上是我们的小解析器使用的 lexical analyzer。词法分析器返回原始“标记”。标记是编程语言的基本句法元素。令牌可以是单个字符,如 evalexprevalterm,也可以是多字符实体。像 + 这样的整数常量被视为单个标记。在 C 中,其他标记是关键字(如 -)和标识符(如 123)以及多字符运算符(如 whileprintf)。 (不过,我们的小表情评估器没有任何这些。)

所以 <= 必须返回两件事。首先,它必须返回它找到的令牌类型的代码。对于像 ++gettok 这样的单字符标记,我们会说代码就是字符。对于数字常量(即 any 数字常量),我们将说 + 将返回字符 -。但是我们将需要某种方式来知道数值常量的值是什么,而且您可能已经猜到,这就是 gettok 函数的第二个指针参数的用途。当 1 返回 gettok 表示一个数字常量时,如果调用者传递一个指向 gettok 值的指针,1 将在那里填充整数值。 (稍后我们将看到 int 函数的定义。)

无论如何,通过对 gettok 的解释,我们可以理解 gettok。它获得一个令牌,传递局部变量 gettok 的地址,如有必要,可以在其中返回令牌的值。如果标记是表示整数常量的 evalpri,我们只需返回该整数常量的值。如果标记是 v,这是一个一元减号,所以我们得到另一个子表达式,否定它,然后返回它。最后,如果标记是 1,我们会得到另一个完整的表达式,并返回 它的 值,检查以确保在它之后还有另一个 - 标记。而且,正如您可能注意到的,在括号内,我们递归调用返回顶级 ( 函数以获取子表达式,因为显然我们希望允许任何子表达式,即使是包含较低优先级运算符的子表达式,例如 { {1}} 和 ),在括号内。

我们快完成了。接下来我们可以看看evalexpr。正如我提到的,+ 是词法分析器,检查单个字符并从中构建完整的标记。现在,我们终于开始了解如何使用传递的指针到指针 -

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,表示一个整数常量。

否则——如果下一个字符不是数字——它是一个普通字符,大概是像 vpstrtoul 这样的运算符或像 1 + 这样的标点符号,所以我们简单地返回字符,在增加 - 后记录我们已经消费它的事实。正确地“递增” ( 有点棘手:它是一个指向指针的指针,我们想要递增指向的指针。如果我们写了 )*p,它会增加指针 p,所以我们需要 p++ 来表示它是我们想要增加的指向指针。 (另见C FAQ 4.3。)

另外两个实用函数,然后我们就完成了。这是*p++

p

这只是跳过零个或多个空白字符,由 (*p)++ 中的 skipwhite 函数确定。 (同样,我们要小心记住 void skipwhite(const char **p) { while(isspace(**p)) (*p)++; } 是一个指向指针的指针。)

最后,我们来到isspace。这是递归下降解析器(或者实际上几乎所有解析器)的一个标志,它必须在输入中“向前看”,根据下一个标记做出决定。然而,有时它决定它毕竟还没有准备好处理下一个标记,所以它希望将它留在输入中,供解析器的其他部分稍后处理。

填充输入“回到输入流”,可以这么说,可能很棘手。 <ctype.h> 的这种实现很简单,但它绝对不完美:

p

它甚至不看它被要求放回的令牌;它只是将指针后退 1。如果 (但仅当)它被要求取消获取的令牌实际上是最近获得的令牌,并且它不是整数常量,这将起作用令牌。事实上,对于编写的程序,只要它解析的表达式格式正确,情况总是如此。可以编写一个更复杂的 ungettok 版本来明确检查这些假设,并且在必要时能够备份多字符标记(例如整数常量),但是这篇文章已经得到比我预期的要长得多,所以我现在不会担心。

但如果你还在我身边,我们就完了!如果您还没有,我鼓励您将我提供的所有代码复制到您友好的邻居 C 编译器中,然后尝试一下。例如,您会发现 ungettok 给出 7(而不是 9),因为解析器“知道”void ungettok(int op, const char **p) { (*p)--; } gettok 的优先级高于 1 + 2 * 3 和 { {1}}。就像在真正的编译器中一样,您可以使用括号覆盖默认优先级:* 给出 9。从左到右的结合也有效:/ 是 -4,而不是 +2。它也处理了许多复杂的,也许令人惊讶(但合法)的情况:+ 的计算结果仅为 5,而 - 的计算结果仅为 4(它被解析为“负负负负四”,因为我们的简化解析器没有 C 的 (1 + 2) * 3 运算符)。

这个解析器确实有一个很大的局限性,但是:它的错误处理很糟糕。它会处理合法的表达式,但对于非法的表达式,它要么做一些奇怪的事情,要么就忽略这个问题。例如,它只是忽略它无法识别或未预料到的任何尾随垃圾——表达式 1 - 2 - 3(((((5)))))----4 的计算结果都为 9。

尽管有点像“玩具”,但这也是一个非常真实的解析器,正如我们所见,它可以解析很多真实的表达式。通过研究此代码,您可以了解很多关于表达式的解析方式(以及编译器的编写方式)。 (一个脚注:递归下降并不是编写解析器的唯一方法,事实上,真正的编译器通常会使用更复杂的技术。)

您甚至可能想尝试扩展此代码,以处理其他运算符或其他“主要”(例如可设置变量)。事实上,曾几何时,我从这样的东西开始,并将其一直扩展到实际的 C 解释器中。