|$ curl https://forge-ai.dev/api/markdown?path=docs/c/advanced-pointers
$cat docs/c-—-advanced-pointers.md
updated Recently·50 min read·published

C — Advanced Pointers

CPointersFunction PointersAdvancedAdvanced🎯Free Tools
Function Pointers

A function pointer stores the address of a function. Unlike data pointers, function pointers point to executable code, not data. They enable callbacks, dynamic dispatch, and strategy patterns — fundamental building blocks of flexible C programs.

function_pointer_basics.c
C
1#include <stdio.h>
2
3/* Regular functions */
4int add(int a, int b) { return a + b; }
5int subtract(int a, int b) { return a - b; }
6int multiply(int a, int b) { return a * b; }
7
8int main(void) {
9 /* Declare a function pointer: return_type (*name)(param_types) */
10 int (*operation)(int, int);
11
12 /* Assign the address of a function (no & needed, but allowed) */
13 operation = add;
14 printf("add: %d\n", operation(3, 4)); /* Output: 7 */
15
16 /* Reassign to a different function */
17 operation = subtract;
18 printf("subtract: %d\n", operation(10, 3)); /* Output: 7 */
19
20 operation = multiply;
21 printf("multiply: %d\n", operation(5, 6)); /* Output: 30 */
22
23 /* Using & is optional but adds clarity */
24 operation = &add;
25 printf("add via &: %d\n", operation(2, 3)); /* Output: 5 */
26
27 return 0;
28}
function_pointer_passed.c
C
1#include <stdio.h>
2
3/* Function pointers work with any matching signature */
4double apply(double x, double y, double (*func)(double, double)) {
5 return func(x, y);
6}
7
8double divide(double a, double b) { return a / b; }
9double power(double a, double b) {
10 double result = 1.0;
11 for (int i = 0; i < (int)b; i++) result *= a;
12 return result;
13}
14
15int main(void) {
16 /* Pass function pointers as arguments */
17 printf("10 / 3 = %.2f\n", apply(10.0, 3.0, divide)); /* 3.33 */
18 printf("2 ^ 8 = %.0f\n", apply(2.0, 8.0, power)); /* 256 */
19 return 0;
20}
Typedef for Function Pointers

Function pointer syntax is verbose and error-prone. Using typedef creates a type alias that makes declarations readable and reduces mistakes. This is especially valuable for callback-heavy code.

typedef_fnptr.c
C
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4
5/* Define type aliases for function pointer signatures */
6typedef int (*compare_fn)(const void*, const void*);
7typedef void (*destroy_fn)(void*);
8typedef void (*print_fn)(const void*);
9typedef double (*transform_fn)(double);
10
11/* A struct that holds function pointers — a "vtable" / strategy pattern */
12typedef struct {
13 compare_fn compare;
14 destroy_fn destroy;
15 print_fn print;
16 const char* name;
17} CollectionOps;
18
19/* Functions that accept the type alias */
20void sort_array(void* arr, size_t n, size_t elem_size, compare_fn cmp) {
21 qsort(arr, n, elem_size, cmp);
22}
23
24void transform_array(double* arr, size_t n, transform_fn fn) {
25 for (size_t i = 0; i < n; i++) {
26 arr[i] = fn(arr[i]);
27 }
28}
29
30/* Concrete implementations for the ops struct */
31static int int_compare(const void* a, const void* b) {
32 return (*(const int*)a - *(const int*)b);
33}
34
35static void int_print(const void* val) {
36 printf("%d", *(const int*)val);
37}
38
39static void int_destroy(void* val) {
40 free(val);
41}
42
43int main(void) {
44 CollectionOps int_ops = {
45 .compare = int_compare,
46 .destroy = int_destroy,
47 .print = int_print,
48 .name = "integer"
49 };
50
51 printf("Collection type: %s\n", int_ops.name);
52
53 int nums[] = {5, 2, 8, 1, 9, 3};
54 size_t n = sizeof(nums) / sizeof(nums[0]);
55
56 sort_array(nums, n, sizeof(int), int_ops.compare);
57
58 printf("Sorted: ");
59 for (size_t i = 0; i < n; i++) {
60 int_ops.print(&nums[i]);
61 if (i < n - 1) printf(", ");
62 }
63 printf("\n");
64
65 return 0;
66}
Callback Example: qsort

The standard library qsort function takes a function pointer as its last argument. This is the canonical example of a callback — the sort algorithm calls your comparison function to determine element ordering.

qsort_callback.c
C
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4
5typedef struct {
6 char name[32];
7 int score;
8} Student;
9
10/* qsort callback: must return <0, 0, or >0 */
11static int compare_by_score(const void* a, const void* b) {
12 const Student* sa = (const Student*)a;
13 const Student* sb = (const Student*)b;
14 /* Descending order by score */
15 return sb->score - sa->score;
16}
17
18static int compare_by_name(const void* a, const void* b) {
19 const Student* sa = (const Student*)a;
20 const Student* sb = (const Student*)b;
21 return strcmp(sa->name, sb->name);
22}
23
24static void print_student(const Student* s) {
25 printf(" %-15s %3d\n", s->name, s->score);
26}
27
28int main(void) {
29 Student students[] = {
30 {"Alice", 92},
31 {"Bob", 85},
32 {"Charlie", 98},
33 {"Diana", 77},
34 {"Eve", 95},
35 };
36 size_t n = sizeof(students) / sizeof(students[0]);
37
38 /* Sort by score (descending) */
39 qsort(students, n, sizeof(Student), compare_by_score);
40 printf("By score:\n");
41 for (size_t i = 0; i < n; i++) print_student(&students[i]);
42
43 printf("\n");
44
45 /* Sort by name (alphabetical) */
46 qsort(students, n, sizeof(Student), compare_by_name);
47 printf("By name:\n");
48 for (size_t i = 0; i < n; i++) print_student(&students[i]);
49
50 return 0;
51}
Array of Function Pointers: Dispatch Tables

