C Programming: Low-Level Mastery
Pointers

Const Pointers

Master const with pointers - pointer to const, const pointer, and const pointer to const. Learn to write safer code by preventing unintended modifications and communicating intent clearly.

Understanding Const with Pointers

The const qualifier with pointers creates three distinct scenarios: pointer to const data (can't modify data), const pointer (can't modify pointer), and const pointer to const data (can't modify either). Understanding these variations prevents bugs, improves API design, and enables compiler optimizations. The key is reading declarations right-to-left.

C
#include <stdio.h>

int main(void) {
    int x = 10, y = 20;
    
    /* 1. Pointer to const int - can't modify data */
    const int *ptr1 = &x;
    // *ptr1 = 15;  // ERROR: Can't modify data
    ptr1 = &y;      // OK: Can change pointer
    
    /* 2. Const pointer to int - can't modify pointer */
    int *const ptr2 = &x;
    *ptr2 = 15;     // OK: Can modify data
    // ptr2 = &y;   // ERROR: Can't change pointer
    
    /* 3. Const pointer to const int - can't modify either */
    const int *const ptr3 = &x;
    // *ptr3 = 15;  // ERROR: Can't modify data
    // ptr3 = &y;   // ERROR: Can't change pointer
    
    return 0;
}

/* Reading right-to-left:
   const int *ptr     - ptr is pointer to const int
   int *const ptr     - ptr is const pointer to int
   const int *const ptr - ptr is const pointer to const int

Pointer to Const

A pointer to const (const int *ptr) promises not to modify the data it points to. The data itself might not be const - the pointer just can't change it. This is the most common const pointer pattern, used extensively in function parameters to indicate read-only access.

C
/* Pointer to const int */
int x = 10;
const int *ptr = &x;  // Or: int const *ptr

printf("%d\n", *ptr);  // OK: Read
// *ptr = 20;  // ERROR: Can't modify through ptr

/* But x itself can still be modified */
x = 20;  // OK: x is not const
printf("%d\n", *ptr);  // 20

/* Pointer can be changed */
int y = 30;
ptr = &y;  // OK: Pointer not const
printf("%d\n", *ptr);  // 30

/* Function parameter - read-only array */
void print_array(const int *arr, int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);  // OK: Read
        // arr[i] = 0;  // ERROR: Can't modify
    }
    printf("\n");
}

/* Prevents accidental modification */
void process_data(const char *input) {
    // input[0] = 'x';  // ERROR: Prevented
    printf("Input: %s\n", input);
}

/* Pointing to actual const data */
const int MAX = 100;
const int *ptr2 = &MAX;  // OK: Both const

/* String literals are const */
const char *str = "Hello";  // Correct
// char *str2 = "Hello";  // Warning: Should be const

/* Array of pointers to const */
const char *days[] = {
    "Monday", "Tuesday", "Wednesday"
};

/* Each string is const through the pointer */
// days[0][0] = 'm';  // ERROR: Can't modify

Const Pointer

A const pointer (int *const ptr) cannot be changed to point elsewhere after initialization. It's like a reference in C++ - always points to the same location. The data can be modified, but the pointer itself is fixed. Less common than pointer to const, but useful for ensuring a pointer remains valid.

C
/* Const pointer to int */
int x = 10, y = 20;
int *const ptr = &x;  // Must initialize!

*ptr = 15;  // OK: Can modify data
printf("%d\n", x);  // 15

// ptr = &y;  // ERROR: Can't change pointer
// ptr++;  // ERROR: Can't modify pointer

/* Use case: Fixed buffer pointer */
char buffer[1024];
char *const buf_ptr = buffer;  // Always points to buffer

/* Can modify buffer contents */
buf_ptr[0] = 'A';
strcpy(buf_ptr, "Hello");

/* But pointer stays fixed */
// buf_ptr = malloc(2048);  // ERROR

/* Function with const pointer parameter */
void initialize_buffer(char *const buf, size_t size) {
    /* buf can't be reassigned in function */
    // buf = NULL;  // ERROR
    
    /* But can modify contents */
    memset(buf, 0, size);
}

/* Array as const pointer */
void process_array(int *const arr, int size) {
    /* arr can't be modified */
    // arr = NULL;  // ERROR
    // arr++;  // ERROR
    
    /* But contents can */
    for (int i = 0; i < size; i++) {
        arr[i] *= 2;
    }
}

