加载中...
代码改善:一个“坑爹”的文字类冒险游戏
第1节:代码改善:一个“坑爹”的文字类冒险游戏
第2节:在禁止多重继承的情况下,如何设计“直立智慧猩猩”类?
第3节:C++多线程代码中的“乱序”执行现象
第4节:C++中函数指针有什么作用呢?
第5节:为什么我用c++写的游戏那么简陋?
第6节:多线程读写socket导致的数据混乱的原因是什么?
第7节:WebSocket 是什么原理?为什么可以实现持久连接?
第8节:怎样在c++中实现instanceof?
第9节:一个函数多处 return 是好风格吗?
第10节:C++中虚函数相比非虚函数的优势
第11节:为什么 C::C::C::C::foo() 能编译成功?
第12节:如何静态反射C++枚举的名字
第13节:看C++大叔如何拥 java 妹子入怀……
第14节:坨——理解递归实现“汉诺塔”代码的关键
第15节:C++编译器如何实现 const(常量)?
第16节:C++如何为断言加上消息
第17节:初学C++到什么水平,算是合格的初级开发工程师?
第18节:C++编程要避免使用单例模式吗?
第19节:学习C++要学boost库吗?
第20节:C++的继承就是复制吗?
第21节:C++构造函数失败,如何中止创建对象?
第22节:C++学完多线程后,学什么呢?
第23节:string_view 适合用做函数的返回值类型吗?
第24节:为指针取别名,为何影响const属性?
第25节:std::enable_shared_from_this 的存在意义?
课文封面

有CSDN网友分享了一个C++写了非常坑玩家的控制台游戏的代码。是那种典型的从头直线写到尾的代码,这是很多编程初学者容易犯的问题,本文指出其中存在的问题,并给出新的代码,可在线玩。

1. 原贴

CSDN 用户 mcxd_llhn 发的帖子:

本人小白,C++代码,分享一下。下面是代码,没写完的,有大佬帮忙指出简化程序更好。

他的代码如下:

#include<iostream> #include<stdlib.h> using namespace std; int main() { int i,sy;//i=输入,sy=死因 cout<<"如果需要操作帮助,可以在第一个输入界面输入114514"; _sleep(5*1000);//时间,要乘以1000为秒 system("cls"); cout<<"你掉入了洞穴!"<<endl; cout<<"你的面前有三条路"<<endl; cout<<"1.前面"<<endl; cout<<"2.左边(该线路未更新,但是有预告)"<<endl; cout<<"3.右边"<<endl; cin>>i; if(i==1)//往前 { cout<<"你遇见了一个宝箱,要打开吗?"<<endl; cout<<"1.那必须啊"<<endl; cout<<"2.还是苟点好"<<endl; cin>>i; if(i==1)sy=3; else if(i==2) { cout<<"你遇见一只小狗,他看见你的腰上没有信物,咬了上来"<<endl; sy=4; } else sy=1; } else if(i==2)//往左 { cout<<"你遇见了一扇门,要打开吗?(该线路尚未更新,请换一条线路)"<<endl; } else if(i==3)//往右 { cout<<"轰!!!"<<endl; _sleep(3*1000); sy=2; system("cls"); } else if(i==114514)//帮助 { cout<<"本游戏是一款文字类冒险游戏,通过数字选择进行选项冒险"<<endl; _sleep(3*1000); } else sy=1; //死亡 if(sy==1) { cout<<"你没有做出正确th选项中的选择,视为放弃"; _sleep(3*1000);biov } else if(sy==2) { cout<<"你死了!死因:被通路的碎石头掩埋,缺水而死"; _sleep(3*1000); } else if(sy==3) { cout<<"你死了!死因:宝箱中弹出的刀片(你不会真的以为有东西吧?不开箱子的玩家)"; _sleep(3*1000); } else if(sy==4) { cout<<"你死了!死因:小狗撕咬" ; _sleep(3*1000); } return 0; }

2. 问题分析

这个代码确实比较乱,小问题很多,我给列的就有:

  1. 引入C库头文件不规范:C++代码中包含C库头文件,用法不对
  2. 不跨平台,用了windows特定的 cls 命令行,以及 _sleep()函数
  3. 代码没分层,像面条一样的窜;
  4. 业务逻辑似乎不完整,用户要是那条未实现的路线的后续怎么处理?
  5. 代码组织不合理:死因通过整数传递,这样写需要额外对应,正常做法要么定义表,是直接给出死因
  6. 错误处理不强壮,你试试在需要输入数字的地方,给它个字母,程序可能要狂跳

