我写了Brainfuck解释器,以便为自己的C工作做好准备。我尝试编写尽可能清晰清晰的代码。有人可以看一下代码,给我一些改进的提示吗?
// tape.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef struct Tape {
    long pointer;
    long capacity;
    unsigned short *data;
} Tape;


void initializeTape(Tape *tape);

void growTape(Tape *tape);

void incrementPointer(Tape *tape);

void decrementPointer(Tape *tape);

void incrementValue(Tape *tape);

void decrementValue(Tape *tape);

void read(Tape *tape);

void get(Tape *tape);

void freeTape(Tape *tape);

long interpret(Tape *tape, const char *source_code, long source_code_size, long position);

tape.h的实现
// tape.c
#include "tape.h"

void initializeTape(Tape *tape) {
    tape->pointer = 0;
    tape->capacity = 8;
    tape->data = (unsigned short *) calloc( tape->capacity, sizeof(unsigned short));
    if (tape->data == NULL) {
        fprintf(stderr, "Out of memory error.\n");
        exit(1);
    }
}

void growTape(Tape *tape) {
    tape->capacity *= 2;
    tape->data = (unsigned short *) realloc(tape->data, tape->capacity);
    if (tape->data == NULL) {
        fprintf(stderr, "Out of memory error.\n");
        exit(1);
    }
}

void incrementPointer(Tape *tape) {
    if (tape->pointer >= tape->capacity) {
        growTape(tape);
    }
    tape->pointer++;
}

void decrementPointer(Tape *tape) {
    if (tape->pointer == 0) {
        fprintf(stderr, "Syntax error. Negative pointer detected.");
        exit(1);
    }
    tape->pointer--;
}

void incrementValue(Tape *tape) {
    tape->data[tape->pointer]++;
}

void decrementValue(Tape *tape) {
    tape->data[tape->pointer]--;
}

void read(Tape *tape) {
    putchar(tape->data[tape->pointer]);
}

void get(Tape *tape) {
    tape->data[tape->pointer] = (char) getchar();
}

void freeTape(Tape *tape) {
    free(tape->data);
    tape->pointer = 0;
    tape->capacity = 0;
}

long interpret(Tape *tape, const char *source_code, long source_code_size, long position) {
    char c = source_code[position];
    switch (c) {
        case '>':
            incrementPointer(tape);
            break;
        case '<':
            decrementPointer(tape);
            break;
        case '+':
            incrementValue(tape);
            break;
        case '-':
            decrementValue(tape);
            break;
        case '.':
            read(tape);
            break;
        case ',':
            get(tape);
            break;
        case '[':
            if (tape->data[tape->pointer] == (char) 0) {
                int stack = 1;
                long j = position + 1;
                for (; j < source_code_size && stack > 0 && tape->pointer < source_code_size; j++) {
                    char _c = source_code[j];
                    if (_c == '[') {
                        ++stack;
                    } else if (_c == ']') {
                        --stack;
                    }
                }
                if (stack != 0) {
                    fprintf(stderr, "Syntax error. Missing closing ].\n");
                    exit(1);
                } else {
                    position = j + 1;
                }
            }
            break;
        case ']':
            if (tape->data[tape->pointer] != (char) 0) {
                int stack = 1;
                long j = position - 1;
                for (; j >= 0 && stack > 0 && tape->pointer >= 0; j--) {
                    char _c = source_code[j];
                    if (_c == '[') {
                        --stack;
                    } else if (_c == ']') {
                        ++stack;
                    }
                }
                if (stack != 0) {
                    fprintf(stderr, "Syntax error. Missing opening [.\n");
                    exit(1);
                } else {
                    position = j + 1;
                }
            }
            break;
        default:
            break;
    }
    return ++position;
}

main文件:
// main.c
#include "tape.h"

