Mastering C Preprocessor Directives, Pointers, and Structured Data Types

  1. Preprocessor Directives and Header Inclusion

The C preprocessor executes before compilation begins, handling directives that manipulate source code. One of the most fundamental directives is file inclusion, which merges the contents of an external file directly into the curent source file.

Header Inclusion Syntax

The preprocessor supports two distinct inclusion patterns, each dictating a different file resolution strategy:

  • #include "filename.h": The compiler searches the local project directory first. If the file is absent, it falls back to the standard system include paths.
  • #include <filename.h>: The compiler skips the local directory entirely and searches exclusively within predefined system library directories.

While the preprocessor can technically insert any file extension, convention dictates using this mechanism for header files (.h). Angle brackets are standard for system libraries, while quotation marks are reserved for project-specific or custom headers.

  1. Memory Addresses and Pointer Fundamentals

In C, memory is modeled as a continuous sequence of bytes, each assigned a unique numerical identifier known as an address. The width of these addresses depends on the system architecture (32-bit or 64-bit). Every variable declared in a program occupies a specific memory location.

The sizeof Operator

The sizeof keyword evaluates the memory footprint of a variable or data type in bytes. It returns a value of type size_t, which should be printed using the %zu format specifier.

#include <stdio.h>

int main(void) {
    int numeric_val = 42;
    char alpha_val = 'Z';
    
    size_t int_bytes = sizeof(numeric_val);
    size_t double_bytes = sizeof(double);
    
    printf("Integer size in bytes: %zu\n", int_bytes);
    printf("Double size in bytes: %zu\n", double_bytes);
    
    return 0;
}

Defining and Using Pointers

A pointer variable stores the memory address of another variable. The address-of operator (&) retrieves a variable's location, while the dereference operator (*) accesses or modifies the value stored at that location. Note that variables explicitly allocated to CPU registers cannot have their addresses taken, as they reside outside standard RAM.

#include <stdio.h>

int main(void) {
    int base_value = 100;
    char symbol = 'X';
    long long hex_addr = 0xDEADBEEF;
    
    printf("Address of base_value: %p\n", (void*)&base_value);
    printf("Address of symbol: %p\n", (void*)&symbol);
    
    int* ptr_to_int = &base_value;
    *ptr_to_int = 250; // Modifying value via pointer
    
    printf("Updated base_value: %d\n", base_value);
    return 0;
}

The size of a pointer variable itself depends on the architecture: 4 bytes on 32-bit systems and 8 bytes on 64-bit systems, regardless of the underlying data type it references.

  1. Pointers in Functions and Advanced Operations

Modifying Arguments via Pointers

C passes function arguments by value, creating local copies. To allow a function to modify the original variables, pointers must be used to pass memory addresses instead.

#include <stdio.h>

void exchange_values(int* first, int* second) {
    int temporary = *first;
    *first = *second;
    *second = temporary;
}

int main(void) {
    int x = 10;
    int y = 20;
    
    exchange_values(&x, &y);
    printf("x: %d, y: %d\n", x, y);
    return 0;
}

Arrays and Function Parameters

When an array name is passed to a function, it automatically decays into a pointer to its first element. This behavior enables efficient in-place manipulation of the original array data with out copying the entire dataset.

#include <stdio.h>

void organize_data(int* dataset, int count) {
    for (int i = 0; i < count - 1; ++i) {
        for (int j = 0; j < count - i - 1; ++j) {
            if (dataset[j] > dataset[j + 1]) {
                int temp = dataset[j];
                dataset[j] = dataset[j + 1];
                dataset[j + 1] = temp;
            }
        }
    }
}

int main(void) {
    int numbers[] = {9, 2, 7, 1, 5, 8, 3, 6, 4, 0};
    organize_data(numbers, 10);
    
    for (int i = 0; i < 10; ++i) {
        printf("%d ", numbers[i]);
    }
    printf("\n");
    return 0;
}

Returning Pointers from Functions

Functions can return pointer types. It is critical to ensure that the returned address points to valid, long-lived memory, such as a global variable or heap-allocated space. Returning addresses of local variables results in undefined behavior due to stack frame destruction.

#include <stdio.h>

int global_store = 50;

int* fetch_reference(void) {
    return &global_store;
}

int main(void) {
    int* ref_ptr = fetch_reference();
    printf("Address from function: %p\n", (void*)ref_ptr);
    printf("Direct global address: %p\n", (void*)&global_store);
    
    *ref_ptr = 999;
    printf("Global value after modification: %d\n", global_store);
    return 0;
}

Pointer Arithmetic

