凯撒密码

C 语言实现凯撒密码:分步编程教程

通过分步代码示例学习如何在 C 语言中实现凯撒密码。涵盖基础函数、基于指针的方法、ASCII 处理、文件加密、命令行参数以及内存安全实践。

发布于 2025年8月11日
12 分钟阅读
密码学指南

凯撒密码是 C 程序员天然的第一个密码学项目。用 C 语言实现它,能让你直接接触字符编码、指针算术、内存管理和文件 I/O,这些都是区别优秀 C 开发者的核心技能。与高级语言将这些细节抽象掉不同,C 语言要求你仔细思考字符在字节层面是如何存储和操作的。

本教程将依次介绍五种逐步进阶的实现:使用数组索引的基础函数、基于指针的方法、能正确处理完整 ASCII 表的程序、文件加密工具,以及带有完善参数解析的命令行工具。所有代码均可使用 GCC 或 Clang 以 C11 标准或更高版本编译。

在线体验:在动手编写代码之前,先用我们免费的凯撒密码编码器体验一下加密效果。

凯撒密码与 ASCII 的工作原理

在 C 语言中,字符根据 ASCII 标准以整数值存储。大写字母 A 到 Z 对应值 65 到 90,小写字母 a 到 z 对应值 97 到 122。这些范围是连续的,使得位移运算十分直观。

单个字符的加密公式为:

encrypted = (ch - base + shift) % 26 + base

其中 base 对大写字母为 'A'(65),对小写字母为 'a'(97),shift 是 0 到 25 之间的值。取模运算确保结果从 Z 回绕到 A。

C 语言中有一个重要细节:当左操作数为负数时,% 运算符可能返回负值。例如,(-3) % 26 在 C 中结果为 -3,而非 23。在实现解密或处理负位移时,必须考虑这一行为。标准的修正方法是在取模之前加上 26:((ch - base - shift) % 26 + 26) % 26 + base

基础凯撒密码实现

最简单的实现使用两个对字符数组进行操作的函数。该版本原地处理字符串,直接修改原始缓冲区。

#include <stdio.h>
#include <string.h>
#include <ctype.h>

void caesar_encrypt(char *text, int shift) {
    /* 将位移规范化到 0-25 范围 */
    shift = ((shift % 26) + 26) % 26;

    for (int i = 0; text[i] != '\0'; i++) {
        if (isupper((unsigned char)text[i])) {
            text[i] = (text[i] - 'A' + shift) % 26 + 'A';
        } else if (islower((unsigned char)text[i])) {
            text[i] = (text[i] - 'a' + shift) % 26 + 'a';
        }
        /* 非字母字符保持不变 */
    }
}

void caesar_decrypt(char *text, int shift) {
    /* 解密即用互补位移进行加密 */
    caesar_encrypt(text, 26 - ((shift % 26 + 26) % 26));
}

int main(void) {
    char message[] = "Hello, World! The quick brown fox jumps over the lazy dog.";
    int shift = 7;

    printf("Original:  %s\n", message);

    caesar_encrypt(message, shift);
    printf("Encrypted: %s\n", message);

    caesar_decrypt(message, shift);
    printf("Decrypted: %s\n", message);

    return 0;
}

编译并运行:

gcc -std=c11 -Wall -Wextra -o caesar caesar_basic.c
./caesar

输出:

Original:  Hello, World! The quick brown fox jumps over the lazy dog.
Encrypted: Olssv, Dvysk! Aol xbpjr iyvdu mve qbtwz vcly aol shgf kvn.
Decrypted: Hello, World! The quick brown fox jumps over the lazy dog.