2.1 头文件不规范

这一行:

#include <stdlib.h>

在C++代码中包含C语言标准库的头文件,应该使用“#include cxx”,比如这里用到的 <stdlib.h>,应改为 ,如:

#include<cstdlib>

后者能将所引入的C的头文件的内容,都变成在C++标准库名字空间 std 之下,有效降低C库中的符号和来自其地方的符号发生重名的概率。

2.2 使用了依赖于特定平台的功能

主要是 “cls” 这个控制台命令 和 “_sleep” 这个函数。

代码中为了清空控制台屏幕,方法是通过 system 这个C 函数(它就来自前面提到的 cstdlib ),以直接执行控制台命令;这是个好办法,不过,Windows下的控制台,和 Linux 下的 shell ,各自的命令相差很大。比如这个清屏用的“cls”,在Linux下,就得用“clear”。

_sleep 则一个 C 函数,用于让当前程序(的当前线程)睡眠一段时间,但它在不同平台下相差很多。其实,C++ 11 已经引入可以通用各平台的标准方法来实现这一功能,即: this_thread::sleep_for()。

2.3 代码没分层

这是最大问题,一条线实现下来去,不仅重复逻辑或相似代码多,而且阅读起来比较困难。解决办法通常是对逻辑分组,同一组的逻辑归入一个函数。比如例中用等首先要选择 直行、左转还是右转。则可以将三者的行为,各自写成一个函数。

2.4 有遗漏逻辑

一大串代码实现下来的坏处之一,就是很容易让作者自己都漏了某个分支功能的处理。注意,这里并不是指实现。代码原作者特意指明用户如果选择“左转”,那么这条分支上的功能是没有实现的——这当然可以,一个程序可以明确标明某块功能尚未实现,但“具体功能没有实现”,和这个分支未处理是两回事:比如,程序显示“抱歉,你选择的功能还没实现”后,是不是应该允许用户换一个选择?

另外,整个程序也没有实现重复玩的功能,这个或许不是因为遗漏,而是“设计就如此”,但这样的设计,会造成用户想换一个选择,就只能重新运行程序,整个游戏的友好性大受影响。

2.5 代码组织不合理

注意看原代码中的 sy (死因) 这个变量,除了用拼音缩写命名非常不推荐以外,你会发现,代码作者和阅读者都必须去记忆:死因为1时,是什么原因?为2时又是什么原因?可以有以下几种解决方法:

  • 不使用立即数,改为使用枚举 (enum)
  • 事先建立一张映射表(通常用数组或map),将用于表达死因的数字和死因的描述“绑”起来

但在本例中,该问题最好的解决方法,是避免问题发生(而不是问题发生后再想方法)。观察这个sy变量的前后使用,不难发现:前面在不同死因的发生处,辛苦设置了sy的不同值,到后面也应用用它做个区分,然后输出不同死因的说明而已;这种情况下,不如在玩家失败时,直接输出死因说明,无需借助sy这个变量“长距离”传递。

“长距离”传输信息的坏处:我在改写这段代码时,为了保证我不看错,也为了确保原代码逻辑无误,我就不得不在距离很远的两处代码,使劲比较这个sy的值。

2.6 容错性差

程序全程让用户输入数字,大家可以试试,但凡用户有意或无意输入一个字母,整个程序就会疯狂的一直重复输出……

3. 新代码

下面新代码解决了上面所有问题,从功能上看,用户可以反复玩,容错也处理了,并且也支持跨平台了(试了 Windows、Linux)。从代码组织上看,通过函数来实现具体功能分组。主流程因此非常短小易读。为增加趣味性,部分输出或交互也做了改进。

除解决问题之外,在新代码中,你还可以找到:

  • 用到了C++的异常机制,大家可以用它来帮助理解“异常”和“错误”的区别;
  • 用到了“带class的枚举”;
  • 用到了 using 定义类型别名;

代码后面给了一个可以 在线运行 、试玩这个程序的链接。