An array of function pointers creates a dispatch table (jump table) — a data-driven way to select behavior. This replaces long switch statements with array lookups, improving extensibility and readability.

dispatch_table.c
C
1#include <stdio.h>
2#include <stdlib.h>
3
4/* Calculator operations */
5static double op_add(double a, double b) { return a + b; }
6static double op_sub(double a, double b) { return a - b; }
7static double op_mul(double a, double b) { return a * b; }
8static double op_div(double a, double b) { return b != 0.0 ? a / b : 0.0; }
9static double op_mod(double a, double b) { return b != 0.0 ? (int)a % (int)b : 0.0; }
10
11/* Dispatch table: index maps to operation */
12typedef double (*op_fn)(double, double);
13
14static op_fn operations[] = {
15 op_add, /* 0: + */
16 op_sub, /* 1: - */
17 op_mul, /* 2: * */
18 op_div, /* 3: / */
19 op_mod, /* 4: % */
20};
21
22static const char* op_names[] = {
23 "add", "sub", "mul", "div", "mod"
24};
25
26#define NUM_OPS (sizeof(operations) / sizeof(operations[0]))
27
28static double execute(int op_index, double a, double b) {
29 if (op_index < 0 || (size_t)op_index >= NUM_OPS) {
30 fprintf(stderr, "Invalid operation: %d\n", op_index);
31 return 0.0;
32 }
33 printf(" %s(%.1f, %.1f) = ", op_names[op_index], a, b);
34 return operations[op_index](a, b);
35}
36
37int main(void) {
38 printf("Dispatch table demo:\n");
39 double results[] = {
40 execute(0, 10.0, 3.0), /* add */
41 execute(1, 10.0, 3.0), /* sub */
42 execute(2, 10.0, 3.0), /* mul */
43 execute(3, 10.0, 3.0), /* div */
44 execute(4, 10.0, 3.0), /* mod */
45 };
46 printf("Results: ");
47 for (size_t i = 0; i < NUM_OPS; i++) {
48 printf("%.2f ", results[i]);
49 }
50 printf("\n");
51
52 return 0;
53}
token_dispatch.c
C
1/* Tokenizer dispatch table example */
2#include <stdio.h>
3#include <string.h>
4#include <ctype.h>
5
6typedef enum { TOK_INT, TOK_FLOAT, TOK_IDENT, TOK_STRING, TOK_UNKNOWN } TokenType;
7
8typedef struct {
9 TokenType type;
10 const char* value;
11 size_t length;
12} Token;
13
14typedef void (*token_handler)(const Token*);
15
16static void handle_int(const Token* t) {
17 printf("Integer: %.*s\n", (int)t->length, t->value);
18}
19
20static void handle_float(const Token* t) {
21 printf("Float: %.*s\n", (int)t->length, t->value);
22}
23
24static void handle_ident(const Token* t) {
25 printf("Identifier: %.*s\n", (int)t->length, t->value);
26}
27
28static void handle_string(const Token* t) {
29 printf("String: %.*s\n", (int)t->length, t->value);
30}
31
32static void handle_unknown(const Token* t) {
33 printf("Unknown: %.*s\n", (int)t->length, t->value);
34}
35
36/* Dispatch table indexed by TokenType */
37static token_handler handlers[] = {
38 handle_int, /* TOK_INT */
39 handle_float, /* TOK_FLOAT */
40 handle_ident, /* TOK_IDENT */
41 handle_string, /* TOK_STRING */
42 handle_unknown /* TOK_UNKNOWN */
43};
44
45void process_token(const Token* t) {
46 if (t->type >= 0 && (size_t)t->type < sizeof(handlers) / sizeof(handlers[0])) {
47 handlers[t->type](t);
48 }
49}
50
51int main(void) {
52 Token tokens[] = {
53 {TOK_INT, "42", 2},
54 {TOK_FLOAT, "3.14", 4},
55 {TOK_IDENT, "foo", 3},
56 {TOK_STRING, "\"hi\"", 4},
57 };
58
59 for (size_t i = 0; i < 4; i++) {
60 process_token(&tokens[i]);
61 }
62 return 0;
63}
void* — The Universal Pointer

void* is a generic pointer that can point to any data type. It is the C mechanism for polymorphism — malloc returns void*, and generic data structures use it to store elements of any type. You must cast void* to the correct type before dereferencing.

