第5章 插叙:进程API 读书笔记

UNIX系统通过一对系统调用fork()exec()来创建新进程,通过wait()等待其创建的子进程执行完成。

5.1 fork()系统调用

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    printf("hello world (pid:%d)\n", (int) getpid());
    int rc = fork();
    if (rc < 0) { // fork failed
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) { // child (new process)
        printf("hello, I am child (pid:%d)\n", (int) getpid());
    } else { // parent goes down this path (main)
        printf("hello, I am parent of %d (pid:%d)\n", rc, (int) getpid());
    }
    return 0;
}

对于操作系统来说,看起来有两个一样的程序在运行,并且都从fork()调用中返回。子进程不会从main()函数开始,而是直接从fork()调用中返回,就好像自己调用了fork()

子进程(child)并不是完全拷贝了父进程(parent)。子进程拥有自己的地址空间、寄存器、程序计数器等,但从fork()的返回值不同。父进程获得的返回值是子进程的PID,子进程获得的是0。

输出不是确定的(deterministic)。单CPU的系统上,两者都有先运行的可能。

CPU调度程序(scheduler)决定了某个时刻哪个进程被执行。

5.2 wait()系统调用

父进程等待子进程执行完毕。也可使用waitpid()

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[]) {
    printf("hello world (pid:%d)\n", (int) getpid());
    int rc = fork();
    if (rc < 0) { // fork failed; exit
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) { // child (new process)
        printf("hello, I am child (pid:%d)\n", (int) getpid());
    } else { // parent goes down this path (main)
        int rc_wait = wait(NULL);
        printf("hello, I am parent of %d (rc_wait:%d) (pid:%d)\n", rc, rc_wait, (int) getpid());
    }
    return 0;
}

增加了wait()调用后,输出结果变得确定了。即使父进程先运行,会马上调用wait(),停下来等待子进程执行结束后才返回。附注中提到,有些情况下会在子进程退出前返回,细节见man

5.3 最后是exec()系统调用

有多个变体,个人看起来好像只是传参方式不一样而已。让子进程执行和父进程不一样的程序。而fork()只能运行相同程序。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main(int argc, char *argv[]) {
    printf("hello world (pid:%d)\n", (int) getpid());
    int rc = fork();
    if (rc < 0) { // fork failed; exit
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) { // child (new process)
        printf("hello, I am child (pid:%d)\n", (int) getpid());
        char *myargs[3];
        myargs[0] = strdup("wc"); // program: "wc" (word count)
        myargs[1] = strdup("p3.c"); // argument: file to count
        myargs[2] = NULL; // marks end of array
        execvp(myargs[0], myargs); // runs word count
        printf("this shouldn’t print out");
    } else { // parent goes down this path (main)
        int rc_wait = wait(NULL);
        printf("hello, I am parent of %d (rc_wait:%d) (pid:%d)\n", rc, rc_wait, (int) getpid());
    }
    return 0;
}

exec()会从可执行程序中加载代码和静态数据,覆盖原本的代码段和静态数据,重新初始化堆和栈等内存空间。没有创建新进程,而是直接替换为不同的程序。成功调用不会返回。

5.4 为什么这么设计API

事实证明,分离fork()exec()的做法在构建UNIX shell的时候非常有用,这给了shell在fork之后exec之前运行代码的机会。

做对事(Get it right)。抽象和简化都不能替代做对事。

Lampson – 《Hints for Computer Systems Design》

有许多方式来设计创建进程的API,但fork()exec()的组合既简单又极其强大。

shell找到可执行程序,调用fork()创建新进程,调用exec()的某个变体来执行这个可执行程序,调用wait()等待该命令完成。子进程执行结束后,shell从wait()中返回并再次输出一个提示符,等待用户输入下一条命令。

重定向则在调用exec()前先关闭标准输出(standard output),然后打开文件。其实也可以用dup2()之类的,似乎比较方便。

close(STDOUT_FILENO);
open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC, S_IRWXU);

UNIX管道也是用类似方法实现的,但用的是pipe()系统调用。一个进程的输出被链接到一个内核管道(pipe)上(队列),另一个进程的输入也被链接到了同一个管道上。前一个进程的输出无缝地作为后一个进程的输入。

5.5 其他API

UNIX中还有其他许多和进程交互的方式,比如通过kill()系统调用向进程发送信号(signal),包括要求睡眠、终止等。

第1章 入门 读书笔记

发现这本书真的挺不错的。图灵社区似乎只有纸质书的版权,各大电商均有在售。电子书似乎GitBook上有中文版,如果字体阅读起来难受的话,里面似乎提到,有提供一份正常的。

1.3 Git基础

1.3.5 三种状态

已提交(committed,已存入本地数据库)、已修改(modified,未提交到数据库)和已暂存(staged,对已修改文件做出标识并加入下一次要提交的快照)。

三个主要的区域:Git目录、工作目录、暂存区。

