feat(WIKI): wiki 2025 BREAKING CHANGE: Older content is categorized into older folders
139
2023旧版内容/3.编程思维体系构建/3.0 编程入门之道.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# 3.0 编程入门之道
|
||||
|
||||
> 作者:[爱飞的鸟](https://github.com/aFlyBird0)
|
||||
|
||||
## 缘由
|
||||
|
||||
### 为什么会有这篇讲义
|
||||
|
||||
原先的第三章,即 [3.编程思维体系构建](3.编程思维体系构建)是从 3.1 开始的。但我简单翻阅一下之后“很不爽”,新人得有多牛逼才能看完这章的讲义?
|
||||
|
||||
所以我强行塞了这篇教程进来,正好计算机一般从 0 开始计数,所以就有了 "3.0"。
|
||||
|
||||
### 这篇讲义讲什么
|
||||
|
||||
- 首先,如上文所述,如何轻松地利用本章乃至整个讲义
|
||||
- 在第一点的基础上,引申出我自己归纳的**编程入门之“道”**
|
||||
|
||||
### 请随意喷这篇讲义
|
||||
|
||||
故意写这么大的题目,就是为了“噱头”,为了让读者有点开的欲望。
|
||||
|
||||
可能本文的所谓的“道”并不是普适的,甚至是错误的,请尽你所能地喷这篇讲义,让我们一起完善它。
|
||||
|
||||
## 如何利用好本章的讲义
|
||||
|
||||
<del>(首先纠正一个心态,当我们获得一大片非常丰富的新的知识的时候,不应该感到焦虑。不知道自己不知道什么,远比知道自己不知道什么来得好。同时,虽然你收获的信息量很大,但也并不意味着你要全部看完,不需要焦虑。)</del>
|
||||
|
||||
1. 这里的文章的最大的作用是帮你打开信息壁垒,告诉你编程的世界有哪些东西,可以去学什么。
|
||||
2. 把讲义当成字典来用。很多文章并不是完全对新人友好的,你现在不需要看懂,甚至可能不需要看。你只要大概记下有这么个概念和工具,当下次要用到的时候,再查阅、再仔细学习就好。
|
||||
|
||||
简单来说就是,**抱着平和的心态,随便看看**,知道这一章都讲了哪些东西,看完有个印象就好,然后常回家看看。
|
||||
|
||||
技术细节本身永远都不是最重要的,重要的是思想和方法,如何快速掌握一门技术。
|
||||
|
||||
## 编程入门之道
|
||||
|
||||
### 先“run”起来
|
||||
|
||||
这是我的第一个也是最重要的建议。
|
||||
|
||||
无论是学一门语言,还是学一个工具:**尽可能地先用最短的时间搞懂这个东西是做什么的,然后以最快的方式把它“run”起来。**
|
||||
|
||||
当你已经能跑起一个语言、一个工具的最简单的示例的时候,再去花时间慢慢了解背后的复杂的内容,再去拓展即可。先用起来,跑起来,带着问题去翻资料。
|
||||
|
||||
- 比如学写 C 语言,我建议大家直接跳过 [3.1 该使用哪个编辑器???](3.1%E8%AF%A5%E4%BD%BF%E7%94%A8%E5%93%AA%E4%B8%AA%E7%BC%96%E8%BE%91%E5%99%A8%EF%BC%9F%EF%BC%9F%EF%BC%9F.md)这章。直接打开在看的教程的第一章,把代码复制到这个[在线编译](https://rextester.com/l/c_online_compiler_gcc)的网站里,点一下 "Run it" 看效果。为什么要去详细了解编译器、编辑器、IDE、gcc、g++、make 这种东西?能最快地上手,最快地运行看到效果,对于初学者来说是最好的。等你把环境装好了,人家已经学完三章了。当你已经会了简单的循环、判断、函数,已经建立了信心,就可以慢慢地去看那些让人头疼的东西了。
|
||||
- 比如学 Linux,如果你电脑操作系统是 MacOS,直接打开“终端”,可以勉强当 Linux 使;如果你是 Windows,直接跟着微软的 [WSL 安装教程](https://docs.microsoft.com/zh-cn/windows/wsl/install),一步步无脑地用鼠标点击然后装起来。这时候你就拥有一个 Linux 了,再对着教程去敲。等闲下来了就可以了解了解内核是什么,发行版是什么,去了解云服务器,去了解不同的装 Linux 的方式。
|
||||
|
||||
为什么要这样?
|
||||
|
||||
- 第一,计算机是很重实操的东西,不要光看所谓的理论不动手。动手是王道。
|
||||
- 第二,要尽可能多地、尽可能快地给自己找一些正反馈,学编程不是当苦行僧
|
||||
- 第三,很多东西没必要学,学了也忘。用到了再去学,是最省事最容易记住最高效的。
|
||||
- 第四,程序员这一辈子会接触无数的新的东西,如何快速上手一个东西,是非常重要的能力。
|
||||
|
||||
### 任务/项目驱动
|
||||
|
||||
任务/项目驱动的意思承接上文,意思就是不要瞎看文章,不要让阅读教程和学习课本成为你的驱动力,等你看完一本 C 语言的书,一行代码就没敲,你就 g 了。
|
||||
|
||||
那么该怎么学呢?
|
||||
|
||||
**先简单地会一样东西的最核心的部分,再去找一个实际的编程场景、编程任务、项目。你会在完成这个项目中遇到各种各样的问题,无论是遗漏了知识点还是压根没思路,这时候不断地用搜索引擎来学习。( **[2.3 高效的信息检索](../2.%E9%AB%98%E6%95%88%E5%AD%A6%E4%B9%A0/2.3%E9%AB%98%E6%95%88%E7%9A%84%E4%BF%A1%E6%81%AF%E6%A3%80%E7%B4%A2.md)**)**
|
||||
|
||||
举个例子:你想做一个小程序,来检测某电影院的电影预售。程序大概要做到不断刷新网页,一检测到这个电影预售了,就马上发短信给自己手机(或者直接帮你抢)
|
||||
|
||||
1. 你通过搜索引擎或者从不知道哪个学长/学姐那里得知,这玩意叫爬虫(简单来说就是用程序抓取网页上的内容)。我们又通过搜索引擎得知,python 写爬虫最舒服。[3.6python(灵巧的胶水)](3.6Python%EF%BC%88%E7%81%B5%E5%B7%A7%E7%9A%84%E8%83%B6%E6%B0%B4%EF%BC%89.md)
|
||||
2. 我们又通过例如菜鸟教程这种最简单的极速入门教程,在 3 个小时内掌握了 python 的核心语法。
|
||||
3. 这时候我们开始写代码了,但是我该怎么获取到网页啊?这时候,你在浏览器里分别搜索了以下几个内容:“程序如何获取网页内容”。
|
||||
4. 你会学习到 http 相关的知识,大概知道了我们平时打开网页可以简单理解为一次 http GET。
|
||||
5. 可是,还是好抽象啊,python 又怎么才能获取到网页的内容啊?这时候,你在浏览器里精确了一下搜索内容,“python 如何获取网页内容”。
|
||||
6. 搜索引擎会告诉你,可以用诸如 `requests` 一类的库来请求网页。但问题又来了,这个库一下子会返回整个网页内容,我怎么才能筛选出我要的电影有没有放出来?
|
||||
7. 这时候你又去搜索,学会了怎么解析网页。比如用 `xpath`, `bs4`, 甚至直接用正则。
|
||||
8. 我们现在能不断地刷新网页,并且筛选出所有的电影的信息,然后分析出自己想看的那个电影有没有放出来了。但是问题又来了,怎么让程序给自己发信息啊?
|
||||
9. 这时候又打开了浏览器,我们可以知道诸如阿里云、腾讯云这样的云服务商提供了发短信的服务,并且会教你怎么写代码,主要注册、申请一下,就能发短信了。
|
||||
10. 但是你又会发现这个申请流程真 tm 麻烦,我还是发邮箱吧!这时候又开始搜,如何用 python 发邮件。
|
||||
11. 然后你会了解到 python 用于发邮件的一些库,以及简单的邮件协议的知识,如 `POP3`
|
||||
12. 程序真的能如期运行了,能不断刷新、获取网页内容、解析内容以判断电影是否上映,上映了还会发邮件给你。
|
||||
13. 又又又出问题了!!!你发现程序出错了,因为你请求频率过高,电影院的网站发现你是爬虫了,把你给 ban 了!这时候你知道了“反爬”这个概念,就是反爬虫,类似于外挂和反外挂。你开始学习反爬的知识。
|
||||
14. 你了解到可以给程序挂个“代理”,相当于每次请求网页的时候,都伪装成了世界上上某个角落的另外一台电脑的请求。
|
||||
15. 能走到这里太艰辛了,但你很快又发现了问题!现在程序是跑在自己的电脑上的,我们不知道电影院啥时候放票,所以程序得一直跑着,但是电脑会关机啊,关机了还怎么抢???
|
||||
16. 这时候你了解到了服务器的概念。服务器可以简单地了解为一台远程的几乎从不关机的电脑,一般用的是 Linux 操作系统。[3.Y 附加模块:Linux](3.Y%20%E9%99%84%E5%8A%A0%E6%A8%A1%E5%9D%97%EF%BC%9ALinux.md)
|
||||
17. 这时候你又开始去学 Linux,当然不是非常系统地学,而是像我之前说的,以最快的方式知道它是做什么的,然后运行起来。比如可以直接在阿里云、腾讯云、AWS 等云服务器商那里购买。甚至也可以用自己的旧手机装个 Linux 等等,这些都是后话了。
|
||||
18. 服务器是远程的,我们怎么连上去操纵它呢?我怎么把我的 python 代码传上去呢?这时候你又去求助万能的搜索引擎,或者神秘的学长学姐。你知道了世界上有 `ssh`、`ftp`、`sftp` 这些东西。
|
||||
|
||||
以上的“简单”的例子,就是所谓的任务驱动。带着目的去学,带着任务去学,带着问题去学,快速搞定。在上面的例子中,如果能成功走完,你会学到编程语言本身、学到网络知识、学到 Linux 服务器及其相关操作、学到云服务器和各种云服务(例如短信服务)。更重要的是,每一次从问题到答案的搜索与解决过程,你的编程内功就增强了一波。以后上手一个新东西,或者遇到问题再去解决它的速度,只会越来越快。
|
||||
|
||||
刚开始你可能什么都不会,什么地方都被阻塞,但当你把坑踩遍了。就发现,哎嘿,不好意思,这玩意我怎么又会!
|
||||
|
||||
**所以让我们基于这个“任务驱动”,再看看本章的内容。这些内容大多看了就忘,因为细节非常多,而且并不一定能解决你手头上的问题。但这些文档,带你领进了新的领域的大门,让你的工具箱里多了一个可以解决问题的工具,以后用到了可以想起他们。并且,这些文章多是通俗的,且作者多是讲述了 ta 所认为的该语言/工具的最核心、最精华的部分,或者说第一次入门最需要学习的部分。**
|
||||
|
||||
## 圈子
|
||||
|
||||
先干了这碗鸡汤,我之前写的,就单纯想让你看看。
|
||||
|
||||
> 每个大学的同一个专业,录取的时候分数是差不多的,但是毕业的时候工资可以差几十倍。他们的大环境是相同的,外界对他们的学历评价也是一样的。外部的大环境,我称之为大圈子,最典型的是学校。大圈子决定的是外部对你的整体性的刻板印象式的评价,以及决定了你能以多大的成本来获取资源(比如有没有公司来学校主要招生,有没有免费出国交流的机会,有没有图书馆)。但是,这个大圈子,在一般情况下,在你思考这个问题的时候就已经决定了,比如大家的大学是不能改变的。但更重要的是,小圈子,即你自身所营造的环境。比如每个人都有几个要好的朋友,他们的编程水平,其实就决定了你的编程水平,比如你是否有加入有技术大佬坐镇的社团,这样他们能带你学。如果线下都没有的话,你是否主动去加一些网上的社群、编程社区,问问公司对计算机专业学生的要求是什么,问问能进大厂的人,都在学些什么(知道要学什么很重要,瞎学悔大学)小圈子能时刻让你知道重要的讯息,比如计算机哪个细分方向的就业好,什么厂最近又有提前批,等等。圈子这东西太重要了,我以前很长一段时间就是一个所谓的一直在学习,很想提高自己,但因为比较自闭没有认识牛逼的人,所以我压根不知道学什么啊。那就只能随便弄,瞎搞,做无用功。
|
||||
|
||||
圈子,从另外一个角度,也可以理解为“信息差”,“信息壁垒”。对于编程这东西,知道有哪些东西能学,哪些东西赚钱多好找工作,哪些地方有项目,然后有着志同道合的人一起唠,太重要了。
|
||||
|
||||
我最近的两次编程能力的飞跃,一次是在加入了某个社团认识了很多技术大佬以及有了非常多的项目可以写,一次是意外地获得了一份实习所以能够和行业的领跑人物经常聊天,以及在开源社区慢慢接触到来自世界各地的技术大牛。
|
||||
|
||||
“破圈”的话,我目前接触到的最好的方式是参与开源社区。参与开源的好处如下:
|
||||
|
||||
1. 输入决定输出。开源的代码多是经过检验的牛逼的代码,通过多看看优雅的代码来提高编程能力,比自己无中生有简单地多。
|
||||
2. 开源圈牛人多。无论是拓宽视野,还是在 issue 下的交流,还是别人给你的 review 建议,都能学到很多。你会在开源的过程中认识很多的人,很多大厂的人,说不定就是你以后的面试官。
|
||||
3. 参与开源社区能极大地锻炼自己的编程能力,能给简历贴金。
|
||||
4. 开源是程序员的浪漫。
|
||||
|
||||
对于学生而言,可以参加一些仅面向学生开放的开源活动。一般会有一个主办方,然后有许多知名的开源社区报名。他们会罗列出一些有一定难度的任务,学生可以提交申请书,陈述如何完成这个任务。中选后会分配单独的导师来带你,还会发奖金给你,一般是大几千起步。推荐阅读这个系列的文章:[https://erdengk.github.io/gsoc-analyse/](https://erdengk.github.io/gsoc-analyse/)
|
||||
|
||||
## 不同的知识媒介
|
||||
|
||||
这里主要讲讲我对不同媒介来学习编程知识的看法,媒介主要指的是
|
||||
|
||||
- 图文 vs 音视频
|
||||
|
||||
我身边的牛逼的人,一般都更喜欢通过看图文来学习,而不是视频。主要有以下几个原因:
|
||||
|
||||
1. 图文的信息密度最大。同样的时间内,看一篇图文获得的信息量比视频大很多。
|
||||
2. 图文易定位、检索。你可以通过搜索功能非常迅速地在一个工具的官网内找到想要的内容。
|
||||
3. 一些杂七杂八的原因:图文往往更新,因为视频制作耗时长,不容易更新;牛逼的人它不一定会做视频,或者说牛逼的人中有写博客习惯的远大于做视频习惯的。
|
||||
|
||||
当然,视频也有很大的好处,就是直观、简单。适合学习初期。
|
||||
|
||||
当然也不必强行把自己往上套,结合自身特质,以什么的方式学习效率最高,就采用什么样的方式。不过我建议你多锻炼阅读官方文档、文档教程的能力。
|
||||
|
||||
## 选择大于努力
|
||||
|
||||
[正确解读 GPA](../1.%E6%9D%AD%E7%94%B5%E7%94%9F%E5%AD%98%E6%8C%87%E5%8D%97/1.6%E6%AD%A3%E7%A1%AE%E8%A7%A3%E8%AF%BBGPA.md) 这篇文档写得很好,和我的想法完全一致,但是被放得太后面了,我想把它提上来。
|
||||
|
||||
大学不是唯分数论的,起码编程不是这样。我的建议是,如果以后大概率考研,可以多抓一下绩点;如果以后大概率工作,就不必追求高绩点了(指把大部分时间都花在提高绩点上)。
|
||||
|
||||
至于为什么是“选择大于努力”这么笼统的标题,因为我想表达更多的意思(虽然我一时想不到那么多)。
|
||||
|
||||
选择真的比你想象的重要。
|
||||
|
||||
- 例如,你是选择把大部分的时间花在把成绩从 85 提高到 90 上,还是把大部分时间用来做实际的项目直接上手以后大厂需要的框架上?
|
||||
- 例如,你选择什么技术栈,哪个发展方向(这个目前来说大家不需要考虑,只是简单提一下。大家选计算机这个行为本身相对来说就已经很对了,比土木工程当牛马好太多)?
|
||||
- 例如,你是选择以老师上课教的内容为主,埋头搞 C 语言,还是多去扩展自己的视野去了解现行的流行的框架是什么,大厂需要什么样的能力,然后去做项目?
|
||||
|
||||
以及,一旦作出了选择,就不要像祥林嫂那样自怨自艾。过去的事情无可改变,我们只能基于当前的状态和资源,去把接下来的事情做得更好。
|
||||
89
2023旧版内容/3.编程思维体系构建/3.1该使用哪个编辑器???.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# 3.1 该使用哪个编辑器???
|
||||
|
||||
## 编辑器,编译器,集成开发环境
|
||||
|
||||
我们平时所说的程序,是指双击后就可以直接运行的程序,这样的程序被称为可执行程序(Executable Program)。
|
||||
|
||||
在 Windows 下,可执行程序的后缀主要有 .exe
|
||||
|
||||
在类 UNIX 系统(STFW)下,可执行程序没有特定的后缀,系统根据文件的头部信息来判断是否是可执行程序
|
||||
|
||||
可执行程序的内部是一系列计算机指令和数据的集合,它们都是二进制形式的,CPU 可以直接识别,毫无障碍;但是对于程序员,它们非常晦涩,难以记忆和使用。
|
||||
|
||||
(你也不想用一沓纸带写程序吧)
|
||||
|
||||
### 什么是编辑器
|
||||
|
||||
编辑器的概念很简单,百度百科上这么写道:
|
||||
|
||||
> 编辑器是软件程序,一般是指用来修改电脑档案的编写软件,但也有人称 PE2、HE4(汉书)……等文书软件为编辑器。常见的编辑器有文本编辑器、网页编辑器、源程序编辑器、图像编辑器,声音编辑器和视频编辑器等。
|
||||
|
||||
当然在这里我们主要讲的是代码编辑器,一个好的编辑器可以节省开发时间,提高工作效率,它们都能提供非常方便易用的开发环境。你可以用它们来编写代码,查看源文件和文档等,简化你的工作。以下是一些常用的代码编辑器,每个不同的编辑器都有不尽相同的目标用户群体。
|
||||
|
||||
- *Visual Studio Code* : 微软 VS 系列的新作品,适用于多平台的代码编辑器,其很好服从了轻量化 + 拓展的 Unix 思想,在整体快捷方便的同时具有极强的功能拓展空间,是值得首要推荐的编辑器。
|
||||
- *Vim*: Vim 是从 vi 发展出来的一个文本编辑器,在程序员中被广泛使用,运行在 Linux 环境下。
|
||||
- *GNU Emacs* : Emacs 是一个轻便、可扩展、免费的编辑器,它比其它的编辑器要更强大,是一个整合环境,或可称它为集成开发环境。它可以处理文字,图像,高亮语法,将代码更直观地展现给开发者。
|
||||
|
||||
### 什么是编译器
|
||||
|
||||
C 语言代码由固定的词汇按照固定的格式组织起来,简单直观,程序员容易识别和理解,但是对于 CPU,C 语言代码就是天书,根本不认识,CPU 只认识几百个二进制形式的指令。这就需要一个工具,将 C 语言代码转换成 CPU 能够识别的二进制指令,也就是将代码加工成 .exe 程序;这个工具是一个特殊的软件,叫做编译器(Compiler)。
|
||||
编译器能够识别代码中的词汇、句子以及各种特定的格式,并将他们转换成计算机能够识别的二进制形式,这个过程称为编译(Compile)。
|
||||
|
||||
·机器语言、汇编语言和高级语言区别
|
||||
|
||||
1. 机器语言
|
||||
计算机执行的二进制命令,都是 0 和 1 表示的。
|
||||
2. 汇编语言
|
||||
具有一定意义的文字命令,与机器语言一一对应。汇编语言可以通过汇编得到机器语言,机器语言可以通过反汇编得到汇编语言。汇编过程还包括变量内存管理,即经过汇编之后所有的变量和函数都变成了地址,而常量也变成了对应的值。
|
||||
但是汇编语言还是不够直观,一个简单的动作需要大量的语句来描述,因此又有了高级语言。
|
||||
3. 高级语言
|
||||
更简单,符合人们的习惯,也更容易理解和修改。高级语言经过编译器编译之后可以得到目标程序。
|
||||
编译器的作用就是把高级语言的源代码转换成对应平台的目标代码。高级语言书写比较简单,但是翻译起来比较复杂,同样的高级语言语句可以有不同的机器语言实现方法。
|
||||
|
||||
而<u>编译器所做的就是进行这三种语言的互相转换。大多数情况下,编译是从更高级的语言(高级语言、汇编语言)编译成低级语言(汇编语言、机器语言)。</u>
|
||||
|
||||
另一种情况是,从他人的可执行程序(低级语言)编译成高级语言,以推导出他人的软件产品所使用的思路、原理、结构、算法、处理过程、运行方法等设计要素,某些特定情况下可能推导出源代码。这个过程叫做反向编译。
|
||||
|
||||
编译器:将你所编辑的源代码编译成机器所能理解的语言,比如 VC++ 把你的.cpp 文件编译成.obj 文件(经过编译器编译这时的代码计算机已经可以识别),而最后的.exe 则是通过连接生成的(这里的工作是由连接器完成的,跟编译器无关)。
|
||||
|
||||
语言的编译器有很多种,不同的平台下有不同的编译器,例如:
|
||||
|
||||
- Windows 下常用的是微软开发的 cl.exe,它被集成在 Visual Studio 或 Visual C++ 中,一般不单独使用;
|
||||
- Linux 下常用的是 GUN 组织开发的 GCC,很多 Linux 发行版都自带 GCC;
|
||||
- Mac 下常用的是 LLVM/Clang,它被集成在 Xcode 中(Xcode 以前集成的是 GCC,后来由于 GCC 的不配合才改为 LLVM/Clang,LLVM/Clang 的性能比 GCC 更加强大)。
|
||||
|
||||
你的代码语法正确与否,编译器说了才算,我们学习 C 语言,从某种意义上说就是学习如何使用编译器,让编译器生成可执行程序(例如 Windows 下的 .exe 程序)。
|
||||
|
||||
编译器可以 100% 保证你的代码从语法上讲是正确的,因为哪怕有一点小小的错误,编译也不能通过,编译器会告诉你哪里错了,便于你的更改。
|
||||
|
||||
### 什么是集成开发环境
|
||||
|
||||
实际开发中,除了编译器是必须的工具,我们往往还需要很多其他辅助软件,例如:
|
||||
|
||||
- 编辑器:用来编写代码,并且给代码着色,以方便阅读;
|
||||
- 代码提示器:输入部分代码,即可提示全部代码,加速代码的编写过程;
|
||||
- 调试器:观察程序的每一个运行步骤,发现程序的逻辑错误;
|
||||
- 项目管理工具:对程序涉及到的所有资源进行管理,包括源文件、图片、视频、第三方库等;
|
||||
- 漂亮的界面:各种按钮、面板、菜单、窗口等控件整齐排布,操作更方便。
|
||||
|
||||
这些工具通常被打包在一起,统一发布和安装,例如 Visual Studio、Dev C++、Xcode、Visual C++ 6.0、C-Free、Code::Blocks、JetBrains Clion 等,它们统称为集成开发环境(IDE,Integrated Development Environment)。
|
||||
|
||||
集成开发环境就是一系列开发工具的组合套装。这就好比台式机,一个台式机的核心部件是主机,有了主机就能独立工作了,但是我们在购买台式机时,往往还要附带上显示器、键盘、鼠标、U 盘、摄像头等外围设备,因为只有主机太不方便了,必须有外设才能玩的爽。
|
||||
|
||||
集成开发环境也是这个道理,只有编译器不方便,所以还要增加其他的辅助工具。
|
||||
|
||||
## 我的推荐
|
||||
|
||||
作为个人使用比较顺手的几款 IDE
|
||||
|
||||
Java: [JetBrains IntelliJ IDEA](https://www.jetbrains.com/zh-cn/idea/)
|
||||
|
||||
C: [Visual Studio(宇宙第一 IDE)](https://visualstudio.microsoft.com/zh-hans/vs/), [JetBrains Clion](https://www.jetbrains.com/zh-cn/clion/),Visual Studio Code(编辑器 IDE 化需要额外配置)
|
||||
|
||||
Python: [JetBrains Pycharm](https://www.jetbrains.com/zh-cn/pycharm/)
|
||||
|
||||
Vim 在附加篇章里有额外介绍
|
||||
|
||||
[JetBrains 白嫖指南](https://www.cnblogs.com/Coline1/p/15229244.html)
|
||||
|
||||
当然,适合你的才是最好的
|
||||
164
2023旧版内容/3.编程思维体系构建/3.2.1手把手教你学算法——如何使用OJ(Online Judge).md
Normal file
@@ -0,0 +1,164 @@
|
||||
# 3.2.2 手把手教你学算法——如何使用 OJ(Online Judge)
|
||||
|
||||
在之前的篇章中,我们向新手 acmer 推荐了两个编程网站——Luogu 与 Codeforces,下面由笔者向各位介绍一下网站的详细用法。
|
||||
|
||||
## Luogu
|
||||
|
||||
进入 [https://www.luogu.com.cn/](https://www.luogu.com.cn/)
|
||||
|
||||

|
||||
|
||||
### 社交模块
|
||||
|
||||
做为一个刷题网站,Luogu 提供了符合中文用户习惯的社交模块。体现于左侧边栏的讨论及主页的最近讨论,以及底部的“发射犇犇”系统。但是我并不建议 Acmer 使用该功能,因为 Luogu 主要面向初高中生甚至小学生等参加 NOIP 系列竞赛的用户,讨论不可避免存在一些低龄化现象。对于社交模块的使用,我推荐当且仅当一种做不出题的求助手段,这点放在之后题目模块讲解。
|
||||
|
||||
### 题目模块
|
||||
|
||||
点开题库,我们看见以下界面
|
||||
|
||||

|
||||
|
||||
在上方我们可以筛选我们想要的题目,接下来我们点开 P1000 为例
|
||||
|
||||

|
||||
|
||||
右侧三个模块为折叠状态,下面介绍他们的作用
|
||||
|
||||
① 标签:假如你已经对算法有了基本的了解,面对一道题毫无思路,那么你可以试试看正解所使用的算法,寻找思路的突破口
|
||||
|
||||
② 讨论:假如你的代码因未知原因一直出错,你可以试试讨论(越难的题有效信息越多)看看自己是否犯下别人犯过的错误或者可以发帖求助(关于如何正确发帖求助,请参考《提问的艺术》)
|
||||
|
||||
③ 推荐题目:没做爽?再来一道类似的(请勿沉迷刷水题,过题量除了炫耀毫无意义)
|
||||
|
||||
右上方点击查看题解,查看其他用户撰写的参考答案和讲解。
|
||||
|
||||
点击提交答案
|
||||
|
||||

|
||||
|
||||
左侧可以选择语言类型,C++ 用户建议选择 C++14。
|
||||
|
||||
O2 优化是一种优化(废话)假如您的代码复杂度正确但 TLE,可以尝试该选项。
|
||||
|
||||
### 记录模块
|
||||
|
||||
怎么知道自己代码的问题出在哪里呢?记录模块是帮助你的好工具。
|
||||
|
||||

|
||||
|
||||
AC:通过该数据点
|
||||
|
||||
WA:答案错误 常见原因:没开 Long Long 导致数据溢出、少取模、格式错误、忘记删除调试代码
|
||||
|
||||
RE:运行错误 常见原因:数组访问越界、访问空指针、除零模零、主函数返回非 0,评测机炸了(极小概率)
|
||||
|
||||
UKE:未知错误 常见于 Remote Judge,建议重交或者去原网站交
|
||||
|
||||
TLE:运行超时 请检查算法复杂度与是否存在死循环,也可尝试使用 O2 优化。搜索“卡常数”了解更多缩短运行时间小寄巧
|
||||
|
||||
MLE:空间超限 请检查是否递归爆栈、数组过大
|
||||
|
||||
OLE:输出超限 放心你见不到的
|
||||
|
||||
### 题单模块
|
||||
|
||||
点开侧栏题单
|
||||
|
||||

|
||||
|
||||
建议新手从官方精选题单开始,由浅入深,由简到难。等到对算法形成概念,针对漏洞补习时可以尝试用户分享题单(到那个阶段已经有很多手段去找题了,刘教练的题单就够你做了)
|
||||
|
||||
### 比赛模块
|
||||
|
||||
点开侧栏就能看见准备举办和已结束的比赛。笔者不建议大家在 Luogu 打比赛,首先赛制不一样,其次出题风格不一样,最后对于初学者 Luogu 比赛的难度曲线过大。
|
||||
|
||||
## Codeforces
|
||||
|
||||
进入 [https://codeforces.com/?locale=en](https://codeforces.com/?locale=en)
|
||||
|
||||

|
||||
|
||||
比起 Luogu,这样的 UI 设计离 CN 互联网已经很远了(然而比起更硬核的一些做题网站,CF 的 UI 真是越看越顺眼)
|
||||
|
||||
右上角注册登录切语言(哇塞,可以选俄语,你说的对,但是 CF 是一款由俄罗斯开发的多人在线竞技游戏)
|
||||
|
||||
### HOME 模块
|
||||
|
||||
主页显示各种数据,主要为近期比赛的一些公告。
|
||||
|
||||
### TOP 模块
|
||||
|
||||
热帖,如果擅长英语的话,CF 的交流氛围还是不错的,做为一个答疑解惑的论坛肯定比国内强。
|
||||
|
||||
### CATALOG 模块
|
||||
|
||||
文档目录,你可以在这学习算法竞赛入门,体系化学习算法,只要你会英语
|
||||
|
||||
### CONTESTS
|
||||
|
||||
重中之重!CF 的比赛系统可以说是我们选择这个网站的最大原因!
|
||||
|
||||
进入比赛页面
|
||||
|
||||

|
||||
|
||||
上方为将举办比赛,显示开始时间(UTC+8 也就是我们时区的时间)和持续时间大多都开始的比较晚,例如笔者就没有这么晚学习的习惯,所以一般赛后写题。比赛分为以下几种类型(例如写在括号里的 Div.2)
|
||||
|
||||
Div.1、Div.2、Div.3、Div.4 数字越小难度越大。
|
||||
|
||||
建议新手从 Div.2 及以下的难度打起,在比赛时间内写的题目很少也不要气馁,CF 出题审题质量稳定,写到就是赚到,赛后补题就行
|
||||
|
||||
对于已经结束的比赛,我们可以直接点击“Enter”进入比赛看题补题,也可以点击“Virtual partipation”简称“VP”,重现赛时场景,例如显示赛时排行榜,即时过题人数等,在比赛完成后你也可以看见如果你以该状态参赛,你会获得怎样的排名。
|
||||
|
||||
下面以一场 Div.2 比赛为例,展示我们该如何打一场 CF。
|
||||
|
||||
### VP
|
||||
|
||||

|
||||
|
||||
这是一场笔者之前赛后补过的 Div.2,画面右下角分别为赛后公告和题解,右侧便是开启 VP 的按钮。
|
||||

|
||||
|
||||
*VP 模拟赛时的好处就是在虚拟参赛中获得真实比赛才能积累的经验,比如这里笔者发现通过前三题后,我应该先去看看 F 题,因为做出来的人更多,我有更大的可能性做出来,ACM 中题目并不是 100% 按难度排序。*
|
||||
|
||||

|
||||
|
||||
进入 VP 后,我们可以发现比起正常赛后补题有了明显不同。
|
||||
|
||||
首先我们可以看见赛时某道题的通过人数,随比赛时间流逝 100% 仿真变化。而且也可以与当时的“虚拟选手”同步竞争,例如笔者这里就复制之前写过的代码荣登榜三(乐)
|
||||
|
||||
对于大多数比赛,采用 ICPC 赛制,解决某题得到的分数由该题当前的分数减去 (不成功的提交次数)*50,这里某道题的分数是由比赛开始时的分数随时间线性减少得到的。
|
||||
|
||||
也就是做题越快,错误次数越少,分数和排名就越高,这点大体是与 ACM 赛制相同的。
|
||||
|
||||
当然,CF 还有极具特色的 Hack 玩法,这些深入内容留给有上分兴趣的读者研究。
|
||||
|
||||
让我们点开 A 题,来看看如何提交答案
|
||||
|
||||

|
||||
|
||||
可以看见,右侧有一个 submit,与 luogu 不同的是,你需要上传源代码文件(如 cpp)然后选择 G++17 为语言,提交。
|
||||
|
||||
当然,你也可以点开上侧的 submit code
|
||||
|
||||

|
||||
|
||||
选择题目、语言,填写代码后提交,就和 Luogu 的方式一样了。
|
||||
|
||||
同样,在上侧 MY SUBMISSIONS 处可以查看已提交的代码和状态
|
||||
|
||||

|
||||
|
||||
### PROBLEMSET
|
||||
|
||||
同样,CF 也有题库
|
||||
|
||||

|
||||
|
||||
如果你只想做某道题而不是某场比赛,这里也许更适合你。
|
||||
|
||||
不过 CF 的题库比较鸡肋,标签筛选也不是很方便(大概是把想要的标签在右上角分隔好)
|
||||
|
||||
## 总结
|
||||
|
||||
笔者向读者详细介绍了两个 OJ,至于如何让 OJ 更好的辅助你的 ACM 学习,我应该在什么时间节点或训练阶段,出于什么训练目的选择哪个网站,笔者留到下一个篇章继续介绍。
|
||||
79
2023旧版内容/3.编程思维体系构建/3.2.2ACM 竞赛从入门到入坟.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# 3.2.3 ACM 竞赛从入门到入坟
|
||||
|
||||
> 作者:[选择公理](https://github.com/axiomofchoice-hjt)
|
||||
>
|
||||
> 利益相关:2021 杭电六队,2022 杭电一队成员
|
||||
|
||||
相信大家从前面的文章中已经了解 ACM 的基本情况,这里就不赘述了。
|
||||
|
||||
首先需要强调的是,选择 ACM 这条路非常辛苦,每天要花大量时间刷题;同时,ACM 也是残酷的,唯有实力才能保证你不被集训队淘汰。
|
||||
|
||||
这里就不得不提一下参加 ACM 竞赛的一般流程了:
|
||||
|
||||
1. 大一上:参加集训队选拔,进入集训队(错过选拔的要用 [Codeforces](https://codeforces.com/) 分数来向老刘申请)。
|
||||
2. 大一下:主要是个人训练,经历多轮淘汰,期末会组队(三人一队)。
|
||||
3. 大一暑假:以组队形式参加多校等训练,这些训练将决定你在大二这个赛季能参加几次比赛。
|
||||
4. 大二:训练或比赛。没比赛资格没关系,你可以沉淀,大三还有机会。
|
||||
5. 大三同上,但是也有选择退出的。
|
||||
|
||||
## 第一阶段
|
||||
|
||||
打 ACM 要趁早。从上面流程图可以看出来,大一入学就应该决定好了,然后是学习,再然后参加选拔(冷知识:集训队不是想进就能进的)。
|
||||
|
||||
### 群
|
||||
|
||||
首先是加群。每一届老刘都会弄个杭电 ACM 新生群,没有门槛的。想要加这个群,可以问一问杭电的其他群 / 学长学姐。
|
||||
|
||||
群里主要关注两件事,一个是 ACM 公选课,一个是选拔。然后有问题也可以丢群里。
|
||||
|
||||
### 公选课
|
||||
|
||||
老刘每学期都有公选课,大一上的时候可以去旁听。公选课的内容和选拔是相关的,课讲了哪些,选拔就考哪些。所以,跟上课程进度是很有必要的。
|
||||
|
||||
### 要学什么
|
||||
|
||||
编程语言(C/C++)是算法的前提,而公选课是不会教你语言的,得自学。零基础的同学要注意了,千万不要跟着 C 语言的课来学,太慢了。尽量在一个礼拜内学完 C 的基础内容(指针可以跳过),然后跟上公选课。C++ 可以晚点再学。
|
||||
|
||||
说明一下,语言其实包含很多语法,但是 ACM 用得到的只是其中的一个子集。像 C 的指针,C++ 的模板,都是很难但是鸡肋的知识点。如果你要做工程,那这些语法都得学。
|
||||
|
||||
学会语言后,可以找一个算法书(算法竞赛入门经典(刘汝佳),算法赛进阶指南(李煜东)等等)系统学习各种算法。
|
||||
|
||||
提醒一下,这里讲到的所有东西都要上机实践,ACM 非常注重实践。你在开始学语言的时候就要多上机。
|
||||
|
||||
### 选拔
|
||||
|
||||
在大一上 10 月开始,每个月至少 1 场选拔赛(上机编程的模式),每场比赛都会根据参选人员的实际表现确定若干数量的同学入围集训队。
|
||||
|
||||
除了选拔,还有一种进队方法那就是 [Codeforces](https://codeforces.com/)。Codeforces 每个账号都会有一个分数(rating),打 Codeforces 比赛打得好就上分,反之就掉分。只要大一上你的 rating 连续三次达到 1400(具体以老刘为准),就可以向老刘申请入队。事实上,这种方法甚至到了大二都是可以的(大二你的 rating 可能要 1900),不过几乎没有人这么做,所以还是要趁早。
|
||||
|
||||
大一上会选拔 50-60 人,大一下会保留 20 人左右。(仅供参考)
|
||||
|
||||
## 第二阶段
|
||||
|
||||
恭喜你,你已经学完了基础算法,可以进入以刷题为主的学习模式了。
|
||||
|
||||
刷题平台(OJ)有很多,如果没有特别适合你的,那就去刷 [Codeforces](https://codeforces.com/)。因为这个平台比赛多,老刘认可。建议 Codeforces 的每场比赛都参加(如果时间不好可以第二天补上)。
|
||||
|
||||
说到时间,Codeforces 比赛最常见的时间是 22:35 到 00:35(Codeforces 是俄罗斯的,有时差,所以时间有点阴间)。ACMer 其实很多都是熬夜党。
|
||||
|
||||
打完比赛,建议钻研一下自己没做出的前一两题,写个题解。为什么要写题解呢,一个是方便以后来回顾,一个是加深印象,一个是把自己的思维用文字表达出来,这样能发现思维的漏洞(比如证明不严谨之类的)。题解写出来发不发博客就看个人喜好吧。作者以前也是坚持写博客写了很久。
|
||||
|
||||

|
||||
|
||||
为什么要打 Codeforces 比赛呢?主要原因是打比赛有计时,有压力(怕掉分的压力),能让人提升更快。不要因为怕掉分就不参加了,你要相信只要你一直打比赛,你的 rating 曲线一定是波动上升的。
|
||||
|
||||
另外建议大家整理一个自己的模板(算法套路),这里推荐几个资料供参考:[OI Wiki](https://oi-wiki.org),[Kuangbin 模板](https://kuangbin.github.io/2018/08/01/ACM-template/)。想要验证模板可以去 [luogu](https://www.luogu.com.cn/),luogu 收录了很多模板题。
|
||||
|
||||
## 第三阶段
|
||||
|
||||
恭喜你,你应该已经度过淘汰阶段,到了组队环节了,祝愿你组队能抱到大腿。
|
||||
|
||||
有了队伍,可以考虑一下分工问题,包括读题、键盘手、各种算法类型等分工。这里建议对于任何一类问题(比如数据结构),队伍里要有至少两个人擅长,因为两个人讨论解决问题比一个人要快很多很多。而在比赛中期,最好的策略也是双开,即两个人研究一题,剩下一个研究另一题(不过这只是经验之谈了)。
|
||||
|
||||
如果你在激烈的竞争中获得参赛资格,那么你基本可以认为你有至少银牌的实力了。(~~拿了铜牌及以下老刘会生气的~~)
|
||||
|
||||
## 退役的姿势
|
||||
|
||||
打 ACM 很多是为了方便找工作的,但是作者这届找工作太难了😭,光凭借 ACM 奖牌是远远不够的,要拥有好多其他能力。
|
||||
|
||||
不管你 ACM 是否取得了成绩,建议退役后要好好做规划,多了解行情,选择好的方向进行研究。
|
||||
37
2023旧版内容/3.编程思维体系构建/3.2算法杂谈.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# 3.2 算法杂谈
|
||||
|
||||
## 学计算机要先学算法吗?
|
||||
|
||||
也许有的同学在高中阶段接触过信息学奥赛,那么也许你已经对基础的算法知识已经有了一定的了解。
|
||||
|
||||
那么对于没有接触过算法的同学来说呢?也许你们听过 Niklaus Emil Wirth 的一句话:“程序=算法 + 数据结构。”可能有的同学这个时候就要说了:“你看算法多重要,你没有算法就没有程序!”但这句话中的算法并不是你们所理解的狭义的“算法”。
|
||||
|
||||
广义上来说,“算法”可以被理解为是一组解决特定问题的有序步骤,它在描述一个“流程”,诸如做菜的流程、排课表都可以被认为是一种算法。利用计算机逻辑解决问题才是算法的核心。
|
||||
|
||||
学习算法的基础是拥有最基本的计算机素养,你需要优先学习一些基本的计算机概念、编程语言、简单的数据结构(数组、链表等),这些基本知识是你能够灵活利用算法的基础。
|
||||
|
||||
## 学了算法就相当于学好了计算机吗?
|
||||
|
||||
学好了算法当然不等于学好了计算机科学。计算机科学是一个非常庞大的知识体系,包括更为底层的计算机组成原理、编译原理等,更为表层的 AI,开发等,是一门综合性学科。总的来说,算法是计算机科学中较为重要的一部分,但**远远**不是全部。
|
||||
|
||||
## 学算法就要用《算法导论》一类的书吗?
|
||||
|
||||
我的答案是否定的。它更适合作为“工具书”(就像你英语的词典那样),而不是一本适合新生入门学习的书。可以使用《我的第一本算法书》一类的更为基础更为有趣的算法内容。相比于完全严谨的逻辑推导,初学者的诉求是在"看得见,摸得着的例子和环境下探索和吸收新概念". 像这样的大部头可以在之后进行阅读。
|
||||
|
||||
## 学算法一定要用 C 语言吗?不用 C 语言可以吗?
|
||||
|
||||
不一定要用 C 语言。但是 C 语言作为一种贴近底层面向过程语言,对日后学习其他的语言会有较大的帮助。你也可以先学习 Python、JAVA 等等。学校的课程仅仅是教授一些比较基础的知识,如果想要真正掌握一门语言,需要在学校课程的基础上更进一大大大步。
|
||||
|
||||
## ACM 怎么说?
|
||||
|
||||
前情提要,请尽量不要以功利的心态去参加 ACM,你想要的与你能得到的可能存在过大落差
|
||||
|
||||
ACM 是美国计算机协会(Association for Computing Machinery)的缩写,原先是 ICPC(国际大学生程序设计竞赛)的赞助商(2018 年后不再赞助),在国内与 ICPC 齐名的比赛还有 CCPC(中国大学生程序设计竞赛)。
|
||||
|
||||
但常说的“ACM”一般泛指一切采用 ACM 赛制(给定一定数量的题目,采用罚时制,通过相同题目数量的选手依据罚时决定排名)的程序设计竞赛。
|
||||
|
||||
在我校,参加 ACM 社团(姑且叫做社团)并不代表能够参加有含金量的团体赛(ICPC、CCPC 等)。你需要先参加由我校教练刘春英老师组织的各种比赛,有资格进入集训队后,才有机会代表学校参加比赛(当然不限名额的个人赛想参加就参加)。
|
||||
|
||||
进入集训队后采取末位淘汰制度(最后留下来的人在 20 人左右),最后留下来的人才有机会参加比赛。**因此个人并不推荐 0 基础的同学对于 ACM 过于执着**,有 0 基础的同学最后进入校队的例子,不过这通常意味着你一天至少得刷一道算法题。如果还是想尝试的同学,可以去洛谷 ([www.luogu.com.cn](http://www.luogu.com.cn))、Codeforces([www.codeforces.com](http://www.codeforces.com))、Atcoder([atcoder.jp](https://atcoder.jp/)) 等平台上注册账号,练习题目,参加这些网站定期组织的一些比赛。
|
||||
|
||||
如果经过一段时间的练习能够在 Codefoces([www.codeforces.com](http://www.codeforces.com))上达到 1400 以上的 Rating,那么可以再观望观望参与 ACM。
|
||||
61
2023旧版内容/3.编程思维体系构建/3.3如何选择编程语言.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# 3.3 如何选择编程语言
|
||||
|
||||
## 编程语言的工具属性
|
||||
|
||||
在回答这个问题之前,需要各位同学明确的一点是,编程并不是一个独立的学科,像数学那样做题是学不好的。
|
||||
|
||||
编程语言的选择更像是锤子与扳手之间的选择,更大程度上看的是你需要解决什么样的问题。当你需要砸钉子的时候,使用螺丝刀总归是不顺手的,因此了解不同语言的特性,针对任务进行选择是非常有必要的。
|
||||
|
||||
## 编程语言特性
|
||||
|
||||
首先附上一张经典老图
|
||||
|
||||

|
||||
|
||||
### C 语言/C++
|
||||
|
||||
C 语言/C 艹一脉同源,从图中来看,C 和 C 艹都像多功能瑞士军刀,说明其是用来做细活的工具,C 上面的优盘说明其可以进行硬件开发的相关工作。
|
||||
|
||||
不少同学对可能会感到困惑,为什么要从 C 语言学起<del>(因为学校菜教不了别的)</del>
|
||||
|
||||
C 语言其实是一门优秀的承上启下的语言,既具有高级语言的特点,编写不依赖特定的计算机硬件应用程序,应用广泛,又具有底层汇编的特点,其指针可以让此语言操纵更为底层的内存空间。
|
||||
|
||||
但是其功能毕竟受限,有时候用起来会苦恼其操作受限以及各种奇奇怪怪的 bug 问题。
|
||||
|
||||
**如果为了增强自身的编程能力和计算机素养,培养解决问题的能力,C 语言的你的不二选择。在这里强烈推荐 jyy 老师的各类课程。([http://jyywiki.cn/](http://jyywiki.cn/))**
|
||||
|
||||
**我们的任务一部分会使用 C 语言,一方面培养大家编程能力,一方面辅助大家期末考试。**
|
||||
|
||||
### C++
|
||||
|
||||
现代 C++ 程序可看成以下三部分组成。
|
||||
|
||||
- 更接近底层的语言,大多继承于 C
|
||||
- 更高级的语言特征,可自定义数据类型
|
||||
- 标准库
|
||||
|
||||
**C++ 既有 C 面向过程的特点,又拥有面向对象的特性,是一门系统级的语言。**
|
||||
|
||||
编译器、操作系统的开发,高性能服务器的开发,游戏引擎的开发,硬件编程,深度学习框架的开发......只要是和底层系统或者是与性能相关的事情,通常都会有 C++ 的一席之地。
|
||||
|
||||
## Python
|
||||
|
||||
Python 在图里是电锯,适合干比较“狂野”的任务,也是深度学习的主要编程语言。其在数据处理方面具有诸多出色成熟的库,编程语言也较为简单。
|
||||
|
||||
但是过度的包装可能造成出现意想不到的错误,出现的结果看不懂等等。
|
||||
|
||||
使用缩进控制语句是此语言的特点。
|
||||
|
||||
**作为深度学习的主要使用语言,我们将以****P****ython 为主。**
|
||||
|
||||
## JAVA
|
||||
|
||||
一门面向对象编程语言,不仅吸收了 C++ 语言的各种优点,还摒弃了 C++ 里难以理解的多继承,指针等概念,因此 java 语言具有功能强大和简单易用两个特征。
|
||||
|
||||
他太老了,虽然不少框架都依托于 Java,但是不得不说,一些地方略有落后。
|
||||
|
||||
**频繁应用于****W****eb 开发,安卓应用等等。**
|
||||
|
||||

|
||||
|
||||
当然还有各种形形色色的编程语言等着同学们去探索。
|
||||
72
2023旧版内容/3.编程思维体系构建/3.4.1FAQ:常见问题.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# FAQ:常见问题
|
||||
|
||||
## 我完全没基础觉得好难呜呜
|
||||
|
||||
教育除了知识的记忆之外,更本质的是能力的训练,即所谓的 training. 而但凡 training 就必须克服一定的难度,否则你就是在做重复劳动,能力也不会有改变。如果遇到难度就选择退缩,或者让别人来替你克服本该由你自己克服的难度,等于是自动放弃了获得 training 的机会
|
||||
|
||||
## 我觉得无从下手
|
||||
|
||||
尝试借鉴他人的代码也未尝不可,但是要保证每一行都看懂哦
|
||||
|
||||

|
||||
|
||||
## 我感觉讲义写的不够细
|
||||
|
||||
首先,我无法照顾到每一个人的情况,保证你每一个地方都看懂
|
||||
|
||||
其次,很多地方的坑是故意留给你让你尝试独立解决问题的。
|
||||
|
||||
## 我觉得我以后不会从事 C 相关的工作
|
||||
|
||||
这种"只要不影响我现在 survive, 就不要紧"的想法其实非常的利己和短视:你在专业上的技不如人,迟早有一天会找上来,会影响到你个人职业生涯的长远的发展
|
||||
|
||||
更严重的是,他可能会透支学校的信誉。
|
||||
|
||||
**同时,先学好 C 语言对你有以下帮助:**
|
||||
|
||||
1. 掌握计算机底层知识:C 语言是一种高效的系统级语言,它的语法和数据结构设计直接映射到底层计算机硬件,通过学习 C 语言可以更深入地了解计算机底层运作原理,为理解更高级的编程语言和开发工具奠定基础。
|
||||
2. 提高编程能力:C 语言的语法相对较为简单,但是它要求程序员手动管理内存,这需要编程者深入了解内存结构和指针的使用。通过学习 C 语言,可以锻炼编程能力,提高代码质量和效率。
|
||||
3. 能够理解其他语言:C 语言是很多编程语言的基础,如 C++、Java、Python 等语言都从 C 语言继承了很多特性。因此,学好 C 语言可以帮助你更好地理解其他编程语言的设计思路和工作原理。
|
||||
4. 开发底层软件:由于 C 语言具有高效、灵活、可移植等特点,因此它被广泛用于开发操作系统、嵌入式系统、网络协议、游戏引擎等底层软件。学习好 C 语言可以为你将来从事底层软件开发提供必要的基础知识。
|
||||
|
||||
## 我感觉我写了也不会学到啥
|
||||
|
||||
复杂的问题总是存在简单的解释,C 语言虽然不擅长带 GUI 界面的编写,但是我们每日在用的都和他息息相关,那些庞大的系统也无非就是由这些简单的东西搭建而成的
|
||||
|
||||
## 我觉得我没有学懂 C 语言就开始别的合适吗
|
||||
|
||||
学习本章内容更大程度上是为了让你搞清楚编程世界运行的基本原理
|
||||
|
||||
其实各类编程语言本质上没有什么不同(并且 C 语言是一切语言的爹),你熟悉了一门别的语言都会变得非常容易
|
||||
|
||||
想要彻彻底底的摸清楚这门语言并达到实操只能靠常年累月的积累,如果各位真的有打算,欢迎尝试本篇教程的爹
|
||||
|
||||
NJU-ICS-PA 南京大学计算机系统基础
|
||||
|
||||
但是建议大家大二再进行尝试,非常难
|
||||
|
||||
## 我总觉得文章没写清楚
|
||||
|
||||
你毕业后进入公司/课题组,不会再有讲义具体地告诉你应该做什么,总有一天你需要在脱离讲义的情况下完成任务。我们希望你现在就放弃"讲义和框架代码会把我应该做的一切细节清楚地告诉我"的幻想,为自己的成长负起责任:
|
||||
|
||||
- 不知道在说什么,说明你对知识点的理解还不够清楚,这时候你应该去看书/看手册
|
||||
- 不知道要做什么/怎么做,说明你的系统观好是零碎的,理解不了系统中各个模块之间的联系,这时候你应该 RTFSC, 尽自己最大努力梳理并理解系统中的一切细节
|
||||
- bug 调不出来,说明你不清楚程序正确的预期行为,你需要 RTFSC 理解程序应该如何运行; 此外也说明你不重视工具和方法的使用,你需要花时间去体验和总结它们
|
||||
|
||||
如果你发现自己有以上情况,你还是少抱怨,多吃苦吧。
|
||||
|
||||
当然,如果你发现有更好的想法欢迎联系我
|
||||
|
||||
## 这些对我太简单了
|
||||
|
||||
你可以从广度和深度两个角度对自己进行拔高
|
||||
|
||||
但是我的建议是涉猎更多的方向更多的领域帮助你建立系统的认知
|
||||
|
||||
有且仅有大学有这样好的资源帮助你了
|
||||
|
||||
## **坚持了好久还是搞不定,我想放弃了**
|
||||
|
||||

|
||||
|
||||
也许是你坚持的姿势不对,来和 ZZM 聊聊吧
|
||||
195
2023旧版内容/3.编程思维体系构建/3.4.2用什么写 C 语言.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# 用什么写 C 语言
|
||||
|
||||
初学 C 语言,第一个问题莫过于用什么软件编写 C 语言程序。学校的老师可能会推荐包括但不限于 VC6.0,CodeBlocks,devC++,Visual Studio2013 等,如果你的电脑不是老年机,那么以上软件衷心建议你不要去使用,过于老旧了。
|
||||
|
||||
## Windows-Visual Studio
|
||||
|
||||
[vs2022(Visual Studio 2022) 指南&&技巧要领](https://www.bilibili.com/video/BV1Xt411g7jT)
|
||||
|
||||
<Bilibili bvid='BV1Xt411g7jT'/>
|
||||
|
||||
Visual Studio(以下简称 VS)是 Windows 下最完美的 C/C++ 等语言的开发平台,有“宇宙第一 IDE”之称,功能丰富,开箱即用。目前更新到 2022 版。
|
||||
|
||||
什么是 IDE,什么是代码编辑器,什么是编译器等等细碎问题参考文档 [3.1 该使用哪个编辑器???](3.1%E8%AF%A5%E4%BD%BF%E7%94%A8%E5%93%AA%E4%B8%AA%E7%BC%96%E8%BE%91%E5%99%A8%EF%BC%9F%EF%BC%9F%EF%BC%9F.md) 看不懂的话直接无脑装
|
||||
|
||||
### **下载**
|
||||
|
||||
[https://visualstudio.microsoft.com/zh-hans/downloads/](https://visualstudio.microsoft.com/zh-hans/downloads/)
|
||||
|
||||
选择社区版
|
||||
|
||||

|
||||
|
||||
社区版和专业版等的区别:社区版免费,功能上几乎无差别
|
||||
|
||||
### VS 安装
|
||||
|
||||
选择 C++ 桌面开发,其他不用选,有需要了再说。另外,Python 开发不好使,不要像我一样选 Python 开发。
|
||||
|
||||

|
||||
|
||||
安装完成后,一般来说 VS 不会自动创建桌面快捷方式,你需要到开始菜单中启动 VS。
|
||||
|
||||
请勿使用鼠标右键中的“在 VS 中打开”来打开 VS
|
||||
|
||||
首次打开应该会让你选择开发环境和主题,建议开发环境选择 C++ ,主题根据个人喜好选择。
|
||||
|
||||
### 创建项目
|
||||
|
||||
VS 是项目制,你需要创建一个项目才能开始编写代码并运行。
|
||||
|
||||
打开 VS,会打开如下界面(我使用深色主题),在此处单击“创建新项目”
|
||||
|
||||

|
||||
|
||||
在创建新项目页面中选择项目模板为控制台应用(空项目亦可,后续手动添加.c 源文件),并单击下一步
|
||||
|
||||

|
||||
|
||||
为你的项目起一个名字,以及选择项目的位置,一般默认即可,如果你有强迫症,C 盘一定不能放个人数据,请自行修改。完成后单击“创建”
|
||||
|
||||

|
||||
|
||||
自此就创建了一个项目了,你将会到达如下界面:
|
||||
|
||||

|
||||
|
||||
其中,左侧(如果在一开始没有选择 C++ 开发环境的话可能在右侧)为资源管理器,列出了本项目所用到的所有文件,包括代码(外部依赖项、源文件、头文件),以及将来开发图形化界面所需的资源文件;最中间占据面积最多的是代码编辑器窗口,你以后将会在这里编写你的 C 语言代码。最下面是输出窗口,源代码进行编译时,会在此处给出编译进度以及可能的代码中的错误。
|
||||
|
||||
请仔细阅读注释(绿色)中的各项内容,这很重要。
|
||||
|
||||
阅读完以后,就可以将代码全部删去,编写自己的代码了。
|
||||
|
||||
注意控制台项目初始源文件后缀为.cpp 为 C++ 文件,如果编写 C 语言**建议将后缀改为.c**。.cpp 存在隐患:如果不小心使用了 C++ 的语法而非 C 存在的语法,编译器并不会报错,且 C 与 C++ 在某些特性存在区别。
|
||||
|
||||
### “运行”你的 C 语言代码
|
||||
|
||||
C 语言是编译型语言,因此说“运行”代码其实并不是十分合适,不过我们初学,不用过分抠字眼,知道什么意思即可。
|
||||
|
||||
当你编写完自己的代码后,即可单击“本地 Windows 调试器”(或者使用快捷键 F5)进行“运行”。
|
||||
|
||||

|
||||
|
||||
你可能会发现在“本地 Windows 调试器”右侧还有一个绿色三角形,并且单击这个也可以“运行”,这两个的区别在于“本地 Windows 调试器”是调试运行,右侧那个是不调试直接运行。
|
||||
|
||||
### scanf 报错
|
||||
|
||||
如果你的代码被 VS 提示“This function or variable may be unsafe. Consider using scanf_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.”
|
||||
|
||||

|
||||
|
||||
需要你在项目-xxx 属性(xxx 是你的项目名)-C/C++-代码生成 - 安全检查里将安全检查禁用
|
||||
|
||||

|
||||
|
||||
### 调试
|
||||
|
||||
IDE 相比于代码编辑器,最强大的一点莫过于成熟的调试系统。通过调试,可以快速定位代码中没有被编译器检查出来的逻辑错误。如果需要调试,则可以在这个位置单击,打下断点,并且运行程序,程序运行时,就会在此处暂停下来,暂停时就可以查看各个变量的值了。
|
||||
|
||||

|
||||
|
||||
### **深色主题**
|
||||
|
||||
需要深色主题请在工具 - 主题里更改为深色
|
||||
|
||||
### Tips
|
||||
|
||||
#### 仔细查看报错
|
||||
|
||||

|
||||
|
||||
如果程序代码中出现红色波浪线,则表示该处代码有“错误”,并且该处的错误会同步显示在下面的这个位置,单击即可看到错误详情。如果代码中出现绿色波浪线,则表示该处代码中有警告。警告和错误的区别是警告可以通过编译运行,但编译器认为你这里可能写错了;错误是完全不可以通过编译。
|
||||
|
||||

|
||||
|
||||
#### 善用提示
|
||||
|
||||

|
||||
|
||||
当你打一些函数名或者关键字时,VS 会给出你语法提示,如果这个提示正确,按下 Tab 键即可将这个提示补全到你的代码里;或者你也可以跟着这个提示打一遍,防止打错关键字。
|
||||
|
||||
### VS 的缺点
|
||||
|
||||
过于庞大,很多功能对于初学者来说用不上,对电脑的性能也有略微的要求,但瑕不掩瑜,他的开箱即用的使用体验还是很不错的。
|
||||
|
||||
## Windows-Visual Studio Code
|
||||
|
||||
Visual Studio Code(以下简称 vscode)和 Visual Studio 都是微软开发的软件,区别在于 Visual Studio Code 是一个比较轻量的代码编辑器,在没有经过配置的情况下一般只能编写和查看代码,而不能运行,并且 Visual Studio Code 跨平台,在安装了丰富的插件后体验不输于一众 IDE。
|
||||
|
||||
> NX 的留言:
|
||||
> 鄙人认为 C 的初学者应该使用 VSCode 更佳,环境准备可见鄙人博客 [『C/C++』VScode 环境配置](https://nickxu.me/2021/12/31/cc-vscode-huan-jing-pei-zhi/)
|
||||
|
||||
### vscode 安装
|
||||
|
||||
#### 安装软件本体
|
||||
|
||||
[https://code.visualstudio.com/](https://code.visualstudio.com/)
|
||||
|
||||
在该网站进行下载,并安装,安装完成并打开后可以根据右下角的提示来修改显示语言等
|
||||
|
||||
#### 安装编译器
|
||||
|
||||
如果你电脑上下载有 VS,那么安装编译器这一环节可以省略。如果电脑上没有 VS,则需要安装 VS,或者下载其他 C 语言编译器,如 gcc,clang,icc 等
|
||||
|
||||
### 创建“项目”
|
||||
|
||||
vscode 的项目和 VS 不同,vscode 的项目比较松散,并没有 VS 那样是一套非常完善的项目系统。
|
||||
|
||||
首先需要一个空文件夹,并在 vscode 里打开这个文件夹。然后点击文件 - 新建文本文件,并选择语言为 C 语言。此时如果你是第一次创建 C 语言文件,那么右下角会弹出提示,提示你安装 C/C++ 插件,安装即可。
|
||||
|
||||
### 编写代码并运行
|
||||
|
||||
编写完代码后,保存文件,并点击运行 - 启动调试
|
||||
|
||||

|
||||
|
||||
此时会弹出如下选择框,我的电脑上同时安装有 VS 和 gcc 编译器,因此有两个,大部分的电脑上应该只有一个“C++ (Windows)”,选择你电脑上的编译器并运行即可。
|
||||
|
||||
至此就已经完成了编程和调试的基本功能。如果你想要更丰富的功能,比如多文件编译等等,可以自行去网上搜索相关的配置教程。vscode 配置好了是非常好用的,但缺点就在于配置比较麻烦。
|
||||
|
||||
## Windows-CLion
|
||||
|
||||
CLion 是 jetbrains 家族的 C 语言 IDE
|
||||
|
||||
[https://www.jetbrains.com/clion/](https://www.jetbrains.com/clion/)
|
||||
|
||||
收费软件,但可以从 GitHub 学生包里白嫖,喜欢折腾或者喜欢 jetbrains 家族软件风格的可以自己去折腾折腾。
|
||||
|
||||
## Mac OS-Visual Studio Code
|
||||
|
||||
用法和 Windows 的差不多,但由于 Mac OS 自带 clang 编译器,所以无需额外安装编译器。
|
||||
|
||||
> NX 的留言:
|
||||
> 使用自带的 clang 的确没问题,但是如果你想在 macOS 上使用 gcc/g++ ,[可参考鄙人的博客 在 Mac 的 VSC 中使用 g++ 编译器](https://nickxu.me/2023/04/04/%E5%9C%A8Mac%E7%9A%84VSCode%E4%B8%AD%E4%BD%BF%E7%94%A8g-%E7%BC%96%E8%AF%91%E5%99%A8)
|
||||
|
||||
## Mac OS-CLion
|
||||
|
||||
同样和 Windows 的差不多。
|
||||
|
||||
## Mac OS-Xcode
|
||||
|
||||
XCode 是 mac 官方的 IDE,能编写所有 mac 家族设备的软件。但缺点是没有中文。
|
||||
|
||||

|
||||
|
||||
打开以后选择 Create a new Xcode project,选择 macOS-Command Line Tool
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
两个空里第一个填项目名,第二个随便填就行
|
||||
|
||||
next 后选择项目保存的位置,之后即可到达以下界面:
|
||||
|
||||

|
||||
|
||||
点左上方小三角即可运行
|
||||
|
||||
在行号上点击并运行即可调试
|
||||
|
||||

|
||||
|
||||
## Linux
|
||||
|
||||
### 你都用 Linux 了你还来问我?一边玩去
|
||||
67
2023旧版内容/3.编程思维体系构建/3.4.3解决编程问题的普适性过程.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# 解决编程问题的普适性过程
|
||||
|
||||
- 本篇不需要任何前置知识,推荐在学习 C 语言和学完 C 语言后各看一遍。
|
||||
- 我们鼓励你在解决问题的时候进行思考,锻炼解决问题的能力,而不只是成为一个做代码翻译工作的“码农”。
|
||||
|
||||

|
||||
|
||||
解决编程问题的常见误区:
|
||||
|
||||
从编写代码入手,抓来就写(没有任何计划),对于简单编程问题,也许它是有效的,但往往它不可避免的不起作用。然后花费无数个小时试图修复代码(依旧没有任何计划),由于没有明确的计划来做什么,当“修复”代码时,往往导致它变得更复杂、更混乱。最终这个程序有点奏效,你对此心满意足。
|
||||
|
||||
相反,你应该以一种严谨的方式设计一种算法。下图显示了如何设计算法。然而,请注意,“编写代码”只有在你有了一个经过手动测试的算法之后,才能在你建立计划之前给你一些信心,证明你的计划是可靠的。
|
||||
|
||||
如果你计划得足够好并且代码编写得正确,你的代码将在第一次工作。即便它第一次不起作用,那么你至少有一个对于代码如何调试的可靠计划。
|
||||
|
||||

|
||||
|
||||
## Work an Example Yourself
|
||||
|
||||
尝试设计算法的第一步是**自己(手动)处理至少一个问题实例,为每个参数选择特定值。**往往需要确定**一个正确的示例,以及错误的示例。**
|
||||
|
||||
如果你在这一步陷入困境,这通常意味着两件事中的一件。第一种情况是问题不明确,不清楚你应该做什么。在这种情况下,你必须在继续之前解决问题。如果你正在解决自己创造的问题,你可能需要更仔细地思考正确的答案应该是什么,并完善你对问题的定义。
|
||||
|
||||
第二种情况是,缺乏领域知识,即问题所涉及的特定领域或学科的知识。也许你应该适当补充学习对应的知识。注意,领域知识可能来自数学以外的领域。它可以来自任何领域,因为编程对于处理任何类型的信息都很有用。
|
||||
|
||||
## Write Down What You Just Did
|
||||
|
||||
这一步中,必须思考解决问题所做的工作,并写下**解决该特定实例的步骤。**思考这一步骤的另一种方式是,写下一组清晰的指示,**其他人可以按照这些指示来重现刚刚解决的特定问题实例的答案**。如果在步骤 1 中执行了多个实例,那么也将重复步骤 2 多次,对步骤 1 中的每个实例重复一次。如果一条指令有点复杂,那没关系,只要指令稍后有明确的含义,我们将把这些复杂的步骤转化为它们自己的编程问题,这些问题将单独解决。
|
||||
|
||||
## Generalize Your Steps
|
||||
|
||||
**将步骤 2 得到的具体步骤,抽象为一般性的结论。**有时可能很难概括步骤。发生这种情况时,返回步骤 1 和 2 可能会有所帮助。做更多的问题实例将提供更多的信息供参考,更能帮助深入算法。这个过程通常被称为编写“伪代码”,以编程方式设计算法,而不使用特定的目标语言。几乎所有的程序员在编写任何实际代码之前都会使用这种方法来确保他们的算法是正确的。
|
||||
|
||||
## Test Your Algorithm
|
||||
|
||||
在步骤 3 之后,我们有了一个我们认为正确的算法。然而,我们完全有可能在这一路上搞砸了。步骤 4 的主要目的是确保我们的步骤在继续之前是正确的。为了实现这一点,我们使用**不同于设计算法时使用的参数值**来测试我们的算法。我们手动执行算法,并将其获得的答案与正确的答案进行比较。如果它们不同,那么我们知道我们的算法是错误的。我们使用的测试用例(参数值)越多,我们就越能确信我们的算法是正确的。不幸的是,通过测试无法确保我们的算法是正确的。要完全确定你的算法是正确的,唯一的方法就是正式证明它的正确性(使用数学证明),这超出了这个专门化的范围。
|
||||
|
||||
确定好的测试用例是一项重要的技能,可以随着实践而提高。对于步骤 4 中的测试,您需要使用至少产生几个不同答案的情况进行测试(例如,如果您的算法有“是”或“否”答案,则应使用同时产生“是”和“否”的参数进行测试)。您还应该测试任何角落情况,其中行为可能与更一般的情况不同。每当您有条件决定(包括计算位置的限制)时,您应该在这些条件的边界附近测试潜在的角点情况。
|
||||
|
||||
## Translation to Code
|
||||
|
||||
既然你对你的算法很有信心,那么是时候把它翻译成代码了。大多数时候,你会想将代码输入编辑器,以便编译和运行程序。
|
||||
|
||||
## Test Program
|
||||
|
||||
- [黑盒测试](https://zh.wikipedia.org/wiki/%E9%BB%91%E7%9B%92%E6%B5%8B%E8%AF%95)
|
||||
|
||||
在黑盒测试中,测试人员只考虑功能的预期行为,而不考虑设计测试用例的任何实现细节。缺乏对实现细节的访问是这种测试方法的由来——函数的实现被视为测试人员无法查看的“黑盒子”。
|
||||
|
||||
事实上我们无需执行步骤 1-5 就能够为假设问题设想好的测试用例。实际上,在开始解决问题之前,您可以针对问题提出一组黑盒测试。一些程序员提倡**测试优先**的开发方法。一个优点是,如果您在开始之前编写了一个全面的测试套件,那么在实现代码之后就不太可能在测试上有所疏漏。另一个优点是,通过提前考虑你的情况,你在开发和实现算法时能够降低错误率。
|
||||
|
||||
- 选择测试用例的一些建议
|
||||
|
||||
1. 确保测试涵盖所有错误情况。
|
||||
2. 确保测试“太多”和“太少”。例如,如果程序需要一行正好有 10 个字符的输入,则用 9 个和 11 个进行测试。
|
||||
3. 任何给定的测试用例只能测试一个“错误”条件。这意味着,如果要测试两个不同的错误条件,则需要两个不同测试用例。
|
||||
4. 准确地在有效性边界处进行测试。
|
||||
|
||||
- [白盒测试](https://zh.wikipedia.org/wiki/%E7%99%BD%E7%9B%92%E6%B5%8B%E8%AF%95)
|
||||
|
||||
与黑盒测试不同,白盒测试涉及检查代码以设计测试用例。白盒测试中的一个考虑因素是测试覆盖率——描述测试用例如何覆盖代码的不同行为。
|
||||
|
||||
请注意,虽然白盒和黑盒测试不同,但它们不是互斥的,而是互补的。可以从形成一组黑盒测试用例开始,实现算法,然后创建更多测试用例以达到所需的测试覆盖率。
|
||||
|
||||
## Debug Program
|
||||
|
||||
一旦在代码中发现了问题,就需要修复它,这个过程称为调试。许多新手程序员(甚至一些经验丰富的程序员)以临时方式调试,试图更改代码中的某些内容,并希望它能解决他们的问题。这样的方法很少有效,常常会导致很多挫折。
|
||||
45
2023旧版内容/3.编程思维体系构建/3.4.4C语言前置概念学习.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# C 语言前置概念学习
|
||||
|
||||
如何学习 C 语言?**第一步:Throw away the textbook。**也许你可以通过以下途径:
|
||||
|
||||
以下方式难度由易到难,但并不意味着收获由小到大:
|
||||
|
||||
1.Video:[B 站翁恺的 C 语言课程](https://www.bilibili.com/video/BV1dr4y1n7vA)(非常基础,缺点是只看视频学的过浅)
|
||||
|
||||
<Bilibili bvid='BV1dr4y1n7vA'/>
|
||||
|
||||
2.MOOC:[翁凯 C 课程的 MOOC 慕课](https://www.icourse163.org/course/ZJU-9001)(同上,慕课的习题和 Projects 性价比不高,几乎没有差别)
|
||||
|
||||
3.Web:[菜鸟教程](https://www.runoob.com/cprogramming/c-tutorial.html)(基础但是覆盖面较广,不够深入)
|
||||
|
||||
4.Web:[CNote](https://github.com/coderit666/CNote)(例子密集,学习曲线平滑,覆盖面广且具有深度)
|
||||
|
||||
::: tip 📥
|
||||
《C Primer Plus》(第六版中文版)(216MB)附件下载 <Download url="https://cdn.xyxsw.site/files/C%20Primer%20Plus%E7%AC%AC6%E7%89%88%20%E4%B8%AD%E6%96%87%E7%89%88.pdf"/>
|
||||
:::
|
||||
|
||||
5.Book:**教材替换用书——《C Primer Plus》!**(基础且深入的恰到好处,有一定拓展,可能后面的章节有点难懂,是一本不可多得的好书,不要忽视课本习题及 Projects)
|
||||
|
||||
6.MOOC:[Introductory C Programming 专项课程](https://www.coursera.org/specializations/c-programming)(**全英文**,好处是涉及到计算机思维,包含许多常用 tools 的教学例如 git、make、emacs、gdb,视频讲解结合文档阅读,对于 C 的重要核心知识讲解透彻,难度颇高,建议用作提升)
|
||||
|
||||
7.Web:[LinuxC 一站式编程](https://akaedu.github.io/book/)(难度大,枯燥硬核,收获多,基于 linux)
|
||||
|
||||
## 学习建议:可以选择其一或多种学习
|
||||
|
||||
- 对于缺乏计算机基础(这里的基础指的是计算机的日常使用)的同学,(1、2)是不错的选择,但在学完后要选择 4、5、6 进行补充巩固提高。
|
||||
- 对于有一定计算机基础的同学,直接上手 4、5、6 都是很不错的选择。
|
||||
- 对于有一定 linux 基础以及计算机基础的同学,或是同时想同步学习 linux 的同学,7 是可选择的。
|
||||
|
||||
关于“6.Introductory C Programming 专项课程”的一些思考
|
||||
|
||||
该课程的免费修读需要在 coursera 上申请助学金(并不困难)[申请教程](https://zhuanlan.zhihu.com/p/394479617)。
|
||||
|
||||
对于以上几种方式,6 有其特殊性,6 不仅仅是对于 C 语言的学习,其其中包含的计算机思维,Tools 的使用等等都有其普遍适用性。对比其他方式(包含学校课程)不难发现,对于计算机思维的训练与提升,在一定程度上,我们存在着缺失,但它又是极其重要的,如果你希望在学习 C 的同时又有其他多方面的提升,强烈建议你试试该课程(不要被英语劝退 )。
|
||||
|
||||
当然你也可以通过其他方式培养计算机思维以及学习 Tools 的使用。但是越早培养,越有优势。
|
||||
|
||||
计算机思维与计算机科学与编码能力
|
||||
|
||||

|
||||
|
||||
### **CS education is more than just “learning how to code”!**
|
||||
348
2023旧版内容/3.编程思维体系构建/3.4.5.1C语言自测标准——链表.md
Normal file
@@ -0,0 +1,348 @@
|
||||
# C 语言自测标准——链表
|
||||
|
||||
## 链表(单链表)是什么
|
||||
|
||||
链表又称单链表、链式存储结构,用于存储逻辑关系为“一对一”的数据。
|
||||
|
||||
使用链表存储数据,不强制要求数据在内存中集中存储,各个元素可以分散存储在内存中。例如,使用链表存储 {1,2,3},各个元素在内存中的存储状态可能是:
|
||||
|
||||

|
||||
|
||||
可以看到,数据不仅没有集中存放,在内存中的存储次序也是混乱的。那么,链表是如何存储数据间逻辑关系的呢?
|
||||
|
||||
链表存储数据间逻辑关系的实现方案是:为每一个元素配置一个指针,每个元素的指针都指向自己的直接后继元素,如下图所示:
|
||||
|
||||

|
||||
|
||||
显然,我们只需要记住元素 1 的存储位置,通过它的指针就可以找到元素 2,通过元素 2 的指针就可以找到元素 3,以此类推,各个元素的先后次序一目了然。像图 2 这样,数据元素随机存储在内存中,通过指针维系数据之间“一对一”的逻辑关系,这样的存储结构就是链表。
|
||||
|
||||
### 结点(节点)
|
||||
|
||||
在链表中,每个数据元素都配有一个指针,这意味着,链表上的每个“元素”都长下图这个样子:
|
||||
|
||||

|
||||
|
||||
数据域用来存储元素的值,指针域用来存放指针。数据结构中,通常将这样的整体称为结点。
|
||||
|
||||
也就是说,链表中实际存放的是一个一个的结点,数据元素存放在各个结点的数据域中。举个简单的例子,图 3 中 {1,2,3} 的存储状态用链表表示,如下图所示:
|
||||
|
||||

|
||||
|
||||
在 C 语言中,可以用结构体表示链表中的结点,例如:
|
||||
|
||||
```c
|
||||
typedef struct Node{
|
||||
int elem; //代表数据域
|
||||
struct Node * next; //代表指针域,指向直接后继元素
|
||||
}Node;
|
||||
typedef struct Node* Link;
|
||||
```
|
||||
|
||||
### 头结点、头指针和首元结点
|
||||
|
||||
图 4 所示的链表并不完整,一个完整的链表应该由以下几部分构成:
|
||||
|
||||
头指针:是指向链表中一个结点所在存储位置的指针。如果链表中有头结点,则头指针指向头结点;若链表中没有头结点,则头指针指向链表中第一个数据结点(也叫首元结点)。
|
||||
|
||||
链表有头指针,当我们需要使用链表中的数据时,我们可以使用遍历查找等方法,从头指针指向的结点开始,依次搜索,直到找到需要的数据;反之,若没有头指针,则链表中的数据根本无法使用,也就失去了存储数据的意义。
|
||||
|
||||
结点:链表中的节点又细分为头结点、首元结点和其它结点:
|
||||
|
||||
头结点:位于链表的表头,即链表中第一个结点,其一般不存储任何数据,特殊情况可存储表示链表信息(表的长度等)的数据。
|
||||
|
||||
头结点的存在,其本身没有任何作用,就是一个空结点,但是在对链表的某些操作中,链表有无头结点,可以直接影响编程实现的难易程度。
|
||||
|
||||
例如,若链表无头结点,则对于在链表中第一个数据结点之前插入一个新结点,或者对链表中第一个数据结点做删除操作,都必须要当做特殊情况,进行特殊考虑;而若链表中设有头结点,以上两种特殊情况都可被视为普通情况,不需要特殊考虑,降低了问题实现的难度。
|
||||
|
||||
**链表有头结点,也不一定都是有利的。例如解决约瑟夫环问题,若链表有头结点,在一定程度上会阻碍算法的实现。**
|
||||
|
||||
**所以,对于一个链表来说,设置头指针是必要且必须的,但有没有头结点,则需要根据实际问题特殊分析。**
|
||||
|
||||
首元结点:指的是链表开头第一个存有数据的结点。
|
||||
|
||||
其他节点:链表中其他的节点。
|
||||
|
||||
也就是说,一个完整的链表是由头指针和诸多个结点构成的。每个链表都必须有头指针,但头结点不是必须的。
|
||||
|
||||
例如,创建一个包含头结点的链表存储 {1,2,3},如下图所示:
|
||||
|
||||

|
||||
|
||||
## 链表的创建
|
||||
|
||||
创建一个链表,实现步骤如下:
|
||||
|
||||
1. 定义一个头指针;
|
||||
2. 创建一个头结点或者首元结点,让头指针指向它;
|
||||
3. 每创建一个结点,都令其直接前驱结点的指针指向它(尾插法/头插法)。
|
||||
|
||||
### 创建头指针和头结点(首元结点)
|
||||
|
||||
```c
|
||||
typedef struct Node{
|
||||
int elem; //代表数据域
|
||||
struct Node * next; //代表指针域,指向直接后继元素
|
||||
}Node;
|
||||
typedef struct Node* Link;
|
||||
-----------------------------------------------------------------------
|
||||
Link* head = (Link*)malloc(sizeof(Link)); //创建头指针
|
||||
*head = (Link)malloc(sizeof(Node));//创建头结点(首元结点)
|
||||
(*head)->elem = element;//头结点可以不存储数据或存储特殊数据
|
||||
(*head)->next = NULL;//初始头结点/首元结点的后继元素为空
|
||||
```
|
||||
|
||||
### 创建结点——头插法
|
||||
|
||||
```c
|
||||
Link p;
|
||||
while (Judgement) //for 同理
|
||||
{
|
||||
p = (Link)malloc(sizeof(Node));
|
||||
p->elem = element;
|
||||
p->next = (*head)->next;
|
||||
(*head)->next = p;
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
### 创建结点——尾插法
|
||||
|
||||
```c
|
||||
Link p;
|
||||
Link r = (*head); //临时中间结构指针,在尾插法中始终指向最后一个结点
|
||||
while (Judgement) //for 同理
|
||||
{
|
||||
p = (Link)malloc(sizeof(Node));
|
||||
p->elem = element;
|
||||
p->next = NULL;
|
||||
r->next = p;
|
||||
r = p;
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 链表的基本操作
|
||||
|
||||
学会创建链表之后,本节继续讲解链表的一些基本操作,包括向链表中添加数据、删除链表中的数据、读取、查找和更改链表中的数据。
|
||||
|
||||
### 链表读取元素
|
||||
|
||||
获得链表第 i 个数据的算法思路:
|
||||
|
||||
1. 声明一个结点 p 指向链表的第一个结点,初始化 j 从 1 开始;
|
||||
2. 当 j<i 时,就遍历链表,让 p 的指针向后移动,不断指向下一个结点,j 累加 1;
|
||||
3. 若到链表末尾 p 为空,则说明第 i 个元素不存在;
|
||||
4. 否则读取成功,返回结点 p 的数据
|
||||
|
||||
实现代码如下:
|
||||
|
||||
```c
|
||||
#define error 0
|
||||
#define ok 1
|
||||
/*用 e 返回 L 中第 i 个数据元素的值*/
|
||||
int GetElem(Link *L, int i; int *e)
|
||||
{
|
||||
Link p;
|
||||
p = (*L)->next; //p 指向第一个结点
|
||||
int j = 1;
|
||||
while (p && j < i) //p 不为空或者计数器 j 还没有等于 i 时,循环继续
|
||||
{
|
||||
p = p->next; //p 指向下一个结点
|
||||
j++;
|
||||
}
|
||||
if (!p) //第 i 个元素不存在
|
||||
return error;
|
||||
*e = p->elem; //取第 i 个元素的数据
|
||||
return ok;
|
||||
}
|
||||
```
|
||||
|
||||
了解了链表如何读取元素,同理我们可以实现更新和查找链表元素。
|
||||
|
||||
### 链表插入元素
|
||||
|
||||
向链表中增添元素,根据添加位置不同,可分为以下 3 种情况:
|
||||
|
||||
- 插入到链表的头部,作为首元节点;
|
||||
- 插入到链表中间的某个位置;
|
||||
- 插入到链表的最末端,作为链表中最后一个结点;
|
||||
|
||||
对于有头结点的链表,3 种插入元素的实现思想是相同的,具体步骤是:
|
||||
|
||||
1. 将新结点的 next 指针指向插入位置后的结点;
|
||||
2. 将插入位置前结点的 next 指针指向插入结点;
|
||||
|
||||
例如,在链表 `{1,2,3,4}` 的基础上分别实现在头部、中间、尾部插入新元素 5,其实现过程如图所示:
|
||||
|
||||

|
||||
|
||||
从图中可以看出,虽然新元素的插入位置不同,但实现插入操作的方法是一致的,都是先执行步骤 1,再执行步骤 2。实现代码如下:
|
||||
|
||||
```c
|
||||
/*在 L 中第 i 个位置(注意链表中的位置不一定为结点的个数)之前插入新的数据元素 e,
|
||||
L 的长度加一(可以用头结点存储链表长度)*/
|
||||
int ListInsert(Link *L, int i, int e)
|
||||
{
|
||||
Link p, r; //r 为临时中间结构指针,用于实现插入
|
||||
p = *L; //p 指向头结点
|
||||
int j = 1;
|
||||
while (p && j < i) //寻找第 i 个结点,
|
||||
{
|
||||
p = p->next;
|
||||
j++;
|
||||
}
|
||||
if (!p)
|
||||
return error;
|
||||
r = (Link)malloc(sizeof(Node));
|
||||
r->elem = e;
|
||||
r->next = p->next;
|
||||
p->next = r;
|
||||
return ok;
|
||||
}
|
||||
```
|
||||
|
||||
注意:链表插入元素的操作必须是先步骤 1,再步骤 2;反之,若先执行步骤 2,除非再添加一个指针,作为插入位置后续链表的头指针,否则会导致插入位置后的这部分链表丢失,无法再实现步骤 1。
|
||||
|
||||
对于没有头结点的链表,在头部插入结点比较特殊,需要单独实现。
|
||||
|
||||

|
||||
|
||||
和 2)、3) 种情况相比,由于链表没有头结点,在头部插入新结点,此结点之前没有任何结点,实现的步骤如下:
|
||||
|
||||
1. 将新结点的指针指向首元结点;
|
||||
2. 将头指针指向新结点。
|
||||
|
||||
实现代码如下:
|
||||
|
||||
```c
|
||||
/*在 L 中第 i 个位置(注意链表中的位置不一定为结点的个数)之前插入新的数据元素 e,
|
||||
L 的长度加一(可以用头结点存储链表长度)*/
|
||||
int ListInsert(Link *L, int i, int e)
|
||||
{
|
||||
if (i == 1)
|
||||
{
|
||||
Link r = (Link)malloc(sizeof(Node));
|
||||
r->elem = e;
|
||||
r->next = (*L)->next;
|
||||
*L = r;
|
||||
}
|
||||
else
|
||||
{
|
||||
//......
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 链表删除元素
|
||||
|
||||
从链表中删除指定数据元素时,实则就是将存有该数据元素的节点从链表中摘除。
|
||||
|
||||
对于有头结点的链表来说,无论删除头部(首元结点)、中部、尾部的结点,实现方式都一样,执行以下三步操作:
|
||||
|
||||
1. 找到目标元素所在结点的直接前驱结点;
|
||||
2. 将目标结点从链表中摘下来;
|
||||
3. 手动释放结点占用的内存空间;
|
||||
|
||||
从链表上摘除目标节点,只需找到该节点的直接前驱节点 temp,执行如下操作:
|
||||
|
||||
```c
|
||||
temp->next=temp->next->next;
|
||||
```
|
||||
|
||||
例如,从存有 `{1,2,3,4}` 的链表中删除存储元素 3 的结点,则此代码的执行效果如图 3 所示:
|
||||
|
||||

|
||||
|
||||
实现代码如下:
|
||||
|
||||
```c
|
||||
/*删除 L 中的第 i 个数据元素,并用 e 返回其值,L 的长度减一
|
||||
(可以用头结点存储链表长度)*/
|
||||
int ListDelete(Link *L, int i, int* e)
|
||||
{
|
||||
Link p, r;
|
||||
p = *L;
|
||||
int j = 1;
|
||||
while (p->next && j < i) //寻找删除元素中的前驱元素
|
||||
{
|
||||
p = p->next;
|
||||
j++;
|
||||
}
|
||||
if (!(p->next))
|
||||
return error; //L 中不存在第 i 个元素
|
||||
r = p->next; //标记要删除的结点
|
||||
p->next = r->next; //移除结点
|
||||
*e = r->elem; //返回结点所存数据
|
||||
free(r); //释放结点
|
||||
return ok;
|
||||
}
|
||||
```
|
||||
|
||||
对于不带头结点的链表,需要单独考虑删除首元结点的情况,删除其它结点的方式和图 3 完全相同,如下图所示:
|
||||
|
||||

|
||||
|
||||
实现代码如下:
|
||||
|
||||
```c
|
||||
/*删除 L 中的第 i 个数据元素,并用 e 返回其值,L 的长度减一
|
||||
(可以用头结点存储链表长度)*/
|
||||
int ListDelete(Link *L, int i, int* e)
|
||||
{
|
||||
if (i == 1)
|
||||
{
|
||||
Link r = *L;
|
||||
*L= r->next;
|
||||
*e = r->elem;
|
||||
free(r);
|
||||
}
|
||||
else
|
||||
{
|
||||
//......
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 链表查找元素
|
||||
|
||||
在链表中查找指定数据元素,最常用的方法是:从首元结点开始依次遍历所有节点,直至找到存储目标元素的结点。如果遍历至最后一个结点仍未找到,表明链表中没有存储该元素。
|
||||
|
||||
### 链表更新元素
|
||||
|
||||
更新链表中的元素,只需通过遍历找到存储此元素的节点,对节点中的数据域做更改操作即可。
|
||||
|
||||
## 约瑟夫环
|
||||
|
||||
约瑟夫环问题,是一个经典的循环链表问题,题意是:已知 n 个人(分别用编号 1,2,3,…,n 表示)围坐在一张圆桌周围,从编号为 k 的人开始顺时针报数,数到 m 的那个人出列;他的下一个人又从 1 开始,还是顺时针开始报数,数到 m 的那个人又出列;依次重复下去,直到圆桌上剩余一个人。
|
||||
|
||||
如图所示,假设此时圆周周围有 5 个人,要求从编号为 3 的人开始顺时针数数,数到 2 的那个人出列:
|
||||
|
||||

|
||||
|
||||
出列顺序依次为:
|
||||
|
||||
- 编号为 3 的人开始数 1,然后 4 数 2,所以 4 先出列;
|
||||
- 4 出列后,从 5 开始数 1,1 数 2,所以 1 出列;
|
||||
- 1 出列后,从 2 开始数 1,3 数 2,所以 3 出列;
|
||||
- 3 出列后,从 5 开始数 1,2 数 2,所以 2 出列;
|
||||
- 最后只剩下 5 自己,所以 5 胜出。
|
||||
|
||||
那么,究竟要如何用链表实现约瑟夫环呢?如何让一个含 5 个元素的约瑟夫环,能从第 5 个元素出发,访问到第 2 个元素呢?上面所讲的链表操作显然是难以做到的,解决这个问题就需要用到**循环链表**。
|
||||
|
||||
## 循环链表
|
||||
|
||||
将单链表中终端结点的指针端由空指针改为指向头结点,使得整个单链表形成一个环,这种头尾相接的单链表成为单循环链表,简称循环链表。
|
||||
|
||||
循环链表解决了一个很麻烦的问题。如何从当中一个结点出发,访问到链表的全部结点。
|
||||
|
||||
为了使空链表和非空链表处理一致,我们通常设一个头结点,当然,并不是说,循环链表一定要头结点,这需要注意。循环链表带有头结点的空链表如图所示:
|
||||
|
||||

|
||||
|
||||
对于非空的循环链表如图所示:
|
||||
|
||||

|
||||
|
||||
循环链表和单链表的主要差异就在于循环的判断条件上,原来是判断 p->next 是否为空,现在则是 p->next 不等于头结点,则循环未结束。
|
||||
45
2023旧版内容/3.编程思维体系构建/3.4.5阶段一:编程属性.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# 阶段一:编程属性
|
||||
|
||||
## [C 语言任务模块](https://github.com/E1PsyCongroo/HDU_C_Assignments/)
|
||||
|
||||
作为一名合格的大学生,更应深谙“纸上得来终觉浅,绝知此事要躬行”的道理,编程语言就像是一个工具,无论你如何熟读说明书(语法、特性),未经实践终究是靠不住的。
|
||||
|
||||
本模块将以有趣的任务的形式替你检测是否你已经达到了基本掌握 C 语言语法和一些特性的目的
|
||||
|
||||
- 该任务模块旨在帮助巩固 C 语言基础知识,传递一些编程思维,入门学习请看 [3.4.4C 语言前置概念学习](3.4.4C%E8%AF%AD%E8%A8%80%E5%89%8D%E7%BD%AE%E6%A6%82%E5%BF%B5%E5%AD%A6%E4%B9%A0.md)
|
||||
- 你可以通过使用 git 工具 `git clone https://github.com/E1PsyCongroo/HDU_C_Assignments.git` 获取任务
|
||||
- 或者访问 [https://github.com/E1PsyCongroo/HDU_C_Assignments](https://github.com/E1PsyCongroo/HDU_C_Assignments) 学习
|
||||
|
||||
## 任务一做前必查
|
||||
|
||||
1. 理解[3.4.3 解决编程问题的普适性过程](3.4.3%E8%A7%A3%E5%86%B3%E7%BC%96%E7%A8%8B%E9%97%AE%E9%A2%98%E7%9A%84%E6%99%AE%E9%80%82%E6%80%A7%E8%BF%87%E7%A8%8B.md) 。
|
||||
2. 理解 C 语言语法基础:变量、表达式、函数、判断、循环、常用标准库函数。
|
||||
3. 理解 C 语言中的一切都是数字。
|
||||
4. 初步理解 C 语言各类数据类型:基本数据类型和复杂自定义数据类型。
|
||||
5. 初步理解 C 语言数组及字符串。
|
||||
|
||||
## 任务二做前必查
|
||||
|
||||
1. 深入理解 C 语言指针、数组和字符串。
|
||||
2. 理解递归思想。
|
||||
3. 理解复杂自定义数据类型。
|
||||
|
||||
## 请阅读各个任务的 README.md,了解完成任务所需的前置知识
|
||||
|
||||
进阶:评价一个程序,大体分为以下四个层次。
|
||||
|
||||
1.程序没有语法错误。
|
||||
|
||||
2.程序对于合法的输入数据能够产生满足要求的输入结果。
|
||||
|
||||
3.程序对于非法的输入数据能够得出满足规格说明的结果。
|
||||
|
||||
4.程序对于精心选择的,甚至刁难的测试数据都有满足要求的输入结果。
|
||||
|
||||
在你写完这些代码后会不会感觉你的代码不够优雅呢?
|
||||
|
||||
假设你的逻辑更为复杂,需要完成的功能更多,如果全部写在 main 里面你会不会觉得越来越困难呢?
|
||||
|
||||
有没有一种方法可以让你更为优雅的把每一个功能拆分开呢?
|
||||
|
||||
当然有,在下一章,你会深刻的体会到函数的意义
|
||||
30
2023旧版内容/3.编程思维体系构建/3.4.6.1.开始冒险.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# 1.开始冒险
|
||||
|
||||
让我们从一个最基本的函数开始
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
printf("Welcome to Little Cave Adventure.\n");
|
||||
printf("It is very dark in here.\n");
|
||||
printf("\nBye!\n");
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
输出样例:
|
||||
|
||||
Welcome to Little Cave Adventure.
|
||||
It is very dark in here.
|
||||
|
||||
Bye!
|
||||
|
||||
尽管可能微不足道,但该程序确实展示 *了*任何文本冒险中最重要的方面:描述性文本。一个好的故事是制作一款好的冒险游戏的要素之一。
|
||||
|
||||
## 为什么要用英文?
|
||||
|
||||
因为中文的编码模式可能会带来奇怪的影响。
|
||||
|
||||
思考题:大家可自行去了解 utf-8,GDB 等编码模式及其历程
|
||||
508
2023旧版内容/3.编程思维体系构建/3.4.6.10.增添属性.md
Normal file
@@ -0,0 +1,508 @@
|
||||
# 10.增添属性
|
||||
|
||||
我们在第 4 章中介绍 object 时,它们只有三个属性。
|
||||
|
||||
在第 6 章中,我们增加了第四个。这他妈太少了!
|
||||
|
||||
为了在我们的冒险中加入更多细节,我们需要更多的属性。下面是一些示例。
|
||||
|
||||
1. 环顾四周这个命令可以给出玩家位置的全局描述,包括在那里的物品、NPC 和其他对象的列表。很多冒险活动都需要玩家对这些对象进行查询,从而揭示出在游戏中获得进展所需要的某些线索,或者简单地提升游戏的氛围。我们将添加一个对每个对象都有详细说明的属性细节,加上一个与包含其他对象的对象一起使用的属性内容。
|
||||
2. 如果玩家开始一段话时,响应总是“OK”和新位置的描述。这似乎有点沉闷。所以,为每个段落提供自己的自定义消息会更好。我们将添加一个属性 textGo 来保存此消息。
|
||||
3. 有些通道可能是特殊设定的,比如说传送门,陷阱等等。
|
||||
|
||||
举个例子,一条林道可能隐藏着陷阱。虽然通道似乎从位置 A 通向位置 B,但实际上终点是位置 C,即掉进坑了。
|
||||
|
||||
假设我们的洞口被警卫挡住了。玩家就过不去,我们可以简单地将通道的*目的地*更改为终点位置(或 *NULL*),但这会导致对*诸如 go cave 和 look cave* 这样的命令做出不正确的回应:“你在这里看不到任何洞穴。我们需要一个将通道的实际终点和虚假终点分开的单独属性。为此,我们将引入一个属性 prospect 来表示后者。
|
||||
|
||||
1. 在许多冒险中,玩家以及游戏中的 NPC 在携带量方面受到限制。给每件物品一个重量,角色库存中所有物品的总重量不应超过该角色所能承载的最大重量。当然,我们也可以给一个物体一个非常高的重量,使它不可移动(一棵树,一座房子,一座山)。
|
||||
2. RPG 式的冒险游戏需要角色的整个属性范围 ( 玩家与非玩家 ),例如 HP。HP 为零的对象要么死了,要么根本不是角色。
|
||||
|
||||
我们在 object.txt 中定义了七个新属性:
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
|
||||
typedef struct object {
|
||||
const char *description;
|
||||
const char **tags;
|
||||
struct object *location;
|
||||
struct object *destination;
|
||||
struct object *prospect;
|
||||
const char *details;
|
||||
const char *contents;
|
||||
const char *textGo;
|
||||
int weight;
|
||||
int capacity;
|
||||
int health;
|
||||
} OBJECT;
|
||||
|
||||
extern OBJECT objs[];
|
||||
|
||||
- field
|
||||
description "an open field"
|
||||
tags "field"
|
||||
details "The field is a nice and quiet place under a clear blue sky."
|
||||
capacity 9999
|
||||
|
||||
- cave
|
||||
description "a little cave"
|
||||
tags "cave"
|
||||
details "The cave is just a cold, damp, rocky chamber."
|
||||
capacity 9999
|
||||
|
||||
- silver
|
||||
description "a silver coin"
|
||||
tags "silver", "coin", "silver coin"
|
||||
location field
|
||||
details "The coin has an eagle on the obverse."
|
||||
weight 1
|
||||
//如果你觉得不合理,可以自己更改
|
||||
|
||||
- gold
|
||||
description "a gold coin"
|
||||
tags "gold", "coin", "gold coin"
|
||||
location cave
|
||||
details "The shiny coin seems to be a rare and priceless artefact."
|
||||
weight 1
|
||||
|
||||
- guard
|
||||
description "a burly guard"
|
||||
tags "guard", "burly guard"
|
||||
location field
|
||||
details "The guard is a really big fellow."
|
||||
contents "He has"
|
||||
health 100
|
||||
capacity 20
|
||||
|
||||
- player
|
||||
description "yourself"
|
||||
tags "yourself"
|
||||
location field
|
||||
details "You would need a mirror to look at yourself."
|
||||
contents "You have"
|
||||
health 100
|
||||
capacity 20
|
||||
|
||||
- intoCave
|
||||
description "a cave entrance to the east"
|
||||
tags "east", "entrance"
|
||||
location field
|
||||
prospect cave
|
||||
details "The entrance is just a narrow opening in a small outcrop."
|
||||
textGo "The guard stops you from walking into the cave."
|
||||
|
||||
- exitCave
|
||||
description "an exit to the west"
|
||||
tags "west", "exit"
|
||||
location cave
|
||||
destination field
|
||||
details "Sunlight pours in through an opening in the cave's wall."
|
||||
textGo "You walk out of the cave."
|
||||
|
||||
- wallField
|
||||
description "dense forest all around"
|
||||
tags "west", "north", "south", "forest"
|
||||
location field
|
||||
details "The field is surrounded by trees and undergrowth."
|
||||
textGo "Dense forest is blocking the way."
|
||||
|
||||
- wallCave
|
||||
description "solid rock all around"
|
||||
tags "east", "north", "south", "rock"
|
||||
location cave
|
||||
details "Carved in stone is a secret password 'abccb'."
|
||||
textGo "Solid rock is blocking the way."
|
||||
```
|
||||
|
||||
注意:textGo 不仅对通道对象有用,而且对非通道对象也有用 ( 在这种情况下,以后我们将介绍“墙”这个概念)
|
||||
|
||||
::: warning 🤔 思考题:你能否自行实现上述伪代码?
|
||||
:::
|
||||
|
||||
现在,我们已经可以使用新属性 (如果你完成了上面的思考题),**details** 用于新识别的命令*外观`<object>`*,**textGo** 在我们的命令 *go* 实现中替换固定文本*“OK*”。
|
||||
|
||||
## location.c
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "object.h"
|
||||
#include "misc.h"
|
||||
#include "noun.h"
|
||||
|
||||
void executeLook(const char *noun)
|
||||
{
|
||||
if (noun != NULL && strcmp(noun, "around") == 0)
|
||||
{
|
||||
printf("You are in %s.\n", player->location->description);
|
||||
listObjectsAtLocation(player->location);
|
||||
}
|
||||
else
|
||||
{
|
||||
OBJECT *obj = getVisible("what you want to look at", noun);
|
||||
switch (getDistance(player, obj))
|
||||
{
|
||||
case distHereContained:
|
||||
printf("Hard to see, try to get it first.\n");
|
||||
break;
|
||||
case distOverthere:
|
||||
printf("Too far away, move closer please.\n");
|
||||
break;
|
||||
case distNotHere:
|
||||
printf("You don't see any %s here.\n", noun);
|
||||
break;
|
||||
case distUnknownObject:
|
||||
// already handled by getVisible
|
||||
break;
|
||||
default:
|
||||
printf("%s\n", obj->details);
|
||||
listObjectsAtLocation(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void movePlayer(OBJECT *passage)
|
||||
{
|
||||
printf("%s\n", passage->textGo);
|
||||
if (passage->destination != NULL)
|
||||
{
|
||||
player->location = passage->destination;
|
||||
printf("\n");
|
||||
executeLook("around");
|
||||
}
|
||||
}
|
||||
|
||||
void executeGo(const char *noun)
|
||||
{
|
||||
OBJECT *obj = getVisible("where you want to go", noun);
|
||||
switch (getDistance(player, obj))
|
||||
{
|
||||
case distOverthere:
|
||||
movePlayer(getPassage(player->location, obj));
|
||||
break;
|
||||
case distNotHere:
|
||||
printf("You don't see any %s here.\n", noun);
|
||||
break;
|
||||
case distUnknownObject:
|
||||
// already handled by getVisible
|
||||
break;
|
||||
default:
|
||||
movePlayer(obj);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
属性权重和容量一起成为不能将某些对象移动到周围的可能原因。而 HP 检查代替了角色的硬编码白名单。
|
||||
|
||||
## move.c
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
#include "misc.h"
|
||||
|
||||
static int weightOfContents(OBJECT *container)
|
||||
{
|
||||
int sum = 0;
|
||||
OBJECT *obj;
|
||||
for (obj = objs; obj < endOfObjs; obj++)
|
||||
{
|
||||
if (isHolding(container, obj)) sum += obj->weight;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
static void describeMove(OBJECT *obj, OBJECT *to)
|
||||
{
|
||||
if (to == player->location)
|
||||
{
|
||||
printf("You drop %s.\n", obj->description);
|
||||
}
|
||||
else if (to != player)
|
||||
{
|
||||
printf(to->health > 0 ? "You give %s to %s.\n" : "You put %s in %s.\n",
|
||||
obj->description, to->description);
|
||||
}
|
||||
else if (obj->location == player->location)
|
||||
{
|
||||
printf("You pick up %s.\n", obj->description);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("You get %s from %s.\n",
|
||||
obj->description, obj->location->description);
|
||||
}
|
||||
}
|
||||
|
||||
void moveObject(OBJECT *obj, OBJECT *to)
|
||||
{
|
||||
if (obj == NULL)
|
||||
{
|
||||
// already handled by getVisible or getPossession
|
||||
}
|
||||
else if (to == NULL)
|
||||
{
|
||||
printf("There is nobody here to give that to.\n");
|
||||
}
|
||||
else if (obj->weight > to->capacity)
|
||||
{
|
||||
printf("That is way too heavy.\n");
|
||||
}
|
||||
else if (obj->weight + weightOfContents(to) > to->capacity)
|
||||
{
|
||||
printf("That would become too heavy.\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
describeMove(obj, to);
|
||||
obj->location = to;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这里还有一个模块可以使用 HP 来识别角色。
|
||||
|
||||
## inventory.c
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
#include "misc.h"
|
||||
#include "noun.h"
|
||||
#include "move.h"
|
||||
|
||||
void executeGet(const char *noun)
|
||||
{
|
||||
OBJECT *obj = getVisible("what you want to get", noun);
|
||||
switch (getDistance(player, obj))
|
||||
{
|
||||
case distSelf:
|
||||
printf("You should not be doing that to yourself.\n");
|
||||
break;
|
||||
case distHeld:
|
||||
printf("You already have %s.\n", obj->description);
|
||||
break;
|
||||
case distOverthere:
|
||||
printf("Too far away, move closer please.\n");
|
||||
break;
|
||||
case distUnknownObject:
|
||||
// already handled by getVisible
|
||||
break;
|
||||
default:
|
||||
if (obj->location != NULL && obj->location->health > 0)
|
||||
{
|
||||
printf("You should ask %s nicely.\n", obj->location->description);
|
||||
}
|
||||
else
|
||||
{
|
||||
moveObject(obj, player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void executeDrop(const char *noun)
|
||||
{
|
||||
moveObject(getPossession(player, "drop", noun), player->location);
|
||||
}
|
||||
|
||||
void executeAsk(const char *noun)
|
||||
{
|
||||
moveObject(getPossession(actorHere(), "ask", noun), player);
|
||||
}
|
||||
|
||||
void executeGive(const char *noun)
|
||||
{
|
||||
moveObject(getPossession(player, "give", noun), actorHere());
|
||||
}
|
||||
|
||||
void executeInventory(void)
|
||||
{
|
||||
if (listObjectsAtLocation(player) == 0)
|
||||
{
|
||||
printf("You are empty-handed.\n");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
::: warning 🤔 思考题:仔细观察这段代码,看看与你写的有何不同?
|
||||
:::
|
||||
|
||||
权重检查利用了新功能 *weightOfContents,*它将在*misc.c*中实现。在同一模块中,我们还对一些现有函数进行了修改,以支持最后几个属性。
|
||||
|
||||
属性内容将替换固定文本*“You see”。*在列出玩家的库存时,原始文本已经有点奇怪了,但是现在函数*listObjectsAtLocation*用于显示任何可能对象的内容(请参阅上面的函数*expertLook*),我们真的需要一些更灵活的东西。
|
||||
|
||||
在函数 *getPassage* 中我们将属性*目标*替换为 prospect,并改进*对所有*命令(而不仅仅是 *go* and *look*)的响应,这些命令应用于位于“隐藏通道”另一端的位置。
|
||||
|
||||
## misc.h
|
||||
|
||||
```c
|
||||
typedef enum {
|
||||
distSelf,
|
||||
distHeld,
|
||||
distHeldContained,
|
||||
distLocation,
|
||||
distHere,
|
||||
distHereContained,
|
||||
distOverthere,
|
||||
distNotHere,
|
||||
distUnknownObject
|
||||
} DISTANCE;
|
||||
|
||||
extern bool isHolding(OBJECT *container, OBJECT *obj);
|
||||
extern OBJECT *getPassage(OBJECT *from, OBJECT *to);
|
||||
extern DISTANCE getDistance(OBJECT *from, OBJECT *to);
|
||||
extern OBJECT *actorHere(void);
|
||||
extern int listObjectsAtLocation(OBJECT *location);
|
||||
```
|
||||
|
||||
## misc.c
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
#include "misc.h"
|
||||
|
||||
bool isHolding(OBJECT *container, OBJECT *obj)
|
||||
{
|
||||
return obj != NULL && obj->location == container;
|
||||
}
|
||||
|
||||
OBJECT *getPassage(OBJECT *from, OBJECT *to)
|
||||
{
|
||||
if (from != NULL && to != NULL)
|
||||
{
|
||||
OBJECT *obj;
|
||||
for (obj = objs; obj < endOfObjs; obj++)
|
||||
{
|
||||
if (isHolding(from, obj) && obj->prospect == to)
|
||||
{
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
DISTANCE getDistance(OBJECT *from, OBJECT *to)
|
||||
{
|
||||
return to == NULL ? distUnknownObject :
|
||||
to == from ? distSelf :
|
||||
isHolding(from, to) ? distHeld :
|
||||
isHolding(to, from) ? distLocation :
|
||||
isHolding(from->location, to) ? distHere :
|
||||
isHolding(from, to->location) ? distHeldContained :
|
||||
isHolding(from->location, to->location) ? distHereContained :
|
||||
getPassage(from->location, to) != NULL ? distOverthere :
|
||||
distNotHere;
|
||||
}
|
||||
|
||||
OBJECT *actorHere(void)
|
||||
{
|
||||
OBJECT *obj;
|
||||
for (obj = objs; obj < endOfObjs; obj++)
|
||||
{
|
||||
if (isHolding(player->location, obj) && obj != player &&
|
||||
obj->health > 0)
|
||||
{
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int listObjectsAtLocation(OBJECT *location)
|
||||
{
|
||||
int count = 0;
|
||||
OBJECT *obj;
|
||||
for (obj = objs; obj < endOfObjs; obj++)
|
||||
{
|
||||
if (obj != player && isHolding(location, obj))
|
||||
{
|
||||
if (count++ == 0)
|
||||
{
|
||||
printf("%s:\n", location->contents);
|
||||
}
|
||||
printf("%s\n", obj->description);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
```
|
||||
|
||||
::: warning 🤔 思考题:
|
||||
为什么上面的 getPassage 函数使用了函数指针这种语法?
|
||||
|
||||
函数指针和指针函数有什么区别?
|
||||
:::
|
||||
|
||||
为了使整个画面完整,最好扩展前面生成的地图,我们可以用虚线表示“明显”的通道。
|
||||
|
||||
```awk
|
||||
BEGIN { print "digraph map {"; }
|
||||
/^- / { outputEdges(); delete a; }
|
||||
/^[ \t]/ { a[$1] = $2; }
|
||||
END { outputEdges(); print "}"; }
|
||||
|
||||
function outputEdges()
|
||||
{
|
||||
outputEdge(a["location"], a["destination"], "");
|
||||
outputEdge(a["location"], a["prospect"], " [style=dashed]");
|
||||
}
|
||||
|
||||
function outputEdge(from, to, style)
|
||||
{
|
||||
if (from && to) print "\t" from " -> " to style;
|
||||
}
|
||||
```
|
||||
|
||||
注意:
|
||||
|
||||
- 尽量不要太担心浪费仅在某些类型的对象中使用的属性上的内存空间(例如,*textGo*仅用于通道),或者许多重复的字符串文本。
|
||||
- 为了演示属性 prospect 的使用,我们使洞穴无法访问。当您查看新*地图时,*这一点立即变得很明显。进入洞穴的箭头是虚线的,这意味着这是一个虚假的通道,但不是实际的通道。请放心,洞穴将在下一章重新开放。
|
||||
- 请注意,更详细的描述往往需要一个更大的字典(更多的对象,更多的标签)。例如,命令 look silver coin 现在返回 "该硬币的正面有一只鹰"。玩家通过输入一个命令 look eagle 来查看银币,但程序并不知道鹰是什么意思 (显然这样子是不行的)。
|
||||
|
||||
输出样例
|
||||
|
||||
Welcome to Little Cave Adventure.
|
||||
You are in an open field.
|
||||
You see:
|
||||
a silver coin
|
||||
a burly guard
|
||||
a cave entrance to the east
|
||||
dense forestall around
|
||||
|
||||
--> look guard
|
||||
The guard is a really big fellow.
|
||||
|
||||
--> get guard
|
||||
That is way too heavy.
|
||||
|
||||
--> get coin
|
||||
You pick up a silver coin.
|
||||
|
||||
--> inventory
|
||||
You have:
|
||||
a silver coin
|
||||
|
||||
--> give coin
|
||||
You give a silver coin to a burly guard.
|
||||
|
||||
--> look guard
|
||||
The guard is a really big fellow.
|
||||
He has:
|
||||
a silver coin
|
||||
|
||||
--> go cave
|
||||
The guard stops you from walking into the cave.
|
||||
|
||||
--> go north
|
||||
Dense forest is blocking the way.
|
||||
|
||||
--> quit
|
||||
|
||||
Bye!
|
||||
322
2023旧版内容/3.编程思维体系构建/3.4.6.11.设置条件.md
Normal file
@@ -0,0 +1,322 @@
|
||||
# 11.设置条件
|
||||
|
||||
到目前为止,所有对象的属性都是数据:文本、数字。但属性也可以是代码。
|
||||
|
||||
在上一章中,我们通过关闭洞穴入口(进入洞穴的通道)来限制玩家的行动自由。这已经让游戏变得更有挑战性了,但这并没有带来多少谜题,除非我们为玩家提供一个微小的可能性来打开通道。真正的挑战应该是让玩家找到通道打开的条件。
|
||||
|
||||
让我们举一个简单的例子。为了越过守卫进入山洞,玩家必须杀死或贿赂守卫(或两者兼而有之,这很有价值)。换句话说:
|
||||
|
||||
- 当警卫死亡时(HP=0),入口开放
|
||||
- 当警卫拿着银币 (贿赂警卫) 时,入口开放
|
||||
- 两者都不是,入口关闭
|
||||
|
||||
打开一个封闭的通道(在这里是进入洞穴)涉及到改变一些属性值:
|
||||
|
||||
- 目的地从 NULL(空地点) 变为洞穴
|
||||
- **textGo**从 "警卫阻止你...... "改为 "你走进山洞"
|
||||
- 在一些特殊情况下,描述和细节不需要改变。但对于一个门洞或栅栏,其中之一(或两者)通常会包含一些从 "开放 "到 "关闭 "的文字。
|
||||
|
||||
有许多方法来实现这一目标。在这里,我们将讨论一种简单、可维护和通用的方法。
|
||||
|
||||
首先,我们定义了两个独立的通道:一个代表开放通道,另一个代表封闭通道。除了上面列出的那些,这两条通道在每一个属性上都是相同的。(在你下面看到的生成的地图中,注意有两个箭头通向洞穴;一个是实线,一个是虚线)。
|
||||
|
||||
接下来,我们引入一个名为条件的新属性,它决定了某个对象是否存在。这两个通道将被赋予互斥的条件,因此在任何时候都只能有一个存在。
|
||||
|
||||
每个条件将被实现为一个布尔函数:**TRUE**意味着该对象存在,**FALSE**意味着它不存在。
|
||||
|
||||
```c
|
||||
bool intoCaveIsOpen(void)
|
||||
{
|
||||
return guard->health == 0 || silver->location == guard;
|
||||
}
|
||||
|
||||
bool intoCaveIsClosed(void)
|
||||
{
|
||||
return guard->health > 0 && silver->location != guard;
|
||||
}
|
||||
```
|
||||
|
||||
::: warning 🤔 思考题:你能仿照上面例子自己写一些条件函数吗?
|
||||
:::
|
||||
|
||||
新的属性条件是一个指向这样一个函数的指针。
|
||||
|
||||
```c
|
||||
bool (*condition)(void);
|
||||
```
|
||||
|
||||
接下来,我们可以立即开始为 object.txt 中的新属性分配函数。
|
||||
|
||||
## object.txt
|
||||
|
||||
```txt
|
||||
- intoCave
|
||||
condition intoCaveIsOpen
|
||||
description "a cave entrance to the east"
|
||||
tags "east", "entrance"
|
||||
location field
|
||||
destination cave
|
||||
detail "The entrance is just a narrow opening in a small outcrop.\n"
|
||||
textGo "You walk into the cave.\n"
|
||||
|
||||
- intoCaveBlocked
|
||||
condition intoCaveIsClosed
|
||||
description "a cave entrance to the east"
|
||||
tags "east", "entrance"
|
||||
location field
|
||||
prospect cave
|
||||
detail "The entrance is just a narrow opening in a small outcrop.\n"
|
||||
textGo "The guard stops you from walking into the cave.\n"
|
||||
```
|
||||
|
||||
::: warning 🤔 思考题:尝试自己实现上面的伪代码
|
||||
:::
|
||||
|
||||
这两个 "条件 "函数是如此具体,每一个条件函数都只用这一次。现在,我们可以在我们需要的地方定义这些函数。许多编程语言都支持匿名函数,像这样:
|
||||
|
||||
```txt
|
||||
- intoCave
|
||||
condition { return guard->health == 0 || silver->location == guard; }
|
||||
...
|
||||
|
||||
- intoCaveBlocked
|
||||
condition { return guard->health > 0 && silver->location != guard; }
|
||||
...
|
||||
```
|
||||
|
||||
所以现在我们可以把额外的段落和条件添加到 object.txt 中,就像前面解释的那样。
|
||||
|
||||
## new object.txt
|
||||
|
||||
```txt
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
|
||||
typedef struct object {
|
||||
bool (*condition)(void);
|
||||
const char *description;
|
||||
const char **tags;
|
||||
struct object *location;
|
||||
struct object *destination;
|
||||
struct object *prospect;
|
||||
const char *details;
|
||||
const char *contents;
|
||||
const char *textGo;
|
||||
int weight;
|
||||
int capacity;
|
||||
int health;
|
||||
} OBJECT;
|
||||
|
||||
extern OBJECT objs[];
|
||||
|
||||
- field
|
||||
description "an open field"
|
||||
tags "field"
|
||||
details "The field is a nice and quiet place under a clear blue sky."
|
||||
capacity 9999
|
||||
|
||||
- cave
|
||||
description "a little cave"
|
||||
tags "cave"
|
||||
details "The cave is just a cold, damp, rocky chamber."
|
||||
capacity 9999
|
||||
|
||||
- silver
|
||||
description "a silver coin"
|
||||
tags "silver", "coin", "silver coin"
|
||||
location field
|
||||
details "The coin has an eagle on the obverse."
|
||||
weight 1
|
||||
|
||||
- gold
|
||||
description "a gold coin"
|
||||
tags "gold", "coin", "gold coin"
|
||||
location cave
|
||||
details "The shiny coin seems to be a rare and priceless artefact."
|
||||
weight 1
|
||||
|
||||
- guard
|
||||
description "a burly guard"
|
||||
tags "guard", "burly guard"
|
||||
location field
|
||||
details "The guard is a really big fellow."
|
||||
contents "He has"
|
||||
health 100
|
||||
capacity 20
|
||||
|
||||
- player
|
||||
description "yourself"
|
||||
tags "yourself"
|
||||
location field
|
||||
details "You would need a mirror to look at yourself."
|
||||
contents "You have"
|
||||
health 100
|
||||
capacity 20
|
||||
|
||||
- intoCave
|
||||
condition { return guard->health == 0 || silver->location == guard; }
|
||||
description "a cave entrance to the east"
|
||||
tags "east", "entrance"
|
||||
location field
|
||||
destination cave
|
||||
details "The entrance is just a narrow opening in a small outcrop."
|
||||
textGo "You walk into the cave."
|
||||
|
||||
- intoCaveBlocked
|
||||
condition { return guard->health > 0 && silver->location != guard; }
|
||||
description "a cave entrance to the east"
|
||||
tags "east", "entrance"
|
||||
location field
|
||||
prospect cave
|
||||
details "The entrance is just a narrow opening in a small outcrop."
|
||||
textGo "The guard stops you from walking into the cave."
|
||||
|
||||
- exitCave
|
||||
description "an exit to the west"
|
||||
tags "west", "exit"
|
||||
location cave
|
||||
destination field
|
||||
details "Sunlight pours in through an opening in the cave's wall."
|
||||
textGo "You walk out of the cave."
|
||||
|
||||
- wallField
|
||||
description "dense forest all around"
|
||||
tags "west", "north", "south", "forest"
|
||||
location field
|
||||
details "The field is surrounded by trees and undergrowth."
|
||||
textGo "Dense forest is blocking the way."
|
||||
|
||||
- wallCave
|
||||
description "solid rock all around"
|
||||
tags "east", "north", "south", "rock"
|
||||
location cave
|
||||
details "Carved in stone is a secret password 'abccb'."
|
||||
textGo "Solid rock is blocking the way."
|
||||
```
|
||||
|
||||
::: warning 🤔 思考题:尝试自己实现这些功能,并看看与你之前设计的有何不同
|
||||
:::
|
||||
|
||||
为了使这些条件发挥作用,我们需要调整函数 isHolding 和 getDistance。
|
||||
|
||||
## misc.c
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
#include "misc.h"
|
||||
|
||||
bool isHolding(OBJECT *container, OBJECT *obj)
|
||||
{
|
||||
return validObject(obj) && obj->location == container;
|
||||
}
|
||||
|
||||
OBJECT *getPassage(OBJECT *from, OBJECT *to)
|
||||
{
|
||||
if (from != NULL && to != NULL)
|
||||
{
|
||||
OBJECT *obj;
|
||||
for (obj = objs; obj < endOfObjs; obj++)
|
||||
{
|
||||
if (isHolding(from, obj) && obj->prospect == to)
|
||||
{
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
DISTANCE getDistance(OBJECT *from, OBJECT *to)
|
||||
{
|
||||
return to == NULL ? distUnknownObject :
|
||||
!validObject(to) ? distNotHere :
|
||||
to == from ? distSelf :
|
||||
isHolding(from, to) ? distHeld :
|
||||
isHolding(to, from) ? distLocation :
|
||||
isHolding(from->location, to) ? distHere :
|
||||
isHolding(from, to->location) ? distHeldContained :
|
||||
isHolding(from->location, to->location) ? distHereContained :
|
||||
getPassage(from->location, to) != NULL ? distOverthere :
|
||||
distNotHere;
|
||||
}
|
||||
|
||||
OBJECT *actorHere(void)
|
||||
{
|
||||
OBJECT *obj;
|
||||
for (obj = objs; obj < endOfObjs; obj++)
|
||||
{
|
||||
if (isHolding(player->location, obj) && obj != player &&
|
||||
obj->health > 0)
|
||||
{
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int listObjectsAtLocation(OBJECT *location)
|
||||
{
|
||||
int count = 0;
|
||||
OBJECT *obj;
|
||||
for (obj = objs; obj < endOfObjs; obj++)
|
||||
{
|
||||
if (obj != player && isHolding(location, obj))
|
||||
{
|
||||
if (count++ == 0)
|
||||
{
|
||||
printf("%s:\n", location->contents);
|
||||
}
|
||||
printf("%s\n", obj->description);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
```
|
||||
|
||||
::: warning 🤔 思考题:想想我们调整了什么
|
||||
:::
|
||||
|
||||
注意:
|
||||
|
||||
1. 警卫不可能会死,所以可以说我们的条件函数中的**HP**是很无用的。当然,这很容易通过添加一个 kill 命令来解决,见第 20 章。
|
||||
2. 这两个条件函数是互补的;它们有资格成为重复的代码。为了消除这一点,我们可能决定让一个函数调用另一个函数(用'!'操作符来否定结果)。一个匿名函数没有(稳定的)名字,但我们可以用它的对象来指代它。我们可以用 intoCaveBlocked 的条件函数代替。
|
||||
3. 为了简单起见,条件函数没有参数。实际上,传递一个参数 OBJECT *obj 可能更好;这使得编写更多的通用条件函数成为可能,可以在多个对象中重复使用。
|
||||
4. 在理论上,任何对象都可以成为 "条件"。在下一章,你可以看到一个类似的技术被应用于此。
|
||||
|
||||
::: warning 🤔 思考题:想一想上面第二点要怎么用 C 来实现?
|
||||
:::
|
||||
|
||||
输出样例
|
||||
|
||||
Welcome to Little Cave Adventure.
|
||||
You are in an open field.
|
||||
You see:
|
||||
a silver coin
|
||||
a burly guard
|
||||
a cave entrance to the east
|
||||
dense forest all around
|
||||
|
||||
--> go entrance
|
||||
The guard stops you from walking into the cave.
|
||||
|
||||
--> get coin
|
||||
You pick up a silver coin.
|
||||
|
||||
--> give coin
|
||||
You give a silver coin to a burly guard.
|
||||
|
||||
--> go entrance
|
||||
You walk into the cave.
|
||||
|
||||
You are in a little cave.
|
||||
You see:
|
||||
a gold coin
|
||||
an exit to the west
|
||||
solid rock all around
|
||||
|
||||
--> quit
|
||||
|
||||
Bye!
|
||||
673
2023旧版内容/3.编程思维体系构建/3.4.6.12.开启关闭.md
Normal file
@@ -0,0 +1,673 @@
|
||||
# 12.开启关闭
|
||||
|
||||
在上一章中,我们使用 "条件 "函数来使对象消失。当然,还有一个更简单的方法来实现同样的目的:只要清除对象的位置属性就可以了!
|
||||
|
||||
洞口是一个典型的例子,条件函数在那里工作得特别好。这是因为入口受到其他对象(守卫和银币)中的属性的影响;我们可以使用函数使得所有的逻辑都能保持一致。
|
||||
|
||||
让我们举一个更直接的例子。假设山洞有一扇门通向一个密室。只是一个简单的门洞,玩家可以打开和关闭。就像前一章一样,我们将使用两个对象来表示这个通道;一个表示打开的门,另一个表示门关闭时。
|
||||
|
||||
```txt
|
||||
- backroom
|
||||
description "a backroom"
|
||||
tags "backroom"
|
||||
details "The room is dusty and messy.\n"
|
||||
|
||||
- openDoorToBackroom
|
||||
description "an open door to the south"
|
||||
tags "south", "door", "doorway"
|
||||
destination backroom
|
||||
details "The door is open.\n"
|
||||
textGo "You walk through the door into the backroom.\n"
|
||||
|
||||
- closedDoorToBackroom
|
||||
description "a closed door to the south"
|
||||
tags "south", "door", "doorway"
|
||||
location cave
|
||||
prospect backroom
|
||||
details "The door is closed.\n"
|
||||
textGo "The door is closed.\n"
|
||||
```
|
||||
|
||||
::: warning 🤔 思考题:尝试自己用 C 语言实现
|
||||
:::
|
||||
|
||||
自然,门也应该能从另一侧进入。
|
||||
|
||||
```txt
|
||||
- openDoorToCave
|
||||
description "an open door to the north"
|
||||
tags "north", "door", "doorway"
|
||||
destination cave
|
||||
details "The door is open.\n"
|
||||
textGo "You walk through the door into the cave.\n"
|
||||
|
||||
- closedDoorToCave
|
||||
description "a closed door to the north"
|
||||
tags "north", "door", "doorway"
|
||||
location backroom
|
||||
prospect cave
|
||||
details "The door is closed.\n"
|
||||
textGo "The door is closed.\n"
|
||||
```
|
||||
|
||||
注意我们只给封闭的门洞一个位置,开放的门洞没有。所以一开始,门是关闭的(因此你在右边看到的生成的地图中,山洞和密室之间有虚线箭头)。要打开门,我们所要做的就是交换位置。
|
||||
|
||||
```c
|
||||
openDoorToBackroom->location = cave;
|
||||
closedDoorToBackroom->location = NULL;
|
||||
openDoorToCave->location = backroom;
|
||||
closedDoorToCave->location = NULL;
|
||||
```
|
||||
|
||||
接下来,让我们创建一个辅助函数来适应这种情况。
|
||||
|
||||
```c
|
||||
void swapLocations(OBJECT *obj1, OBJECT *obj2)
|
||||
{
|
||||
OBJECT *tmp = obj1->location;
|
||||
obj1->location = obj2->location;
|
||||
obj2->location = tmp;
|
||||
}
|
||||
```
|
||||
|
||||
现在可以用下面的语句来打开这扇门;一旦它被打开,同样的语句将再次关闭它。
|
||||
|
||||
```c
|
||||
swapLocations(openDoorToBackroom, closedDoorToBackroom);
|
||||
swapLocations(openDoorToCave, closedDoorToCave);
|
||||
```
|
||||
|
||||
函数 swapLocations 不依赖于固定的位置,因为它在两个对象之间来回传递当前位置。
|
||||
|
||||
当相关对象是可移动的时候,辅助功能就特别方便。例如,一个盒子可以被打开和关闭,但它也是一个可以被拿起并移动到其他地方的物品。换句话说,它的位置是不固定的。
|
||||
|
||||
当然,一个盒子不是一个通道:玩家总是在盒子外面。所以一对物体就够了,也就是说,我们调用一次 swapLocations 函数就够了。
|
||||
|
||||
```c
|
||||
swapLocations(openBox, closedBox);
|
||||
```
|
||||
|
||||
这或多或少是我们实现一些新命令 open 和 close 所需要的。下面是 open 的一个简单实现;close 的实现也类似。
|
||||
|
||||
```c
|
||||
OBJECT *obj = parseObject(noun);
|
||||
if (obj == closedDoorToBackRoom || obj == closedDoorToCave)
|
||||
{
|
||||
swapLocations(openDoorToBackroom, closedDoorToBackroom);
|
||||
swapLocations(openDoorToCave, closedDoorToCave);
|
||||
printf("OK.\n");
|
||||
}
|
||||
else if (obj == closedBox)
|
||||
{
|
||||
swapLocations(openBox, closedBox);
|
||||
printf("OK.\n");
|
||||
}
|
||||
else if (obj == openDoorToBackRoom || obj == openDoorToCave || obj == openBox)
|
||||
{
|
||||
printf("That is already open.\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("That cannot be opened.\n");
|
||||
}
|
||||
```
|
||||
|
||||
::: warning 🤔 思考题:你能不能仿照上面的代码实现 close 功能?
|
||||
:::
|
||||
|
||||
为了使事情稍微复杂一些,我们可以在门上或盒子上加一把锁。这需要(至少)三个相互排斥的对象;每个可能的状态都有一个:打开、关闭和锁定。但是我们仍然可以使用同一个函数来交换对象的位置。例如,这里是如何解锁一个上锁的盒子;反之亦然。
|
||||
|
||||
```c
|
||||
swapLocations(closedBox, lockedBox);
|
||||
```
|
||||
|
||||
但这仅仅是不够的,我们对命令 open 的实现必须进行扩展,以处理新的对象 lockedBox。
|
||||
|
||||
```c
|
||||
...
|
||||
else if (obj == lockedBox)
|
||||
{
|
||||
printf("You can't, it is locked.\n");
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
显然,代码的行数与游戏中的门(以及盒子和其他可以打开的物体)的数量成正比。因此,如果你的游戏有不止几扇门,那么选择一个更通用的解决方案是个好主意。顺便说一下,这对每一个命令都是适用的:当它涉及到许多物体时,尽量写通用代码;但当你处理一两个特殊情况时,就坚持使用直接的、专门的代码。
|
||||
|
||||
::: warning 🤔 思考题:
|
||||
我们可以使用什么方法来解决这个问题?
|
||||
|
||||
提示:C++ 中的模板功能(这只是一种选择)
|
||||
|
||||
下面我们将揭晓答案
|
||||
:::
|
||||
|
||||
通用代码通常带有数据驱动的方法。换句话说,我们需要向我们的对象结构添加一个或多个属性。在这种特殊情况下,我们将为我们希望支持的每个命令添加一个函数指针:打开、关闭、锁定和解锁。
|
||||
|
||||
## object.txt
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
#include "toggle.h"
|
||||
|
||||
typedef struct object {
|
||||
bool (*condition)(void);
|
||||
const char *description;
|
||||
const char **tags;
|
||||
struct object *location;
|
||||
struct object *destination;
|
||||
struct object *prospect;
|
||||
const char *details;
|
||||
const char *contents;
|
||||
const char *textGo;
|
||||
int weight;
|
||||
int capacity;
|
||||
int health;
|
||||
void (*open)(void);
|
||||
void (*close)(void);
|
||||
void (*lock)(void);
|
||||
void (*unlock)(void);
|
||||
} OBJECT;
|
||||
|
||||
extern OBJECT objs[];
|
||||
|
||||
- field
|
||||
description "an open field"
|
||||
tags "field"
|
||||
details "The field is a nice and quiet place under a clear blue sky."
|
||||
capacity 9999
|
||||
|
||||
- cave
|
||||
description "a little cave"
|
||||
tags "cave"
|
||||
details "The cave is just a cold, damp, rocky chamber."
|
||||
capacity 9999
|
||||
|
||||
- silver
|
||||
description "a silver coin"
|
||||
tags "silver", "coin", "silver coin"
|
||||
location field
|
||||
details "The coin has an eagle on the obverse."
|
||||
weight 1
|
||||
|
||||
- gold
|
||||
description "a gold coin"
|
||||
tags "gold", "coin", "gold coin"
|
||||
location openBox
|
||||
details "The shiny coin seems to be a rare and priceless artefact."
|
||||
weight 1
|
||||
|
||||
- guard
|
||||
description "a burly guard"
|
||||
tags "guard", "burly guard"
|
||||
location field
|
||||
details "The guard is a really big fellow."
|
||||
contents "He has"
|
||||
health 100
|
||||
capacity 20
|
||||
|
||||
- player
|
||||
description "yourself"
|
||||
tags "yourself"
|
||||
location field
|
||||
details "You would need a mirror to look at yourself."
|
||||
contents "You have"
|
||||
health 100
|
||||
capacity 20
|
||||
|
||||
- intoCave
|
||||
condition { return guard->health == 0 || silver->location == guard; }
|
||||
description "a cave entrance to the east"
|
||||
tags "east", "entrance"
|
||||
location field
|
||||
destination cave
|
||||
details "The entrance is just a narrow opening in a small outcrop."
|
||||
textGo "You walk into the cave."
|
||||
open isAlreadyOpen
|
||||
|
||||
- intoCaveBlocked
|
||||
condition { return guard->health > 0 && silver->location != guard; }
|
||||
description "a cave entrance to the east"
|
||||
tags "east", "entrance"
|
||||
location field
|
||||
prospect cave
|
||||
details "The entrance is just a narrow opening in a small outcrop."
|
||||
textGo "The guard stops you from walking into the cave."
|
||||
open isAlreadyOpen
|
||||
|
||||
- exitCave
|
||||
description "an exit to the west"
|
||||
tags "west", "exit"
|
||||
location cave
|
||||
destination field
|
||||
details "Sunlight pours in through an opening in the cave's wall."
|
||||
textGo "You walk out of the cave."
|
||||
open isAlreadyOpen
|
||||
|
||||
- wallField
|
||||
description "dense forest all around"
|
||||
tags "west", "north", "south", "forest"
|
||||
location field
|
||||
details "The field is surrounded by trees and undergrowth."
|
||||
textGo "Dense forest is blocking the way."
|
||||
|
||||
- wallCave
|
||||
description "solid rock all around"
|
||||
tags "east", "north", "rock"
|
||||
location cave
|
||||
details "Carved in stone is a secret password 'abccb'."
|
||||
textGo "Solid rock is blocking the way."
|
||||
|
||||
- backroom
|
||||
description "a backroom"
|
||||
tags "backroom"
|
||||
details "The room is dusty and messy."
|
||||
capacity 9999
|
||||
|
||||
- wallBackroom
|
||||
description "solid rock all around"
|
||||
tags "east", "west", "south", "rock"
|
||||
location backroom
|
||||
details "Trendy wallpaper covers the rock walls."
|
||||
textGo "Solid rock is blocking the way."
|
||||
|
||||
- openDoorToBackroom
|
||||
description "an open door to the south"
|
||||
tags "south", "door", "doorway"
|
||||
destination backroom
|
||||
details "The door is open."
|
||||
textGo "You walk through the door into a backroom."
|
||||
open isAlreadyOpen
|
||||
close toggleDoorToBackroom
|
||||
|
||||
- closedDoorToBackroom
|
||||
description "a closed door to the south"
|
||||
tags "south", "door", "doorway"
|
||||
location cave
|
||||
prospect backroom
|
||||
details "The door is closed."
|
||||
textGo "The door is closed."
|
||||
open toggleDoorToBackroom
|
||||
close isAlreadyClosed
|
||||
|
||||
- openDoorToCave
|
||||
description "an open door to the north"
|
||||
tags "north", "door", "doorway"
|
||||
destination cave
|
||||
details "The door is open."
|
||||
textGo "You walk through the door into the cave."
|
||||
open isAlreadyOpen
|
||||
close toggleDoorToCave
|
||||
|
||||
- closedDoorToCave
|
||||
description "a closed door to the north"
|
||||
tags "north", "door", "doorway"
|
||||
location backroom
|
||||
prospect cave
|
||||
details "The door is closed."
|
||||
textGo "The door is closed."
|
||||
open toggleDoorToCave
|
||||
close isAlreadyClosed
|
||||
|
||||
- openBox
|
||||
description "a wooden box"
|
||||
tags "box", "wooden box"
|
||||
details "The box is open."
|
||||
weight 5
|
||||
capacity 10
|
||||
open isAlreadyOpen
|
||||
close toggleBox
|
||||
lock isStillOpen
|
||||
unlock isAlreadyOpen
|
||||
|
||||
- closedBox
|
||||
description "a wooden box"
|
||||
tags "box", "wooden box"
|
||||
details "The box is closed."
|
||||
weight 5
|
||||
open toggleBox
|
||||
close isAlreadyClosed
|
||||
lock toggleBoxLock
|
||||
unlock isAlreadyUnlocked
|
||||
|
||||
- lockedBox
|
||||
description "a wooden box"
|
||||
tags "box", "wooden box"
|
||||
location backroom
|
||||
details "The box is closed."
|
||||
weight 5
|
||||
open isStillLocked
|
||||
close isAlreadyClosed
|
||||
lock isAlreadyLocked
|
||||
unlock toggleBoxLock
|
||||
|
||||
- keyForBox
|
||||
description "a tiny key"
|
||||
tags "key", "tiny key"
|
||||
location cave
|
||||
details "The key is really small and shiny."
|
||||
weight 1
|
||||
```
|
||||
|
||||
注意:
|
||||
|
||||
- 第 89 行:乍一看,isAlreadyOpen 在这里似乎不合适;从技术上讲,intoCaveBlocked 是一个封闭的通道。但从故事上讲,它是一个不错的开场白。
|
||||
- 第 169、180、191 行。如果你喜欢沉重的宝箱而不是盒子,那么你所要做的就是增加重量(并相应调整相关文字和标签)。
|
||||
|
||||
为了避免重复的代码,我们这次特意没有使用匿名函数。相反,我们将在一个单独的模块中实现必要的逻辑。函数 swapLocations 也在其中,这不过是一个稍微扩展的版本,它也会向用户输出反馈。
|
||||
|
||||
## toggle.h
|
||||
|
||||
```c
|
||||
extern void cannotBeOpened(void);
|
||||
extern void cannotBeClosed(void);
|
||||
extern void cannotBeLocked(void);
|
||||
extern void cannotBeUnlocked(void);
|
||||
|
||||
extern void isAlreadyOpen(void);
|
||||
extern void isAlreadyClosed(void);
|
||||
extern void isAlreadyLocked(void);
|
||||
extern void isAlreadyUnlocked(void);
|
||||
|
||||
extern void isStillOpen(void);
|
||||
extern void isStillLocked(void);
|
||||
|
||||
extern void toggleDoorToBackroom(void);
|
||||
extern void toggleDoorToCave(void);
|
||||
extern void toggleBox(void);
|
||||
extern void toggleBoxLock(void);
|
||||
```
|
||||
|
||||
## toggle.c
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
|
||||
static void swapLocations(const char *verb1, OBJECT *obj1,
|
||||
const char *verb2, OBJECT *obj2)
|
||||
{
|
||||
OBJECT *tmp = obj1->location;
|
||||
OBJECT *obj = tmp != NULL ? obj1 : obj2;
|
||||
const char *verb = tmp != NULL ? verb1 : verb2;
|
||||
obj1->location = obj2->location;
|
||||
obj2->location = tmp;
|
||||
if (verb != NULL) printf("You %s %s.\n", verb, obj->description);
|
||||
}
|
||||
|
||||
void cannotBeOpened(void) { printf("That cannot be opened.\n"); }
|
||||
void cannotBeClosed(void) { printf("That cannot be closed.\n"); }
|
||||
void cannotBeLocked(void) { printf("That cannot be locked.\n"); }
|
||||
void cannotBeUnlocked(void) { printf("That cannot be unlocked.\n"); }
|
||||
|
||||
void isAlreadyOpen(void) { printf("That is already open.\n"); }
|
||||
void isAlreadyClosed(void) { printf("That is already closed.\n"); }
|
||||
void isAlreadyLocked(void) { printf("That is already locked.\n"); }
|
||||
void isAlreadyUnlocked(void) { printf("That is already unlocked.\n"); }
|
||||
|
||||
void isStillOpen(void) { printf("That is still open.\n"); }
|
||||
void isStillLocked(void) { printf("That is locked.\n"); }
|
||||
|
||||
void toggleDoorToBackroom(void)
|
||||
{
|
||||
swapLocations(NULL, openDoorToCave, NULL, closedDoorToCave);
|
||||
swapLocations("close", openDoorToBackroom, "open", closedDoorToBackroom);
|
||||
}
|
||||
|
||||
void toggleDoorToCave(void)
|
||||
{
|
||||
swapLocations(NULL, openDoorToBackroom, NULL, closedDoorToBackroom);
|
||||
swapLocations("close", openDoorToCave, "open", closedDoorToCave);
|
||||
}
|
||||
|
||||
void toggleBox(void)
|
||||
{
|
||||
swapLocations("close", openBox, "open", closedBox);
|
||||
}
|
||||
|
||||
void toggleBoxLock(void)
|
||||
{
|
||||
if (keyForBox->location == player)
|
||||
{
|
||||
swapLocations("lock", closedBox, "unlock", lockedBox);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("You don't have a key.\n");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
正如前面所宣布的,打开、关闭、锁定和解锁这四个命令的实现是完全通用的。
|
||||
|
||||
## openclose.h
|
||||
|
||||
```c
|
||||
extern void executeOpen(const char *noun);
|
||||
extern void executeClose(const char *noun);
|
||||
extern void executeLock(const char *noun);
|
||||
extern void executeUnlock(const char *noun);
|
||||
```
|
||||
|
||||
## openclose.c
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
#include "reach.h"
|
||||
|
||||
void executeOpen(const char *noun)
|
||||
{
|
||||
OBJECT *obj = reachableObject("what you want to open", noun);
|
||||
if (obj != NULL) (*obj->open)();
|
||||
}
|
||||
|
||||
void executeClose(const char *noun)
|
||||
{
|
||||
OBJECT *obj = reachableObject("what you want to close", noun);
|
||||
if (obj != NULL) (*obj->close)();
|
||||
}
|
||||
|
||||
void executeLock(const char *noun)
|
||||
{
|
||||
OBJECT *obj = reachableObject("what you want to lock", noun);
|
||||
if (obj != NULL) (*obj->lock)();
|
||||
}
|
||||
|
||||
void executeUnlock(const char *noun)
|
||||
{
|
||||
OBJECT *obj = reachableObject("what you want to unlock", noun);
|
||||
if (obj != NULL) (*obj->unlock)();
|
||||
}
|
||||
```
|
||||
|
||||
上面,我们使用了一个通用函数 reachableObject 来处理不在这里的对象;其实现见下文。这样,我们就不必把同样的代码写四遍(每个执行函数写一遍)。更多的命令将在第 15 章中加入;这些命令将受益于同样的函数。
|
||||
|
||||
## reach.h
|
||||
|
||||
```c
|
||||
extern OBJECT *reachableObject(const char *intention, const char *noun);
|
||||
```
|
||||
|
||||
## reach.c
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
#include "misc.h"
|
||||
#include "noun.h"
|
||||
|
||||
OBJECT *reachableObject(const char *intention, const char *noun)
|
||||
{
|
||||
OBJECT *obj = getVisible(intention, noun);
|
||||
switch (getDistance(player, obj))
|
||||
{
|
||||
case distSelf:
|
||||
printf("You should not be doing that to yourself.\n");
|
||||
break;
|
||||
case distHeldContained:
|
||||
case distHereContained:
|
||||
printf("You would have to get it from %s first.\n",
|
||||
obj->location->description);
|
||||
break;
|
||||
case distOverthere:
|
||||
printf("Too far away, move closer please.\n");
|
||||
break;
|
||||
case distNotHere:
|
||||
case distUnknownObject:
|
||||
// already handled by getVisible
|
||||
break;
|
||||
default:
|
||||
return obj;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
```
|
||||
|
||||
同样,我们也要对 parsexec.c 进行补充
|
||||
|
||||
## parsexec.c
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "location.h"
|
||||
#include "inventory.h"
|
||||
#include "openclose.h"
|
||||
|
||||
bool parseAndExecute(char *input)
|
||||
{
|
||||
char *verb = strtok(input, " \n");
|
||||
char *noun = strtok(NULL, "\n");
|
||||
if (verb != NULL)
|
||||
{
|
||||
if (strcmp(verb, "quit") == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (strcmp(verb, "look") == 0)
|
||||
{
|
||||
executeLook(noun);
|
||||
}
|
||||
else if (strcmp(verb, "go") == 0)
|
||||
{
|
||||
executeGo(noun);
|
||||
}
|
||||
else if (strcmp(verb, "get") == 0)
|
||||
{
|
||||
executeGet(noun);
|
||||
}
|
||||
else if (strcmp(verb, "drop") == 0)
|
||||
{
|
||||
executeDrop(noun);
|
||||
}
|
||||
else if (strcmp(verb, "give") == 0)
|
||||
{
|
||||
executeGive(noun);
|
||||
}
|
||||
else if (strcmp(verb, "ask") == 0)
|
||||
{
|
||||
executeAsk(noun);
|
||||
}
|
||||
else if (strcmp(verb, "inventory") == 0)
|
||||
{
|
||||
executeInventory();
|
||||
}
|
||||
else if (strcmp(verb, "open") == 0)
|
||||
{
|
||||
executeOpen(noun);
|
||||
}
|
||||
else if (strcmp(verb, "close") == 0)
|
||||
{
|
||||
executeClose(noun);
|
||||
}
|
||||
else if (strcmp(verb, "lock") == 0)
|
||||
{
|
||||
executeLock(noun);
|
||||
}
|
||||
else if (strcmp(verb, "unlock") == 0)
|
||||
{
|
||||
executeUnlock(noun);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("I don't know how to '%s'.\n", verb);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
注意:
|
||||
|
||||
- 你可能已经注意到,object.txt 在本章中的大小几乎增加了一倍。我们已经可以向你保证,这只是一个开始。object.txt 是我们游戏数据的主要来源;一旦我们认真开始添加地点、物品和演员,行数很容易就会增加到数千行。
|
||||
- 除了门和锁之外,函数 swapLocation 还可以用于许多其他事情。在第 15 章,它将再次被使用,这次是用来打开和关闭灯。
|
||||
- 正如你在样本输出中所看到的,玩家可以从盒子里拿到金子,但他无法再把金子放回去!我们的解析器无法处理像把硬币放进盒子这样的 "复杂 "命令。因此,在下一章中,我们将编写一个全新的解析器:目前的双线实现急需更换!
|
||||
|
||||
输出样例
|
||||
|
||||
Welcome to Little Cave Adventure.
|
||||
You are in an open field.
|
||||
You see:
|
||||
a silver coin
|
||||
a burly guard
|
||||
a cave entrance to the east
|
||||
dense forest all around
|
||||
|
||||
--> get coin
|
||||
You pick up a silver coin.
|
||||
|
||||
--> give coin
|
||||
You give a silver coin to a burly guard.
|
||||
|
||||
--> go cave
|
||||
You walk into the cave.
|
||||
|
||||
You are in a little cave.
|
||||
You see:
|
||||
an exit to the west
|
||||
solid rock all around
|
||||
a closed door to the south
|
||||
a tiny key
|
||||
|
||||
--> get key
|
||||
You pick up a tiny key.
|
||||
|
||||
--> go south
|
||||
The door is closed.
|
||||
|
||||
--> open door
|
||||
You open a closed door to the south.
|
||||
|
||||
--> go south
|
||||
You walk through the door into a backroom.
|
||||
|
||||
You are in a backroom.
|
||||
You see:
|
||||
solid rock all around
|
||||
an open door to the north
|
||||
a wooden box
|
||||
|
||||
--> unlock box
|
||||
You unlock a wooden box.
|
||||
|
||||
--> open box
|
||||
You open a wooden box.
|
||||
|
||||
--> look box
|
||||
The box is open.
|
||||
You see:
|
||||
a gold coin
|
||||
|
||||
--> get gold
|
||||
You get a gold coin from a wooden box.
|
||||
|
||||
--> quit
|
||||
|
||||
Bye!
|
||||
525
2023旧版内容/3.编程思维体系构建/3.4.6.13.编写解析器.md
Normal file
@@ -0,0 +1,525 @@
|
||||
# 13.编写解析器
|
||||
|
||||
每个文本冒险都有一个解析器,但是解析器也有高下之分。一个简单的 "动词 - 名词 "解析器(就像我们从第二章开始一直使用的那个)对于一个精心设计的冒险游戏来说可能已经足够了。
|
||||
|
||||
然而,Infocom 已经证明,一个更高级的解析器确实有助于制作一个令人愉快的游戏。它不一定要通过图灵测试。
|
||||
|
||||
记住,它只是一个游戏。但是解析器应该使玩家能够以一种或多或少的自然方式来表达他的意图。
|
||||
|
||||
我们目前的分析器主要由两行代码组成,隐藏在 parsexec.c 中。
|
||||
|
||||
```c
|
||||
char *verb = strtok(input, " \n");
|
||||
char *noun = strtok(NULL, "\n");
|
||||
```
|
||||
|
||||
好吧,我们再加上将动词映射到命令的 strcmp 调用序列,以及将名词映射到对象的 noun.c 中的函数,但仅此而已。在过去的 12 章中,这个系统为我们提供了良好的服务,但它也有缺陷。
|
||||
|
||||
- 它只接受 "动词名词 "形式的简单命令;它不理解既有直接宾语又有间接宾语的句子,如把硬币放进盒子。
|
||||
- 它确实接受多字对象(如银币),但字与字之间的空格必须准确无误。我们的游戏拒绝银币和硬币之间的双空格。
|
||||
- 它是区分大小写的;"向北走 "的命令因为大写的 "G "而不被识别。
|
||||
|
||||
::: warning 🤔 思考题:你能想到有什么办法解决这些问题吗?
|
||||
:::
|
||||
|
||||
编写一个好的分析器并不是一件小事,但在这里我将给你一个相对简单的方法,我们将定义一个由模式列表组成的语法,类似于(但比)正则表达式要简单得多。
|
||||
|
||||
| look around | 仅仅是匹配而已。双空格、前导空格、尾部空格和大小写差异都被忽略了 |
|
||||
| ----------- | ------------------------------------------------------------------ |
|
||||
| go A | 匹配单词 go 和对象的标签(见第 5 章和第 9 章关于 "标签 "的解释)。 |
|
||||
| put A in B | 匹配单词 put 后面的标签,单词 in 和另一个标签。 |
|
||||
|
||||
为了解析用户的输入,我们将从上到下遍历模式列表,依次尝试将用户的输入与每个模式匹配。我们将在发现第一个匹配时停止。为了简单起见,我们将不使用回溯,尽管这可以在以后添加。
|
||||
|
||||
::: warning 🤔 思考题:如果我们使用回溯,那该怎么编写代码?
|
||||
:::
|
||||
|
||||
大写字母是我们语法中的非终端符号,它们可以匹配任何标签(任何对象)。当解析器在两个不同的标签(例如 "银币 "和 "银")之间进行选择时,较长的标签将被优先考虑。
|
||||
|
||||
然后,匹配的标签可以作为参数名词传递给其中一个执行函数。对于有一个以上名词的命令(在下一章介绍),参数传递变得有点不切实际。为了简单起见,我们将使用一个全局变量而不是参数(尽管全局变量的名声不好)。该变量将是一个字符串指针的数组。
|
||||
|
||||
```c
|
||||
const char *params[26];
|
||||
```
|
||||
|
||||
该数组有 26 个元素;字母表中的每个(大写)字母都有一个,这足以满足一个模式中多达 26 个不同的非终端。对于一个(匹配的)模式中的每个非终端,将通过在非终端的数组元素中填充一个指向该特定标签的指针来 "捕获 "一个匹配的标签。params[0](第一个数组元素)捕获非终端 "A",params[1]捕获 "B",以此类推。一个简单的宏定义可以用来找到属于某个非终端的数组元素。
|
||||
|
||||
```c
|
||||
#define paramByLetter(letter) (params + (letter) - 'A')
|
||||
```
|
||||
|
||||
注意:你可能会发现数组长度为 26 有点过头了,但它确实让我们省去了写一些边界检查代码的麻烦,以防止在出现畸形模式时出现缓冲区溢出。
|
||||
|
||||
现在,我们必须想出一个办法来处理缺失的或不被承认的名词。
|
||||
|
||||
假设用户打错了字,输入了 "go kave"。问题是,这个命令到底应不应该匹配 "go A "这个命令?如果我们不想出解决办法,那么这个命令将无法匹配任何其他命令,并最终进入一个通用的错误处理程序,它可能会回答 "我不知道如何去 kave "这样的话。这就失去了改进这些 "消极反应 "的所有机会;回答 "我不知道你要去哪里 "已经感觉更自然了。最好的办法是将所有关于命令去向的答复保持在函数 executeGo 内。
|
||||
|
||||
::: warning 🤔 停下来想一想,可以怎么解决这个问题?
|
||||
:::
|
||||
|
||||
有几种方法可以实现这一点,但最简单的方法是允许非终止符匹配任何东西。所以不仅仅是一个有效的标签,也包括完全的胡言乱语、空白或什么都没有这种语句。这种 "无效 "的输入将被捕获为一个空字符串("")。
|
||||
|
||||
在模式中间有这样一个 "松散 "的非终端,确实会使模式匹配过程复杂化;在匹配输入 "把 foo 放在盒子里 "和模式 "把 A 放在 B 里 "时,它需要回溯以正确对齐 "in "这个词。
|
||||
|
||||
为了简单起见,我们将只对出现在模式最末端的非终止符进行松散匹配。为了能够正确匹配上面的例子,我们需要两个独立的模式:
|
||||
|
||||
- 模式 "put A in B "将匹配有效的命令 put coin in box,以及无效的命令 put coin in booox 和 put coin in。注意,非终端 A 只匹配有效的标签(在这个例子中是硬币)。
|
||||
- 模式 "put A "匹配所有剩余的无效命令:put coin, put koin, put koin in box, put koin in booox 和一个裸 put。还有最初的例子(put foo in in box)。
|
||||
|
||||
模式的顺序在这里至关重要:如果 "放 A "在上面(意味着它将被首先尝试),那么它将消耗每个放的命令;一个有效的 "put coin in box "命令将被认为是无效的,因为没有 "硬币在盒子里 "的标签。
|
||||
|
||||
对于以多种形式出现的命令也是如此,比如说 look 这个命令:
|
||||
|
||||
- Look around
|
||||
- look(作为环顾四周的缩写)
|
||||
- Look A
|
||||
|
||||
前两个命令形式可以为任何顺序,但第三个必须在最后。
|
||||
|
||||
::: warning 🤔 思考题:你是否有办法解决这个问题?
|
||||
:::
|
||||
|
||||
是时候将其付诸行动了。我们将抛弃模块 parsexec.c 的现有内容,用一个新的函数 parseAndExecute 的实现来取代它,该函数使用一个模式列表,应该能够匹配我们到目前为止实现的每一条命令。每个模式都与一个执行相应命令的函数相联系。
|
||||
|
||||
## parsexec.c
|
||||
|
||||
```c
|
||||
extern bool parseAndExecute(const char *input);
|
||||
```
|
||||
|
||||
## parsexec.h
|
||||
|
||||
```c
|
||||
#include <ctype.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
#include "misc.h"
|
||||
#include "match.h"
|
||||
#include "location.h"
|
||||
#include "inventory.h"
|
||||
#include "openclose.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
const char *pattern;
|
||||
bool (*function)(void);
|
||||
} COMMAND;
|
||||
|
||||
static bool executeQuit(void)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool executeNoMatch(void)
|
||||
{
|
||||
const char *src = *params;
|
||||
int len;
|
||||
for (len = 0; src[len] != '\0' && !isspace(src[len]); len++);
|
||||
//计算玩家输入的第一个字符串的长度
|
||||
if (len > 0) printf("I don't know how to '%.*s'.\n", len, src);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseAndExecute(const char *input)
|
||||
{
|
||||
static const COMMAND commands[] =
|
||||
//模式数组
|
||||
{
|
||||
{ "quit" , executeQuit },
|
||||
{ "look" , executeLookAround },
|
||||
{ "look around" , executeLookAround },
|
||||
{ "look at A" , executeLook },
|
||||
{ "look A" , executeLook },
|
||||
{ "examine A" , executeLook },
|
||||
{ "go to A" , executeGo },
|
||||
{ "go A" , executeGo },
|
||||
{ "get A" , executeGet },
|
||||
{ "drop A" , executeDrop },
|
||||
{ "ask A" , executeAsk },
|
||||
{ "give A" , executeGive },
|
||||
{ "inventory" , executeInventory },
|
||||
{ "open A" , executeOpen },
|
||||
{ "close A" , executeClose },
|
||||
{ "lock A" , executeLock },
|
||||
{ "unlock A" , executeUnlock },
|
||||
{ "A" , executeNoMatch }
|
||||
};
|
||||
const COMMAND *cmd;
|
||||
for (cmd = commands; !matchCommand(input, cmd->pattern); cmd++);
|
||||
return (*cmd->function)();
|
||||
}
|
||||
```
|
||||
|
||||
最难的部分是函数 matchCommand 的实现。但正如你在下面看到的,这也可以在不到 100 行的代码中完成。
|
||||
|
||||
## match.h
|
||||
|
||||
```c
|
||||
#define MAX_PARAMS 26
|
||||
|
||||
extern const char *params[];
|
||||
|
||||
#define paramByLetter(letter) (params + (letter) - 'A')
|
||||
|
||||
extern bool matchCommand(const char *src, const char *pattern);
|
||||
```
|
||||
|
||||
## match.c
|
||||
|
||||
```c
|
||||
#include <ctype.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include "object.h"
|
||||
#include "misc.h"
|
||||
#include "match.h"
|
||||
|
||||
const char *params[MAX_PARAMS];
|
||||
|
||||
static const char *skipSpaces(const char *src)
|
||||
{
|
||||
while (isspace(*src)) src++;
|
||||
return src;
|
||||
}
|
||||
|
||||
static const char *matchSpaces(const char *src)
|
||||
{
|
||||
return *src == '\0' || isspace(*src) ? skipSpaces(src) : NULL;
|
||||
}
|
||||
|
||||
static const char *matchTerminal(const char *src, char terminal)
|
||||
{
|
||||
return terminal == ' ' ? matchSpaces(src) :
|
||||
tolower(*src) == tolower(terminal) ? src + 1 : NULL;
|
||||
}
|
||||
|
||||
static const char *matchTag(const char *src, const char *tag, bool atEnd)
|
||||
{
|
||||
while (src != NULL && *tag != '\0')
|
||||
{
|
||||
src = matchTerminal(src, *tag++);
|
||||
}
|
||||
return atEnd && src != NULL && *skipSpaces(src) != '\0' ? NULL : src;
|
||||
}
|
||||
|
||||
static const char *matchParam(const char *src, const char **par, bool loose)
|
||||
{
|
||||
const char *restOfSrc = loose ? src + strlen(src) : NULL;
|
||||
OBJECT *obj;
|
||||
for (obj = objs; obj < endOfObjs; obj++)
|
||||
{
|
||||
const char **tag;
|
||||
for (tag = obj->tags; *tag != NULL; tag++)
|
||||
{
|
||||
const char *behindMatch = matchTag(src, *tag, loose);
|
||||
if (behindMatch != NULL && strlen(*tag) > strlen(*par))
|
||||
{
|
||||
*par = *tag;
|
||||
restOfSrc = behindMatch;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (**par == '\0')
|
||||
{
|
||||
*par = src;
|
||||
}
|
||||
return restOfSrc;
|
||||
}
|
||||
|
||||
bool matchCommand(const char *src, const char *pattern)
|
||||
{
|
||||
const char **par;
|
||||
for (par = params; par < params + MAX_PARAMS; par++)
|
||||
{
|
||||
*par = "";
|
||||
}
|
||||
for (src = skipSpaces(src); src != NULL && *pattern != '\0'; pattern++)
|
||||
{
|
||||
src = isupper(*pattern)
|
||||
? matchParam(src, paramByLetter(*pattern), pattern[1] == '\0')
|
||||
: matchTerminal(src, *pattern);
|
||||
}
|
||||
return src != NULL && *skipSpaces(src) == '\0';
|
||||
}
|
||||
```
|
||||
|
||||
我们调整各种命令的实现,以利用新的数组参数。
|
||||
|
||||
## **inventory.h**
|
||||
|
||||
```c
|
||||
extern bool executeGet(void);
|
||||
extern bool executeDrop(void);
|
||||
extern bool executeAsk(void);
|
||||
extern bool executeGive(void);
|
||||
extern bool executeInventory(void);
|
||||
```
|
||||
|
||||
## **inventory.c**
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
#include "misc.h"
|
||||
#include "match.h"
|
||||
#include "noun.h"
|
||||
#include "move.h"
|
||||
|
||||
bool executeGet(void)
|
||||
{
|
||||
OBJECT *obj = getVisible("what you want to get", params[0]);
|
||||
switch (getDistance(player, obj))
|
||||
{
|
||||
case distSelf:
|
||||
printf("You should not be doing that to yourself.\n");
|
||||
break;
|
||||
case distHeld:
|
||||
printf("You already have %s.\n", obj->description);
|
||||
break;
|
||||
case distOverthere:
|
||||
printf("Too far away, move closer please.\n");
|
||||
break;
|
||||
case distUnknownObject:
|
||||
// already handled by getVisible
|
||||
break;
|
||||
default:
|
||||
if (obj->location != NULL && obj->location->health > 0)
|
||||
{
|
||||
printf("You should ask %s nicely.\n", obj->location->description);
|
||||
}
|
||||
else
|
||||
{
|
||||
moveObject(obj, player);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool executeDrop(void)
|
||||
{
|
||||
moveObject(getPossession(player, "drop", params[0]), player->location);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool executeAsk(void)
|
||||
{
|
||||
moveObject(getPossession(actorHere(), "ask", params[0]), player);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool executeGive(void)
|
||||
{
|
||||
moveObject(getPossession(player, "give", params[0]), actorHere());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool executeInventory(void)
|
||||
{
|
||||
if (listObjectsAtLocation(player) == 0)
|
||||
{
|
||||
printf("You are empty-handed.\n");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
我们在上一章中添加的模块也是如此。
|
||||
|
||||
## opemclose.h
|
||||
|
||||
```c
|
||||
extern bool executeOpen(void);
|
||||
extern bool executeClose(void);
|
||||
extern bool executeLock(void);
|
||||
extern bool executeUnlock(void);
|
||||
```
|
||||
|
||||
## openclose.c
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
#include "match.h"
|
||||
#include "reach.h"
|
||||
|
||||
bool executeOpen(void)
|
||||
{
|
||||
OBJECT *obj = reachableObject("what you want to open", params[0]);
|
||||
if (obj != NULL) (*obj->open)();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool executeClose(void)
|
||||
{
|
||||
OBJECT *obj = reachableObject("what you want to close", params[0]);
|
||||
if (obj != NULL) (*obj->close)();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool executeLock(void)
|
||||
{
|
||||
OBJECT *obj = reachableObject("what you want to lock", params[0]);
|
||||
if (obj != NULL) (*obj->lock)();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool executeUnlock(void)
|
||||
{
|
||||
OBJECT *obj = reachableObject("what you want to unlock", params[0]);
|
||||
if (obj != NULL) (*obj->unlock)();
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
在 location.c 中,look around 命令被赋予了自己的功能,与检查特定对象的 look 命令分开(见第 7-12 行)。
|
||||
|
||||
## location.h
|
||||
|
||||
```c
|
||||
extern bool executeLookAround(void);
|
||||
extern bool executeLook(void);
|
||||
extern bool executeGo(void);
|
||||
```
|
||||
|
||||
## location.c
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
#include "misc.h"
|
||||
#include "match.h"
|
||||
#include "noun.h"
|
||||
|
||||
bool executeLookAround(void)
|
||||
{
|
||||
printf("You are in %s.\n", player->location->description);
|
||||
listObjectsAtLocation(player->location);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool executeLook(void)
|
||||
{
|
||||
OBJECT *obj = getVisible("what you want to look at", params[0]);
|
||||
switch (getDistance(player, obj))
|
||||
{
|
||||
case distHereContained:
|
||||
printf("Hard to see, try to get it first.\n");
|
||||
break;
|
||||
case distOverthere:
|
||||
printf("Too far away, move closer please.\n");
|
||||
break;
|
||||
case distNotHere:
|
||||
printf("You don't see any %s here.\n", params[0]);
|
||||
break;
|
||||
case distUnknownObject:
|
||||
// already handled by getVisible
|
||||
break;
|
||||
default:
|
||||
printf("%s\n", obj->details);
|
||||
listObjectsAtLocation(obj);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void movePlayer(OBJECT *passage)
|
||||
{
|
||||
printf("%s\n", passage->textGo);
|
||||
if (passage->destination != NULL)
|
||||
{
|
||||
player->location = passage->destination;
|
||||
printf("\n");
|
||||
executeLookAround();
|
||||
}
|
||||
}
|
||||
|
||||
bool executeGo(void)
|
||||
{
|
||||
OBJECT *obj = getVisible("where you want to go", params[0]);
|
||||
switch (getDistance(player, obj))
|
||||
{
|
||||
case distOverthere:
|
||||
movePlayer(getPassage(player->location, obj));
|
||||
break;
|
||||
case distNotHere:
|
||||
printf("You don't see any %s here.\n", params[0]);
|
||||
break;
|
||||
case distUnknownObject:
|
||||
// already handled by getVisible
|
||||
break;
|
||||
default:
|
||||
movePlayer(obj);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
我们的游戏仍然只接受简单的动词 - 名词命令,但新的解析器确实有可能接受有多个名词的命令,如 "put coin in box",这将在下一章中演示。
|
||||
|
||||
新的解析器比原来的解析器有了很大的改进,但以今天的标准来看,它仍然远远不够完美。例如,没有结构性的方法可以用一个命令操纵多个对象("获得硬币、钥匙和灯")或连续执行两个或多个命令("获得钥匙然后向东走")。这对于一个 RPG 游戏来说限制实在太大了!
|
||||
|
||||
在真正意义上,我们的分析器甚至不是一个分析器,它只是一个简单的模式匹配器。我们认为,对于一个简单的冒险游戏来说,它的工作已经足够好了。它的一些缺陷可以通过添加更多的模式而得到缓解。但最终,你会遇到它的局限性,你可能想转到更成熟的东西上。在这种情况下,我们推荐使用一个体面的分析器生成器(例如 Yacc)。请注意,这不是一件容易做到的事情。目前,这已经超出了本教程的范围。
|
||||
输出样例
|
||||
|
||||
Welcome to Little Cave Adventure.
|
||||
You are in an open field.
|
||||
You see:
|
||||
a silver coin
|
||||
a burly guard
|
||||
a cave entrance to the east
|
||||
dense forestall around
|
||||
|
||||
--> get coin
|
||||
You pick up a silver coin.
|
||||
|
||||
--> give coin
|
||||
You give a silver coin to a burly guard.
|
||||
|
||||
--> go cave
|
||||
You walk into the cave.
|
||||
|
||||
You are in a little cave.
|
||||
You see:
|
||||
an exit to the west
|
||||
solid rock all around
|
||||
a closed door to the south
|
||||
a tiny key
|
||||
|
||||
--> get key
|
||||
You pick up a tiny key.
|
||||
|
||||
--> go south
|
||||
The door is closed.
|
||||
|
||||
--> open door
|
||||
You open a closed door to the south.
|
||||
|
||||
--> go south
|
||||
You walk through the door into a backroom.
|
||||
|
||||
You are in a backroom.
|
||||
You see:
|
||||
solid rock all around
|
||||
an open door to the north
|
||||
a wooden box
|
||||
|
||||
--> unlock box
|
||||
You unlock a wooden box.
|
||||
|
||||
--> open box
|
||||
You open a wooden box.
|
||||
|
||||
--> examine box
|
||||
The box is open.
|
||||
You see:
|
||||
a gold coin
|
||||
|
||||
--> get gold
|
||||
You get a gold coin from a wooden box.
|
||||
|
||||
--> quit
|
||||
|
||||
Bye!
|
||||
366
2023旧版内容/3.编程思维体系构建/3.4.6.14.丰富命令.md
Normal file
@@ -0,0 +1,366 @@
|
||||
# 14.丰富命令
|
||||
|
||||
是时候证明新的解析器确实有能力解释更复杂的命令了。
|
||||
|
||||
到目前为止,我们的解析器只接受简单的动词 - 名词命令。让我们试着解析一些更复杂的命令,比如:
|
||||
|
||||
- get coin from box
|
||||
- put coin in box
|
||||
- ask coin from guard
|
||||
- give coin to guard
|
||||
|
||||
在 parsexec.c 中添加模式似乎很简单。
|
||||
|
||||
- get A from B
|
||||
- put A in B
|
||||
- ask A from B
|
||||
- give A to B
|
||||
|
||||
::: warning 🤔 思考题:你能否自行实现这些命令
|
||||
:::
|
||||
|
||||
但是正如前一章所解释的,类似的命令(比如 "从 B 中获取 A "和已经存在的 "获取 A")必须以正确的顺序排列。如果'get A'首先出现,那么它将消耗任何以'get'开头的命令,包括所有应该被新模式匹配的命令。简而言之,"从 B 获取 A "必须出现在 "获取 A "之前。
|
||||
|
||||
有些命令(例如 put)没有单名词的变体。然而,我们将添加一个带有单非词的模式('put A')。我们需要这个模式来正确捕捉一个拼写错误的名词。完整的模式 "把 A 放进 B "不会匹配 "把 Koin 放进盒子 "这样的命令。正如前一章所解释的,只有模式末尾的非终端才能够捕捉到拼写错误的名词或其他不被识别的名词。
|
||||
|
||||
这也适用于有两个以上的名词的命令。有 n 个名词,你需要 n 个模式来处理所有可能的不匹配。一个有三个参数的例子:
|
||||
|
||||
1. “paint A on B with C”
|
||||
2. “paint A on B”
|
||||
3. “paint A”
|
||||
|
||||
同样,模式的顺序是至关重要的:如果 "涂抹 A "在最上面(意味着它将被首先尝试),那么它将消耗所有的涂抹命令,包括那些本应由 1 和 2 捕获的命令。
|
||||
|
||||
目前,我们不需要有两个以上非终结点的模式。
|
||||
|
||||
## parsexec.h
|
||||
|
||||
```c
|
||||
extern bool parseAndExecute(const char *input);
|
||||
```
|
||||
|
||||
## parsexec.c
|
||||
|
||||
```c
|
||||
#include <ctype.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
#include "misc.h"
|
||||
#include "match.h"
|
||||
#include "location.h"
|
||||
#include "inventory.h"
|
||||
#include "inventory2.h"
|
||||
#include "openclose.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
const char *pattern;
|
||||
bool (*function)(void);
|
||||
} COMMAND;
|
||||
|
||||
static bool executeQuit(void)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool executeNoMatch(void)
|
||||
{
|
||||
const char *src = *params;
|
||||
int len;
|
||||
for (len = 0; src[len] != '\0' && !isspace(src[len]); len++);
|
||||
if (len > 0) printf("I don't know how to '%.*s'.\n", len, src);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseAndExecute(const char *input)
|
||||
{
|
||||
static const COMMAND commands[] =
|
||||
{
|
||||
{ "quit" , executeQuit },
|
||||
{ "look" , executeLookAround },
|
||||
{ "look around" , executeLookAround },
|
||||
{ "look at A" , executeLook },
|
||||
{ "look A" , executeLook },
|
||||
{ "examine A" , executeLook },
|
||||
{ "go to A" , executeGo },
|
||||
{ "go A" , executeGo },
|
||||
{ "get A from B" , executeGetFrom },
|
||||
{ "get A" , executeGet },
|
||||
{ "put A in B" , executePutIn },
|
||||
{ "drop A in B" , executePutIn },
|
||||
{ "drop A" , executeDrop },
|
||||
{ "ask A from B" , executeAskFrom },
|
||||
{ "ask A" , executeAsk },
|
||||
{ "give A to B" , executeGiveTo },
|
||||
{ "give A" , executeGive },
|
||||
{ "inventory" , executeInventory },
|
||||
{ "open A" , executeOpen },
|
||||
{ "close A" , executeClose },
|
||||
{ "lock A" , executeLock },
|
||||
{ "unlock A" , executeUnlock },
|
||||
{ "A" , executeNoMatch }
|
||||
};
|
||||
const COMMAND *cmd;
|
||||
for (cmd = commands; !matchCommand(input, cmd->pattern); cmd++);
|
||||
return (*cmd->function)();
|
||||
}
|
||||
```
|
||||
|
||||
在一个新的模块 inventory2.c 中,我们实现了新的多名词命令 get、drop/put、ask 和 give。现在我们终于可以把金币放回盒子里了 (可喜可贺,可喜可贺)。
|
||||
|
||||
## inventory2.h
|
||||
|
||||
```c
|
||||
extern bool executeGetFrom(void);
|
||||
extern bool executePutIn(void);
|
||||
extern bool executeAskFrom(void);
|
||||
extern bool executeGiveTo(void);
|
||||
```
|
||||
|
||||
## inventory2.c
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
#include "match.h"
|
||||
#include "noun.h"
|
||||
#include "move.h"
|
||||
#include "reach.h"
|
||||
|
||||
bool executeGetFrom(void)
|
||||
{
|
||||
OBJECT *from = reachableObject("where to get that from", params[1]);
|
||||
if (from != NULL && getVisible("what you want to get", params[0]) != NULL)
|
||||
{
|
||||
if (from->health > 0)
|
||||
{
|
||||
printf("You should ask %s nicely.\n", from->description);
|
||||
}
|
||||
else
|
||||
{
|
||||
moveObject(getPossession(from, "get", params[0]), player);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool executePutIn(void)
|
||||
{
|
||||
OBJECT *obj = getPossession(player, "put", params[0]);
|
||||
if (obj != NULL)
|
||||
{
|
||||
OBJECT *to = reachableObject("where to put that in", params[1]);
|
||||
if (to != NULL)
|
||||
{
|
||||
if (to->health > 0)
|
||||
{
|
||||
printf("You should offer that nicely to %s.\n", to->description);
|
||||
}
|
||||
else
|
||||
{
|
||||
moveObject(obj, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool executeAskFrom(void)
|
||||
{
|
||||
OBJECT *from = reachableObject("who to ask that", params[1]);
|
||||
if (from != NULL)
|
||||
{
|
||||
if (from->health > 0)
|
||||
{
|
||||
if (getVisible("what you want to ask", params[0]) != NULL)
|
||||
{
|
||||
moveObject(getPossession(from, "ask", params[0]), player);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("There is no response from %s.\n", from->description);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool executeGiveTo(void)
|
||||
{
|
||||
OBJECT *obj = getPossession(player, "give", params[0]);
|
||||
if (obj != NULL)
|
||||
{
|
||||
OBJECT *to = reachableObject("who to give that to", params[1]);
|
||||
if (to != NULL)
|
||||
{
|
||||
if (to->health > 0)
|
||||
{
|
||||
moveObject(obj, to);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("No eagerness is shown by %s.\n", to->description);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
仔细观察上面的代码,你可能会注意到,像 "把硬币放进森林 "和 "把硬币放进盒子 "这样的命令(当盒子被关闭时)会得到以下奇怪的回答:"这太重了。" 这是因为大多数物体(包括封闭的盒子,以及像森林这样的场景)容纳其他物体的能力为零。这是正确的,但这个信息是很不恰当的。为了避免这种情况,我们将为那些容量为零的物体引入一个单独的信息:"这样做似乎不太适合。"
|
||||
|
||||
::: warning 🤔 先想想有没有什么办法解决?
|
||||
:::
|
||||
|
||||
然而,当它作为对 "把钥匙放进盒子里 "的回应时,这个特殊的信息是完全误导的。所以我们将为这种特殊的对象组合做一个特殊的例外。
|
||||
|
||||
## move.c
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
#include "misc.h"
|
||||
|
||||
static int weightOfContents(OBJECT *container)
|
||||
{
|
||||
int sum = 0;
|
||||
OBJECT *obj;
|
||||
for (obj = objs; obj < endOfObjs; obj++)
|
||||
{
|
||||
if (isHolding(container, obj)) sum += obj->weight;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
static void describeMove(OBJECT *obj, OBJECT *to)
|
||||
{
|
||||
if (to == player->location)
|
||||
{
|
||||
printf("You drop %s.\n", obj->description);
|
||||
}
|
||||
else if (to != player)
|
||||
{
|
||||
printf(to->health > 0 ? "You give %s to %s.\n" : "You put %s in %s.\n",
|
||||
obj->description, to->description);
|
||||
}
|
||||
else if (obj->location == player->location)
|
||||
{
|
||||
printf("You pick up %s.\n", obj->description);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("You get %s from %s.\n",
|
||||
obj->description, obj->location->description);
|
||||
}
|
||||
}
|
||||
|
||||
void moveObject(OBJECT *obj, OBJECT *to)
|
||||
{
|
||||
if (obj == NULL)
|
||||
{
|
||||
// already handled by getVisible or getPossession
|
||||
}
|
||||
else if (to == NULL)
|
||||
{
|
||||
printf("There is nobody here to give that to.\n");
|
||||
}
|
||||
else if (to->capacity == 0)
|
||||
{
|
||||
printf(obj == keyForBox && (to == closedBox || to == lockedBox) ?
|
||||
"The key seems to fit the lock.\n" :
|
||||
"It doesn't seem to fit in.\n");
|
||||
}
|
||||
else if (obj->weight > to->capacity)
|
||||
{
|
||||
printf("That is way too heavy.\n");
|
||||
}
|
||||
else if (obj->weight + weightOfContents(to) > to->capacity)
|
||||
{
|
||||
printf("That would become too heavy.\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
describeMove(obj, to);
|
||||
obj->location = to;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
毫无疑问,我们接受的命令越复杂,我们就需要花更多的精力来做出令人信服的答复。
|
||||
输出样例
|
||||
|
||||
Welcome to Little Cave Adventure.
|
||||
You are in an open field.
|
||||
You see:
|
||||
a silver coin
|
||||
a burly guard
|
||||
a cave entrance to the east
|
||||
dense forest all around
|
||||
|
||||
--> get coin
|
||||
You pick up a silver coin.
|
||||
|
||||
--> give coin to guard
|
||||
You give a silver coin to a burly guard.
|
||||
|
||||
--> go cave
|
||||
You walk into the cave.
|
||||
|
||||
You are in a little cave.
|
||||
You see:
|
||||
an exit to the west
|
||||
solid rock all around
|
||||
a closed door to the south
|
||||
a tiny key
|
||||
|
||||
--> get key
|
||||
You pick up a tiny key.
|
||||
|
||||
--> go south
|
||||
The door is closed.
|
||||
|
||||
--> open door
|
||||
You open a closed door to the south.
|
||||
|
||||
--> go south
|
||||
You walk through the door into a backroom.
|
||||
|
||||
You are in a backroom.
|
||||
You see:
|
||||
solid rock all around
|
||||
an open door to the north
|
||||
a wooden box
|
||||
|
||||
--> unlock box
|
||||
You unlock a wooden box.
|
||||
|
||||
--> open box
|
||||
You open a wooden box.
|
||||
|
||||
--> examine box
|
||||
The box is open.
|
||||
You see:
|
||||
a gold coin
|
||||
|
||||
--> get gold from box
|
||||
You get a gold coin from a wooden box.
|
||||
|
||||
--> inventory
|
||||
You have:
|
||||
a gold coin
|
||||
a tiny key
|
||||
|
||||
--> put gold in box
|
||||
You put a gold coin in a wooden box.
|
||||
|
||||
--> examine box
|
||||
The box is open.
|
||||
You see:
|
||||
a gold coin
|
||||
|
||||
--> quit
|
||||
|
||||
Bye!
|
||||
1008
2023旧版内容/3.编程思维体系构建/3.4.6.15.赋予明暗.md
Normal file
15
2023旧版内容/3.编程思维体系构建/3.4.6.16.结语:你终将自由.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# 结语:你终将自由
|
||||
|
||||
15 章的内容,你已经实现了一个最基本游戏的最基本功能,不知道你是不是感觉到满满的成就感呢?
|
||||
|
||||
但是还是有非常多可以做的可以思考的。
|
||||
|
||||
比如说,你可以增加战斗功能,你可以设计自动生成的测试样例,你甚至可以添加联网联机功能,真正构造一个奇幻的世界。
|
||||
|
||||
我相信你经过上面的训练,已经初步理解了结构化编程的基本要义,可以走向更广阔罗的世界且游刃有余的自由的处理它了。
|
||||
|
||||
希望你可以牢记其中各种设计原则,并且不断精进不断练习。
|
||||
|
||||
也许有的同学没有完整的将他刷完,那也没关系,你完全可以在进行一小部分后就进行后面的实验。
|
||||
|
||||
不会造成什么太大的影响的,只不过你可能编程能力上会失去一个宝贵的锻炼机会罢了。
|
||||
153
2023旧版内容/3.编程思维体系构建/3.4.6.2.探索未知.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# 2.探索未知
|
||||
|
||||
::: tip <font size=5>**驾驭项目,而不是被项目驾驭**</font>
|
||||
|
||||
你和一个项目的关系会经历 4 个阶段:
|
||||
|
||||
1. 被驾驭:你对它一无所知
|
||||
2. 一知半解:你对其中的主要模块和功能有了基本的了解
|
||||
3. 驾轻就熟:你对整个项目的细节都了如指掌
|
||||
4. 为你所用:你可以随心所欲地在项目中添加你认为有用的功能
|
||||
|
||||
如果你想要达成第二个阶段,你需要仔细学习不断探索更新的内容,达到第三个阶段的主要手段是独立完成实验内容和独立调试。至于要达到第四个阶段,就要靠你的主观能动性了:代码还有哪里做得不够好?怎么样才算是够好?应该怎么做才能达到这个目标?
|
||||
|
||||
你毕业后到了工业界或学术界,就会发现真实的项目也都是这样:
|
||||
|
||||
1. 刚接触一个新项目,不知道如何下手
|
||||
2. RTFM, RTFSC, 大致明白项目组织结构和基本的工作流程
|
||||
3. 运行项目的时候发现有非预期行为 (可能是配置错误或环境错误,可能是和已有项目对接出错,也可能是项目自身的 bug), 然后调试。在调试过程中,对这些模块的理解会逐渐变得清晰。
|
||||
4. 哪天需要你在项目中添加一个新功能,你会发现自己其实可以胜任。
|
||||
|
||||
这说明了:如果你一遇到 bug 就找大神帮你调试,你失去的机会和能力会比你想象的多得多
|
||||
|
||||
:::
|
||||
文字冒险游戏的基本交互很简单
|
||||
|
||||
1. 玩家输入命令。
|
||||
2. 程序 [解析](http://en.wikipedia.org/wiki/Parsing) 并执行命令。
|
||||
3. 重复步骤 1 和 2,直到玩家决定退出
|
||||
|
||||
那么,当命令很多的时候,如果你将他写在一起,一个文件有五六千行,我相信这样的情况你是不愿意去看的,因此,我们引入了函数的概念。
|
||||
|
||||
::: warning 🤔 自行了解函数的概念,同时去了解当我需要引用别的文件的函数时该怎么办?
|
||||
|
||||
了解一下什么是“驼峰原则”,我们为什么要依据它命名函数?
|
||||
:::
|
||||
下面的代码示例包含三个函数,每个步骤一个函数:
|
||||
|
||||
1. 函数*getInput*。
|
||||
2. 函数*parseAndExecute*。
|
||||
3. 函数*main*,负责重复调用其他两个函数。
|
||||
|
||||
## main.c
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include "parsexec.h"
|
||||
//当我需要引用别的文件
|
||||
static char input[100] = "look around";
|
||||
//定义全局变量
|
||||
static bool getInput(void)
|
||||
{
|
||||
printf("\n--> ");
|
||||
//你可以将他改成你喜欢的内容
|
||||
return fgets(input, sizeof input, stdin) != NULL;
|
||||
//fgets 用于收集键盘的输入
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
printf("Welcome to Little Cave Adventure.\n");
|
||||
while (parseAndExecute(input) && getInput());
|
||||
printf("\nBye!\n");
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
注意:某些老版本的 C 语言不支持 bool 选项,你将他改为 int 是一样的。
|
||||
|
||||
::: warning 🤔 思考题:static 是什么意思?我为什么要用他?
|
||||
:::
|
||||
|
||||
## **parsexec.h**
|
||||
|
||||
```c
|
||||
extern bool parseAndExecute(char *input);
|
||||
```
|
||||
|
||||
::: warning 🤔 思考题:
|
||||
extern 是干什么的?.h 文件又在干嘛?
|
||||
|
||||
哇,我用了一个指针!input 前面是个指针!!!
|
||||
|
||||
指针是啥?[C 指针详解](https://www.runoob.com/w3cnote/c-pointer-detail.html) STFW(虽然都给你了)
|
||||
|
||||
在这里用指针是为了传参的时候可以传字符串哦
|
||||
:::
|
||||
|
||||
## **parsexec.c**
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
|
||||
bool parseAndExecute(char *input)
|
||||
{
|
||||
char *verb = strtok(input, " \n");
|
||||
char *noun = strtok(NULL, " \n");
|
||||
//strtok 是 string 库下的一个函数
|
||||
if (verb != NULL)
|
||||
{
|
||||
if (strcmp(verb, "quit") == 0)
|
||||
//strcmp 也是
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (strcmp(verb, "look") == 0)
|
||||
{
|
||||
printf("It is very dark in here.\n");
|
||||
}
|
||||
else if (strcmp(verb, "go") == 0)
|
||||
{
|
||||
printf("It's too dark to go anywhere.\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("I don't know how to '%s'.\n", verb);
|
||||
//%s是 verb 附加参数的占位符
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
你的编译器可能会给出警告 the unused variable‘noun’,这些不用担心,将会在下一章解决。
|
||||
|
||||
返回*false*将导致主循环结束。
|
||||
|
||||
::: warning <font size=5>**RTFM&&STFW**</font>
|
||||
搞懂 strtok 和 strcmp 的用法
|
||||
:::
|
||||
|
||||
考虑一下 NULL 是干什么的
|
||||
|
||||
测试样例
|
||||
|
||||
Welcome to Little Cave Adventure.
|
||||
It is very dark in here.
|
||||
|
||||
--> go north
|
||||
It's too dark to go anywhere.
|
||||
|
||||
--> look around
|
||||
It is very dark in here.
|
||||
|
||||
--> eat sandwich
|
||||
I don't know how to 'eat'.
|
||||
|
||||
--> quit
|
||||
|
||||
Bye
|
||||
274
2023旧版内容/3.编程思维体系构建/3.4.6.3.指明地点.md
Normal file
@@ -0,0 +1,274 @@
|
||||
# 3.指明地点
|
||||
|
||||
::: warning <font size=5>某种极其糟糕的编程习惯</font>
|
||||
<font size=5><strong>Copy-paste</strong></font>
|
||||
|
||||
我们很多同学在编程的过程中,可能会写出一大堆重复性很强的代码,在最近看的 pa 中,举了这样一个例子,你不需要看懂只需要感受到就可:
|
||||
:::
|
||||
|
||||
```c
|
||||
if (strcmp(s, "$0") == 0)
|
||||
return cpu.gpr[0]._64;
|
||||
else if (strcmp(s, "ra") == 0)
|
||||
return cpu.gpr[1]._64;
|
||||
else if (strcmp(s, "sp") == 0)
|
||||
return cpu.gpr[2]._64;
|
||||
else if (strcmp(s, "gp") == 0)
|
||||
return cpu.gpr[3]._64;
|
||||
else if (strcmp(s, "tp") == 0)
|
||||
return cpu.gpr[4]._64;
|
||||
else if (strcmp(s, "t0") == 0)
|
||||
return cpu.gpr[5]._64;
|
||||
else if (strcmp(s, "t1") == 0)
|
||||
return cpu.gpr[6]._64;
|
||||
else if (strcmp(s, "s2") == 0)
|
||||
return cpu.gpr[7]._64;
|
||||
else if (strcmp(s, "s0") == 0)
|
||||
return cpu.gpr[8]._64;
|
||||
else if (strcmp(s, "s1") == 0)
|
||||
return cpu.gpr[9]._64;
|
||||
else if (strcmp(s, "a0") == 0)
|
||||
return cpu.gpr[10]._64;
|
||||
else if (strcmp(s, "a1") == 0)
|
||||
return cpu.gpr[11]._64;
|
||||
else if (strcmp(s, "a2") == 0)
|
||||
return cpu.gpr[12]._64;
|
||||
else if (strcmp(s, "a3") == 0)
|
||||
return cpu.gpr[13]._64;
|
||||
else if (strcmp(s, "a4") == 0)
|
||||
return cpu.gpr[14]._64;
|
||||
else if (strcmp(s, "a5") == 0)
|
||||
return cpu.gpr[15]._64;
|
||||
else if (strcmp(s, "a6") == 0)
|
||||
return cpu.gpr[16]._64;
|
||||
else if (strcmp(s, "a7") == 0)
|
||||
return cpu.gpr[17]._64;
|
||||
else if (strcmp(s, "s2") == 0)
|
||||
return cpu.gpr[18]._64;
|
||||
else if (strcmp(s, "s3") == 0)
|
||||
return cpu.gpr[19]._64;
|
||||
else if (strcmp(s, "s4") == 0)
|
||||
return cpu.gpr[20]._64;
|
||||
else if (strcmp(s, "s5") == 0)
|
||||
return cpu.gpr[21]._64;
|
||||
else if (strcmp(s, "s6") == 0)
|
||||
return cpu.gpr[22]._64;
|
||||
else if (strcmp(s, "s7") == 0)
|
||||
return cpu.gpr[23]._64;
|
||||
else if (strcmp(s, "s8") == 0)
|
||||
return cpu.gpr[24]._64;
|
||||
else if (strcmp(s, "s8") == 0)
|
||||
return cpu.gpr[25]._64;
|
||||
else if (strcmp(s, "s10") == 0)
|
||||
return cpu.gpr[26]._64;
|
||||
else if (strcmp(s, "t2") == 0)
|
||||
return cpu.gpr[27]._64;
|
||||
else if (strcmp(s, "t3") == 0)
|
||||
return cpu.gpr[28]._64;
|
||||
else if (strcmp(s, "t4") == 0)
|
||||
return cpu.gpr[29]._64;
|
||||
else if (strcmp(s, "t5") == 0)
|
||||
return cpu.gpr[30]._64;
|
||||
else if (strcmp(s, "t5") == 0)
|
||||
return cpu.gpr[31]._64;
|
||||
```
|
||||
|
||||
以下是某论文的代码节选,可以说是错误的范例:
|
||||
|
||||
```python
|
||||
fx = torch.cat((xs[0], fs[0], xs[1], fs[1], xs[2], fs[2], xs[3], fs[3], xs[4], fs[4], xs[5], fs[5], xs[6], fs[6], xs[7], fs[7],
|
||||
xs[8], fs[8], xs[9], fs[9], xs[10], fs[10], xs[11], fs[11], xs[12], fs[12], xs[13], fs[13], xs[14], fs[14], xs[15], fs[15],
|
||||
xs[16], fs[16], xs[17], fs[17], xs[18], fs[18], xs[19], fs[19], xs[20], fs[20], xs[21], fs[21], xs[22], fs[22], xs[23], fs[23],
|
||||
xs[24], fs[24], xs[25], fs[25], xs[26], fs[26], xs[27], fs[27], xs[28], fs[28], xs[29], fs[29], xs[30], fs[30], xs[31], fs[31]), 1)
|
||||
bx = torch.cat((xs[0], bs[0], xs[1], bs[1], xs[2], bs[2], xs[3], bs[3], xs[4], bs[4], xs[5], bs[5], xs[6], bs[6], xs[7], bs[7],
|
||||
xs[8], bs[8], xs[9], bs[9], xs[10], bs[10], xs[11], bs[11], xs[12], bs[12], xs[13], bs[13], xs[14], bs[14], xs[15], bs[15],
|
||||
xs[16], bs[16], xs[17], bs[17], xs[18], bs[18], xs[19], bs[19], xs[20], bs[20], xs[21], bs[21], xs[22], bs[22], xs[23], bs[23],
|
||||
xs[24], bs[24], xs[25], bs[25], xs[26], bs[26], xs[27], bs[27], xs[28], bs[28], xs[29], bs[29], xs[30], bs[30], xs[31], bs[31]), 1)
|
||||
```
|
||||
|
||||
::: tip <font size=5>你想想,你遇到这么长的代码,你愿意看他吗?</font>
|
||||
|
||||
更可怕的是,这种编码模式可能会导致意想不到的 bug。
|
||||
|
||||
当你发现这些代码有 bug 的时候,噩梦才刚刚开始。也许花了好几天你又调出一个 bug 的时候,才会想起这个 bug 你好像之前在哪里调过。你也知道代码里面还有类似的 bug, 但你已经分辨不出哪些代码是什么时候从哪个地方复制过来的了。
|
||||
|
||||
这种糟糕的编程习惯叫 Copy-Paste, 经过上面的分析,相信你也已经领略到它的可怕了。事实上,[周源源教授](https://cseweb.ucsd.edu/~yyzhou/)的团队在 2004 年就设计了一款工具 CP-Miner, 来自动检测操作系统代码中由于 Copy-Paste 造成的 bug. 这个工具还让周源源教授收获了一篇[系统方向顶级会议 OSDI 的论文](http://pages.cs.wisc.edu/~shanlu/paper/OSDI04-CPMiner.pdf), 这也是她当时所在学校 UIUC 史上的第一篇系统方向的顶级会议论文。
|
||||
|
||||
后来周源源教授发现,相比于操作系统,应用程序的源代码中 Copy-Paste 的现象更加普遍。于是她们团队把 CP-Miner 的技术应用到应用程序的源代码中,并创办了 PatternInsight 公司。很多 IT 公司纷纷购买 PatternInsight 的产品,并要求提供相应的定制服务,甚至 PatternInsight 公司最后还被 VMWare 收购了。
|
||||
|
||||
这个故事折射出,大公司中程序员的编程习惯也许不比你好多少,他们也会写出 Copy-Paste 这种难以维护的代码。但反过来说,重视编码风格这些企业看中的能力,你从现在就可以开始培养。
|
||||
:::
|
||||
|
||||
*传统上,文本冒险是由(许多)不同位置组成的虚拟世界。虽然这不是必需的(一些冒险发生在一个房间里!),但这是解释**数据结构**使用的好方法。*
|
||||
|
||||
我们首先定义一个[结构](http://en.wikipedia.org/wiki/Struct_(C_programming_language))来表示一个位置。它包含两个简单的属性开始(稍后可能会有更多的属性)。
|
||||
|
||||
1. 描述:对物品进行描述
|
||||
2. 标记:具体的对其进行标记
|
||||
|
||||
```c
|
||||
struct location {
|
||||
const char *description;
|
||||
const char *tag;
|
||||
};
|
||||
```
|
||||
|
||||
::: warning 🤔 思考题:
|
||||
我们为什么要用结构体来保存位置?
|
||||
|
||||
这样子做有什么好处?
|
||||
|
||||
const 又是什么?
|
||||
:::
|
||||
|
||||
接下来,我们定义一个位置数组。目前,我们保持它非常简单:只有两个位置。
|
||||
|
||||
```c
|
||||
struct location locs[2];
|
||||
```
|
||||
|
||||
我们还可以使用初始值设定项立即填充所有静态数据。
|
||||
|
||||
```c
|
||||
struct location locs[2] = {
|
||||
{"an open field", "field"},
|
||||
{"a little cave", "cave"}
|
||||
};
|
||||
```
|
||||
|
||||
让我们把它付诸实践。在上一章(*parsexec.c)* 的代码示例中,我们更改了第 4、18 和 22 行)。
|
||||
|
||||
## <strong>parsexec.c</strong>
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "location.h"
|
||||
|
||||
bool parseAndExecute(char *input)
|
||||
{
|
||||
char *verb = strtok(input, " \n");
|
||||
char *noun = strtok(NULL, " \n");
|
||||
if (verb != NULL)
|
||||
{
|
||||
if (strcmp(verb, "quit") == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (strcmp(verb, "look") == 0)
|
||||
{
|
||||
executeLook(noun);
|
||||
}
|
||||
else if (strcmp(verb, "go") == 0)
|
||||
{
|
||||
executeGo(noun);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("I don't know how to '%s'.\n", verb);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
接下来,我们将一个新模块添加到项目中
|
||||
|
||||
## <strong>location.h</strong>
|
||||
|
||||
```c
|
||||
extern void executeLook(const char *noun);
|
||||
extern void executeGo(const char *noun);
|
||||
```
|
||||
|
||||
## <strong>location.c</strong>
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
struct location {
|
||||
const char *description;
|
||||
const char *tag;
|
||||
}
|
||||
locs[] = {
|
||||
{"an open field", "field"},
|
||||
{"a little cave", "cave"}
|
||||
};
|
||||
|
||||
#define numberOfLocations (sizeof locs / sizeof *locs)
|
||||
//欸?这个是干啥呢?
|
||||
static unsigned locationOfPlayer = 0;
|
||||
|
||||
void executeLook(const char *noun)
|
||||
{
|
||||
if (noun != NULL && strcmp(noun, "around") == 0)
|
||||
{
|
||||
printf("You are in %s.\n", locs[locationOfPlayer].description);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("I don't understand what you want to see.\n");
|
||||
}
|
||||
}
|
||||
|
||||
void executeGo(const char *noun)
|
||||
{
|
||||
unsigned i;
|
||||
for (i = 0; i < numberOfLocations; i++)
|
||||
{
|
||||
if (noun != NULL && strcmp(noun, locs[i].tag) == 0)
|
||||
{
|
||||
if (i == locationOfPlayer)
|
||||
{
|
||||
printf("You can't get much closer than this.\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("OK.\n");
|
||||
locationOfPlayer = i;
|
||||
executeLook("around");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
printf("I don't understand where you want to go.\n");
|
||||
}
|
||||
```
|
||||
|
||||
在 C 语言中,你可以使用单个语句来定义类型(*结构位置*),声明变量(*locs*)并用其初始值填充它。
|
||||
|
||||
思考题:变量*locs*是[静态分配的](http://en.wikipedia.org/wiki/Static_memory_allocation),什么是静态分配?
|
||||
|
||||
静态分配和动态分配有什么不同之处?
|
||||
|
||||
复杂思考题:13 行宏定义好像实现了一个函数!很神奇!为什么不用函数来做这个知识呢?
|
||||
|
||||
提示:这个问题涉及编程从代码到可执行文件的四个步骤,希望你可以认真学习和思考,如果你用 Linux 去完成。你可以尝试用 gcc 逐步输出编译结果。
|
||||
|
||||
测试样例:
|
||||
|
||||
Welcome to Little Cave Adventure.
|
||||
You are in an open field.
|
||||
|
||||
--> go cave
|
||||
OK.
|
||||
You are in a little cave.
|
||||
|
||||
--> go field
|
||||
OK.
|
||||
You are in an open field.
|
||||
|
||||
--> go field
|
||||
You can't get much closer than this.
|
||||
|
||||
--> look around
|
||||
You are in an open field.
|
||||
|
||||
--> go kitchen
|
||||
I don't understand where you want to go.
|
||||
|
||||
--> quit
|
||||
|
||||
Bye!
|
||||
360
2023旧版内容/3.编程思维体系构建/3.4.6.4.创建对象.md
Normal file
@@ -0,0 +1,360 @@
|
||||
# 4.创建对象
|
||||
|
||||
*在我们继续之前,我们在这里使用的是[哲学意义上](https://en.wikipedia.org/wiki/Object_(philosophy))的“对象”一词。它与[面向对象编程](https://en.wikipedia.org/wiki/Object-oriented_programming)无关,也与Java,C#和Python等编程语言中预定义的“对象”类型没有任何共同之处。下面,我将定义一个名为 object 的结构体。*
|
||||
|
||||
冒险游戏中的大多数谜题都围绕着**物品**。例子:
|
||||
|
||||
- 必须找到一把钥匙,然后用来解锁某扇门。
|
||||
- 必须杀死守卫或者诱骗守卫才能开启房间
|
||||
|
||||
所以,为了表示这个物品,我们可以使用如下[结构](http://en.wikipedia.org/wiki/Struct_(C_programming_language)):
|
||||
|
||||
- **\*description: 对物品的描述**
|
||||
- **\*tag: 物品的类型**
|
||||
- **\*location: 物品所在的位置。这是对应上一章中定义的物品位置的指针。**
|
||||
|
||||
```c
|
||||
struct object {
|
||||
const char *description;
|
||||
const char *tag;
|
||||
struct location *location;
|
||||
}
|
||||
objs[] = {
|
||||
{"a silver coin", "silver", &locs[0]},
|
||||
{"a gold coin" , "gold" , &locs[1]},
|
||||
{"a burly guard", "guard" , &locs[0]}
|
||||
};
|
||||
```
|
||||
|
||||
我们发现这一章的物品的信息和上一章好像也差不多呀,所以我们直接把他合并好了!
|
||||
|
||||
```c
|
||||
struct object {
|
||||
const char *description;
|
||||
const char *tag;
|
||||
struct object *location;
|
||||
}
|
||||
objs[] = {
|
||||
{"an open field", "field" , NULL},
|
||||
{"a little cave", "cave" , NULL},
|
||||
{"a silver coin", "silver", &objs[0]},
|
||||
{"a gold coin" , "gold" , &objs[1]},
|
||||
{"a burly guard", "guard" , &objs[0]}
|
||||
};
|
||||
```
|
||||
|
||||
这样子我们的代码就会看起来更加简洁!
|
||||
|
||||
我们发现 OBJECT 的结构体里面有一个指针和自己长得一样,不用担心,这和链表的操作类似。
|
||||
|
||||
::: warning 🤔 思考题:
|
||||
链表是什么,为什么要有这么一个操作指针?
|
||||
|
||||
链表和数组有什么异同点,他们分别在增删改查上有什么优劣?
|
||||
:::
|
||||
|
||||
为了更容易地用那些所谓的物品或者是地点,我们将为每个元素定义一个名字
|
||||
|
||||
```c
|
||||
#define field (objs + 0)
|
||||
#define cave (objs + 1)
|
||||
#define silver (objs + 2)
|
||||
#define gold (objs + 3)
|
||||
#define guard (objs + 4)
|
||||
```
|
||||
|
||||
如何用各个元素的指针来方便的进行操作呢?
|
||||
|
||||
```c
|
||||
printf("You are in %s.\n", field->description);
|
||||
```
|
||||
|
||||
然后用这样的操作可以列出一个物品里面所有的小东西
|
||||
|
||||
```c
|
||||
struct object *obj;
|
||||
for (obj = objs; obj < objs + 5; obj++)
|
||||
{
|
||||
if (obj->location == cave)
|
||||
{
|
||||
printf("%s\n", obj->description);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
::: warning 🤔 暂停理解一下吧
|
||||
:::
|
||||
|
||||
那么,我们有合并这个物品(或地点)列表有什么好处呢?答案是这会让我们的代码变得更加简单,因为许多函数(如上面的函数通过这样的列表)只需要扫描单个列表就可以实现,而不是三个列表。有人可能会说没必要,因为每个命令仅适用于一种类型的对象:
|
||||
|
||||
- 命令 *go* 适用于位置对象。
|
||||
- 命令 *get* 应用于获得物品。
|
||||
- 命令 kill 适应用于杀死人物。
|
||||
|
||||
但这种方法不太对劲,原因有三:
|
||||
|
||||
1. 某些命令适用于多种类型的对象,尤其是*检查*。
|
||||
2. 有时候会出现很没意思的交互方式,比如说你要吃掉守卫,他说不行。
|
||||
3. 某些对象在游戏中可能具有多个角色。比如说队友系统,NPC 可以是你的物品也可以是对象
|
||||
|
||||
将所有对象放在一个大列表中,很容易添加一个名为“type”的属性来*构造对象*,以帮助我们区分不同类型的对象。
|
||||
|
||||
::: warning 🤔 怎么做怎么遍历呢?先思考吧
|
||||
:::
|
||||
|
||||
但是,对象通常具有同样有效的其他特征:
|
||||
|
||||
- **Locations:通过道路连接(将在后面介绍)。如果一个物体无法通过一条通道到达,那么它就不是一个位置。就是这么简单。**
|
||||
- **Items:玩家唯一可以捡起的物品;可以给他们整一个重量的属性**
|
||||
- **Actors:玩家唯一可以与之交谈,交易,战斗的对象;当然,前提是他们还活着!可以加一个 HP 属性**
|
||||
|
||||
我们还要向数组中添加一个对象:玩家自己。
|
||||
|
||||
在上一章中,有一个单独的变量 *locationOfPlayer*。我们将删除它,然后换上用户的位置属性取代他!
|
||||
|
||||
例如,此语句会将玩家移入洞穴:
|
||||
|
||||
```c
|
||||
player->location = cave;
|
||||
```
|
||||
|
||||
此表达式返回玩家当前位置的描述:
|
||||
|
||||
```c
|
||||
player->location->description
|
||||
```
|
||||
|
||||
是时候把它们放在一起了。我们从对象数组的全新模块开始
|
||||
|
||||
## Object.h
|
||||
|
||||
```c
|
||||
typedef struct object {
|
||||
const char *description;
|
||||
const char *tag;
|
||||
struct object *location;
|
||||
} OBJECT;
|
||||
|
||||
extern OBJECT objs[];
|
||||
|
||||
#define field (objs + 0)
|
||||
#define cave (objs + 1)
|
||||
#define silver (objs + 2)
|
||||
#define gold (objs + 3)
|
||||
#define guard (objs + 4)
|
||||
#define player (objs + 5)
|
||||
|
||||
#define endOfObjs (objs + 6)
|
||||
```
|
||||
|
||||
## Object.c
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
|
||||
OBJECT objs[] = {
|
||||
{"an open field", "field" , NULL },
|
||||
{"a little cave", "cave" , NULL },
|
||||
{"a silver coin", "silver" , field },
|
||||
{"a gold coin" , "gold" , cave },
|
||||
{"a burly guard", "guard" , field },
|
||||
{"yourself" , "yourself", field }
|
||||
};
|
||||
```
|
||||
|
||||
<strong>注意:</strong>要编译此模块,编译器*必须*支持 Constant folding。这排除了一些更原始的编译器,如 [Z88DK](http://en.wikipedia.org/wiki/Z88DK)。
|
||||
|
||||
以下模块将帮助我们找到与指定名词匹配的对象。
|
||||
|
||||
## noun.h
|
||||
|
||||
```c
|
||||
extern OBJECT *getVisible(const char *intention, const char *noun);
|
||||
```
|
||||
|
||||
::: warning <font size=5>**🤔 指针?函数?希望你已经掌握这是什么了**</font>
|
||||
:::
|
||||
|
||||
## noun.c
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "object.h"
|
||||
|
||||
static bool objectHasTag(OBJECT *obj, const char *noun)
|
||||
{
|
||||
return noun != NULL && *noun != '\0' && strcmp(noun, obj->tag) == 0;
|
||||
}
|
||||
|
||||
static OBJECT *getObject(const char *noun)
|
||||
{
|
||||
OBJECT *obj, *res = NULL;
|
||||
for (obj = objs; obj < endOfObjs; obj++)
|
||||
{
|
||||
if (objectHasTag(obj, noun))
|
||||
{
|
||||
res = obj;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
OBJECT *getVisible(const char *intention, const char *noun)
|
||||
{
|
||||
OBJECT *obj = getObject(noun);
|
||||
if (obj == NULL)
|
||||
{
|
||||
printf("I don't understand %s.\n", intention);
|
||||
}
|
||||
else if (!(obj == player ||
|
||||
//玩家本人。是的,这也是一个可见的物体。
|
||||
obj == player->location ||
|
||||
//玩家的当前位置。
|
||||
obj->location == player ||
|
||||
//玩家持有的物品
|
||||
obj->location == player->location ||
|
||||
//玩家当前位置的物体
|
||||
obj->location == NULL ||
|
||||
//玩家可以去的任意位置,具体完善在后面
|
||||
obj->location->location == player ||
|
||||
//玩家持有的另一个物体内的物体
|
||||
obj->location->location == player->location))
|
||||
//当前位置存在的另一个对象内部的对象
|
||||
{
|
||||
printf("You don't see any %s here.\n", noun);
|
||||
obj = NULL;
|
||||
}
|
||||
return obj;
|
||||
//感受到注释有多伟大了吧
|
||||
}
|
||||
```
|
||||
|
||||
这是另一个辅助程序的函数。它打印存在于特定位置的对象(物品,NPC)的列表。它将用于函数 *executeLook*,在下一章中,我们将介绍另一个需要它的命令。
|
||||
|
||||
## misc.h
|
||||
|
||||
```c
|
||||
extern int listObjectsAtLocation(OBJECT *location);
|
||||
```
|
||||
|
||||
## misc.c
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
|
||||
int listObjectsAtLocation(OBJECT *location)
|
||||
{
|
||||
int count = 0;
|
||||
OBJECT *obj;
|
||||
for (obj = objs; obj < endOfObjs; obj++)
|
||||
{
|
||||
if (obj != player && obj->location == location)
|
||||
//排除玩家在玩家的位置这种蠢东西
|
||||
{
|
||||
if (count++ == 0)
|
||||
//我们需要保证找到一个东西之前他不会打印 you see
|
||||
{
|
||||
printf("You see:\n");
|
||||
}
|
||||
printf("%s\n", obj->description);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
//返回的是数目的数量,下一章对此做额外操作
|
||||
}
|
||||
```
|
||||
|
||||
在 *location.c* 中,命令环*顾四周的实现*,并根据新的数据结构进行调整。旧的位置数组被删除,变量 *locationOfPlayer* 也是如此。
|
||||
|
||||
## location.h
|
||||
|
||||
```c
|
||||
extern void executeLook(const char *noun);
|
||||
extern void executeGo(const char *noun);
|
||||
```
|
||||
|
||||
## location.c
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "object.h"
|
||||
#include "misc.h"
|
||||
#include "noun.h"
|
||||
|
||||
void executeLook(const char *noun)
|
||||
{
|
||||
if (noun != NULL && strcmp(noun, "around") == 0)
|
||||
{
|
||||
printf("You are in %s.\n", player->location->description);
|
||||
listObjectsAtLocation(player->location);
|
||||
//显示当前位置的玩家和物品
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("I don't understand what you want to see.\n");
|
||||
}
|
||||
}
|
||||
|
||||
void executeGo(const char *noun)
|
||||
{
|
||||
//消除了函数*executeGo*中的循环,代码更优雅了~
|
||||
OBJECT *obj = getVisible("where you want to go", noun);
|
||||
if (obj == NULL)
|
||||
{
|
||||
// already handled by getVisible
|
||||
}
|
||||
else if (obj->location == NULL && obj != player->location)
|
||||
{
|
||||
printf("OK.\n");
|
||||
player->location = obj;
|
||||
executeLook("around");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("You can't get much closer than this.\n");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
你可以自由添加对象哦,自己设计一个游戏道具一定很有意思
|
||||
|
||||
现在金银宝物散落一地可是我们捡不起来,下一章我们会试着解决问题
|
||||
|
||||
测试样例:
|
||||
|
||||
Welcome to Little Cave Adventure.
|
||||
You are in an open field.
|
||||
You see:
|
||||
a silver coin
|
||||
a burly guard
|
||||
|
||||
--> go cave
|
||||
OK.
|
||||
You are in a little cave.
|
||||
You see:
|
||||
a gold coin
|
||||
|
||||
--> go field
|
||||
OK.
|
||||
You are in an open field.
|
||||
You see:
|
||||
a silver coin
|
||||
a burly guard
|
||||
|
||||
--> go field
|
||||
You can't get much closer than this.
|
||||
|
||||
--> look around
|
||||
You are in an open field.
|
||||
You see:
|
||||
a silver coin
|
||||
a burly guard
|
||||
|
||||
--> quit
|
||||
|
||||
Bye!
|
||||
433
2023旧版内容/3.编程思维体系构建/3.4.6.5.捡起物品.md
Normal file
@@ -0,0 +1,433 @@
|
||||
# 5.捡起物品
|
||||
|
||||
在上一章中,我们制作了一个大大大数组来存储所有对象,包括玩家本人。
|
||||
|
||||
通过将玩家视为对象的一部分,你已经成为了游戏的重要组成部分,而不是以局外人的视角看世界。
|
||||
|
||||
这种方法的优势在具有玩家角色的游戏中变得最为明显,你可以换人 hhhhh。
|
||||
|
||||
玩家属性不再需要存储在单独的变量中;我们可以使用与任何其他对象相同的数据结构。所以玩家,作为一个对象必须具有以下特点:
|
||||
|
||||
- 所处位置(我在哪)
|
||||
- 玩家可能持有的任何物品的位置。
|
||||
|
||||
这使得某些常见操作非常容易实现:
|
||||
|
||||
|**Action**|**Typical Command**|**Example**|
|
||||
| ------------------------------ | --------- | ------------------------------------ |
|
||||
| 玩家从一个位置移动到另一个位置 | go | player->location = cave; |
|
||||
| 列出某个位置存在的项和参与者 | look | listObjectsAtLocation(cave); |
|
||||
| 玩家获取物品 | get | silver->location = player; |
|
||||
| 玩家掉落物品 | drop | silver->location = player->location; |
|
||||
| 列出玩家的物品栏 | inventory | listObjectsAtLocation(player); |
|
||||
| 玩家将物品赠送给演员 | give | listObjectsAtLocation(player); |
|
||||
| 玩家从演员那里收到物品 | ask | silver->location = player; |
|
||||
| 列出其他演员的库存 | examine | listObjectsAtLocation(guard); |
|
||||
|
||||
你可以尝试去使用这些命令(上面的前两个示例已经在上一章中实现了)。现在,我们将为玩家和非玩家角色介绍一些典型的**物品栏**操作(命令*获取*、*掉落*、*给予*、*询问*和*物品栏*)。
|
||||
|
||||
::: warning 🤔 思考题:
|
||||
你能不能尝试自己实现一下上面的命令?
|
||||
|
||||
如果你可以在不参考下面内容的情况下就写出基本内容会有很大收获的
|
||||
:::
|
||||
|
||||
## parsexec.c
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "location.h"
|
||||
#include "inventory.h" //这是一个新模块
|
||||
|
||||
bool parseAndExecute(char *input)
|
||||
{
|
||||
char *verb = strtok(input, " \n");
|
||||
char *noun = strtok(NULL, " \n");
|
||||
//第二次使用 strtok 要用 NULL 传参
|
||||
if (verb != NULL)
|
||||
{
|
||||
if (strcmp(verb, "quit") == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (strcmp(verb, "look") == 0) //使游戏识别上述命令
|
||||
{
|
||||
executeLook(noun);
|
||||
}
|
||||
else if (strcmp(verb, "go") == 0)
|
||||
{
|
||||
executeGo(noun);
|
||||
}
|
||||
else if (strcmp(verb, "get") == 0)
|
||||
{
|
||||
executeGet(noun);
|
||||
}
|
||||
else if (strcmp(verb, "drop") == 0)
|
||||
{
|
||||
executeDrop(noun);
|
||||
}
|
||||
else if (strcmp(verb, "give") == 0)
|
||||
{
|
||||
executeGive(noun);
|
||||
}
|
||||
else if (strcmp(verb, "ask") == 0)
|
||||
{
|
||||
executeAsk(noun);
|
||||
}
|
||||
else if (strcmp(verb, "inventory") == 0)
|
||||
{
|
||||
executeInventory();
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("I don't know how to '%s'.\n", verb);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
新命令由以下模块实现。
|
||||
|
||||
## inventory.h
|
||||
|
||||
```c
|
||||
extern void executeGet(const char *noun);
|
||||
extern void executeDrop(const char *noun);
|
||||
extern void executeAsk(const char *noun);
|
||||
extern void executeGive(const char *noun);
|
||||
extern void executeInventory(void);
|
||||
```
|
||||
|
||||
## inventory.c
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
#include "misc.h"
|
||||
#include "noun.h"
|
||||
#include "move.h"
|
||||
|
||||
void executeGet(const char *noun)
|
||||
{
|
||||
OBJECT *obj = getVisible("what you want to get", noun);
|
||||
if (obj == NULL)
|
||||
{
|
||||
// already handled by getVisible
|
||||
}
|
||||
else if (obj == player)
|
||||
{
|
||||
printf("You should not be doing that to yourself.\n");
|
||||
}
|
||||
else if (obj->location == player)
|
||||
{
|
||||
printf("You already have %s.\n", obj->description);
|
||||
}
|
||||
else if (obj->location == guard)
|
||||
{
|
||||
printf("You should ask %s nicely.\n", obj->location->description);
|
||||
}
|
||||
else
|
||||
{
|
||||
moveObject(obj, player);
|
||||
//用于将对象传输到其新位置
|
||||
}
|
||||
}
|
||||
|
||||
void executeDrop(const char *noun)
|
||||
{
|
||||
moveObject(getPossession(player, "drop", noun), player->location);
|
||||
}
|
||||
|
||||
void executeAsk(const char *noun)
|
||||
{
|
||||
moveObject(getPossession(actorHere(), "ask", noun), player);
|
||||
}
|
||||
|
||||
void executeGive(const char *noun)
|
||||
{
|
||||
moveObject(getPossession(player, "give", noun), actorHere());
|
||||
}
|
||||
|
||||
void executeInventory(void)
|
||||
{
|
||||
if (listObjectsAtLocation(player) == 0) //函数返回值告诉我们有多少个对象
|
||||
{
|
||||
printf("You are empty-handed.\n"); //告诉用户啥也没有
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
注意:由于动词名词比较好弄,命令 *ask* 和 *give* 只有一个参数:item。
|
||||
|
||||
::: warning 🤔 思考题:
|
||||
为什么我们要这样设计?
|
||||
|
||||
你能否为这些命令多加几个参数?
|
||||
:::
|
||||
|
||||
从本质上讲,*get*, *drop*, *give* and *ask 这些命令*除了将项目从一个地方移动到另一个地方之外,什么都不做。单个函数 *move 对象*可以对所有四个命令执行该操作。
|
||||
|
||||
## move.h
|
||||
|
||||
```c
|
||||
extern void moveObject(OBJECT *obj, OBJECT *to);
|
||||
```
|
||||
|
||||
## move.c
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
|
||||
static void describeMove(OBJECT *obj, OBJECT *to) //确认移动命令
|
||||
{
|
||||
if (to == player->location)
|
||||
{
|
||||
printf("You drop %s.\n", obj->description);
|
||||
}
|
||||
else if (to != player)
|
||||
{
|
||||
printf(to == guard ? "You give %s to %s.\n" : "You put %s in %s.\n",
|
||||
obj->description, to->description);
|
||||
}
|
||||
else if (obj->location == player->location)
|
||||
{
|
||||
printf("You pick up %s.\n", obj->description);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("You get %s from %s.\n",
|
||||
obj->description, obj->location->description);
|
||||
}
|
||||
}
|
||||
|
||||
void moveObject(OBJECT *obj, OBJECT *to)
|
||||
{
|
||||
if (obj == NULL) //不移动的各种条件
|
||||
{
|
||||
// already handled by getVisible or getPossession
|
||||
}
|
||||
else if (to == NULL)
|
||||
{
|
||||
printf("There is nobody here to give that to.\n");
|
||||
}
|
||||
else if (obj->location == NULL) //有些物体无法拾取,具体识别条件将在后面得到改进
|
||||
{
|
||||
printf("That is way too heavy.\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
describeMove(obj, to);
|
||||
obj->location = to; //移动对象
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
::: warning 🤔 思考题:识别一些我们拿不了的物品需要考虑什么因素?
|
||||
:::
|
||||
|
||||
命令“get”使用函数*getVisible*将名词转换为 object,就像命令“go”一样;请参阅上一章。但是对于对玩家(或其他一些参与者)已经持有的对象进行*drop*, *ask*, *give 等*命令时,我们需要稍微不同的东西。我们将在 *noun.c* 中添加一个函数 *getPossession*。
|
||||
|
||||
## noun.h
|
||||
|
||||
```c
|
||||
extern OBJECT *getVisible(const char *intention, const char *noun);
|
||||
extern OBJECT *getPossession(OBJECT *from, const char *verb, const char *noun);
|
||||
```
|
||||
|
||||
## noun.c
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "object.h"
|
||||
|
||||
static bool objectHasTag(OBJECT *obj, const char *noun)
|
||||
{
|
||||
return noun != NULL && *noun != '\0' && strcmp(noun, obj->tag) == 0;
|
||||
}
|
||||
|
||||
static OBJECT *getObject(const char *noun)
|
||||
{
|
||||
OBJECT *obj, *res = NULL;
|
||||
for (obj = objs; obj < endOfObjs; obj++)
|
||||
{
|
||||
if (objectHasTag(obj, noun))
|
||||
{
|
||||
res = obj;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
OBJECT *getVisible(const char *intention, const char *noun)
|
||||
{
|
||||
OBJECT *obj = getObject(noun);
|
||||
if (obj == NULL)
|
||||
{
|
||||
printf("I don't understand %s.\n", intention);
|
||||
}
|
||||
else if (!(obj == player ||
|
||||
obj == player->location ||
|
||||
obj->location == player ||
|
||||
obj->location == player->location ||
|
||||
obj->location == NULL ||
|
||||
obj->location->location == player ||
|
||||
obj->location->location == player->location))
|
||||
{
|
||||
printf("You don't see any %s here.\n", noun);
|
||||
obj = NULL;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
OBJECT *getPossession(OBJECT *from, const char *verb, const char *noun)
|
||||
{
|
||||
OBJECT *obj = NULL;
|
||||
if (from == NULL)
|
||||
{
|
||||
printf("I don't understand who you want to %s.\n", verb);
|
||||
}
|
||||
else if ((obj = getObject(noun)) == NULL)
|
||||
{
|
||||
printf("I don't understand what you want to %s.\n", verb);
|
||||
}
|
||||
else if (obj == from)
|
||||
{
|
||||
printf("You should not be doing that to %s.\n", obj->description);
|
||||
obj = NULL;
|
||||
}
|
||||
else if (obj->location != from)
|
||||
{
|
||||
if (from == player)
|
||||
{
|
||||
printf("You are not holding any %s.\n", noun);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("There appears to be no %s you can get from %s.\n",
|
||||
noun, from->description);
|
||||
}
|
||||
obj = NULL;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
```
|
||||
|
||||
注意:新函数(45-75 行) *getPossession* 是 *getObject* 的装饰器(wrapper)(参见第 52 行),它要么返回匹配的对象,要么返回 NULL(如果没有合适的对象与名词匹配)。
|
||||
|
||||
函数 *actor 这里*用于命令 *give* 和 *ask*,但它也可能由其他命令调用。所以我们在*misc.c*中定义了它。
|
||||
|
||||
## misc.h
|
||||
|
||||
```c
|
||||
extern OBJECT *actorHere(void);
|
||||
extern int listObjectsAtLocation(OBJECT *location);
|
||||
```
|
||||
|
||||
## misc.c
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
|
||||
OBJECT *actorHere(void)
|
||||
{
|
||||
OBJECT *obj;
|
||||
for (obj = objs; obj < endOfObjs; obj++)
|
||||
{
|
||||
if (obj->location == player->location && obj == guard)
|
||||
{
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int listObjectsAtLocation(OBJECT *location)
|
||||
{
|
||||
int count = 0;
|
||||
OBJECT *obj;
|
||||
for (obj = objs; obj < endOfObjs; obj++)
|
||||
{
|
||||
if (obj != player && obj->location == location)
|
||||
{
|
||||
if (count++ == 0)
|
||||
{
|
||||
printf("You see:\n");
|
||||
}
|
||||
printf("%s\n", obj->description);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
```
|
||||
|
||||
::: warning 🤔 思考题:上面第四行中的函数 actorHere 返回的指针指向什么?
|
||||
:::
|
||||
|
||||
在第 9 行中,有一个详尽的,硬编码的非玩家角色列表(到目前为止,只有一个:*守卫*)。
|
||||
|
||||
在第 10 章中,我们将开始使用属性作为区分角色与项目和其他非参与者对象的更优雅方式。
|
||||
|
||||
测试样例
|
||||
|
||||
Welcome to Little Cave Adventure.
|
||||
You are in an open field.
|
||||
You see:
|
||||
a silver coin
|
||||
a burly guard
|
||||
|
||||
--> get silver
|
||||
You pick up a silver coin.
|
||||
|
||||
--> inventory
|
||||
You see:
|
||||
a silver coin
|
||||
|
||||
--> look around
|
||||
You are in an open field.
|
||||
You see:
|
||||
a burly guard
|
||||
|
||||
--> give silver
|
||||
You give a silver coin to a burly guard.
|
||||
|
||||
--> inventory
|
||||
You are empty-handed.
|
||||
|
||||
--> ask silver
|
||||
You get a silver coin from a burly guard.
|
||||
|
||||
--> inventory
|
||||
You see:
|
||||
a silver coin
|
||||
|
||||
--> go cave
|
||||
OK.
|
||||
You are in a little cave.
|
||||
You see:
|
||||
a gold coin
|
||||
|
||||
--> give silver
|
||||
There is nobody here to give that to.
|
||||
|
||||
--> drop silver
|
||||
You drop a silver coin.
|
||||
|
||||
--> look around
|
||||
You are in a little cave.
|
||||
You see:
|
||||
a silver coin
|
||||
a gold coin
|
||||
|
||||
--> inventory
|
||||
You are empty-handed.
|
||||
|
||||
--> quit
|
||||
|
||||
Bye!
|
||||
338
2023旧版内容/3.编程思维体系构建/3.4.6.6.绘制地图.md
Normal file
@@ -0,0 +1,338 @@
|
||||
# 6.绘制地图
|
||||
|
||||
作为一个 RPG 游戏怎么能没有地图呢,*是时候绘制地图了!*
|
||||
|
||||
绘制地图的最佳工具始终是:一支铅笔和一张纸。基本地图由**位置**(矩形)组成,由道路(箭头)连接。我们已经在第 3 章中创建了位置,现在我们将开始添加道路。
|
||||
|
||||
在虚拟世界中,“道路”可能是连接两个位置的任何东西:一条路,一扇门,沙漠中。基本上,一段经文具有以下属性:
|
||||
|
||||
- 起点(位置)。
|
||||
- 目标(位置)。
|
||||
- 叙述性描述,例如“森林小径”。
|
||||
- 在 *go* 命令中往哪里走的描述性标记
|
||||
|
||||
考虑到这些属性,第 4 章中定义的结构对象就非常适合存储道路了。事实上,一个道路与一个项目或 NPC 并没有太大的不同,它作为“可见出口”存在于某个位置(该位置是起点)。它只是与某些命令的行为不同,特别是命令“go”:应用于道路,*go*将改变玩家的位置。
|
||||
|
||||
```c
|
||||
struct object {
|
||||
const char *description;
|
||||
const char *tag;
|
||||
struct object *location;
|
||||
struct object *destination;
|
||||
};
|
||||
```
|
||||
|
||||
注意:
|
||||
|
||||
- 显然,*目的地*在大多数其他对象(物品,NPC)中都没有使用
|
||||
- 通道总是朝一个方向运行;要双向连接两个位置,我们总是必须创建两个单独的通道。乍一看,这似乎很笨拙,但它确实给了我们很大的灵活性来完善命令“go”的行为
|
||||
- 在大地图上,你可能会发现手动创建所有通道很乏味。所以,我强烈建议你使用自定义工具*生成*地图中重复性更强的部分。这里不会介绍这一点,但您可能会在第 9 章中找到一些灵感,我们将在其中讨论自动胜场。
|
||||
|
||||
::: warning 🤔 思考题:为什么创建两个通道可以使我们的程序更加灵活?
|
||||
:::
|
||||
|
||||
接下来我们将展开对象数组
|
||||
|
||||
## object.h
|
||||
|
||||
```c
|
||||
typedef struct object {
|
||||
const char *description;
|
||||
const char *tag;
|
||||
struct object *location;
|
||||
struct object *destination;
|
||||
} OBJECT;
|
||||
|
||||
extern OBJECT objs[];
|
||||
|
||||
#define field (objs + 0) //是不是觉得这个很熟悉
|
||||
#define cave (objs + 1)
|
||||
#define silver (objs + 2)
|
||||
#define gold (objs + 3)
|
||||
#define guard (objs + 4)
|
||||
#define player (objs + 5)
|
||||
#define intoCave (objs + 6)
|
||||
#define exitCave (objs + 7)
|
||||
|
||||
#define endOfObjs (objs + 8)
|
||||
```
|
||||
|
||||
## object.c
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
|
||||
OBJECT objs[] = {
|
||||
{"an open field" , "field" , NULL , NULL },
|
||||
{"a little cave" , "cave" , NULL , NULL },
|
||||
{"a silver coin" , "silver" , field, NULL },
|
||||
{"a gold coin" , "gold" , cave , NULL },
|
||||
{"a burly guard" , "guard" , field, NULL },
|
||||
{"yourself" , "yourself", field, NULL },
|
||||
{"a cave entrance", "entrance", field, cave },
|
||||
{"an exit" , "exit" , cave , field }
|
||||
};
|
||||
```
|
||||
|
||||
我们将在 *misc.c* 中添加一个小的帮助函数,以确定两个给定位置之间是否存在通道。
|
||||
|
||||
## misc.h
|
||||
|
||||
```c
|
||||
extern OBJECT *getPassage(OBJECT *from, OBJECT *to);
|
||||
extern OBJECT *actorHere(void);
|
||||
extern int listObjectsAtLocation(OBJECT *location);
|
||||
```
|
||||
|
||||
## misc.c
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
|
||||
OBJECT *getPassage(OBJECT *from, OBJECT *to)
|
||||
{
|
||||
if (from != NULL && to != NULL)
|
||||
{
|
||||
OBJECT *obj;
|
||||
for (obj = objs; obj < endOfObjs; obj++) //寻找物品之间是否具有通道
|
||||
{
|
||||
if (obj->location == from && obj->destination == to)
|
||||
{
|
||||
return obj; //找到了
|
||||
}
|
||||
}
|
||||
}
|
||||
return NULL;//找不到
|
||||
}
|
||||
|
||||
OBJECT *actorHere(void)
|
||||
{
|
||||
OBJECT *obj;
|
||||
for (obj = objs; obj < endOfObjs; obj++)
|
||||
{
|
||||
if (obj->location == player->location && obj == guard)
|
||||
{
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int listObjectsAtLocation(OBJECT *location)
|
||||
{
|
||||
int count = 0;
|
||||
OBJECT *obj;
|
||||
for (obj = objs; obj < endOfObjs; obj++)
|
||||
{
|
||||
if (obj != player && obj->location == location)
|
||||
{
|
||||
if (count++ == 0)
|
||||
{
|
||||
printf("You see:\n");
|
||||
}
|
||||
printf("%s\n", obj->description);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
```
|
||||
|
||||
我们将在命令“go”的实现中使用新功能*getPassage*来确定是否存在可以将玩家带到所需位置的通道。
|
||||
|
||||
## location.h
|
||||
|
||||
```c
|
||||
extern void executeLook(const char *noun);
|
||||
extern void executeGo(const char *noun);
|
||||
```
|
||||
|
||||
## location.c
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "object.h"
|
||||
#include "misc.h"
|
||||
#include "noun.h"
|
||||
|
||||
void executeLook(const char *noun)
|
||||
{
|
||||
if (noun != NULL && strcmp(noun, "around") == 0)
|
||||
{
|
||||
printf("You are in %s.\n", player->location->description);
|
||||
listObjectsAtLocation(player->location);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("I don't understand what you want to see.\n");
|
||||
}
|
||||
}
|
||||
|
||||
void executeGo(const char *noun)
|
||||
{
|
||||
OBJECT *obj = getVisible("where you want to go", noun);
|
||||
if (obj == NULL)
|
||||
{
|
||||
// already handled by getVisible
|
||||
}
|
||||
else if (getPassage(player->location, obj) != NULL)
|
||||
//go 只会在有地方的时候才会运行起来
|
||||
{
|
||||
printf("OK.\n");
|
||||
player->location = obj;
|
||||
executeLook("around");
|
||||
}
|
||||
else if (obj->location != player->location)
|
||||
{
|
||||
printf("You don't see any %s here.\n", noun);
|
||||
}
|
||||
else if (obj->destination != NULL)
|
||||
{
|
||||
printf("OK.\n");
|
||||
player->location = obj->destination;
|
||||
executeLook("around");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("You can't get much closer than this.\n");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
我们还将使用新功能*getPassage*来确定从玩家站立的位置是否可以看到某个位置。未通过通道连接到当前位置的位置不被视为可见。
|
||||
|
||||
## noun.h
|
||||
|
||||
```c
|
||||
extern OBJECT *getVisible(const char *intention, const char *noun);
|
||||
extern OBJECT *getPossession(OBJECT *from, const char *verb, const char *noun);
|
||||
```
|
||||
|
||||
## noun.c
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "object.h"
|
||||
#include "misc.h"
|
||||
|
||||
static bool objectHasTag(OBJECT *obj, const char *noun)
|
||||
{
|
||||
return noun != NULL && *noun != '\0' && strcmp(noun, obj->tag) == 0;
|
||||
}
|
||||
|
||||
static OBJECT *getObject(const char *noun)
|
||||
{
|
||||
OBJECT *obj, *res = NULL;
|
||||
for (obj = objs; obj < endOfObjs; obj++)
|
||||
{
|
||||
if (objectHasTag(obj, noun))
|
||||
{
|
||||
res = obj;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
OBJECT *getVisible(const char *intention, const char *noun)
|
||||
{
|
||||
OBJECT *obj = getObject(noun);
|
||||
|
||||
if (obj == NULL)
|
||||
{
|
||||
printf("I don't understand %s.\n", intention);
|
||||
}
|
||||
else if (!(obj == player ||
|
||||
obj == player->location ||
|
||||
obj->location == player ||
|
||||
obj->location == player->location ||
|
||||
getPassage(player->location, obj) != NULL || //检查两个位置是否相邻
|
||||
(obj->location != NULL &&
|
||||
(obj->location->location == player ||
|
||||
obj->location->location == player->location))))
|
||||
{
|
||||
printf("You don't see any %s here.\n", noun);
|
||||
obj = NULL;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
OBJECT *getPossession(OBJECT *from, const char *verb, const char *noun)
|
||||
{
|
||||
OBJECT *obj = NULL;
|
||||
if (from == NULL)
|
||||
{
|
||||
printf("I don't understand who you want to %s.\n", verb);
|
||||
}
|
||||
else if ((obj = getObject(noun)) == NULL)
|
||||
{
|
||||
printf("I don't understand what you want to %s.\n", verb);
|
||||
}
|
||||
else if (obj == from)
|
||||
{
|
||||
printf("You should not be doing that to %s.\n", obj->description);
|
||||
obj = NULL;
|
||||
}
|
||||
else if (obj->location != from)
|
||||
{
|
||||
if (from == player)
|
||||
{
|
||||
printf("You are not holding any %s.\n", noun);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("There appears to be no %s you can get from %s.\n",
|
||||
noun, from->description);
|
||||
}
|
||||
obj = NULL;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
```
|
||||
|
||||
显然,此示例中的地图是微不足道的:只有两个位置,并且它们在两个方向上都连接在一起。第 12 章将增加第三个地点。
|
||||
|
||||
::: warning 🤔 思考题:
|
||||
你能否绘制一张更精细的地图,并将其变成对象列表(位置和通道)
|
||||
|
||||
注:不用当成任务,自行实验即可
|
||||
|
||||
:::
|
||||
|
||||
输出样例
|
||||
|
||||
Welcome to Little Cave Adventure.
|
||||
You are in an open field.
|
||||
You see:
|
||||
a silver coin
|
||||
a burly guard
|
||||
a cave entrance
|
||||
|
||||
--> go entrance
|
||||
OK.
|
||||
You are in a little cave.
|
||||
You see:
|
||||
a gold coin
|
||||
an exit
|
||||
|
||||
--> go exit
|
||||
OK.
|
||||
You are in an open field.
|
||||
You see:
|
||||
a silver coin
|
||||
a burly guard
|
||||
a cave entrance
|
||||
|
||||
--> go cave
|
||||
OK.
|
||||
You are in a little cave.
|
||||
You see:
|
||||
a gold coin
|
||||
an exit
|
||||
|
||||
--> quit
|
||||
|
||||
Bye!
|
||||
510
2023旧版内容/3.编程思维体系构建/3.4.6.7.增大距离.md
Normal file
@@ -0,0 +1,510 @@
|
||||
# 7.增大距离
|
||||
|
||||
*一个典型的冒险包含许多谜题。众所周知,[Infocom](https://en.wikipedia.org/wiki/Infocom)的冒险很难完成。解决每个难题可能需要数周甚至数月的反复试验。*
|
||||
|
||||
*当玩家操纵角色失败后,如果只是返回“你不能这么操作”来回应玩家,会很 nt,很没意思。*
|
||||
|
||||
*它忽略了电脑游戏的一个重要方面,而这也是生活本身的一部分:玩家必须从错误中吸取教训。*
|
||||
|
||||
当你的游戏反复输入东西都是,你不能这样做的时候,会显得很无聊的。
|
||||
|
||||
*冒险游戏至少应该做的是解释为什么玩家的命令无法完成:“你不能这样做,因为......”这有助于使虚拟世界更具说服力,故事更可信,游戏更有趣。*
|
||||
|
||||
我们已经付出了相当大的努力让游戏解释**为什么**某些命令是无效的。只需看看*名词.c,inventory.c,location.c*,*move.c*中的许多*printf*调用。但随着游戏变得越来越复杂,这正成为一个相当大的负担。我们需要一种更结构化的方法来检测和处理错误情况。这就是我们在本章中将要讨论的内容。
|
||||
|
||||
大多数命令对一个或多个对象进行操作,例如:
|
||||
|
||||
- 玩家拿起一件物品,然后把它交给另一个 NPC。
|
||||
- 玩家沿着一条通道到另一个位置。
|
||||
|
||||
首先要检查的(在[解析器](http://en.wikipedia.org/wiki/Parsing)捕获检测是否会有明显[拼写错误](http://en.wikipedia.org/wiki/Typographical_error)之后)是这些对象**是否存在**;
|
||||
|
||||
失败应该导致类似“这里没有...“或”你看不到任何东西...”等文字出现。在本章中,我们将构建一个通用函数,每个命令都可以使用它来找出玩家是否在可及的范围内。
|
||||
|
||||
你可能认为我们只需要区分两种情况:对象在这里,或者它不在这里。
|
||||
|
||||
但是许多命令需要更多的渐变,而不仅仅是“这里”和“不在这里”。例子:
|
||||
|
||||
- 要使用武器或工具,玩家必须持有它;仅仅在现场存在是不够的。
|
||||
- 你不能放下一个你没拿起来的道具,也不能拿起一个已经有的东西
|
||||
- 如果你跟商人买东西的时候,他店里东西你不能随便拿
|
||||
|
||||
| distSelf | 对象是玩家 | object == player |
|
||||
| ----------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
||||
| distHeld | 玩家持有物体 | object->location == player |
|
||||
| distHeldContained | 玩家拿着另一个包含该物体的物体(例如袋子) | object->location != NULL &&<br/>object->location->location == player |
|
||||
| distLocation | 对象是玩家的位置 | object == player->location |
|
||||
| distHere | 对象位于玩家的位置 | object->location == player->location |
|
||||
| distHereContained | 一个物体(NPC 或“容器”)存在于玩家的位置,正在拿着另一个物体 | object->location != NULL &&<br/>object->location->location == player->location |
|
||||
| distOverthere | 对象是附近的位置 | getPassage(player->location, object) != NULL |
|
||||
|
||||
第一种情况(对象是玩家)可能看起来微不足道,但它仍然很重要。例如,命令“examine yourself”*不应该*返回“这里没有你自己”。
|
||||
|
||||
我试图遵循一个逻辑顺序:附近的事物最高优先级,随后优先级会变低。
|
||||
|
||||
| distNotHere | 对象不在这里(或看起来)不在这里 | |
|
||||
| ----------------- | -------------------------------- | -------------- |
|
||||
| distUnknownObject | 解析器无法识别输入的名词 | object == NULL |
|
||||
|
||||
请注意,我们有七种不同的“这里”案例,但只有一种是“不在这里”。这是因为通常,游戏只需要提供有关玩家可以感知的事物的信息。如果它不在这里,那么就没什么可说的了。
|
||||
|
||||
在最左边的列中,我们为每个案例提出了一个符号名称。我们将在名为 **DISTANCE** 的[枚举](http://en.wikipedia.org/wiki/Enumerated_type)中收集这些名称。
|
||||
|
||||
```c
|
||||
typedef enum {
|
||||
distSelf,
|
||||
distHeld,
|
||||
distHeldContained,
|
||||
distLocation,
|
||||
distHere,
|
||||
distHereContained,
|
||||
distOverthere,
|
||||
distNotHere,
|
||||
distUnknownObject
|
||||
} DISTANCE;
|
||||
```
|
||||
|
||||
::: warning 💡 typedef 以及枚举类 enum 之前有接触过吗?没有接触过的话就去学习一下吧。
|
||||
:::
|
||||
|
||||
在最右边的列中,我们为每个情况提出了一个满足条件。通过一些重新洗牌,我们可以很容易地将其转换为计算对象“距离”的函数(从玩家的角度来看):
|
||||
|
||||
```c
|
||||
DISTANCE getDistance(OBJECT *from, OBJECT *to)
|
||||
{
|
||||
return to == NULL ? distUnknownObject :
|
||||
to == from ? distSelf :
|
||||
to->location == from ? distHeld :
|
||||
to == from->location ? distLocation :
|
||||
to->location == from->location ? distHere :
|
||||
getPassage(from->location, to) != NULL ? distOverthere :
|
||||
to->location == NULL ? distNotHere :
|
||||
to->location->location == from ? distHeldContained :
|
||||
to->location->location == from->location ? distHereContained :
|
||||
distNotHere;
|
||||
}
|
||||
```
|
||||
|
||||
::: warning 🤔 思考题:
|
||||
你是否有其他方法实现这个功能?
|
||||
|
||||
注:自行实验即可
|
||||
:::
|
||||
|
||||
就这样!我们可以调用此函数并对其返回值进行比较。例如,我们在 noun*.c*中有以下代码:
|
||||
|
||||
```c
|
||||
else if (!(obj == player ||
|
||||
obj == player->location ||
|
||||
obj->location == player ||
|
||||
obj->location == player->location ||
|
||||
getPassage(player->location, obj) != NULL ||
|
||||
(obj->location != NULL &&
|
||||
(obj->location->location == player ||
|
||||
obj->location->location == player->location))))
|
||||
```
|
||||
|
||||
现在,我们可以用适当的距离检查替换每个子条件:
|
||||
|
||||
```c
|
||||
else if (!(getDistance(player, obj) == distSelf ||
|
||||
getDistance(player, obj) == distLocation ||
|
||||
getDistance(player, obj) == distHeld ||
|
||||
getDistance(player, obj) == distHere ||
|
||||
getDistance(player, obj) == distOverthere ||
|
||||
getDistance(player, obj) == distHeldContained ||
|
||||
getDistance(player, obj) == distHereContained))
|
||||
```
|
||||
|
||||
这可以简化为:
|
||||
|
||||
```c
|
||||
else if (getDistance(player, obj) >= distNotHere)
|
||||
```
|
||||
|
||||
::: warning 🤔 尝试理解一下这样做的意义
|
||||
:::
|
||||
|
||||
这只是一个例子,让你对这个概念有所了解;您将在下面找到*noun.c*的实际实现,看起来略有不同。
|
||||
|
||||
是时候把事情落实到位了。枚举 *DISTANCE* 和函数 *getDistance* 的定义被添加到 *misc.h* 和 *misc.c* 中,因为我们将在多个模块中使用它们。
|
||||
|
||||
## misc.h
|
||||
|
||||
```c
|
||||
typedef enum {
|
||||
distSelf,
|
||||
distHeld,
|
||||
distHeldContained,
|
||||
distLocation,
|
||||
distHere,
|
||||
distHereContained,
|
||||
distOverthere,
|
||||
distNotHere,
|
||||
distUnknownObject
|
||||
} DISTANCE;
|
||||
|
||||
extern bool isHolding(OBJECT *container, OBJECT *obj);
|
||||
//是否持有物体
|
||||
extern OBJECT *getPassage(OBJECT *from, OBJECT *to);
|
||||
//获取通道
|
||||
extern DISTANCE getDistance(OBJECT *from, OBJECT *to);
|
||||
//计算距离
|
||||
extern OBJECT *actorHere(void);
|
||||
extern int listObjectsAtLocation(OBJECT *location);
|
||||
```
|
||||
|
||||
## misc.c
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
#include "misc.h"
|
||||
|
||||
bool isHolding(OBJECT *container, OBJECT *obj)
|
||||
{
|
||||
return obj != NULL && obj->location == container;
|
||||
}
|
||||
|
||||
OBJECT *getPassage(OBJECT *from, OBJECT *to)
|
||||
{
|
||||
if (from != NULL && to != NULL)
|
||||
{
|
||||
OBJECT *obj;
|
||||
for (obj = objs; obj < endOfObjs; obj++)
|
||||
{
|
||||
if (isHolding(from, obj) && obj->destination == to)
|
||||
{
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
DISTANCE getDistance(OBJECT *from, OBJECT *to)
|
||||
{
|
||||
return to == NULL ? distUnknownObject :
|
||||
to == from ? distSelf :
|
||||
isHolding(from, to) ? distHeld :
|
||||
isHolding(to, from) ? distLocation :
|
||||
isHolding(from->location, to) ? distHere :
|
||||
isHolding(from, to->location) ? distHeldContained :
|
||||
isHolding(from->location, to->location) ? distHereContained :
|
||||
getPassage(from->location, to) != NULL ? distOverthere :
|
||||
distNotHere;
|
||||
}
|
||||
|
||||
OBJECT *actorHere(void)
|
||||
{
|
||||
OBJECT *obj;
|
||||
for (obj = objs; obj < endOfObjs; obj++)
|
||||
{
|
||||
if (isHolding(player->location, obj) && obj == guard)
|
||||
{
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int listObjectsAtLocation(OBJECT *location)
|
||||
{
|
||||
int count = 0;
|
||||
OBJECT *obj;
|
||||
for (obj = objs; obj < endOfObjs; obj++)
|
||||
{
|
||||
if (obj != player && isHolding(location, obj))
|
||||
{
|
||||
if (count++ == 0)
|
||||
{
|
||||
printf("You see:\n");
|
||||
}
|
||||
printf("%s\n", obj->description);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
```
|
||||
|
||||
注意:isHolding 这个函数之后我们将在各个地方使用
|
||||
|
||||
## location.h
|
||||
|
||||
```c
|
||||
extern void executeLook(const char *noun);
|
||||
extern void executeGo(const char *noun);
|
||||
```
|
||||
|
||||
在函数 *executeGo* 中,我们可以用检查距离来替换大多数 *if* 条件。
|
||||
|
||||
## location.c
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "object.h"
|
||||
#include "misc.h"
|
||||
#include "noun.h"
|
||||
|
||||
void executeLook(const char *noun)
|
||||
{
|
||||
if (noun != NULL && strcmp(noun, "around") == 0)
|
||||
{
|
||||
printf("You are in %s.\n", player->location->description);
|
||||
listObjectsAtLocation(player->location);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("I don't understand what you want to see.\n");
|
||||
}
|
||||
}
|
||||
|
||||
void executeGo(const char *noun)
|
||||
{
|
||||
OBJECT *obj = getVisible("where you want to go", noun);
|
||||
switch (getDistance(player, obj))
|
||||
{
|
||||
case distOverthere:
|
||||
printf("OK.\n");
|
||||
player->location = obj;
|
||||
executeLook("around");
|
||||
break;
|
||||
case distNotHere:
|
||||
printf("You don't see any %s here.\n", noun);
|
||||
break;
|
||||
case distUnknownObject:
|
||||
// already handled by getVisible
|
||||
break;
|
||||
default:
|
||||
//上述情况均为出现
|
||||
if (obj->destination != NULL)
|
||||
{
|
||||
printf("OK.\n");
|
||||
player->location = obj->destination;
|
||||
executeLook("around");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("You can't get much closer than this.\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
::: warning 🤔 思考题:你能否为 switch 函数增加更多 case 来完善判断条件?
|
||||
:::
|
||||
|
||||
函数 *executeGet* 也是如此。
|
||||
|
||||
## **inventory.h**
|
||||
|
||||
```c
|
||||
extern void executeGet(const char *noun);
|
||||
extern void executeDrop(const char *noun);
|
||||
extern void executeAsk(const char *noun);
|
||||
extern void executeGive(const char *noun);
|
||||
extern void executeInventory(void);
|
||||
```
|
||||
|
||||
## **inventory.c**
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
#include "misc.h"
|
||||
#include "noun.h"
|
||||
#include "move.h"
|
||||
|
||||
void executeGet(const char *noun)
|
||||
{
|
||||
OBJECT *obj = getVisible("what you want to get", noun);
|
||||
switch (getDistance(player, obj))
|
||||
{
|
||||
case distSelf:
|
||||
printf("You should not be doing that to yourself.\n");
|
||||
break;
|
||||
case distHeld:
|
||||
printf("You already have %s.\n", obj->description);
|
||||
break;
|
||||
case distOverthere:
|
||||
printf("Too far away, move closer please.\n");
|
||||
break;
|
||||
case distUnknownObject:
|
||||
// already handled by getVisible
|
||||
break;
|
||||
default:
|
||||
if (obj->location == guard)
|
||||
{
|
||||
printf("You should ask %s nicely.\n", obj->location->description);
|
||||
}
|
||||
else
|
||||
{
|
||||
moveObject(obj, player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void executeDrop(const char *noun)
|
||||
{
|
||||
moveObject(getPossession(player, "drop", noun), player->location);
|
||||
}
|
||||
|
||||
void executeAsk(const char *noun)
|
||||
{
|
||||
moveObject(getPossession(actorHere(), "ask", noun), player);
|
||||
}
|
||||
|
||||
void executeGive(const char *noun)
|
||||
{
|
||||
moveObject(getPossession(player, "give", noun), actorHere());
|
||||
}
|
||||
|
||||
void executeInventory(void)
|
||||
{
|
||||
if (listObjectsAtLocation(player) == 0)
|
||||
{
|
||||
printf("You are empty-handed.\n");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
最后,我们将调整 noun*.c*中的约束。我们正在向函数*getObject*添加两个参数,以便找到特定名词的匹配项,同时忽略任何被认为不存在的对象。这将在下一章中得到真正的回报,我们将在下一章中介绍具有相同标签的不同对象。
|
||||
|
||||
## noun.h
|
||||
|
||||
```c
|
||||
extern OBJECT *getVisible(const char *intention, const char *noun);
|
||||
extern OBJECT *getPossession(OBJECT *from, const char *verb, const char *noun);
|
||||
```
|
||||
|
||||
## noun.c
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "object.h"
|
||||
#include "misc.h"
|
||||
|
||||
static bool objectHasTag(OBJECT *obj, const char *noun)
|
||||
{
|
||||
return noun != NULL && *noun != '\0' && strcmp(noun, obj->tag) == 0;
|
||||
}
|
||||
|
||||
static OBJECT *getObject(const char *noun, OBJECT *from, DISTANCE maxDistance)
|
||||
{
|
||||
OBJECT *obj, *res = NULL;
|
||||
for (obj = objs; obj < endOfObjs; obj++)
|
||||
{
|
||||
if (objectHasTag(obj, noun) && getDistance(from, obj) <= maxDistance)
|
||||
//只考虑与对象距离小于或等于最大距离的物体
|
||||
{
|
||||
res = obj;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
OBJECT *getVisible(const char *intention, const char *noun)
|
||||
{
|
||||
OBJECT *obj = getObject(noun, player, distOverthere);
|
||||
//玩家看不到的自动忽略
|
||||
if (obj == NULL)
|
||||
{
|
||||
if (getObject(noun, player, distNotHere) == NULL)
|
||||
{
|
||||
printf("I don't understand %s.\n", intention);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("You don't see any %s here.\n", noun);
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
OBJECT *getPossession(OBJECT *from, const char *verb, const char *noun)
|
||||
{
|
||||
OBJECT *obj = NULL;
|
||||
if (from == NULL)
|
||||
{
|
||||
printf("I don't understand who you want to %s.\n", verb);
|
||||
}
|
||||
else if ((obj = getObject(noun, from, distHeldContained)) == NULL)
|
||||
//限制范围
|
||||
{
|
||||
if (getObject(noun, player, distNotHere) == NULL)
|
||||
{
|
||||
printf("I don't understand what you want to %s.\n", verb);
|
||||
}
|
||||
else if (from == player)
|
||||
{
|
||||
printf("You are not holding any %s.\n", noun);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("There appears to be no %s you can get from %s.\n",
|
||||
noun, from->description);
|
||||
}
|
||||
}
|
||||
else if (obj == from)
|
||||
{
|
||||
printf("You should not be doing that to %s.\n", obj->description);
|
||||
obj = NULL;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
```
|
||||
|
||||
::: warning 🤔 思考题:你能理解什么时候加 const,什么时候不用吗?
|
||||
:::
|
||||
|
||||
在本章中,*距离*的概念主要用于在游戏可以给用户的不同响应之间进行选择。但是,距离的好处并不局限于**输出**端;它可以同样很好地用于在**输入**端进行改进。在下一章中,我们将使用距离来提高对用户输入的名词的识别。
|
||||
|
||||
输出样例
|
||||
|
||||
Welcome to Little Cave Adventure.
|
||||
You are in an open field.
|
||||
You see:
|
||||
a silver coin
|
||||
a burly guard
|
||||
a cave entrance
|
||||
|
||||
--> go guard
|
||||
You can't get much closer than this.
|
||||
|
||||
--> give silver
|
||||
You are not holding any silver.
|
||||
|
||||
--> ask silver
|
||||
There appears to be no silver you can get from a burly guard.
|
||||
|
||||
--> get silver
|
||||
You pick up a silver coin.
|
||||
|
||||
--> get gold
|
||||
You don't see any gold here.
|
||||
|
||||
--> give silver
|
||||
You give a silver coin to a burly guard.
|
||||
|
||||
--> go cave
|
||||
OK.
|
||||
You are in a little cave.
|
||||
You see:
|
||||
a gold coin
|
||||
an exit
|
||||
|
||||
--> get gold
|
||||
You pick up a gold coin.
|
||||
|
||||
--> give gold
|
||||
There is nobody here to give that to.
|
||||
|
||||
--> quit
|
||||
|
||||
Bye!
|
||||
254
2023旧版内容/3.编程思维体系构建/3.4.6.8.移动方向.md
Normal file
@@ -0,0 +1,254 @@
|
||||
# 8.移动方向
|
||||
|
||||
*传统的文本冒险使用[指南针方向](https://en.wikipedia.org/wiki/Cardinal_direction)进行导航。*
|
||||
|
||||
例如,我们在第 6 章中绘制的地图上,玩家可能想**向东**移动,从田野移动到洞穴。我们可以通过给连接**Cave**的通道标上“east”来实现这一点。但是,我们首先需要解决两个问题。
|
||||
|
||||
1. 我们可能仍然想把这段通道称为“entrance”和“east”。但现在,一个对象只能有一个标签。
|
||||
2. 在更大的地图上,具有更多的位置和道路,标签“east”将被定义多次。到目前为止,标签在我们的游戏中是全球独一无二的,没有重复项。
|
||||
|
||||
::: warning 🤔 思考题:你能否想出解决办法?
|
||||
:::
|
||||
|
||||
这些问题同样适用于其他对象,而不仅仅是通道。
|
||||
|
||||
在我们的冒险中,我们有一枚银币和一枚金币。一方面,如果不接受在只有一枚硬币存在的地点得到硬币,那将是愚蠢的。
|
||||
|
||||
另一方面,在两种硬币都在同一位置存在的情况下,玩家应该可以有两个拾取选择。
|
||||
|
||||
这立即将我们带到了解析器的第三个问题:
|
||||
|
||||
1. 一个标签只能是一个单词;“sliver coin”这是俩单词,他不接受啊
|
||||
|
||||
所有三个问题都将在本章中解决,从问题#1 开始。它通过为每个对象提供一个标签列表来解决,而不仅仅是一个标签。
|
||||
|
||||
## object.h
|
||||
|
||||
```c
|
||||
typedef struct object {
|
||||
const char *description;
|
||||
const char **tags;
|
||||
struct object *location;
|
||||
struct object *destination;
|
||||
} OBJECT;
|
||||
|
||||
extern OBJECT objs[];
|
||||
|
||||
#define field (objs + 0)
|
||||
#define cave (objs + 1)
|
||||
#define silver (objs + 2)
|
||||
#define gold (objs + 3)
|
||||
#define guard (objs + 4)
|
||||
#define player (objs + 5)
|
||||
#define intoCave (objs + 6)
|
||||
#define exitCave (objs + 7)
|
||||
#define wallField (objs + 8)
|
||||
#define wallCave (objs + 9)
|
||||
|
||||
#define endOfObjs (objs + 10)
|
||||
```
|
||||
|
||||
## object.c
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
|
||||
static const char *tags0[] = {"field", NULL};
|
||||
static const char *tags1[] = {"cave", NULL};
|
||||
static const char *tags2[] = {"silver", "coin", "silver coin", NULL};
|
||||
static const char *tags3[] = {"gold", "coin", "gold coin", NULL};
|
||||
static const char *tags4[] = {"guard", "burly guard", NULL};
|
||||
static const char *tags5[] = {"yourself", NULL};
|
||||
static const char *tags6[] = {"east", "entrance", NULL};
|
||||
static const char *tags7[] = {"west", "exit", NULL};
|
||||
static const char *tags8[] = {"west", "north", "south", "forest", NULL};
|
||||
static const char *tags9[] = {"east", "north", "south", "rock", NULL};
|
||||
//我们不固定标签长度,在结束的时候用 NULL 来标记
|
||||
OBJECT objs[] = {
|
||||
{"an open field" , tags0, NULL , NULL },
|
||||
{"a little cave" , tags1, NULL , NULL },
|
||||
{"a silver coin" , tags2, field, NULL },
|
||||
{"a gold coin" , tags3, cave , NULL },
|
||||
{"a burly guard" , tags4, field, NULL },
|
||||
{"yourself" , tags5, field, NULL },
|
||||
{"a cave entrance to the east", tags6, field, cave },
|
||||
{"an exit to the west" , tags7, cave , field },
|
||||
{"dense forest all around" , tags8, field, NULL },
|
||||
{"solid rock all around" , tags9, cave , NULL }
|
||||
//我们用 NULL 来阻绝进入一个你不知道的地方
|
||||
};
|
||||
```
|
||||
|
||||
当然,要让这个改动生效,我们还需要调整*noun.c*中的*objectHasTag*函数。
|
||||
|
||||
*同时,我们将让函数 getVisible*和*getPossession* 告知玩家他必须更具体的选择你到底是银币还是金币,而不是随机选择任何一个对象。
|
||||
|
||||
## noun.h
|
||||
|
||||
```c
|
||||
extern OBJECT *getVisible(const char *intention, const char *noun);
|
||||
extern OBJECT *getPossession(OBJECT *from, const char *verb, const char *noun);
|
||||
```
|
||||
|
||||
## **noun.c**
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "object.h"
|
||||
#include "misc.h"
|
||||
|
||||
static bool objectHasTag(OBJECT *obj, const char *noun)
|
||||
{
|
||||
if (noun != NULL && *noun != '\0')
|
||||
{
|
||||
const char **tag;
|
||||
for (tag = obj->tags; *tag != NULL; tag++)
|
||||
{
|
||||
if (strcmp(*tag, noun) == 0) return true;
|
||||
}//扫描对象的 tag 列表
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static OBJECT ambiguousNoun;//我们需要这玩意的地址帮助我们把它当成一个返回值
|
||||
|
||||
static OBJECT *getObject(const char *noun, OBJECT *from, DISTANCE maxDistance)
|
||||
{
|
||||
OBJECT *obj, *res = NULL;
|
||||
for (obj = objs; obj < endOfObjs; obj++)
|
||||
{
|
||||
if (objectHasTag(obj, noun) && getDistance(from, obj) <= maxDistance)
|
||||
{
|
||||
res = res == NULL ? obj : &ambiguousNoun;//标签不明确的解决方案哦
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
OBJECT *getVisible(const char *intention, const char *noun)
|
||||
{
|
||||
OBJECT *obj = getObject(noun, player, distOverthere);
|
||||
if (obj == NULL)
|
||||
{
|
||||
if (getObject(noun, player, distNotHere) == NULL)
|
||||
{
|
||||
printf("I don't understand %s.\n", intention);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("You don't see any %s here.\n", noun);
|
||||
}
|
||||
}
|
||||
else if (obj == &ambiguousNoun)//模糊匹配
|
||||
{
|
||||
printf("Please be specific about which %s you mean.\n", noun);
|
||||
obj = NULL;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
OBJECT *getPossession(OBJECT *from, const char *verb, const char *noun)
|
||||
{
|
||||
OBJECT *obj = NULL;
|
||||
if (from == NULL)
|
||||
{
|
||||
printf("I don't understand who you want to %s.\n", verb);
|
||||
}
|
||||
else if ((obj = getObject(noun, from, distHeldContained)) == NULL)
|
||||
{
|
||||
if (getObject(noun, player, distNotHere) == NULL)
|
||||
{
|
||||
printf("I don't understand what you want to %s.\n", verb);
|
||||
}
|
||||
else if (from == player)
|
||||
{
|
||||
printf("You are not holding any %s.\n", noun);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("There appears to be no %s you can get from %s.\n",
|
||||
noun, from->description);
|
||||
}
|
||||
}
|
||||
else if (obj == &ambiguousNoun)//模糊匹配
|
||||
{
|
||||
printf("Please be specific about which %s you want to %s.\n",
|
||||
noun, verb);
|
||||
obj = NULL;
|
||||
}
|
||||
else if (obj == from)
|
||||
{
|
||||
printf("You should not be doing that to %s.\n", obj->description);
|
||||
obj = NULL;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
```
|
||||
|
||||
问题 #3 可以通过从函数*parseAndExecute*中删除一个 [空格](http://en.wikipedia.org/wiki/Space_(punctuation))字符来解决(下面的第 10 行)。这个解决方案远非完美('silver' 和 'coin' 之间的双空格是打咩的),但直到我们在第 13 章中让自己成为一个更好的解析器之前。
|
||||
|
||||
## parsexec.c
|
||||
|
||||
```c
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "location.h"
|
||||
#include "inventory.h"
|
||||
|
||||
bool parseAndExecute(char *input)
|
||||
{
|
||||
char *verb = strtok(input, " \n");
|
||||
char *noun = strtok(NULL, "\n");
|
||||
if (verb != NULL)
|
||||
{
|
||||
if (strcmp(verb, "quit") == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (strcmp(verb, "look") == 0)
|
||||
{
|
||||
executeLook(noun);
|
||||
}
|
||||
else if (strcmp(verb, "go") == 0)
|
||||
{
|
||||
executeGo(noun);
|
||||
}
|
||||
else if (strcmp(verb, "get") == 0)
|
||||
{
|
||||
executeGet(noun);
|
||||
}
|
||||
else if (strcmp(verb, "drop") == 0)
|
||||
{
|
||||
executeDrop(noun);
|
||||
}
|
||||
else if (strcmp(verb, "give") == 0)
|
||||
{
|
||||
executeGive(noun);
|
||||
}
|
||||
else if (strcmp(verb, "ask") == 0)
|
||||
{
|
||||
executeAsk(noun);
|
||||
}
|
||||
else if (strcmp(verb, "inventory") == 0)
|
||||
{
|
||||
executeInventory();
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("I don't know how to '%s'.\n", verb);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
模块*main.c*、*inventory.c*、*location.c*、*move.c* 和*misc.c*保持不变
|
||||
|
||||
现在对象数组 ( *object.c* ) 开始在多个维度上增长(特别是在引入多个标签的情况下),我们需要一种使其更易于维护的方法。
|
||||
|
||||
::: warning 🤔 猜猜看该怎么办?
|
||||
:::
|
||||
91
2023旧版内容/3.编程思维体系构建/3.4.6.9.练习:生成代码.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# 9.练习:生成代码
|
||||
|
||||
*到目前为止,我们的冒险游戏有 10 个对象。每个对象由有 5 个属性组成。一个真正的文本冒险可能有数百个甚至数千个对象,并且每个对象的属性数量也可能增加(请参阅下一章)。在目前的形式下,维护如此庞大的对象和属性列表将很困难。*
|
||||
|
||||
例如,当我们在添加对象 *wallField* 和 *wallCave* 时,我们必须在三个不同的位置执行此操作:一次在 *object.h* 中(作为<em>#define</em>),两次在 *object.c* 中(数组 *objs* 中的一个元素,以及一个单独的标签数组)。这显然十分笨拙并且容易出错。
|
||||
|
||||
我们将不再手工维护 object. h 和 object. c,而是从更适合我们需要的单一源开始生成文件。这个新的源文件可以用你喜欢的任何语言 ( 典型的是某些特定领域的语言 ),只要你有工具把它转换回 C。下面是一个简单的例子,考虑下列布局来组织我们的对象:
|
||||
|
||||
```txt
|
||||
/* Raw C code (declarations) */
|
||||
- ObjectName
|
||||
AttributeName AttributeValue
|
||||
AttributeName AttributeValue
|
||||
...
|
||||
- ObjectName
|
||||
AttributeName AttributeValue
|
||||
AttributeName AttributeValue
|
||||
...
|
||||
- ...
|
||||
```
|
||||
|
||||
根据到目前为止收集的对象,我们可以构造以下源文件。文件名并不重要;我只是简单地将其命名为*object.txt*,以明确它与*object.h*和*object.c*相关。
|
||||
|
||||
## object.txt
|
||||
|
||||
```txt
|
||||
#include <stdio.h>
|
||||
#include "object.h"
|
||||
|
||||
typedef struct object {
|
||||
const char *description;
|
||||
const char **tags;
|
||||
struct object *location;
|
||||
struct object *destination;
|
||||
} OBJECT;
|
||||
|
||||
extern OBJECT objs[];
|
||||
//对象
|
||||
- field
|
||||
description "an open field"
|
||||
tags "field"
|
||||
|
||||
- cave
|
||||
description "a little cave"
|
||||
tags "cave"
|
||||
|
||||
- silver
|
||||
description "a silver coin"
|
||||
tags "silver", "coin", "silver coin"
|
||||
location field
|
||||
|
||||
- gold
|
||||
description "a gold coin"
|
||||
tags "gold", "coin", "gold coin"
|
||||
location cave
|
||||
|
||||
- guard
|
||||
description "a burly guard"
|
||||
tags "guard", "burly guard"
|
||||
location field
|
||||
|
||||
- player
|
||||
description "yourself"
|
||||
tags "yourself"
|
||||
location field
|
||||
|
||||
- intoCave
|
||||
description "a cave entrance to the east"
|
||||
tags "east", "entrance"
|
||||
location field
|
||||
destination cave
|
||||
|
||||
- exitCave
|
||||
description "an exit to the west"
|
||||
tags "west", "exit"
|
||||
location cave
|
||||
destination field
|
||||
|
||||
- wallField
|
||||
description "dense forest all around"
|
||||
tags "west", "north", "south", "forest"
|
||||
location field
|
||||
|
||||
- wallCave
|
||||
description "solid rock all around"
|
||||
tags "east", "north", "south", "rock"
|
||||
location cave
|
||||
```
|
||||
|
||||
::: warning 🤔 思考题:你能否自己用 C 来实现这段伪代码?
|
||||
:::
|
||||
43
2023旧版内容/3.编程思维体系构建/3.4.6阶段二:文字冒险(cool).md
Normal file
@@ -0,0 +1,43 @@
|
||||
# 阶段二:文字冒险(cool)
|
||||
|
||||
## 前言
|
||||
|
||||
本来打算让各位做下面的任务来进行进一步的学习的,但是想了想,实在是,**太无聊啦**!
|
||||
|
||||
又想让你们来做一个管理系统,但是又想到你们可能会进行无数个管理系统,**这也太无聊啦**!
|
||||
|
||||
因此呢,我打算带大家玩一个文字冒险游戏,如果你想自己体验全流程的话可以试试玩哦!
|
||||
|
||||
当然,在编写的过程中,难免会出现你感到困惑的地方,比如说,你觉得这样更好,或者说,你不明白为什么要这样编写代码,欢迎你进行更多的独立的尝试。
|
||||
|
||||
其次我要说的是:
|
||||
|
||||
这个学习过程会非常硬核,所以你感觉非常多东西不会是很正常的!
|
||||
|
||||
我希望你可以通过这种方式,以后在面临一个大项目或者别人的代码时(你经常要借鉴别人的代码)保持冷静。
|
||||
|
||||
可以保证的是,如果你成功坚持下来了,你将会在接下来的编程生涯中保持长时间的游刃有余。
|
||||
|
||||
当然,如果你选择跳过,也不会对 python 开发那里造成非常大的影响但是你会错失一个非常宝贵的学习机会。
|
||||
|
||||

|
||||
|
||||
在 1980 年代, [文字冒险](http://en.wikipedia.org/wiki/Text_adventure) 是一种受人尊敬的电脑游戏类型。但是时代已经变了,在 21 世纪,它们与 带有 3D 引擎的现代 [MMORPG 相比显得苍白无力。](http://en.wikipedia.org/wiki/Mmorpg)书籍在电影的兴起中幸存下来,而基于文本的游戏很快就输掉了与图形游戏的战斗。“互动小说”由一个活跃的社区保持活力,但它的商业价值早已不复存在。
|
||||
|
||||
## 系统调试的黄金法则:KISS 原则
|
||||
|
||||
这里的 `KISS` 是 `Keep It Simple, Stupid` 的缩写,它的中文翻译是:不要在一开始追求绝对的完美。
|
||||
|
||||
随着以后代码量会越来越多,各个模块之间的交互也越来越复杂,工程的维护变得越来越困难,一个很弱智的 bug 可能需要调好几天。
|
||||
|
||||
在这种情况下,系统能跑起来才是王道,跑不起来什么都是浮云,追求面面俱到只会增加代码维护的难度。
|
||||
|
||||
唯一可以把你从 bug 的混沌中拯救出来的就是 KISS 法则,它的宗旨是**从易到难,逐步推进**, 一次只做一件事,少做无关的事。
|
||||
|
||||
如果你不知道这是什么意思,我们以可能发生的 `str` 成员缓冲区溢出问题来作为例子。KISS 法则告诉你,你应该使用 `assert(0)`, 就算不"得体"地处理上述问题,仍然不会影响表达式求值的核心功能的正确性。
|
||||
|
||||
如果你还记得调试公理,你会发现两者之间是有联系的:调试公理第二点告诉你,未测试代码永远是错的。与其一下子写那么多"错误"的代码,倒不如使用 `assert(0)` 来有效帮助你减少这些"错误".
|
||||
|
||||
如果把 KISS 法则放在软件工程领域来解释,它强调的就是多做[单元测试](http://en.wikipedia.org/wiki/Unit_testing): 写一个函数,对它进行测试,正确之后再写下一个函数,再对它进行测试... 一种好的测试方式是使用 assertion 进行验证,
|
||||
|
||||
KISS 法则不但广泛用在计算机领域,就连其它很多领域也视其为黄金法则,[这里](http://blog.sciencenet.cn/blog-414166-562616.html)有一篇文章举出了很多的例子,我们强烈建议你阅读它,体会 KISS 法则的重要性。
|
||||
67
2023旧版内容/3.编程思维体系构建/3.4.7.1.1调试理论.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# 调试理论
|
||||
|
||||
::: warning 🌲 调试公理
|
||||
|
||||
- The machine is always right. (机器永远是对的)
|
||||
|
||||
- Corollary: If the program does not produce the desired output, it is the programmer's fault.
|
||||
- Every line of untested code is always wrong. (未测试代码永远是错的)
|
||||
|
||||
- Corollary: Mistakes are likely to appear in the "must-be-correct" code.
|
||||
|
||||
这两条公理的意思是:抱怨是没有用的,接受代码有 bug 的现实,耐心调试.
|
||||
:::
|
||||
|
||||
::: warning 😋 如何调试
|
||||
|
||||
- 不要使用"目光调试法", 要思考如何用正确的工具和方法帮助调试
|
||||
|
||||
- 程序设计课上盯着几十行的程序,你或许还能在大脑中像 NEMU 那样模拟程序的执行过程; 但程序规模大了之后,很快你就会放弃的:你的大脑不可能模拟得了一个巨大的状态机
|
||||
- 我们学习计算机是为了学习计算机的工作原理,而不是学习如何像计算机那样机械地工作
|
||||
- 使用 `assert()` 设置检查点,拦截非预期情况
|
||||
|
||||
- 例如 `assert(p != NULL)` 就可以拦截由空指针解引用引起的段错误
|
||||
- 结合对程序执行行为的理解,使用 `printf()` 查看程序执行的情况 (注意字符串要换行)
|
||||
|
||||
- `printf()` 输出任意信息可以检查代码可达性:输出了相应信息,当且仅当相应的代码块被执行
|
||||
- `printf()` 输出变量的值,可以检查其变化过程与原因
|
||||
- 使用 GDB 观察程序的任意状态和行为
|
||||
|
||||
- 打印变量,断点,监视点,函数调用栈...
|
||||
:::
|
||||
|
||||

|
||||
|
||||
## 什么是调试理论
|
||||
|
||||
如果我们能判定任意程序状态的正确性,那么给定一个 failure,我们可以通过二分查找定位到第一个 error 的状态,此时的代码就是 fault (bug)。
|
||||
|
||||
## 正确的方法:理解程序的执行过程,弄清楚到底为何导致了 bug
|
||||
|
||||
- `ssh`:使用 `-v` 选项检查日志
|
||||
- `gcc`:使用 `-v` 选项打印各种过程
|
||||
- `make`:使用 `-n` 选项查看完整命令
|
||||
|
||||
- `make -nB | grep -ve '^\(echo\|mkdir\)'` 可以查看完整编译 nemu 的编译过程
|
||||
|
||||
各个工具普遍提供调试功能,帮助用户/开发者了解程序的行为
|
||||
|
||||
## 错误概念
|
||||
|
||||
我们来简单梳理一下段错误发生的原因。首先,机器永远是对的。如果程序出了错,先怀疑自己的代码有 bug . 比如由于你的疏忽,你编写了 `if (p = NULL)` 这样的代码。但执行到这行代码的时候,也只是 `p` 被赋值成 `NULL`, 程序还会往下执行。然而等到将来对 `p` 进行了解引用的时候,才会触发段错误,程序彻底崩溃。
|
||||
|
||||
我们可以从上面的这个例子中抽象出一些软件工程相关的概念:
|
||||
|
||||
- Fault: 实现错误的代码,例如 `if (p = NULL)`
|
||||
- Error: 程序执行时不符合预期的状态,例如 `p` 被错误地赋值成 `NULL`
|
||||
- Failure: 能直接观测到的错误,例如程序触发了段错误
|
||||
|
||||
调试其实就是从观测到的 failure 一步一步回溯寻找 fault 的过程,找到了 fault 之后,我们就很快知道应该如何修改错误的代码了。但从上面的例子也可以看出,调试之所以不容易,恰恰是因为:
|
||||
|
||||
- fault 不一定马上触发 error
|
||||
- 触发了 error 也不一定马上转变成可观测的 failure
|
||||
- error 会像滚雪球一般越积越多,当我们观测到 failure 的时候,其实已经距离 fault 非常遥远了
|
||||
|
||||
理解了这些原因之后,我们就可以制定相应的策略了:
|
||||
|
||||
- 尽可能把 fault 转变成 error. 这其实就是测试做的事情,所以我们在上一节中加入了表达式生成器的内容,来帮助大家进行测试,后面的实验内容也会提供丰富的测试用例。但并不是有了测试用例就能把所有 fault 都转变成 error 了,因为这取决于测试的覆盖度。要设计出一套全覆盖的测试并不是一件简单的事情,越是复杂的系统,全覆盖的测试就越难设计。但是,如何提高测试的覆盖度,是学术界一直以来都在关注的问题。
|
||||
65
2023旧版内容/3.编程思维体系构建/3.4.7.1GDB初探索(编程可阅览).md
Normal file
@@ -0,0 +1,65 @@
|
||||
# GDB 初探索(编程可阅览)
|
||||
|
||||
请在开始进行 C 语言编程之后查阅使用
|
||||
|
||||

|
||||
|
||||
## GDB 是什么?
|
||||
|
||||
调试器,简单来说就是当你代码跑不通时候修正错误用的
|
||||
|
||||
[GDB's Mascot?](https://sourceware.org/gdb/mascot/)
|
||||
|
||||
可搭配插件 gef pwndbg pwngdb peda
|
||||
|
||||
## 基本操作
|
||||
|
||||
[GDB 快速入门教程](https://www.bilibili.com/video/BV1EK411g7Li/)
|
||||
|
||||
### **GDB 使用表**
|
||||
|
||||
`run (r)`运行程序
|
||||
|
||||
`b`打断点,可以在函数和位置打断点
|
||||
|
||||
`info b`查看打断点的位置
|
||||
|
||||
`n`下一步,跳过函数的
|
||||
|
||||
`list`查看源代码
|
||||
|
||||
`-p`走 PID 路线
|
||||
|
||||
`edit [file:]function` 看现在停下的函数位置
|
||||
|
||||
`step` 进入任何函数
|
||||
|
||||
`p`打印变量
|
||||
|
||||
`shell`输入命令
|
||||
|
||||
`set logging on`记录日志
|
||||
|
||||
`watchpoint`观察变量是否变化的观察点
|
||||
|
||||
`watch`设置观察点位置,watch*(地址)
|
||||
|
||||
`layout split`开启 TUI 模式
|
||||
|
||||
`whatis`查看变量类型
|
||||
|
||||
`ptype`查看详细信息
|
||||
|
||||
#### **TUI**
|
||||
|
||||
`ctrl + x + a`开启
|
||||
|
||||
`ctrl + p + n`往前
|
||||
|
||||
`ctrl + l`重新整理页面
|
||||
|
||||
## 官方手册
|
||||
|
||||
[GDB User Manual](https://sourceware.org/gdb/current/onlinedocs/gdb)
|
||||
|
||||
有非常多高级用法,可以在必要的时候进行查阅,受益无穷
|
||||
67
2023旧版内容/3.编程思维体系构建/3.4.7.2C的历史问题:undefined behavior.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# C 的历史问题:undefined behavior
|
||||
|
||||

|
||||
|
||||
简写为 UB
|
||||
|
||||
“Anything at all can happens; the Standard imposes no requirements. The program may fail to compile, or it may execute incorrectly (either crashing or silently generating incorrect results), or it may fortuitously do exactly what the programmer intended”
|
||||
|
||||
简单来说就是:他没定义这种操作该怎么办。
|
||||
|
||||
比如说/0 以及一些你们可能没接触过的操作。
|
||||
|
||||
给你们看个恶心的例子:
|
||||
|
||||
```c
|
||||
int main(int argc, char **argv) {
|
||||
int i = 3;
|
||||
int r = (i++*++i+i--*--i);
|
||||
printf("The answer : %dn", r);
|
||||
system("pause");
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
让我们看看不同编译器的 Debug 模式下执行的结果
|
||||
|
||||
Visual C++ 6.0
|
||||
|
||||
> The answer : 25
|
||||
|
||||
Visual C++ 2008 Express
|
||||
|
||||
> The answer : 18
|
||||
|
||||
MinGW(GCC)
|
||||
|
||||
> The answer : 25
|
||||
|
||||
我们试试看在 Release 下执行的结果
|
||||
|
||||
Visual C++ 6.0
|
||||
|
||||
> The answer : 18
|
||||
|
||||
Visual C++ 2008 Express
|
||||
|
||||
> The answer : 18
|
||||
|
||||
MinGW(GCC)
|
||||
|
||||
> The answer : 25
|
||||
|
||||
C 语言最初为了开发 UNIX 和系统软体而生,本质是低阶的程式语言
|
||||
|
||||
在语言规范层级存在 UB,可允许编译器引入更多最佳化。比方说 `X * 2 / 2` 在沒有 overflow 发生的時候,可最佳化为 `X`。
|
||||
|
||||
而且值得注意的是,在你的程序初始化之前,栈里面塞的一堆东西也是 UB。
|
||||
|
||||
但是高级语言比如说 java,python 都不会出现这样的问题了。
|
||||
|
||||
因为发现少一个时钟周期的运算多一堆问题,完全得不偿失啊。
|
||||
|
||||
当然,有更多兴趣的话可以看看这个东西
|
||||
|
||||
[万恶的未定义行为 | 程式设计 遇上 小提琴](https://blog.ez2learn.com/2008/09/27/evil-undefined-behavior/)
|
||||
|
||||
<del>关键是,老师喜欢出题刁难你啊!真烦啊!</del>
|
||||
27
2023旧版内容/3.编程思维体系构建/3.4.7.3C编译器干了什么.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# C 编译器干了什么
|
||||
|
||||
## 以 gcc 为例
|
||||
|
||||
1、预处理,生成 .i 的文件[预处理器 cpp]
|
||||
|
||||
2、将预处理后的文件转换成汇编语言,生成文件 .s [编译器 egcs]
|
||||
|
||||
3、有汇编变为目标代码 (机器代码) 生成 .o 的文件[汇编器 as]
|
||||
|
||||
4、连接目标代码,生成可执行程序 [链接器 ld]
|
||||
|
||||
## 有啥用
|
||||
|
||||
有一天你发现,某一段 C 语言只有几行,但是用了大量的魔法宏定义以及魔法操作以及神奇的元编程。
|
||||
|
||||
你想看懂他,你可能需要学会 gcc -E。将他们都翻译成.i 文件。
|
||||
|
||||
如果有一天你要学习汇编语言,或者说出现了在代码中看不出的 bug,你可能需要翻译成.s 文件
|
||||
|
||||
## 了解更多
|
||||
|
||||
当然不止如此,编译器还承担了优化的过程,有时候同一份代码,经过 O1 和 O2 不同优化可能最后代码都不一样。
|
||||
|
||||
推荐阅读
|
||||
|
||||
[编译器的工作过程](http://www.ruanyifeng.com/blog/2014/11/compiler.html)
|
||||
83
2023旧版内容/3.编程思维体系构建/3.4.7.4Inline Assembly与链接加载.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Inline Assembly 与链接加载
|
||||
|
||||
## Inline Assembly
|
||||
|
||||
可以在 C 代码里嵌入汇编语言的骚操作。毕竟编译器本身也就是个复制,翻译,,粘贴的过程。
|
||||
|
||||
> C 语言是高级的汇编语言!
|
||||
|
||||
你可以把每一个 C 语言的语句都直译成一条一条的汇编代码。反正也是顺序执行的。
|
||||
|
||||
C 艹可能没那么容易了····比如说虚函数调用,那你就不太好翻译嘛。
|
||||
|
||||
最简单的就是用用个 asm(.....)
|
||||
|
||||
当然这里可以参考 c inline Asm 的教程 但是已经脱离了本文的主题了
|
||||
|
||||
这里给指条路 [How to Use Inline Assembly Language in C Code](https://dmalcolm.fedorapeople.org/gcc/2015-08-31/rst-experiment/how-to-use-inline-assembly-language-in-c-code.html)
|
||||
|
||||
> 诸如 Go 的高级语言 也可以通过 inline C 来 内链汇编
|
||||
|
||||
你可以写任何一个指令,他完全不会检查 也不会优化 编译器默认你知道你在干什么。
|
||||
|
||||
然后 C 编译器就会将这部分代码 **原封不动地**拷贝进你的二进制代码当中
|
||||
|
||||
当然,你可以通过 RTFM 来将 C 语言的变量塞到汇编中处理。
|
||||
|
||||
在 Windows 平台下 VS 没有 Code 可以以 `__asm {}` 代码块来进行实验 但是注意 只能在 x86 模式下使用 x64 不支持 可以参考 [__asm](https://docs.microsoft.com/zh-tw/cpp/assembler/inline/asm?view=msvc-170)
|
||||
|
||||
以上两种平台的方案都其实本质是编译器特殊宏 并不包括在 C 的标准内 所以要针对不同的编译器 寻找对应的文档
|
||||
|
||||
## 静态链接
|
||||
|
||||
当你使用 GCC 生成可执行文件./a.out 时,到底发生了什么?
|
||||
|
||||
为什么就可以直接执行呢?当你问及这个问题时,那么就大有一番探索的空间了
|
||||
|
||||
## 链接器
|
||||
|
||||
链接器的功能:将一个可执行程序所需的目标文件和库最终整合在一起。
|
||||
|
||||
就是说,你调用的一些库,是必须要有外部的东西支持的
|
||||
|
||||
这个就是帮你和外部库连接起来的重要部分。
|
||||
|
||||
一个程序包含三个段:.text、.data 和 .bss 段。
|
||||
|
||||
而各目标文件和库都包含这三段,所以,ld 链接器的功能就是将各个目标文件和库的三段合并在一起。
|
||||
|
||||
当然,链接器所完成的链接工作并非只是简单地将各个目标文件和库的三段内存堆砌在一起,而是还要完成“重定位”的工作。
|
||||
|
||||
### 查看二进制文件的工具
|
||||
|
||||
使用 objdump 查看 data 节的 x,y
|
||||
|
||||
查看 main 对应的汇编代码
|
||||
|
||||
使用 readelf 查看 relocation 的信息
|
||||
|
||||
使用 IDA BinaryNInja 一类反汇编工具
|
||||
|
||||
## 动态链接
|
||||
|
||||
静态链接一次用那么多,实在是太大太大了
|
||||
|
||||
比如说一个 printf 就要几十 KB,完全没必要把 libc 代码包含到程序里面
|
||||
|
||||
可以等程序加载好之后再去做一个链接
|
||||
|
||||
Windows 下一般是 DLL 作为程序使用的 动态链接库
|
||||
|
||||
Linux 下一般是 .so 如果你看到了 .a 那个一般是 archive 的缩写
|
||||
|
||||
使用动态链接的好处在于 可以热加载和热更新
|
||||
|
||||
## 共享连接的加载
|
||||
|
||||
使用 ldd 来查看 a.out 就可以查看动态链接库
|
||||
|
||||
不过 ldd 这个是个神奇的脚本!!!
|
||||
|
||||
他做的事情就是挨个调用去试着加载 a.out
|
||||
|
||||
加载会读取头中的一些表 比如全局 GOT 然后根据名称查找
|
||||
3
2023旧版内容/3.编程思维体系构建/3.4.7C基础知识杂谈.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# C 基础知识杂谈
|
||||
|
||||
本章节内容涉及 C 的调试,C 的历史遗留问题以及 C 的执行过程,可以作为补充资料让大家学习和思考
|
||||
13
2023旧版内容/3.编程思维体系构建/3.4C语言.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# 3.4C 语言
|
||||
|
||||
对于计算机学院的学生来说,C 语言要陪伴你以后要非常多的时光,以后的程序设计实践,数据结构,操作系统等等课程都是用 C 语言写的,因此我希望各位同学可以尝试着多完成一些内容
|
||||
|
||||
对于自动化等别的学院的同学来说,C 语言更贴近计算机底层的运行原理,可以帮助你进行更为底层的各类开发,也是非常有帮助的。
|
||||
|
||||
值得一提的是,我不会在本教程讲授过于基础的概念,但是会贴出你可能需要学习的内容。
|
||||
|
||||

|
||||
|
||||
同时我要说的是:C 语言为了适配多种多样的硬件以及各式各样的操作,他对非常多的 undefined 操作不做太多限制,也就是说你可能会出现各种各样的问题,<del>甚至把你电脑炸了</del>
|
||||
|
||||
但是不用担心,不会造成什么严重后果的,只不过你需要在编程的时候多加留心
|
||||
583
2023旧版内容/3.编程思维体系构建/3.5git与github.md
Normal file
@@ -0,0 +1,583 @@
|
||||
# 3.5git 与 github
|
||||
|
||||
引自 nju-ics-pa
|
||||
|
||||
## 光玉
|
||||
|
||||
想象一下你正在玩 Flappy Bird,你今晚的目标是拿到 100 分,不然就不睡觉。经过千辛万苦,你拿到了 99 分,就要看到成功的曙光的时候,你竟然失手了!你悲痛欲绝,滴血的心在呼喊着,“为什么上天要这样折磨我?为什么不让我存档?”
|
||||
|
||||
[Play Happy Bird](https://flappybird.io/)
|
||||
|
||||
想象一下你正在写代码,你今晚的目标是实现某一个新功能,不然就不睡觉。经过千辛万苦,你终于把代码写好了,保存并编译运行,你看到调试信息一行一行地在终端上输出。就要看到成功的曙光的时候,竟然发生了错误!你仔细思考,发现你之前的构思有着致命的错误,但之前正确运行的代码已经永远离你而去了。你悲痛欲绝,滴血的心在呼喊着,“为什么上天要这样折磨我?”你绝望地倒在屏幕前 ... ... 这时,你发现身边渐渐出现无数的光玉,把你包围起来,耀眼的光芒令你无法睁开眼睛 ... ... 等到你回过神来,你发现屏幕上正是那份之前正确运行的代码!但在你的记忆中,你确实经历过那悲痛欲绝的时刻 ... ... 这一切真是不可思议啊 ... ...
|
||||
|
||||
## 人生如戏,戏如人生
|
||||
|
||||
人生就像不能重玩的 Flappy Bird,但软件工程领域却并非如此,而那不可思议的光玉就是“版本控制系统”。版本控制系统给你的开发流程提供了比朋也收集的更强大的光玉,能够让你在过去和未来中随意穿梭,避免上文中的悲剧降临你的身上。
|
||||
|
||||
没听说过版本控制系统就完成实验,艰辛地排除万难,就像游戏通关之后才知道原来游戏可以存档一样,其实玩游戏的时候进行存档并不是什么丢人的事情。
|
||||
|
||||
在本节,我们使用 `git` 进行版本控制。下面简单介绍如何使用 `git` :
|
||||
|
||||
### 游戏设置
|
||||
|
||||
首先你得安装 `git` :
|
||||
|
||||
- Linux 下
|
||||
|
||||
```bash
|
||||
# Debian , Ubuntu
|
||||
sudo apt install git
|
||||
```
|
||||
|
||||
- windows 下
|
||||
|
||||
从这里下载 [https://git-scm.com/download/win](https://git-scm.com/download/win)
|
||||
|
||||
下载完安装一路点 `next` 就行,什么配置也不用动。
|
||||
|
||||
安装好之后,你需要先进行一些配置工作。在终端里输入以下命令:
|
||||
|
||||
```bash
|
||||
git config --global user.name "Zhang San" # your name
|
||||
git config --global user.email "zhangsan@foo.com" # your email
|
||||
```
|
||||
|
||||
经过这些配置,你就可以开始使用 `git` 了。
|
||||
|
||||
你会通过 `git clone` 命令来拉取远程仓库的代码,里面已经包含一些 `git` 记录,因此不需要额外进行初始化。如果你想在别的实验、项目中使用 `git` ,你首先需要切换到实验、项目的目录中,然后输入
|
||||
|
||||
```bash
|
||||
git init
|
||||
```
|
||||
|
||||
进行初始化。初始化后会创建一个隐藏的文件夹名为 `.git` git 会基于这个文件夹来进行版本控制功能。
|
||||
|
||||
### 查看 commit 信息
|
||||
|
||||
使用
|
||||
|
||||
```bash
|
||||
git log
|
||||
```
|
||||
|
||||
查看目前为止所有的 commit 记录。
|
||||
|
||||
(commit:提交)
|
||||
|
||||
使用
|
||||
|
||||
```bash
|
||||
git status
|
||||
```
|
||||
|
||||
可以得知,与当前存档相比,哪些文件发生了变化。
|
||||
|
||||
### 提交
|
||||
|
||||
你可以像以前一样编写代码。等到你的开发取得了一些阶段性成果,你应该马上进行“提交(commit)”。
|
||||
|
||||
首先你需要使用 `git status` 查看是否有新的文件或已修改的文件未被跟踪;若有,则使用 `git add` 将文件加入跟踪列表,例如
|
||||
|
||||
```bash
|
||||
git add file.c
|
||||
```
|
||||
|
||||
会将 `file.c` 加入**跟踪列表**。如果需要一次添加所有未被跟踪的文件,你可以使用
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
# 或者
|
||||
git add .
|
||||
```
|
||||
|
||||
但这样可能会跟踪了一些不必要的文件(二进制文件),例如编译产生的 `.o` 文件,和最后产生的可执行文件。事实上,我们只需要跟踪代码源文件即可。为了让 `git` 在添加跟踪文件之前作筛选,你可以编辑 `.gitignore` 文件(没有的话手动创建 文件名就叫这个),在里面给出需要被 `git` 忽略的文件和文件类型。
|
||||
|
||||
[这个网页](https://www.toptal.com/developers/gitignore) 可以根据你搜索的语言来给你创建必要的 `.gitignore` 文件
|
||||
|
||||
```bash
|
||||
# .gitignore文件示例
|
||||
.idea # 编辑器配置
|
||||
__pycache__ # 缓存文件夹
|
||||
node_modules # 某个可以拉取的模块文件夹
|
||||
dist
|
||||
*.local # 以.local为后缀的文件
|
||||
index.html # 一个文件
|
||||
file.o # 一个编译后的文件
|
||||
```
|
||||
|
||||
建议把编译后的文件都加入 `.gitignore` 并在 `README.md` 文件中留下编译的详细操作流程,节省 `.git` 空间、符合提交规范。
|
||||
|
||||
把新文件加入跟踪列表后,使用 `git status` 再次确认。确认无误后就可以存档了,使用
|
||||
|
||||
```bash
|
||||
git commit -m "...comment..."
|
||||
```
|
||||
|
||||
提交工程当前的状态(注释)。其中 `...comment...` 是你本次提交的注释(一般在注释中简略写出本次提交干了什么)以下为注释规范,养成良好习惯请遵守:
|
||||
|
||||
```bash
|
||||
模板:
|
||||
type(scope): subject
|
||||
|
||||
type为commit的类型
|
||||
feat: 新特性
|
||||
fix: 修改问题
|
||||
refactor: 代码重构
|
||||
docs: 文档修改
|
||||
style: 代码格式修改
|
||||
test: 测试用例修改
|
||||
chore: 其他修改, 比如构建流程, 依赖管理.
|
||||
perf: 性能提升的修改
|
||||
build: 对项目构建或者依赖的改动
|
||||
ci: CI 的修改(ci是自动构建 感兴趣可以搜搜 github workflow )
|
||||
revert: revert 前一个 commit ( 撤销前一个commit )
|
||||
|
||||
scope是文件名/模块名/影响的范围
|
||||
例如 schoolSchedule
|
||||
|
||||
subject为commit概述
|
||||
建议符合 50/72 formatting
|
||||
|
||||
例 feat(JoinForm): add success submit tips
|
||||
|
||||
注意 冒号和subject之间要加空格
|
||||
```
|
||||
|
||||
其中详细内容可以参照 [约定式提交](https://www.conventionalcommits.org/zh-hans/v1.0.0/)
|
||||
|
||||
附上 [语义化版本 2.0.0 规范](https://semver.org/lang/zh-CN/)
|
||||
|
||||
你可以使用 `git log` 查看存档记录,你应该能看到刚才编辑的注释。
|
||||
|
||||
### 读档(回溯到某一个 commit)
|
||||
|
||||
如果你遇到了上文提到的让你悲痛欲绝的情况,现在你可以使用光玉来救你一命了。首先使用 `git log` 来查看已有的存档,并决定你需要回到哪个过去。每一份存档都有一个 `hash code`,例如 `b87c512d10348fd8f1e32ddea8ec95f87215aaa5` , 你需要通过 `hash code` 来告诉 `git` 你希望读哪一个档。使用以下命令进行读档:
|
||||
|
||||
```bash
|
||||
git reset --hard b87c
|
||||
```
|
||||
|
||||
其中 `b87c` 是上文 `hash code` 的前缀:你不需要输入整个 hash code. 这时你再看看你的代码,你已经成功地回到了过去!
|
||||
|
||||
但事实上,在使用 `git reset` 的 `hard` 模式之前,你需要再三确认选择的存档是不是你的真正目标。如果你读入了一个较早的存档,那么比这个存档新的所有记录都将被删除!这意味着你不能随便回到“将来”了。
|
||||
|
||||
### 分支
|
||||
|
||||
当然还是有办法来避免上文提到的副作用的,这就是 `git` 的分支功能。使用命令:
|
||||
|
||||
```bash
|
||||
git branch
|
||||
```
|
||||
|
||||
查看所有分支。其中 `master` 是主分支,使用 `git init` 初始化之后会自动建立主分支。
|
||||
|
||||
读档的时候使用以下命令:
|
||||
|
||||
```bash
|
||||
git checkout b87c
|
||||
```
|
||||
|
||||
而不是 `git reset` 。这时你将处于一个虚构的分支中,你可以
|
||||
|
||||
- 查看 `b87c` 存档的内容
|
||||
- 使用以下命令切换到其它分支
|
||||
|
||||
```bash
|
||||
git checkout 分支名
|
||||
```
|
||||
|
||||
- 对代码的内容进行修改,但你不能使用 `git commit` 进行存档,你需要使用
|
||||
|
||||
```bash
|
||||
git checkout -B 分支名
|
||||
```
|
||||
|
||||
- 把修改结果保存到一个新的分支中,如果分支已存在,其内容将会被覆盖
|
||||
|
||||
不同的分支之间不会相互干扰,这也给项目的分布式开发带来了便利。有了分支功能,你就可以像第三视点那样在一个世界的不同时间 ( 一个分支的多个存档 ),或者是多个平行世界(多个分支)之间来回穿梭了。
|
||||
|
||||
### 更多功能
|
||||
|
||||
以上介绍的是 `git` 的一些基本功能,`git` 还提供很多强大的功能,例如使用 `git diff` 比较同一个文件在不同版本中的区别,使用 `git bisect` 进行二分搜索来寻找一个 bug 在哪次提交中被引入...
|
||||
|
||||
其它功能的使用请参考 `git help` , `man git` ,或者在网上搜索相关资料。
|
||||
|
||||
## 全球最大男性交友网站 —— Github
|
||||
|
||||
::: tip 🤡
|
||||
想象一下你正在进行人生中第一次软件开发的小组合作。
|
||||
|
||||
你把任务分配好让组员去写代码中的某一个模块。组员写好之后发给你。
|
||||
|
||||
你一看,通过 QQ 发过来的是一个文件啊文件 比如说 `学生管理模块.c` 你打开一看,我去是**乱码**。
|
||||
|
||||
你废了九牛二虎之力把他的 GBK 编码改成 UTF8 之后,细细地品鉴这段代码,发现我去有严重逻辑错误,而且代码很不规范。
|
||||
|
||||
你通过 QQ 告诉他,这一行有问题,能不能改一下。他说,好的我改一下。
|
||||
|
||||
然后又发了文件啊文件给你,如此反复循环,你俩已经互相传了好几百个源代码文件,很没效率!
|
||||
:::
|
||||
|
||||
> 通过 Git 版本控制管理自己的代码,再通过 Github 来发布、同步互联,是一个很好的解决方案!
|
||||
|
||||
简介
|
||||
|
||||
作为开源代码库以及版本控制系统,Github 拥有超过 900 万开发者用户。随着越来越多的应用程序转移到了云上,Github 已经成为了管理软件开发以及发现已有代码的首选方法。
|
||||
|
||||
页面大概是这样(老图):
|
||||
|
||||

|
||||
|
||||
### Git 和 Github
|
||||
|
||||
[GitHub](https://github.com/)是一个面向开源及私有软件项目的托管平台,因为只支持 Git 作为唯一的版本库格式进行托管,故名 GitHub。
|
||||
|
||||
### Git 绑定 Github
|
||||
|
||||
::: tip 🤗
|
||||
下面将教学如何注册这个网站,并给[本项目](https://github.com/camera-2018/hdu-cs-wiki)点一个小小的 kita kita 的 star🎇
|
||||
:::
|
||||
|
||||
#### 第一步:注册账号
|
||||
|
||||
([GitHub 官网](https://github.com/))右上角点击 sign up 会有一个非常酷炫的界面指引你注册 🥳
|
||||
|
||||
他会用一个像是命令行交互的方式引导注册,你需要依次填写 `邮箱` 、`密码` 、 `用户名(此为 ID 非昵称)` 、`是否同意邮箱广告推送` 、`机器验证码` 之后创建账户,随后会给你填写的邮箱发送验证码,填写注册。
|
||||
|
||||
随后是一个欢迎问卷😋随便填写、如果他问你什么 PRO Plan 选择 free 不付费就好。
|
||||
|
||||
最后你访问[GitHub 官网](https://github.com)应该会显示你的 dashboard 管理台界面
|
||||
|
||||
#### 第二步:选择你拉取仓库的方式
|
||||
|
||||
点开 github 某个仓库的绿油油的 `<>Code` 按钮,你会发现有三种 clone 方式。
|
||||
|
||||
分别为
|
||||
|
||||
- https(基本无配置,有图形化界面就能用)
|
||||
- ssh(有公私钥设置,没有图形化界面也能用)
|
||||
- gh-cli(github 出品的 cli 工具)
|
||||
|
||||

|
||||
|
||||
#### 【https 方法配置】账号绑定 github
|
||||
|
||||
在命令行配置好你的 id 和邮箱
|
||||
|
||||
```bash
|
||||
git config --global user.name "Zhang San" # your name
|
||||
git config --global user.email "zhangsan@foo.com" # your email
|
||||
```
|
||||
|
||||
在 GitHub 主页,找到 `New` 或者 `Create repository` 一个绿色的按钮,创建一个新的仓库
|
||||
|
||||

|
||||
|
||||
然后填上这个仓库的大名就可以创建了
|
||||
|
||||

|
||||
|
||||
进入你新创建的仓库
|
||||
|
||||
跟随新创建之后的指引,`…or create a new repository on the command line` 内他描述了如何创建一个文件夹、创建一个 README.md 的文件,然后和 github 仓库绑定。
|
||||
|
||||
push 的时候 github 会弹窗索要你的 github 个人信息,如下图
|
||||
|
||||

|
||||
|
||||
输入你的 github 用户名
|
||||
|
||||

|
||||
|
||||
但如果这个时候输入你的账户密码,就会出现如下提示
|
||||
|
||||

|
||||
|
||||
这是因为 github 在 2021 年 8 月 13 日之后移除了对密码验证的支持,你需要使用令牌认证的方式
|
||||
|
||||
进入个人设置,拉到最底下
|
||||
|
||||

|
||||
|
||||
选择
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
会看到这样一个界面
|
||||
|
||||

|
||||
|
||||
可以随便起一个名字,选择 token 的有效期,然后给这个 token 一些权限,如果不清楚这些选项分别代表什么意思的话,把 repo 这个勾选一般就足够了。
|
||||
|
||||
:::warning
|
||||
这个令牌只会在生成时可见,之后就不会再显示了,请做好复制并保存
|
||||
|
||||
:::
|
||||
|
||||
这样我们就可以使用 token 来进行 https 方法绑定 github 账号
|
||||
|
||||

|
||||
|
||||
#### 【ssh 方法配置】创建 SSH Key 并获取公钥
|
||||
|
||||
先在 `C:\Users\用户名\.ssh` 下找有没有 `id_rsa` 和 `id_rsa.pub` 文件
|
||||
|
||||
:::tip
|
||||
这里 `.ssh` 文件夹是一个隐藏文件夹
|
||||
|
||||
如何显示隐藏文件夹请搜索互联网或看这篇文章 [support microsoft 显示隐藏的文件](https://support.microsoft.com/zh-cn/windows/%E6%98%BE%E7%A4%BA%E9%9A%90%E8%97%8F%E7%9A%84%E6%96%87%E4%BB%B6-0320fe58-0117-fd59-6851-9b7f9840fdb2)
|
||||
:::
|
||||
|
||||
如果有就直接跳过这一步
|
||||
|
||||
如果没有,打开 Shell(Windows 下打开 Git Bash *前提是你已经安装好了 git 在桌面右键应该会有 Git bash here 选项*),创建 SSH Key:
|
||||
|
||||
```bash
|
||||
ssh-keygen -t rsa -C "youremail@example.com" # youremail为你注册用的电子邮件地址
|
||||
```
|
||||
|
||||
打开 `id_rsa.pub`,复制里面的内容
|
||||
|
||||
#### 【ssh 方法配置】绑定 Github
|
||||
|
||||
登陆 `GitHub`,点击右上角自己的头像,打开 `settings`
|
||||
|
||||

|
||||
|
||||
然后打开左侧栏 `SSH and GPG keys` 页面
|
||||
|
||||

|
||||
|
||||
然后,点 `New SSH Key`,填上任意 Title,在 Key 文本框里粘贴 `id_rsa.pub` 文件的内容即可
|
||||
|
||||
#### 【ssh 方法配置】创建仓库并和本地绑定
|
||||
|
||||
绑定完 GitHub 然后你可以创建仓库了
|
||||
|
||||
首先在 GitHub 主页,找到 `New` 或者 `Create repository` 一个绿色的按钮,创建一个新的仓库
|
||||
|
||||

|
||||
|
||||
然后填上这个仓库的大名就可以创建了
|
||||
|
||||

|
||||
|
||||
根据之前学习的方法在本地创建完 git 仓库之后
|
||||
|
||||
在 git bash 中输入:
|
||||
|
||||
```bash
|
||||
git remote add origin git@github.com:yourname/gitexample.git
|
||||
# 请将yourname换成自己的id
|
||||
```
|
||||
|
||||
就可以绑定
|
||||
|
||||
或者是直接 git clone 下来
|
||||
|
||||
```bash
|
||||
git clone git@github.com:yourname/gitexample.git
|
||||
```
|
||||
|
||||
或者你可以跟随新创建之后的指引,`…or create a new repository on the command line` 内他描述了如何创建一个文件夹、创建一个 README.md 的文件,然后和 github 仓库绑定。
|
||||
|
||||

|
||||
|
||||
### 下载代码——clone
|
||||
|
||||
拷贝他人存档也未尝不可,而我们如果用他人存档时,次次都需要一样一样的拷贝文件和代码未免太过折磨,下面简单介绍如何使用
|
||||
|
||||
```bash
|
||||
git clone <url>
|
||||
```
|
||||
|
||||
接下去对着下面这个 GitHub 的代码使用一下
|
||||
|
||||
首先,代码的 url 在下图所示的位置
|
||||
|
||||

|
||||
|
||||
然后复制完代码后切换回我们的命令行
|
||||
|
||||
```bash
|
||||
# powershell or cmd
|
||||
# 进入(cd)到想要存储该代码的地方(父文件夹目录)
|
||||
git clone https://github.com/camera-2018/git-example.git
|
||||
```
|
||||
|
||||
> 这里使用的[例子](https://github.com/camera-2018/git-example)是我的仓库,当然你也可以用你自己的仓库。
|
||||
>
|
||||
> 如果你使用我的仓库的话,你 clone 过后在你自己电脑更改的文件等东西,是没法直接提交回来的(因为你没有我仓库管理权限)
|
||||
>
|
||||
> 如果你非要给我的仓库添加东西呢 也是可以,参照下面的 PR(Pull Request)教程
|
||||
|
||||
一阵抽搐过后就下载好了
|
||||
|
||||

|
||||
|
||||
::: tip
|
||||
用完之后别忘记给 camera-2018 点个 follow 😋 `呃呃 follow 没用 star 有用`
|
||||
:::
|
||||
|
||||
注意:失败就重试吧 还失败建议使用魔法(非国内网你懂得)
|
||||
|
||||
### 提交和合并分支
|
||||
|
||||
如图 我在仓库里新建了 `helloworld.c` 并且写了一些代码
|
||||
|
||||

|
||||
|
||||
接下来是提交操作
|
||||
|
||||
```bash
|
||||
git status #看一下文件暂存区
|
||||
```
|
||||
|
||||

|
||||
|
||||
红色表示文件没有提交到暂存区 我们要提交
|
||||
|
||||
接下来
|
||||
|
||||
```bash
|
||||
git add . #将没有提交的所有文件加入暂存区
|
||||
```
|
||||
|
||||

|
||||
|
||||
绿色表示所有文件已加入暂存
|
||||
|
||||
```bash
|
||||
git commit -m "feat(helloworld): add helloworld file"
|
||||
```
|
||||
|
||||
将刚才加入暂区的文件发起了一个提交,提交注释(commit message)是 `feat(helloworld): add helloworld file`
|
||||
|
||||

|
||||
|
||||
1. 如果这是你自己的仓库有权限(本人仓库或 Collaborators 有权限的情况下)你就可以直接使用
|
||||
|
||||
```bash
|
||||
git push origin main # origin 是第四步里 remote add 起的远程名字
|
||||
# main 是分支名
|
||||
```
|
||||
|
||||
上传本次提交
|
||||
|
||||

|
||||
|
||||
2. 如果你没有本仓库的主分支提交权限 可以提交 PR(Pull Requests)
|
||||
|
||||
**第一种情况:这里假设我是协作者 无主分支权限,但有创建分支权限**
|
||||
|
||||
首先创建一个新分支 命名为 `yourname-dev`
|
||||
|
||||

|
||||
|
||||
然后按照上面的方法 `git clone` 并切换到你刚创建的分支
|
||||
|
||||
```bash
|
||||
git switch camera-2018-dev
|
||||
```
|
||||
|
||||
然后提交一个文件,这里直接使用 vscode 自带的 git 工具试试(很方便、不用敲命令行)
|
||||
|
||||

|
||||
|
||||
点暂存所有更改 写好 comment 之后点提交
|
||||
|
||||

|
||||
|
||||
最后点同步更改上传
|
||||
|
||||

|
||||
|
||||
如果是你提交 在 github 上会显示这个 快捷创建 pr 的按钮
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
点它创建 PR
|
||||
|
||||

|
||||
|
||||
这样管理本仓库的人看到 pr 请求就可以 merge 合并辣
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
实际合作过程中可能会出现代码冲突无法 merge 的情况 😋 遇到了自己去 STFW 吧
|
||||
|
||||
**第二种情况:我不是协作者、我什么权限也没有,我看了这个 public 项目后觉得很好但是有一些问题,我要给他贡献一些代码**
|
||||
|
||||
可以点击仓库右上角的 fork
|
||||
|
||||

|
||||
|
||||
这样会在你的名下多出来一份这个同名仓库,而这个仓库你是拥有所有权限的,你可以 clone 你这个同名仓库,更改代码,提交代码之后
|
||||
|
||||
回到源仓库点击 Pull Request 然后创建 PR `New pull request`
|
||||
|
||||
最上面会提示你说
|
||||
|
||||
Comparing changes
|
||||
Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also **compare across forks** .
|
||||
|
||||
点击小蓝字 `compare across forks` 这会让你对比你 fork 仓库和源代码仓库的提交记录,之后就可以创建 PR 了,原作者看到了会合并。
|
||||
|
||||
### 其他功能
|
||||
|
||||
问题跟踪:GitHub 的问题跟踪功能可用于报告软件中的问题、错误和功能请求,并进行讨论、分配和解决。
|
||||
|
||||
Wiki 页面:用户可以创建和编辑与存储库相关的 Wiki 页面,用于提供项目的文档、指南、示例代码等。
|
||||
|
||||
Pull 请求(Pull Requests):使用者可以将自己的代码变更提交给其他项目的所有者,并请求合并到主干代码中。
|
||||
|
||||
项目管理:GitHub 提供项目管理功能,包括任务管理、里程碑(milestones)、项目板(project boards)等工具,可用于组织和跟踪项目的进展。
|
||||
|
||||
部署功能:GitHub 可以与各种持续集成和部署(CI/CD)工具集成,帮助开发人员自动化构建、测试和部署他们的应用程序。
|
||||
|
||||
统计信息:GitHub 提供有关存储库活动和贡献者的统计信息,例如提交图表、活动日历等,有助于跟踪和分析项目的发展。
|
||||
|
||||
社交功能:用户可以关注其他用户、存储库和组织,接收他们的更新和活动通知,并与他们进行交流和讨论。
|
||||
|
||||
代码审核(Code Review):GitHub 的 Pull 请求功能允许团队成员对代码进行审查和讨论,以确保代码质量和最佳实践。
|
||||
|
||||
集成和扩展:GitHub 支持与其他工具和服务的集成,例如持续集成(CI)工具、代码质量检查工具、项目管理工具等。
|
||||
|
||||
页面托管:GitHub Pages 功能使您可以托管静态网站和文档,这在展示和共享项目文档、演示和博客等方面非常有用。
|
||||
|
||||
然后还有一些比如说 Copilot 之类的有用的功能。
|
||||
|
||||
[Copilot](https://github.com/features/copilot) 是 GitHub 推出的一款基于人工智能技术的代码辅助工具。它使用了机器学习模型 codex,并针对编写代码的场景进行了训练。
|
||||
|
||||
Copilot 可以根据上下文和输入的提示,为开发人员生成代码建议和自动完成。它可以通过分析现有代码库、注释和上下文来生成代码片段,提高编码效率并减少重复劳动。
|
||||
|
||||
## [Copilot](https://github.com/features/copilot) 白嫖教程
|
||||
|
||||
你需要学生认证你的 Github 账号。
|
||||
|
||||
地址在 [https://education.github.com/students](https://education.github.com/students) 点击 `Sign up for Global Campus` 来开始认证,下面会让你输入学校,绑定学校邮箱(杭电为 @hdu.edu.cn 结尾的邮箱)(如果你是杭电新生的话,可能要等到智慧杭电账号发放时才能注册杭电邮箱)并上传**学生证明**(从 21 年开始这个验证越来越严,如果不过的话你可以尝试 `学生证第一页`、`学生证第一页英文翻译(像有道翻译那样 P 图上去)`、`学信网学籍证明英文翻译(英文也是 P 上去)`)
|
||||
|
||||
通过了的话你的账户会有 Pro 标识 然后你可以享受的 Github 学生包里包含[这些东西](https://education.github.com/pack)
|
||||
|
||||
里面比较有用的有
|
||||
|
||||
- JETBRAINS 全家桶的免费用(我没用,我用的是 jb 自己家的验证方式,不是 github)
|
||||
- name.com 家的一个一年期的免费域名(大概价值吧 六七十块钱?)
|
||||
- github 的容量扩容和 actions 时间扩容、Codespaces 时间延长、Pages 扩容(没啥用倒是)
|
||||
- Termius 学生包,这是我很喜欢用的终端软件,有学生包可以多端同步 ssh 的账号密码啥的,很方便。
|
||||
- Sentry 容量扩容
|
||||
- Copilot 免费用
|
||||
|
||||
你可以在 `settings` 里看到你的 copilot,配置如下
|
||||
|
||||

|
||||
|
||||
然后就可以在你喜欢的 IDE 或编辑器上下载 Copilot 插件,来启用他。
|
||||
|
||||

|
||||
49
2023旧版内容/3.编程思维体系构建/3.6.1从CS61A看编程语言学习.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# 从 CS61A 看编程语言学习
|
||||
|
||||
## 什么是 CS61A
|
||||
|
||||
首先声明,本教程的 python 部分很多内容摘自 CS61A
|
||||
|
||||
如果你看过 CS 自学指南,想必你在第一页就看过这样的描述
|
||||
|
||||
> 大一入学时我是一个对计算机一无所知的小白,装了几十个 G 的 Visual Studio 天天和 OJ 你死我活。凭着高中的数学底子我数学课学得还不错,但在专业课上对竞赛大佬只有仰望。提到编程我只会打开那笨重的 IDE,新建一个我也不知道具体是干啥的命令行项目,然后就是 `cin`, `cout`, `for` 循环,然后 CE, RE, WA 循环。当时的我就处在一种拼命想学好但不知道怎么学,课上认真听讲但题还不会做,课后做作业完全是用时间和它硬耗的痛苦状态。我至今电脑里还存着自己大一上学期计算概论大作业的源代码 —— 一个 1200 行的 C++ 文件,没有头文件、没有类、没有封装、没有 unit test、没有 Makefile、没有 Git,唯一的优点是它确实能跑,缺点是“能跑”的补集。我一度怀疑我是不是不适合学计算机,因为童年对于极客的所有想象,已经被我第一个学期的体验彻底粉碎了。<br/>这一切的转机发生在我大一的寒假,我心血来潮想学习 Python。无意间看到知乎有人推荐了 CS61A 这门课,说是 UC Berkeley 的大一入门课程,讲的就是 Python。我永远不会忘记那一天,打开 [CS61A](https://cs61a.org/) 课程网站的那个瞬间,就像哥伦布发现了新大陆一样,我开启了新世界的大门。<br/>我一口气 3 个星期上完了这门课,它让我第一次感觉到原来 CS 可以学得如此充实而有趣,原来这世上竟有如此精华的课程。<br/>为避免有崇洋媚外之嫌,我单纯从一个学生的视角来讲讲自学 CS61A 的体验:<br/>- 独立搭建的课程网站:一个网站将所有课程资源整合一体,条理分明的课程 schedule、所有 slides, hw, discussion 的文件链接、详细明确的课程给分说明、历年的考试题与答案。这样一个网站抛开美观程度不谈,既方便学生,也让资源公正透明。<br/>- 课程教授亲自编写的教材:CS61A 这门课的开课老师将 MIT 的经典教材 Structure and Interpretation of Computer Programs (SICP) 用 Python 这门语言进行改编(原教材基于 Scheme 语言),保证了课堂内容与教材内容的一致性,同时补充了更多细节,可以说诚意满满。而且全书开源,可以直接线上阅读。<br/>- 丰富到让人眼花缭乱的课程作业:14 个 lab 巩固随堂知识点,10 个 homework,还有 4 个代码量均上千行的 project。与大家熟悉的 OJ 和 Word 文档式的作业不同,所有作业均有完善的代码框架,保姆级的作业说明。每个 Project 都有详尽的 handout 文档、全自动的评分脚本。CS61A 甚至专门开发了一个[自动化的作业提交评分系统](https://okpy.org/)(据说还发了论文)。当然,有人会说“一个 project 几千行代码大部分都是助教帮你写好的,你还能学到啥?”。此言差矣,作为一个刚刚接触计算机,连安装 Python 都磕磕绊绊的小白来说,这样完善的代码框架既可以让你专注于巩固课堂上学习到的核心知识点,又能有“我才学了一个月就能做一个小游戏了!”的成就感,还能有机会阅读学习别人高质量的代码,从而为自己所用。我觉得在低年级,这种代码框架可以说百利而无一害。唯一的害也许是苦了老师和助教,因为开发这样的作业可想而知需要相当的时间投入。<br/>- 每周 Discussion 讨论课,助教会讲解知识难点和考试例题:类似于北京大学 ICS 的小班研讨,但习题全部用 LaTeX 撰写,相当规范且会明确给出 solution。<br/>这样的课程,你完全不需要任何计算机的基础,你只需要努力、认真、花时间就够了。此前那种有劲没处使的感觉,那种付出再多时间却得不到回报的感觉,从此烟消云散。这太适合我了,我从此爱上了自学。<br/>试想如果有人能把艰深的知识点嚼碎嚼烂,用生动直白的方式呈现给你,还有那么多听起来就很 fancy,种类繁多的 project 来巩固你的理论知识,你会觉得他们真的是在倾尽全力想方设法地让你完全掌握这门课,你会觉得不学好它简直是对这些课程建设者的侮辱。<br/>如果你觉得我在夸大其词,那么不妨从 [CS61A](https://cs61a.org/) 开始,因为它是我的梦开始的地方。
|
||||
|
||||
如果看完了这些,你可能会震惊,会怀疑并且试着打开它并且去尝试这门课程,但是也有可能你会被纯英文的视频或者油管劝退,也有可能你会怀疑我在使用别人的课程体系的前提下仍然要把它放到自己的内容里面的目的,That's all right,我会在下面对她讲的东西进行一定的补充,并且附着上自己的学习建议以及学习思考。
|
||||
|
||||
## 错误的学习方式
|
||||
|
||||
很多人看到要自学 python 之后,第一时间想到的是,我要去 B 站/百度搜一搜,然后一般搜出来就是菜鸟教程,然后就是一连串枯燥乏味的知识堆叠,或者说是培训班的网课,给我们一种知识好像就是这么枯燥乏味以及自己好像学到了很多但是真的用起来却一无所知痛苦万分的感觉。
|
||||
|
||||
我觉得在这个时候大伙可以思考的是,是否我们停留在舒适区的学习方法是错误的呢?
|
||||
|
||||
当然这个时候会出现勇敢的人,尝试这个推荐,但是根据我目前的发现来说,杭电百分之九十五的学生学习一两章之后就荒废了。
|
||||
|
||||
课好吗?
|
||||
|
||||
是很好。
|
||||
|
||||
但是为什么坚持不下来呢?
|
||||
|
||||
根据我的统计,原因无外乎是以下几点:英语太难太劝退了!课程设置太难,好像听懂了但是写起东西来却还是推行不下去!我不知道学了这个能干什么,所以没有动力!学了一两章虽然好像学到了东西,但是感觉有很多东西学了却记不住,导致难度曲线陡增了!<del>游戏太好玩了!</del>
|
||||
|
||||
舒适区看起来很难打破!
|
||||
|
||||
## 正确的思考方式
|
||||
|
||||
面对英语,我们前文有过一而再再而三的提醒过使用正确的工具,但是不得不说的是,在翻译的过程中可能难免丢失了一定的信息,使得我们在困惑中可能变得没有了前进的动力,或者从另一个角度来说我们没有动力是因为没有足够的原因来告诉我们的意识,我们到底应该在做这个超过看菜鸟教程所需精力好多倍的工作,到底能得到除了一点新奇感以外的什么东西,以此来给我们更为充分的理由支撑自己学习完成。
|
||||
|
||||
## 建立正确的认知论
|
||||
|
||||
编程思想本身远比学习某一门编程语言的具体内容更为重要,我们很多的同学在进行这方面内容的时候总是只想着记忆某一门语言的某些内容,然后学的非常的痛苦。
|
||||
|
||||
但不得不提到的是,你学到的语言,技术框架等等在信息化发展如此迅速的现在,可能随时都会过时,诸如早些年间的 B 语言,PHP 等等,到现在都是很少有些人在用的东西了,如果你只扣着一些编程的具体语法,那你学的东西很有可能在某一天就扔进了历史的垃圾堆。
|
||||
|
||||
但是编程语言的设计思想一般不会出现太大的波动,并且就算是发展也有其适配的场景和知识脉络的,如果你乐于去发掘这个知识脉络和思想,那么你就可以优雅的掌握一种思维方式而不是简单的拧螺丝一样的机械化工作。而好的思考方式往往是可以应用在同类的所有语言甚至是在所有的更多的
|
||||
|
||||
## 更有趣的练习方式
|
||||
|
||||
我不得不承认的一点是,越痛苦的练习往往可以让你获得更大的提升模拟的成长往往与你面对苦难以及解决他的时间是成正比的。
|
||||
|
||||
不过遗憾的是,我们大多数的同学是在面对跳起来都够不到的反馈的时候,会选择放弃。(除非你有 M 的倾向)
|
||||
|
||||
因此,有时候我们说大道至简。好的东西往往是能让人们快速理解的东西,我觉得 61A 的难度梯度设计,就是兼具了趣味性和间接性的,并且全面优于互联网上的非常多的教程,比硬啃要给我们更多的正反馈(当然,改变思维和学习方式是很重要的。
|
||||
45
2023旧版内容/3.编程思维体系构建/3.6.2环境配置.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# 环境配置
|
||||
|
||||
当你开始制作大型项目或者复现论文时,环境配置就开始变得至关重要。
|
||||
|
||||
## 什么是环境?
|
||||
|
||||
环境是**包的集合**,我们一般用 Anaconda 来配置虚拟环境。
|
||||
|
||||
[戳这里安装](https://www.anaconda.com/)
|
||||
|
||||
装下来之后具体操作可以看[安装教程](https://blog.csdn.net/in546/article/details/117400839),如果自动配置环境变量的选项是灰色的话,请按照下面的教程把下面的几个文件路径加入环境变量。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
在里面添加并写入文件路径加入就好了~
|
||||
|
||||

|
||||
|
||||
然后打开 Pycharm,创建新项目,设置按照以下方式操作,记得挂梯子。
|
||||
|
||||
如果不挂梯子,请按照教程配置清华源。[我是教程](https://blog.csdn.net/jasneik/article/details/114227716)
|
||||
|
||||

|
||||
|
||||
然后一个新的环境就创建好辣~
|
||||
|
||||
## 如何配置环境
|
||||
|
||||
### 1.配置你自己的环境
|
||||
|
||||
你可以尝试命令 `pip install <包的名字>` 或者 `conda install <包的名字>`
|
||||
|
||||
> 在下载某个包失败的时候可以查一查有没有人写相关攻略~
|
||||
|
||||
你可以用 `conda list` 查看你这个环境已有的包。你也可以在包的名字后面加上 `==版本号` 来指定版本。
|
||||
|
||||
> 请注意各个包之间的依赖关系,否则容易导致无法运行或效果变差!
|
||||
|
||||
### 2.复现论文代码时配置环境
|
||||
|
||||
> 一般我们可以在 Github 的 README 中找到环境的配置方法,遇到难以下载的特殊版本包时可以考虑下载它的源码手动编译,具体流程不展开了,可以自行搜索
|
||||
61
2023旧版内容/3.编程思维体系构建/3.6.3安装python.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# 安装 python
|
||||
|
||||
::: warning 😍 教程
|
||||
|
||||
[Python 小白必看,非常生动的 Pycharm 与 Anaconda 安装教学,干货!_哔哩哔哩_bilibili](https://www.bilibili.com/video/BV1Bp4y117UW)
|
||||
|
||||
<Bilibili bvid='BV1Bp4y117UW'/>
|
||||
|
||||
[Win10 下 Conda-Pycharm-Pytorch 的安装_哔哩哔哩_bilibili](https://www.bilibili.com/video/BV15U4y1J7Ss)
|
||||
|
||||
<Bilibili bvid='BV15U4y1J7Ss'/>
|
||||
:::
|
||||
|
||||
## 巧妇难为无米之炊
|
||||
|
||||
你可以在终端上用 Python 解释器
|
||||
|
||||
也可以尝试使用 VSC,Pycharm 等集成开发环境
|
||||
|
||||
同样我们也推荐你使用 Jupyter Notebook
|
||||
|
||||
我们推荐你都进行一波尝试,同样这也会作为一个任务
|
||||
|
||||
::: warning <font size=5>😐 解释器</font>
|
||||
|
||||
<font size=5><strong>Windows 用户</strong></font>
|
||||
|
||||
打开 [Python 官方网站](https://www.python.org/),找到“Download”里的“Latest: Python 3.x.y”。
|
||||
|
||||
**下载完成后,请大家按照下图的示意,务必勾选“Add Python 3.x to PATH”,然后再点击“Install Now”,等待安装完成后关闭安装程序。**
|
||||
|
||||
**注意:windows11 安装好后 命令行输入 python 可能会跳到 Microsoft 应用商店 可在 customize installation(自定义安装)next 勾选 install for all users**
|
||||
|
||||
<font size=5>**GNU/Linux 系统**</font>
|
||||
|
||||
在终端输入 `sudo apt install python3` 即可完成 Python3 的全部安装流程
|
||||
|
||||
可以输入 `python3 --version` 检验是否成功。
|
||||
:::
|
||||

|
||||
|
||||
::: warning 🤔 Jupyter Notebook
|
||||
|
||||
[官方网站](https://jupyter.org/) Jupyter Notebook 是基于网页的用于交互计算的应用程序。其可被应用于全过程计算:开发、文档编写、运行代码和展示结果。——[Jupyter](https://jupyter-notebook.readthedocs.io/en/stable/notebook.html)
|
||||
|
||||
你在一个框框中直接输入代码,运行,它立马就在下面给你输出。怎么样,是不是很酷?
|
||||
|
||||
[Notebook 官方介绍](https://jupyter-notebook.readthedocs.io/en/stable/notebook.html)
|
||||
|
||||
可能需要你安装 anaconda,如果你不知道这是什么,请去阅读这篇文章并进行相关配置 [Anaconda](https://zhuanlan.zhihu.com/p/32925500)(STFW)
|
||||
|
||||
安装完 jupyer 后,可以在终端直接输入
|
||||
|
||||
jupyter notebook
|
||||
|
||||
进行使用
|
||||
:::
|
||||
|
||||

|
||||
|
||||
[Pycharm](https://www.jetbrains.com/zh-cn/pycharm/):可能很多同学已经用上了,我在这里不做更多解释
|
||||
61
2023旧版内容/3.编程思维体系构建/3.6.4.0阶段零:Python解释器.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# 阶段零:Python 解释器
|
||||
|
||||
::: warning 😍 可参考资料
|
||||
|
||||
[官方文档](https://wiki.python.org/moin/BeginnersGuide)
|
||||
|
||||
[菜鸟教程](https://www.runoob.com/python3/python3-interpreter.html)
|
||||
:::
|
||||
|
||||
你可以在终端与解释器进行交互
|
||||
|
||||
在终端中输入 python 即可进入解释器
|
||||
|
||||
解释器可以进行简单的表达式和语句操作
|
||||
|
||||
你可以自己把玩一下
|
||||
|
||||
```python
|
||||
>>> 1 + 2
|
||||
3
|
||||
```
|
||||
|
||||
```python
|
||||
>>> 3 - 2
|
||||
1
|
||||
```
|
||||
|
||||
```python
|
||||
>>> 5 * 6
|
||||
30
|
||||
```
|
||||
|
||||
```python
|
||||
>>> 7 / 4
|
||||
1.75
|
||||
```
|
||||
|
||||
```python
|
||||
>>> 7 // 4
|
||||
1
|
||||
```
|
||||
|
||||
```python
|
||||
>>> 7 % 4
|
||||
3
|
||||
```
|
||||
|
||||
```python
|
||||
>>> 4 ** 3
|
||||
64
|
||||
```
|
||||
|
||||
同时可以输入 `exit``()` 或按 Ctrl+D 退出交互
|
||||
|
||||
:::: warning 🤔 同学们可能已经发现 python 这门编程语言的神奇之处了
|
||||
|
||||
在这里留一个思考题
|
||||
|
||||
为什么 python 可以做出不需要任何语句的神奇操作呢?
|
||||
|
||||
别的语言可以这样吗?
|
||||
74
2023旧版内容/3.编程思维体系构建/3.6.4.1阶段一:熟悉语句.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# 阶段一:熟悉语句
|
||||
|
||||
::: warning 🐱 在进行本章之前,请你谨记一个原则:基本所有的功能都被人提前实现好了
|
||||
|
||||
你需要关心的仅仅是逻辑该如何设立
|
||||
|
||||
在做本章任务前,请熟悉 python 的函数,循环和判断语句即可
|
||||
:::
|
||||
|
||||
P1:请仅使用一行语句求出三个数的最小平方和
|
||||
|
||||
```python
|
||||
def two_of_three(x, y, z):
|
||||
"""Return a*a + b*b, where a and b are the two smallest members of the
|
||||
positive numbers x, y, and z.
|
||||
|
||||
>>> two_of_three(1, 2, 3)
|
||||
5
|
||||
>>> two_of_three(5, 3, 1)
|
||||
10
|
||||
>>> two_of_three(10, 2, 8)
|
||||
68
|
||||
>>> two_of_three(5, 5, 5)
|
||||
50
|
||||
>>> # check that your code consists of nothing but an expression (this docstring)
|
||||
>>> # and a return statement
|
||||
>>> import inspect, ast
|
||||
>>> [type(x).__name__ for x in ast.parse(inspect.getsource(two_of_three)).body[0].body]
|
||||
['Expr', 'Return']
|
||||
"""
|
||||
return _____
|
||||
```
|
||||
|
||||
提示:可以使用 `min()` 函数哦
|
||||
|
||||
P2:下降阶乘
|
||||
|
||||
```python
|
||||
def falling(n, k):
|
||||
"""Compute the falling factorial of n to depth k.
|
||||
|
||||
>>> falling(6, 3) # 6 * 5 * 4
|
||||
120
|
||||
>>> falling(4, 3) # 4 * 3 * 2
|
||||
24
|
||||
>>> falling(4, 1) # 4
|
||||
4
|
||||
>>> falling(4, 0)
|
||||
1
|
||||
"""
|
||||
"*** YOUR CODE HERE ***"
|
||||
```
|
||||
|
||||
P3:判断一个函数是否有两个或者两个连续的 8
|
||||
|
||||
```python
|
||||
def double_eights(n):
|
||||
"""Return true if n has two eights in a row.
|
||||
|
||||
>>> double_eights(8)
|
||||
False
|
||||
>>> double_eights(88)
|
||||
True
|
||||
>>> double_eights(2882)
|
||||
True
|
||||
>>> double_eights(880088)
|
||||
True
|
||||
>>> double_eights(12345)
|
||||
False
|
||||
>>> double_eights(80808080)
|
||||
False
|
||||
"""
|
||||
"*** YOUR CODE HERE ***"
|
||||
```
|
||||
143
2023旧版内容/3.编程思维体系构建/3.6.4.2阶段二:递归操作.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# 阶段二:递归操作
|
||||
|
||||
什么是递归呢?
|
||||
|
||||

|
||||
|
||||
## 释义
|
||||
|
||||
递归是在函数主体中重复调用函数的基本方案
|
||||
|
||||
让我们来看一个经典的例子
|
||||
|
||||
> 阶乘,即 n! =n \* (n - 1) \*...... \* 2 \* 1<br/>例如:5! = 5 \* 4 \* 3 \* 2 \* 1 = 120.
|
||||
|
||||
而阶乘的代码如下编辑
|
||||
|
||||
```python
|
||||
def factorial(n):
|
||||
if n == 0:
|
||||
return 1
|
||||
return n * factorial(n - 1)
|
||||
```
|
||||
|
||||
编写递归函数的一些小 tips:
|
||||
|
||||
- 你必须假设之前的所有功能都是正确的,这种被称为:the recursive leap of faith
|
||||
- 想想在最简单的情况下函数将如何跳转
|
||||
- 考虑使用问题的更简单版本来进行解决问题
|
||||
|
||||
## 任务
|
||||
|
||||
P4:编写一个递归函数 `skip_add`,它接受一个参数 n 并返回 `n + n-2 + n-4 + n-6 +...+ 0`。假设 n 是非负数。
|
||||
|
||||
```python
|
||||
def skip_add(n):
|
||||
""" Takes a number n and returns n + n-2 + n-4 + n-6 + ... + 0.
|
||||
|
||||
>>> skip_add(5) # 5 + 3 + 1 + 0
|
||||
9
|
||||
>>> skip_add(10) # 10 + 8 + 6 + 4 + 2 + 0
|
||||
30
|
||||
|
||||
"""
|
||||
"*** YOUR CODE HERE ***"
|
||||
```
|
||||
|
||||
P5:GCD,给出两个正整数,求出两个正整数的最大公约数
|
||||
|
||||
Euclid, a Greek mathematician in 300 B.C., realized that the greatest common divisor of `a` and `b` is one of the following:
|
||||
|
||||
- the smaller value if it evenly divides the larger value, or
|
||||
- the greatest common divisor of the smaller value and the remainder of the larger value divided by the smaller value.
|
||||
|
||||
提示:gcd(a, b) = gcd(b, a % b)
|
||||
|
||||
```python
|
||||
def gcd(a, b):
|
||||
"""Returns the greatest common divisor of a and b.
|
||||
Should be implemented using recursion.
|
||||
|
||||
>>> gcd(34, 19)
|
||||
1
|
||||
>>> gcd(39, 91)
|
||||
13
|
||||
>>> gcd(20, 30)
|
||||
10
|
||||
>>> gcd(40, 40)
|
||||
40
|
||||
"""
|
||||
"*** YOUR CODE HERE ***"7
|
||||
```
|
||||
|
||||
P6:汉诺塔问题(选做)
|
||||
|
||||
汉诺塔:汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着 64 片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
|
||||
|
||||
汉诺塔有递归和非递归两种方法,你最好选择递归的方法进行书写
|
||||
|
||||
```python
|
||||
def print_move(origin, destination):
|
||||
"""Print instructions to move a disk."""
|
||||
print("Move the top disk from rod", origin, "to rod", destination)
|
||||
|
||||
def move_stack(n, start, end):
|
||||
"""Print the moves required to move n disks on the start pole to the end
|
||||
pole without violating the rules of Towers of Hanoi.
|
||||
|
||||
n -- number of disks
|
||||
start -- a pole position, either 1, 2, or 3
|
||||
end -- a pole position, either 1, 2, or 3
|
||||
|
||||
There are exactly three poles, and start and end must be different. Assume
|
||||
that the start pole has at least n disks of increasing size, and the end
|
||||
pole is either empty or has a top disk larger than the top n start disks.
|
||||
|
||||
>>> move_stack(1, 1, 3)
|
||||
Move the top disk from rod 1 to rod 3
|
||||
>>> move_stack(2, 1, 3)
|
||||
Move the top disk from rod 1 to rod 2
|
||||
Move the top disk from rod 1 to rod 3
|
||||
Move the top disk from rod 2 to rod 3
|
||||
>>> move_stack(3, 1, 3)
|
||||
Move the top disk from rod 1 to rod 3
|
||||
Move the top disk from rod 1 to rod 2
|
||||
Move the top disk from rod 3 to rod 2
|
||||
Move the top disk from rod 1 to rod 3
|
||||
Move the top disk from rod 2 to rod 1
|
||||
Move the top disk from rod 2 to rod 3
|
||||
Move the top disk from rod 1 to rod 3
|
||||
"""
|
||||
assert 1 <= start <= 3 and 1 <= end <= 3 and start != end, "Bad start/end"
|
||||
"*** YOUR CODE HERE ***"
|
||||
```
|
||||
|
||||
ZZM 在这里恶意提升亿下难度,你能不能尝试理解下面这个用 C 语言写的汉诺塔呢
|
||||
|
||||
当然,是非递归!
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
int pc, n;
|
||||
char from, to, via;
|
||||
} Frame;
|
||||
|
||||
#define call(...) ({ *(++top) = (Frame) { .pc = 0, __VA_ARGS__ }; })
|
||||
#define ret() ({ top--; })
|
||||
#define goto(loc) ({ f->pc = (loc) - 1; })
|
||||
|
||||
void hanoi(int n, char from, char to, char via) {
|
||||
Frame stk[64], *top = stk - 1;
|
||||
call(n, from, to, via);
|
||||
for (Frame *f; (f = top) >= stk; f->pc++) {
|
||||
switch (f->pc) {
|
||||
case 0: if (f->n == 1) { printf("%c -> %c\n", f->from, f->to); goto(4); } break;
|
||||
case 1: call(f->n - 1, f->from, f->via, f->to); break;
|
||||
case 2: call( 1, f->from, f->to, f->via); break;
|
||||
case 3: call(f->n - 1, f->via, f->to, f->from); break;
|
||||
case 4: ret(); break;
|
||||
default: assert(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
316
2023旧版内容/3.编程思维体系构建/3.6.4.3阶段三:数据抽象.md
Normal file
@@ -0,0 +1,316 @@
|
||||
# 阶段三:数据抽象
|
||||
|
||||
数据抽象 (Data Abstraction)
|
||||
|
||||
::: warning 🐱 [可参考教程](https://zhuanlan.zhihu.com/p/343133774)
|
||||
|
||||
各位需要认真了解以下内容,他们是构建任何大厦的基石
|
||||
:::
|
||||
|
||||
## Data Abstraction
|
||||
|
||||
数据抽象是一个伟大的概念,它允许程序员将代码以对象的形式进行看待,并且从更高的层面去审视问题。
|
||||
|
||||
简单来说,它可以帮助程序员不用具体去了解程序做了什么,而是更去重视它有什么用。
|
||||
|
||||
举个例子:你在开车时,如果要控制发动机的活塞怎么动,对你来说是否有些太过于困难了。因此将其抽象成了离合器,油门,刹车这些较为简单的操作。
|
||||
|
||||
## 组成
|
||||
|
||||
一个抽象的数据类型(ADT)由两个主要部分组成
|
||||
|
||||
- Constructors:架构抽象数据类型的主要函数
|
||||
- Selectors:操作数据类型的各式方法
|
||||
|
||||
## 列表与元组
|
||||
|
||||
列表是可以存储多个元素的 Python 数据结构。每个元素可以是任何类型,甚至可以是另一个列表!
|
||||
|
||||
```python
|
||||
>>> list_of_nums = [1, 2, 3, 4]
|
||||
>>> list_of_bools = [True, True, False, False]
|
||||
>>> nested_lists = [1, [2, 3], [4, [5]]]
|
||||
```
|
||||
|
||||
其可以随意的增加删除或改动元素
|
||||
|
||||
元组何其差不多,但是最大的差距在于,元组是静态的,不可随意更改的,要想改动,必须重新开启一片内存空间
|
||||
|
||||
```python
|
||||
tup = (1, 2, 3, 4)
|
||||
new_tup = tup + (5, ) # 创建新的元组 new_tup,并依次填充原元组的值
|
||||
new _tup
|
||||
(1, 2, 3, 4, 5)
|
||||
|
||||
l = [1, 2, 3, 4]
|
||||
l.append(5) # 添加元素 5 到原列表的末尾
|
||||
l
|
||||
[1, 2, 3, 4, 5]
|
||||
```
|
||||
|
||||
同时,你可以对列表和元组进行索引,甚至使用:进行切片操作,这部分内容在以后的任务会有体现
|
||||
|
||||
```python
|
||||
>>> lst = [True, False, True, True, False]
|
||||
>>> lst[1:4]
|
||||
[False, True, True]
|
||||
>>> lst[:3] # Start index defaults to 0
|
||||
[True, False, True]
|
||||
>>> lst[3:] # End index defaults to len(lst)
|
||||
[True, False]
|
||||
>>> lst[:] # Creates a copy of the whole list
|
||||
[True, False, True, True, False]
|
||||
```
|
||||
|
||||
```python
|
||||
>>> lst = [6, 5, 4, 3, 2, 1]
|
||||
>>> lst[0]
|
||||
6
|
||||
>>> lst[3]
|
||||
3
|
||||
```
|
||||
|
||||
::: warning 🤔 思考题:
|
||||
|
||||
列表和元组在性能上有什么差异呢?
|
||||
|
||||
他们对应的使用场景有哪些呢?
|
||||
:::
|
||||
|
||||
## ;ltyi 字典与集合
|
||||
|
||||
字典是一系列由键(key)和值(value)配对组成的元素的集合,在 Python3.7+,字典被确定为有序
|
||||
|
||||
相比于列表和元组,字典的性能更优,特别是对于查找、添加和删除操作,字典都能在常数时间复杂度内完成。
|
||||
|
||||
而集合和字典基本相同,唯一的区别,就是集合没有键和值的配对,是一系列无序的、唯一的元素组合。
|
||||
|
||||
```python
|
||||
d1 = {'name': 'jason', 'age': 20, 'gender': 'male'}
|
||||
d2 = dict({'name': 'jason', 'age': 20, 'gender': 'male'})
|
||||
d3 = dict([('name', 'jason'), ('age', 20), ('gender', 'male')])
|
||||
d4 = dict(name='jason', age=20, gender='male')
|
||||
d1 == d2 == d3 ==d4
|
||||
True
|
||||
|
||||
s1 = {1, 2, 3}
|
||||
s2 = set([1, 2, 3])
|
||||
s1 == s2
|
||||
True
|
||||
```
|
||||
|
||||
当然,除了创建和访问,字典和集合也同样支持增加、删除、更新等操作。
|
||||
|
||||
```python
|
||||
d = {'name': 'jason', 'age': 20}
|
||||
d['gender'] = 'male' # 增加元素对'gender': 'male'
|
||||
d['dob'] = '1999-02-01' # 增加元素对'dob': '1999-02-01'
|
||||
d
|
||||
{'name': 'jason', 'age': 20, 'gender': 'male', 'dob': '1999-02-01'}
|
||||
d['dob'] = '1998-01-01' # 更新键'dob'对应的值
|
||||
d.pop('dob') # 删除键为'dob'的元素对
|
||||
'1998-01-01'
|
||||
d
|
||||
{'name': 'jason', 'age': 20, 'gender': 'male'}
|
||||
|
||||
s = {1, 2, 3}
|
||||
s.add(4) # 增加元素 4 到集合
|
||||
s
|
||||
{1, 2, 3, 4}
|
||||
s.remove(4) # 从集合中删除元素 4
|
||||
s
|
||||
{1, 2, 3}
|
||||
```
|
||||
|
||||
::: warning 🤔 思考题:
|
||||
|
||||
字典和集合分别是什么原理呢?
|
||||
|
||||
字典可以是一个列表吗?为什么?
|
||||
:::
|
||||
|
||||
## 可变性
|
||||
|
||||
我们说如果一个对象可以由代码进行操作而改变那么我们称其具有可变性。
|
||||
|
||||
可变对象的示例包括列表和字典。不可变对象的示例包括元组和函数。
|
||||
|
||||
我们假定已经知道了如何使用 `==` 运算符来检查两个表达式的计算结果是否**相同**。
|
||||
|
||||
我们现在引入一个新的比较运算符 `is`,它检查两个表达式的计算结果是否**相同**。
|
||||
|
||||
```python
|
||||
>>> 2 + 2 == 3 + 1
|
||||
True
|
||||
>>> 2 + 2 is 3 + 1
|
||||
True
|
||||
```
|
||||
|
||||
有什么不同呢?
|
||||
|
||||
```python
|
||||
>>> large_num1 = 23333333333333333333
|
||||
>>> large_num2 = 23333333333333333333
|
||||
>>> large_num1 == large_num2
|
||||
True
|
||||
>>> large_num1 is large_num2
|
||||
False
|
||||
>>> lst1 = [1, 2, 3, 4]
|
||||
>>> lst2 = [1, 2, 3, 4]
|
||||
>>> lst1 == lst2
|
||||
True
|
||||
>>> lst1 is lst2
|
||||
False
|
||||
```
|
||||
|
||||
欸?为什么最后一个不一样了?
|
||||
|
||||
其实原因是,你创建的两个列表虽然内容相同,但是毕竟是两个不同的列表,其在内存空间上并不一样。
|
||||
|
||||
这在讨论可变性的时候非常重要,当我们改变一个数的值的时候这非常重要
|
||||
|
||||
```python
|
||||
>>> lst1 = [1, 2, 3, 4]
|
||||
>>> lst2 = lst1
|
||||
>>> lst1.append(5)
|
||||
>>> lst2
|
||||
[1, 2, 3, 4, 5]
|
||||
>>> lst1 is lst2
|
||||
True
|
||||
```
|
||||
|
||||
::: warning 🤔 思考题,你能否从指针的角度去理解可变性呢?
|
||||
:::
|
||||
|
||||
## 任务
|
||||
|
||||
P7:9*9 乘法表
|
||||
|
||||
可能现在对你来说,构建像下图这样的 99 乘法表已经是非常容易的一件事了,可是如果我要求你使用 python 的列表推导式 (list comprehension),在两行以内完成呢?
|
||||
|
||||

|
||||
|
||||
P8:couple 情侣
|
||||
|
||||
实现函数 `couple`,它接受两个列表并返回一个列表,其中包含两个序列的第 i 个元素耦合在一起的列表。您可以假设两个序列的长度相同。
|
||||
tips:zip(list1,list2)
|
||||
|
||||
```python
|
||||
def couple(lst1, lst2):
|
||||
"""Return a list that contains lists with i-th elements of two sequences
|
||||
coupled together.
|
||||
>>> lst1 = [1, 2, 3]
|
||||
>>> lst2 = [4, 5, 6]
|
||||
>>> couple(lst1, lst2)
|
||||
[[1, 4], [2, 5], [3, 6]]
|
||||
>>> lst3 = ['c', 6]
|
||||
>>> lst4 = ['s', '1']
|
||||
>>> couple(lst3, lst4)
|
||||
[['c', 's'], [6, '1']]
|
||||
"""
|
||||
assert len(lst1) == len(lst2)
|
||||
"*** YOUR CODE HERE ***"
|
||||
```
|
||||
|
||||
P 8.5:对城市数据抽象
|
||||
|
||||
假设我们有一个城市的抽象数据类型。城市有名称、纬度坐标和经度坐标。
|
||||
|
||||
这是我们构造城市的函数
|
||||
|
||||
make_city(name, lat, lon)
|
||||
|
||||
构建一个城市对象有经纬度和名字
|
||||
|
||||
- `get_name(city)`:返回城市名称
|
||||
- `get_lat(city)`:返回城市的纬度
|
||||
- `get_lon(city)`:返回城市的经度
|
||||
|
||||
```python
|
||||
>>> nanjing = make_city('Nanjing', 31, 118)
|
||||
>>> get_name(nanjing)
|
||||
'Nanjing'
|
||||
>>> get_lat(nanjing)
|
||||
31
|
||||
>>> beijing = make_city('Beijing', 39, 116)
|
||||
>>> get_lon(beijing)
|
||||
116
|
||||
```
|
||||
|
||||
```python
|
||||
def make_city(name, lat, lon):
|
||||
"""
|
||||
>>> city = make_city('Berkeley', 0, 1
|
||||
>>> get_name(city)
|
||||
'Berkeley
|
||||
>>> get_lat(city)
|
||||
0
|
||||
>>> get_lon(city)
|
||||
1
|
||||
"""
|
||||
return [name, lat, lon]
|
||||
|
||||
def get_name(city):
|
||||
"""
|
||||
>>> city = make_city('Berkeley', 0, 1)
|
||||
>>> get_name(city)
|
||||
'Berkeley'
|
||||
"""
|
||||
return city[0]
|
||||
|
||||
def get_lat(city):
|
||||
"""
|
||||
>>> city = make_city('Berkeley', 0, 1)
|
||||
>>> get_lat(city)
|
||||
0
|
||||
"""
|
||||
return city[1]
|
||||
|
||||
def get_lon(city):
|
||||
"""
|
||||
>>> city = make_city('Berkeley', 0, 1)
|
||||
>>> get_lon(city)
|
||||
1
|
||||
"""
|
||||
return city[2]
|
||||
```
|
||||
|
||||
首先你试试求出两个地方的距离。
|
||||
|
||||
`(x2, y2)` 可以通过计算 的 sqrt 来找到 `(x1 - x2)**2 + (y1 - y2)**2`。`sqrt` 我们引用了
|
||||
|
||||
```python
|
||||
from math import sqrt
|
||||
def distance(city1, city2):
|
||||
"""
|
||||
>>> city1 = make_city('city1', 0, 1)
|
||||
>>> city2 = make_city('city2', 0, 2)
|
||||
>>> distance(city1, city2)
|
||||
1.0
|
||||
>>> city3 = make_city('city3', 6.5, 12)
|
||||
>>> city4 = make_city('city4', 2.5, 15)
|
||||
>>> distance(city3, city4)
|
||||
5.0
|
||||
"""
|
||||
"*** YOUR CODE HERE ***"
|
||||
```
|
||||
|
||||
实现 `closer_city` 一个函数,该函数接受一个纬度、经度和两个城市,并返回与提供的经纬度相对较近的城市名称。
|
||||
|
||||
```python
|
||||
def closer_city(lat, lon, city1, city2):
|
||||
"""
|
||||
Returns the name of either city1 or city2, whichever is closest to
|
||||
coordinate (lat, lon).
|
||||
|
||||
>>> berkeley = make_city('Berkeley', 37.87, 112.26)
|
||||
>>> stanford = make_city('Stanford', 34.05, 118.25)
|
||||
>>> closer_city(38.33, 121.44, berkeley, stanford)
|
||||
'Stanford'
|
||||
>>> bucharest = make_city('Bucharest', 44.43, 26.10)
|
||||
>>> vienna = make_city('Vienna', 48.20, 16.37)
|
||||
>>> closer_city(41.29, 174.78, bucharest, vienna)
|
||||
'Bucharest'
|
||||
"""
|
||||
"*** YOUR CODE HERE ***"
|
||||
```
|
||||
152
2023旧版内容/3.编程思维体系构建/3.6.4.4阶段四:高阶函数.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# 阶段四:高阶函数
|
||||
|
||||
::: warning 🐱 阅读以及完成本部分内容可以帮助你有效减少代码冗余。
|
||||
|
||||
让你完成更为优雅的代码
|
||||
|
||||
各位要记住的是
|
||||
|
||||
<font size=5><strong>代码首先是给人看的</strong></font>
|
||||
|
||||
机器看的永远只是你的机器码。
|
||||
|
||||
可参考教程 [Lambda](https://zhuanlan.zhihu.com/p/80960485)
|
||||
:::
|
||||
|
||||
## Lambda 介绍
|
||||
|
||||
Lambda 表达式是通过指定两件事来评估函数的表达式:参数和返回表达式。
|
||||
|
||||
请尝试阅读以下英文表格,对比函数与 lambda 表达式的不同
|
||||
|
||||
## Lambda 实验
|
||||
|
||||
以下代码 python 会显示什么?通过对这些代码的实验,加深你对代码的学习
|
||||
|
||||
提示:当你对解释器输入 x 或 x=none 时什么都没有
|
||||
|
||||
```python
|
||||
>>> lambda x: x # A lambda expression with one parameter x
|
||||
______
|
||||
|
||||
>>> a = lambda x: x # Assigning the lambda function to the name a
|
||||
>>> a(5)
|
||||
______
|
||||
|
||||
>>> (lambda: 3)() # Using a lambda expression as an operator in a call exp.
|
||||
______
|
||||
|
||||
>>> b = lambda x: lambda: x # Lambdas can return other lambdas!
|
||||
>>> c = b(88)
|
||||
>>> c
|
||||
______
|
||||
|
||||
>>> c()
|
||||
______
|
||||
|
||||
>>> d = lambda f: f(4) # They can have functions as arguments as well.
|
||||
>>> def square(x):
|
||||
... return x * x
|
||||
>>> d(square)
|
||||
______
|
||||
```
|
||||
|
||||
```python
|
||||
>>> higher_order_lambda = lambda f: lambda x: f(x)
|
||||
>>> g = lambda x: x * x
|
||||
>>> higher_order_lambda(2)(g) # Which argument belongs to which function call?
|
||||
______
|
||||
|
||||
>>> higher_order_lambda(g)(2)
|
||||
______
|
||||
|
||||
>>> call_thrice = lambda f: lambda x: f(f(f(x)))
|
||||
>>> call_thrice(lambda y: y + 1)(0)
|
||||
______
|
||||
|
||||
>>> print_lambda = lambda z: print(z) # When is the return expression of a lambda expression executed?
|
||||
>>> print_lambda
|
||||
______
|
||||
|
||||
>>> one_thousand = print_lambda(1000)
|
||||
______
|
||||
|
||||
>>> one_thousand
|
||||
______
|
||||
```
|
||||
|
||||
## 任务
|
||||
|
||||
P9:我们发现以下两个函数看起来实现的非常相似,是否可以进行改进,将其整合?
|
||||
|
||||
```python
|
||||
def count_factors(n):
|
||||
"""Return the number of positive factors that n has.
|
||||
>>> count_factors(6)
|
||||
4 # 1, 2, 3, 6
|
||||
>>> count_factors(4)
|
||||
3 # 1, 2, 4
|
||||
"""
|
||||
i, count = 1, 0
|
||||
while i <= n:
|
||||
if n % i == 0:
|
||||
count += 1
|
||||
i += 1
|
||||
return count
|
||||
|
||||
def count_primes(n):
|
||||
"""Return the number of prime numbers up to and including n.
|
||||
>>> count_primes(6)
|
||||
3 # 2, 3, 5
|
||||
>>> count_primes(13)
|
||||
6 # 2, 3, 5, 7, 11, 13
|
||||
"""
|
||||
i, count = 1, 0
|
||||
while i <= n:
|
||||
if is_prime(i):
|
||||
count += 1
|
||||
i += 1
|
||||
return count
|
||||
|
||||
def is_prime(n):
|
||||
return count_factors(n) == 2 # only factors are 1 and n
|
||||
```
|
||||
|
||||
需求:
|
||||
|
||||
你需要通过自己写一个函数: `count_cond` ,来接受一个含有两个参数的函数 `condition(n, i)`(使用 lambda 表达式),
|
||||
|
||||
且`condition`函数应该满足第一个参数为 N,而第二个参数将会在`condition`函数中遍历 1 to N。
|
||||
|
||||
`count_cond` 将返回一个单参数函数 (ps:一个匿名函数),此单参数函数将会在被调用时返回 1 to N 中所有满足`condition`的数字的个数 (如:1 到 n 中素数的个数)。
|
||||
|
||||
```python
|
||||
def count_cond(condition):
|
||||
"""Returns a function with one parameter N that counts all the numbers from
|
||||
1 to N that satisfy the two-argument predicate function Condition, where
|
||||
the first argument for Condition is N and the second argument is the
|
||||
number from 1 to N.
|
||||
|
||||
>>> count_factors = count_cond(lambda n, i: n % i == 0)
|
||||
>>> count_factors(2) # 1, 2
|
||||
2
|
||||
>>> count_factors(4) # 1, 2, 4
|
||||
3
|
||||
>>> count_factors(12) # 1, 2, 3, 4, 6, 12
|
||||
6
|
||||
|
||||
>>> is_prime = lambda n, i: count_factors(i) == 2
|
||||
>>> count_primes = count_cond(is_prime)
|
||||
>>> count_primes(2) # 2
|
||||
1
|
||||
>>> count_primes(3) # 2, 3
|
||||
2
|
||||
>>> count_primes(4) # 2, 3
|
||||
2
|
||||
>>> count_primes(5) # 2, 3, 5
|
||||
3
|
||||
>>> count_primes(20) # 2, 3, 5, 7, 11, 13, 17, 19
|
||||
8
|
||||
"""
|
||||
"*** YOUR CODE HERE ***"
|
||||
```
|
||||
241
2023旧版内容/3.编程思维体系构建/3.6.4.5阶段五:迭代生成.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# 阶段五:迭代生成
|
||||
|
||||
## 前言
|
||||
|
||||
在写乘法表的时候,你可能写过类似
|
||||
|
||||
```python
|
||||
for i in [2, 3, 5, 7, 11, 13]: print(i)
|
||||
```
|
||||
|
||||
这样的语句。for in 语句理解起来很直观形象,比起 C++ 和 java 早期的
|
||||
|
||||
```c
|
||||
for (int i = 0; i < n; i ++)
|
||||
```
|
||||
|
||||
这样的语句,不知道简洁清晰到哪里去了。
|
||||
|
||||
但是,你想过 Python 在处理 for in 语句的时候,具体发生了什么吗?什么样的对象可以被 for in 来枚举呢?
|
||||
|
||||
## 容器迭代
|
||||
|
||||
容器这个概念非常好理解。
|
||||
|
||||
在 Python 中一切皆对象,对象的抽象就是类,而对象的集合就是容器。
|
||||
|
||||
列表`list: [0, 1, 2]`,元组`tuple: (0, 1, 2)`,字典`dict: {0:0, 1:1, 2:2}`,集合`set: set([0, 1, 2])`都是容器。
|
||||
|
||||
对于容器,你可以很直观地想象成多个元素在一起的单元;而不同容器的区别,正是在于内部数据结构的实现方法。
|
||||
|
||||
然后,你就可以针对不同场景,选择不同时间和空间复杂度的容器。所有的容器都是可迭代的(iterable)。
|
||||
|
||||
```python
|
||||
iterator = iter(iterable)
|
||||
while True:
|
||||
elem = next(iterator)
|
||||
# do something
|
||||
```
|
||||
|
||||
- 首先,在可迭代对象上调用内置 `iter` 函数以创建对应的*迭代器*。
|
||||
- 要获取序列中的下一个元素,在此迭代器上调用内置 `next` 函数。
|
||||
|
||||
如果没有下一个元素了,怎么办?
|
||||
|
||||
我们需要对这种异常进行处理。
|
||||
|
||||
思考题:什么是异常处理,为什么要进行异常处理?有什么好处?
|
||||
|
||||
多次调用 `iter` 可迭代对象每次都会返回一个具有不同状态的新迭代器
|
||||
|
||||
你也可以调用 `iter` 迭代器本身,它只会返回相同的迭代器而不改变它的状态。但是,请注意,您不能直接在可迭代对象上调用 next。
|
||||
|
||||
```python
|
||||
>>> lst = [1, 2, 3, 4]
|
||||
>>> next(lst) # Calling next on an iterable
|
||||
TypeError: 'list' object is not an iterator
|
||||
>>> list_iter = iter(lst) # Creates an iterator for the list
|
||||
>>> list_iter
|
||||
<list_iterator object ...>
|
||||
>>> next(list_iter) # Calling next on an iterator
|
||||
1
|
||||
>>> next(list_iter) # Calling next on the same iterator
|
||||
2
|
||||
>>> next(iter(list_iter)) # Calling iter on an iterator returns itself
|
||||
3
|
||||
>>> list_iter2 = iter(lst)
|
||||
>>> next(list_iter2) # Second iterator has new state
|
||||
1
|
||||
>>> next(list_iter) # First iterator is unaffected by second iterator
|
||||
4
|
||||
>>> next(list_iter) # No elements left!
|
||||
StopIteration
|
||||
>>> lst # Original iterable is unaffected
|
||||
[1, 2, 3, 4]
|
||||
```
|
||||
|
||||
## 英语练习,对迭代器的类比
|
||||
|
||||
**Analogy**: An iterable is like a book (one can flip through the pages) and an iterator for a book would be a bookmark (saves the position and can locate the next page). Calling `iter` on a book gives you a new bookmark independent of other bookmarks, but calling `iter` on a bookmark gives you the bookmark itself, without changing its position at all. Calling `next` on the bookmark moves it to the next page, but does not change the pages in the book. Calling `next` on the book wouldn't make sense semantically. We can also have multiple bookmarks, all independent of each other.
|
||||
|
||||
## 生成器:懒人迭代器!
|
||||
|
||||
```python
|
||||
def test_iterator():
|
||||
show_memory_info('initing iterator')
|
||||
list_1 = [i for i in range(100000000)]
|
||||
show_memory_info('after iterator initiated')
|
||||
print(sum(list_1))
|
||||
show_memory_info('after sum called')
|
||||
|
||||
def test_generator():
|
||||
show_memory_info('initing generator')
|
||||
list_2 = (i for i in range(100000000))
|
||||
show_memory_info('after generator initiated')
|
||||
print(sum(list_2))
|
||||
show_memory_info('after sum called')
|
||||
|
||||
%time test_iterator()
|
||||
%time test_generator()
|
||||
|
||||
########## 输出 ##########
|
||||
|
||||
initing iterator memory used: 48.9765625 MB
|
||||
after iterator initiated memory used: 3920.30078125 MB
|
||||
4999999950000000
|
||||
after sum called memory used: 3920.3046875 MB
|
||||
Wall time: 17 s
|
||||
initing generator memory used: 50.359375 MB
|
||||
after generator initiated memory used: 50.359375 MB
|
||||
4999999950000000
|
||||
after sum called memory used: 50.109375 MB
|
||||
Wall time: 12.5 s
|
||||
```
|
||||
|
||||
声明一个迭代器很简单,[i for i in range(100000000)]就可以生成一个包含一亿元素的列表。每个元素在生成后都会保存到内存中,你通过代码可以看到,它们占用了巨量的内存,内存不够的话就会出现 OOM 错误。
|
||||
|
||||
::: warning 🤔 了解下 yield()函数吧,他可以返回一个生成器对象,试试看懂这个
|
||||
:::
|
||||
|
||||
```python
|
||||
>>> def gen_list(lst):
|
||||
... yield from lst
|
||||
...
|
||||
>>> g = gen_list([1, 2, 3, 4])
|
||||
>>> next(g)
|
||||
1
|
||||
>>> next(g)
|
||||
2
|
||||
>>> next(g)
|
||||
3
|
||||
>>> next(g)
|
||||
4
|
||||
>>> next(g)
|
||||
StopIteration
|
||||
```
|
||||
|
||||
## 思考题:python 会显示什么?为什么?
|
||||
|
||||
```python
|
||||
>>> s = [1, 2, 3, 4]
|
||||
>>> t = iter(s)
|
||||
>>> next(s)
|
||||
______
|
||||
|
||||
>>> next(t)
|
||||
______
|
||||
|
||||
>>> next(t)
|
||||
______
|
||||
|
||||
>>> iter(s)
|
||||
______
|
||||
|
||||
>>> next(iter(s))
|
||||
______
|
||||
|
||||
>>> next(iter(t))
|
||||
______
|
||||
|
||||
>>> next(iter(s))
|
||||
______
|
||||
|
||||
>>> next(iter(t))
|
||||
______
|
||||
|
||||
>>> next(t)
|
||||
______
|
||||
```
|
||||
|
||||
```python
|
||||
>>> r = range(6)
|
||||
>>> r_iter = iter(r)
|
||||
>>> next(r_iter)
|
||||
______
|
||||
|
||||
>>> [x + 1 for x in r]
|
||||
______
|
||||
|
||||
>>> [x + 1 for x in r_iter]
|
||||
______
|
||||
|
||||
>>> next(r_iter)
|
||||
______
|
||||
|
||||
>>> list(range(-2, 4)) # Converts an iterable into a list
|
||||
______
|
||||
```
|
||||
|
||||
## 任务
|
||||
|
||||
P10:实现 `count`,它接受一个迭代器 `t` 并返回该值 `x` 出现在的前 n 个元素中的次数 `t`
|
||||
|
||||
```python
|
||||
def count(t, n, x):
|
||||
"""Return the number of times that x appears in the first n elements of iterator t.
|
||||
|
||||
>>> s = iter([10, 9, 10, 9, 9, 10, 8, 8, 8, 7])
|
||||
>>> count(s, 10, 9)
|
||||
3
|
||||
>>> s2 = iter([10, 9, 10, 9, 9, 10, 8, 8, 8, 7])
|
||||
>>> count(s2, 3, 10)
|
||||
2
|
||||
>>> s = iter([3, 2, 2, 2, 1, 2, 1, 4, 4, 5, 5, 5])
|
||||
>>> count(s, 1, 3)
|
||||
1
|
||||
>>> count(s, 4, 2)
|
||||
3
|
||||
>>> next(s)
|
||||
2
|
||||
>>> s2 = iter([4, 1, 6, 6, 7, 7, 8, 8, 2, 2, 2, 5])
|
||||
>>> count(s2, 6, 6)
|
||||
2
|
||||
"""
|
||||
"*** YOUR CODE HERE ***"
|
||||
```
|
||||
|
||||
P11:实现生成器函数 `scale(it, multiplier)`,它产生给定迭代的元素 `it`,按 `multiplier`.
|
||||
|
||||
同时也希望你尝试使用 `yield from` 语句编写这个函数!
|
||||
|
||||
```python
|
||||
def scale(it, multiplier):
|
||||
"""Yield elements of the iterable it scaled by a number multiplier.
|
||||
|
||||
>>> m = scale(iter([1, 5, 2]), 5)
|
||||
>>> type(m)
|
||||
<class 'generator'>
|
||||
>>> list(m)
|
||||
[5, 25, 10]
|
||||
>>> # generators allow us to represent infinite sequences!!!
|
||||
>>> def naturals():
|
||||
... i = 0
|
||||
... while True:
|
||||
... yield i
|
||||
... i += 1
|
||||
>>> m = scale(naturals(), 2)
|
||||
>>> [next(m) for _ in range(5)]
|
||||
[0, 2, 4, 6, 8]
|
||||
"""
|
||||
"*** YOUR CODE HERE ***"
|
||||
```
|
||||
41
2023旧版内容/3.编程思维体系构建/3.6.4.6结语.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# 结语
|
||||
|
||||
## 感觉学了不少内容,又感觉什么都没学?
|
||||
|
||||
确实,这也是学习过程中一个非常普遍存在的状态,你学了五个非常重要的 python 语言的特性,但是在你用他们真正来优化代码解决代码之前,你都无法真正的掌握他们。
|
||||
|
||||
那为什么要学呢?
|
||||
|
||||
非常核心的一点在于,你以后面对困难的问题的时候,将他不断分解,成为一个个独立的小问题的时候,你也许会想起你之前在某文档内,有一个简单的教程曾经将某种神奇的方法一笔带过,这也许会提供给你意想不到的解决问题的思路和方法。
|
||||
|
||||
同时,python 的核心特性就这些吗?远远不止呢,这些只是你入手其的敲门砖,我在下面会列举一些别的特性,你可以自行前去了解,也可以等到你真正遇到问题的时候去思考?
|
||||
|
||||
## 为什么没有突出面向对象
|
||||
|
||||
因为代码量实在是太少了,当你去理解面向对象的时候的伟大意义时,最好与 C 语言这种面向过程的语言进行对比,不过,如果你完成了我们的文字冒险小游戏的时候,你可能会有非常深刻的体验。
|
||||
|
||||
## 还有什么是我值得注意的?
|
||||
|
||||
这些内容值得你去学习与思考,但是碍于各种因素,我暂时没有详细介绍
|
||||
|
||||
- 面向对象编程
|
||||
- 深浅拷贝
|
||||
- 值传递
|
||||
- 装饰器
|
||||
- metaclass
|
||||
- GIL
|
||||
- 垃圾回收机制
|
||||
- 协程
|
||||
- 数不清的各种框架和包!!!
|
||||
|
||||
如果有机会,我会继续补充相关内容
|
||||
|
||||
值得一提的是:后面人工智能模块,我们将以 python 为主去进行编程,这对完成了所有任务的你一定是 a piece of cake!
|
||||
|
||||
## 一些不错的补充
|
||||
|
||||
[WTF for python](https://github.com/robertparley/wtfpython-cn)
|
||||
|
||||
[真正的 python 教程](https://realpython.com/)
|
||||
|
||||
[python100 天](https://github.com/jackfrued/Python-100-Days)
|
||||
9
2023旧版内容/3.编程思维体系构建/3.6.4Python for fun.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Python for fun
|
||||
|
||||
值得一提的是,Python 是面向对象的编程语言,与你之前书写的 C 语言(面向过程)有非常多的不同。
|
||||
|
||||
我们为什么要选择要使用 Python 来首先讲解面向对象这个概念?
|
||||
|
||||
究竟什么是面向对象?为什么面向对象?
|
||||
|
||||
这值得你在书写 Python 代码中思考并尝试将其思想融会贯通到编码风格中去。
|
||||
7
2023旧版内容/3.编程思维体系构建/3.6Python(灵巧的胶水).md
Normal file
@@ -0,0 +1,7 @@
|
||||
# 3.6Python(灵巧的胶水)
|
||||
|
||||
Python 是一门简单易学的脚本语言学会,其能做的事情也很多,常见的就有网络爬虫,数据分析,前端开发,[机器学习](https://www.zhihu.com/search?q=%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0&search_source=Entity&hybrid_search_source=Entity&hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A%222550582151%22%7D),都能很好地提高工作效率。
|
||||
|
||||
对于计算机科学的学生来说,python 不会设置专门的课程教你而是会要求你在假期内自学,因为其丰富的库函数和简洁的语法让 python 可以轻松完成一些本来非常难做到的事情。
|
||||
|
||||
[如果各位想要以查阅字典或者线性的方式了解一下不要看菜鸟教程!来看看这个](https://docs.python.org/zh-cn/3/tutorial/index.html),手册的是第一手资料正确性很高,或者说他基本不会出错!
|
||||
81
2023旧版内容/3.编程思维体系构建/3.X 聊聊设计模式和程序设计.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# 3.X 聊聊设计模式和程序设计
|
||||
|
||||
*Author: yyl
|
||||
Last revised 2022/08/07*
|
||||
|
||||
## 前言
|
||||
|
||||
在开始讲内容之前,我觉得我得首先聊一聊到底为什么我们需要这些东西。为什么我们会需要设计模式呢?
|
||||
|
||||
这个问题并不可笑,也不幼稚,事实上,很多东西红极一时,最终却被历史证明是不被需要的。类似的事情太多了,可以是一种理念,比如日心说,可以是一种产品,比如软驱、RAMBUS 内存,等等。历史已经丢弃了太多不被需要的东西了。
|
||||
|
||||
学习本身是有成本的事情,如果在开始之前能够理解为什么要开始就更好,尽管很多时候我们做不到这件事,但也不能忘了这件事。
|
||||
|
||||
上个世纪 80~90 年代的时候,人们试图通过“设计模式”这一工具,抽象总结所有的软件设计方法,构建一个无敌的知识库。最终使得软件工程就像堆积木一样简单,避免软件随时间变得杂乱的熵增问题,但毫无疑问这是失败的,你永远无法抽象总结所有东西,设计模式的巅峰,可能是在 JDK 之中。在阅读 Java 自身提供的诸多 API 时,你会发现其中蕴藏了大量的设计模式,而且是赤裸裸地把使用了的设计模式写在了类名中,如“XXXListener”,很少有另一个语言也是这样的。
|
||||
|
||||
设计模式并没有完全成功,但也没有完全失败。他现在以一种“术语”的形式存在,帮助人们理解常见的编程范式。——这是什么意思呢?举个例子,如果你尝试和同事说明你的一个设计时,你说,它首先有两个类,一个类提供了一个通用的接口,可以被另一个类注册,另一个类则负责把所有的动作通知给已经注册的类。这种说法显然非常繁琐,对方也未必能理解你的意思。但如果你说,这里使用了监听者模式,如果你的同事碰巧学习过一些设计模式,他就能立即理解你的意思。即使他没有学习过相关知识,只要简单百度一下也能理解个大概。这是当今设计模式的一个重要作用。
|
||||
|
||||
另一个重要作用则是它的本源了,用于总结抽象程序设计的范式。但并不是说要你写的所有代码能用设计模式的就都用设计模式,这更多是提供给你一种选项,让你能够充分权衡。关于这部分,下文还会有说明。
|
||||
|
||||
目前的设计模式,主要集中在如何避免程序复杂度上。
|
||||
|
||||
什么是程序复杂度呢?就是说你今天写了个软件,明天给他加功能的时候发现这个程序不怎么可维护,于是直接在之前的逻辑上加了个 if,你的一个 if 通常不会有什么大问题,程序复杂性问题是日积月累的。随着大家都在不同的地方写自己的 if,整个程序最终会变得不可维护。可能一个微小的改动会导致很多地方的 if 出现不符合预期的判断,可能终有一天有一个需求无法再用 if 来实现,等等。
|
||||
|
||||
为了解决这个问题,设计模式使用的方法是原则,通过制定一系列的原则,并确保大家都遵守,来避免代码腐烂。
|
||||
|
||||
## 拿个锤子,见什么都是钉子
|
||||
|
||||
设计模式,或者说广义的程序设计架构的初学者,很多都会想去设计一个“完美的架构”。
|
||||
|
||||
当就像计算机程序设计本身是关于权衡的艺术一样,学习程序架构也不能看见什么都上架构。下面举例子具体说明。
|
||||
|
||||
你要设计一个小程序给自己用,这个程序的作用是定时读取一边本地硬盘上的全部文件,通过 sha256 算法生成每个文件的文件摘要,比对文件是否遭到篡改,如果遭到了篡改就报个警。
|
||||
|
||||
如果你没有学习过这些乱七八糟的设计模式,我想你大概会这么做:
|
||||
|
||||
main():
|
||||
|
||||
walk("/");
|
||||
|
||||
walk(String path):
|
||||
|
||||
For file : os.listfile(path):
|
||||
|
||||
// 检查文件摘要
|
||||
|
||||
For dir : os.listdir(path):
|
||||
|
||||
walk(dir);
|
||||
|
||||
但如果你很不幸,学习过一些设计模式,或者说有一些程序设计经验,你会开始考虑一下问题:
|
||||
|
||||
1. 我用什么语言实现?这个语言的代码文件组织有没有推荐的架构?(比如,第三方包引用了放在哪里?文件加密的 util 是放在内部还是封装成可供其他人引用的三方包?)
|
||||
2. 选用什么样的依赖管理机制?
|
||||
3. 编译最终产物的时候用什么工具,make 还是 maven 还是 gradle 还是 shell 还是 python?...
|
||||
4. walk 能不能兼容不同平台的系统?在 win 下面能不能用?
|
||||
5. 有没有什么设计模式可以用?(开始翻书)
|
||||
6. 卧槽,用官方推荐的组织架构的话这个程序有点单薄啊,要不要加点功能上去
|
||||
7. ...
|
||||
|
||||
要明白,拿着个锤子绝不能看什么都是钉子。
|
||||
|
||||
设计模式本身也是权衡的艺术,我们今天经常说的东西,比如 DDD,SOA,微服务,monorepo,也都是一种广义的设计模式。权衡什么呢?权衡的是:是要维持程序在发生功能变动时的可拓展性、降低程序维护复杂度,还是追求程序的快速实现。
|
||||
|
||||
拿上文的例子来说,如果你的所有程序都是在运行 Linux 上的,这个也只是给你自己快速检查文件完整性,之后也几乎不存在增加新的功能的可能,为什么不随便找个脚本语言快速十几行写完呢?
|
||||
|
||||
## 如何学习
|
||||
|
||||
个人认为,现在所谓的设计模式分两种,第一种是狭义的设计模式,就是各种设计模式的堆积。
|
||||
|
||||
此外,还有广义的设计模式,这块内容就很多了,但也是讨论如何降低代码复杂度的,包括:
|
||||
|
||||
1. 程序代码怎么组织(在开发 web 应用时这块经常有争议,可以参考 [浅析整洁架构之道 (二) 初步了解 The Clean Architecture](https://cloud.tencent.com/developer/article/1837487))
|
||||
2. 程序之间怎么组织(是放在一个编成个大的,还是微服务,还是用 FaaS 之类的变体)
|
||||
3. 一些帮助减少程序复杂度的代码原则:[设计模式之 SOLID 原则](https://zhuanlan.zhihu.com/p/82324809)
|
||||
|
||||
这部分的学习不能操之过急。个人的建议是:
|
||||
|
||||
1. 学习一些设计模式,看完两次肯定没什么感觉,甚至会觉得看了个寂寞(可以先看看 Head first 设计模式)
|
||||
2. 忘了他,去写你自己的代码,遇到复杂度诅咒了再考虑如何解决
|
||||
3. 读他人的优秀代码,想一想这里他为什么要这么写,换成是你的话会怎么写,各自有什么利弊
|
||||
4. 重复 1
|
||||
63
2023旧版内容/3.编程思维体系构建/3.Y 附加模块:Linux.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# 附加模块:Linux
|
||||
|
||||
::: warning 😇 本来这个模块在编程模块内,但是鉴于大家都反应做这一块非常难,因此我将他提出作为一个额外的附加模块。
|
||||
|
||||
如果你想尝试使用 Linux 编程或者想了解更多计算机科学领域知识,你可以学习并阅览本部分内容。
|
||||
|
||||
当然你也可以先尝试完成第三部分的一些内容再回过头解决本部分的内容。
|
||||
|
||||
可能会花费你大量的时间,并且让你感受到非常困难,但是可以保证的是:你的一切投入,都是有收获的。
|
||||
:::
|
||||
|
||||
## What???Linux???
|
||||
|
||||
大家可能知道我们的电脑是 Windows 作为操作系统的。
|
||||
|
||||
而 Linux 也是一款有趣的开源的操作系统
|
||||
|
||||
它既免费也自由 (能知道它内部的实现),而且互联网上有丰富的 (英文) 文档。
|
||||
|
||||
它的设计继承自“Keep it simple, stupid”的 UNIX,这个经典的设计背后的动机反而更容易为第一次接触操作系统的初学者所理解。让我们看看它的威力:
|
||||
|
||||
- 首先,操作系统里的一切对象都用文件表示 (Everything is a file)。进程、设备……都可以在任何编程语言里用文件 API 访问。
|
||||
- Linux 的命令行 Shell 是一门编程语言——没错,你每天都在“编程”!更准确地说,Shell 的功能就是把你想要做的事情 (类似自然语言描述的代码) 翻译成操作系统能看懂的文件/进程管理 API 调用。
|
||||
|
||||
## Why Linux???
|
||||
|
||||
作为一个双系统用户体验者来说,他除了玩游戏不那么方便以外,可以更为高效且便捷的办到 Windows 费很大力气才能办到的事情。
|
||||
|
||||
并且相当多的开发软件在 Linux 上有更好的兼容性,而到 windows 上你将会花费大量的时间配置各种环境变量还容易出错。
|
||||
|
||||
并且目前,服务器上为了保证低损耗,高效率,基本上百分之九十九都是 Linux 的系统,实验室的服务器也是 Linux 系统。
|
||||
|
||||
简单来说就是,你如果想干点事情,肯定要靠 Linux,因此学会 Linux 的操作是不可或缺的
|
||||
|
||||
而且我个人认为,linux 的自由性对于 CSer 来说非常适合,他不会阻止你干任何操作,你可以充分体会所以你的命令带来的影响 (rm -rf /)
|
||||
|
||||
### GUI 与 CLI
|
||||
|
||||
诚然,我们现在的图形化界面(GUI)已经深入到了生活的方方面面,但是优劣如何对比呢?
|
||||
|
||||
[Command line vs. GUI](https://www.computerhope.com/issues/ch000619.htm)
|
||||
|
||||
这篇文章详细对比了图形化界面和单纯的终端命令的优劣
|
||||
|
||||
## How Linux???
|
||||
|
||||
那么这么好的东西哪里可以获得呢?
|
||||
|
||||
因为 Linux 有诸多发行版本,我在这里建议大家使用 Ubuntu22.04 作为主要版本进行使用
|
||||
|
||||
如果你很猛,去试试 arch!
|
||||
|
||||
任务:装 Ubuntu22.04 或者 debian,如果你想删了自己的系统,可以试试 deepin,当然,也会有一些兼容性问题,不过会支持一些中文软件
|
||||
|
||||
tip1:推荐这个 [3.Y.3VMware 的安装与安装 ubuntu22.04 系统](3.Y.3VMware%E7%9A%84%E5%AE%89%E8%A3%85%E4%B8%8E%E5%AE%89%E8%A3%85Ubuntu22.04%E7%B3%BB%E7%BB%9F.md)
|
||||
|
||||
tip2:可以使用 WSL[3.Y.4WSL 的安装](3.Y.4WSL%E7%9A%84%E5%AE%89%E8%A3%85.md),<del>但是我更建议实装到电脑上双系统之类的</del>,正好锻炼一下<del>装系统</del>倒腾的能力。大可不必删了 windows 换成 ubuntu。
|
||||
|
||||
tip3:前两个 tip 二选一。
|
||||
|
||||
在开始之前,建议先阅读[3.Y.1Linux概念普及](3.Y.1Linux概念普及.md),了解一些基本概念,**免得把系统搞坏了**,尤其是 WSL 有可能把 Windows 也一块带走,**之前就有群友做到过**。
|
||||
|
||||
任务:阅读 GUI 与命令行之间对比的文章,尝试开始阅读英文文章
|
||||
31
2023旧版内容/3.编程思维体系构建/3.Y.0计算机教育中缺失的一课.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# 计算机教育中缺失的一课
|
||||
|
||||
Author : ek1ng , Data : 2023.07.24 , Mail : <ek1ng@qq.com>
|
||||
|
||||
## 计算机教育中缺失的一课
|
||||
>
|
||||
> [https://missing-semester-cn.github.io/](https://missing-semester-cn.github.io/)
|
||||
> [https://ek1ng.com/Missing%20Semester.html](https://ek1ng.com/Missing%20Semester.html)
|
||||
|
||||
这是一份国外的课程,主要专注于各类工具的使用,可以看一看课程的介绍:
|
||||
|
||||
>大学里的计算机课程通常专注于讲授从操作系统到机器学习这些学院派的课程或主题,而对于如何精通工具这一主题则往往会留给学生自行探索。在这个系列课程中,我们讲授命令行、强大的文本编辑器的使用、使用版本控制系统提供的多种特性等等。学生在他们受教育阶段就会和这些工具朝夕相处(在他们的职业生涯中更是这样)。
|
||||
>因此,花时间打磨使用这些工具的能力并能够最终熟练地、流畅地使用它们是非常有必要的。
|
||||
|
||||
以及相应的目录:
|
||||
|
||||
- **1/13**: [课程概览与 shell](https://missing-semester-cn.github.io/2020/course-shell/)[](https://missing-semester-cn.github.io/missing-notes-and-solutions/2020/solutions//course-shell-solution)
|
||||
- **1/14**: [Shell 工具和脚本](https://missing-semester-cn.github.io/2020/shell-tools/)[](https://missing-semester-cn.github.io/missing-notes-and-solutions/2020/solutions//shell-tools-solution)
|
||||
- **1/15**: [编辑器 (Vim)](https://missing-semester-cn.github.io/2020/editors/)[](https://missing-semester-cn.github.io/missing-notes-and-solutions/2020/solutions//editors-solution)
|
||||
- **1/16**: [数据整理](https://missing-semester-cn.github.io/2020/data-wrangling/)[](https://missing-semester-cn.github.io/missing-notes-and-solutions/2020/solutions//data-wrangling-solution)
|
||||
- **1/21**: [命令行环境](https://missing-semester-cn.github.io/2020/command-line/)[](https://missing-semester-cn.github.io/missing-notes-and-solutions/2020/solutions//command-line-solution)
|
||||
- **1/22**: [版本控制 (Git)](https://missing-semester-cn.github.io/2020/version-control/)[](https://missing-semester-cn.github.io/missing-notes-and-solutions/2020/solutions//version-control-solution)
|
||||
- **1/23**: [调试及性能分析](https://missing-semester-cn.github.io/2020/debugging-profiling/)[](https://missing-semester-cn.github.io/missing-notes-and-solutions/2020/solutions//debugging-profiling-solution)
|
||||
- **1/27**: [元编程](https://missing-semester-cn.github.io/2020/metaprogramming/)[](https://missing-semester-cn.github.io/missing-notes-and-solutions/2020/solutions//metaprogramming-solution)
|
||||
- **1/28**: [安全和密码学](https://missing-semester-cn.github.io/2020/security/)[](https://missing-semester-cn.github.io/missing-notes-and-solutions/2020/solutions//security-solution)
|
||||
- **1/29**: [大杂烩](https://missing-semester-cn.github.io/2020/potpourri/)
|
||||
- **1/30**: [提问&回答](https://missing-semester-cn.github.io/2020/qa/)
|
||||
|
||||
目录中的内容和这份`Wiki`中不少内容重合,当然我觉得作为一份校园学生为爱发电多人合作编辑的`Wiki`,内容有重复冗余再所难免。我比较推荐以这份教材作为计算机工具的学习,下面是我大一时学习课程的一些记录,这些课程都比较缺少一些中文的文章,能够直接看英文的一些材料当然很好,但是如果遇到一些困难,也许你可以在这里找到我先前所踩的坑。
|
||||
|
||||
> [The Missing Semester of Your CS Education](https://ek1ng.com/Missing%20Semester.html)
|
||||
173
2023旧版内容/3.编程思维体系构建/3.Y.1Linux概念普及.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# Linux 概念普及
|
||||
|
||||
## Before Start
|
||||
|
||||
在使用 Linux 之前,请确保自己会使用计算机。如果 Windows 都玩不转的,可以先补下基础知识。
|
||||
|
||||
- 什么是文件?什么是文件系统?
|
||||
- 操作系统是干什么的?
|
||||
- 软件是怎么运行起来的?
|
||||
|
||||
## What is Linux Distribution
|
||||
|
||||
Linux 不同于 Windows 和 macOS 这类系统,它没有官方维护的版本。Linux 的官方只负责维护核心,而给这个核心加上各种软件变成能用的操作系统的重任,就交给了下游的各个组织和公司。所谓发行版,就是把一系列的软件和核心放在一起,经过测试之后,做成一个完整的系统,分发给用户。
|
||||
|
||||
这个模式给 Linux 提供了多样性。用户可以选择适合自己的发行版。虽然在早期这使适配软件变得有些困难,使不同发行版割裂开来,给 Linux 的发展带来了阻碍。但是现在是 2023 年,随着包管理的规范化和新一代打包系统的兴起,这个状况得到了极大的改善。现在我们可以自由选取发行版,而不用过度担心软件适配的问题。
|
||||
|
||||
简而言之,GNU/Linux 加包管理加桌面约等于发行版,虽然不严谨,但是差不多贴近事实。
|
||||
|
||||
### Package Manager
|
||||
|
||||
一般来说,一个 Linux 的发行版最核心的部分是包管理模式。这通常通过包管理器和软件源实现。
|
||||
|
||||
包 (package) 类似 Windows 的安装包,它包含了要安装的软件以及一些额外的信息,但是同 Windows 安装包不同,它只是一个压缩包,不能自己运行安装,必须经过包管理器。用户很少需要直接下载安装它,而是通过包管理器。我们也不鼓励安装不在仓库里面的包,如果系统软件源里面没有可以去 flatpak。至于某些专有软件,我更推荐丢进 Windows 10 LTSC 虚拟机。
|
||||
|
||||
包管理器 (package manager) 负责安装和管理软件包。它读取软件包,按照软件包的要求去安装它的依赖 (通常是一些运行库,就像 Windows 下面的 VC++ 和 .NET),然后自动把包安装到规定的位置。
|
||||
|
||||
软件源 (repository) 是用来存放软件包的网站。包管理器就是从软件源服务器上下载包的。因为软件源通常在国外,软件包下载可能会比较慢。所以我们一般使用国内各个高校的镜像源,修改软件源的地址就是所谓换源。
|
||||
|
||||
有些人可能会纠结软件装到哪里去了,是装在什么盘上面了。这个不需要管。因为 Linux 的目录的定义和 Windows 有很大不同,这使得一些 Windows 上面的繁琐操作在 Linux 上面很多余。
|
||||
|
||||
这些常见的包管理器
|
||||
|
||||
- apt(dpkg): apt 是 Debian 系发行版的包管理器。基本上提供 Linux 安装包的软件都会支持。
|
||||
- pacman: Arch 系的包管理器。可以使用 AUR(Arch User Repository),这里也能找到很多软件。
|
||||
- rpm: Red Hat 系的包管理器。包比较少。只是由于中文互联网有很多死去的 CentOS 的教程遗留,所以提出来介绍一下。
|
||||
- dnf: Fedora 的包管理器。
|
||||
- flatpak: 通用包管理器,是个 Linux 桌面端都能安装使用。使用的 flathub 软件源上面有非常丰富的桌面软件支持(甚至有 QQ),建议发行版软件源里面没有的都可以先看看 flathub 有没有,如果有就 flatpak 安装,而不是拿着网站上面下过来的 deb 文件手动安装,这样很容易搞坏系依赖管理。
|
||||
- snap: 通用包管理器,只有 Ubuntu 强推。我建议使用 Ubuntu 的把这个卸载了换成 flatpak 避免影响使用体验。
|
||||
|
||||
不同的包管理器决定了不同的发行版。因此一般用包管理器做发行版的分类依据。
|
||||
|
||||
### Desktop Environment
|
||||
|
||||
桌面环境对用户而言当然也是很重要的,很大程度决定了用户体验。在 Linux 上面,桌面只是一些普通的程序,可以随便安装和更改。所以会有很多的桌面可供选择。不过这个选择一般来说发行版都帮你选好了。
|
||||
|
||||
#### Gnome
|
||||
|
||||
Gnome 曾一度成为 Linux 桌面的代名词,因为很多发行版默认会安装这个桌面。Gnome 的风格比较独特,有点类似 macOS,但是有不少自己的东西。对触控板手势的适配不错。
|
||||
|
||||
开箱状态连任务栏和桌面图标都没有,好在可以手动安装。一般各个发行版默认安装了 dash-to-dock(任务栏),desktop-icons(桌面图标) 这些插件。
|
||||
|
||||
设置比较少,不过可以通过 gnome-tweaks 补上。这样基本的设置都能覆盖。
|
||||
|
||||
#### KDE
|
||||
|
||||
KDE 也是非常热门的桌面。类似 Windows 的操作逻辑和极强的可定制性让他更适合 Linux“玩家”。
|
||||
|
||||
比较旧的版本里面可能会默认单击打开文件夹,可以在工作区 (workspace) 设置中改成双击。
|
||||
|
||||
KDE 的 GUI 功能做的还是不错的。而且提供了不少有用的套件。
|
||||
|
||||
Discover 应用商店可以直接使用发行版包管理器和 flatpak 进行安装,收录了很多软件,并且可以自动进行系统更新检查。
|
||||
|
||||
设置里面的选项很多,可以随便改。喜欢折腾的可以去看看 Theme,只是系统自带的主题安装器需要哈利波特才能使用。
|
||||
|
||||
自带的代理设置有些软件不会读,比如 Firefox, 给这些软件单独设置一下就行。
|
||||
|
||||
#### Cinnamon
|
||||
|
||||
Cinnamon 目前大众的发行版只有 Linux Mint 在支持,但是这也是个不错的桌面,适合小白。
|
||||
|
||||
#### Xfce || LXQT
|
||||
|
||||
如果你有一台老爷机,这两个桌面也许是个不错的选择。
|
||||
|
||||
#### Display Manager
|
||||
|
||||
在开机登陆用户的时候,那个让你输密码的界面并不是桌面,而是 DM(Display Manager),如果你有多个桌面,可以在这里切换。
|
||||
|
||||
我建议不要管这个,发行版用什么就跟着用。如果要自己安装都建议 SDDM。
|
||||
|
||||
#### Wayland and X11
|
||||
|
||||
显示服务器是比桌面更底层的东西,处在系统内核和桌面之间,用来管理窗口。这个一般碰不到,只要了解你使用的是 X11 还是 Wayland 就行。X11 是老的,Wayland 是新的。
|
||||
|
||||
现在 (2023 年) 的时间点非常尴尬,处于 X11 和 Wayland 换代的节点上面。一方面 X11 太老旧了 (十几年没有大更新了) 对有些新事物支持不好,比如 2k 屏幕 1.5 倍缩放的屏幕撕裂问题。另一方面 Wayland 支持虽然已经大致完善,但是有些死硬派没跟上,说的就是你,Nvidia!
|
||||
|
||||
好在大多数发行版并不需要纠结这些。非 N 卡的 Gnome 和 KDE 桌面基本都是 Wayland 了,其他的桌面环境或者使用 N 卡都会用 X11。
|
||||
|
||||
但是有些发行版可能忘了给 N 卡换 X11, **如果你桌面登不进去**,请检查自己的环境是否是 Wayland,如果是,**换成 X11**。
|
||||
|
||||
如果你 N 卡要强开 Wayland,请参照自己使用的发行版的 wiki 以及 Arch Linux wiki, 看看有什么需要注意的点。
|
||||
|
||||
## Distro
|
||||
|
||||
大多数发行版是基于某几个特定的发行版魔改的。所以会有“系”的说法。常见的有 Debian 系,Arch 系 Red Hat 系和 SUSE 系。其中 Red Hat 系主要面向企业,桌面版除了 Fedora 并不多见。
|
||||
|
||||
服务器发行版建议 Debian,用 Ubuntu 也是可以的。如果在中文互联网找资料可能会见到 CentOS,但是 CentOS 如今已经停止维护了,所以看到之后绕着走就行。
|
||||
|
||||
在[3.Y.2 双系统安装和发行版推荐](./3.Y.2双系统安装和发行版推荐.md) 推荐的发行版都是针对**双系统/单系统方案**的,因为在实机安装日常使用的时候,发行版对体验的影响才会体现出来,这样我写下的文字就会帮你剩下不少时间。
|
||||
|
||||
**对于虚拟机**,你不会在乎用户体验的,安装完新鲜感一过肯定就不打开了,偶尔遇到什么必须要用 Linux 的需求才会突然想起来有这个虚拟机。所以发行版**选择 Ubuntu 即可**,毕竟人气最高。要是对着百度上面刚刚找到的教程一顿猛敲之后,发现发行版不一样,那 Linux 就又风评被害了。
|
||||
|
||||
在安装系统的时候,建议安装时统一使用 English,装好之后再换成中文或者干脆不换。
|
||||
|
||||
对某些很新的硬件,比如 13 代酷睿和 40 系 N 卡 (2023 年),在其他发行版出现兼容性问题的情况下,可以使用 Debian 或者 Arch Linux 等等靠近上游的发行版,他们通常支持得比较好。
|
||||
|
||||
## Linux How to?
|
||||
|
||||
### Directory
|
||||
|
||||
在 Linux 中,文件目录结构与 Windows 完全不同。Windows 存在 C 盘、D 盘等盘符,而在 Linux 中不存在这些划分,最上层的目录是根目录,路径为 `/` ,并以一个树形结构从此向下一级一级区分。没有盘符,只有路径。虽然可以多分区,但是分区是挂载到某个路径的,而不是分配盘符。用 Windows 的思维去理解就是盘符没了,全部挂进文件夹里面了,从 / 开始是根分区,就像 C 盘,`/` 底下有 `usr` `home` `var` 等等文件夹,这些文件夹可以是普通文件夹,也可以让其他磁盘分区当这个文件夹。分区还可以挂载到 `/media/root/abcd` 。这样的好处很明显,就是在路径上面模糊了分区,分区的地位和普通文件夹差不多了,非常简单,对写程序很友好。
|
||||
|
||||
因为舍弃了盘符的概念,一般我们在 Linux 系统上仅仅使用一个挂载到 `/` 的分区 (简称 `/` 分区) 或者一个 `/` 分区和一个 `/home` 分区。这样分区可以得到充足的空间,所以不会出现 C 盘装满了或者 C 盘文件多导致开机慢的情况,也就没有必要支持自定义的安装目录。
|
||||
|
||||
对于 Linux 的树形文件结构,存在相对路径与绝对路径之分。绝对路径是代表从根路径 `/` 开始的完整路径,如 `/home/testuser/Download`。相对路径代表从当前目录,到目标目录的一个部分路径。比如当前你所在的目录为 `/home/testuser`,那么切换到绝对路径 `/home/testuser/Download` 的相对路径即为 `./Download`。其中 `./` 代表从当前目录,再向下寻找。另外,`..` 这种两个句点代表的是向上层寻找,比如你当前所在的路径为 `/home/testuser/Download`,向上寻找到 `/home/testuser/Desktop` 的相对路径即为 `../Desktop`。
|
||||
|
||||
当前用户的 home 文件夹简称为 `~/`,假设我们的用户名是 `user`:
|
||||
|
||||
```bash
|
||||
user@computer:~$ cd ~/
|
||||
user@computer:~$ pwd
|
||||
/home/user
|
||||
```
|
||||
|
||||
### User
|
||||
|
||||
Linux 在设计之初就是一个多用户操作系统,不像潜伏在多用户操作系统里面的纯正单用户操作系统 Windows。
|
||||
|
||||

|
||||
|
||||
因此,Linux 对于用户和权限的管理比较严格,可能经常要你输 root 密码。
|
||||
|
||||
简单来说,Linux 中存在两类用户。第一类用户即为 root 用户,也称为超级用户,它拥有系统中最高的权限。第二类用户就是除了 root 用户的普通用户,他们可以拥有不同等级的权限。使用 root 权限时需要十分小心。
|
||||
|
||||
一般情况下,我们使用的都是普通用户。但是要进行一些涉及较高权限的操作,比如安装软件和修改系统设置的的时候,我们就会使用 sudo 软件临时切换到 root 用户进行操作。
|
||||
|
||||
```bash
|
||||
# 一些例子。
|
||||
# 当你尝试安装 vim ,却忘记了 sudo
|
||||
$ apt install vim
|
||||
E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)
|
||||
E: Unable to acquire the dpkg frontend lock (/var/lib/dpkg/lock-frontend), are you root?
|
||||
# 这样就可以安装了
|
||||
$ sudo apt install vim
|
||||
```
|
||||
|
||||
切记,root 权限的使用要小心,不要随便粘贴指令!一条命令就可以干掉整个系统,比如 `sudo rm -rf /*`!
|
||||
|
||||
### 本土化
|
||||
|
||||
想要正常使用系统,哈利波特是必须的。建议跟着[这篇教程](https://arch.icekylin.online/guide/rookie/transparent.html)走,但是 Debian 系没有这个软件,自己去 Github 上面找这个软件。
|
||||
|
||||
发行版会有一个全局的代理设置,但是有些软件就是不肯自己读取,点名 Firefox。不过好办,分别单独设置就行。
|
||||
|
||||
由于海外的服务器下载慢,我们会把软件源换成国内各个高校的镜像。这点参考镜像站给出的教程。
|
||||
|
||||
输入法一律推荐 fcitx5 搭配 fcitx5-chinese-addons,并打开云拼音。不推荐 rime,太老了。强烈不推荐搜狗,搜狗使用的是 fcitx4 框架,太老了。一般来说 Debian 系的发行版都有一个叫做 im-config 的软件包负责管理输入法,快捷方式名字通常是叫做 Input Method,在这里切换到 fcitx5 就行。Arch Linux 系则需要自己设置环境变量。
|
||||
|
||||
2024 年更新:rime-ice 也是个不错的输入法,并且支持 ibus, Ubuntu 用户也许可以选择,这样不需要动默认输入法设置,虽然我更推荐再装一个 fcitx5。
|
||||
|
||||
### 如何寻求帮助
|
||||
|
||||
首先阅读[提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md),这对提升个人素养很有帮助。也有助于和能够提供帮助的人有效交流。
|
||||
|
||||
一般来说,各个发行版都有自己的 Wiki,里面介绍了发行版本身的特点,常用的软件和各种问题的解法。但是有些发行版的 wiki 年久失修,可能会过时或者缺失内容导致无法解决问题。这时候可以其他发行版的 wiki,只要了解发行版之间的差异,自己适度发挥,也能解决问题。推荐的有 [Arch Linux wiki](https://wiki.archlinux.org) (神中神,非常推荐) 和 [Debian wiki](https://wiki.debian.org) (Debian 系可以看),他们的社区比较活跃,维护比较积极。
|
||||
|
||||
社区是 Linux 当中重要的组成部分。发行版通常有自己的论坛,邮件组和 IRC 频道。如果你确信你面对的是一个全新的问题,网上找不到已有的解决方案。或者你的能力不足以找到解决方案。可以尝试在这些地方求助。保持良好的态度,尽可能详细地描述问题,相信会有志愿者来解答的。
|
||||
|
||||
### 推荐阅读
|
||||
|
||||
- [archlinux 简明指南](https://arch.icekylin.online/): 虽然是 Arch Linux 的教程,但是写的很好,其他发行版也能参考。
|
||||
- [Linux 就该这样学](https://www.linuxprobe.com/docs/LinuxProbe.pdf): 不错的书,适合长期看。
|
||||
613
2023旧版内容/3.编程思维体系构建/3.Y.2双系统安装和发行版推荐.md
Normal file
@@ -0,0 +1,613 @@
|
||||
# Distro
|
||||
|
||||
这篇文章是关于双系统的发行版推荐,如果只是想尝试 Linux, 暂时没有长期使用的打算,可以直接跳过。
|
||||
|
||||
## 前言
|
||||
|
||||
我接触桌面版 Linux 有些年头了。
|
||||
Linux 省了我不少时间,也花了我不少时间。也算用出点心得了,知道 Linux 有多必要,也知道有多坑。
|
||||
曾经我试图寻找一个“最好的”版本向新人推荐,事实证明这样的想法是走了弯路。
|
||||
现在我敢说没有一个发行版能完全做到“新手友好”,都或多或少存在一点坑。与其寻找一个“终极”发行版,不如列一个发行版选择清单,可以根据自己的需求选择。给常见坑摘要,提供修复方法,或者给出某个发行版出现致命问题时给出替代选择。争取让入门 Linux 难度降下来。
|
||||
|
||||
## Notice
|
||||
|
||||
::: warning
|
||||
再次声明:这篇文章是关于双系统的发行版推荐,如果只是想尝试 Linux, 暂时没有长期使用的打算,可以直接跳过。
|
||||
请仔细阅读本文,并按照步骤操作,否则可能会**导致系统无法使用甚至丢失数据**。建议提前**备份数据**,避免手误。
|
||||
:::
|
||||
|
||||
双系统安装时,**关闭快速启动,关闭安全启动,关闭 Bitlocker**!
|
||||
|
||||
已知快速启动开启时,Windows 会锁定无线网卡和硬盘等硬件,导致 Linux 无法使用。关闭快速启动可以在主板或者 Windows 上面设置。建议在 Windows 上面设置。[如何关闭快速启动(知乎)](https://zhuanlan.zhihu.com/p/589927741)
|
||||
|
||||
大部分 Linux 发行版不支持安全启动,因为要给微软交钱。事实上这个功能对安全性没有太大作用,请在主板的 BOIS 设置内关闭。如果听说过引导区病毒并且很确信自己需要防御的,可以自行解决签名问题,这里限于篇幅不会给出。
|
||||
|
||||
Bitlocker 可能会自杀最好关掉。如果你很确信自己有加密需求,建议提前备份所有重要数据,并准备 [WePE](https://www.wepe.com.cn/download.html) 和 [Windows 安装镜像](https://www.microsoft.com/en-us/software-download/windows10),随时准备修复。
|
||||
|
||||
我们始终建议使用包管理器。目前已知的除了 VSCode 需要手动安装,其他的都尽量使用系统自带和 Flatpak。
|
||||
|
||||
## USB-live
|
||||
|
||||
在开始之前,请准备一个空的 U 盘。
|
||||
|
||||
建议使用 [Ventoy](https://www.ventoy.net/cn/index.html) 这款工具制作可启动 U 盘,这样可以直接把 ISO 文件拖进 U 盘,在先后安装不同发行版的时候不需要反复烧录 U 盘。
|
||||
|
||||
但是这个方案无法使用某些 iso,比如 Debian 的 DVD 镜像就会因为找不到挂载点而失败。这时候你可能需要使用 [rufus](https://rufus.ie/zh/) 对 U 盘进行烧录。
|
||||
|
||||
## Beginner
|
||||
|
||||
列出常见的开箱即用发行版,不一定推荐,但是总觉得得提两句,方便选择。
|
||||
|
||||
### Debian based
|
||||
|
||||
由于大部分 Debian 系发行版使用同样的安装程序,只有一些细微的差别,所以我只给出 Linux Mint 的详细安装教程,其他发行版提出一些要点即可举一反三。
|
||||
|
||||
这类发行版使用的都是 apt 包管理器,大多数软件都是互通的。
|
||||
|
||||
#### Linux Mint
|
||||
|
||||
就我个人认为,对新手来说 Linux Mint 是个不错的入门发行版。它基于 Ubuntu,软件生态好。Cinnamon 桌面可能不是很惊艳,但是简洁直观。
|
||||
|
||||
Mint 对很多需要打命令的操作都做了 GUI,这样新手更容易使用。这是我见过为数不多的把开箱即用和可定制结合的比较好的主流发行版。
|
||||
|
||||
首先去 Mint 官网 <https://linuxmint.com/download.php> 下载 Cinnamon 版本。如果你使用新硬件可以下载 Cinnamon Edge。
|
||||
|
||||
点击 Download 之后会跳转到下载页面,你可以下拉列表选择 China 源进行下载,速度会很快。
|
||||
|
||||
如果有 Windows 存在的情况下,Mint 可以自动选择一个最大的分区,让你划出一定大小的空间用来安装。如果你分了很多盘,你可以在 Windows 下提前使用 Diskgenius 之类的分区软件整理你的分区,把剩余空间集中到一个盘上面,好让 Mint 自动缩小你想要缩小的盘,而不是手动指定。操作页面就像这样。
|
||||
|
||||

|
||||
|
||||
##### 安装
|
||||
|
||||
用手机打开这个教程,插上你的 U 盘,关机。我们就可以开始安装了。
|
||||
|
||||
首先要启动到 U 盘,这个请自行查阅你们的电脑的启动方法。如果成功,你就可以看到这个页面。按下回车,耐心等待,就可以进入安装页面了。在这个页面按下回车。
|
||||
|
||||

|
||||
|
||||
不久就进入这个页面。打开左上角的安装程序可以进行安装。如果使用 Ventoy 让你 umount 什么点确定就可以了。
|
||||
|
||||

|
||||
|
||||
建议使用英文进行安装,以后自己手动设置中文,这样配置不容易出错。所以这里点击 Continue 就行
|
||||
|
||||

|
||||
|
||||
continue
|
||||
|
||||

|
||||
|
||||
勾选,然后 continue. 这样 mp4 一类的格式就可以默认正常打开。
|
||||
|
||||

|
||||
|
||||
由于我这里是虚拟机,只有清空磁盘的选项。你们如果已经有 Windows 的机子可以选择 install alongside Windows。
|
||||
|
||||

|
||||
|
||||
在地图上找到中国,点击。
|
||||
|
||||

|
||||
|
||||
输入你的用户名和密码。密码建议别太简单,以后如果要跑公网服务给打烂了就不好了。这里的密码就是反面教材,字典两分钟打爆的那种。
|
||||
|
||||

|
||||
|
||||
等待安装。
|
||||
|
||||

|
||||
|
||||
你可以查看安装程序的输出。
|
||||
|
||||

|
||||
|
||||
准备重启。
|
||||
|
||||

|
||||
|
||||
##### 配置
|
||||
|
||||
第一次启动。可以看到画风还是很贴近 Windows 的。
|
||||
|
||||

|
||||
|
||||
默认使用的软件源在国外,下载很慢,我们要换成国内源。
|
||||
|
||||
Linux Mint 的很多操作都是有对应的软件的的,非常方便。通过按 Win 键左下角的启动菜单,直接搜索名字就能打开相应的软件。很多人从 Windows 那里带来了放一桌面的快捷方式的习惯。Linux 的软件一般不放桌面快捷方式,而是通过菜单打开,这样更清爽高效。
|
||||
|
||||
在左下角搜索 software sources, 进入换源页面。其他软件也可以用这样的方式打开。
|
||||
|
||||

|
||||
|
||||
输入密码。
|
||||
|
||||

|
||||
|
||||
分别将 Main 和 Base 的源都换成国内源。我这里换成中科大的源,你们也可以换其他的。
|
||||
|
||||

|
||||
|
||||
换源的页面长这样,可以点击选择,然后 右下角 Apply 即可
|
||||
|
||||

|
||||
|
||||
这里选 Main 的源。
|
||||
|
||||

|
||||
|
||||
Base 的源。
|
||||
|
||||

|
||||
|
||||
可以看到源换好了,按下 OK 就可以保存更改。
|
||||
|
||||

|
||||
|
||||
接下来就是安装输入法。打开 Synaptic 包管理器,这是一个 apt 的 GUI 页面,比较适合新手。点开 Search, 输入 fcitx5,搜索。
|
||||
|
||||

|
||||
|
||||
勾选 fcitx5,在弹出的窗口点击确定。然后勾选 fcitx5-chinese-addons。因为 fcitx5 只是一个框架,输入法在 fcitx5-chinese-addons 里面,所以两个都要安装。
|
||||
|
||||
点击左上方的 Apply 就可以安装了。
|
||||
|
||||

|
||||
|
||||
安装完成之后,打开 input method,将输入法从 none 切换到 fcitx5, 保存。
|
||||
|
||||

|
||||
|
||||
现在已经切换成功了。
|
||||
|
||||

|
||||
|
||||
打开 Fcitx5 Configuration,在右侧的可用输入法页面中搜索 Pinyin,选中,点击两个分页面中间的左箭头即可添加 pinyin 到 fcitx5. 然后点击下方 Apply 即可。
|
||||
|
||||

|
||||
|
||||
可以点击 Global Options 的标签页来修改快捷键。
|
||||
|
||||

|
||||
|
||||
下一步,在 Language Settings 里面修改当前的语言为中文。
|
||||
|
||||

|
||||
|
||||
如果你是 Nvidia 显卡,现在可以打开 Driver Manager 安装 Nvidia 的驱动。我因为是虚拟机截不到图。建议 40 系显卡使用 525 版本的驱动,而不是他的推荐的 535,亲测崩溃。
|
||||
|
||||

|
||||
|
||||
这些都完成了就可以重启了。重启之后会问你要不要更新路径,选择保留就的名称 (Keep Old Names).这很重要,如果你不想在 bash 中 cd 来 cd 去的时候切换输入法的话。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
剩下就是一些安装后常用的操作了。Mint 默认自带 Flatpak,所以也不需要手动安装了。
|
||||
|
||||
#### Ubuntu
|
||||
|
||||
Ubuntu 可能是最热门的发行版,某些情况下提到 Linux 就是 Ubuntu。但是 Ubuntu 有时候会作出对开发者而言比较迷惑的操作。
|
||||
|
||||
wiki 已有安装教程,我就不自己写了。
|
||||
|
||||
那么如何配置?
|
||||
|
||||
##### 卸载 snap
|
||||
|
||||
首先卸载 Snap。注意这样会直接干掉 Firefox,所以确保你已经阅读下面所有内容再开始操作。可以先复制到文本编辑器当中。
|
||||
|
||||
打开终端,输入
|
||||
|
||||
```bash
|
||||
sudo systemctl disable snapd.service
|
||||
sudo systemctl disable snapd.socket
|
||||
sudo systemctl disable snapd.seeded.service
|
||||
sudo snap remove firefox
|
||||
sudo snap remove snap-store
|
||||
sudo snap remove gtk-common-themes
|
||||
sudo snap remove gnome-3-38-2004
|
||||
sudo snap remove core18
|
||||
sudo snap remove snapd-desktop-integration
|
||||
sudo rm -rf /var/cache/snapd/
|
||||
sudo apt autoremove --purge snapd
|
||||
rm -rf ~/snap
|
||||
```
|
||||
|
||||
接着禁用 firefox 的 snap。
|
||||
|
||||
打开配置文件:
|
||||
|
||||
```bash
|
||||
sudo nano /etc/apt/preferences.d/firefox-no-snap
|
||||
```
|
||||
|
||||
在文件中粘贴以下内容,保存:
|
||||
|
||||
```text
|
||||
Package: firefox*
|
||||
Pin: release o=Ubuntu*
|
||||
Pin-Priority: -1
|
||||
```
|
||||
|
||||
把 Firefox 请回来
|
||||
|
||||
```bash
|
||||
sudo add-apt-repository ppa:mozillateam/ppa
|
||||
sudo apt update
|
||||
sudo apt install firefox
|
||||
```
|
||||
|
||||
如果安装过慢,可以 `Ctrl+C` 暂时杀掉。打开 Software Update(软件与更新),修改 ppa,从 `http://ppa.launchpad.net` 换成 `https://launchpad.proxy.ustclug.org`。最后重新执行安装命令。
|
||||
|
||||

|
||||
|
||||
##### 安装输入法
|
||||
|
||||
参考[Ubuntu22.04 安装 Fcitx5 中文输入法(知乎)](https://zhuanlan.zhihu.com/p/508797663)。基本都可以照做,但是不要跟着他往 `~/.bash_profile` 和 `/etc/profile` 里面丢垃圾。环境变量要写到 `~/.pam_environment` 里面,内容如下,不要和他一样带 `export`:
|
||||
|
||||
```text
|
||||
XMODIFIERS=@im=fcitx
|
||||
GTK_IM_MODULE=fcitx
|
||||
QT_IM_MODULE=fcitx
|
||||
```
|
||||
|
||||
提示一下,`~/` 是当前用户文件夹的简称,假如用户名是 `user`,对应的路径就是 `/home/user/`。
|
||||
|
||||
#### Pop! OS
|
||||
|
||||
这个不是很热门,感觉也一般,只是带了 N 卡的开箱支持,所以 N 卡用户不行可以试试看。
|
||||
|
||||
Pop! OS 的安装程序会直接无视 Windows,建议先在 Windows 下面用 Diskgenius 之类的软件划出一个 1G 的 FAT32 分区和一个 200G 以上的 EXT4 分区,然后在安装选项里面选择高级选项,把 FAT32 分区作为 EFI,EXT4 分区作为 `/`。
|
||||
|
||||
#### Zorin OS
|
||||
|
||||
不得不吐槽这帮人把时间都用到魔改 Gnome 外观上面了。到现在还基于 Ubuntu focal, 都 2023 年了。连装个 fcitx5 都费劲。
|
||||
|
||||
自带 Wine 支持,听说不错,我反正“网络不好”没安装上。
|
||||
|
||||
#### MX Linux
|
||||
|
||||
distrowatch 上面排名挺高,但是结合发行版实际情况感觉很有刷榜嫌疑。没有很肯定的理由选择的话,还是用 Mint 吧。
|
||||
|
||||
### Arch based
|
||||
|
||||
因为 Arch Linux 太强势,这里系列主要用的比较多的就是 Manjaro,所以先只写 Manjaro。后面可能会继续添加。
|
||||
|
||||
#### Manjaro
|
||||
|
||||
基于 Arch Linux 的开箱即用的发行版,有 Arch Linux 的部分优点,而且对新手更加易用。
|
||||
|
||||
有过忘记更新证书的黑历史,不过这两年消停会了。
|
||||
|
||||
安装过程如图。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
这里要留意一下
|
||||
|
||||

|
||||
|
||||
如果有 Windows 这么选
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
安装完成,重启。
|
||||
|
||||
输入法是没有安装的,自己安装。
|
||||
|
||||
```zsh
|
||||
sudo pacman -S fcitx5 fcitx5-configtool fcitx5-qt fcitx5-gtk fcitx5-chinese-addons kcm-fcitx5 fcitx5-lua
|
||||
kate ~/.pam_environment
|
||||
```
|
||||
|
||||
kate 会打开 `~/.pam_environment`,接着把下面的环境变量写进去:
|
||||
|
||||
```text
|
||||
GTK_IM_MODULE=fcitx
|
||||
QT_IM_MODULE=fcitx
|
||||
XMODIFIERS=@im=fcitx
|
||||
```
|
||||
|
||||
### Others
|
||||
|
||||
#### Deepin
|
||||
|
||||
目前还是 Debian 系,鉴于他们官宣要脱离 Debian,那我就放 Other 里面了。
|
||||
|
||||
Deepin 的本土化做的很不错,支持一些国内常用的软件。
|
||||
|
||||
但是总是让我感觉不够 Linux,手感比较奇怪。定位类似产品而不是工具。如果想要在国内替代 Windows 可以试试看。我之前使用的时候太不稳定,小 bug 一堆。现在不知道好点没有。我建议写程序还是少用,设计哲学不一样,容易把自己带偏。
|
||||
|
||||
#### UOS
|
||||
|
||||
反正不是给我们用的。毕竟 root 权限还要注册他们的账号登陆,没绷住。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
顺便说下我至今没找到 UOS 的源码,只看到[一篇干巴巴的新闻](https://www.zgswcn.com/article/202212/202212211344581036.html)声称“开源了开源了真的在开源了”。也就欺负 Linux Foundation 不打跨国官司。本来不想挂人的,但是[如果 UOS 用户就这素质](https://bbs.chinauos.com/post/7543)我真忍不了:
|
||||
|
||||

|
||||
|
||||
## Advanced(Debian, Arch Linux, etc.)
|
||||
|
||||
面向桌面用户的进阶发行版。Red Hat 根本看不上个人用户,所以我就不自找麻烦了。
|
||||
|
||||
### Debian
|
||||
|
||||
Debian 的招牌就是稳定。在服务端这个优点非常明显。虽然在桌面端有些软件拖后腿导致它没那么稳,但是比起其他发行版还是更加稳定的。Debian 主要面向专业人士,桌面端不够开箱即用,需要很多额外的配置。祖传的安装界面对新手也不太友好。好在有 live 版本可以使用。
|
||||
|
||||
Debian 的兼容性非常优秀,在其他发行版挂掉的情况下面都能稳定跑。如果遇到兼容性问题那就直接上 Debian 吧,再怎么样也比 Arch Linux 容易安装一点。而且安装配置结束基本就不会再挂了。最近的 11 和 12 两个大版本一改老旧的形象,积极拥抱新事物,值得尝试。
|
||||
|
||||
不建议使用 Ventoy 启动 Debian 的镜像,因为 Ventoy 和 Debian 都很喜欢 hack,两者加起来容易爆炸。老老实实用 Rufus 烧录空 U 盘吧。
|
||||
|
||||
官网下的 ISO 文件是真的多。我这里推荐下载 [Live 镜像](https://mirrors.ustc.edu.cn/debian-cd/current-live/amd64/iso-hybrid/),因为安装相对来说比较方便直观。。下载页面很传统,而且可选的很多,但是不要紧,kde 和 gnome 二选一即可,老爷机就 lxqt。
|
||||
|
||||
如果你的 Live 镜像出了什么锅炸掉了,可以试试 [DVD 镜像](https://mirrors.ustc.edu.cn/debian-cd/current/amd64/iso-dvd/),祖传的安装页面很不友好,但有时候是唯一的选择。
|
||||
|
||||
#### Live 安装
|
||||
|
||||
这里以 Live KDE 为例子,Gnome 也是一样的。
|
||||
|
||||
首先是祖传的选择系统。直接回车。
|
||||
|
||||

|
||||
|
||||
进来之后是不是很懵?哪里有 Install 呢?多半因为打包的志愿者忘了放快捷方式,自己左下角菜单点出来就好了。
|
||||
|
||||

|
||||
|
||||
这样点出来。
|
||||
|
||||

|
||||
|
||||
还要输入密码,密码也没告诉你。我去网上搜了一下,这个密码是 `live`。
|
||||
|
||||

|
||||
|
||||
这样就打开了安装页面了。一路下一步吧。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
到这里,我们需要选择安装方式。我这里是虚拟机,只能看到 Erase disk 这个选项。如果有是 Windows 会出现别的选项。
|
||||
|
||||

|
||||
|
||||
在已经有 Windows 安装好的情况下面是这样的。
|
||||
|
||||

|
||||
|
||||
用户名和密码
|
||||
|
||||

|
||||
|
||||
双系统的用户在这一页面检查一下,别把 Windows 干掉了。
|
||||
|
||||

|
||||
|
||||
重启吧。
|
||||
|
||||

|
||||
|
||||
#### 传统安装
|
||||
|
||||
有时候只能传统安装。思路和 Live 是差不多的,只是程序有点丑,然后有些不是很直观。看仔细点就行。一般很少用到。
|
||||
|
||||
#### 配置系统
|
||||
|
||||
##### 通用部分
|
||||
|
||||
打开 Konsole(KDE) 或者 Terminal(Gnome) 准备打命令。Gnome 用户按下 Win 键即可呼出搜索。
|
||||
|
||||
先[换源](https://mirrors.ustc.edu.cn/help/debian.html),再[安装输入法](https://wiki.debian.org/I18n/Fcitx5)和 [flatpak](https://flathub.org/setup/Debian),顺便换个 [flathub 源](https://mirror.sjtu.edu.cn/docs/flathub)。下面把这些教程整合起来。
|
||||
|
||||
依次输入这些命令,看清楚要求。
|
||||
|
||||
```bash
|
||||
# 换源
|
||||
sudo sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
|
||||
sudo sed -i 's/security.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
|
||||
sudo apt update && sudo apt upgrade
|
||||
```
|
||||
|
||||
如果出现类似 `E: 仓库 "http://mirrors.ustc.edu.cn/debian/ bookworm-security Release" 没有 Release 文件` 的报错,请用 `sudo nano /etc/apt/sources.list` 打开,手动将 `http://mirrors.ustc.edu.cn/debian/ bookworm-security` 改成 `http://mirrors.ustc.edu.cn/debian-security/ bookworm-security`,并再次 `sudo apt update`。
|
||||
|
||||
```bash
|
||||
# 时间同步
|
||||
sudo apt install systemd-timesyncd
|
||||
# 安装输入法
|
||||
sudo apt install --install-recommends fcitx5 fcitx5-chinese-addons
|
||||
```
|
||||
|
||||
```bash
|
||||
# flatpak
|
||||
sudo apt install flatpak
|
||||
# 下面两个命令二选一即可
|
||||
sudo apt install plasma-discover-backend-flatpak # 对于 KDE 桌面
|
||||
sudo apt install gnome-software-plugin-flatpak # 对 Gnome 桌面
|
||||
# 添加仓库
|
||||
flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
# 换源
|
||||
flatpak remote-modify flathub --url=https://mirror.sjtu.edu.cn/flathub
|
||||
```
|
||||
|
||||
对于 Nvidia 显卡,还要[安装驱动](https://wiki.debian.org/NvidiaGraphicsDrivers#Debian_12_.22Bookworm.22)。
|
||||
|
||||
我们需要把 `non-free contrib` 这两个仓库加上。可以在文件管理器中打开 `/etc/apt/` 这个目录,双击 `sources.list` 文件。(Gnome 的文件管理器按 Win+L 可以输入路径)
|
||||
|
||||
Gnome 会弹出一个窗口,全勾起来保存就行。
|
||||
|
||||

|
||||
|
||||
KDE 会用 Kate 打开它,每行都加上 `non-free contrib`,保存,Over.
|
||||
|
||||

|
||||
|
||||
接下来打开命令行,执行:
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install nvidia-driver firmware-misc-nonfree
|
||||
```
|
||||
|
||||
结束之后,重启。KDE 重启之后记得在登录页面把左下角桌面设置换成 X11。
|
||||
|
||||
##### KDE 部分
|
||||
|
||||
安装完大概是这样的。
|
||||
|
||||

|
||||
|
||||
打开 input method,修改输入法。注意不要开错了。
|
||||
|
||||

|
||||
|
||||
OK
|
||||
|
||||

|
||||
|
||||
YES
|
||||
|
||||

|
||||
|
||||
选中 fcitx5
|
||||
|
||||

|
||||
|
||||
OK
|
||||
|
||||

|
||||
|
||||
打开 Fcitx 5 的设置。
|
||||
|
||||

|
||||
|
||||
现在还没自动运行。平时也可以在这里启动设置。
|
||||
|
||||

|
||||
|
||||
点击右下角 Add Input Method
|
||||
|
||||

|
||||
|
||||
搜索 Pinyin,选中,Add。
|
||||
|
||||

|
||||
|
||||
默认的字体非常小,建议更改。点击 Configure addons,因为 UI 属于 Addon。至于快捷键可以在另一个设置里面改。
|
||||
|
||||

|
||||
|
||||
设置 Classic User Interface
|
||||
|
||||

|
||||
|
||||
这里可以更改字体。
|
||||
|
||||

|
||||
|
||||
然后就能使用了。
|
||||
|
||||

|
||||
|
||||
可以更改 Language 为中文了。
|
||||
|
||||

|
||||
|
||||
Flathub 上面的软件可以通过 KDE 自带的 Discover 应用中心安装。
|
||||
|
||||
这里我们看到 flatpak 已经启用了。
|
||||
|
||||

|
||||
|
||||
##### Gnome
|
||||
|
||||
Gnome 开箱状态就是残废,本来应该是官方做的事情,结果 Gnome 摆 Debian 也摆,都丢给用户了。
|
||||
|
||||
Gnome 一上来就让你改语言,改中文就行,然后一路下一步。下次重启可能会问你是否更改文件名称,选择否。
|
||||
|
||||

|
||||
|
||||
然后按下 Win 键进入菜单,呼出 Terminal, 安装一些插件。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
```bash
|
||||
sudo apt install gnome-shell-extension-dashtodock gnome-shell-extension-desktop-icons-ng gnome-shell-extension-kimpanel
|
||||
```
|
||||
|
||||
我们之前已经装好 fcitx5, 所以这里启用就行。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
一切完成,重启。
|
||||
|
||||

|
||||
|
||||
打开 Extension,启用任务栏,桌面图标和输入法面板的拓展。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
打开 tweaks, 启用最大化和最小化按钮。当然 2k 屏幕也可以改改缩放。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
差不多能用了,剩下可以自己折腾。
|
||||
|
||||
#### Trouble shooting
|
||||
|
||||
##### 没有 sudo 权限
|
||||
|
||||
多半是打包的忘了加上去,自己加一下就好了。
|
||||
|
||||
```bash
|
||||
su root
|
||||
sudo usermod -a -G sudo <你的用户名>
|
||||
```
|
||||
|
||||
### Arch Linux
|
||||
|
||||
邪教教主。好用是真的好用,邪教也是真的邪教。建议有事没事看那边的 wiki,写的是真的很好。
|
||||
|
||||
安装教程我就不再班门弄斧了,可以自己看。
|
||||
|
||||
### Fedora
|
||||
|
||||
以后写吧,不会比 Debian 难的。
|
||||
|
||||
### OpenSUSE
|
||||
|
||||
为数不多官方 KDE 的发行版,可能是因为他们总部都在德国。感觉 SUSE 中规中矩,这么多年都没搞出什么大新闻。
|
||||
119
2023旧版内容/3.编程思维体系构建/3.Y.3VMware的安装与安装Ubuntu22.04系统.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# VMware 的安装与安装 Ubuntu22.04 系统
|
||||
|
||||
::: warning
|
||||
一般与 wsl 安装二选一,因为都是虚拟系统,安装了 wsl 不用 VMware
|
||||
|
||||
文章撰写于 2022 年,可能其中的一些内容已过时。
|
||||
:::
|
||||
|
||||
首先下载 VMware
|
||||
|
||||
如果是 pro16 版本(key **ZF3R0-FHED2-M80TY-8QYGC-NPKYF**)
|
||||
|
||||
如果是 pro17 版本(key **JU090-6039P-08409-8J0QH-2YR7F**)
|
||||
|
||||
本文写的时候用的版本是 pro16,但目前已经更新到 pro17 所以来更新个 key(如下安装与 16 版本无异)
|
||||
|
||||
[https://www.vmware.com/products/workstation-pro/workstation-pro-evaluation.html](https://www.vmware.com/products/workstation-pro/workstation-pro-evaluation.html)
|
||||
|
||||
一路下一步
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
这俩我推荐勾掉
|
||||
|
||||

|
||||
|
||||
安装过后点许可证 输上面的 key 激活
|
||||
|
||||
[https://mirror.nju.edu.cn/ubuntu-releases/22.04](https://mirror.nju.edu.cn/ubuntu-releases/22.04)
|
||||
|
||||
去这里下载 Ubuntu22.04 镜像包 iso 选择 `ubuntu-<version>-desktop-amd64.iso`
|
||||
|
||||
:::tip
|
||||
这里推荐使用多线程下载器下载,比如 [IDM](../2023旧版内容/2.高效学习/2.2优雅的使用工具.md),如果直接用浏览器下载(线程少)可能会出现下载慢、下载失败的情况。
|
||||
:::
|
||||
|
||||
下好回到 VMware
|
||||
|
||||

|
||||
|
||||
创建新的虚拟机 - 典型(推荐)- 下一步 - 安装程序 iso 选中你刚下的 iso 下一步
|
||||
|
||||

|
||||
|
||||
这里填你一会儿要登录 linux 的个人信息
|
||||
|
||||

|
||||
|
||||
这里建议把位置改到其他盘
|
||||
|
||||
一路下一步直到完成
|
||||
|
||||
启动后进入 Ubuntu 安装
|
||||
|
||||

|
||||
|
||||
键盘映射 直接 continue
|
||||
|
||||
接下来一路 continue install now
|
||||
|
||||

|
||||
|
||||
最后 restart
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
这个 skip
|
||||
|
||||
后面一路 next 最后 done
|
||||
|
||||
点右上角 settings
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
然后按指引 restart 系统
|
||||
|
||||

|
||||
|
||||
会提示你要不要重新命名这些用户下的文件夹
|
||||
|
||||
我建议选 `keep old names`
|
||||
|
||||
如果你的语言还没有变过来的话
|
||||
|
||||

|
||||
|
||||
点击这个他会安装语言
|
||||
|
||||

|
||||
|
||||
把汉语拖到英文之上 点应用到整个系统
|
||||
|
||||

|
||||
|
||||
右上角 logout 重新登陆 就是中文辣
|
||||
|
||||
最后在设置 - 电源把息屏改成从不
|
||||
|
||||

|
||||
|
||||
**至此 恭喜安装完成!**
|
||||
|
||||
之后就可以在桌面上右键
|
||||
|
||||

|
||||
|
||||
打开命令行
|
||||
|
||||
**开始你的 Linux 学习吧**
|
||||
25
2023旧版内容/3.编程思维体系构建/3.Y.4WSL的安装.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# WSL 的安装
|
||||
|
||||
::: warning 💡与 VMware 安装二选一 安装了 VMware 不用 wsl
|
||||
:::
|
||||
|
||||
先说**坏处**:
|
||||
|
||||
1. 开启 hyperv 的后果是 如果你电脑装模拟器玩手游的话 装了 hyperv 你的模拟器是打不开的(目前只有 `蓝叠国际版HyperV版`(性能很差)支持共存 hyperv 和模拟器)
|
||||
2. WSL 很难装辣 安装过程中会出很多 bug 需要你自行 STFW
|
||||
|
||||
## **官方文档**
|
||||
|
||||
## [史上最全的 WSL 安装教程](https://blog.csdn.net/wojiuguowei/article/details/122100090)
|
||||
|
||||
笔者不清楚当前版本 wsl 安装步骤 但是笔者安装的时候是需要在 `windows 功能` 中开启这三项
|
||||
|
||||
(现在可能是只开 `适用于Linux的windows子系统`)
|
||||
|
||||

|
||||
|
||||
如果你的 windows 版本为**家庭版** 那么 hyperv 选项是没有的
|
||||
|
||||
你需要右键以管理员权限打开以下脚本来强行开启 hyperv
|
||||
|
||||

|
||||
158
2023旧版内容/3.编程思维体系构建/3.Y.5Linux初探索.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# Linux 初探索
|
||||
|
||||
如果你是第一次接触 linux,请一边仔细阅读,一边尝试敲命令在终端内。
|
||||
|
||||
有一点非常重要,这章节的内容到后面会略为困难,并且 linux 知识繁杂多样。
|
||||
|
||||
希望你可以参考这个链接!
|
||||
[一些基本常识](https://linux.cn/article-6160-1.html)
|
||||
|
||||
当然,你也可以从蓝桥云课开始,不过学会 linux 的最好办法是删掉你的 windows 换一个 linux 系统当开发环境,比任何临时的训练都有效!
|
||||
|
||||
[蓝桥云课-Linux 基础入门](https://www.lanqiao.cn/courses/1)
|
||||
|
||||
## 探索命令行
|
||||
|
||||
Linux 命令行中的命令使用格式都是相同的:
|
||||
|
||||
```bash
|
||||
命令名称 参数1 参数2 参数3 ...
|
||||
```
|
||||
|
||||
参数之间用任意数量的空白字符分开。关于命令行,可以先阅读[一些基本常识](https://linux.cn/article-6160-1.html). 然后我们介绍最常用的一些命令:
|
||||
|
||||
- (重要)首先教一个命令 `sudo su` 进入 root 账户(敲完之后会让你敲当前登录账户的密码 密码敲得过程中没有*****这种传统敲密码的提示 为 linux 传统艺能 其实是敲进去了),因为本身普通账户没什么权限,会出现处处的权限提示,建议直接使用 root 账户。
|
||||
|
||||
```txt
|
||||
这里有一个彩蛋(如果你用的是 centos 的话)
|
||||
当用户第一次使用 sudo 权限时 CentOS 的系统提示:
|
||||
我们信任您已经从系统管理员那里了解了日常注意事项。
|
||||
总结起来无外乎这三点:
|
||||
#1) 尊重别人的隐私。
|
||||
#2) 输入前要先考虑 (后果和风险)。
|
||||
#3) 权力越大,责任越大。
|
||||
```
|
||||
|
||||
- `ls` 用于列出当前目录 (即"文件夹") 下的所有文件 (或目录). 目录会用蓝色显示。`ls -l` 可以显示详细信息。
|
||||
- `pwd` 能够列出当前所在的目录。
|
||||
- `cd DIR` 可以切换到 `DIR` 目录。在 Linux 中,每个目录中都至少包含两个目录:`.` 指向该目录自身,`..` 指向它的上级目录。文件系统的根是 `/`.
|
||||
- `touch NEWFILE` 可以创建一个内容为空的新文件 `NEWFILE`, 若 `NEWFILE` 已存在,其内容不会丢失。
|
||||
- `cp SOURCE DEST` 可以将 `SOURCE` 文件复制为 `DEST` 文件; 如果 `DEST` 是一个目录,则将 `SOURCE` 文件复制到该目录下。
|
||||
- `mv SOURCE DEST` 可以将 `SOURCE` 文件重命名为 `DEST` 文件; 如果 `DEST` 是一个目录,则将 `SOURCE` 文件移动到该目录下。
|
||||
- `mkdir DIR` 能够创建一个 `DIR` 目录。
|
||||
- `rm FILE` 能够删除 `FILE` 文件; 如果使用 `-r` 选项则可以递归删除一个目录。删除后的文件无法恢复,使用时请谨慎!
|
||||
- `man` 可以查看命令的帮助。例如 `man ls` 可以查看 `ls` 命令的使用方法。灵活应用 `man` 和互联网搜索,可以快速学习新的命令。
|
||||
|
||||
`man` 的功能不仅限于此。`man` 后可以跟两个参数,可以查看不同类型的帮助 (请在互联网上搜索). 例如当你不知道 C 标准库函数 `freopen` 如何使用时,可以键入命令
|
||||
|
||||
```bash
|
||||
man 3 freopen
|
||||
```
|
||||
|
||||
### **统计代码行数**
|
||||
|
||||
第一个例子是统计一个目录中 (包含子目录) 中的代码行数。如果想知道当前目录下究竟有多少行的代码,就可以在命令行中键入如下命令:
|
||||
|
||||
```bash
|
||||
find . | grep '\.c$\|\.h$' | xargs wc -l
|
||||
```
|
||||
|
||||
如果用 `man find` 查看 `find` 操作的功能,可以看到 `find` 是搜索目录中的文件。Linux 中一个点 `.` 始终表示 Shell 当前所在的目录,因此 `find .` 实际能够列出当前目录下的所有文件。如果在文件很多的地方键入 `find .`, 将会看到过多的文件,此时可以按 `CTRL + c` 退出。
|
||||
|
||||
同样,用 `man` 查看 `grep` 的功能——"print lines matching a pattern". `grep` 实现了输入的过滤,我们的 `grep` 有一个参数,它能够匹配以 `.c` 或 `.h` 结束的文件。正则表达式是处理字符串非常强大的工具之一,每一个程序员都应该掌握其相关的知识。? 上述的 `grep` 命令能够提取所有 `.c` 和 `.h` 结尾的文件。
|
||||
|
||||
刚才的 `find` 和 `grep` 命令,都从标准输入中读取数据,并输出到标准输出。关于什么是标准输入输出,请参考[这里](http://en.wikipedia.org/wiki/Standard_streams). 连接起这两个命令的关键就是管道符号 `|`. 这一符号的左右都是 Shell 命令,`A | B` 的含义是创建两个进程 `A` 和 `B`, 并将 `A` 进程的标准输出连接到 `B` 进程的标准输入。这样,将 `find` 和 `grep` 连接起来就能够筛选出当前目录 (`.`) 下所有以 `.c` 或 `.h` 结尾的文件。
|
||||
|
||||
我们最后的任务是统计这些文件所占用的总行数,此时可以用 `man` 查看 `wc` 命令。`wc` 命令的 `-l` 选项能够计算代码的行数。`xargs` 命令十分特殊,它能够将标准输入转换为参数,传送给第一个参数所指定的程序。所以,代码中的 `xargs wc -l` 就等价于执行 `wc -l aaa.c bbb.c include/ccc.h ...`, 最终完成代码行数统计。
|
||||
|
||||
### **统计磁盘使用情况**
|
||||
|
||||
以下命令统计 `/usr/share` 目录下各个目录所占用的磁盘空间:
|
||||
|
||||
```bash
|
||||
du -sc /usr/share/* | sort -nr
|
||||
```
|
||||
|
||||
`du` 是磁盘空间分析工具,`du -sc` 将目录的大小顺次输出到标准输出,继而通过管道传送给 `sort`. `sort` 是数据排序工具,其中的选项 `-n` 表示按照数值进行排序,而 `-r` 则表示从大到小输出。`sort` 可以将这些参数连写在一起。
|
||||
|
||||
然而我们发现,`/usr/share` 中的目录过多,无法在一个屏幕内显示。此时,我们可以再使用一个命令:`more` 或 `less`.
|
||||
|
||||
```bash
|
||||
du -sc /usr/share/* | sort -nr | more
|
||||
```
|
||||
|
||||
此时将会看到输出的前几行结果。`more` 工具使用空格翻页,并可以用 `q` 键在中途退出。`less` 工具则更为强大,不仅可以向下翻页,还可以向上翻页,同样使用 `q` 键退出。这里还有一个[关于 less 的小故事](http://en.wikipedia.org/wiki/Less_(Unix)).
|
||||
|
||||
### **在 Linux 下编写 Hello World 程序**
|
||||
|
||||
Linux 中用户的主目录是 `/home/用户名称`, 如果你的用户名是 `user`, 你的主目录就是 `/home/user`. 用户的 `home` 目录可以用波浪符号 `~` 替代,例如临时文件目录 `/home/user/Templates` 可以简写为 `~/Templates`. 现在我们就可以进入主目录并编辑文件了。如果 `Templates` 目录不存在,可以通过 `mkdir` 命令创建它:
|
||||
|
||||
```bash
|
||||
cd ~
|
||||
mkdir Templates
|
||||
```
|
||||
|
||||
创建成功后,键入
|
||||
|
||||
```bash
|
||||
cd Templates
|
||||
```
|
||||
|
||||
可以完成目录的切换。注意在输入目录名时,`tab` 键可以提供联想。
|
||||
|
||||
#### 你感到键入困难吗?
|
||||
|
||||
::: warning 💡 你可能会经常要在终端里输入类似于
|
||||
|
||||
cd AVeryVeryLongFileName
|
||||
|
||||
的命令,你一定觉得非常烦躁。回顾上面所说的原则之一:如果你感到有什么地方不对,就一定有什么好办法来解决。试试 `tab` 键吧。
|
||||
|
||||
Shell 中有很多这样的小技巧,你也可以使用其他的 Shell 例如 zsh, 提供更丰富好用的功能。总之,尝试和改变是最重要的。
|
||||
:::
|
||||
|
||||
进入正确的目录后就可以编辑文件了,开源世界中主流的两大编辑器是 `vi(m)` 和 `emacs`, 你可以使用其中的任何一种。如果你打算使用 `emacs`, 你还需要安装它
|
||||
|
||||
```bash
|
||||
apt-get install emacs
|
||||
```
|
||||
|
||||
`vi` 和 `emacs` 这两款编辑器都需要一定的时间才能上手,它们共同的特点是需要花较多的时间才能适应基本操作方式 (命令或快捷键), 但一旦熟练运用,编辑效率就比传统的编辑器快很多。
|
||||
|
||||
进入了正确的目录后,输入相应的命令就能够开始编辑文件。例如输入
|
||||
|
||||
```bash
|
||||
vi hello.c
|
||||
或emacs hello.c
|
||||
```
|
||||
|
||||
就能开启一个文件编辑。例如可以键入如下代码 (对于首次使用 `vi` 或 `emacs` 的同学,键入代码可能会花去一些时间,在编辑的同时要大量查看网络上的资料):
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
int main(void) {
|
||||
printf("Hello, Linux World!\n");
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
> 相信你在写完代码之后苦于不知道怎么保存并退出,不用担心,这个是正常的,毕竟上面提到的两个文本编辑器都是以入门时的学习曲线极其陡峭而著称。
|
||||
> 对于 vi(m) 风格的编辑器,你需要先按 `ESC` 返回 NORMAL 模式(具体处于那个模式可以观察窗口左下角,NORMAL 模式是空白的),再输入 `:wq` 来保存并退出(注意 `:` 是输入的一部分)(`:q 仅退出` `:q! 不保存退出` )
|
||||
>
|
||||
> [【保姆级入门】Vim 编辑器](https://www.bilibili.com/video/BV13t4y1t7Wg)
|
||||
>
|
||||
> <Bilibili bvid='BV13t4y1t7Wg'/>
|
||||
|
||||
保存后就能够看到 `hello.c` 的内容了。终端中可以用 `cat hello.c` 查看代码的内容。如果要将它编译,可以使用 `gcc` 命令:
|
||||
|
||||
```bash
|
||||
gcc hello.c -o hello
|
||||
```
|
||||
|
||||
`gcc` 的 `-o` 选项指定了输出文件的名称,如果将 `-o hello` 改为 `-o hi`, 将会生成名为 `hi` 的可执行文件。如果不使用 `-o` 选项,则会默认生成名为 `a.out` 的文件,它的含义是 [assembler output](http://en.wikipedia.org/wiki/A.out). 在命令行输入
|
||||
|
||||
```bash
|
||||
./hello
|
||||
```
|
||||
|
||||
就能够运行该程序。命令中的 `./` 是不能少的,点代表了当前目录,而 `./hello` 则表示当前目录下的 `hello` 文件。与 Windows 不同,Linux 系统默认情况下并不查找当前目录,这是因为 Linux 下有大量的标准工具 (如 `test` 等), 很容易与用户自己编写的程序重名,不搜索当前目录消除了命令访问的歧义。
|
||||
76
2023旧版内容/3.编程思维体系构建/3.Y.6Vim初探索.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Vim 初探索
|
||||
|
||||
## 下载 vim
|
||||
|
||||
vim 被称为编辑器之神
|
||||
|
||||
::: warning 💡 看到这一句可能就激发了你学习 vim 的热情,但是看完整篇文章和文章里面的所有参考资料,可能这股来之不易的热情也早就消失了。为了避免这种情况,我给一个小小的建议:
|
||||
|
||||
1. 首先学会盲打,不会的话,不是很建议用 vim / Emacs 这样的编辑器,还是拥抱鼠标吧
|
||||
2. 学习使用 hjklia 这六个键,然后理解插入模式和普通模式,再了解怎么退出
|
||||
3. 使用 vim 作为日常的编辑工具,在你所有的代码编辑器里面都装上 vim 插件并使用,强迫自己习惯 hjkl 的移动和带模式的输入,习惯按 `<ESC>`
|
||||
4. 到这个时候你就会感觉的确可以不用鼠标了,但是有的时候会比较别扭,比如想新建一行时,得按 L 移到行尾,然后按 a 追加,再按回车,远远比鼠标麻烦有没有,这种情况就可以上网查询,`vim 如何新建一行`,就会学到 o 可以向下新建一行,O 可以向上新建一行,然后你就能自然地学会更多的操作。
|
||||
:::
|
||||
|
||||
因为其具有着非常完整的生态以及诸多配套的插件,但是第一次使用得你可能感觉很不习惯。
|
||||
|
||||
讲一个笑话,你如何获得一个随机字符串,只要让新人使用 vim 就好了。
|
||||
|
||||
::: waning 💡 不开玩笑,为了让你不小心在命令行模式下进入 vim 又不知道怎么退出时不需要拔电源来退出,先按几次 `<ESC>` 键(避免你之前不小心按到了 i 或 a 或 o 或等等按键)进入普通模式,然后顺序敲击 `:q`(冒号 和 q 两个按键 ),再按回车就可以退出了。
|
||||
:::
|
||||
|
||||
```bash
|
||||
apt-get install vim
|
||||
```
|
||||
|
||||
但是我仍然推荐你尝试使用或者结合 VSC 一起使用,使用习惯后将有效提高你的开发效率。
|
||||
|
||||
## 如何学习 vim
|
||||
|
||||
作为程序员,我们大部分时间都花在代码编辑上,所以花点时间掌握某个适合自己的编辑器是非常值得的。通常学习使用一个新的编辑器包含以下步骤:
|
||||
|
||||
- 阅读教程(比如这节课以及我们为您提供的资源)
|
||||
- 坚持使用它来完成你所有的编辑工作(即使一开始这会让你的工作效率降低)
|
||||
- 随时查阅:如果某个操作看起来像是有更方便的实现方法,一般情况下真的会有。
|
||||
|
||||
如果您能够遵循上述步骤,并且坚持使用新的编辑器完成您所有的文本编辑任务,那么学习一个复杂的代码编辑器的过程一般是这样的:头两个小时,您会学习到编辑器的基本操作,例如打开和编辑文件、保存与退出、浏览缓冲区。当学习时间累计达到 20 个小时之后,您使用新编辑器的效率应该已经和使用老编辑器一样快。在此之后,其益处开始显现:有了足够的知识和肌肉记忆后,使用新编辑器将大大节省你的时间。而现代文本编辑器都是些复杂且强大的工具,永远有新东西可学:学的越多,效率越高。
|
||||
|
||||
## **Vim 的哲学**
|
||||
|
||||
在编程的时候,你会把大量时间花在阅读/编辑而不是在写代码上。所以,Vim 是一个_多模态_编辑 器:它对于插入文字和操纵文字有不同的模式。Vim 是可编程的(可以使用 Vimscript 或者像 Python 一样的其他程序语言),Vim 的接口本身也是一个程序语言:键入操作(以及其助记名)是命令,这些命令也是可组合的。Vim 避免了使用鼠标,因为那样太慢了;Vim 甚至避免用 上下左右键因为那样需要太多的手指移动。
|
||||
|
||||
这样的设计哲学使得 Vim 成为了一个能跟上你思维速度的编辑器。
|
||||
|
||||
## 学习 Vim
|
||||
|
||||
如果想要使用他最基本的操作的话,在电脑上键入 vimtutor
|
||||
|
||||
会有官方的教程进行引导哦。
|
||||
|
||||
## 配置 vim
|
||||
|
||||
vim 有大量的配置,通过更改./vimrc 文件或者安装插件都可以有效提高你的开发效率,定制属于你个人的编辑器哦~
|
||||
|
||||
快去试试吧
|
||||
|
||||
## 任务
|
||||
|
||||
定制 vim 成为你喜欢的模样,加装足够多的插件和更改足够多的配置让他满足以下几点或以上
|
||||
|
||||
- 文件管理
|
||||
- 快速回退
|
||||
- 变得好看
|
||||
- 行号
|
||||
- 代码搜索
|
||||
- 模糊搜索
|
||||
- ...............
|
||||
|
||||
可以尝试查看 vim Awesome 哦
|
||||
|
||||
[vim awesome](https://vimawesome.com/)
|
||||
|
||||
## 拓展阅读
|
||||
|
||||
[Learn-Vim(the Smart Way) 中文翻译](https://github.com/wsdjeg/Learn-Vim_zh_cn)
|
||||
|
||||
讲述了 vim 哲学的优秀教程
|
||||
126
2023旧版内容/3.编程思维体系构建/3.Y.7linux小任务.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# linux 自测
|
||||
|
||||
1. 本课程需要使用类 Unix shell,例如 Bash 或 ZSH。如果您在 Linux 或者 MacOS 上面完成本课程的练习,则不需要做任何特殊的操作。如果您使用的是 Windows,则您不应该使用 cmd 或是 Powershell;您可以使用<u>Windows Subsystem for Linux</u>或者是 Linux 虚拟机。使用 `echo $SHELL` 命令可以查看您的 shell 是否满足要求。如果打印结果为 `/bin/bash` 或 `/usr/bin/zsh` 则是可以的。
|
||||
2. 在 `/tmp` 下新建一个名为 `missing` 的文件夹。
|
||||
3. 用 `man` 查看程序 `touch` 的使用手册。
|
||||
4. 用 `touch` 在 `missing` 文件夹中新建一个叫 `semester` 的文件。
|
||||
5. 将以下内容一行一行地写入 `semester` 文件:
|
||||
|
||||
```bash
|
||||
#!/bin/sh
|
||||
curl --head --silent https://missing.csail.mit.edu
|
||||
```
|
||||
|
||||
第一行可能有点棘手, `#` 在 Bash 中表示注释,而 `!` 即使被双引号(`"`)包裹也具有特殊的含义。单引号(`'`)则不一样,此处利用这一点解决输入问题。更多信息请参考 <u>Bash quoting 手册</u>
|
||||
|
||||
1. 尝试执行这个文件。例如,将该脚本的路径(`./semester`)输入到您的 shell 中并回车。如果程序无法执行,请使用 `ls` 命令来获取信息并理解其不能执行的原因。
|
||||
2. 查看 `chmod` 的手册 (例如,使用 `man chmod` 命令)
|
||||
3. 使用 `chmod` 命令改变权限,使 `./semester` 能够成功执行,不要使用 `sh semester` 来执行该程序。您的 shell 是如何知晓这个文件需要使用 `sh` 来解析呢?更多信息请参考:<u>shebang</u>
|
||||
4. 使用 `|` 和 `>` ,将 `semester` 文件输出的最后更改日期信息,写入主目录下的 `last-modified.txt` 的文件中
|
||||
5. 写一段命令来从 `/sys` 中获取笔记本的电量信息,或者台式机 CPU 的温度
|
||||
6. 使用 shell 编程写一个类似脚本的图书管理系统,包含增删改查四个功能
|
||||
|
||||
当然,可能会有点困难我在这里附上一段参考代码
|
||||
|
||||
```shell
|
||||
#! /usr/bin/env bash
|
||||
|
||||
initialization()
|
||||
{
|
||||
echo -n "| " ;echo "1:添加图书"
|
||||
echo -n "| " ;echo "2:删除图书"
|
||||
echo -n "| " ;echo "3:显示馆藏图书"
|
||||
echo -n "| " ;echo "4:查找图书"
|
||||
echo -n "| " ;echo "5:退出系统"
|
||||
mainmenu
|
||||
}
|
||||
|
||||
mainmenu()
|
||||
{
|
||||
read operation
|
||||
case $operation in
|
||||
1)
|
||||
addbook ;;
|
||||
2)
|
||||
delbook ;;
|
||||
3)
|
||||
listbooks ;;
|
||||
4)
|
||||
search ;;
|
||||
5)
|
||||
exit ;;
|
||||
*)
|
||||
"无效操作,请重试。"
|
||||
initialization ;;
|
||||
esac
|
||||
}
|
||||
|
||||
#直接在文件夹内添加书(所以没有 书单.txt),若没有就find遍历系统匹配并加入
|
||||
#不考虑系统中多个匹配结果的情况
|
||||
addbook()
|
||||
{
|
||||
echo "添加图书"
|
||||
echo "-------------------------"
|
||||
read -p "输入添加的书名" bookname
|
||||
if [[ -f ~/Desktop/bookregister/$bookname ]] ;
|
||||
then
|
||||
echo "已经存在这本书。"
|
||||
return
|
||||
else
|
||||
find ~ -name $bookname -exec mv {} ~/Desktop/bookregister \;
|
||||
if [[ -f ~/Desktop/bookregister/$bookname ]] ;
|
||||
then
|
||||
echo "匹配成功,已加入书单。"
|
||||
else
|
||||
read -p "未找到这本书,手动输入链接从网上下载?[y/n]" download
|
||||
if [[ $download = 'y' ]] ;
|
||||
then
|
||||
read -p "请输入链接。" url
|
||||
curl ${url} -o $bookname
|
||||
find ~ -name $bookname -exec mv {} ~/Desktop/bookregister \;
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
delbook()
|
||||
{
|
||||
echo "删除图书"
|
||||
echo "-------------------------"
|
||||
read -p "输入删除的书名" bookname
|
||||
if [[ ! -f ~/Desktop/bookregister/$bookname ]] ;
|
||||
then
|
||||
echo "查无此书。"
|
||||
return
|
||||
else
|
||||
read -p "输入 delete 以确认。" delete
|
||||
if [[ $delete = delete ]];
|
||||
then
|
||||
rm ~/Desktop/bookregister/$bookname
|
||||
echo "删除完毕。"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
#因为脚本图书放在一起,所以脚本自身也会显示(不要在意这些细节)
|
||||
listbooks()
|
||||
{
|
||||
ls ~/Desktop/bookregister
|
||||
}
|
||||
|
||||
|
||||
#若找到直接用less打开
|
||||
search()
|
||||
{
|
||||
echo "查找图书"
|
||||
echo "-------------------------"
|
||||
read -p "输入查找的书名" bookname
|
||||
if [[ -f ~/Desktop/bookregister/$bookname ]] ;
|
||||
then
|
||||
less ~/Desktop/bookregister/$bookname
|
||||
else
|
||||
echo "查无此书。"
|
||||
fi
|
||||
}
|
||||
|
||||
```
|
||||
57
2023旧版内容/3.编程思维体系构建/3.编程思维体系构建.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# 3.编程思维体系构建
|
||||
|
||||
无论怎么说,编程都是计算机科学不可或缺的一部分。
|
||||
|
||||
他的核心思想在于用将可量化的重复性劳作交给计算机来完成,以达成提高效率的目的。
|
||||
|
||||
这里引用一位老师的比喻就是,将编程形容为开车,你可以讨厌他,也可以喜欢他。
|
||||
|
||||
但是,他作为一个交通工具,你不开你就得腿着去学校。想想如果你从杭电走到西湖只能走着去,那是多么恐怖的一件事。
|
||||
|
||||
(当然,现在 GPT 的强大功能可以帮大伙解决相当多的工作,因此,我们可能需要掌握更多的逻辑思维能力和分解问题的能力,将问题简化之后用 GPT 解决也不失为一个选择)
|
||||
|
||||
因此本章节的目标是让大家面对一个实际问题,有使用编程解决的思路和能力。
|
||||
|
||||
## 阅读本章内容,我可以得到什么
|
||||
|
||||
本章提供了非常多的软实力文章,阅读相关软实力文章,你可以根据自己的情况构建适合自己一通百通的学习编程知识的方法论。
|
||||
|
||||
本章提供了相当完善的,足够面对多数需求的 C 语言体系结构,通过完成 C 语言的体系,你可以较为熟练地掌握 C 语言,并且对构建一个较小规模的项目组织和项目拆分有一定的理解
|
||||
|
||||
python 内容完成后,基本学习到如何使用 python 当一门工具使用,当有具体的需求可以进行后续的补充学习
|
||||
|
||||
与此同时,Git or Linux 也是作为一个 CSer 或者说想要提升自身效率的程序员,不可或缺的一个内容,希望你能掌握
|
||||
|
||||
如果你要开始,推荐你从 3.0 开始阅读,然后挑选你喜欢的内容
|
||||
|
||||
[【计算机科学速成课】[40 集全/精校] - Crash Course Computer Science](https://www.bilibili.com/video/BV1EW411u7th)
|
||||
|
||||
<Bilibili bvid='BV1EW411u7th'/>
|
||||
|
||||

|
||||
|
||||
## 本章参考内容
|
||||
|
||||
[cs61a](https://cs61a.org/)
|
||||
|
||||
[CS 自学指南](https://csdiy.wiki/)
|
||||
|
||||
[MIT-Missing-Semester](https://missing.csail.mit.edu/2020/)
|
||||
|
||||
[Introductory C Programming](https://www.coursera.org/specializations/c-programming)
|
||||
|
||||
[一生一芯 nemu](https://ysyx.oscc.cc/)
|
||||
|
||||
[jyy 的 OS 课程](https://jyywiki.cn/)
|
||||
|
||||
[迷宫 game](https://github.com/helderman/htpataic)
|
||||
|
||||
[GDB User Manual](https://www.sourceware.org/gdb/)
|
||||
|
||||
[learn vim](https://github.com/wsdjeg/Learn-Vim_zh_cn)
|
||||
|
||||
Book:教材替换用书——《C Primer Plus》
|
||||
|
||||
::: tip 📥
|
||||
《C Primer Plus》(第六版中文版)(216MB)附件下载 <Download url="https://cdn.xyxsw.site/files/C%20Primer%20Plus%E7%AC%AC6%E7%89%88%20%E4%B8%AD%E6%96%87%E7%89%88.pdf"/>
|
||||
:::
|
||||
BIN
2023旧版内容/3.编程思维体系构建/code/HW 01.zip
Normal file
BIN
2023旧版内容/3.编程思维体系构建/static/Axiomofchoice_1.png
Normal file
|
After Width: | Height: | Size: 230 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/CWDhbW6gzogyMFxtd6kcnPBunv2.png
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/Cpfzb9oK2oMyGxxgkqVceE8DnId.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/FkOybDwtnoQeRyxejlwcjhQ2nch.png
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/GEN5b1HHdoDegPxAp8WcQDGknoc.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/HMipbO4vSoM3jhxSZ7Kcuddqnxh.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/HZNMbzGZOoQGAhxQ29gcM5V4nNd.png
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/HgTfbMhCGodZbzxBNh9crH3cnCe.png
Normal file
|
After Width: | Height: | Size: 450 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/Hqzbbs6iYobnxWxz11Ocfa9gnHd.png
Normal file
|
After Width: | Height: | Size: 386 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/L5HvblSuYonJn4x03a4cMLKknrh.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/MF4ZbUZ0qo70gRxeNGocsYvmnwe.png
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/YAOvb6gquofiAYxsn3tcxcCYngf.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcn05Ca6Wu5TxFMplZCw2N8Jb.png
Normal file
|
After Width: | Height: | Size: 161 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcn0VMYQlez7tQTNkTPDkCsvg.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcn1HbQct335qvZ71tGNu7jne.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcn1hlL1Fk4kDK4CPT2hJxwnV.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcn2ouk043lNQEUkVkIS7bSSd.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcn30VJILYpO81pq89mAmzjTf.png
Normal file
|
After Width: | Height: | Size: 175 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcn3PLPIvKSSvYiCnwx50FYvf.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcn3l30usevMTgv1ZbZ0mfJdh.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcn5Uk41JyjjdTzXWQqUkexzc.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcn5sVnE76FYpVW2RDxtWDiZc.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcn6MgNnY2qBd1yAudeirx6Sh.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcn6ZnAzhaj2Tj7xk9K6FxBJh.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcn7zL0QFakVTpYBdpOmmWOvc.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcn85Yb3JIQ3520KeaSoyPVDd.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcn8ZxT5oMkScArZjZhgM6TYb.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcn8aRDQpe7uuDxFv9v1WvZ4c.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcn95LbcwuMC2dIViOxWk8BFb.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcn9DSPlFgG2WMZhTOE9Zhzgb.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcn9VFPUYHl8ghJ3C78RsXjtf.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcnAnXUHDqsMYVrDlBfFunoVf.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcnAnkVAJmMT0NSNvo6crXYAd.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcnBMq0sw6c48jvjdPJwmAGtZ.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcnC6TAAdtS0P5HzebFgFn2lc.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcnCNpeAE9Hy61cyvtxfioIHg.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcnCX92JHjg8PU3quKs4GziZb.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcnFwZpWZ3fQkdd3mCO8Mr9Wj.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
2023旧版内容/3.编程思维体系构建/static/boxcnG6z1VpAYUGMSkSwDBUxEvf.png
Normal file
|
After Width: | Height: | Size: 114 KiB |