K&R возможная ошибка в польском калькуляторе

Я не знаю, куда это опубликовать, но я думаю, что нашел довольно серьезную ошибку в программе калькулятора польского языка K&R. По сути, когда вы выполняете операцию, извлекаются два значения, а передается только результат. Проблема в том, что результат не помещается на вершину стека! Вот иллюстрация:

Ошибка польского калькулятора

Полный код польского калькулятора, представленный в учебнике, показан ниже:

#include <stdio.h>
#include <stdlib.h> /* for atof() */

#define MAXOP 100 /* max size of operand or operator */
#define NUMBER '0' /* signal that a number was found */

int getop(char []);
void push(double);
double pop(void);

/* reverse Polish calculator */
main()
{
    int type;
    double op2;
    char s[MAXOP];

    while ((type= getop(s)) != EOF) {
        switch (type) {
        case NUMBER:
            push(atof(s));
            break;
        case '+':
            push (pop() + pop()) ;
            break;
        case '*':
            push(pop() * pop());
            break;
        case '-':
            op2 = pop();
            push(pop() - op2);
            break;
        case '/':
            op2 = pop();
            if (op2 != 0.0)
                push(pop() / op2);
            else
                printf("error: zero divisor\n");
            break;
        case '\n':
            printf("\t%.8g\n", pop());
            break;
        default:
            printf("error: unknown command %s\n", s);
            break;
        }
    }
    system("Pause");
    return 0;
}

#define MAXVAL 100 /* maximum depth of val stack */

int sp = 0; /* next free stack position */
double val[MAXVAL]; /* value stack */

/* push: push f onto value stack */
void push(double f)
{
    if ( sp < MAXVAL)
        val[sp++] = f;
    else
        printf("error: stack full. can't push %g\n", f);
}

/* pop: pop and return top value from stack */
double pop(void)
{
    if (sp > 0)
        return val[--sp];
    else {
        printf("error: stack empty\n");
        return 0.0;
    }
}

#include <ctype.h>

int getch(void);
void ungetch(int);

/* getop: get next operator or numeric operand */
int getop(char s[])
{
    int i, c;

    while ((s[0] = c = getch()) == ' ' || c == '\t')
        ;
    s[1] = '\0';
    if (!isdigit(c) && c != '.')
        return c; /* not a number */
    i = 0;
    if (isdigit(c)) /*collect integer part*/
        while (isdigit(s[++i] = c = getch()))
            ;
    if (c == '.') /*collect fraction part*/
        while (isdigit(s[++i] = c = getch()))
            ;
    s[i] = '\0';
    if (c != EOF)
        ungetch(c);
    return NUMBER;
}

#define BUFSIZE 100

char buf[BUFSIZE]; /* buffer for ungetch */
int bufp = 0; /* next free position in buf */

int getch(void) /* get a (possibly pushed back) character */
{
    return (bufp > 0) ? buf[--bufp] : getchar();
}

void ungetch(int c) /* push character back on input */
{
    if (bufp >= BUFSIZE)
        printf("ungetch: too many characters\n");
    else
        buf[bufp++] = c;
}

Если вы хотите проверить сами, все, что я сделал, это добавил

static int pass = 0;
int i, check;
i = check = 0;

внутри цикла while в main() и

if(!check) {
    printf("pass #%d\n",pass++);
    while(val[i] != '\0') {
        printf("val[%d]: %.2f\n",i,val[i]);
        ++i;
    }
}

в конце цикла, сразу после оператора switch. Я также поставил check = 1; в случае '\n'.

В качестве возможного обходного пути я переписал функцию pop таким образом, чтобы извлеченные значения удалялись из массива val сразу после обращения к ним. Итак, вместо

double pop(void)
{
    if (sp > 0)
        return val[--sp];
    else {
        printf("error: stack empty\n");
        return 0.0;
    }
}

у вас будет что-то вроде

double pop(void)
{
    if (sp > 0) {
        double temp = val[--sp];
        val[sp] = '\0';
        return temp;
    }
    else {
        printf("error: stack empty\n");
        return 0.0;
    }
}

Я также переписал функцию push, чтобы значения всегда помещались в конец массива val. Итак, вместо

void push(double f)
{
    if ( sp < MAXVAL)
        val[sp++] = f;
    else
        printf("error: stack full. can't push %g\n", f);
}

у тебя было бы

void push(double f)
{
    if ( sp < MAXVAL) {
        while (val[sp] != '\0')
            ++sp;
        val[sp++] = f;
    }
    else
        printf("error: stack full. can't push %g\n", f);
}

