Nick Barker's Tips for More Effective C Programming

If you have seen many of my social media posts or articles, you will know my fondness and frustrations with the C language. This means when I see someone share valuable tips, my ears prick up just like when our dog hears a packet of chips open.
Hackaday writes:
If you're going to be a hacker, learning C is a rite of passage. If you don't have much experience with C, or if your experience is out of date, you very well may benefit from hearing [Nic Barker] explain tips for C programming.
Nic Barker's video offers great insights into modern C programming, along the way dispelling myths about the language's inherent complexity and provides ways to improve your experience.
-
The evolution of C standards
-
Features like designated initializers, clearer type definitions, and flexible syntax to enhance code clarity and portability.
-
Tips for compiling and organising projects, such as avoiding 'include hell'
-
Diagnosing issues like segmentation faults, memory corruption, and buffer overflows.
-
Redefining string handling for safer and more efficient string operations.
-
The risks of dangling pointers and careful pointer management, especially when resizing arrays or working with dynamic memory.
I suggest you watch the full thing but I include notes below for people like me who prefer to read:
Why C is Still Dominant and Worth Learning
-
Major systems like Linux, Windows, and macOS kernels, as well as essential tools such as Git, FFmpeg, and web servers like Nginx, are written in C.
-
C offers unparalleled control over system resources, making it a powerful choice for high-performance.
Different Versions of C
-
C89 (or ANSI C, 1989): The oldest widely supported version, highly compatible across platforms. It requires all local variables to be declared at the top of functions - an outdated style for modern programming.
-
C99 (1999): Introduces many improvements, such as declaring variables anywhere within a scope, designated initializers for structs, fixed-size integer types, and new syntax for comments (
//). -
Beyond C99: Standards like C11 and C23 continue evolving, adding features like thread support and atomic operations.
-
Tip: Use a modern standard like C99. Provides more flexible syntax and safer types.
Compilation Flags
Flags ensure compatibility and help catch bugs:
-
Specify the C standard:
-std=c99(or later, depending on your needs) -
Show lots of warnings:
-Walland-Wextra -
Treat warnings as errors:
-Werror
Example:
clang -std=c99 -Wall -Wextra -Werror -o my_program my_source.c
Use Unity Build
C traditionally involves header files (.h) and implementation files (.c). This can lead to include hell, where duplicate symbols and complex build systems cause headaches.
-
Combine multiple C files into a single translation unit by including all
.cfiles in one main file. -
Reduced build complexity
-
Fewer include-related errors
-
Easier management for small to medium projects
-
Use
#pragma onceor include guards to prevent duplication.
Debuggers

C's low-level nature means errors like segmentation faults (segfaults) happen often and can be tricky to diagnose using only print statements.
-
GDB, LLDB, Visual Studio
-
See where and why your program crashes
-
Prevent bugs before shipping
-
Pause execution exactly where the fault occurs.
-
Inspect variable states and call stacks.
Segfault : Accessing invalid memory, dereferencing null, out-of-bounds arrays, or freed memory.
Address Sanitizer (ASAN)
Silent memory corruption bugs are some of the hardest to find:
-
They happen when memory is overwritten outside array bounds.
-
Sometimes no immediate crash; problems surface much later, causing elusive bugs.
-
Enable Address Sanitizer (
-fsanitize=address) in your compiler:
clang -fsanitize=address -g my_source.c -o my_program
How it works:
-
It adds red zones around allocated memory blocks.
-
Detects illegal memory access during runtime.
-
Shows full call stacks and variable states when errors occur.
ASAN can slow down execution and allocate extra memory, so use it during development, not in production.
Safeguard Arrays with Bounds Checking
Arrays in C are just pointers; the language doesn't check if your index is valid:
int arr[10];
arr[11] = 42; // Out-of-bounds access
This can corrupt memory or crash your program with a silent bug.
-
Wrap array access in "get" functions that verify indices at runtime.
-
Use debug breakpoints to catch invalid accesses early.
-
Create a struct that tracks length and capacity:
c typedef struct {
int* data;
size_t length;
size_t capacity;
} Array;
int get_element(const Array* arr, size_t index) {
if (index >= arr->length) {
// Handle error, e.g., break into debugger or return default
}
return arr->data[index];
}
Handling Strings Without Terminators
C strings usually end on a null terminator (\0), which:
-
Increases risk of missing or corrupting the terminator.
-
Causes functions like
strlen()to potentially read invalid memory, leading to bugs.
Define a string as a structure:
typedef struct {
char* data; size_t length;
} String;
-
No null terminator needed.
-
You can process or slice strings without copying or risking missing terminators.
-
More efficient, especially when working with larger blocks of text.
-
Safer operations, as length is explicitly stored.
Use Indexes Instead of Pointers
Storing pointers to objects can be dangerous when arrays resize or objects move:
object_t* ptr = &array[0];
array = realloc(array, new_size); // ptr may now be invalid
Store indexes (integers) that refer to positions in arrays instead of raw pointers.
typedef struct {
size_t index; // position in array
} Ref;
-
Indexes remain valid after resizing.
-
Simplifies serialisation and deserialisation.
-
Saves memory: 4 bytes (on 64-bit) per reference versus 8 bytes for a pointer.
Memory Management and Arenas
Managing many small malloc/free calls can be error-prone and slow.
Use memory arenas that allocate large chunks at once. When a task or object completes, deallocate the entire arena at once.
-
Allocate a big block of memory.
-
Divide it into sub-objects during runtime.
-
When done, free all sub-objects together by freeing the arena.
Simplifies memory cleanup, improves performance by reducing system calls and limits fragmentation and dangling pointers.
TL;DR / Conclusion
-
Use modern standards (
C99or later). -
Enable compiler warnings and sanitisers.
-
Organise code to minimise include hell.
-
Debug with modern tools.
-
Implement safety checks for arrays and strings.
-
Manage memory with arenas and indexes.