The const keyword in C documents intent and catches bugs at compile time. But the placement of const relative to the asterisk in pointer declarations trips up even experienced programmers. Get it right and the compiler catches entire classes of bugs for free.

const on a Non-Pointer Variable

#include <stdio.h>

int main() {
    const int MAX = 100;
    MAX = 200;   /* ERROR: assignment of read-only variable 'MAX' */
    return 0;
}

The value cannot be changed after initialization. This is the easy case — most programmers understand this intuitively. The subtlety is that const int is not a compile-time constant in C (unlike C++). You cannot use it as an array size or case label without a workaround.

/* Use #define or enum for true compile-time constants */
#define MAX_SIZE 100        /* preprocessor constant */
enum { BUFFER_SIZE = 256 }; /* enum constant — also compile-time */

const int runtime_const = 100;   /* runtime const — cannot be a VLA size in C89 */

Pointer to const — The Important One

#include <stdio.h>

void print_string(const char *str) {
    /* str[0] = 'X';  ERROR — cannot modify through const pointer */
    printf("%sn", str);
}

int main() {
    char hello[] = "hello";
    print_string(hello);   /* fine — passing modifiable array to const param */
    return 0;
}

const char *str means “pointer to const char” — you cannot modify what str points to, but you can change where str points. This is what you put on function parameters to promise “I will not modify this data.” Every function that takes a string but doesn’t modify it should take const char *.

const Pointer — The Rare One

#include <stdio.h>

int main() {
    int x = 10, y = 20;
    int * const ptr = &x;   /* const pointer to int — pointer cannot change */

    *ptr = 99;   /* fine — can modify what ptr points to */
    ptr = &y;   /* ERROR — cannot reassign ptr */

    printf("%dn", x);   /* 99 */
    return 0;
}

int * const ptr means “const pointer to int” — the pointer address is fixed, but you can modify the value it points to. This is useful for class-like encapsulation where you want a pointer that always refers to the same object.

const Pointer to const — Both Fixed

#include <string.h>

const char * const VERSION = "1.0.0";

/* Cannot modify the string or reassign the pointer */

The Clockwise Rule for Reading const Declarations

const int *p       /* pointer to const int — can change p, cannot change *p */
int const *p       /* same as above — const binds to int */
int * const p      /* const pointer to int — cannot change p, can change *p */
const int * const p  /* const pointer to const int — cannot change either */

Read right-to-left: “p is a [const] pointer to [const] int.” The rule: if const is to the left of *, the pointed-to data is const; if const is to the right of *, the pointer itself is const.

Why const Correctness Matters for APIs

#include <string.h>

/* Without const: compiler cannot warn when callers pass literal strings */
int count_vowels(char *str) {
    int count = 0;
    while (*str) {
        char c = *str++;
        if (c=='a'||c=='e'||c=='i'||c=='o'||c=='u') count++;
    }
    return count;
}

/* With const: safer and documents that str is not modified */
int count_vowels_safe(const char *str) {
    int count = 0;
    while (*str) {
        char c = *str++;
        if (c=='a'||c=='e'||c=='i'||c=='o'||c=='u') count++;
    }
    return count;
}

int main() {
    count_vowels("hello");        /* implicit cast const char* → char* — compiler warning */
    count_vowels_safe("hello");   /* fine — matches signature exactly */
    return 0;
}

Casting Away const — The Warning Sign

const char *str = "immutable";
char *mutable = (char*)str;     /* compiles, but dangerous */
mutable[0] = 'X';               /* undefined behavior — string literals are read-only */

Casting away const with (char*) is a code smell. If you find yourself doing this, the function receiving the pointer should have taken const char *. Only cast away const when you are interfacing with legacy code that incorrectly lacks const. Modifying what was originally a const object is undefined behavior.

For a deeper look at how pointers work in C and how the const qualifier interacts with pointer semantics, that guide covers the memory model. You can test const pointer behavior in our c online compiler — try assigning to a const-qualified lvalue and see the compiler error.

TL;DR

  • const char *p — pointer to const char; you can move p, cannot modify *p
  • char * const p — const pointer to char; cannot move p, can modify *p
  • If a function does not modify its pointer argument, the parameter should be const
  • Passing const char* where char* is expected generates a warning — fix the parameter, not the call site
  • Casting away const to modify a string literal is undefined behavior
  • const int is not a compile-time constant in C — use #define or enum for array sizes and case labels
  • Let the compiler enforce const correctness — it catches modification bugs for free at compile time