基本工作流:修改工作目录中的文件;暂存,将文件快照加入暂存区;提交,永久保存在Git目录中。

1.5 安装Git

1.5.3 Windows上的安装方法

在Git网站上下载,即Git for Windows项目,独立于Git。

或是安装Windows版的Git,既包含命令行版本的Git,也包括GUI。

1.6 Git的首次配置

使用git config来获取和设置配置变量。

/etc/gitconfig:所有用户及其仓库的值,通过--system参数。
~/.gitconfig~/.config/git/config:针对自己,通过--global参数。
当前仓库的Git目录(.git/config):针对单个仓库。

每一级都会覆盖上一级中的设置。

1.6.1 用户身份

每次提交都需要,并且会被保存,不可更改。

git config --global user.name "John Doe"
git config --global user.email "johndoe@example.com"

也可不带--global参数,不同项目设置不同的用户。

1.6.2 个人编辑器
git config --global core.editor emacs
1.6.3 检查个人设置
git config --list

查看某个键,例如:

git config user.name

1.7 获取帮助

git help <verb>
git <verb> --help
man git-<verb>

关于CLion的基本的安装和配置建议

可能有人要问我了:为什么要用这个IDE?这么复杂的安装过程我为什么不用Dev-C++或者CodeBlocks?VC6.0多好用啊!

这个…我的回答是:CLion确实是一个优秀的、强大的跨平台C/C++ IDE;安装过程本身不复杂,官网提供了详尽的文档;此外,理解(划重点)并按照向导一步步做下来也是相当简单的,写这篇主要是因为…很多同学真的看到英文就害怕,对此我只能引用一下这段话:

“英文在科学技术领域是世界语”这个事实在未来几十年都不会改变。我们在授课过程中应用的非技术词汇都很简单。因此停止抱怨,以开放的心态来迎接挑战吧。你会发现,其实挑战也并不大。

学堂在线 《电路原理》 于歆杰教授 朱桂萍教授 陆文娟教授 清华大学电机系

至于它的特性和优点,你可以看这里:Intelligent Coding Assistance & Code Analysis – Features | CLion。如果新手的你看不懂的话,我大概可以告诉你这几点:

  • 静态代码审查:在构建前即可知道代码中存在的问题,一般情况下一些比较傻的问题都能很快发现,可以节约大量的时间。这些提示对于提高你的代码质量也有帮助,我觉得这点非常重要。
  • 智能代码补全:你可以利用多种技术来进行补全,包括但不限于补全关键字、变量名或者使用模板,可以大幅提高编写代码的速度。
  • 你可以比较方便地在考试机房的电脑上安装并使用,而不需要花费很长时间,然而你可能需要等待较长时间才能把Visual Studio 2019(或者未来的更高版本)部署好。

Anyway,现在已经是2020年了,我再次修订这篇文章,还是那句话:建议你在学习和未来的开发过程中使用先进的工具。包括但不限于:

如果你期望使用Visual Studio,那么你只要到官网上下载对应的Installer,选中你需要的开发用途并安装就可以了。由于会自动配置工具链,整个安装过程非常友好且不劝退,安装好之后马上就可以开始使用。我建议你再装上ReSharper Ultimate,这可能对你学习或是开发有很大的帮助。

下文主要探讨CLion的一些配置建议。但不管怎样,这些都只是工具而已,水平怎么样还是要看个人,没有必要非要争个谁更厉害,甚至是用出优越感来。更重要的是你平时有没有努力学习和探索,给你个宇宙第一IDE,用了几年还是只会按几个常用的功能,其它的都不碰的话,那跟你一开始就用文本编辑器似乎没什么区别,甚至编辑器还要更快一点。

废话了这么多,开始说正事,希望我写的这些能让你看到,为什么它值得我们去用。此外,这不是教程向,也不会手把手地教你并给你截图,我始终反对无脑跟随一个教程的做法,希望你会理解后再去动手。

安装CLion

作为计算机专业学生,如果需要安装某某应用软件,正确的操作应当是前往官网,阅读说明后下载并安装。而非点击某某下载站的“快速下载”按钮。

你会发现,你可以选择直接下载CLion的安装程序进行安装;也可以选择下载JetBrains Toolbox,然后用Toolbox来安装并管理包括CLion在内的JetBrains IDE。我更推荐后者,虽然多了个应用,但是方便你管理IDE和最近的项目,并且可以静默为你安装更新。

那我就假设你已经安装好了Toolbox并且在当中安装了CLion。我的Toolbox大概长这样:

JetBrains Toolbox

申请教育授权

JetBrains全家桶对于教育用途来说都是免费的,具体可以自行前往这里查看。

不用我说,你应该要能找到这里,点击进入申请即可。通常情况下,这里会使用教育邮箱来验证你的身份,也就是学校为你提供的那个。