/* Const pointer in struct */
typedef struct {
    int *const data;  // Fixed pointer
    size_t size;
} FixedBuffer;

/* Must initialize in struct initialization */
int storage[100];
FixedBuffer buf = {storage, 100};
// buf.data = NULL;  // ERROR: Can't reassign

Const Pointer to Const

Combining both: const pointer to const data (const int *const ptr) means neither the pointer nor the data can be modified. Maximum immutability - useful for truly read-only references that should never change. Less common but provides strongest guarantees.

C
/* Const pointer to const int */
int x = 10;
const int *const ptr = &x;

// *ptr = 20;  // ERROR: Can't modify data
// ptr = &y;  // ERROR: Can't modify pointer

printf("%d\n", *ptr);  // OK: Read only

/* Global const configuration */
const char *const CONFIG_FILE = "/etc/config.ini";
const int *const MAGIC_NUMBERS = (const int[]){1, 2, 3, 5, 8};

/* Function parameter - completely read-only */
void display_message(const char *const msg) {
    /* Can't modify message content */
    // msg[0] = 'H';  // ERROR
    
    /* Can't reassign pointer */
    // msg = "Different";  // ERROR
    
    printf("%s\n", msg);  // OK: Read
}

/* Use case: Read-only lookup table */
typedef struct {
    int code;
    const char *const message;
} ErrorEntry;

const ErrorEntry *const ERROR_TABLE = (const ErrorEntry[]){
    {404, "Not Found"},
    {500, "Internal Server Error"},
    {403, "Forbidden"}
};

/* Protecting function pointers */
typedef int (*Operation)(int, int);

int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }

/* Const pointer to const function pointer */
Operation const *const ops = (Operation[]){add, subtract};

/* Very explicit immutability */
void process_immutable_data(const int *const data, 
                           const size_t size) {
    /* Nothing can be modified */
    for (size_t i = 0; i < size; i++) {
        printf("%d ", data[i]);
    }
}

Const Correctness in Functions

Using const in function parameters documents intent, enables compiler optimizations, and prevents bugs. Mark parameters const if the function doesn't modify them. This is "const correctness" - a best practice for clean, safe APIs.

C
/* Read-only array parameter */
int sum_array(const int *arr, int size) {
    int sum = 0;
    for (int i = 0; i < size; i++) {
        sum += arr[i];  // OK: Read
        // arr[i] = 0;  // ERROR: Prevented
    }
    return sum;
}

/* String processing */
size_t count_chars(const char *str, char c) {
    size_t count = 0;
    while (*str) {
        if (*str == c) count++;
        str++;  // OK: Local copy of pointer
    }
    return count;
}

/* Const return value (less common) */
const int* get_max(const int *arr, int size) {
    const int *max = arr;
    for (int i = 1; i < size; i++) {
        if (arr[i] &gt; *max) {
            max = &arr[i];
        }
    }
    return max;  // Returns const pointer
}

/* Using result */
void example_const_return(void) {
    int numbers[] = {5, 2, 8, 1, 9};
    const int *max = get_max(numbers, 5);
    
    printf("Max: %d\n", *max);  // OK
    // *max = 10;  // ERROR: Can't modify
}

/* Const with structures */
typedef struct {
    int id;
    char name[50];
} Person;

void print_person(const Person *p) {
    printf("ID: %d, Name: %s\n", p-&gt;id, p-&gt;name);
    // p-&gt;id = 0;  // ERROR: Can't modify
}

/* Const member function pattern (C++) */
int person_get_id(const Person *p) {
    return p-&gt;id;  // Read-only access
}

/* Non-const when modification needed */
void person_set_id(Person *p, int new_id) {
    p-&gt;id = new_id;  // Modification allowed
}

/* Return pointer to const */
const char* get_error_message(int error_code) {
    static const char *messages[] = {
        "Success",
        "Error",
        "Fatal"
    };
    
    if (error_code < 0 || error_code &gt; 2) {
        return "Unknown";
    }
    
    return messages[error_code];
}

/* Const with function pointers */
typedef void (*Callback)(const int *data);

void register_callback(Callback cb) {
    static const int sample_data[] = {1, 2, 3};
    cb(sample_data);  // Passes const data
}

void my_callback(const int *data) {
    printf("%d\n", *data);
    // *data = 0;  // ERROR: data is const
}

