Various Notes on C Programming

A collection of miscellaneous hints and tips on writing code in the C programming language, not intended as a guide for beginners.

Contents

Memory Management in C Programs

C provides only minimal automatic memory management, making code prone to memory leaks (where allocated memory is not freed after its lifecycle ends) and memory corruption (e. g., overwriting data via a dangling pointer that references already reassigned memory). Conversely, stack memory occupied by a function’s local variables is automatically reclaimed when the function returns.

Freeing Dynamically Allocated Memory

Programs frequently allocate memory dynamically at runtime using the functions malloc, calloc, and realloc. On most computer architectures, this memory is allocated on the heap, and it must be manually released for reuse by passing the original pointer to the free function once its lifecycle ends. The example below dynamically creates a memory buffer based on user input, populates it with values entered by the user, and subsequently deallocates it:

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

int main(void)
{
    printf("Enter the size of the storage: ");
    int size;
    scanf("%d", &size);
    if (size <= 0)
    {
        printf("Size must be greater than 0.\n");
        return EXIT_FAILURE;
    }
    int *numbers = malloc(sizeof(int) * size);
    if (numbers == NULL)
    {
        printf("Memory allocation failed.\n");
        return EXIT_FAILURE;
    }
    for (int i = 0; i < size; i++)
    {
        printf("Enter a number: ");
        int input;
        scanf("%d", &input);
        numbers[i] = input;
    }
    for (int i = 0; i < size; i++)
        printf("numbers[%d] = %d\n", i, numbers[i]);
    free(numbers);
    numbers = NULL;
    return EXIT_SUCCESS;
}

It is generally best practice to set pointers to NULL immediately after calling free to prevent dangling pointer dereferences. Additionally, declaring a pointer variable as read-only using const t *const v prevents both losing the original address through reassignment and modifying the underlying data it references.

In more complex data structures, a clear ownership policy must be established to define which component is responsible for deallocating memory when objects are destroyed. A linked list consisting of individual nodes serves as a prime example:


// Forward declaration required because nodes store pointers to nodes.
typedef struct node node;

typedef struct node
{
    node *next;
    void *value;
};

Each Node instance stores a pointer to the subsequent node in its next member and a pointer to the associated payload within its value member. Implementing this seemingly straightforward data structure raises several critical questions regarding memory management:

  1. Should nodes be allocated exclusively on the heap via dedicated list functions, or can they be instantiated independently and inserted later?

  2. Must the list implementation deallocate a node’s memory when it is removed? Automatically doing so would leave dangling pointers in any external references to that node, which the list component cannot safely set to NULL.

  3. Should the external data referenced by the value member be freed upon node deletion? This becomes critical if the node holds the sole remaining reference to that dynamically allocated memory.

One potential solution to these linked list management challenges is structured as follows:

Unfortunately, C lacks built-in language features to enforce these policies, such as data encapsulation, smart pointer types, or automatic garbage collection. Consequently, developers must rely entirely on clear architectural documentation and disciplined implementation practices.

Utilizing an arena allocator can significantly mitigate these challenges. An arena is a contiguous block of pre-allocated memory dedicated to a specific data structure and managed directly by the application. For instance, when hosting a linked list within an arena, the entire memory block is released at once when the list is destroyed, rather than allocating and freeing individual nodes via malloc and free. This approach guarantees that all memory occupied by the nodes is reclaimed simultaneously. Depending on the workload, some internal memory management—such as tracking and recycling space from deleted elements within the arena—may still be required.

Recommendations

The principles of object memory management can be generalized as follows:

  1. The component that instantiates an object (the entity performing the dynamic allocation) bears sole responsibility for its deallocation.

  2. Objects must be manipulated exclusively through dedicated functions.

  3. External object references should be stored sparingly and retained only for the duration of the required operation—such as fetching the pointer, executing the task, and immediately setting the reference to NULL—to minimize the window for dangling pointer vulnerabilities.

Declaration of Local Variables

