Advanced C
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.