#include <cstdlib> #include <iostream> #include <vector> #include <thread> void ClearScreen() { char const* cmd = #if defined(_WIN32) "cls"; #elif defined(__linux__) "clear"; #endif std::system(cmd); } int const HELP_OPTION = 99; void HintWithSleep(char const* hint, int64_t seconds_would_wait = 0) // 显示提示 { std::cout << hint << std::endl; if (seconds_would_wait > 0) { std::this_thread::sleep_for(std::chrono::seconds(seconds_would_wait)); } } using Options = std::vector<char const*>; int Select(char const* question, Options options) // 选择 { do { std::cout << "\n" << question << "\n"; if (options.size() >= HELP_OPTION) { throw "扯淡吗?让用户做这么多选择?"; } for (int i=0; i<options.size(); ++i) { std::cout << i+1 << ":\t" << options[i] << "\n"; } std::cout << HELP_OPTION << ":\t 帮助\n"; std::cout << "请选择(1-" << options.size() << ",Ctrl+C 强行退出):"; int i; std::cin >> i; if (std::cin.fail()) { std::cin.clear(); // 清除错误的流状态 std::cin.ignore(); throw "错误的输入(非法选择项)"; } if (i == HELP_OPTION) { std::cout << "这一是款闻名全球的文字游戏,你只需输入选项开头的数字以\n" "做出选择即可参与一场惊心动魄的大战!" << std::endl; HintWithSleep("看明白了吗?即将开始重选", 4); ClearScreen(); continue; } if (i < 1 || i > options.size()) { std::cout << "你会不会玩啊!你得输入符合范围的选择!\n(碰上你们这种玩家真是烦死了!)" << std::endl; } return i; } while (false); return -1; } void GameOver(char const* additions = nullptr) // game over! { std::cout << ">>>>>> 抱歉,你挂了!!!玩家村全村人民高兴的准备吃席 <<<<<<<" << std::endl; if (additions && *additions) { std::cout << additions << std::endl; } } enum class Result {YouLose /*你死了*/, YouWin /*你赢了*/, WrongRoad /*错误路径*/}; void ByeBye(Result r) // 退出之前... { if ( r == Result::YouLose) { std::cout << "\n下次要努力哦!再见!\n" << std::endl; } else if (r == Result::YouWin) { std::cout << "\n这不可能!算你狗屎运好!再见!\n" << std::endl; } } Result OnBox() // 遇见盒子 { int sel = Select("哇!遇见了一个盒子,要不要打开?", Options { "那必须啊!", "还是苟点好……" }); switch(sel) { case 1 : // 打开 HintWithSleep("盒子里弹一把小刀,刺向你的下体……", 3); GameOver(); break; case 2 : HintWithSleep("没有打开宝盒,你拿不到信物。一只观察你很久的小狗冲上来咬住你的下体……", 3); GameOver("(但那只可爱的小狗狗说它已经吃饱了。)"); break; } return Result::YouLose; // 这关反正要你死 } Result OnBomb() { HintWithSleep("轰!!!\n突然,从天而降的地雷……", 3); GameOver("(地雷说:我又没爆炸,他怎么挂了?答:被砸的)"); return Result::YouLose; } Result UnImplemented() // 未实现的 { HintWithSleep("此路还在开发中,欢迎各大开发商参加招投标!", 1); return Result::WrongRoad; } void Run() { int sel = Select("你掉入了洞穴!\n你的面前有三条路", Options{ "往前继续直走", "向左拐", "向右拐" }); enum Dir {goStraight=1, turnLeft=2, turnRight=3}; switch(sel) { case goStraight: ByeBye(OnBox()); //是的,直行必死 break; case turnRight: // 是的,往右更是死 ByeBye(OnBomb()); break; case turnLeft: // 往左没实现 UnImplemented(); // 唯一活路还没实现... break; } } int main() { for(;;) { try { Run(); auto c = Select("再来一把?", Options {"好!", "滚!"}); if (c == 2) { std::cout << "好吧!我滚了!" << std::endl; break; } ClearScreen(); } catch (char const* e) { std::cerr << "\n好像出现了异常:" << e << "……必须重来!" << std::endl; HintWithSleep("系统正在努力重启中...", 2); ClearScreen(); continue; } } }

在线运行(onlinegdb)