The Game of Thrones in C: How to Rule the Kingdom of Pointers with const

1. The Basic Relationship Between const and Pointers

In C, the combination of the const keyword with pointers can create a powerful type safety protection mechanism. Depending on the position of const, four different pointer types can be generated:

int a = 10;
int b = 20;

// 1. Pointer to a constant
const int *p1 = &a;  // Pointer is mutable, content pointed to is immutable
// *p1 = 30;  // Error: cannot modify the content pointed to
p1 = &b;     // Correct: can change the pointer's target

// 2. Constant pointer
int * const p2 = &a;  // Pointer is immutable, content pointed to is mutable
*p2 = 30;     // Correct: can modify the content pointed to
// p2 = &b;   // Error: cannot change the pointer's target

// 3. Constant pointer to a constant
const int * const p3 = &a;  // Both pointer and content pointed to are immutable
// *p3 = 30;  // Error
// p3 = &b;   // Error

// 4. Non-constant pointer (regular pointer)
int *p4 = &a;  // Both pointer and content pointed to are mutable
*p4 = 30;      // Correct
p4 = &b;       // Correct

2. Advanced Applications of const Pointers

1. const Pointers in Function Parameters

The application of const in function parameters can clearly express the function’s intent:

// Parameter is a pointer to a constant: the function promises not to modify the passed data
void print_array(const int *arr, size_t size) {
    for(size_t i = 0; i < size; i++) {
        printf("%d ", arr[i]);
        // arr[i] = 0;  // Compilation error
    }
}

// Parameter is a constant pointer: the pointer itself cannot change (less commonly used)
void process_data(int * const data) {
    *data = 100;  // Can modify the content pointed to
    // data++;     // Compilation error
}

// Best practice: combine both
void safe_processor(const int * const data) {
    // Cannot modify the pointer or the content pointed to
    // Completely read-only access
}

2. Multi-level Pointers with const

The combination of multi-level pointers and const can provide finer-grained protection:

int x = 10;
int y = 20;
int *p = &x;
int **pp = &p;

// 1. Pointer to a pointer to a constant
const int **pp1 = &p;  // Allowed but not recommended, may cause type safety issues

// 2. Pointer to a constant pointer
int * const *pp2 = &p;  // pp2 can change, *pp2 cannot change
pp2 = &p;     // Correct
// *pp2 = &y;  // Error

// 3. Constant pointer to a pointer
int ** const pp3 = &p;  // pp3 cannot change, *pp3 can change
// pp3 = &p;   // Error
*pp3 = &y;     // Correct

// 4. Fully constant
const int * const * const pp4 = &p;  // All immutable

3. Type Safety with const and Pointers

1. const Correctness Rules

C follows the following type conversion rules:

  • Non-constant pointers can be implicitly converted to pointers to constants
  • The reverse is not allowed (unless explicitly cast)
int a = 10;
const int b = 20;

const int *p1 = &a;  // Legal: int* -> const int*
int *p2 = &b;       // Illegal: const int* -> int*
int *p3 = (int*)&b;  // Legal but dangerous: explicit type cast

// Special case for string literals
char *str = "hello";  // Dangerous: string literal is actually const char[]
const char *safe_str = "world";  // Correct way

2. Practical Application Case: Immutable Lookup Table

// Define a read-only lookup table
typedef struct {
    const char * const key;  // Key is immutable
    const int value;         // Value is immutable
} LookupEntry;

// Statically allocated lookup table (stored in ROM)
static const LookupEntry TABLE[] = {
    {"start", 0},
    {"stop", 1},
    {"pause", 2},
    {NULL, -1}  // End marker
};

// Lookup function, both parameter and return value use const protection
const LookupEntry *find_entry(const char * const key) {
    const LookupEntry *entry = TABLE;
    while(entry->key != NULL) {
        if(strcmp(entry->key, key) == 0) {
            return entry;  // Return pointer to constant
        }
        entry++;
    }
    return NULL;
}

4. Special Applications of const in Embedded Development

1. Accessing Hardware Registers

// Define a read-only hardware register
#define READ_ONLY_REG (*(volatile const uint32_t *)0x40021000)

// Define a write-only hardware register
#define WRITE_ONLY_REG (*(volatile uint32_t * const)0x40021004)

// Define a read-write hardware register
#define READ_WRITE_REG (*(volatile uint32_t *)0x40021008)

void hardware_init(void) {
    uint32_t value = READ_ONLY_REG;  // Legal: read from register
    // READ_ONLY_REG = 0x1234;       // Compilation error: attempt to write to read-only register
    
    WRITE_ONLY_REG = 0x5678;         // Legal: write to register
    // value = WRITE_ONLY_REG;        // Compilation error: attempt to read from write-only register
    
    READ_WRITE_REG = 0x9ABC;         // Legal: write
    value = READ_WRITE_REG;           // Legal: read
}

2. const Protection in Callback Functions

// Define callback function type, using const to protect user data
typedef void (*EventCallback)(const void * const user_data);

// Event handler structure
typedef struct {
    EventCallback callback;
    const void *user_data;  // User data cannot be modified through pointer
} EventHandler;

// Register event handler
void register_handler(EventHandler * const handler, 
                     EventCallback callback,
                     const void * const user_data) {
    handler->callback = callback;
    handler->user_data = user_data;  // Safe assignment, type matches
}

// Trigger event
void trigger_event(const EventHandler * const handler) {
    if(handler->callback) {
        handler->callback(handler->user_data);
    }
}

5. Advanced Techniques and Patterns

1. Creating Immutable Objects

// Expose only const interface in header file
typedef struct ImmutableData ImmutableData;

// Constructor returns const pointer
const ImmutableData *create_immutable_data(int value);

