What you'll learn
Professional C is not about never creating defects. It is about building several fast, independent ways to expose them before users do—and leaving behind tests that prevent yesterday's defect from returning.
By the end, you'll be able to:
- Compile with strict, actionable diagnostics
- Inspect control flow and program state with a debugger
- Write assertions and focused automated tests
- Detect memory errors and undefined behavior with sanitizers
- Distinguish undefined, unspecified, and implementation-defined behavior
Compiler diagnostics
Your compiler understands C deeply enough to spot conversions, shadowed names, missing declarations, unreachable code, and other suspicious constructs. Enable diagnostics from the beginning; do not wait until the end of a project.
# GCC or Clang: a strict development build
cc -std=c17 -Wall -Wextra -Wpedantic \
-Wconversion -Wshadow -Werror \
-g -O0 src/main.c -o build/app
# A separate optimized release build
cc -std=c17 -Wall -Wextra -Wpedantic \
-O2 -DNDEBUG src/main.c -o build/app| Option | Purpose |
|---|---|
-Wall -Wextra | Enable broad, high-value warning groups |
-Wpedantic | Report code outside the selected C standard |
-Wconversion | Expose potentially value-changing conversions |
-Wshadow | Report declarations hiding an outer name |
-Werror | Treat warnings as build failures in controlled builds |
-g -O0 | Preserve debug information and simple stepping behavior |
Tip
Debuggers
A debugger pauses a live program so you can inspect variables, step through statements, watch values change, and examine the call stack. Start from a repeatable failure and a concrete question such as “when does this index first become invalid?”
#include <stddef.h>
#include <stdio.h>
double average(const int values[], size_t count) {
long total = 0;
for (size_t i = 0; i <= count; i++) { // bug: one step too far
total += values[i];
}
return (double) total / (double) count;
}
int main(void) {
int scores[] = {84, 91, 78};
printf("%.2f\n", average(scores, 3));
return 0;
}$ cc -std=c17 -g -O0 average.c -o average
$ gdb ./average
(gdb) break average
(gdb) run
(gdb) next
(gdb) print i
(gdb) print count
(gdb) watch i
(gdb) continue
(gdb) backtraceKey idea
i < count. At i == count, the program reads beyond the array. The debugger reveals the state; reasoning about the contract explains why it is wrong.LLDB provides equivalent concepts with slightly different commands. Learn breakpoints, stepping, variable inspection, watchpoints, and stack traces in whichever debugger your platform supports.
Assertions & contracts
An assertion documents a condition that must be true if the programmer used the function correctly. A failed assertion aborts the program close to the violated assumption, making the defect easier to locate.
#include <assert.h>
#include <stddef.h>
int maximum(const int values[], size_t count) {
assert(values != NULL); // programmer contract
assert(count > 0);
int result = values[0];
for (size_t i = 1; i < count; i++) {
if (values[i] > result) result = values[i];
}
return result;
}Watch out
NDEBUG is defined. Never use an assertion for required side effects, ordinary user input validation, recoverable file errors, or security checks. Handle those cases explicitly.Automated testing
A unit test calls a small piece of code with known input and checks observable results. Cover normal cases, boundaries, empty or invalid inputs where supported, and regressions for bugs you have fixed.
#include <stdio.h>
static int failures = 0;
void check(int condition, const char *expression,
const char *file, int line) {
if (!condition) {
fprintf(stderr, "%s:%d: CHECK(%s) failed\n",
file, line, expression);
failures++;
}
}
#define CHECK(condition) check((condition), #condition, __FILE__, __LINE__)
int clamp(int value, int low, int high) {
if (value < low) return low;
if (value > high) return high;
return value;
}
int main(void) {
CHECK(clamp(5, 0, 10) == 5); // normal case
CHECK(clamp(-1, 0, 10) == 0); // below boundary
CHECK(clamp(11, 0, 10) == 10); // above boundary
CHECK(clamp(0, 0, 10) == 0); // exact boundary
if (failures == 0) puts("All tests passed");
return failures == 0 ? 0 : 1;
}- Keep tests deterministic, isolated, and fast
- Return a nonzero process status when any test fails
- Separate reusable implementation files from the production
main - Test public behavior; avoid coupling every test to internal implementation
- Run the suite on every meaningful change
Note
Sanitizers
Sanitizers instrument a development build. AddressSanitizer detects many out-of-bounds and use-after-free errors; UndefinedBehaviorSanitizer detects operations such as invalid shifts and signed overflow while the affected path runs.
# Build an instrumented executable with GCC or Clang
cc -std=c17 -g -O1 \
-fsanitize=address,undefined \
-fno-omit-frame-pointer \
tests.c -o tests
# Run tests normally; the runtime reports detected defects
./tests
# Valgrind is another option on supported platforms
valgrind --leak-check=full ./testsWatch out
Static analysis
Static analyzers explore source and possible control-flow paths without relying only on one execution. They can identify leaks, null dereferences, uninitialized values, ignored return codes, and mismatched resource ownership.
| Technique | Finds defects | Main limitation |
|---|---|---|
| Compiler warnings | During every build | Focused on language and local diagnostics |
| Static analysis | Across possible code paths | Can produce false positives and needs configuration |
| Unit/integration tests | When checked behavior differs | Limited to designed cases |
| Sanitizers | When an instrumented path executes | Runtime overhead and incomplete path coverage |
| Debugger | During an investigated execution | Interactive diagnosis, not prevention |
Tip
Undefined behavior
The C standard places behaviors into important categories. When behavior is undefined, the standard imposes no requirements; an optimizer may assume that path never occurs. The result is not guaranteed to be a friendly crash.
#include <limits.h>
#include <stddef.h>
#include <stdio.h>
int main(void) {
int values[3] = {10, 20, 30};
// Undefined: index is outside the array.
// printf("%d\n", values[3]);
// Undefined: signed integer overflow.
// int impossible = INT_MAX + 1;
// Defined unsigned arithmetic wraps modulo UINT_MAX + 1.
unsigned counter = UINT_MAX;
counter++;
printf("%u\n", counter);
// Defined because the pointer is valid and checked before use.
int *selected = &values[1];
if (selected != NULL) printf("%d\n", *selected);
return 0;
}| Category | Meaning | Example |
|---|---|---|
| Defined | The standard specifies the result | Unsigned integer wraparound |
| Implementation-defined | The implementation chooses and documents a result | Size of int; signed right shift behavior |
| Unspecified | One of several allowed results; no documentation required | Order of evaluating function arguments |
| Undefined | The standard gives no requirements | Out-of-bounds access; signed overflow; use after free |
Key idea
A professional workflow
- Reduce the failure to a repeatable input or test
- Build with strict warnings and debug information
- Run the test suite under address and undefined-behavior sanitizers
- Use the debugger to inspect the earliest incorrect state
- Fix the violated invariant or ownership rule—not just the final symptom
- Add a regression test, rerun analysis, and review nearby code
Tip
Recap & quick check
Key takeaways
- Strict compiler diagnostics are the first and cheapest quality gate.
- Debuggers expose runtime state, while invariants explain why that state is invalid.
- Assertions enforce programmer contracts; tests verify repeatable observable behavior.
- Sanitizers and static analysis find different classes of defects and work best together.
- Undefined behavior has no portable meaning and must be prevented, not merely observed.
Quick check
1. Which condition is appropriate for an assertion?
2. What does AddressSanitizer primarily help detect?
3. Why is one clean sanitizer run insufficient proof of correctness?
4. What guarantee does C give after signed integer overflow?
Next: Module 19 — Build Tools, Portability & Secure C, where these quality checks become part of a reproducible project pipeline.