В вашем коде есть несколько проблем:
- , если во входной строке нет токенов или синтаксическая ошибка, вы возвращаете
ret
неинициализированным из tokenize
.Вместо этого вы должны вернуть NULL
. ret[ret_ind].value[size + 1] = '\0';
сохраняет нулевой терминатор на один шаг дальше в выделенном массиве.Это должно быть ret[ret_ind].value[size] = '\0';
malloc(size * sizeof(char) + 1)
несовместимо: если вы настаиваете на использовании sizeof(char)
, который по определению равен 1
, вы должны написать malloc((size + 1) * sizeof(char))
, но использовать * 1017 идиоматично* в C, и вы также можете заменить несколько строк кода на простые ret[ret_ind].value = strndup(string + i, k);
- случаи для
L_PARENTHESIS
и R_PARENTHESIS
могут быть объединены в один блок. цикл освобождения должен прекратиться, когда вы достигнете токена TERMINAL
.В текущем коде вы не можете обрабатывать пустой список, который не следует создавать, но лучше сделать функции утилит более устойчивыми к последующим изменениям.
void deallocate(token *in) {
if (in) {
for (int i = 0; in[i] != TERMINAL; i++)
free(in[i].value);
free(in);
}
}
прототипы в token.h должен включать списки типизированных аргументов.
Вот упрощенная версия:
#include <stdio.h>
#include <stdlib.h>
#include "tokenize.h"
int token_count(const char *string) {
int count = 0;
int i = 0;
while (string[i] != '\0') {
switch (string[i++]) {
case ' ':
continue;
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
i += strspn(string + i, "0123456789");
continue;
case ADD:
case SUBTRACT:
case MULTIPLY:
case DIVIDE:
case EXPONENT:
case L_PARENTHESIS:
case R_PARENTHESIS:
count++;
continue;
default:
return -1;
}
}
return count;
}
token *tokenize(const char *string) {
int count = token_count(string);
if (count <= 0)
return NULL;
token *ret = malloc((count + 1) * sizeof(token));
int i = 0;
int ret_ind = 0;
while (string[i] != '\0') {
if (string[i] >= '0' && string[i] <= '9') {
int size = strspn(string + i, "0123456789");
ret[ret_ind].id = OPERAND;
ret[ret_ind].value = strndup(string + i, size);
ret_ind++;
i += size;
continue;
}
switch (string[i]) {
case ' ':
i++;
continue;
case ADD:
case SUBTRACT:
case MULTIPLY:
case DIVIDE:
case EXPONENT:
ret[ret_ind].id = OPERATOR;
ret[ret_ind].value = malloc(2);
ret[ret_ind].value[0] = string[i];
ret[ret_ind].value[1] = '\0';
ret_ind++;
i++;
continue;
case L_PARENTHESIS:
case R_PARENTHESIS:
ret[ret_ind].id = PARENTHESIS;
ret[ret_ind].value = malloc(2);
ret[ret_ind].value[0] = string[i];
ret[ret_ind].value[1] = '\0';
ret_ind++;
i++;
continue;
default:
break;
}
break;
}
ret[ret_ind].id = TERMINAL;
return ret;
}
void deallocate(token *in) {
if (in) {
for (int i = 0; in[i] != TERMINAL; i++)
free(in[i].value);
free(in);
}
}
Вот дополнительные замечания для остальныхкод:
зачем очищать экран при входе и выходе?
вы должны проверить конец файла в основном цикле:
if (!fgets(user_in, 1024, stdin))
break;
Вы должны эффективно удалить новую строку:
#include <string.h>
user_in[strcspn(user_in, "\n")] = '\0';
, тогда вы можете упростить тест для выхода:
if (!strcmp(user_in, "exit"))
break;
нет необходимости очищать user_in
после solve()
вы можете упростить тестирование, решив аргументы командной строки:
for (int i = 1; i < argc; i++)
solve(argv[i]);
Вы должны игнорировать пробелы и принимать пустые строки
Вы должны использовать "%.17g
вместо %lf
.Обратите внимание, что l
является обязательным для scanf()
для типа double
, но игнорируется для printf
, поскольку аргументы float
преобразуются в double
при передаче в функции vararg, такие как printf
.
вы должны использовать контекстную структуру и передавать указатель на нее в parse
и его вспомогательные функции, чтобы избежать глобальных переменных
, как вы можете видеть вtry_add_sub
и try_mul_div
, это упростит переключение для унификации типов токенов и позволит избежать классификации OPERATOR
.
синтаксический анализатор слишком сложен: вы должны использовать рекурсивное снижение более непосредственно: try_add_sub
должен сначала вызвать try_mul_div
и перебрать аддитивные операторы, вызывая try_mul_div
для каждого последующего операнда.Точно так же, try_mul_div
должен сначала вызвать try_exp
, а try_exp
вызовет try_primitive
, который будет обрабатывать скобки и константы.
этот подход потребляет один токен за раз, что можетбыть прочитанным из источника выражений на лету, минуя необходимость токенизации всей строки.
вы должны принять синтаксис полного числа для констант, что легко с strtod()
.
Вот упрощенная версия по этим направлениям:
//---- tokenize.h ----
#define TERMINAL 0
#define OPERAND 1
#define ERROR 2
#define ADD '+'
#define SUBTRACT '-'
#define MULTIPLY '*'
#define DIVIDE '/'
#define EXPONENT '^'
#define L_PARENTHESIS '('
#define R_PARENTHESIS ')'
#define SYNTAX_ERROR 1
#define PAREN_ERROR 2
typedef struct context {
char *p;
char *nextp;
int parenthesis_balance;
int error_code;
double value;
} context;
int this_token(context *cp);
void skip_token(context *cp);
//---- tokenize.c ----
#include <stdlib.h>
//#include "tokenize.h"
int this_token(context *cp) {
char *p = cp->p;
for (;;) {
switch (*p) {
case '\0':
cp->nextp = p;
return TERMINAL;
case ' ':
case '\t':
case '\n':
/* ignore white space */
p++;
continue;
case ADD:
case SUBTRACT:
case MULTIPLY:
case DIVIDE:
case EXPONENT:
case L_PARENTHESIS:
case R_PARENTHESIS:
/* single character operators */
cp->nextp = p + 1;
return *p;
default:
/* try and parse as a number constant */
cp->value = strtod(p, &cp->nextp);
if (cp->nextp > p)
return OPERAND;
return ERROR;
}
}
}
void skip_token(context *cp) {
cp->p = cp->nextp;
}
//---- parse.h ----
int parse(char expression[], double *result);
void solve(char expression[]);
//---- parse.c ----
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
//#include "tokenize.h"
//#include "parse.h"
/* expression parsers return non zero upon error */
int try_add_sub(context *cp, double *result);
int try_mul_div(context *cp, double *result);
int try_exp(context *cp, double *result);
int try_primary(context *cp, double *result);
int try_add_sub(context *cp, double *result) {
if (try_mul_div(cp, result))
return 1;
for (;;) {
double operand;
switch (this_token(cp)) {
case ADD:
skip_token(cp);
if (try_mul_div(cp, &operand))
return 1;
*result += operand;
continue;
case SUBTRACT:
skip_token(cp);
if (try_mul_div(cp, &operand))
return 1;
*result -= operand;
continue;
}
return 0;
}
}
int try_mul_div(context *cp, double *result) {
if (try_exp(cp, result))
return 1;
for (;;) {
double operand;
switch (this_token(cp)) {
case MULTIPLY:
skip_token(cp);
if (try_exp(cp, &operand))
return 1;
*result *= operand;
continue;
case DIVIDE:
skip_token(cp);
if (try_exp(cp, &operand))
return 1;
*result /= operand;
continue;
}
return 0;
}
}
int try_exp(context *cp, double *result) {
if (try_primary(cp, result))
return 1;
if (this_token(cp) == EXPONENT) {
double operand;
skip_token(cp);
if (try_exp(cp, &operand))
return 1;
*result = pow(*result, operand);
}
return 0;
}
int try_primary(context *cp, double *result) {
switch (this_token(cp)) {
case OPERAND:
skip_token(cp);
*result = cp->value;
return 0;
case L_PARENTHESIS:
skip_token(cp);
cp->parenthesis_balance++;
if (try_add_sub(cp, result))
return 1;
cp->parenthesis_balance--;
if (this_token(cp) != R_PARENTHESIS) {
cp->error_code = PAREN_ERROR;
return 1;
}
skip_token(cp);
return 0;
}
cp->error_code = SYNTAX_ERROR;
return 1;
}
/* parse and evaluate an expression, return error code, update result */
int parse(char expression[], double *result) {
context cc;
cc.nextp = cc.p = expression;
cc.parenthesis_balance = 0;
cc.error_code = 0;
cc.value = 0;
if (try_add_sub(&cc, result))
return cc.error_code;
if (this_token(&cc) != TERMINAL)
return SYNTAX_ERROR;
return 0;
}
void solve(char expression[]) {
double result = 0;
switch (parse(expression, &result)) {
case 0:
printf(" %.17g\n", result);
break;
case SYNTAX_ERROR:
printf("ERROR: Syntax\n");
break;
case PAREN_ERROR:
printf("ERROR: Unbalanced parenthesis\n");
break;
}
}
//---- calculator.c ----
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//#include "parse.h"
int main(int argc, char **argv) {
for (int i = 1; i < argc; i++)
solve(argv[i]);
if (argc == 1) {
char user_in[1024];
char *p;
printf("Terminal Calculator\n");
printf("Type 'exit' to terminate\n\n");
for (;;) {
printf("=> ");
if (!fgets(user_in, sizeof user_in, stdin)) {
printf("\n");
break;
}
/* strip trailing newline */
user_in[strcspn(user_in, "\n")] = '\0';
/* skip initial white space */
p = user_in + strspn(user_in, " \t");
/* ignore empty and comment lines */
if (*p == '\0' || *p == '#')
continue;
/* trap exit command */
if (!strcmp(p, "exit"))
break;
solve(p);
}
}
return 0;
}