Casting Away Const

You can cast away const, but it's dangerous and usually wrong. If the underlying data is actually const, modifying it through a cast causes undefined behavior. Only cast away const when you know the data is mutable and the const was added for API reasons.

C
/* Casting away const (dangerous!) */
const int x = 10;
const int *ptr = &x;

int *bad_ptr = (int*)ptr;  // Cast away const
// *bad_ptr = 20;  // UNDEFINED BEHAVIOR: x is actually const

/* When it might be okay */
void legacy_function(char *str);  // Old API without const

void call_legacy(const char *input) {
    /* We know legacy_function doesn't modify, 
       but its signature isn't const */
    legacy_function((char*)input);  // Risky but sometimes necessary
}

/* Better: Fix the legacy function */
void legacy_function_fixed(const char *str) {
    // Now properly const
}

/* Const vs mutable */
int y = 10;  // Mutable
const int *cptr = &y;  // Pointer treats it as const

int *mptr = (int*)cptr;  // Cast away const
*mptr = 20;  // OK: y itself is mutable

printf("%d\n", y);  // 20 - works because y is mutable

/* When you should NEVER cast away const */
const int CONSTANT = 42;  // Truly const
const int *p = &CONSTANT;

// int *bad = (int*)p;
// *bad = 100;  // UNDEFINED BEHAVIOR

/* String literals */
const char *str = "Hello";
// char *mutable = (char*)str;
// mutable[0] = 'h';  // CRASH: String literals are in read-only memory

/* Better pattern: Document mutability */
char buffer[100] = "Hello";
const char *readonly = buffer;

/* If you need mutable, copy */
char *mutable = strdup((const char*)readonly);
mutable[0] = 'h';  // Safe on copy
free(mutable);

Best Practices

Following const best practices makes code safer, clearer, and more maintainable. Const documents intent, catches bugs at compile-time, and enables optimizations.

C
/* Practice 1: Use const for read-only parameters */
// Good:
void print_array(const int *arr, int size);

// Bad (if not modifying):
void print_array(int *arr, int size);

/* Practice 2: Put const on left */
const int *ptr;  // Preferred
// int const *ptr;  // Valid but less common

/* Practice 3: Default to const, remove when needed */
void process(const char *input) {
    const int *data = get_data();
    // Work with const by default
}

/* Practice 4: Const return for immutable data */
const char* get_config_value(const char *key);

/* Practice 5: Const in struct members */
typedef struct {
    const int id;  // Immutable after creation
    char name[50]; // Mutable
} Record;

/* Practice 6: Document const expectations */
/**
 * Processes data without modification.
 * @param data Read-only input data
 * @param size Number of elements
 */
void process_readonly(const int *data, size_t size);

/* Practice 7: Use const with globals */
const double PI = 3.14159;
const char *const APP_NAME = "MyApp";

/* Practice 8: Const with static */
static const int MAX_CONNECTIONS = 100;

/* Practice 9: Const correctness throughout codebase */
const char* string_find(const char *str, char c);
size_t string_length(const char *str);
int string_compare(const char *s1, const char *s2);

/* Practice 10: Don't cast away const */
// Avoid:
legacy_api((char*)const_string);

// Better: Fix API or document necessity
// If truly needed, comment why
legacy_api((char*)const_string);  // Safe: legacy_api doesn't modify

/* Antipatterns to avoid */

// Bad: Missing const
void read_only_function(int *data) {  // Looks like it modifies!
    printf("%d\n", *data);
}

// Good: Const shows intent
void read_only_function_good(const int *data) {
    printf("%d\n", *data);
}

// Bad: Excessive const
const int const *const func(const int const *const p) {
    // Overkill
}

// Good: Const where it matters
const int* func(const int *p) {
    // Clear and sufficient
}

Summary & What's Next

Key Takeaways:

  • ✅ const int *ptr - pointer to const (can't modify data)
  • ✅ int *const ptr - const pointer (can't modify pointer)
  • ✅ const int *const ptr - both const
  • ✅ Read right-to-left to understand declarations
  • ✅ Use const for read-only function parameters
  • ✅ Const prevents bugs and documents intent
  • ✅ Never cast away const from actually const data
  • ✅ Default to const, remove only when modification needed

What's Next?

Let's learn about void pointers and generic programming in C!