有几个 C 语言特有的细节值得注意:

  • unsigned char 强制转换isupper()islower() 函数期望接收 unsigned char 值或 EOF。在 char 为有符号类型的平台上,直接传入普通 char 对于值大于 127 的字符会导致未定义行为。向 <ctype.h> 函数传递字符时,请始终转换为 unsigned char
  • 原地修改:该函数直接修改输入字符串。这很高效(无需内存分配),但意味着必须传入可变字符数组,而非字符串字面量。写 char *msg = "Hello"; caesar_encrypt(msg, 3); 会产生未定义行为,因为字符串字面量是只读的。
  • 空终止符:循环使用 text[i] != '\0' 作为终止条件,这是 C 语言遍历以空终止符结尾字符串的标准惯用写法。

基于指针的实现

C 程序员通常更倾向于使用指针算术而非数组索引。以下版本全程使用指针,许多经验丰富的 C 开发者认为这更符合 C 语言惯用风格。该版本还会返回一个新分配的字符串,而非修改输入。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

char *caesar_transform(const char *text, int shift) {
    shift = ((shift % 26) + 26) % 26;

    size_t len = strlen(text);
    char *result = malloc(len + 1);
    if (result == NULL) {
        return NULL;  /* 内存分配失败 */
    }

    const char *src = text;
    char *dst = result;

    while (*src != '\0') {
        if (*src >= 'A' && *src <= 'Z') {
            *dst = (*src - 'A' + shift) % 26 + 'A';
        } else if (*src >= 'a' && *src <= 'z') {
            *dst = (*src - 'a' + shift) % 26 + 'a';
        } else {
            *dst = *src;
        }
        src++;
        dst++;
    }
    *dst = '\0';

    return result;
}

char *caesar_encrypt_alloc(const char *text, int shift) {
    return caesar_transform(text, shift);
}

char *caesar_decrypt_alloc(const char *text, int shift) {
    return caesar_transform(text, 26 - ((shift % 26 + 26) % 26));
}

int main(void) {
    const char *original = "Secrets must be kept safe!";
    int shift = 13;

    char *encrypted = caesar_encrypt_alloc(original, shift);
    if (encrypted == NULL) {
        fprintf(stderr, "Memory allocation failed\n");
        return 1;
    }

    char *decrypted = caesar_decrypt_alloc(encrypted, shift);
    if (decrypted == NULL) {
        fprintf(stderr, "Memory allocation failed\n");
        free(encrypted);
        return 1;
    }

    printf("Original:  %s\n", original);
    printf("Encrypted: %s\n", encrypted);
    printf("Decrypted: %s\n", decrypted);

    /* 始终释放已分配的内存 */
    free(encrypted);
    free(decrypted);

    return 0;
}

这种方式有几个优势:

  • const 正确性:输入字符串声明为 const char *,编译器会捕获任何意外的修改尝试。
  • 无副作用:原始字符串永远不会被修改,使函数在多线程代码中或需要保留输入时更加安全。
  • 显式内存管理:调用者负责释放返回的字符串,这是 C 语言中分配内存的函数的标准模式。

指针递增模式(src++; dst++;)每次处理一个字符,同时推进读写位置。这比数组索引稍微高效一些,因为避免了重复的偏移量计算,不过现代编译器通常会将两种形式优化为相同的机器码。

文件加密程序

一个实用的凯撒密码程序应该能够加密和解密文件。以下实现以分块缓冲方式读取文件,对每个字符进行变换,然后将结果写入输出文件。这种方法无需将全部内容加载到内存中,即可处理任意大小的文件。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>

#define BUFFER_SIZE 4096

static int normalize_shift(int shift) {
    return ((shift % 26) + 26) % 26;
}

static char transform_char(char ch, int shift) {
    if (ch >= 'A' && ch <= 'Z') {
        return (ch - 'A' + shift) % 26 + 'A';
    } else if (ch >= 'a' && ch <= 'z') {
        return (ch - 'a' + shift) % 26 + 'a';
    }
    return ch;
}