(深大特供)我想想估计又有好多同学会问:我忘记了密码怎么办?教育邮箱是什么?首先,忘记了密码,目前请持校园卡前往沧海校区(原南校区)致理楼C(原理工楼L3)1007进行重置,重置后记得在设置中绑定你的微信,这样的话,以后忘记密码也可以自己重置了。(最好还是弄个密码管理器)其次,教育邮箱通常是你所在的教育机构提供的、网域归属于该教育机构的邮箱。像我这样的人脑收藏夹应该比较少,那我就推荐你到内部网里点击这个:

内部网->学生邮箱

其它方式我就不扯了,自己研究吧。不管怎样,你申请到的教育授权有效期应该是1年的。

不过不用慌,每年到期前,你都会收到邮件提醒你续期,只要你没毕业(教育授权政策似乎在慢慢收紧来应对白嫖党)并且持续持有教育邮箱,你就能一直免费使用。

更多关于授权的信息就自己看吧。

启动CLion

不同于往常你可能用过的IDE,这里需要你自行选择并配置Toolchain才能开始构建项目。如果你一路Next来到主界面后就会发现你写的程序没法构建并运行。以前你使用的IDE可能已经为你包办好了一切,希望这不会让你感到特别不习惯。

在不同平台上,所需的工具链通常是不同的,简单说来大概是这样的:

  • Linux。我本来打算写的,但是想想就算是某厂预装了Linux系统笔记本的用户,应该第一时间就安装了Windows。除此之外的Linux用户应该都是能人,不用我来教,只要用包管理把cmakemakegccgdb这些东西装上就好了。
  • MacOS。由于本菜买不起,所以只好给你放个文档了:Quick Tutorial: Configure CLion on macOS – Help | CLion。据文档所述,开发所需的工具应该系统已经事先预装好了,你只需要检测它们是否能正常工作就可以了。
  • Windows,有很多选择。很多选择倒不是说它很好,我个人觉得都是因为不太完美所以才会搞出这么多方案来。包括但不限于WSL(子系统)、Cygwin、MinGW这些。

下面主要简单来说说Windows下的那些方案,具体的还是自己看文档比较好。

Windows Subsystem for Linux (WSL) on Windows 10 适用于Linux的Windows子系统

非常推荐你日常学习和开发用这个方案(和GUI无关的话)。(虽然现在已经有了WSL 2,但是由于它是基于Hyper-V的,开了之后会和VMware、VirtualBox这些应用冲突,有点蛋疼,所以我还没打算迁移过去。如果你想折腾的话,那么请:Install WSL 2 | Microsoft Docs。)

不管怎样,我都觉得看官方文档比较好:WSL – Help | CLion。其实就是在WSL中安装所需的工具链,然后CLion会通过SSH来调用它们。

首先还是要强调一下你需要使用Windows 10(1607之后)或是Windows Server 2019。更多信息请参阅这里:适用于 Linux 的 Windows 子系统 – 维基百科,自由的百科全书

安装WSL其实很简单,你首先需要打开这个特性。你可以在控制面板中的启用或关闭Windows功能中打开它:

控制面板->程序->启用或关闭Windows功能->适用于Linux的Windows子系统

当然,这和直接在PowerShell中执行如下语句是等效的:

Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux

打开该特性后请务必重启你的电脑,否则并不会生效,后续打开WSL会直接提示The Windows Subsystem for Linux optional component is not enabled. Please enable it and try again.

随后在Windows Store中搜索WSL并安装你需要的Linux发行版:

不得不接受的现实是,目前还是Ubuntu的支持最好。

下面不打算假设你对Linux有多少了解,我只希望你会自己去了解…

首次打开Ubuntu时会让你设定用户名和密码,随后就可以进入系统。我们需要来安装软件包,在这之前,你需要先刷新软件包列表:

sudo apt update

如果成功,那么随后你就可以开始安装软件包了:

sudo apt install cmake make clang gcc g++ build-essential gdb

如果你发现下载软件包的速度慢到难以接受,那么更换软件源可能是最简单粗暴的解决方法。例如你可以到这里查看阿里提供的镜像的文档,照抄不是明智的做法,直接改域名可能更好一点。如果更换软件源,你需要重新执行apt update才会生效,然后再进行安装。

至此工具链应该已经安装好了。前面提到,CLion会通过SSH去连接WSL来调用工具链,默认情况下刚装好的Ubuntu WSL里的SSH Server没有密钥对,直接启动后应该会连接不上。当然,按照报错信息去找到对应地方生成密钥对再重启SSH Server就可以了。不过有个更简单的方法:

sudo apt remove openssh-server
sudo apt install openssh-server

直接删除并重新安装的时候,在安装过程中会自动生成,也就不用自己去折腾了。然后再启动就可以了。

题外话:注意安全