generic_void_ptr.c
C
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4
5/* A generic swap that works with any data type */
6void generic_swap(void* a, void* b, size_t elem_size) {
7 unsigned char* pa = (unsigned char*)a;
8 unsigned char* pb = (unsigned char*)b;
9 for (size_t i = 0; i < elem_size; i++) {
10 unsigned char tmp = pa[i];
11 pa[i] = pb[i];
12 pb[i] = tmp;
13 }
14}
15
16/* A generic print function using a callback */
17typedef void (*print_fn)(const void*);
18
19void print_array(const void* arr, size_t n, size_t elem_size, print_fn print) {
20 const unsigned char* base = (const unsigned char*)arr;
21 printf("[");
22 for (size_t i = 0; i < n; i++) {
23 print(base + i * elem_size);
24 if (i < n - 1) printf(", ");
25 }
26 printf("]\n");
27}
28
29/* Concrete print functions */
30static void print_int(const void* val) { printf("%d", *(const int*)val); }
31static void print_double(const void* val) { printf("%.2f", *(const double*)val); }
32static void print_char(const void* val) { printf("'%c'", *(const char*)val); }
33
34int main(void) {
35 /* Works with int */
36 int a = 5, b = 10;
37 printf("Before swap: a=%d, b=%d\n", a, b);
38 generic_swap(&a, &b, sizeof(int));
39 printf("After swap: a=%d, b=%d\n\n", a, b);
40
41 /* Works with double */
42 double x = 3.14, y = 2.71;
43 printf("Before swap: x=%.2f, y=%.2f\n", x, y);
44 generic_swap(&x, &y, sizeof(double));
45 printf("After swap: x=%.2f, y=%.2f\n\n", x, y);
46
47 /* Generic array printing */
48 int nums[] = {10, 20, 30, 40, 50};
49 print_array(nums, 5, sizeof(int), print_int);
50
51 double vals[] = {1.1, 2.2, 3.3};
52 print_array(vals, 3, sizeof(double), print_double);
53
54 return 0;
55}
Generic Linked List with void*

A generic linked list stores void* in each node, allowing it to hold any data type. The caller provides functions for printing, comparing, and freeing elements. This is how C achieves type-safe generic containers.

generic_linked_list.c
C
1#include <stdio.h>
2#include <stdlib.h>
3
4typedef struct Node {
5 void* data;
6 struct Node* next;
7} Node;
8
9typedef struct {
10 Node* head;
11 size_t size;
12 size_t elem_size;
13 void (*free_fn)(void*);
14 void (*print_fn)(const void*);
15 int (*cmp_fn)(const void*, const void*);
16} LinkedList;
17
18LinkedList* list_create(size_t elem_size,
19 void (*free_fn)(void*),
20 void (*print_fn)(const void*),
21 int (*cmp_fn)(const void*, const void*)) {
22 LinkedList* list = malloc(sizeof(LinkedList));
23 if (!list) return NULL;
24 list->head = NULL;
25 list->size = 0;
26 list->elem_size = elem_size;
27 list->free_fn = free_fn;
28 list->print_fn = print_fn;
29 list->cmp_fn = cmp_fn;
30 return list;
31}
32
33int list_push_front(LinkedList* list, const void* data) {
34 Node* node = malloc(sizeof(Node));
35 if (!node) return -1;
36 node->data = malloc(list->elem_size);
37 if (!node->data) { free(node); return -1; }
38 memcpy(node->data, data, list->elem_size);
39 node->next = list->head;
40 list->head = node;
41 list->size++;
42 return 0;
43}
44
45void* list_find(LinkedList* list, const void* target) {
46 Node* curr = list->head;
47 while (curr) {
48 if (list->cmp_fn(curr->data, target) == 0) {
49 return curr->data;
50 }
51 curr = curr->next;
52 }
53 return NULL;
54}
55
56void list_print(const LinkedList* list) {
57 Node* curr = list->head;
58 printf("List (%zu elements): ", list->size);
59 while (curr) {
60 list->print_fn(curr->data);
61 if (curr->next) printf(" -> ");
62 curr = curr->next;
63 }
64 printf("\n");
65}
66
67void list_destroy(LinkedList* list) {
68 Node* curr = list->head;
69 while (curr) {
70 Node* next = curr->next;
71 list->free_fn(curr->data);
72 free(curr);
73 curr = next;
74 }
75 free(list);
76}
77
78/* --- Usage with integers --- */
79static void int_free(void* val) { free(val); }
80static void int_print(const void* v) { printf("%d", *(const int*)v); }
81static int int_cmp(const void* a, const void* b) {
82 return *(const int*)a - *(const int*)b;
83}
84
85int main(void) {
86 LinkedList* list = list_create(sizeof(int), int_free, int_print, int_cmp);
87
88 int values[] = {10, 20, 30, 40};
89 for (int i = 0; i < 4; i++) {
90 list_push_front(list, &values[i]);
91 }
92 list_print(list); /* List (4 elements): 40 -> 30 -> 20 -> 10 */
93
94 int target = 30;
95 int* found = list_find(list, &target);
96 if (found) printf("Found: %d\n", *found); /* Found: 30 */
97
98 list_destroy(list);
99 return 0;
100}
Pointer to Pointer (int **pp)

