Caesar

凯撒密码

要解决的问题

据说,凯撒(没错,就是那位凯撒)曾经通过把消息中的每个字母移动若干位来“加密”(也就是以可逆方式隐藏)机密信息。例如,他可能把 一份 写成 B,把 B 写成 C,把 C 写成 D,……,并在字母表末尾回绕,把 Z 写成 A。因此,如果要向某人说 HELLO,凯撒可能会写成 IFMMP。收到这类消息的人必须把字母按相同位数反方向移动,才能“解密”。

这个“密码系统”的保密性依赖于只有凯撒和接收者知道一个秘密:字母被移动了多少位(例如 1 位)。按现代标准看,这并不特别安全;不过,如果你可能是世界上第一个这么做的人,那也算相当安全了。

未加密的文本通常称为明文。加密后的文本通常称为密文。所使用的秘密则称为密钥

更明确地说,用密钥 1 加密 HELLO 会得到 IFMMP,过程如下:

明文 H E L L O
+ 密钥 1 1 1 1 1
= 密文 I F M M P

更正式地说,凯撒算法(也就是这种密码)会把每个字母“旋转” k 个位置来加密消息。设 p 为某段明文(即未加密的消息),pip 中的第 i 个字符,k 为秘密密钥(即非负整数),则密文 c 中的每个字母 ci 可按如下方式计算:

ci = (pi + k) % 26

其中 % 26 表示“除以 26 后的余数”。这个公式可能让密码看起来比实际更复杂,但它其实只是用简洁的方式精确表达算法。为了便于讨论,可以把 A(或 a)看作 0,B(或 b)看作 1,……,H(或 h)看作 7,I(或 i)看作 8,……,Z(或 z)看作 25。假设 Caesar 这次只想用密钥 k = 3 私密地向某人发送 Hi。那么他的明文 pHi,其中第一个字符 p0H(也就是 7),第二个字符 p1i(也就是 8)。因此密文的第一个字符 c0K,第二个字符 c1L。明白了吗?

请在名为 caesar 的文件夹中创建 caesar.c 文件,并编写一个程序,使用凯撒密码加密消息。用户运行程序时,应通过命令行参数指定密钥;运行后再输入需要加密的秘密消息。你不应假设用户提供的密钥一定是数字;不过,如果它确实是数字,你可以假设它是正整数。

演示

规格说明

设计并实现一个名为 caesar 的程序,使用凯撒密码加密消息。

  • 在名为 caesar 的目录中,用名为 caesar.c 的文件实现你的程序。
  • 你的程序必须接受一个命令行参数:一个非负整数。为了便于讨论,我们把它称为 k
  • 如果程序运行时没有命令行参数,或命令行参数多于一个,程序应立即用 printf 输出你自选的错误消息,并让 main 返回 1(通常表示错误)。
  • 如果命令行参数中的任意字符不是十进制数字,程序应输出消息 Usage: ./caesar key,并让 main 返回 1
  • 不要假设 k 小于或等于 26。你的程序应适用于所有小于 231 - 26 的非负整数 k。换句话说,如果用户选择的 k 大到或接近大到无法放进 int,你不必担心程序最终会出问题。(记住,int 可能溢出。)不过,即使 k 大于 26,输入中的字母字符在输出中仍应保持为字母字符。例如,如果 k 是 27,A 不应变成 \,尽管根据 asciitable.com\ 在 ASCII 中距离 A 有 27 个位置;在从 Z 回绕到 A 的前提下,A 应该变成 B
  • 你的程序必须输出 plaintext: (两个空格,不带换行),然后使用 get_string 提示用户输入明文字符串。
  • 你的程序必须输出 ciphertext: (一个空格,不带换行),随后输出明文对应的密文:明文中的每个字母字符都要“旋转” k 个位置,非字母字符保持不变。
  • 你的程序必须保留大小写:大写字母旋转后仍为大写字母;小写字母旋转后仍为小写字母。
  • 输出密文后,应打印一个换行。随后程序应让 main 返回 0 并退出。

建议

该如何开始?让我们一步一步解决这个问题。

伪代码

先尝试在 caesar.c 中写出一个 main 函数,只用伪代码描述程序流程,即使你暂时还不确定如何写成真正的 C 代码也没关系。

提示

实现方式不止一种,下面只是其中一种!

int main(int argc, string argv[])
{
    // Make sure program was run with just one 命令行参数

    // Make sure every character in argv[1] is a digit

    // Convert argv[1] from a `string` to an `int`

    // Prompt user for plaintext

    // For each character in the plaintext:

        // Rotate the character if it's a letter
}

