Caesar

Caesar Cipher in C: Step-by-Step Programming Tutorial

Learn how to implement Caesar cipher in C with step-by-step code examples. Covers basic functions, pointer-based approaches, ASCII handling, file encryption, command-line arguments, and memory safety.

Published 2025年8月11日
12 minute read
Cryptography Guide

The Caesar cipher is a natural first cryptography project for C programmers. Implementing it in C gives you direct exposure to character encoding, pointer arithmetic, memory management, and file I/O, all core skills that distinguish competent C developers. Unlike higher-level languages that abstract away these details, C requires you to think carefully about how characters are stored and manipulated at the byte level.

This tutorial walks through five progressively more advanced implementations: a basic function using array indexing, a pointer-based approach, a program that handles the full ASCII table correctly, a file encryption utility, and a polished command-line tool with proper argument parsing. All code compiles with GCC or Clang using the C11 standard or later.

Try It Online: Experiment with Caesar cipher encryption before writing code using our free Caesar Cipher Encoder.

How Caesar Cipher Works with ASCII

In C, characters are stored as integer values according to the ASCII standard. The uppercase letters A through Z occupy values 65 through 90, and lowercase letters a through z occupy values 97 through 122. These ranges are contiguous, which makes shift arithmetic straightforward.

The encryption formula for a single character is:

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

where base is 'A' (65) for uppercase or 'a' (97) for lowercase, and shift is a value between 0 and 25. The modulo operation ensures that the result wraps around from Z back to A.

One important detail in C: the % operator can return negative values when the left operand is negative. For example, (-3) % 26 yields -3 in C, not 23. When implementing decryption or handling negative shifts, you must account for this behavior. The standard fix is to add 26 before taking the modulo: ((ch - base - shift) % 26 + 26) % 26 + base.

Basic Caesar Cipher Implementation

The simplest implementation uses two functions that operate on character arrays. This version processes strings in place, modifying the original buffer.

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

void caesar_encrypt(char *text, int shift) {
    /* Normalize shift to 0-25 range */
    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';
        }
        /* Non-alphabetic characters remain unchanged */
    }
}

void caesar_decrypt(char *text, int shift) {
    /* Decryption is encryption with the complementary 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;
}

Compile and run:

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

Output:

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.

Several C-specific details deserve attention:

  • unsigned char cast: The isupper() and islower() functions expect an unsigned char value or EOF. Passing a plain char on platforms where char is signed can cause undefined behavior for values above 127. Always cast to unsigned char when passing characters to <ctype.h> functions.
  • In-place modification: The function modifies the input string directly. This is efficient (no memory allocation needed) but means you must pass a mutable character array, not a string literal. Writing char *msg = "Hello"; caesar_encrypt(msg, 3); would be undefined behavior because string literals are read-only.
  • Null terminator: The loop uses text[i] != '\0' as its termination condition, which is the standard C idiom for iterating through null-terminated strings.

Pointer-Based Implementation

C programmers often prefer pointer arithmetic over array indexing. The following version uses pointers throughout, which many experienced C developers consider more idiomatic. This version also returns a newly allocated string rather than modifying the input.

#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;  /* Allocation failed */
    }

    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);

    /* Always free allocated memory */
    free(encrypted);
    free(decrypted);

    return 0;
}

This approach has several advantages:

  • Const correctness: The input string is declared const char *, so the compiler will catch any accidental modification attempts.
  • No side effects: The original string is never modified, making the function safer to use in multithreaded code or when the input must be preserved.
  • Explicit memory management: The caller is responsible for freeing the returned string, which is the standard C pattern for functions that allocate memory.

The pointer increment pattern (src++; dst++;) processes one character at a time, advancing both the read and write positions. This is slightly more efficient than array indexing because it avoids repeated offset calculations, though modern compilers typically optimize both forms to identical machine code.

File Encryption Program

A practical Caesar cipher program should be able to encrypt and decrypt files. The following implementation reads a file in buffered chunks, transforms each character, and writes the result to an output file. This approach handles files of any size without loading the entire contents into memory.

#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;
}

Compile and use:

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

# Encrypt a file
./caesar_file encrypt 5 plaintext.txt encrypted.txt

# Decrypt it back
./caesar_file decrypt 5 encrypted.txt decrypted.txt