int main(int argc, char **argv) {
    FILE *file;
    if (argc < 2) {
        file = fopen("helloworld.bf", "r");
    } else {
        file = fopen(argv[1], "r");
    }
    if (file == NULL) {
        fprintf(stderr, "Can not open file %s\n", argv[1]);
        return 1;
    }

    if (fseek(file, 0L, SEEK_END) != 0) {
        fprintf(stderr, "Fail to fseek file %s\n", argv[1]);
        return 1;
    }
    long filesize = ftell(file);
    if (filesize < 0) {
        fprintf(stderr, "Fail to read file's size\n");
        return 1;
    }
    rewind(file);

    char source_code[filesize];
    size_t result = fread(source_code, 1, filesize, file);

    if (fclose(file) != 0) {
        fprintf(stderr, "Can not close file %s\n", argv[1]);
        return 1;
    }

    if (result != filesize) {
        fprintf(stderr, "Can not read file. Corrupt\n");
    }

    Tape tape;
    initializeTape(&tape);
    long i = 0;

    while(i < filesize) {
        i = interpret(&tape, source_code, filesize, i);
    }
    freeTape(&tape);

    return 0;
}


评论

次要细节:调用realloc()时不要丢弃原始指针,因为如果失败,您可能仍想访问(或释放)原始数据。您通常会看到类似void * newptr = realloc(...);的内容。如果(newptr)data = newptr;其他handle_nomemory();

带着一点阅读困难,这个问题的标题表明一个人在脑中实现了C解释器……这将是相当惊人的,并且肯定意味着对C的了解足以胜任工作!

语法错误是一种特定类型的错误。负的磁带指针不是一个。

#1 楼

总体观察结果
解释器应该能够从标准以及文件中读取内容,这将破坏整个磁带模型。用户还可以将输入文件重定向到标准输入。
如果要使用C语言进行编程,则需要使指针适应。
对于文件输入,我将使用一种读取以下内容的算法:一次一行,这样就不需要读取文件两次,也不需要分配用于存储文件的内存。一次读取一行也可以用于控制台输入。如果在嵌入式环境中使用C,则分配空间来存储内存可能会严重影响可用于处理的内存量。因此,在嵌入式环境中使用malloc()calloc()realloc()时,还需要小心。一些嵌入式C编译器不支持内存分配,一些公司将采用不包含嵌入式应用程序内存分配的编码标准。
仅包含使代码编译所需的头文件
头文件tape.h包括assert.h和程序中未使用assert()。由于include的C预处理器实现通常是创建一个临时源文件并实际上复制包含的头文件,因此无需使用该临时源文件即可增加临时源文件的大小并增加编译时间。
在其他include中隐藏#include语句文件有时可能会导致问题,包括使头文件编译所必需的文件,并且tape.h不需要任何头文件来进行编译。何时需要在tape.h中包含头文件的一个示例是,是否有返回类型为bool的函数,然后该头文件应包含语句#include <stdbool.h>
弄清楚每个C源文件需要编译什么通过将头文件包含在C源文件中。
附带说明一下,最好不要使用assert,因为如果对代码进行了优化,则所有断言都会在代码之外进行优化。
性能(速度)
在程序的主循环中以及在如果使用字符指针而不是整数索引,则可能会改善函数interpret()的执行时间。除了可能改善性能外,这还可以通过减少临时变量的数量来减少函数interpret()中的代码量。请注意,以下代码尚未经过测试,可能无法运行。
main()中:
    char* current_source_code_ptr = source_code;
    char* end_file_ptr = &source_code[filesize - 1];
    while (current_source_code_ptr < end_file_ptr) {
        current_source_code_ptr = interpret(current_source_code_ptr, end_file_ptr, source_code, &tape);
    }