// All access methods use const
int get_data_value(const ImmutableData *data);

// Implementation details in .c file
struct ImmutableData {
    const int value;
    const time_t create_time;
};

const ImmutableData *create_immutable_data(int value) {
    static ImmutableData instance;  // In real projects, more complex memory management may be needed
    instance.value = value;
    instance.create_time = time(NULL);
    return &instance;
}

int get_data_value(const ImmutableData *data) {
    return data->value;
}

2. Type-Safe Container Implementation

// Generic container interface
typedef struct {
    const void * (*get)(const void *container, size_t index);
    size_t (*size)(const void *container);
} ContainerInterface;

// Specific array container implementation
typedef struct {
    const ContainerInterface *vtable;
    const int *data;
    size_t length;
} ArrayContainer;

static const void *array_get(const void *container, size_t index) {
    const ArrayContainer *arr = container;
    if(index >= arr->length) return NULL;
    return &arr->data[index];
}

static size_t array_size(const void *container) {
    const ArrayContainer *arr = container;
    return arr->length;
}

static const ContainerInterface ARRAY_VTABLE = {
    .get = array_get,
    .size = array_size
};

// Create array container (returns const interface pointer)
const ContainerInterface *create_array_container(const int *data, size_t length) {
    static ArrayContainer instance;  // Simplified example, should be dynamically allocated in practice
    instance.vtable = &ARRAY_VTABLE;
    instance.data = data;
    instance.length = length;
    return (const ContainerInterface *)&instance;
}

// Usage example
void process_container(const ContainerInterface *container) {
    size_t size = container->size(container);
    for(size_t i = 0; i < size; i++) {
        const int *element = container->get(container, i);
        printf("%d ", *element);
    }
}

6. Common Pitfalls and Best Practices

1. Errors to Avoid

// Error 1: Ignoring const leads to undefined behavior
const int x = 10;
int *p = (int *)&x;  // Dangerous type cast
*p = 20;              // Undefined behavior!

// Error 2: Returning pointer to local variable
const char *bad_string(void) {
    char str[] = "Hello";  // Local array
    return str;            // After return, str will be destroyed
}

// Error 3: Misunderstanding multi-level const
const int **pp;
int *p;
pp = &p;  // Compiler warning: may modify const int through *pp

2. Best Practice Recommendations

  1. Default to using const: Always use const unless modification is explicitly needed
  2. Read declarations from right to left: Helps understand complex pointer declarations
  • <span>const char * const p</span>: Read from right to left as “p is a constant pointer pointing to a char constant”
  • Use typedef to simplify complex declarations:
    typedef const char * ConstString;
    typedef char * const StringConst;
    
  • Static Analysis Tools: Use tools to check const correctness
  • API Design Principles:
    • Input parameters: use const whenever possible
    • Output parameters: do not use const
    • Input/Output parameters: do not use const

    7. Comprehensive Application Example: Safe String Handling Library

    // safe_string.h
    #ifndef SAFE_STRING_H
    #define SAFE_STRING_H
    
    #include <stddef.h>
    
    // Immutable string type
    typedef const char * const ImmutableString;
    
    // Create safe string (dynamically allocated)
    ImmutableString create_immutable_string(const char *source);
    
    // Free safe string
    void free_immutable_string(ImmutableString *str);
    
    // Safe string concatenation (returns new string)
    ImmutableString immutable_string_concat(ImmutableString s1, ImmutableString s2);
    
    // Safe string comparison
    int immutable_string_compare(ImmutableString s1, ImmutableString s2);
    
    #endif // SAFE_STRING_H
    
    // safe_string.c
    #include "safe_string.h"
    #include <stdlib.h>
    #include <string.h>
    
    ImmutableString create_immutable_string(const char *source) {
        if(!source) return NULL;
        
        char *buffer = malloc(strlen(source) + 1);
        if(!buffer) return NULL;
        
        strcpy(buffer, source);
        return buffer;
    }
    
    void free_immutable_string(ImmutableString *str) {
        if(str && *str) {
            free((void *)*str);  // Need to remove const qualifier
            *str = NULL;
        }
    }
    
    ImmutableString immutable_string_concat(ImmutableString s1, ImmutableString s2) {
        if(!s1 || !s2) return NULL;
        
        size_t len1 = strlen(s1);
        size_t len2 = strlen(s2);
        char *buffer = malloc(len1 + len2 + 1);
        if(!buffer) return NULL;
        
        strcpy(buffer, s1);
        strcat(buffer, s2);
        return buffer;
    }
    
    int immutable_string_compare(ImmutableString s1, ImmutableString s2) {
        if(!s1 && !s2) return 0;
        if(!s1) return -1;
        if(!s2) return 1;
        return strcmp(s1, s2);
    }
    
    // Usage example
    void example_usage(void) {
        ImmutableString s1 = create_immutable_string("Hello");
        ImmutableString s2 = create_immutable_string(" World");
        
        ImmutableString combined = immutable_string_concat(s1, s2);
        printf("Combined: %s\n", combined);
        
        free_immutable_string(&s1);
        free_immutable_string(&s2);
        free_immutable_string(&combined);
    }
    

    Conclusion

    The combination of pointers and const in C is a powerful tool for writing robust and safe code. By properly applying the const qualifier, one can:

    1. Clearly express design intent, enhancing code readability
    2. Catch potential errors at compile time, improving code safety
    3. Optimize program performance (the compiler can perform more optimizations on const objects)
    4. Create better API interfaces, preventing misuse

    Mastering the various combinations and semantics of const and pointers is a necessary path to becoming an advanced C programmer. It is recommended to practice in real projects to gradually cultivate a “const first” programming habit, which will significantly improve code quality and reliability.

    Leave a Comment