考虑到我们是学安全的社团,所以特意拿出来强调一下这个点:不要让别人连接你的WSL的SSH Server!除非你知道你在做什么。不过我敢说大部分时候大部分人都是不知道自己在做什么的……大部分教程是没有强调这个问题的。简单说来,SSH是用来远程通过命令行控制你的主机的,具体请出门左转wiki,不正确的配置可能会导致被恶意连接,从而造成系统被破坏的后果。SSH Server的配置文件默认应该会在/etc/ssh/sshd_config,默认情况下应该会监听来自所有地址的请求,也就是那个ListenAddress 0.0.0.0。如果你只有在本机连接的需求,那么改为只监听本机可能会是个好主意。别忘了还有IPv6,虽然地址量的庞大使得扫描变得困难,但是只监听本机仍然有助于保护你自己。

只监听本机有助于保护你,除非你有远程连接的需求

修改配置文件后,请记得重启你的SSH Server以使得更改生效。

此外,如果你只是希望依赖防火墙来阻断端口22的入站连接,那么你可能要花费更多的精力来保证防火墙策略和区域设置正确。既然如此,那为啥不一开始就直接设置成仅本机访问呢。

回归正题,如果配置好了之后就可以来启动SSH Server了:

sudo service ssh start

我建议你马上测试一下是否能连接成功:

ssh 你希望登录的用户名@localhost -p22

如果不是很有必要,我不建议你直接root用户登录。

如果出现类似Connection refused的错误信息,那么说明服务可能没有开启,你可以通过类似sudo service ssh status的命令来检查;如果出现类似密钥交换失败的错误,请尝试检查密钥对;如果出现认证失败的错误,你可能需要检查一下配置文件中的认证策略,例如:

不管怎样,这些都应该能在Google上找到答案,我就不多说了。

如果能成功建立连接,那么说明大部分工作已经完成了。你可以回到CLion中,简单设置一下连接信息就可以使用了。你可以从Settings->Build, Execution, Deployment->Toolchains进入,也可以直接两下Shift来Search Everywhere,然后直接搜索Toolchains并进入:

如果检测都正确,那么你应该已经可以开始正常使用CLion了。

如果重启电脑后SSH Server没有启动,那么你可能会看到类似的错误信息:

很简单,启动SSH Server后,再点按Reload CMake Project就可以了:

MinGW,通过MSYS2

在这之前,我建议你先去了解一下MinGW、MinGW-w64还有MSYS2。

这也是个不错的方案(特别是和GUI相关的时候)。首先你要下载并安装MSYS2,根据文档快速开始,这应该没有什么问题。

然后安装工具链:

选好路径就可以开始工作了:

程序员也应当非常重视警告(Warning)。CLion可能会对超出版本范围的工具提示警告,不管是更新还是更旧。这都说明没有经过足够的测试来保证该版本的兼容性。如果你后续在调试阶段出现问题,那么可能要安装合适版本的调试器。

MinGW,通过直接在线/离线安装mingw-w64

你可以前往Mingw-w64 – GCC for Windows 64 & 32 bits [mingw-w64]了解更多信息。然后根据实际情况选择安装方式。如果你希望在没有网络的考试环境部署,那么直接离线安装可能是比较好的方案。

这个比较简单,其实没什么好说的,只要在CLion的工具链选项中指定你的安装/解压后的mingw-w64的路径就可以了。默认在线安装的话,CLion应该可以直接自动检测到。

Cygwin

也是个不错的方案,前往Cygwin查看更多信息。也是类似的下载安装,不过由于Cygwin并没有自带包管理,所以你需要在安装过程中就安装工具链,而没法像MSYS2那样。

记得在这里安装CMake、make、gcc-core、gcc-g++和gdb

其实CLion文档中只要求安装gcc-g++、make和gdb。

同样的,安装成功后在CLion中的工具链选项中配置好就要可以使用了。

小结

对了,不管你使用什么工具链,只要某些路径中涉及了非ASCII字符,例如中文用户名,那么可能会出现各种难以预料的错误。即使你在设置中更改了用户名,个人文件夹也不会改变,就算试图更改注册表也不一定稳妥。你可以使用各种链接的操作来试图避开,不过我觉得还是重新安装系统来得好一点。

调试?

毫无疑问,调试(Debug)在编写程序的过程中几乎是不可缺少的。如果你在上面配置工具链时Debugger已经可以工作,那么就不再需要额外的工作了。而如果你调用了Visual Studio的工具链,那么可能不能启动调试。(至少我上次看文档的时候还是不支持的,不过印象中可以调用其它Debugger的样子)

在CLion中进行调试也非常简单,基本的调试只需要点按编辑器左侧的行数就可以打下断点,然后点按右上角Debug图标启动调试即可。同样,你也可以使用快捷键Ctrl+F8来打断点、Shift+F9来启动调试。