看过这里的伪代码后,可以修改你自己的伪代码,但不要简单地把我们的复制粘贴过去!

统计命令行参数

无论你的伪代码如何,在加入其他功能之前,我们先只写 C 代码来检查程序是否只带了一个命令行参数运行。

具体来说,修改 caesar.c 中的 main:如果用户没有提供命令行参数,或提供了两个及以上参数,函数应打印 "Usage: ./caesar key\n",然后返回 1,也就是退出程序。如果用户恰好提供一个命令行参数,程序应什么也不打印,直接返回 0。因此程序应表现如下。

$ ./caesar
Usage: ./caesar key
$ ./caesar 1 2 3
Usage: ./caesar key
$ ./caesar 1
提示
  • 记住,可以用 printf 打印内容。
  • 记住,函数可以用 return 返回一个值。
  • 记住,argc 包含传给程序的命令行参数数量,再加上程序自身的名称。

检查密钥

现在你的程序(希望如此!)已经能按要求接受输入了,可以继续下一步。

caesar.c 中、main 下方,添加一个函数,例如命名为 only_digits。它接受一个 string 参数;如果该字符串只包含 09 的数字,则返回 true,否则返回 false。也别忘了在 main 上方添加函数原型。

提示
  • 你很可能会需要像这样的函数原型:
    bool only_digits(string s);
    

    并且要在文件顶部包含 cs50.h,这样编译器才能识别 string(以及 bool)。

  • 记住,string 本质上只是一个 char 数组。
  • 记住,声明在 string.h 中的 strlen 可以计算 string 的长度。
  • 根据 manual.cs50.io,声明在 ctype.h 中的 isdigit 可能会有帮助。但请注意,它一次只能检查一个 char

然后修改 main,让它对 argv[1] 调用 only_digits。如果该函数返回 false,那么 main 应打印 "Usage: ./caesar key\n" 并返回 1。否则,main 应直接返回 0。程序应表现如下:

$ ./caesar 42
$ ./caesar banana
Usage: ./caesar key

使用密钥

现在修改 main,让它把 argv[1] 转换为 int。根据 manual.cs50.io,声明在 stdlib.h 中的 atoi 可能会有帮助。然后使用 get_string,以 "plaintext: " 提示用户输入明文。

接着,实现一个函数,例如命名为 rotate。它接受一个 char 和一个 int 作为输入;如果该 char 是字母,就将它旋转相应数量的位置,并在需要时从 Z 回绕到 A(以及从 z 回绕到 a)。如果该 char 不是字母,函数应原样返回它。

提示
  • 你很可能会需要像这样的函数原型:
    char rotate(char c, int n);
    

    像下面这样的函数调用

    rotate('A', 1)
    

    甚至

    rotate('A', 27)
    

    都应返回 'B'。而像下面这样的函数调用

    rotate('!', 13)
    

    应返回 '!'

  • 记住,可以用 (int) 显式地把 char“转换”为 int,也可以用 (char)int 转回 char。也可以通过把其中一种类型当作另一种使用来隐式完成转换。
  • 你很可能需要从任意大写字母中减去 'A' 的 ASCII 值,这样在做算术时就能把 'A' 当作 0'B' 当作 1,以此类推。计算完成后再把它加回来。
  • 你很可能也需要从任意小写字母中减去 'a' 的 ASCII 值,这样在做算术时就能把 'a' 当作 0'b' 当作 1,以此类推。计算完成后再把它加回来。
  • 根据 manual.cs50.io,声明在 ctype.h 中的其他函数也可能有帮助。
  • 当你需要在算术上从类似 25 的值“回绕”到 0 时,% 很可能会很有用。

然后修改 main,让它先打印 "ciphertext: ",再遍历用户明文中的每个 char,对每个字符调用 rotate,并打印其返回值。

提示
  • 记住,printf 可以用 %c 打印 char
  • 如果调用 printf 时完全看不到任何输出,很可能是因为你正在打印有效 ASCII 范围 0 到 127 之外的字符。可以临时把字符当作数字打印(用 %i 而不是 %c),看看你实际打印的值是什么!

讲解视频

如何测试

正确性

check50 cs50/problems/2026/x/caesar
如何使用 debug50

想运行 debug50 吗?在用 make 成功编译代码后,可以这样做:

debug50 ./caesar KEY

其中 KEY 是你作为命令行参数传给程序的密钥。注意,运行

debug50 ./caesar

理想情况下会让你的程序最后提示用户输入密钥。

代码风格

style50 caesar.c

如何提交

在终端中执行以下命令提交你的作业,并按提示完成操作。

submit50 cs50/problems/2026/x/caesar