int caesar_process_file(const char *input_path, const char *output_path,
                        int shift, int decrypt) {
    FILE *fin = fopen(input_path, "r");
    if (fin == NULL) {
        fprintf(stderr, "Error opening input file '%s': %s\n",
                input_path, strerror(errno));
        return -1;
    }

    FILE *fout = fopen(output_path, "w");
    if (fout == NULL) {
        fprintf(stderr, "Error opening output file '%s': %s\n",
                output_path, strerror(errno));
        fclose(fin);
        return -1;
    }

    shift = normalize_shift(shift);
    if (decrypt) {
        shift = 26 - shift;
    }

    char buffer[BUFFER_SIZE];
    size_t bytes_read;

    while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, fin)) > 0) {
        for (size_t i = 0; i < bytes_read; i++) {
            buffer[i] = transform_char(buffer[i], shift);
        }

        size_t bytes_written = fwrite(buffer, 1, bytes_read, fout);
        if (bytes_written != bytes_read) {
            fprintf(stderr, "Error writing to output file: %s\n",
                    strerror(errno));
            fclose(fin);
            fclose(fout);
            return -1;
        }
    }

    if (ferror(fin)) {
        fprintf(stderr, "Error reading input file: %s\n", strerror(errno));
        fclose(fin);
        fclose(fout);
        return -1;
    }

    fclose(fin);
    fclose(fout);
    return 0;
}

int main(int argc, char *argv[]) {
    if (argc != 5) {
        fprintf(stderr, "Usage: %s <encrypt|decrypt> <shift> <input_file> <output_file>\n",
                argv[0]);
        return 1;
    }

    int decrypt = 0;
    if (strcmp(argv[1], "decrypt") == 0 || strcmp(argv[1], "d") == 0) {
        decrypt = 1;
    } else if (strcmp(argv[1], "encrypt") != 0 && strcmp(argv[1], "e") != 0) {
        fprintf(stderr, "Invalid mode '%s'. Use 'encrypt' or 'decrypt'.\n", argv[1]);
        return 1;
    }

    char *endptr;
    long shift = strtol(argv[2], &endptr, 10);
    if (*endptr != '\0') {
        fprintf(stderr, "Invalid shift value '%s'. Must be an integer.\n", argv[2]);
        return 1;
    }

    int result = caesar_process_file(argv[3], argv[4], (int)shift, decrypt);
    if (result == 0) {
        printf("File %s successfully: %s -> %s\n",
               decrypt ? "decrypted" : "encrypted",
               argv[3], argv[4]);
    }

    return result == 0 ? 0 : 1;
}

编译并使用:

gcc -std=c11 -Wall -Wextra -o caesar_file caesar_file.c

# 加密文件
./caesar_file encrypt 5 plaintext.txt encrypted.txt

# 解密还原
./caesar_file decrypt 5 encrypted.txt decrypted.txt

该实现的关键设计决策:

  • 缓冲 I/O:每次读取 4096 字节远比逐字符 I/O 高效。缓冲区大小与典型文件系统块大小一致。
  • 错误处理:每次文件操作都检查错误,错误信息通过 strerror(errno) 包含具体的系统错误描述。
  • 资源清理:包括错误路径在内的所有代码路径都关闭了文件。在生产代码中,可以使用 goto cleanup 模式实现更整洁的资源管理。
  • 输入校验:位移值使用 strtol() 解析并检查尾部多余字符,比 atoi() 更健壮。

带暴力破解的命令行工具

最后一个示例是一个功能完整的命令行工具,支持加密、解密、暴力破解以及从标准输入读取数据。它演示了正确的 C 语言参数解析方式,以及破解凯撒密码的暴力破解方法。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#define MAX_INPUT 65536

static int normalize_shift(int shift) {
    return ((shift % 26) + 26) % 26;
}

static void caesar_transform_inplace(char *text, int shift) {
    shift = normalize_shift(shift);
    for (char *p = text; *p != '\0'; p++) {
        if (*p >= 'A' && *p <= 'Z') {
            *p = (*p - 'A' + shift) % 26 + 'A';
        } else if (*p >= 'a' && *p <= 'z') {
            *p = (*p - 'a' + shift) % 26 + 'a';
        }
    }
}