A pointer to a pointer holds the address of a pointer variable. This is essential for: (1) modifying a pointer inside a function (e.g., malloc via double pointer), (2) dynamic 2D arrays, and (3) arrays of strings.

pointer_to_pointer.c
C
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4
5/* Without pointer-to-pointer: cannot modify the original pointer */
6void bad_alloc(int* p) {
7 p = malloc(sizeof(int)); /* Only modifies the local copy */
8 *p = 42;
9 /* Original pointer in caller is unchanged! */
10}
11
12/* With pointer-to-pointer: can modify the caller's pointer */
13void good_alloc(int** pp) {
14 *pp = malloc(sizeof(int)); /* Modifies the pointer at the address */
15 **pp = 42;
16 /* Caller's pointer now points to the allocated memory */
17}
18
19/* Reallocate a buffer (like realloc but with error handling) */
20int resize_buffer(char** buf, size_t new_size) {
21 char* tmp = realloc(*buf, new_size);
22 if (!tmp && new_size > 0) return -1;
23 *buf = tmp;
24 return 0;
25}
26
27int main(void) {
28 int* p = NULL;
29 good_alloc(&p); /* Pass address of pointer */
30 printf("*p = %d\n", *p); /* Output: 42 */
31 free(p);
32
33 /* Dynamic array of strings */
34 char** words = NULL;
35 size_t count = 0;
36
37 const char* input[] = {"hello", "world", "foo"};
38 for (int i = 0; i < 3; i++) {
39 char** tmp = realloc(words, (count + 1) * sizeof(char*));
40 if (!tmp) break;
41 words = tmp;
42 words[count] = strdup(input[i]);
43 count++;
44 }
45
46 printf("Words: ");
47 for (size_t i = 0; i < count; i++) {
48 printf("%s ", words[i]);
49 }
50 printf("\n");
51
52 /* Free everything */
53 for (size_t i = 0; i < count; i++) free(words[i]);
54 free(words);
55
56 return 0;
57}
dynamic_2d_array.c
C
1/* Dynamic 2D array using pointer-to-pointer */
2#include <stdio.h>
3#include <stdlib.h>
4
5int** create_matrix(size_t rows, size_t cols) {
6 int** matrix = malloc(rows * sizeof(int*));
7 if (!matrix) return NULL;
8
9 for (size_t i = 0; i < rows; i++) {
10 matrix[i] = calloc(cols, sizeof(int));
11 if (!matrix[i]) {
12 /* Free previously allocated rows on failure */
13 for (size_t j = 0; j < i; j++) free(matrix[j]);
14 free(matrix);
15 return NULL;
16 }
17 }
18 return matrix;
19}
20
21void free_matrix(int** matrix, size_t rows) {
22 for (size_t i = 0; i < rows; i++) {
23 free(matrix[i]);
24 }
25 free(matrix);
26}
27
28void print_matrix(int** matrix, size_t rows, size_t cols) {
29 for (size_t i = 0; i < rows; i++) {
30 for (size_t j = 0; j < cols; j++) {
31 printf("%4d", matrix[i][j]);
32 }
33 printf("\n");
34 }
35}
36
37int main(void) {
38 size_t rows = 4, cols = 5;
39 int** m = create_matrix(rows, cols);
40
41 /* Fill with values */
42 for (size_t i = 0; i < rows; i++) {
43 for (size_t j = 0; j < cols; j++) {
44 m[i][j] = (int)(i * cols + j + 1);
45 }
46 }
47
48 print_matrix(m, rows, cols);
49 free_matrix(m, rows);
50 return 0;
51}
Array of Pointers vs Pointer to Array

These two declarations look similar but are fundamentally different: int *arr[10] is an array of 10 pointers to int. int (*arr)[10] is a pointer to an array of 10 ints. Understanding this distinction is critical for working with multi-dimensional arrays.

array_vs_pointer.c
C
1#include <stdio.h>
2#include <stdlib.h>
3
4int main(void) {
5 /* int *arr[10] — array of 10 int pointers */
6 int* ptr_array[10];
7 for (int i = 0; i < 10; i++) {
8 static int values[10];
9 values[i] = i * 10;
10 ptr_array[i] = &values[i];
11 }
12 printf("ptr_array[3] = %d\n", *ptr_array[3]); /* 30 */
13 printf("sizeof(ptr_array) = %zu\n", sizeof(ptr_array)); /* 80 on 64-bit */
14
15 /* int (*arr)[10] — pointer to array of 10 ints */
16 int matrix[3][10];
17 int (*arr_ptr)[10] = matrix;
18 arr_ptr[1][5] = 999; /* Access row 1, col 5 */
19 printf("matrix[1][5] = %d\n", matrix[1][5]); /* 999 */
20 printf("sizeof(arr_ptr) = %zu\n", sizeof(arr_ptr)); /* 8 on 64-bit (just a pointer) */
21
22 /* int **pp — pointer to pointer (dynamic 2D) */
23 int** dyn_matrix = malloc(3 * sizeof(int*));
24 for (int i = 0; i < 3; i++) {
25 dyn_matrix[i] = calloc(10, sizeof(int));
26 }
27 dyn_matrix[2][7] = 42;
28 printf("dyn_matrix[2][7] = %d\n", dyn_matrix[2][7]); /* 42 */
29
30 /* Free dynamic matrix */
31 for (int i = 0; i < 3; i++) free(dyn_matrix[i]);
32 free(dyn_matrix);
33
34 return 0;
35}
DeclarationMeaningsizeof
int *arr[10]Array of 10 int pointers80 (64-bit) or 40 (32-bit)
int (*arr)[10]Pointer to array of 10 ints8 (64-bit) or 4 (32-bit)
int **ppPointer to pointer to int8 (64-bit) or 4 (32-bit)
Reading Complex Declarations