启动调试之后,像Step Over、Step Into、Step Out之类的我觉得你应该自己去了解了。此外,调试不应该只停留在查看变量的阶段。例如,如果你在调试一个递归或者哪怕仅仅只是若干函数调用的程序,那么查看栈帧(Frames)可能会有所帮助。如果有些UI中没有实现的功能,你可以直接在GDB窗口中自行输入命令。

当然,我不希望你错过这个强大的功能:Evaluate Expression…(Alt+F8)

你可以利用这个功能,在当前现场像脚本语言一样执行你所需要的语句,这在很多时候都非常有用

智慧树时间:你可能不知道的IntelliJ/CLion

想到一个写一个吧……

Do not use mouse in IntelliJ, mouse is the evil.

Hadi Hariri

你知道吗?复制(Ctrl+C)、剪切(Ctrl+X)和粘贴(Ctrl+V)整行代码并不需要手动选中,不选中就是选中整行。想要重复一行?直接Ctrl+D吧。

这看起来很简单,对吗?你是否厌倦了还要移动右手去调整方向键才能补全例如ifwhile等语句的花括号?我想你还需要Complete Current Statement(Ctrl+Shift+Enter)。

Before
After

想为一个语句加上if吗?别再手动加括号了,直接对选中的语句使用Surround With…(Ctrl+Alt+T)吧!

什么?你说很多变量名起得不好,想改很麻烦,还怕有副作用?别慌,使用Refactor->Rename(Shift+F6),把这些让人头疼的问题交给IDE去分析就好了。相信我,它会做得又快又好的。

嗯……IntelliJ/CLion已经是一个成熟的IDE了,按下Generate(Alt+Ins)就可以帮你生成代码了。

代码经过几手修改,格式乱七八糟?试试Reformat Code(Ctrl+Alt+L)或许会有所帮助的。

糟糕,我的代码上出现了一个黄色/红色的灯泡。别慌,这说明IDE可能想要帮助你!触发Show Intention Actions(Alt+Enter)来看看吧。说不定,这就是以后你最经常使用的快捷键之一了。

找不到想要的东西吗?Search Everywhere(连续的两下Shift)和Run Anything(连续的两下Ctrl)或许能帮到你。

随便说了几个,更多精彩请自行翻阅每天都会弹出的Tip of the Day。对了,安装Presentation Assistant插件试试吧,我敢打赌你不会后悔的。

别人家的小朋友都有缩略代码的滚动条

很简单,安装CodeGlance插件就可以了。

这个主题天天看,看腻了,有没有猛男专用的颜色

喵?安装Material Theme UI插件看看有没有你需要的?在设置里你想怎么搞颜色都可以。

图标包自5.0.0+后的版本被分离出来了,下载Atom Material Icons就可以了。

Compare with Clipboard

什么?你说你又过不了OJ?它说Wrong Answer的时候,别再自己人工对比了!复制样例数据,回到你的程序的输出窗口,让IDE帮你对比它们吧。

Search with Google

遇到了问题?不要睡大觉,直接问Google吧!直接将选中的文字送到搜索引擎上!

以前在右键菜单里可以直接触发的,刚发现2020.1似乎移除了这个选项。不过我们仍然可以在Settings->Keymap中设置快捷键,或者在Settings->Appearance & Behavior->Menus and Toolbars里重新加回来。

不同Project设置不同的工具链?

你或许曾经有过这样的困扰,为了不同的用途,你安装了多个工具链。但是你发现每次切换项目似乎都要切换工具链?不,你只需要在Settings->Build, Execution, Deployment->CMake中设置工具链就可以了。

拷贝到Word后,行间距有违和的白色?

试着在这段代码前面加上一个回车吧,你会回来感谢我的。

Material Theme UI 更新5.0+后图标包不再有效?

JetBrains全家桶(IntelliJ IDEA、CLion等)开始升级2020.1了,如果你正在使用Material Theme UI插件的话,更新5.0.0+后会发现图标包风格还是原生的样子。并且刚更新后会有小弹窗:

如果去插件介绍页可以看到更新历史中写了:

据此,图标包迁移到了Atom Material Icons – Plugins | JetBrains。可以直接在IDE的插件页中搜索并安装。

De1CTF 2020 推文

好吧有点破事水……想了想还是把标题改了

ERQKGDWRP3SBHQQ3MTGBIYAZE4KJG5M3HCKRERQCIYWRP2KBHTGBGDMZE4KBH5M3GDABERQJIBWRP2IBH%D3%D3%D3YXNDBDJDATRYMHYUYC6VYDOGNDRVUSIYBDQ3YXNFMDJDAENYMC6WVDOGNDMRESIYVDM3YXNZNDJDATNQM3%D3%D3%D

大佬

5Yir6Zeu5oiRIOiHquW3seeci+WbvueJhw==

XCTF小秘

简单签到MISC,随便写了一个:

import com.google.common.io.BaseEncoding
import org.apache.commons.text.StringEscapeUtils
import java.net.URLDecoder
import java.nio.charset.StandardCharsets

