我在和一个人讨论如何在堆栈溢出区使用GOTO。有人能教我一些使用后藤的隐秘技巧吗?你有什么改进的建议吗?你可以享受我的小冒险游戏,试试看。^^
在你阅读源之前玩游戏,否则你会被宠坏。
#include <stdio.h>
#include <stdlib.h>
enum _directions{
DIR_0 = 0b0000,
DIR_E = 0b0001,
DIR_W = 0b0010,
DIR_WE = 0b0011,
DIR_S = 0b0100,
DIR_SE = 0b0101,
DIR_SW = 0b0110,
DIR_SWE = 0b0111,
DIR_N = 0b1000,
DIR_NE = 0b1001,
DIR_NW = 0b1010,
DIR_NWE = 0b1011,
DIR_NS = 0b1100,
DIR_NSE = 0b1101,
DIR_NSW = 0b1110,
DIR_NSWE = 0b1111
} DIRECTIONS;
void giveline(){
printf("--------------------------------------------------------------------------------\n");
}
void where(int room, unsigned char dir){
printf("\nYou are in room %i. Where do you want GOTO?\n", room);
if(dir & 8) printf("NORTH: W\n");
else printf(".\n");
if(dir & 4) printf("SOUTH: S\n");
else printf(".\n");
if(dir & 2) printf("WEST: A\n");
else printf(".\n");
if(dir & 1) printf("EAST: D\n");
else printf(".\n");
}
char getdir(){
char c = getchar();
switch(c){
case 'w' :
case 'W' :
return 'N';
case 's' :
case 'S' :
return 'S';
case 'a' :
case 'A' :
return 'W';
case 'd' :
case 'D' :
return 'E';
case '\e' :
return 0;
}
return -1;
}
int main(int argc, char *argv[]){
START:
printf("THE EVIL GOTO DUNGEON\n");
printf("---------------------\n");
printf("\nPress a direction key \"W, A, S, D\" followed with 'ENTER' for moving.\n\n");
char dir = -1;
ROOM1:
giveline();
printf("Somehow you've managed to wake up at this place. You see a LABEL on the wall.\n");
printf("\"Do you know what's more evil than an EVIL GOTO DUNGEON?\"\n");
printf("You're wondering what this cryptic message means.\n");
where(1, DIR_SE);
do{
dir = getdir();
if(dir == 'S') goto ROOM4;
if(dir == 'E') goto ROOM2;
}while(dir);
goto END;
ROOM2:
giveline();
printf("Besides another LABEL, this room is empty.\n");
printf("\"Let's play a game!\"\n");
where(2, DIR_W);
do{
dir = getdir();
if(dir == 'W') goto ROOM1;
}while(dir);
goto END;
ROOM3:
giveline();
printf("Man, dead ends are boring.\n");
printf("Why can't I escape this nightmare?\n");
where(3, DIR_S);
do{
dir = getdir();
if(dir == 'S') goto ROOM6;
}while(dir);
goto END;
ROOM4:
giveline();
printf("Is this a real place, or just fantasy?\n");
printf("\"All good things come in three GOTOs.\"\n");
where(4, DIR_NSE);
do{
dir = getdir();
if(dir == 'N') goto ROOM1;
if(dir == 'S') goto ROOM7;
if(dir == 'E') goto ROOM5;
}while(dir);
goto END;
ROOM5:
giveline();
printf("This is a big river crossing. I guess I need to JUMP.\n");
where(5, DIR_SWE);
do{
dir = getdir();
if(dir == 'S') goto ROOM8;
if(dir == 'W') goto ROOM4;
if(dir == 'E') goto ROOM6;
}while(dir);
goto END;
ROOM6:
giveline();
printf("This place doesn't look very promising.\n");
where(6, DIR_NSW);
do{
dir = getdir();
if(dir == 'N') goto ROOM3;
if(dir == 'S') goto ROOM9;
if(dir == 'W') goto ROOM5;
}while(dir);
goto END;
ROOM7:
giveline();
printf("\"Give a man a LOOP and you feed him FOR a WHILE;\n");
printf(" teach a man a GOTO and you feed him for a RUNTIME.\"\n");
where(7, DIR_NE);
do{
dir = getdir();
if(dir == 'N') goto ROOM4;
if(dir == 'E') goto ROOM8;
}while(dir);
goto END;
ROOM8:
giveline();
printf("This looks like an endless LOOP of rooms.\n");
where(8, DIR_NW);
do{
dir = getdir();
if(dir == 'N') goto ROOM5;
if(dir == 'W') goto ROOM7;
}while(dir);
goto END;
ROOM9:
giveline();
printf("You've found your old friend Domino. He doesn't looks scared, like you do.\n");
printf("\n\"Listen my friend,\n");
printf(" If you want to escape this place, you need to find the ESCAPE KEY.\"\n");
printf("\nWhat does this mean?\n");
where(9, DIR_N);
do{
dir = getdir();
if(dir == 'N') goto ROOM6;
}while(dir);
goto END;
printf("You never saw me.\n");
END:
giveline();
printf("The End\n");
return 0;
}发布于 2020-10-14 15:02:30
goto的争论由来已久,1966年埃德加·迪克斯特拉( Edgar )提出了一份名为“去发表被认为有害的声明”的著名论文。这是有争议的,争论一直持续到今天。尽管如此,他的大部分结论至今仍然有效,goto的大多数用途被认为是有害的意大利面编程。
然而,人们普遍认为goto的某些用途是可以接受的。具体地说:
goto应该只用于向下跳,而不是向上跳。goto只应用于错误处理和函数结束时的集中清理。这是一个古老的“设计模式”,我相信/恐惧来源于基本的“关于错误的.”因为它是错误处理的首选方法。基本上,在这个虚构的示例中使用goto只被认为是确定的:
status_t func (void)
{
status_t status = OK;
stuff_t* stuff = allocate_stuff();
...
while(something)
{
while(something_else)
{
status = get_status();
if(status == ERROR)
{
goto error_handler; // stop execution and break out of nested loops/statements
}
}
}
goto ok;
error_handler:
handle_error(status);
ok:
cleanup(stuff);
return status;
}如上例所示,使用goto被认为是可以接受的。有两个明显的好处:干净的方法打破嵌套语句和集中式错误处理和清理在函数的末尾,避免代码重复。
尽管如此,用return和包装器函数编写同样的东西还是有可能的,我个人认为这个函数更简洁,避免了"goto认为有害“的争论:
static status_t internal_func (stuff_t* stuff)
{
status_t status = OK;
...
while(something)
{
while(something_else)
{
status = get_status();
if(status == ERROR)
{
return status;
}
}
}
return status;
}
status_t func (void)
{
status_t status;
stuff_t* stuff = allocate_stuff();
status = internal_func(stuff);
if(status != OK)
{
handle_error(status);
}
cleanup(stuff);
return status;
}编辑:
我发布了一个单独的长篇大论的答案,这里关于所有不相关的事情。包括如何使用适当的状态机设计重写整个程序的建议。
发布于 2020-10-15 01:23:09
您编写的代码或多或少是一台状态机,编写方式可能是用汇编语言构造的。这样的技术在技术上是可行的,但是它不能很好地扩展,而且您可能最终会遇到非常难以调试的问题。您的代码只需要稍微调整一下,就可以使用更传统的C语言方法来实现状态机,这更容易阅读、维护和调试。
int main(int argc, char *argv[])
{
int state = START;
char dir = -1;
while(1)
{
switch (state)
{
case START:
printf("THE EVIL GOTO DUNGEON\n");
printf("---------------------\n");
printf("\nPress a direction key \"W, A, S, D\" followed with 'ENTER' for moving.\n\n");
state = ROOM1;
break;
case ROOM1:
giveline();
printf("Somehow you've managed to wake up at this place. You see a LABEL on the wall.\n");
printf("\"Do you know what's more evil than an EVIL GOTO DUNGEON?\"\n");
printf("You're wondering what this cryptic message means.\n");
where(1, DIR_SE);
do{
dir = getdir();
if(dir == 'S') { state = ROOM4; break; }
if(dir == 'E') { state = ROOM2; break; }
}while(dir);
break;
case ROOM2:
giveline();
printf("Besides another LABEL, this room is empty.\n");
printf("\"Let's play a game!\"\n");
where(2, DIR_W);
do{
dir = getdir();
if(dir == 'W') { state = ROOM1; break; }
}while(dir);
break;
...
case END:
giveline();
printf("The End\n");
return 0;
}
}
}代码与以前大致相同,只做了几处小调整:
ROOMX:更改为case ROOMX:goto ROOMX;到state = ROOMX; break;的跳转START、ROOMX等定义的常量(未显示)以这种方式构造代码可以提高代码的可读性,并避免了goto意大利面可能带来的许多问题。确保您不会无意中从一个房间的代码掉到下一个房间的代码(如果您绕过设置新状态的代码,只需在同一个房间内再试一次)就容易多了。您还避免了goto的许多限制,比如无法“跳过”变量长度数组的声明(参见C99语言规范第6.8.6.1节中的示例2)。您还可以添加一个显式的default案例,以智能地处理任何意外的或错误的房间选择。
这种结构也为改进开辟了各种途径。您可以获取每个case的内容并将其封装到一个函数中,并且可以将每一种情况简化为case ROOMX: state = do_roomx(); break;。使用每个房间的代码封装,您可以单独单元测试室。
您还可以注意到,每个房间的代码都遵循可预测的顺序(giveline() ->、打印描述、->、where()、->、读取、输入、->、选择下一个房间),并编写了一个可以处理任意房间的通用函数do_room(struct room_data* room)。然后,您将创建一个数据结构struct room_data,它保存每个房间所需的所有信息(描述文本、移动方向、每个出口引线的位置等)。这将更类似于游戏引擎的工作方式。您的代码将变得更短、更通用,并且每个单独的空间都将被实现为数据而不是代码。您甚至可以将房间数据存储在外部文件中,然后您将有一个通用的游戏引擎,您不必每次修改迷宫时都要重新编译。
问:“我如何更好地使用后藤?”就像问:“我怎么才能用更好的方式打自己的脸呢?”答案是:没有。像你这样使用goto与我所知道的“更好”这个词的定义是不相容的。您正在使用C本身处理的构造( switch块),并使用显式跳转重新实现它。你得到的功能更少,潜在的问题更多。唯一接近“更好”的方法是放弃不必要的gotos。
请记住,C语言只是在汇编语言之上的一个薄的、可移植的单板。goto是您的CPU“跳转”指令的包装器。据我所知,没有一个CPU有类似于switch或for之类的指令。这些都是语法糖,编译器为您重写成由“跳转”指令驱动的序列。例如,这样一个简单的循环:
for (i = 0; i < limit; i++)
{
... code ...
}就好像它是这样写的:
i = 0;
LOOP_START:
if (!(i < limit))
goto LOOP_END;
... code ...
LOOP_CONTINUE:
i++;
goto LOOP_START;
LOOP_END:continue语句将等价于goto LOOP_CONTINUE,break语句将等效于goto LOOP_END。
switch块的实现类似。每个大小写都是带有标签的代码块,switch根据输入值跳转到标签。break跳到最后。这通常类似于您编写代码的方式。关键的区别是switch块不会在不同情况下直接跳转。如果您想要执行多个情况,可以使用一个循环来多次运行switch块。
最终,switch版本和goto版本在编译后可能看起来几乎完全相同。当您使用switch时,您可以让编译器有机会避免某些问题,例如确保在跳过局部变量初始化器时不会跳入局部变量的范围。编写goto-based版本时,编译器将按编写的方式编译代码,并相信您知道自己在做什么。如果您坚持显式地使用goto,那么您最终会遇到导致人们发明像switch这样的东西的问题。当“不惜一切代价”使用goto时,这些成本常常是一个不一致和不可预测的程序。如果你在寻找如何编程不当的建议,那你就错了。
发布于 2020-10-14 15:18:40
的错误检查
函数getdir()应该检查有效输入,也许是应该接收一个有效方向数组。输入无效方向时,应向用户发送输入无效的消息。
goto的使用迫使您重复不应该重复的代码,例如
where(2, DIR_W);
do {
dir = getdir();
if (dir == 'W') goto ROOM1;
} while (dir);
goto END;整个程序似乎是一个如何编写意大利面码的例子,这是一个非常难以编写、调试和维护的非结构化代码。
如果代码是结构化的,并且使用了while循环或for循环,那么代码实际上会更小,也更容易理解。
中使用二进制
你输入的字符越多,就越容易出错。由于比特在枚举中很重要,我建议使用八进制或十六进制,最好是十六进制。然后可以使用一个字符定义每个枚举。
与其在代码中使用神奇的数字,不如定义掩码。
https://codereview.stackexchange.com/questions/250656
复制相似问题