Complex C declarations can be read using the "right-left rule" (clockwise spiral rule): start at the variable name, move right when you can, then left, alternating. This helps decode even the most confusing declarations.

complex_declarations.c
C
1#include <stdio.h>
2
3/* Reading declarations with the right-left rule */
4
5/* Step 1: int *p */
6/* p is a ... pointer to ... int */
7int *p;
8
9/* Step 2: int **pp */
10/* pp is a ... pointer to ... pointer to ... int */
11int **pp;
12
13/* Step 3: int (*fp)(int, int) */
14/* fp is a ... pointer to ... function (int, int) returning int */
15int (*fp)(int, int);
16
17/* Step 4: int *arr[10] */
18/* arr is a ... array[10] of ... pointer to ... int */
19int *arr[10];
20
21/* Step 5: int (*arr)[10] */
22/* arr is a ... pointer to ... array[10] of int */
23int (*arr)[10];
24
25/* Step 6: char *(*callback)(void*) */
26/* callback is a ... pointer to ... function (void*) returning pointer to char */
27char *(*callback)(void*);
28
29/* Step 7: void (*signal(int, void(*)(int)))(int) */
30/* signal is a ... function (int, pointer to function(int) returning void) */
31/* returning ... pointer to ... function (int) returning void */
32/* This is the actual signal() signature from <signal.h> */
33
34/* Step 8: int (*(*fp)(int))[10] */
35/* fp is a ... pointer to ... function (int) returning ... */
36/* pointer to ... array[10] of int */
37
38int main(void) {
39 printf("Declarations compile — the types are correct\n");
40 return 0;
41}

info

When a declaration is too complex, use typedef to break it into named parts. This makes code self-documenting and avoids reading complex declarations repeatedly.
const and Pointers

The placement of const relative to the * determines what is immutable: the data being pointed to, the pointer itself, or both. This is a frequent source of confusion but is essential for writing safe, expressive code.

const_pointers.c
C
1#include <stdio.h>
2#include <string.h>
3
4int main(void) {
5 int x = 10;
6 int y = 20;
7
8 /* const int *p — pointer to const data (data is read-only through p) */
9 const int* p = &x;
10 /* *p = 30; ERROR: cannot modify data through p */
11 p = &y; /* OK: can change what p points to */
12 printf("*p = %d\n", *p); /* 20 */
13
14 /* int * const p — const pointer (pointer cannot be reassigned) */
15 int* const q = &x;
16 *q = 30; /* OK: can modify data through q */
17 /* q = &y; ERROR: cannot change what q points to */
18 printf("x = %d\n", x); /* 30 */
19
20 /* const int * const p — both const */
21 const int* const r = &y;
22 /* *r = 50; ERROR: cannot modify data */
23 /* r = &x; ERROR: cannot change pointer */
24 printf("*r = %d\n", *r); /* 20 */
25
26 /* Read right-to-left: const char * const * const pp */
27 const char* name = "hello";
28
29 /* Practical use: function that reads but doesn't modify */
30 /* void print(const char* s) — promises not to modify s */
31
32 /* const with array decay */
33 const char str[] = "immutable";
34 /* str[0] = 'X'; ERROR: array elements are const */
35
36 return 0;
37}
DeclarationDataPointerCan Modify
int *pMutableMutableBoth data and pointer
const int *pRead-onlyMutablePointer only
int * const pMutableRead-onlyData only
const int * const pRead-onlyRead-onlyNeither
The restrict Keyword (C99)

restrictis a promise to the compiler that a pointer is the only way to access the data it points to during the function's lifetime. This allows the compiler to generate more aggressive optimizations by assuming no pointer aliasing.

restrict_keyword.c
C
1#include <stdio.h>
2#include <string.h>
3
4/* Without restrict: compiler must assume overlapping memory */
5void copy_bad(int* dest, const int* src, size_t n) {
6 for (size_t i = 0; i < n; i++) {
7 dest[i] = src[i]; /* Compiler cannot optimize — may overlap */
8 }
9}
10
11/* With restrict: compiler knows dest and src do not overlap */
12void copy_good(int* restrict dest, const int* restrict src, size_t n) {
13 for (size_t i = 0; i < n; i++) {
14 dest[i] = src[i]; /* Compiler can optimize — guaranteed no overlap */
15 }
16}
17
18/* The standard memcpy uses restrict; memmove does not */
19/* memcpy: void* memcpy(void* restrict dest, const void* restrict src, size_t n) */
20/* memmove: void* memmove(void* dest, const void* src, size_t n) */
21
22int main(void) {
23 int a[] = {1, 2, 3, 4, 5};
24
25 /* This is undefined behavior with memcpy (overlap) */
26 /* memcpy(a + 1, a, 4 * sizeof(int)); */
27
28 /* This is safe with memmove (handles overlap) */
29 memmove(a + 1, a, 4 * sizeof(int));
30 for (int i = 0; i < 5; i++) printf("%d ", a[i]);
31 printf("\n"); /* 1 1 2 3 4 */
32
33 /* Restrict only matters for pointer parameters */
34 /* It is a hint — violating it is undefined behavior */
35 /* Most modern compilers support it (GCC, Clang) */
36
37 return 0;
38}
Opaque Pointers: Hiding Implementation