static char *read_stdin(void) {
    char *buffer = malloc(MAX_INPUT);
    if (buffer == NULL) {
        fprintf(stderr, "Memory allocation failed\n");
        return NULL;
    }

    size_t total = 0;
    size_t bytes_read;
    while (total < MAX_INPUT - 1 &&
           (bytes_read = fread(buffer + total, 1, MAX_INPUT - 1 - total, stdin)) > 0) {
        total += bytes_read;
    }
    buffer[total] = '\0';
    return buffer;
}

static void brute_force(const char *ciphertext) {
    size_t len = strlen(ciphertext);
    char *attempt = malloc(len + 1);
    if (attempt == NULL) {
        fprintf(stderr, "Memory allocation failed\n");
        return;
    }

    printf("=== Brute Force: All 26 Possible Decryptions ===\n\n");
    for (int shift = 0; shift < 26; shift++) {
        strcpy(attempt, ciphertext);
        caesar_transform_inplace(attempt, 26 - shift);
        printf("Shift %2d: %s\n", shift, attempt);
    }

    free(attempt);
}

static void print_usage(const char *progname) {
    printf("Caesar Cipher Tool\n\n"
           "Usage:\n"
           "  %s encrypt <shift> <text>\n"
           "  %s decrypt <shift> <text>\n"
           "  %s bruteforce <text>\n"
           "  echo \"text\" | %s encrypt <shift>\n"
           "\nExamples:\n"
           "  %s encrypt 3 \"Hello World\"\n"
           "  %s decrypt 3 \"Khoor Zruog\"\n"
           "  %s bruteforce \"Khoor Zruog\"\n",
           progname, progname, progname, progname,
           progname, progname, progname);
}

int main(int argc, char *argv[]) {
    if (argc < 2) {
        print_usage(argv[0]);
        return 0;
    }

    if (strcmp(argv[1], "encrypt") == 0 || strcmp(argv[1], "decrypt") == 0) {
        if (argc < 3) {
            fprintf(stderr, "Error: shift value required\n");
            return 1;
        }

        char *endptr;
        int shift = (int)strtol(argv[2], &endptr, 10);
        if (*endptr != '\0') {
            fprintf(stderr, "Error: invalid shift value '%s'\n", argv[2]);
            return 1;
        }

        int is_decrypt = (strcmp(argv[1], "decrypt") == 0);
        if (is_decrypt) {
            shift = 26 - normalize_shift(shift);
        }

        char *text;
        int free_text = 0;

        if (argc >= 4) {
            /* 文本作为参数传入 */
            text = argv[3];
        } else {
            /* 从标准输入读取 */
            text = read_stdin();
            if (text == NULL) return 1;
            free_text = 1;
        }

        /* 若文本来自 argv,则在可变副本上操作 */
        char *work;
        if (!free_text) {
            work = malloc(strlen(text) + 1);
            if (work == NULL) {
                fprintf(stderr, "Memory allocation failed\n");
                return 1;
            }
            strcpy(work, text);
        } else {
            work = text;
        }

        caesar_transform_inplace(work, shift);
        printf("%s\n", work);

        if (!free_text) {
            free(work);
        } else {
            free(text);
        }

    } else if (strcmp(argv[1], "bruteforce") == 0) {
        char *text;
        int free_text = 0;

        if (argc >= 3) {
            text = argv[2];
        } else {
            text = read_stdin();
            if (text == NULL) return 1;
            free_text = 1;
        }

        brute_force(text);

        if (free_text) free(text);

    } else if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0) {
        print_usage(argv[0]);
    } else {
        fprintf(stderr, "Unknown command '%s'\n", argv[1]);
        print_usage(argv[0]);
        return 1;
    }

    return 0;
}

编译并使用:

gcc -std=c11 -Wall -Wextra -O2 -o caesar_tool caesar_tool.c

# 加密
./caesar_tool encrypt 13 "Meet me at midnight"

# 解密
./caesar_tool decrypt 13 "Zrrg zr ng zvqavtug"

# 对未知消息进行暴力破解
./caesar_tool bruteforce "Wklv lv d vhfuhw phvvdjh"

