ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C语言实现棋盘加密与凯撒加密:古典密码算法详解与代码示例

C语言实现棋盘加密与凯撒加密:古典密码算法详解与代码示例 简介棋盘加密与凯撒加密解密C代码是一份面向密码学初学者及C语言学习者的古典加密算法实现资源围绕棋盘Playfair加密与凯撒移位密码两种经典方法演示了从明文字符处理到密文输出的完整流程能帮助入门读者快速理解替换与移位的核心逻辑。压缩包仅2KB共含2个文件一个C源文件qipan.cpp和一个txt说明文件代码结构简单便于直接阅读和运行。目前已有2154人学习浏览对于入门级加密算法实践具有较高参考价值。资源通过数组、循环和条件语句实现字符替换与移位逻辑同时文本说明中对两种算法的历史背景、排列规则和实现细节做了梳理读者既能借此理解古典密码学的核心思想也能提升C语言编程调试能力。适合在密码学课程、课后实验或自学练习时作为配套参考也为后续学习更复杂的现代密码体制打下了基础。 棋盘加密和凯撒加密是古典密码学里最典型的两个算法一个靠查表换字一个靠字母移位原理都不复杂但特别适合用来练手 C 语言的基本功。我在给学生讲字符串处理、数组下标运算和文件读写的时候经常拿这两个算法当入门案例因为它们能把数据变换、字符操作、模块化设计这几个核心点全部串起来。这篇就用 C 代码把棋盘加密Polybius 方格和凯撒加密完整实现一遍代码可以直接编译运行也会把背后的设计思路和踩坑点讲清楚。如果你正在学 C 语言或者对古典密码感兴趣又或者单纯想找几个能写进简历的小项目这篇内容应该正合适。1. 内容整体设计与思路拆解1.1 为什么把两个算法放在一起做这两个算法放在一起不是随便拼凑的它们代表了古典密码的两种完全不同的变换思路。凯撒加密属于替换密码里的移位密码它保持字母的相对顺序不变只是把每个字母在字母表上整体平移若干位解密就是把平移的方向反过来。棋盘加密则属于查表替换它把字母映射到方格坐标上把一个字母变成两个数字解密的时候再用坐标反查字母。两种算法的数据结构天然不同凯撒加密处理的是连续的字符流用取模运算就够了棋盘加密需要先构造一个二维的查表结构这正好对应 C 语言里的二维数组。把这两个算法放在一起实现就能覆盖数组遍历、取模运算、字符串复制、边界检查这几个 C 语言核心技能点。从代码结构上看我给两个算法都封装成独立的加密函数和解密函数输入输出全部约定为字符串。这样设计的好处是接口清晰主程序只要调函数就行不用关心内部实现细节。实际写代码的时候这种模块化思路特别重要因为后续如果要扩展成文件加解密、添加密码反馈模式只要在函数外面再套一层就行核心算法不用动。1.2 需求拆解与实现目标在动手写代码之前我先把需求拆清楚。输入是一个明文字符串输出是密文字符串。凯撒加密需要指定一个密钥也就是偏移量取值范围是 0 到 25超出这个范围要先取模规范化。棋盘加密需要把字母映射到 5x5 的方格里为了凑满 25 个格子通常把 I 和 J 合并成一个格子或者把 C 和 K 合并具体选用哪种规则要提前定好否则解密的时候会乱。我定的实现目标是这样的两个算法都支持大写英文字母输入遇到非字母字符时按原样保留这样能保证空格、标点、数字在加解密过程中不丢失。这个设计决策很关键因为真实场景里的文本基本都带标点如果算法把非字母字符直接丢掉解密出来的内容就没法看了。棋盘加密的密文输出为数字序列每两个数字之间用空格分隔方便阅读。凯撒加密的密文保持字母格式不变只是字母做了移位。1.3 方案选型对比数组查表 vs 公式推导棋盘加密有几种实现方式。第一种是直接开一个 5x5 的二维数组存储字母表加密时遍历数组找字母所在的位置解密时直接按下标取数组元素。第二种是不用数组用公式算坐标比如把字母减去 A 再除以 5 得到行号取余 5 得到列号。这两种方案各有利弊。用数组查表代码可读性好而且如果想把棋盘换成自定义密钥表只需要改数组内容就行不用动算法逻辑。用公式算速度快省内存但灵活性差一旦换表中字母顺序公式就得重新推导。我最终选择的是数组查表方案这更贴合教学场景。代码里构造了经典的 Polybius 方格把 I 和 J 合并到同一个格子里也就是经典的 5x5 表。加密的时候先判断字母是大写还是小写统一转成大写再查表。这样实现出来的代码逻辑直接初学者看着不晕我自己回头维护也省事。2. 核心细节解析与实操要点2.1 凯撒加密的数学原理与 C 语言映射凯撒加密的数学表达式不复杂加密公式是c (p k) % 26解密公式是p (c - k 26) % 26。这里的 p 和 c 都是 0 到 25 之间的整数对应 A 到 Z。C 语言里的char类型本质上是整数所以可以直接做加减法。关键点在于把字符转换成 0 到 25 的数字p ch - A。转换完之后做运算最后再ch result A转回字符。有个细节很多人第一次写的时候会踩坑就是解密公式里为什么要加 26。C 语言里负数取余的结果是负数比如-3 % 26在 C 语言里结果是 -3而不是正数 23。如果直接拿这个结果去加 A那就要出问题因为 A 前面的字符不是字母表里的内容。所以解密的时候先加上 26 再取余保证被除数为正数。这个坑我在调试的时候遇到过输出乱码查了半天后来才想起 C 语言的取余特性。另一个细节是大小写处理。我的方案是先把输入统一转成大写再处理这样代码简单逻辑也清晰。如果你想保留大小写格式也可以加一个islower判断分别处理两套字母表就行但核心算法都一样只是基址从 A 换成 a。2.2 棋盘加密的 Polybius 方格构造棋盘加密又叫 Polybius 方格是古希腊历史学家 Polybius 设计的。它把 26 个英文字母塞进一个 5x5 的表格里每格放一个字母但 25 个格子放不下 26 个字母所以必须有两个字母共用一个格子。最通用的做法是 I 和 J 合并因为这两个字母在很多欧洲语言里发音接近合并后影响不大。我构造的方格如下A B C D E F G H I/J K L M N O P Q R S T U V W X Y Z对应的 C 语言定义就是一个二维字符数组char board[5][5] { {A, B, C, D, E}, {F, G, H, I, K}, {L, M, N, O, P}, {Q, R, S, T, U}, {V, W, X, Y, Z} };注意第二行第五列我放的是 K因为 I 被用来代表 I 和 J 的合并格。加密的时候如果遇到 J我会先把它当成 I 处理。解密的时候查到这个格子输出的字符是 I。这个规则我注释在代码里了不然过几个月自己回来看代码都会疑惑为什么 J 输出的不是 J。2.3 字符串缓冲区与结束符的约定C 语言处理字符串最大的坑是缓冲区溢出和\0结束符。我在设计加密函数的时候就约定输入输出都是标准 C 字符串也就是以\0结尾。加密函数内部输出的密文字符串要保证以\0结尾调用者负责提供足够大的缓冲区。棋盘加密有个特殊的地方一个明文字母变成两个数字字符密文长度要翻倍。比如输入是 100 个字母那输出至少需要 200 个字符加一个结束符如果每个数字间再隔一个空格那还要再加 100 个。我提供的示例代码里直接开了一个 4096 字节的缓冲区属于偷懒做法演示够用。但如果要做成正式的工具一定要根据输入长度动态分配内存或者要求调用者传入缓冲区大小这是写生产级代码必须考虑的问题。3. 实操过程与核心环节实现3.1 完整可直接运行的 C 代码我直接贴出一个能跑的完整 C 程序包含了棋盘加密和凯撒加密的加解密函数以及一个简单的菜单交互示例。代码里我加了大量注释方便你理解每一段在干什么。#include stdio.h #include string.h #include ctype.h #define MAX_LEN 4096 // Polybius 方格I/J 合并 static char board[5][5] { {A, B, C, D, E}, {F, G, H, I, K}, {L, M, N, O, P}, {Q, R, S, T, U}, {V, W, X, Y, Z} }; // 在棋盘中查找字符找到则返回行号和列号 // 返回1表示找到返回0表示没有找到 static int board_lookup(char ch, int *row, int *col) { for (int i 0; i 5; i) { for (int j 0; j 5; j) { if (board[i][j] ch) { *row i; *col j; return 1; } } } return 0; } // 凯撒加密 void caesar_encrypt(const char *plaintext, char *ciphertext, int key) { key ((key % 26) 26) % 26; int i 0; while (plaintext[i] ! \0) { char ch plaintext[i]; if (ch a ch z) { ch ch - a A; } if (ch A ch Z) { ch (ch - A key) % 26 A; } ciphertext[i] ch; i; } ciphertext[i] \0; } // 凯撒解密 void caesar_decrypt(const char *ciphertext, char *plaintext, int key) { key ((key % 26) 26) % 26; int i 0; while (ciphertext[i] ! \0) { char ch ciphertext[i]; if (ch a ch z) { ch ch - a A; } if (ch A ch Z) { ch (ch - A - key 26) % 26 A; } plaintext[i] ch; i; } plaintext[i] \0; } // 棋盘加密 void polybius_encrypt(const char *plaintext, char *ciphertext) { int i 0; int pos 0; while (plaintext[i] ! \0) { char ch plaintext[i]; if (ch a ch z) { ch ch - a A; } int row, col; if (board_lookup(ch, row, col)) { ciphertext[pos] 1 row; ciphertext[pos] 1 col; ciphertext[pos] ; } else { // 非字母字符直接保留 ciphertext[pos] ch; } i; } ciphertext[pos] \0; } // 棋盘解密 void polybius_decrypt(const char *ciphertext, char *plaintext) { int i 0; int pos 0; while (ciphertext[i] ! \0) { if (ciphertext[i] 1 ciphertext[i] 5) { int row ciphertext[i] - 1; int col ciphertext[i 1] - 1; plaintext[pos] board[row][col]; i 2; } else if (ciphertext[i] ) { i; } else { plaintext[pos] ciphertext[i]; i; } } plaintext[pos] \0; } // 简单测试主程序 int main(void) { char plaintext[MAX_LEN]; char result[MAX_LEN]; printf( 棋盘加密 (Polybius Cipher) \n); printf(请输入明文: ); fgets(plaintext, MAX_LEN, stdin); // 去掉 fgets 带入末尾的换行 plaintext[strcspn(plaintext, \n)] \0; polybius_encrypt(plaintext, result); printf(棋盘加密密文: %s\n, result); polybius_decrypt(result, plaintext); printf(棋盘解密密文: %s\n, plaintext); printf(\n 凯撒加密 (Caesar Cipher) \n); printf(请输入明文: ); fgets(plaintext, MAX_LEN, stdin); plaintext[strcspn(plaintext, \n)] \0; printf(请输入密钥(0-25): ); int key; scanf(%d, key); getchar(); // 吃掉残留的换行 caesar_encrypt(plaintext, result, key); printf(凯撒加密密文: %s\n, result); caesar_decrypt(result, plaintext, key); printf(凯撒解密密文: %s\n, plaintext); return 0; }这段代码在 GCC 环境下直接编译就能跑Windows 上用 Dev-C 或者 VS Code 配好 MinGW 也没问题。编译命令很简单gcc -o cipher cipher.c如果代码文件名存成cipher.c编译后运行./cipherWindows 下是cipher.exe就能看到交互菜单。3.2 代码里的关键设计index 如何映射到棋盘坐标棋盘加密里最容易绕晕的就是行号和列号的编号方式。我在代码里用1 row和1 col把 0 到 4 的行列索引转成字符 1 到 5这样输出的密文看起来更直观。比如字母 A 在第一行第一列加密结果就是11字母 B 是12字母 Z 在第五行第五列加密结果是55。解密的时候我用ciphertext[i] - 1把字符转回整数下标然后去二维数组里取对应的字母。这种来回转换的操作本质上就是 ASCII 码运算也是 C 语言里处理字符型数据的常用套路。有一点值得注意解密函数遇到两个连续数字字符比如11时我默认它们是一组坐标直接取ciphertext[i]和ciphertext[i 1]。如果密文格式不规范比如只有一个数字访问ciphertext[i 1]可能越界。这属于边界条件老师写代码可以偷懒忽略但如果是你自己做项目必须加上长度判断。我在后面的常见问题部分会展开讲这个坑。3.3 用实际输入跑一遍完整流程我拿一句实际文本HELLO WORLD来测试代码。这句话有 10 个字母加一个空格用来验证非字母字符的保留逻辑。棋盘加密流程是这样的H 在棋盘第二行第三列加密成23E 在第一行第五列加密成15L 在第三行第二列加密成32第二个 L 同样加密成32O 在第三行第四列加密成34。空格是非字母字符按照代码逻辑直接保留原样。W 在第五行第二列加密成52O 加密成34R 加密成43L 加密成32D 加密成14。所以 HELLO WORLD 的棋盘加密结果是23 15 32 32 34 52 34 43 32 14注意两个空格之间原本的单词空格也保留了一个。凯撒加密如果密钥选 3就是经典的 “Shift by 3” 的凯撒密码。HELLO 变成 KHOOR中间的规则很简单H 加 3 是 KE 加 3 是 HL 加 3 是 OL 加 3 是 OO 加 3 是 R。WO RLD 里的 W 加 3 变 ZO 变 RR 变 UL 变 OD 变 G。所以密文是KHOOR ZRUOG。解密的时候每个字母往前移 3 位就能还原。3.4 文件读写场景下的算法改造思路这两个算法封装成函数之后可以直接用在文件加解密场景里。基本思路是打开文件逐行读取调用加密函数处理后写回新文件。这样文本类文件的加解密就完成了。我用 C 标准库示范一下关键代码段方便你参考FILE *fin fopen(input.txt, r); FILE *fout fopen(output.txt, w); char line[MAX_LEN]; char enc[MAX_LEN * 2]; while (fgets(line, MAX_LEN, fin) ! NULL) { line[strcspn(line, \n)] \0; caesar_encrypt(line, enc, 3); fprintf(fout, %s\n, enc); } fclose(fin); fclose(fout);用fgets逐行读取的好处是能控制缓冲区大小避免gets那种无边界读入导致的缓冲区溢出问题。处理完一定要记得fclose否则文件内容可能没有完全落盘这是个特别容易忽略的细节。棋盘加密在文件场景下有个麻烦密文长度几乎翻倍而且每两个数字之间还有空格写盘体积会比原文大不少。这个问题在文件加密里没问题但如果用在通信协议里要额外考虑带宽成本。4. 常见问题与排查技巧实录4.1 中文乱码与文件编码问题运行这段代码时很多同学用 VS Code 编译 C 代码后输出中文乱码这个热搜词我见过太多次了。这不是算法的问题是源文件编码和终端编码不一致导致的。VS Code 默认源文件编码可能是 UTF-8而 Windows 控制台默认用 GBK 解码。解决方案有三个一是把源文件用 GBK 编码另存二是控制台执行chcp 65001切到 UTF-8三是在代码文件顶部加#pragma或在编译参数里指定编码。最简单的做法就是用chcp 65001一劳永逸。我的建议是涉及中文输出时统一用英文提示就完全避开这个坑。4.2 合法的凯撒偏移量范围与负数密钥处理我在代码里写了key ((key % 26) 26) % 26;这一行就是为了处理负数密钥和超范围密钥。比如你输入密钥 30那实际上等价于 30 % 26 4也就是偏移 4 位。如果你输入 -1那等价于偏移 25 位因为 -1 26 25。这个规范化公式很常用尤其是在处理循环计数和环绕取模时。如果不做这步处理负数密钥会让字符数组越界解密结果完全错乱。4.3 棋盘解密时遇到非法密文的处理策略棋盘解密函数是我在测试时最担心出问题的地方。如果密文里出现单独的6或者9我的代码会把它当普通字符直接保留而不是报错因为判断条件是ciphertext[i] 1 ciphertext[i] 5。但如果密文格式是12后面直接没了也就是说字符串在第一个数字就结束了ciphertext[i 1]读到了\0\0减去 1 会产生一个负数下标访问board[row][col]就变成未定义行为。这个问题的根源在于棋盘加密的解码函数没有做足够的格式校验。如果做成正式项目至少应该在取第二个字符之前判断一下它是不是 1 到 5不是的话就报错。我给的示例代码因为已经限定了加密输出的格式所以省略了这层校验但你在复用的时候必须补上。4.4 使用 scanf 和 fgets 混用时的换行处理我在主函数里先用了fgets读字符串后面又用scanf读整数。这两个函数混用时会有一个经典的坑scanf读完整数后输入缓冲区里会残留一个换行符如果不处理紧接着的fgets会直接读到这个换行符并返回空字符串。我代码里在scanf之后加了一行getchar()就是为了把这个残留的换行符吃掉。这个问题在交互式程序里非常常见排查方法就是打印读入字符串的长度如果长度是 0 或者只有换行符基本就是缓冲区残留问题。4.5 常见问题速查表我做个表格把这段代码在学习和实践中最容易出现的问题整理一下问题现象可能原因解决方案解密后输出乱码负数取模导致数组越界解密时先加上 26 再取余中文编译输出乱码源文件编码与终端编码不一致命令行执行 chcp 65001棋盘密文解密崩溃密文格式非法位数不够增加长度判断和字符范围判断scanf 后 fgets 读不到内容缓冲区残留换行符加 getchar() 吃掉换行符非字母字符丢失加密函数未保留原字符查表失败时直接复制原字符密文长度超过预期棋盘加密输出翻倍分配足够缓冲区或动态分配5. 基于 C 语言的扩展方向与进阶玩法两个算法实现完之后代码框架其实已经搭好了可以往几个方向扩展。第一个方向是给凯撒加密添加暴力破解功能。因为密钥空间只有 26 个写一个循环把 0 到 25 全部试一遍打印出 26 个结果人工挑一下哪条像是明文。这个功能实现起来只要 20 行代码却能大幅提升工具的实用性。原理就是穷举所有可能的偏移量让使用者肉眼识别出语义通顺的那一条。严格说这不是破解因为真正安全的加密不会只有 26 种可能但作为教学演示足够了。第二个方向是把棋盘加密的固定方格改成密钥方格。把示例代码里的二维数组改成根据自定义密钥动态生成这样每个用户可以用不同的棋盘安全性上一个台阶。密钥表的生成方式是先写密钥里的字母再用字母表里没出现过的字母按顺序补全剩余格子。这个逻辑和凯撒密码里的密钥字母表构造完全一致感兴趣的同学可以去搜一下 “keyword cipher”。第三个方向是结合文件读写做一个命令行工具支持从文件读入、加解密、写出文件。这是对前面文件处理段落的一个完整实践。命令行参数可以用argc和argv来解析比如./cipher -c -k 3 -i input.txt -o output.txt代表用凯撒加密、密钥 3、输入文件名和输出文件名。做成这样之后功能上已经接近一个真正可用的命令行小工具写简历或者作为课程设计作业都拿得出手。第四个方向是尝试破解已知明文攻击场景比如在不知道密钥的情况下用统计分析和模式匹配猜出凯撒偏移量。这属于密码分析入门但能帮助你更深入理解算法的弱点。比如英语文本里出现频率最高的字母是 E如果密文里出现频率最高的字母是 H那就猜密钥偏移是 3因为 E 加 3 等于 H。这个思路实现成一个频率统计程序代码量很小但很有成就感。这几个方向不管选哪个建议都先把本文的主流程跑通再渐进式加功能。直接一把梭改成文件加解密容易出问题到时候调试都不知道从哪里下手。6. 我在实操中积累的几个关键体会代码写到这里我再啰嗦几句自己实际用这段代码教学和改代码时积累的体会。第一C 语言的字符串处理是真的需要时刻警惕边界。棋盘加密里单个字母变成两位数字如果缓冲区开小了数据写穿就是悄悄发生的程序不会立刻崩溃而是在之后某个神秘的时机突然报错。调试这种内存问题特别耗时所以写代码之前就要估算好最大长度宁可多分配不要少分配。第二写加密算法代码最忌讳想当然。凯撒加密看起来简单到三行就能写完但真正跑起来才发现负数取模、大小写转换、非字母字符过滤这些小细节全都是坑。我在课堂上让学生自己实现一遍十个人里有八个第一次跑出来的结果是错的原因千奇百怪。但这个过程特别值得因为调完这些错误之后你对 C 语言字符类型和取模运算的理解会彻底上一个台阶。第三代码注释不是写给编译器看的是写给未来的自己看的。比如棋盘加密里 I/J 合并这个规则如果你不在代码里注明三个月后回来看大概率想不起来为什么字母表里没有 J。我习惯在所有非直觉判断的地方写上注释这不会影响运行效率但能节省未来大量的回忆时间。这两个算法虽然古老却把现代加密里分组和置换的基本思想体现得很完整。用 C 语言实现一遍既能加深对古典密码的理解也能把数组、循环、字符串处理的 C 语言基本功打扎实是一件一举两得的事。如果你把这篇文章里的代码动手敲一遍再试着扩展成文件版我相信你的收获会比单纯看我总结要大得多。本文还有配套的精品资源点击获取
返回列表