Pointer arithmetic is scale-aware. Incrementing a pointer moves it forward by the size of its pointed-to type. Subtracting two pointers of the same type yields the number of elements between them, not the raw byte difference.

#include <stdio.h>

int main(void) {
    int dataset[] = {10, 20, 30, 40, 50};
    int* start_ptr = dataset;
    int* mid_ptr = &dataset[2];
    
    // Moving forward by type size
    printf("Value at offset 1: %d\n", *(start_ptr + 1));
    
    // Calculating element offset
    long distance = mid_ptr - start_ptr;
    printf("Element offset between pointers: %ld\n", distance);
    
    return 0;
}

Multi-Level Pointers and Pointer Arrays

An array of pointers stores multiple addresses. A pointer to a pointer (double pointer) allows modification of another pointer's target. This pattern extends recursively to triple pointers and beyond.

#include <stdio.h>

int main(void) {
    int val_a = 10;
    int val_b = 20;
    
    int* single_ptr = &val_a;
    int** double_ptr = &single_ptr;
    
    *double_ptr = &val_b; // Redirects single_ptr to val_b
    **double_ptr = 30;   // Modifies val_b through double dereferencing
    
    printf("val_a: %d\n", val_a);
    printf("val_b: %d\n", val_b);
    return 0;
}
  1. Type Aliasing and Structured Data

Using typedef

The typedef keyword creates an alias for an existing data type. Unlike #define, which performs textual substitution during preprocessing, typedef is processed by the compiler and is strictly limited to type definitions.

#include <stdio.h>

typedef unsigned long long ulong;
typedef int integer;

int main(void) {
    ulong big_num = 15000000000ULL;
    integer standard_num = 42;
    printf("%llu %d\n", big_num, standard_num);
    return 0;
}

Defining and Initializing Structures

Structures group heterogeneous data types under a single name. They are ideal for modeling real-world entities with multiple attributes. String members with in structures require standard library functions like strcpy for assignment, as array names decay to constant addresses.

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

typedef struct {
    int student_id;
    char full_name[30];
    char gender;
    int score;
    int age;
} StudentRecord;

int main(void) {
    StudentRecord entry = {1001, "Alex Johnson", 'M', 88, 20};
    
    printf("ID: %d | Name: %s | Score: %d\n", entry.student_id, entry.full_name, entry.score);
    
    // Modifying string members requires standard library functions
    strcpy(entry.full_name, "Jordan Lee");
    printf("Updated Name: %s\n", entry.full_name);
    
    return 0;
}

Structure Arrays and Sorting

Arrays of structures allow batch processing of complex records. Sorting can be implemented by comparing specific member fields and swapping entire structure blocks.

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

typedef struct {
    int id;
    char title[20];
    float rating;
} Product;

void rank_by_rating(Product* catalog, int size) {
    for (int i = 0; i < size - 1; ++i) {
        for (int j = 0; j < size - i - 1; ++j) {
            if (catalog[j].rating < catalog[j + 1].rating) {
                Product temp = catalog[j];
                catalog[j] = catalog[j + 1];
                catalog[j + 1] = temp;
            }
        }
    }
}

int main(void) {
    Product inventory[3] = {
        {1, "Laptop", 4.5},
        {2, "Mouse", 3.8},
        {3, "Keyboard", 4.2}
    };
    
    rank_by_rating(inventory, 3);
    
    for (int i = 0; i < 3; ++i) {
        printf("ID: %d | %s | Rating: %.1f\n", inventory[i].id, inventory[i].title, inventory[i].rating);
    }
    return 0;
}

Nested Structures and Assignment

Structures can contain other structures as members, enabling hierarchical data modeling. Assignment between structure variables performs a member-by-member copy, making the newly assigned variable completely independent of the source.

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

typedef struct {
    char brand[20];
    int year;
} Engine;

typedef struct {
    char model[20];
    Engine motor;
    int mileage;
} Vehicle;

int main(void) {
    Vehicle car1;
    strcpy(car1.model, "Sedan X");
    strcpy(car1.motor.brand, "V8 Turbo");
    car1.motor.year = 2022;
    car1.mileage = 15000;
    
    Vehicle car2 = car1; // Shallow copy
    car2.mileage = 20000; // Modifying copy does not affect car1
    
    printf("Car1 Mileage: %d\n", car1.mileage);
    printf("Car2 Mileage: %d\n", car2.mileage);
    return 0;
}

Tags: C pointers Structures preprocessor MemoryManagement

Posted on Mon, 07 Sep 2026 16:52:00 +0000 by dig412