Даже с этими изменениями мне все равно пришлось переписывать

case '\n':
        printf("\t%.8g\n", pop());
        break;

чтобы получить значение в верхней части стека, не извлекая его, что потребовало замены оператора printf простой функцией, такой как

void print_top(void)
{
    int i = 0;
    while( val[i] != '\0' )
        ++i;
    --i;
    printf("\t%.8g\n",val[i]);
}

Только тогда кажется, что польский калькулятор работает так, как задумано, по крайней мере, с точки зрения того, что стек делает за кулисами. Вы можете попробовать это сами с измененным кодом:

#include <stdio.h>
#include <stdlib.h> /* for atof() */
#include <ctype.h>

#define MAXOP 100 /* max size of operand or operator */
#define NUMBER '0' /* signal that a number was found */
#define MAXVAL 100 /* maximum depth of val stack */

int getop(char []);
void push(double);
double pop(void);
void print_top(void);

int sp = 0; /* next free stack position */
double val[MAXVAL]; /* value stack */

/* reverse Polish calculator */
main()
{
    int type;
    double op2;
    char s[MAXOP];

    while ((type= getop(s)) != EOF) {

        static int pass = 0;
        int i, check;
        i = check = 0;

        switch (type) {
        case NUMBER:
            push(atof(s));
            break;
        case '+':
            push (pop() + pop()) ;
            break;
        case '*':
            push(pop() * pop());
            break;
        case '-':
            op2 = pop();
            push(pop() - op2);
            break;
        case '/':
            op2 = pop();
            if (op2 != 0.0)
                push(pop() / op2);
            else
                printf("error: zero divisor\n");
            break;
        case '\n':
            print_top();
            check = 1;
            break;
        default:
            printf("error: unknown command %s\n", s);
            break;
        }
        if(!check) {
            printf("pass #%d\n",pass++);
            while(val[i] != '\0') {
                printf("val[%d]: %.2f\n",i,val[i]);
                ++i;
            }
        }
    }
    system("Pause");
    return 0;
}

/* push: push f onto value stack */
void push(double f)
{
    if ( sp < MAXVAL) {
        while (val[sp] != '\0')
            ++sp;
        val[sp++] = f;
    }
    else
        printf("error: stack full. can't push %g\n", f);
}

/* pop: pop and return top value from stack */
double pop(void)
{
    if (sp > 0) {
        double temp = val[--sp];
        val[sp] = '\0';
        return temp;
    }
    else {
        printf("error: stack empty\n");
        return 0.0;
    }
}

int getch(void);
void ungetch(int);

/* getop: get next operator or numeric operand */
int getop(char s[])
{
    int i, c;

    while ((s[0] = c = getch()) == ' ' || c == '\t')
        ;
    s[1] = '\0';
    if (!isdigit(c) && c != '.')
        return c; /* not a number */
    i = 0;
    if (isdigit(c)) /*collect integer part*/
        while (isdigit(s[++i] = c = getch()))
            ;
    if (c == '.') /*collect fraction part*/
        while (isdigit(s[++i] = c = getch()))
            ;
    s[i] = '\0';
    if (c != EOF)
        ungetch(c);
    return NUMBER;
}

#define BUFSIZE 100

char buf[BUFSIZE]; /* buffer for ungetch */
int bufp = 0; /* next free position in buf */

int getch(void) /* get a (possibly pushed back) character */
{
    return (bufp > 0) ? buf[--bufp] : getchar();
}

void ungetch(int c) /* push character back on input */
{
    if (bufp >= BUFSIZE)
    printf("ungetch: too many characters\n");
    else
        buf[bufp++] = c;
}

void print_top(void)
{
    int i = 0;
    while( val[i] != '\0' )
        ++i;
    --i;
    printf("\t%.8g\n",val[i]);
}

Обратите внимание, что мне пришлось переместить большинство моих операторов #define и объявлений прототипов в начало, чтобы приспособиться к оператору отладки printf в конце main().

* Отредактированы некоторые из моих смелых заявлений: P