# 从其他命令通过管道传入
echo "Secret message" | ./caesar_tool encrypt 5

内存安全最佳实践

内存安全是编写 C 代码时最关键的问题。以下是本教程中演示的实践方式及其他建议:

始终检查 malloc 返回值:每次调用 malloc() 都可能在系统内存耗尽时返回 NULL。对空指针解引用是未定义行为,通常会导致段错误。

分配的内存必须释放:每个 malloc() 都必须有对应的 free()。使用 Valgrind 等工具验证程序没有内存泄漏:

valgrind --leak-check=full ./caesar_tool encrypt 5 "Hello"

使用 strtol 而非 atoiatoi() 函数不提供任何错误检测。如果用户将 "abc" 作为位移值传入,atoi() 会静默返回 0。strtol() 函数通过 endptr 参数让你检查整个字符串是否被完整解析。

缓冲区溢出防护:读取输入时,始终强制限制最大大小。最后一个示例中的 read_stdin() 函数将输入限制为 MAX_INPUT 字节,并确保以空终止符结尾。

const 正确性:当函数不应修改指针所指向的数据时,将指针参数标记为 const。这能在编译时捕获错误,并以文档形式表明函数的契约。

更安全代码的编译器标志

始终在启用警告的情况下编译。以下标志可在编译时捕获许多常见错误:

gcc -std=c11 -Wall -Wextra -Wpedantic -Werror -O2 -o caesar caesar.c
  • -Wall 启用大多数常见警告
  • -Wextra 启用 -Wall 未覆盖的额外警告
  • -Wpedantic 强制执行严格的 ISO C 合规性
  • -Werror 将所有警告视为错误,强制你修复它们
  • -O2 启用优化,同时还会启用额外的分析以捕获潜在错误

在开发过程中,还可以考虑使用 -fsanitize=address,undefined,它会启用针对内存错误和未定义行为的运行时检查:

gcc -std=c11 -Wall -Wextra -fsanitize=address,undefined -g -o caesar caesar.c

常见错误及规避方法

修改字符串字面量:在 C 语言中,"Hello" 这样的字符串字面量存储在只读内存中。将其赋值给 char *(而非 const char *)然后修改,是未定义行为。需要可变字符串时,请始终使用字符数组(char msg[] = "Hello";)。

有符号 char 陷阱:在许多平台上,char 默认是有符号的,这意味着大于 127 的值是负数。将这些值传给 <ctype.h> 函数是未定义行为。请先转换为 unsigned char

分配时差一错误:为字符串分配空间时,始终加 1 以容纳空终止符。malloc(strlen(text)) 是错误的;malloc(strlen(text) + 1) 才是正确的。

遗忘空终止符:在缓冲区中复制或构造字符串后,始终确保最后一个字节为 '\0'。当源字符串长度超过指定数量时,strncpy() 等函数不保证以空终止符结尾。

总结

这五种实现演示了如何用 C 语言构建凯撒密码程序,从简单函数到生产级命令行工具。基础实现展示了核心字符串操作。指针版本体现了地道的 C 语言编码风格。文件加密器通过完善的错误处理应对真实世界的 I/O 需求。命令行工具则通过参数解析和暴力破解能力将一切融为一体。

C 语言的底层特性使其成为理解密码操作在字符层面如何工作的绝佳语言。C 语言所要求的内存管理纪律,也为实现更复杂的密码做好了准备,因为在那些场景中,数据结构和缓冲区管理会变得更具挑战性。

下一步:探索我们的凯撒密码解码器,了解自动解密的工作原理;或者学习维吉尼亚密码,探索更安全的多表替换加密方案。

关于本文

本文是我们综合 凯撒密码 教程系列的一部分。继续了解古典密码学,并探索我们的交互式密码工具。

更多 凯撒密码 教程

试用 凯撒密码 工具

通过我们的交互式凯撒密码工具,将所学知识付诸实践。

试用 凯撒密码 工具