Opaque pointers hide the internal structure of a type behind a void* or forward-declared incomplete type. The header declares functions that operate on the type but does not expose its fields. This achieves data hiding and encapsulation in C.

include/stack.h
C
1/* stack.h — public API (opaque pointer pattern) */
2#ifndef STACK_H
3#define STACK_H
4
5#include <stddef.h>
6
7/* Forward declaration — users know it exists but not its layout */
8typedef struct Stack Stack;
9
10Stack* stack_create(size_t elem_size);
11void stack_destroy(Stack* s);
12int stack_push(Stack* s, const void* data);
13void* stack_peek(const Stack* s);
14int stack_pop(Stack* s, void* out);
15size_t stack_size(const Stack* s);
16
17#endif /* STACK_H */
src/stack.c
C
1/* stack.c — private implementation */
2#include "stack.h"
3#include <stdlib.h>
4#include <string.h>
5
6/* Users cannot see this structure — it is defined only here */
7struct Stack {
8 void* data; /* Dynamic array of elements */
9 size_t elem_size; /* Size of each element in bytes */
10 size_t capacity; /* Maximum elements before realloc */
11 size_t size; /* Current number of elements */
12};
13
14Stack* stack_create(size_t elem_size) {
15 Stack* s = malloc(sizeof(Stack));
16 if (!s) return NULL;
17 s->capacity = 16;
18 s->size = 0;
19 s->elem_size = elem_size;
20 s->data = malloc(s->capacity * elem_size);
21 if (!s->data) { free(s); return NULL; }
22 return s;
23}
24
25void stack_destroy(Stack* s) {
26 if (s) { free(s->data); free(s); }
27}
28
29int stack_push(Stack* s, const void* data) {
30 if (!s || !data) return -1;
31 if (s->size >= s->capacity) {
32 size_t new_cap = s->capacity * 2;
33 void* tmp = realloc(s->data, new_cap * s->elem_size);
34 if (!tmp) return -1;
35 s->data = tmp;
36 s->capacity = new_cap;
37 }
38 void* dest = (char*)s->data + s->size * s->elem_size;
39 memcpy(dest, data, s->elem_size);
40 s->size++;
41 return 0;
42}
43
44void* stack_peek(const Stack* s) {
45 if (!s || s->size == 0) return NULL;
46 return (char*)s->data + (s->size - 1) * s->elem_size;
47}
48
49int stack_pop(Stack* s, void* out) {
50 if (!s || s->size == 0 || !out) return -1;
51 s->size--;
52 void* src = (char*)s->data + s->size * s->elem_size;
53 memcpy(out, src, s->elem_size);
54 return 0;
55}
56
57size_t stack_size(const Stack* s) {
58 return s ? s->size : 0;
59}
🔥

pro tip

Opaque pointers prevent users from depending on internal layout, so you can change the implementation (switch from array to linked list, change data structures) without breaking any code that uses the API.
State Machine with Function Pointer Tables

Function pointer tables enable elegant state machine implementations. Each state has an associated action function, and transitions are driven by a table lookup rather than nested switch statements.