person ericgrosse    schedule 17.06.2013    source источник
comment
Неа. Просто стек растет вверх, т.е. е. val[0] — это низ (и верх тоже, если есть только один элемент). И в то время, когда вы печатаете результат, val[1] недействителен, он уже выскочил.   -  person    schedule 18.06.2013
comment
` while (val[sp] != '\0')` выглядит неправильно.   -  person wildplasser    schedule 18.06.2013
comment
@wildplasser - вы абсолютно правы, что это абсолютно неправильно. ;-)   -  person Carl Norum    schedule 18.06.2013
comment
Исправление: это неверно.   -  person wildplasser    schedule 18.06.2013
comment
Учитывая, что это первая проблема в книге, которая вводит понятие стеков, тот факт, что она использует стеки неправильно, кажется чем-то, на что стоит обратить внимание... Нет, вот что стоит указать: именно по этим причинам чрезвычайно маловероятно, что ты прав.   -  person Jim Balter    schedule 18.06.2013
comment
Вау, еще вчера вы пытались выяснить разницу между *ptr и ptr, а сегодня публикуете основные ошибки в работе кода K&R, которые не заметили ни K&R, ни кто-либо из их бесчисленных читателей. Вот ваше слово на сегодня (и на следующий день, и на следующий день после этого..): смирение   -  person Jim Balter    schedule 18.06.2013
comment
По крайней мере, он отредактировал себя в сторону смирения. (к сожалению, он никогда не станет способным программистом на C++)   -  person wildplasser    schedule 18.06.2013
comment
К сожалению, он никогда не станет способным программистом на C++ Почему вы так говорите? Я понимаю, что другие язвительные комментарии были оправданы, но не этот.   -  person ericgrosse    schedule 18.06.2013
comment
Потому что ни один способный программист на C++ никогда не стал бы проявлять смирение в ответ на язвительные отзывы. Если бы прирожденный программист на C++ задал этот вопрос, все было бы в огне к настоящему времени... (а если серьезно, спасибо за отзыв с ходу - надеюсь, это урок для следующего раза)   -  person Shog9    schedule 18.06.2013
comment
Да, я чувствую себя идиотом после исправления Карла Норума :/   -  person ericgrosse    schedule 18.06.2013


Ответы (2)


  1. Вы думаете о стеке в обратном порядке - вершина стека находится в самом высоком допустимом индексе, а не в val[0]. Это поведение становится очевидным, когда вы смотрите на нажатия операндов. Ваш вывод:

    3 4 +
    pass #0
    val[0]: 3.00
    pass #1
    val[0]: 3.00
    val[1]: 4.00
    

    Во-первых, 3 помещается на вершину (ранее пустого) стека - он находится в слоте 0. Затем 4 помещается. Как видите, он переходит в val[1], ясно показывая, что val[0] в данном случае не является вершиной стека.

  2. Вы неправильно печатаете стек, что еще больше сбивает вас с толку. Измените цикл печати на:

    while (i < sp) {
        printf("val[%d]: %.2f\n",i,val[i]);
        ++i;
    }
    

    То есть печатайте только допустимые записи в стеке, и вы увидите свою ошибку.

    Ваше текущее сравнение ищет запись 0 в стеке, а программа не идентифицирует свободные записи. Для этого используется переменная sp. Помимо того, что он ищет что-то не то, он делает это странным образом - val - это массив чисел с плавающей запятой - почему вы сравниваете с символьным литералом \0?

    Вот полный исправленный вывод:

    3 4 +
    pass #0
    val[0]: 3.00
    pass #1
    val[0]: 3.00
    val[1]: 4.00
    pass #2
    val[0]: 7.00
        7
    

    Теперь вы видите правильный вывод: и 3.00, и 4.00 извлекаются, а 7.00 помещается обратно в стек. Теперь это единственная действующая запись.

person Carl Norum    schedule 17.06.2013
comment
Или, что более вероятно, какая-то отладка printf(). - person wildplasser; 18.06.2013
comment
Я не уверен, что следую. Я сгенерировал вывод, подняв код OP и исправив его. - person Carl Norum; 18.06.2013
comment
почему вы сравниваете с символьным литералом \0? -- Возможно, ОП переоценил понятие \0 как терминатор. - person Jim Balter; 18.06.2013
comment
Сравнение с \0 — мой способ добраться до конца массива произвольной длины. Я предполагаю, что это работает только для символьных массивов, так что вы обычно делаете, чтобы достичь конца, скажем, массива int? - person ericgrosse; 18.06.2013
comment
Вы знаете, насколько он велик, и используете счетчик. Что делать, если в вашем массиве int есть ноль? В общем, ваш трюк не работает и для массивов символов - только для правильно сформированных строк. - person Carl Norum; 18.06.2013

Неа. Просто стек растет вверх, т.е. е. val[0] — это низ (и верх тоже, если есть только один элемент). И в то время, когда вы печатаете результат, val[1] недействителен, он уже выскочил.

person Community    schedule 17.06.2013