Modern C allows local variables to be declared almost anywhere within a function body. The C99 standard lifted the restriction requiring variables to be declared at the very top of a block (directly following the opening {). The snippet below illustrates the visibility and scope of local variables declared in various locations:

void f(void)
{
    int v1;       // Compiles.
    g();
    int v2;       // Compiles.  (Did not compile prior to C99.)

    while (1)
    {
        int v3;   // Compiles.
        int v1;   // Compiles.  Note the same name as the variable `i` above,
                  //   this is called _variable shadowing_.
        g();
        int v4;   // Compiles.  (Did not compile prior to C99.)
    }

    g(v4);        // Does not compile.  `v4` only visible in the `while` loop.
    v1 = v2;      // `v1` refers to variable declared on top of the function.

    for (int v1 = 0; i < 10; i++)   // Compiles.  Variable shadowing of `v1`.
        v2 = v1;                    // Variables declared in the `for` loop's
                                    //   head are also visible in its body.

    v5 = 10;      // Does not compile.  Variables can only be used below their
    int v5;       //   declaration.

    {             // Unnamed scope.
        int v1;   // Compiles.  Variable shadowing of `v1`.
        int v6;   // Compiles.  Variable only visible in the containing block.
    }
    v6 = 10;      // Does not compile.  Variable `v6` not declared.
}

Under the C99 standard, variables cannot be declared directly after a label (case, default, or a named jump label), because the syntax expects a statement to immediately follow the label. This limitation can be bypassed either by placing an empty statement before the variable declaration or by enclosing the declaration within an unnamed block scope directly after the label.

Recommendations

The visibility of variables should be minimized as much as possible. Most variables—especially helper variables and those of minor importance—should be declared within the specific section of code where they are used. Whenever possible, variables should be declared directly inside the narrowest scope where they are needed:

void f(const char s[const], const size_t len)
{// Variables `i` and `c` are only available in the `for` loop's scope.
    for (int i = 0; i < len; i++)
    {
        char c = s[i];}}

Read-Only Function Parameters Using const

Function parameters can be qualified as const to make them read-only, enabling both compile-time error detection and potential optimizations by the compiler. Qualifying a variable as read-only prevents its value from being modified directly through its identifier. While the value can technically still be altered by casting the variable to a non-const type, doing so is an intentional override. Enforcing immutability this way significantly helps prevent accidental data modification.

The principles governing const function parameters apply equally to const variables. All compiler errors and warnings in the examples below are based on GCC version 11.4.0.

Uses of const for Function Parameters

Qualifying value-type parameters as const makes them read-only within the function body:

void f1(const char c)   // Semantically equivalent to `void f1(char const c)`.
{

    // GCC error: assignment of read-only parameter ‘c’
    c = 'a';
}

Pointer-type function parameters can also be qualified as const to prevent the referenced data from being modified within the function body:

void f2(const char *s)   // Semantically equivalent to `void f2(char const *s)`.
{

    // Compiles.
    s = "Test";

    // Compiles.
    s++;

    // GCC error: assignment of read-only location ‘*s’
    *s = 'a';
}

void f3(char *const s)
{

    // GCC error: assignment of read-only parameter ‘s’
    s = "Test";

    // GCC error: increment of read-only parameter ‘s’
    s++;

    // Compiles.
    *s = 'a';
}

void f4(const char *const s)
{

    // GCC error: assignment of read-only parameter ‘s’
    s = "Test";

    // GCC error: increment of read-only parameter ‘s’
    s++;

    // GCC error: assignment of read-only location ‘*(const char *)s’
    *s = 'a';
}

Array function parameters can also be qualified as const:

void f5(char s[const])
{

    // GCC error: assignment of read-only parameter ‘s’
    s = "Test";

    // GCC error: increment of read-only parameter ‘s’
    s++;

    // Compiles.
    s[2] = 'a';
}

void f6(const char s[const])
{

     // GCC error: assignment of read-only parameter ‘s’
    s = "Test";

    // GCC error: increment of read-only parameter ‘s’
    s++;

    // GCC error: assignment of read-only location ‘*(s + 2)’
    s[2] = 'a';
}

In C++, a common convention is to write the function signature as void f(const char* const s), placing the asterisk next to the data type. This declaration can be parsed from right to left as: s is a const pointer to a char that is const. Comprehensive explanations regarding these patterns are detailed in the article Const Correctness, C++ FAQ.

Practical Examples

Passing a string to a function as const char *const introduces the following behaviors:

char *read_file(const char *const filename)
{}

Ensuring that values—such as IDs, positions, or indices—cannot be modified within the function body:

void show_record(const int id)
{

    // Erroneous assignment (`=`) instead of comparison (`==`).
    // GCC error: assignment of read-only parameter ‘s’
    if (id = 100)
        ;
}

Passing a read-only array to a function:

void show_users(const char user_ids[const])
{}

Passing a read-only string to a function and iterating over its characters introduces the following constraints:

  1. A type cast to a non-const pointer is required and should only be performed if the variable is only used for read access.

  2. Pointer arithmetic using the parameter s—such as while ((c = *s++) != '\0')—will trigger a compilation error because s is qualified as const char *const.

  3. The local pointer variable cp must be qualified as const to ensure that it cannot be used to modify the characters of the string.

void prints(const char *const s)
{
    const char *cp = (const char *)s;   // <-------------------------------- (1)
    char c;
    while ((c = *cp++) != '\0')
        putc(c, stdout);
}

Note that even strings passed to a function via a const char *const parameter can be altered by external references, though not through the parameter itself:

  1. Applying both const qualifiers is inherently contradictory for a function intended to modify data, such as an in-place toupper implementation.

  2. This function serves as a naïve implementation for demonstration purposes only.

#include <stdlib.h>

void toupper_bad(const char *const s)   // <-------------------------------- (1)
{
    char *cp = (char *)s;
    char c;
    while ((c = *cp) != '\0')
    {
        if (c >= 'a' && c <= 'z')
            *cp = c - 32;
        cp++;
    }
}

int main(void)
{
    char s[] = "Hello World!";
    toupper_bad(s);
    printf("%s\n", s);   // Output:  "HELLO WORLD!"
    return EXIT_SUCCESS;
}

Functions Without Parameters

Functions without parameters should be declared and defined as t f(void) in older standards. Starting with C23, they can be natively declared and defined using empty parentheses as t f().

Convention for Writing Pointers

The asterisk * used for pointer declarations should be placed next to the variable or function name. This practice prevents visual confusion with multiplication operations (v1 * v2) and clarifies the actual types in combined variable declarations; for example, t *v1, v2 declares v1 as a pointer of type t *, whereas v2 is declared as a standard variable of type t:

char *reverse(char *text);

int main(int argc, char **argv);

int *prev, *next;

int value = *prev;

int *prev = (int *)first;

int *next = &value;

The parameter declaration char **argv denotes a pointer to a pointer to char and is equivalent to char *argv[], which represents an array of pointers to char. While formatting the parameter declarations as char* argv[] or char* *argv might arguably make these semantics more apparent, doing so would break consistency with the convention used in variable declarations (e.g., t *v1[], v2[] or t **v1, v2).

In C, qualifiers such as const are traditionally placed before the data type in variable and parameter declarations, as seen in const char *const c. In the declaration const char *c1, *c2, both variables are defined as pointers to a const char. The const qualifier is written before the type because—unlike the pointer asterisk *—it modifies the base type rather than the individual identifier (hence, const char *c1, const *c2 triggers a compilation error). Consequently, when declaring constant pointers, const must be explicitly specified for each variable in a comma-separated list; for example, char *const c1, *const c2 correctly declares two distinct constants that are const pointers to a char.

C Programming with the Code::Blocks IDE

Starting a program with arguments
Menu ProjectSet program’s arguments….
Changing the terminal used to launch console programs
Menu SettingsEnvironment…, section General settings, option Terminal to launch console programs. Various presets are available. The pre-configured Xterm is suboptimal, as it lacks user-friendly text-copying functionality.

Terminology Used in C Programming

length
The number of elements in an array or the number of characters in a string, excluding the null terminator.
size
The data size in bytes, typically determined using the sizeof operator and represented by the size_t type. Size values can be constrained by specific minimum (MIN) and maximum (MAX) limits.
OnlineGDB – Online C Compiler
Develop, run, and debug C programs online.
Code::Blocks
A free and open-source IDE for C and C++ that runs cross-platform across various operating systems.
C reference – cppreference.com
A compact yet comprehensive reference for the C programming language and the C standard library.