state_machine.c
C
1#include <stdio.h>
2#include <string.h>
3
4typedef enum {
5 STATE_IDLE,
6 STATE_RUNNING,
7 STATE_PAUSED,
8 STATE_ERROR,
9 STATE_COUNT
10} State;
11
12typedef enum {
13 EVT_START,
14 EVT_PAUSE,
15 EVT_RESUME,
16 EVT_STOP,
17 EVT_ERROR,
18 EVT_COUNT
19} Event;
20
21typedef State (*StateHandler)(void* context);
22
23/* State transition table */
24static State transition_table[STATE_COUNT][EVT_COUNT];
25
26/* State action functions */
27static State handle_idle(void* ctx) {
28 printf("[IDLE] Waiting...\n");
29 return STATE_IDLE;
30}
31
32static State handle_running(void* ctx) {
33 printf("[RUNNING] Processing...\n");
34 return STATE_RUNNING;
35}
36
37static State handle_paused(void* ctx) {
38 printf("[PAUSED] Suspended\n");
39 return STATE_PAUSED;
40}
41
42static State handle_error(void* ctx) {
43 printf("[ERROR] Fault occurred\n");
44 return STATE_ERROR;
45}
46
47/* Transition functions */
48static State transition_to_running(void* ctx) {
49 printf(" -> Starting\n");
50 return STATE_RUNNING;
51}
52
53static State transition_to_paused(void* ctx) {
54 printf(" -> Pausing\n");
55 return STATE_PAUSED;
56}
57
58static State transition_to_idle(void* ctx) {
59 printf(" -> Stopping\n");
60 return STATE_IDLE;
61}
62
63static State transition_to_error(void* ctx) {
64 printf(" -> Error occurred\n");
65 return STATE_ERROR;
66}
67
68/* Initialize the transition table */
69static void init_machine(void) {
70 /* From IDLE */
71 transition_table[STATE_IDLE][EVT_START] = transition_to_running;
72 transition_table[STATE_IDLE][EVT_ERROR] = transition_to_error;
73
74 /* From RUNNING */
75 transition_table[STATE_RUNNING][EVT_PAUSE] = transition_to_paused;
76 transition_table[STATE_RUNNING][EVT_STOP] = transition_to_idle;
77 transition_table[STATE_RUNNING][EVT_ERROR] = transition_to_error;
78
79 /* From PAUSED */
80 transition_table[STATE_PAUSED][EVT_RESUME] = transition_to_running;
81 transition_table[STATE_PAUSED][EVT_STOP] = transition_to_idle;
82
83 /* From ERROR */
84 transition_table[STATE_ERROR][EVT_STOP] = transition_to_idle;
85}
86
87static const char* state_name(State s) {
88 static const char* names[] = {"IDLE", "RUNNING", "PAUSED", "ERROR"};
89 return (s < STATE_COUNT) ? names[s] : "UNKNOWN";
90}
91
92int main(void) {
93 init_machine();
94
95 State current = STATE_IDLE;
96 printf("Initial state: %s\n", state_name(current));
97
98 Event events[] = {EVT_START, EVT_PAUSE, EVT_RESUME, EVT_STOP};
99 size_t n = sizeof(events) / sizeof(events[0]);
100
101 for (size_t i = 0; i < n; i++) {
102 State next = transition_table[current][events[i]](NULL);
103 printf(" %s -> %s\n", state_name(current), state_name(next));
104 current = next;
105 }
106
107 return 0;
108}
Returning Pointers from Functions

Returning pointers from functions has strict rules. Never return a pointer to a local variable (it becomes dangling). You can return pointers to: heap-allocated memory, static variables, or string literals. Each has different ownership implications.

returning_pointers.c
C
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4
5/* BAD: Returns pointer to local variable (dangling pointer) */
6int* bad_return(void) {
7 int local = 42;
8 return &local; /* Undefined behavior — local is on the stack */
9}
10
11/* GOOD: Return heap-allocated memory (caller must free) */
12int* good_return_heap(int value) {
13 int* p = malloc(sizeof(int));
14 if (p) *p = value;
15 return p; /* Caller is responsible for free() */
16}
17
18/* GOOD: Return pointer to static variable (persists between calls) */
19int* good_return_static(int value) {
20 static int saved;
21 saved = value;
22 return &saved; /* Valid until next call overwrites it */
23}
24
25/* GOOD: Return string literal (lives for entire program duration) */
26const char* good_return_literal(int code) {
27 switch (code) {
28 case 200: return "OK";
29 case 404: return "Not Found";
30 case 500: return "Server Error";
31 default: return "Unknown";
32 }
33}
34
35/* GOOD: Return dynamically built string (caller must free) */
36char* build_string(const char* a, const char* b) {
37 size_t len = strlen(a) + strlen(b) + 1;
38 char* result = malloc(len);
39 if (result) {
40 strcpy(result, a);
41 strcat(result, b);
42 }
43 return result;
44}
45
46int main(void) {
47 /* Heap allocation — caller frees */
48 int* hp = good_return_heap(100);
49 printf("Heap: %d\n", *hp);
50 free(hp);
51
52 /* Static variable — valid but overwritten on next call */
53 int* sp1 = good_return_static(10);
54 printf("Static 1: %d\n", *sp1);
55 int* sp2 = good_return_static(20);
56 printf("Static 2: %d\n", *sp2);
57 /* Note: *sp1 and *sp2 point to the same memory! */
58
59 /* String literals — no need to free */
60 printf("Status: %s\n", good_return_literal(200));
61
62 /* Dynamic string — caller frees */
63 char* str = build_string("Hello, ", "World!");
64 printf("Built: %s\n", str);
65 free(str);
66
67 return 0;
68}
Signal Handling with Function Pointers

Signal handlers are callbacks invoked by the operating system when a signal (event) occurs. signal() and sigaction() register function pointers that handle asynchronous events like interrupts, termination requests, and errors.

signal_handler.c
C
1#include <stdio.h>
2#include <signal.h>
3#include <unistd.h>
4#include <stdatomic.h>
5
6/* Signal flag — must be volatile sig_atomic_t or atomic */
7static volatile sig_atomic_t got_signal = 0;
8
9void handle_sigint(int signum) {
10 /* Only async-signal-safe functions allowed here! */
11 got_signal = signum;
12}
13
14void handle_sigterm(int signum) {
15 got_signal = signum;
16}
17
18int main(void) {
19 /* Register signal handlers */
20 signal(SIGINT, handle_sigint); /* Ctrl+C */
21 signal(SIGTERM, handle_sigterm); /* kill command */
22
23 printf("Running... (send SIGINT with Ctrl+C)\n");
24 printf("PID: %d\n", getpid());
25
26 /* Simulated work loop */
27 for (int i = 0; i < 10 && !got_signal; i++) {
28 printf("Working... %d/10\n", i + 1);
29 sleep(1);
30 }
31
32 if (got_signal) {
33 printf("Received signal %d, shutting down gracefully.\n", got_signal);
34 } else {
35 printf("Work completed normally.\n");
36 }
37
38 return 0;
39}

