Advanced C

Everything before this page was the everyday 90%. This chapter opens the door to the idioms that real C systems use daily: callbacks for late binding, bit manipulation for flags and protocols, endianness for networking, and the compiler contracts restrict, volatile, and type punning. These are the tools that make C irreplaceable — and the last things beginners learn.

Function Pointers & Callbacks

A function pointer stores which function to call, decided at runtime. The declaration reads right-to-left from the name: int (*cmp)(const void*, const void*) is "cmp is a pointer to a function returning int". You already used one — qsort's comparator on the algorithms page. Pattern: a library function receives a callback it cannot know about, letting your code run inside its loops.

#include <stdio.h>

typedef int (*Matcher)(int);    // "pointer to function(int) -> int"

// a generic scanner: apply the callback to every element
int scan(const int a[], size_t n, Matcher m) {
    for (size_t i = 0; i < n; i++) {
        int r = m(a[i]);        // call through the pointer
        if (r != 0) return r;   // callback says stop? return its verdict
    }
    return 0;
}

static int is_odd(int v)  { return v % 2; }          // callback candidates
static int is_even(int v) { return !(v % 2); }

int main(void) {
    int a[] = {2, 4, 5, 8};
    printf("first odd:  %d\n", scan(a, 4, is_odd));   // 5
    printf("first even: %d\n", scan(a, 4, is_even));  // 2 — same loop, new policy
    return 0;
}

Dynamic Dispatch — a VTable in C

Object-oriented dispatch is, mechanically, a struct of function pointers. C can build the same table — a vtable — and choose the table at runtime. This is how C drivers and VFS layers implement "interfaces": the caller holds a struct of operations and invokes them by name.

#include <stdio.h>

// a shape interface: two operations, implemented by different "classes"
typedef struct {
    double (*area)(double);       // method pointers
    double (*perimeter)(double);
} ShapeOps;

static double circle_area(double r) { return 3.14159 * r * r; }
static double circle_perimeter(double r) { return 2 * 3.14159 * r; }
static double square_area(double s) { return s * s; }
static double square_perimeter(double s) { return 4 * s; }

int main(void) {
    ShapeOps circle = {circle_area, circle_perimeter};   // "instance" tables
    ShapeOps square = {square_area, square_perimeter};
    double side = 3.0;
    printf("circle: A=%.2f P=%.2f\n", circle.area(side), circle.perimeter(side));
    printf("square: A=%.2f P=%.2f\n", square.area(side), square.perimeter(side));
    return 0;
}

Bit Manipulation

C treats integers as bit fields, and systems code lives in the bits: flags-packed registers, protocol headers, and compact sets. The four canonical operations — set, clear, toggle, test — are one expression each:

#include <stdio.h>

#define FLAG_A (1u << 0)   // bit 0 = 0b0001
#define FLAG_B (1u << 1)   // bit 1 = 0b0010
#define FLAG_C (1u << 2)   // bit 2 = 0b0100

int main(void) {
    unsigned flags = 0;
    flags |= FLAG_A;            // set: OR the bit on
    flags |= FLAG_C;
    flags &= ~FLAG_A;           // clear: AND with the inverted bit
    flags ^= FLAG_B;            // toggle: XOR flips the bit
    printf("flags = 0x%x\n", flags);              // 0x6 (bits B and C)
    if (flags & FLAG_C) {        // test: AND, nonzero means set
        printf("FLAG_C is set\n");
    }
    return 0;
}

Read them aloud and they stop being magic: set = OR, clear = AND-NOT, toggle = XOR, test = AND. Shifts also implement fast multiply/divide by powers of two (x << 3 = x·8) — but write the intent; compilers emit the same code from * 8.

Endianness

Endianness is the byte order of a multi-byte integer in memory. Little-endian (x86, most ARM) stores the least-significant byte first; big-endian (network protocols, older CPUs) stores the most-significant first. Two machines exchanging integers must convert to network byte order (big-endian) or they will read scrambled values.

#include <stdio.h>
#include <stdint.h>

// detect this machine's byte order at runtime
const char *byte_order(void) {
    uint32_t probe = 1;
    return *(uint8_t *)&probe ? "little-endian" : "big-endian";
}

int main(void) {
    printf("this machine is %s\n", byte_order());
    printf("49 in memory: ");              // 0x31 = 49 decimal
    uint32_t v = 49;
    uint8_t *b = (uint8_t *)&v;
    for (int i = 0; i < 4; i++) printf("%02x ", b[i]);  // 31 00 00 00 on LE
    printf("\n");
    return 0;
}

In networking code, use htonl/ntohl (and the 16-bit variants) rather than rolling your own — the standard library functions convert correctly on every platform.

restrict & volatile

Two qualifiers are compiler contracts. restrict (pointers) promises the pointed-to memory is accessed only through this pointer for the block's lifetime — that promise enables aggressive vectorization, and breaking it is undefined behavior. volatile tells the compiler a variable may change outside the program (hardware registers, signals), forbidding it from caching the value in a register across reads.

// restrict: only dst and only src alias their memory — compiler may vectorize
void copy_array(int *restrict dst, const int *restrict src, size_t n) {
    for (size_t i = 0; i < n; i++) dst[i] = src[i];
}

volatile unsigned int timer_reg;   // memory-mapped hardware counter

Use restrict in hot library functions after proving no aliasing; use volatile only for memory the program does not control. Everywhere else, both are wrong.

Type Punning

Reinterpreting the same bytes as another type is type punning. The strict-aliasing rule forbids most of it (the compiler may assume different types do not alias), but two legal routes exist: memcpy (the portable way) and, in practice, union members. Use this power for reading binary files and protocols — understand that it sacrifices safety for control, which is the C bargain in miniature.

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

int main(void) {
    double d = 1.5;
    uint64_t bits;
    memcpy(&bits, &d, sizeof(bits));   // bit pattern of the double
    printf("1.5 as bits: %016llx\n", (unsigned long long)bits);
    return 0;
}

This one trick — reading the exact bit pattern of a float — is how every modern libc implements functions like sqrt and log fast, and how you will decode protocol headers byte by byte.

Next Lessons

From here the track goes deeper into systems C. Parallel & multicore processing puts threads, atomics, and OpenMP to work; GPU computing then moves the hot loops to thousands of cores. ABI & assembly closes the lessons by showing what the machine really does at every function boundary. The lab examples and study projects turn it all into practice, and references & compilers points you onward.