char* interpret(char* current_source_code_ptr, const char* end_file_ptr, const char *source_code, Tape* tape) {
    switch (*current_source_code_ptr) {
    case '>':
        incrementPointer(tape);
        break;
    case '<':
        decrementPointer(tape);
        break;
    case '+':
        incrementValue(tape);
        break;
    case '-':
        decrementValue(tape);
        break;
    case '.':
        read(tape);
        break;
    case ',':
        get(tape);
        break;
    case '[':
        if (tape->data[tape->pointer] == (char)0) {
            int stack = 1;
            for (; current_source_code_ptr < end_file_ptr && stack > 0 && tape->pointer < (end_file_ptr - source_code); current_source_code_ptr++) {
                if (*current_source_code_ptr == '[') {
                    ++stack;
                }
                else if (*current_source_code_ptr == ']') {
                    --stack;
                }
            }
            if (stack != 0) {
                fprintf(stderr, "Syntax error. Missing closing ].\n");
                exit(EXIT_FAILURE);
            }
            else {
                current_source_code_ptr++;
            }
        }
        break;
    case ']':
        if (tape->data[tape->pointer] != (char)0) {
            int stack = 1;
            for (; current_source_code_ptr >= source_code && stack > 0 && tape->pointer >= 0; current_source_code_ptr--) {
                if (*current_source_code_ptr == '[') {
                    --stack;
                }
                else if (*current_source_code_ptr == ']') {
                    ++stack;
                }
            }
            if (stack != 0) {
                fprintf(stderr, "Syntax error. Missing opening [.\n");
                exit(EXIT_FAILURE);
            }
            else {
                current_source_code_ptr++;
            }
        }
        break;
    default:
        break;
    }
    return ++current_source_code_ptr;
}

复杂度
函数interpret()中的switch / case语句太长,每种情况应可以通过函数实现,因此case '[':case ']':的代码应移到单独的函数中。
使用系统定义的常量stdlib.h包含宏EXIT_SUCCESS和EXIT_FAILURE的系统特定定义。这将使代码更具可读性,并且可能更易于移植。
// main.c
#include <stdio.h>
#include <stdlib.h>
#include "tape.h"

int main(int argc, char** argv) {
    FILE* file;
    if (argc < 2) {
        file = fopen("helloworld.bf", "r");
    }
    else {
        file = fopen(argv[1], "r");
    }
    if (file == NULL) {
        fprintf(stderr, "Can not open file %s\n", argv[1]);
        return EXIT_FAILURE;
    }

    if (fseek(file, 0L, SEEK_END) != 0) {
        fprintf(stderr, "Fail to fseek file %s\n", argv[1]);
        return EXIT_FAILURE;
    }
    long filesize = ftell(file);
    if (filesize < 0) {
        fprintf(stderr, "Fail to read file's size\n");
        return EXIT_FAILURE;
    }
    rewind(file);

    char source_code[filesize];
    size_t result = fread(source_code, 1, filesize, file);

    if (fclose(file) != 0) {
        fprintf(stderr, "Can not close file %s\n", argv[1]);
        return EXIT_FAILURE;
    }

    if (result != filesize) {
        fprintf(stderr, "Can not read file. Corrupt\n");
    }

    Tape tape;
    initializeTape(&tape);
    long i = 0;

    while (i < filesize) {
        i = interpret(&tape, source_code, filesize, i);
    }
    freeTape(&tape);

    return EXIT_SUCCESS;
}


评论


\ $ \ begingroup \ $
“一次只需要一行,...不需要分配”-Brainfuck是否有严格的行长限制?
\ $ \ endgroup \ $
– Toby Speight
20年11月9日,9:39

\ $ \ begingroup \ $
我会谨慎地假设指针算术比指针+整数偏移量更有效。通常,CPU具有处理指针+寄存器偏移量的本机指令。而且,如果编译器可以内联所有内容,则可能根本没有任何区别。在这种情况下,没什么大不了的,除了在IMO中使用偏移量编写要比使用指针编写好一点,这是在递增指针()和递减指针()中检查是否到达磁带的末尾。
\ $ \ endgroup \ $
– G. Sliepen
20年11月9日,12:59

#2 楼

仅包括所需的头文件
,请看tape.h,它仅包含声明,没有定义。这样,头文件仅用于膨胀源代码并增加编译时间。您应该将它们移至tape.c
如果可能,请使用static方法
如果我查看main.c,则使用的唯一功能是initializeTapeinterpretfreeTape。这些是构成接口的唯一功能。您可以将其他函数移至tape.c并声明为static。请记住,头文件应仅包含必要的函数。
使用来自的固定宽度整数
我不喜欢使用longunsigned shortlong long等数据类型,因为该标准使无法保证这些类型的实际大小;只有最小尺寸。最好使用诸如int64_tuint16_tintptr_t等固定类型。