warning

Signal handlers run asynchronously. Only use async-signal-safe functions (write, not printf; no malloc, no locks). Keep handlers minimal — set a flag and handle it in the main loop.
Complete Example: Generic Array with Callbacks

A generic dynamic array that stores void* elements with function pointer callbacks for operations. Demonstrates combining function pointers, void*, and pointer-to-pointer in a practical data structure.

generic_array_complete.c
C
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4
5typedef void (*GArrayFreeFn)(void*);
6typedef void (*GArrayPrintFn)(const void*);
7typedef int (*GArrayCmpFn)(const void*, const void*);
8typedef void* (*GArrayCloneFn)(const void*);
9
10typedef struct {
11 void** elements;
12 size_t size;
13 size_t capacity;
14 size_t elem_size;
15 GArrayFreeFn free_fn;
16 GArrayPrintFn print_fn;
17 GArrayCmpFn cmp_fn;
18 GArrayCloneFn clone_fn;
19} GArray;
20
21GArray* garray_create(size_t elem_size, GArrayFreeFn free_fn,
22 GArrayPrintFn print_fn, GArrayCmpFn cmp_fn,
23 GArrayCloneFn clone_fn) {
24 GArray* arr = malloc(sizeof(GArray));
25 if (!arr) return NULL;
26 arr->capacity = 16;
27 arr->size = 0;
28 arr->elem_size = elem_size;
29 arr->elements = malloc(arr->capacity * sizeof(void*));
30 arr->free_fn = free_fn;
31 arr->print_fn = print_fn;
32 arr->cmp_fn = cmp_fn;
33 arr->clone_fn = clone_fn;
34 if (!arr->elements) { free(arr); return NULL; }
35 return arr;
36}
37
38int garray_append(GArray* arr, const void* data) {
39 if (!arr || !data) return -1;
40 if (arr->size >= arr->capacity) {
41 size_t new_cap = arr->capacity * 2;
42 void** tmp = realloc(arr->elements, new_cap * sizeof(void*));
43 if (!tmp) return -1;
44 arr->elements = tmp;
45 arr->capacity = new_cap;
46 }
47 if (arr->clone_fn) {
48 arr->elements[arr->size] = arr->clone_fn(data);
49 } else {
50 arr->elements[arr->size] = malloc(arr->elem_size);
51 if (!arr->elements[arr->size]) return -1;
52 memcpy(arr->elements[arr->size], data, arr->elem_size);
53 }
54 arr->size++;
55 return 0;
56}
57
58void garray_print(const GArray* arr) {
59 if (!arr || !arr->print_fn) return;
60 printf("GArray[%zu]: ", arr->size);
61 for (size_t i = 0; i < arr->size; i++) {
62 arr->print_fn(arr->elements[i]);
63 if (i < arr->size - 1) printf(", ");
64 }
65 printf("\n");
66}
67
68void garray_sort(GArray* arr) {
69 if (!arr || !arr->cmp_fn) return;
70 qsort(arr->elements, arr->size, sizeof(void*), arr->cmp_fn);
71}
72
73void* garray_find(GArray* arr, const void* target) {
74 if (!arr || !arr->cmp_fn || !target) return NULL;
75 for (size_t i = 0; i < arr->size; i++) {
76 if (arr->cmp_fn(arr->elements[i], target) == 0) {
77 return arr->elements[i];
78 }
79 }
80 return NULL;
81}
82
83void garray_destroy(GArray* arr) {
84 if (!arr) return;
85 for (size_t i = 0; i < arr->size; i++) {
86 if (arr->free_fn) arr->free_fn(arr->elements[i]);
87 else free(arr->elements[i]);
88 }
89 free(arr->elements);
90 free(arr);
91}
92
93/* --- Integer callbacks --- */
94static void int_free(void* v) { free(v); }
95static void int_print(const void* v) { printf("%d", *(const int*)v); }
96static void* int_clone(const void* v) {
97 int* copy = malloc(sizeof(int));
98 if (copy) *copy = *(const int*)v;
99 return copy;
100}
101static int int_cmp(const void* a, const void* b) {
102 return *(const int*)a - *(const int*)b;
103}
104
105int main(void) {
106 GArray* arr = garray_create(sizeof(int), int_free, int_print,
107 int_cmp, int_clone);
108
109 int values[] = {50, 20, 80, 10, 60};
110 for (int i = 0; i < 5; i++) {
111 garray_append(arr, &values[i]);
112 }
113
114 printf("Before sort: ");
115 garray_print(arr);
116
117 garray_sort(arr);
118 printf("After sort: ");
119 garray_print(arr);
120
121 int target = 80;
122 int* found = garray_find(arr, &target);
123 if (found) printf("Found: %d\n", *found);
124
125 garray_destroy(arr);
126 return 0;
127}
$Blueprint — Engineering Documentation·Section ID: C-ADVANCED-POINTERS·Revision: 1.0