fun main() {
    val str1First = "ERQKGDWRP3SBHQQ3MTGBIYAZE4KJG5M3HCKRERQCIYWRP2KBHTGBGDMZE4KBH5M3GDABERQJIBWRP2IBH%D3%D3%D3"
    val str1Second = "YXNDBDJDATRYMHYUYC6VYDOGNDRVUSIYBDQ3YXNFMDJDAENYMC6WVDOGNDMRESIYVDM3YXNZNDJDATNQM3%D3%D3%D"
    val str1 = buildString {
        str1First.zip(str1Second).forEach { (first, second) ->
            append(first).append(second)
        }
    }
    val str1UrlDecoded = URLDecoder.decode(str1, "UTF-8")
    println("Step1: $str1UrlDecoded")
    val str1Base32Decoded = String(BaseEncoding.base32().decode(str1UrlDecoded), StandardCharsets.UTF_8)
    println("Step2: $str1Base32Decoded")
    val str1UnicodeUnescaped = StringEscapeUtils.unescapeHtml4(str1Base32Decoded)
    println("Step3: $str1UnicodeUnescaped")
    val str2 = "5Yir6Zeu5oiRIOiHquW3seeci+WbvueJhw=="
    println(String(BaseEncoding.base64().decode(str2), StandardCharsets.UTF_8))
}

为了混合两个字符串,Kotlin标准库的zip还是挺有意思的。

关于Unicode编码,找到了这个:Unicode Escape Formats,所以形如&#x0000;的转为字符串就直接StringEscapeUtils.unescapeHtml4()处理。

引用:
arraylist – Merge two strings in kotlin – Stack Overflow

Docker 容器内连接宿主机localhost

昨天有点特殊需求,需要整一个frp(frpc)的容器。这个东西肯定要去连公网的frps,接受外来的请求后转发给内网。然后发现要转发到内网还好,但是要转发到宿主机的localhost时候就有点问题了,翻了一下文档和问答,大概是这样的:

对于Linux上的Docker,可以直接在docker run时指定:

--network="host"

或者在docker-compose.yml中:

network_mode: "host"

这种方式挺适合现在这个应用场景的。

如果是默认的网桥模式,那么可以在容器中访问宿主机对应docker网桥的IP地址。对于Windows或是Mac的Docker(版本18.03之后,据引用中的链接),可以直接连接主机名host.docker.internal

引用:
nginx – From inside of a Docker container, how do I connect to the localhost of the machine? – Stack Overflow
Use host networking | Docker Documentation
Compose file version 3 reference | Docker Documentation
Networking in Compose | Docker Documentation

分数四则运算(结构)

时间限制: 1 Sec 内存限制: 128 MB

题目描述

分数的分子和分母可用一个结构类型来表示。
编写实现两个分数加(addFS),减(subFS),乘(mulFS),除(divFS)的函数(要求计算结果分数是简化的),以及打印一个分数(printFS),计算两个整数最大公约数的函数(getGCD)。
注意:不能定义全局变量

输入

测试数据的组数 t
第一组第一个分数
第一组第二个分数
第二组第一个分数
第二组第二个分数
……

输出

第一组两个分数的和
第一组两个分数的差
第一组两个分数的积
第一组两个分数的商
第二组两个分数的和
第二组两个分数的差
第二组两个分数的积
第二组两个分数的商
……

样例输入

3
1/2
2/3
3/4
5/8
21/23
8/13

样例输出

7/6
-1/6
1/3
3/4

11/8
1/8
15/32
6/5

457/299
89/299
168/299
273/184

提示

求两数a、b的最大公约数可采用辗转相除法,又称欧几里得算法,其步骤为:

  1. 交换a, b使a > b;
  2. 用a除b得到余数r,若r=0,则b为最大公约数,退出;
  3. 若r不为0,则用b代替a, r代替b,此时a,b都比上一次的小,问题规模缩小了;
  4. 继续第2步。

解决方案

想想化简部分还是可以抽成一个inline函数的,但是懒得改了。

#include <iostream>

class Fraction {
public:
    Fraction() = default;

    Fraction(int numerator, int denominator) : numerator(numerator), denominator(denominator) {}

    Fraction operator+(const Fraction &rhs) {
        int numerator_tmp = this->numerator * rhs.denominator + rhs.numerator * this->denominator;
        int denominator_tmp = this->denominator * rhs.denominator;
        int gcd_tmp = gcd(numerator_tmp, denominator_tmp);
        return {numerator_tmp / gcd_tmp, denominator_tmp / gcd_tmp};
    }

    Fraction operator-(const Fraction &rhs) {
        return (*this + Fraction(-rhs.numerator, rhs.denominator));
    }

    Fraction operator*(const Fraction &rhs) {
        int numerator_tmp = this->numerator * rhs.numerator;
        int denominator_tmp = this->denominator * rhs.denominator;
        int gcd_tmp = gcd(numerator_tmp, denominator_tmp);
        return {numerator_tmp / gcd_tmp, denominator_tmp / gcd_tmp};
    }