initializeTapegrowTape不应退出
想象一下,当用户试图在其项目之一中使用您的代码时。如果分配失败,该程序将退出,并且不会让用户控制如何处理该错误。
请根据是否已成功分配内存,考虑返回一个值,例如0或-1,甚至truefalse(如果可以访问C99)。这样,用户可以检查并确定发生故障时的处理方法。
if(!initializeTape(&tape))
{
    // Do some error handling here
}

分配后不必强制转换为unsigned short *
没问题,但是我应该提到不必在分配后强制转换为所需的类型,因为void*可以隐式转换为其他指针类型。 。

评论


\ $ \ begingroup \ $
tape.h仅包含声明,没有定义。 -是的-这就是重点。它应该像这样保持;这是标准的操作,并且假设我们不生活在1970年代,那么将它们混入.c文件不会为您带来很大的编译效率。
\ $ \ endgroup \ $
– Reinderien
20年11月8日14:34

\ $ \ begingroup \ $
该标准确实为可以由内置类型表示的值的范围提供了一些保证。固定宽度类型(例如int64_t)通常是一个糟糕的选择,例如,与int_fast64_t相比尤其如此。
\ $ \ endgroup \ $
– Toby Speight
20年11月9日,9:42

#3 楼

您可以使用perror()提供有关库调用失败的更多有用信息。例如,考虑

if (file == NULL) {
    fprintf(stderr, "Can not open file %s\n", argv[1]);
    return EXIT_FAILURE;
}


我们可以获得更好的错误消息(例如“找不到文件”,“权限被拒绝”等),如下所示:
if (!file) {
    perror(argv[1]);
    return EXIT_FAILURE;
}


评论


\ $ \ begingroup \ $
但是perror()不幸的是没有显示文件名是什么。您可以编写fprintf(stderr,“无法打开%s:%s \ n”,argv [1],strerror(errno)),或者如果您使用的是Linux或BSD,则有非常方便的err(),可惜没有标准C.
\ $ \ endgroup \ $
– G. Sliepen
20年11月9日,12:53

\ $ \ begingroup \ $
是@ G.Sliepen,尽管在我的示例中,它确实显示了文件名(以不显示哪个操作失败为代价)。正如您所展示的,我通常最终还是将fprintf()与strerror()一起使用(或者在GNU环境中,将fprintf()与%m一起使用)。
\ $ \ endgroup \ $
– Toby Speight
20年11月9日,13:16

#4 楼

通常,您的代码看起来很合理,只需要为将来的开发者保留一件事即可。通常,您的函数initializeTape和文件磁带的其余部分。c
void initializeTape(Tape *tape) {
    tape->pointer = 0;
    tape->capacity = 8;
    tape->data = (unsigned short *) calloc( tape->capacity, sizeof(unsigned short));
    if (tape->data == NULL) {
        fprintf(stderr, "Out of memory error.\n");
        exit(1);
    }
}

应检查指针磁带是否不为空
void initializeTape(Tape *tape) {
    if (tape) {
        tape->pointer = 0;
        tape->capacity = 8;
        tape->data = (unsigned short *) calloc( tape->capacity, sizeof(unsigned short));
        if (tape->data == NULL) {
            fprintf(stderr, "Out of memory error.\n");
            exit(1);
        }
    } // else exit(-1) or whatever you choose
}

这将消除潜在的问题(无效的指针)如果您扩展代码。
希望对您有所帮助

评论


\ $ \ begingroup \ $
对于使用指针arg的函数来说,要求其为非NULL而不检查它是正常的,例如如果您为它的前两个参数之一传递NULL指针,则fprintf本身只会崩溃。除了作为调试检查退出之外,您在此功能中无用可做。但是取消引用NULL指针将在大多数实现中确实可靠地崩溃,因此只需执行此操作,并让人们使用调试器来查找传递NULL指针的位置(如果发生)。
\ $ \ endgroup \ $
– Peter Cordes
20年11月9日23:58