Key design decisions in this implementation:

  • Buffered I/O: Reading 4096 bytes at a time is much more efficient than character-by-character I/O. The buffer size matches a typical filesystem block size.
  • Error handling: Every file operation checks for errors, and error messages include the specific system error via strerror(errno).
  • Resource cleanup: Files are closed on all code paths, including error paths. In production code, you might use goto cleanup pattern for even cleaner resource management.
  • Input validation: The shift value is parsed with strtol() and checked for trailing garbage characters, which is more robust than atoi().

Command-Line Tool with Brute Force

The final example is a feature-complete command-line tool that supports encryption, decryption, brute-force cracking, and reading from standard input. It demonstrates proper C argument parsing and the brute-force approach to breaking Caesar cipher.

#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 provided as argument */
            text = argv[3];
        } else {
            /* Read from stdin */
            text = read_stdin();
            if (text == NULL) return 1;
            free_text = 1;
        }

        /* Work on a mutable copy if text is from 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;
}

Compile and use:

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

# Encrypt
./caesar_tool encrypt 13 "Meet me at midnight"

# Decrypt
./caesar_tool decrypt 13 "Zrrg zr ng zvqavtug"

# Brute force an unknown message
./caesar_tool bruteforce "Wklv lv d vhfuhw phvvdjh"

# Pipe from another command
echo "Secret message" | ./caesar_tool encrypt 5

Memory Safety Best Practices

Memory safety is the most critical concern when writing C code. Here are the practices demonstrated throughout this tutorial and additional recommendations:

Always check malloc return values: Every call to malloc() can return NULL if the system runs out of memory. Dereferencing a null pointer is undefined behavior that typically causes a segmentation fault.

Free what you allocate: Every malloc() must have a corresponding free(). Use tools like Valgrind to verify your program has no memory leaks:

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

Use strtol instead of atoi: The atoi() function provides no error detection. If a user passes "abc" as a shift value, atoi() silently returns 0. The strtol() function lets you check whether the entire string was consumed via the endptr parameter.

Buffer overflow prevention: When reading input, always enforce a maximum size. The read_stdin() function in the final example limits input to MAX_INPUT bytes and ensures null termination.

Const correctness: Mark pointer parameters as const when the function should not modify the data they point to. This catches bugs at compile time and documents the function's contract.

Compiler Flags for Safer Code

Always compile with warnings enabled. The following flags catch many common mistakes at compile time:

gcc -std=c11 -Wall -Wextra -Wpedantic -Werror -O2 -o caesar caesar.c
  • -Wall enables most common warnings
  • -Wextra enables additional warnings not covered by -Wall
  • -Wpedantic enforces strict ISO C compliance
  • -Werror treats all warnings as errors, forcing you to fix them
  • -O2 enables optimization, which also enables additional analysis that can catch bugs

For development, also consider -fsanitize=address,undefined which enables runtime checking for memory errors and undefined behavior:

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

Common Mistakes to Avoid

Modifying string literals: In C, string literals like "Hello" are stored in read-only memory. Assigning them to a char * (not const char *) and then modifying them is undefined behavior. Always use character arrays (char msg[] = "Hello";) when you need a mutable string.

Signed char pitfalls: On many platforms, char is signed by default, meaning values above 127 are negative. Passing these to <ctype.h> functions is undefined behavior. Cast to unsigned char first.

Off-by-one in allocation: When allocating space for a string, always add 1 for the null terminator. malloc(strlen(text)) is wrong; malloc(strlen(text) + 1) is correct.

Forgetting the null terminator: After copying or constructing a string in a buffer, always ensure the final byte is '\0'. Functions like strncpy() do not guarantee null termination when the source string is longer than the specified count.

Summary

These five implementations demonstrate how to build Caesar cipher programs in C, from simple functions to production-quality command-line tools. The basic implementation shows core string manipulation. The pointer version illustrates idiomatic C coding style. The file encryptor handles real-world I/O with proper error handling. And the CLI tool brings everything together with argument parsing and brute-force capabilities.

C's low-level nature makes it an excellent language for understanding exactly how cipher operations work at the character level. The memory management discipline required by C also prepares you for implementing more complex ciphers where data structures and buffer management become more challenging.

Next Steps: Explore our Caesar Cipher Decoder to see how automated decryption works, or learn about the Vigenere Cipher for a more secure polyalphabetic approach.

About This Article

This article is part of our comprehensive caesar cipher tutorial series. Learn more about classical cryptography and explore our interactive cipher tools.

更多 Caesar Cipher 教程

试用 Caesar Cipher 密码工具

通过我们的交互式 caesar cipher 加密解密工具,将所学知识付诸实践。

试用 Caesar Cipher 工具