    Fraction operator/(const Fraction &rhs) {
        int numerator_tmp = rhs.denominator, denominator_tmp = rhs.numerator;
        return (*this * Fraction(numerator_tmp, denominator_tmp));
    }

    friend std::istream &operator>>(std::istream &is, Fraction &rhs) {
        is >> rhs.numerator;
        is.get();
        is >> rhs.denominator;
        return is;
    }

    friend std::ostream &operator<<(std::ostream &os, const Fraction &rhs) {
        if (rhs.denominator < 0) {
            return os << -rhs.numerator << '/' << -rhs.denominator;
        } else {
            return os << rhs.numerator << '/' << rhs.denominator;
        }
    }

private:
    int numerator, denominator;

    static int gcd(int lhs, int rhs) {
        int tmp;
        while (rhs != 0) {
            tmp = rhs;
            rhs = lhs % rhs;
            lhs = tmp;
        }
        return lhs;
    }
};

int main() {
    size_t T;
    std::cin >> T;
    while (T--) {
        Fraction a{}, b{};
        std::cin >> a >> b;
        std::cout << a + b << std::endl
                  << a - b << std::endl
                  << a * b << std::endl
                  << a / b << std::endl
                  << std::endl;
    }
    return 0;
}

扑克牌排序(结构体)

时间限制: 1 Sec 内存限制: 128 MB

题目描述

自定义结构体表示一张扑克牌,包含类型——黑桃、红桃、梅花、方块、王;大小——2,3,4,5,6,7,8,9,10,J,Q,K,A,小王(用0表示)、大王(用1表示)。输入n,输入n张扑克牌信息,从大到小输出它们的排序结果。
假设扑克牌的排序规则是大王、小王为第一大、第二大,剩余52张扑克牌按照先花色后大小排序。
花色:黑桃>红桃>梅花>方块。
大小: A>K>Q>J>>10>9>…>2。
提示:百度sort函数、strstr函数使用。

输入

测试次数t
每组测试数据两行:
第一行:n,表示输入n张扑克牌
第二行:n张扑克牌信息,格式见样例

输出

对每组测试数据,输出从大到小的排序结果

样例输入

3
5
黑桃4 红桃10 梅花Q 方块K 黑桃A
10
大王 梅花10 红桃K 方块9 黑桃2 梅花A 方块Q 小王 黑桃8 黑桃J
5
红桃K 梅花K 黑桃K 方块K 小王

样例输出

黑桃A 黑桃4 红桃10 梅花Q 方块K
大王 小王 黑桃J 黑桃8 黑桃2 红桃K 梅花A 梅花10 方块Q 方块9
小王 黑桃K 红桃K 梅花K 方块K

提示

解决方案

突然发现我之前发过这题的题解…是我很久以前写的了好像,有兴趣看的话可以直接站内搜索。

如果你试图初始化一个静态的例如某些全局变量,那么可能会得到Clang-Tidy的警告,像这样:Initialization of 'variable_name' with static storage duration may throw an exception that cannot be caught。我不建议无视这个警告。解决方法可以是用一个静态的getter函数来包围这个变量,使得该变量的初始化时机从以前的main()函数执行前延迟到首次调用这个getter函数。下面的get_num_size_map()算是一个小例子,更多信息请访问引用链接。

#include <iostream>
#include <algorithm>
#include <iterator>
#include <map>
#include <sstream>
#include <string>
#include <vector>
#include <cassert>

struct Poker {
    explicit Poker(const std::string &raw) {
        if (raw.find("黑桃") != std::string::npos) {
            this->suit = "黑桃";
        } else if (raw.find("红桃") != std::string::npos) {
            this->suit = "红桃";
        } else if (raw.find("梅花") != std::string::npos) {
            this->suit = "梅花";
        } else if (raw.find("方块") != std::string::npos) {
            this->suit = "方块";
        } else if (raw.find("小王") != std::string::npos) {
            this->suit = "小王";
            this->num = "0";
            return;
        } else {
            this->suit = "大王";
            this->num = "1";
            return;
        }
        if (raw.find("10") == std::string::npos) {
            this->num = raw.back();
        } else {
            this->num = "10";
        }
    }

    size_t size() const {
        size_t size = 0;
        size += get_suit_size_map()[this->suit];
        size += get_num_size_map()[this->num];
        return size;
    }

    bool operator<(const Poker &rhs) {
        return this->size() < rhs.size();
    }

    friend std::ostream &operator<<(std::ostream &os, const Poker &rhs) {
        os << rhs.suit;
        if (!(rhs.num == "0" || rhs.num == "1")) {
            os << rhs.num;
        }
        return os;
    }

    std::string suit, num;
private:
    static std::map<std::string, int> get_suit_size_map() {
        static const std::map<std::string, int> suit_size = {
                {"大王", 5 * 13}, {"小王", 4 * 13},
                {"黑桃", 3 * 13}, {"红桃", 2 * 13},
                {"梅花", 1 * 13}, {"方块", 0 * 13},
        };
        return suit_size;
    }

    static std::map<std::string, int> get_num_size_map() {
        static const std::map<std::string, int> num_size = {
                {"2",  0}, {"3",  1}, {"4",  2}, {"5",  3},
                {"6",  4}, {"7",  5}, {"8",  6}, {"9",  7},
                {"10", 8}, {"J",  9}, {"Q",  10}, {"K",  11},
                {"A",  12}, {"0",  0}, {"1",  0},
        };
        return num_size;
    }
};

int main() {
    size_t T;
    std::cin >> T;
    while (T--) {
        size_t size;
        std::cin >> size;
        if (std::cin.get() == '\r') { std::cin.get(); }
        std::string line;
        std::getline(std::cin, line);
        std::istringstream iss(line);
        std::vector<Poker> pokers{std::istream_iterator<std::string>{iss}, std::istream_iterator<std::string>{}};
        assert(!pokers.empty());
        std::sort(pokers.begin(), pokers.end(), [](const Poker &lhs, const Poker &rhs) { return lhs.size() > rhs.size(); });
        std::cout << pokers.front();
        for (size_t i = 1; i < pokers.size(); ++i) {
            std::cout << ' ' << pokers[i];
        }
        std::cout << std::endl;
    }
    return 0;
}

引用:
c++ – How do I iterate over the words of a string? – Stack Overflow
Initialization – cppreference.com
What is this warning in C++: initialization of ‘-‘ with static storage duration may throw an exception that cannot be caught? – Quora
c++ – How to catch exception thrown while initializing a static member – Stack Overflow
c++ – How to deal with static storage duration warnings? – Stack Overflow

访问数组元素(引用)

时间限制: 1 Sec 内存限制: 128 MB

题目描述

输入n,输入n个数,计算n个数的和并输出。
假设主函数定义如下,不可修改。请补齐put函数。

输入

测试次数
每组测试数据一行,正整数n(1~1000),后跟n个整数。

输出

每组测试数据输出一行,即n个整数的和。

样例输入

3
4 10 20 30 40
10 1 2 3 -1 -2 -3 1 2 3 4
5 0 0 0 0 10

样例输出

sum=100
sum=10
sum=10

提示

解决方案

不明白这种混杂着C89和C99还有C++风格的代码有什么理由不让人改

#include <iostream>

const size_t N = 1000;

int &put(int *array, size_t size) {
    return *(array + size);
}

int main() {
    size_t T;
    std::cin >> T;
    while (T--) {
        int array[N]{};
        size_t size;
        std::cin >> size;
        for (size_t i = 0; i < size; ++i) {
            std::cin >> put(array, i);
        }
        int sum = 0;
        for (size_t i = 0; i < size; ++i) {
            sum += array[i];
        }
        std::cout << "sum=" << sum << std::endl;
    }
    return 0;
}

求最大值最小值(引用)

时间限制: 1 Sec 内存限制: 128 MB

题目描述

编写函数void find(int *num,int n,int &minIndex,int &maxIndex),求数组num(元素为num[0],num[1],…,num[n-1])中取最小值、最大值的元素下标minIndex,maxIndex(若有相同最值,取第一个出现的下标。)
输入n,动态分配n个整数空间,输入n个整数,调用该函数求数组的最小值、最大值下标。最后按样例格式输出结果。
改变函数find功能不计分。

输入

测试次数
每组测试数据一行:数据个数n,后跟n个整数

输出

每组测试数据输出两行,分别是最小值、最大值及其下标。具体格式见样例。多组测试数据之间以空行分隔。

样例输入

2
5 10 20 40 -100 0
10 23 12 -32 4 6 230 100 90 -120 15

样例输出

min=-100 minindex=3
max=40 maxindex=2

min=-120 minindex=8
max=230 maxindex=5

提示

解决方案

#include <iostream>

void find(const int *array, size_t size, size_t &min_index, size_t &max_index) {
    for (size_t i = 0; i < size; ++i) {
        if (array[min_index] > array[i]) {
            min_index = i;
        }
        if (array[max_index] < array[i]) {
            max_index = i;
        }
    }
}

int main() {
    size_t T;
    std::cin >> T;
    while (T--) {
        size_t size;
        std::cin >> size;
        int *array = new int[size];
        for (size_t i = 0; i < size; ++i) {
            std::cin >> array[i];
        }
        size_t min_index = 0, max_index = 0;
        find(array, size, min_index, max_index);
        std::cout << "min=" << array[min_index] << " minindex=" << min_index << std::endl
                  << "max=" << array[max_index] << " maxindex=" << max_index << std::endl
                  << std::endl;
    }
    return 0;
}