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

C — Best Practices

CBest PracticesSecurityExpertExpert🎯Free Tools
Why Best Practices Matter

C is unforgiving. There is no garbage collector, no bounds checking, no null safety. Every decision you make — from naming conventions to memory management to error handling — either builds resilience into your code or introduces a future vulnerability. Best practices in C are not style preferences; they are survival strategies.

This guide distills decades of hard-won knowledge from systems programming, security engineering, and large-scale C projects into actionable rules and patterns.

Coding Standards

Consistency is more important than any specific convention. Choose a style and enforce it across the entire project. The Linux kernel, Chromium, and SQLite all have documented coding standards — yours should too.

naming_conventions.c
C
1/* NAMING CONVENTIONS */
2
3/* Functions: snake_case, verb_noun pattern */
4int process_order(const Order *order); /* good */
5int Order_Process(const Order *order); /* also valid (Windows style) */
6int prc_ord(Order *o); /* bad — cryptic abbreviation */
7
8/* Variables: snake_case, descriptive names */
9int total_item_count; /* good */
10int tic; /* bad — too short */
11int theTotalNumberOfItems; /* bad — camelCase in C is non-standard */
12
13/* Constants and macros: UPPER_CASE_SNAKE */
14#define MAX_BUFFER_SIZE 4096
15#define PI 3.14159265358979
16static const int default_timeout_sec = 30; /* const vars: snake_case */
17
18/* Types: PascalCase or snake_case_t suffix (pick one) */
19typedef struct {
20 int x;
21 int y;
22} Point; /* PascalCase — common in Linux kernel */
23
24typedef struct {
25 double real;
26 double imag;
27} complex_t; /* snake_case_t — also common */
28
29/* Enums: PascalCase values */
30typedef enum {
31 COLOR_RED,
32 COLOR_GREEN,
33 COLOR_BLUE
34} Color;

Header file organization — the public interface of your module:

module_name.h
C
1/* ============================================
2 * module_name.h — Public API for module_name
3 * ============================================ */
4#ifndef MODULE_NAME_H
5#define MODULE_NAME_H
6
7/* 1. System includes first */
8#include <stddef.h>
9#include <stdint.h>
10
11/* 2. Project-wide types and macros */
12#include "config.h"
13
14/* 3. Public macros */
15#define MODULE_VERSION "1.2.0"
16#define MAX_ITEMS_PER_PAGE 50
17
18/* 4. Type definitions */
19typedef struct module_context ModuleContext;
20typedef struct item {
21 uint32_t id;
22 char name[128];
23 double price;
24} Item;
25
26/* 5. Function declarations */
27ModuleContext *module_create(const char *config_path);
28void module_destroy(ModuleContext *ctx);
29
30int module_add_item(ModuleContext *ctx, const Item *item);
31int module_get_item(const ModuleContext *ctx, uint32_t id, Item *out);
32int module_remove_item(ModuleContext *ctx, uint32_t id);
33
34size_t module_count(const ModuleContext *ctx);
35
36#endif /* MODULE_NAME_H */

info

Always use include guards (#ifndef HEADER_H) even if your compiler supports #pragma once. Include guards are portable across all C compilers; pragma once is widely supported but not universal.
ElementConventionExample
Functionssnake_case, verb_nounprocess_order()
Local variablessnake_caseitem_count
Macros / constantsUPPER_SNAKE_CASEMAX_BUFFER_SIZE
TypesPascalCase or _t suffixOrderContext
EnumsPascalCase type, UPPER valuesSTATUS_OK
Filessnake_case.c / .horder_handler.c
Security Best Practices

Security in C is not an afterthought — it must be designed in from the start. The C standard library contains dozens of functions that are inherently unsafe. Replacing them with secure alternatives is non-negotiable.

secure_coding.c
C
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4
5/* =========================================
6 * NEVER USE THESE (unsafe functions)
7 * ========================================= */
8
9/* gets() — reads unlimited input, guaranteed buffer overflow */
10/* char buf[64]; gets(buf); // DANGEROUS: banned in C11 */
11
12/* sprintf() — no bounds checking */
13/* char buf[64]; sprintf(buf, "%s: %d", user_input, val); // DANGEROUS */
14
15/* strcpy() — no bounds checking */
16/* char dst[32]; strcpy(dst, user_input); // DANGEROUS */
17
18/* strcat() — no bounds checking */
19/* char buf[64]; strcat(buf, more_data); // DANGEROUS */
20
21/* scanf("%s") — no length limit */
22/* char buf[32]; scanf("%s", buf); // DANGEROUS */
23
24/* =========================================
25 * ALWAYS USE THESE (safe alternatives)
26 * ========================================= */
27
28/* fgets() instead of gets() */
29int safe_read_line(void) {
30 char buf[64];
31 if (fgets(buf, sizeof(buf), stdin) == NULL) {
32 return -1;
33 }
34 /* fgets keeps the newline — strip it */
35 buf[strcspn(buf, "\n")] = '\0';
36 return 0;
37}
38
39/* snprintf() instead of sprintf() */
40int safe_format(char *dst, size_t dst_size, const char *name, int val) {
41 int written = snprintf(dst, dst_size, "%s: %d", name, val);
42 if (written < 0) return -1; /* encoding error */
43 if ((size_t)written >= dst_size) {
44 /* truncation occurred — handle it */
45 return -1;
46 }
47 return 0;
48}
49
50/* strncpy() with explicit null termination */
51int safe_str_copy(char *dst, size_t dst_size, const char *src) {
52 if (dst_size == 0) return -1;
53 strncpy(dst, src, dst_size - 1);
54 dst[dst_size - 1] = '\0'; /* always null-terminate */
55 return 0;
56}
57
58/* scanf with width specifier */
59int safe_scan_int(int *out) {
60 if (scanf("%31d", out) != 1) {
61 return -1;
62 }
63 return 0;
64}

Compile-time security hardening flags:

hardened_build.sh
Bash
1# Stack protector — detects stack buffer overflows
2gcc -fstack-protector-strong -o program main.c
3
4# FORTIFY_SOURCE — runtime checks for buffer overflows in libc calls
5# Must use at least -O1 (optimization required)
6gcc -O2 -D_FORTIFY_SOURCE=2 -o program main.c
7
8# Position-independent executable (required for ASLR)
9gcc -fPIE -pie -o program main.c
10
11# Full hardened build (Linux)
12gcc -Wall -Wextra -Werror \
13 -fstack-protector-strong \
14 -D_FORTIFY_SOURCE=2 \
15 -fPIE -pie \
16 -o program main.c
17
18# Disable common security issues
19gcc -fno-strict-aliasing \
20 -fno-common \
21 -Wformat -Wformat-security \
22 -o program main.c

Input validation — trust nothing, validate everything:

input_validation.c
C
1#include <errno.h>
2#include <limits.h>
3#include <stdlib.h>
4
5/* Validate and parse an integer from a string with range checking */
6int parse_port(const char *str, int *out_port) {
7 if (!str || !out_port) return -1;
8
9 char *endptr;
10 errno = 0;
11 long val = strtol(str, &endptr, 10);
12
13 /* Check for parsing errors */
14 if (errno != 0 || endptr == str || *endptr != '\0') {
15 return -1; /* invalid input */
16 }
17
18 /* Check range */
19 if (val < 1 || val > 65535) {
20 return -1; /* out of valid port range */
21 }
22
23 *out_port = (int)val;
24 return 0;
25}
26
27/* Validate file path (basic sanitization) */
28int validate_path(const char *path) {
29 if (!path) return -1;
30
31 /* Reject paths with directory traversal */
32 if (strstr(path, "..") != NULL) return -1;
33
34 /* Reject absolute paths (if serving relative only) */
35 if (path[0] == '/') return -1;
36
37 /* Reject paths with shell metacharacters */
38 const char *dangerous = "|;&$`";
39 for (const char *p = dangerous; *p; p++) {
40 if (strchr(path, *p) != NULL) return -1;
41 }
42
43 /* Check max length */
44 if (strlen(path) > PATH_MAX) return -1;
45
46 return 0;
47}

warning

Never pass user input directly as a format string to printf(), fprintf(), or snprintf(). An attacker can inject %x %x %x %n to read stack memory and write arbitrary values. Always use printf("%s", user_input).
Memory Management

Every malloc() must have a corresponding free(). This sounds trivial but is the single largest source of bugs in C codebases. Discipline and patterns are your defense.

memory_management.c
C
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4
5/* RULE 1: Always initialize pointers to NULL */
6char *buffer = NULL;
7
8/* RULE 2: Check malloc return value */
9int init_buffer(size_t size) {
10 buffer = malloc(size);
11 if (buffer == NULL) {
12 fprintf(stderr, "malloc(%zu) failed\n", size);
13 return -1;
14 }
15 /* RULE 3: Zero-initialize sensitive allocations */
16 memset(buffer, 0, size);
17 return 0;
18}
19
20/* RULE 4: Free in reverse order of allocation */
21int complex_operation(void) {
22 char *a = malloc(100);
23 char *b = malloc(200);
24 char *c = malloc(300);
25
26 if (!a || !b || !c) {
27 /* Reverse order cleanup on partial failure */
28 free(c);
29 free(b);
30 free(a);
31 return -1;
32 }
33
34 /* ... use a, b, c ... */
35
36 free(c); /* last allocated, first freed */
37 free(b);
38 free(a);
39 return 0;
40}
41
42/* RULE 5: Set pointer to NULL after freeing */
43void cleanup(void) {
44 free(buffer);
45 buffer = NULL; /* prevents use-after-free and double-free */
46}
47
48/* RULE 6: Use a cleanup pattern for complex functions */
49int process_data(const char *input) {
50 char *temp1 = NULL;
51 char *temp2 = NULL;
52 FILE *fp = NULL;
53 int result = -1;
54
55 temp1 = malloc(1024);
56 if (!temp1) goto cleanup;
57
58 temp2 = malloc(2048);
59 if (!temp2) goto cleanup;
60
61 fp = fopen("output.txt", "w");
62 if (!fp) goto cleanup;
63
64 /* ... processing ... */
65 result = 0;
66
67cleanup:
68 if (fp) fclose(fp);
69 free(temp2); /* free(NULL) is safe — it's a no-op */
70 free(temp1);
71 return result;
72}

For many small allocations of the same size, a memory pool eliminates per-allocation overhead and makes cleanup trivial:

memory_pool.c
C
1#include <stdlib.h>
2#include <string.h>
3
4/* Simple memory pool for fixed-size blocks */
5typedef struct pool {
6 char *memory;
7 size_t block_size;
8 size_t capacity;
9 size_t used;
10} Pool;
11
12Pool *pool_create(size_t block_size, size_t count) {
13 Pool *pool = malloc(sizeof(Pool));
14 if (!pool) return NULL;
15
16 pool->memory = calloc(count, block_size);
17 if (!pool->memory) {
18 free(pool);
19 return NULL;
20 }
21
22 pool->block_size = block_size;
23 pool->capacity = count;
24 pool->used = 0;
25 return pool;
26}
27
28void *pool_alloc(Pool *pool) {
29 if (pool->used >= pool->capacity) return NULL;
30 void *ptr = pool->memory + (pool->used * pool->block_size);
31 pool->used++;
32 return ptr;
33}
34
35void pool_destroy(Pool *pool) {
36 if (!pool) return;
37 free(pool->memory); /* single free for entire pool */
38 free(pool);
39}
Error Handling

C has no exceptions. Errors are communicated through return values, and ignoring them is the fastest path to undefined behavior. Every function that can fail must signal failure, and every caller must check.

error_handling.c
C
1#include <errno.h>
2#include <stdio.h>
3#include <stdlib.h>
4#include <string.h>
5
6/* Return code convention: 0 = success, negative = error */
7typedef enum {
8 ERR_OK = 0,
9 ERR_NULL_PTR = -1,
10 ERR_OUT_OF_MEM = -2,
11 ERR_IO = -3,
12 ERR_INVALID = -4,
13 ERR_NOT_FOUND = -5
14} ErrorCode;
15
16const char *error_string(ErrorCode err) {
17 switch (err) {
18 case ERR_OK: return "success";
19 case ERR_NULL_PTR: return "null pointer";
20 case ERR_OUT_OF_MEM: return "out of memory";
21 case ERR_IO: return "I/O error";
22 case ERR_INVALID: return "invalid argument";
23 case ERR_NOT_FOUND: return "not found";
24 default: return "unknown error";
25 }
26}
27
28/* Always check return values — never ignore errors */
29ErrorCode read_file(const char *path, char **out_buf, size_t *out_len) {
30 FILE *fp = fopen(path, "rb");
31 if (!fp) {
32 fprintf(stderr, "ERROR [%s:%d]: fopen failed: %s\n",
33 __FILE__, __LINE__, strerror(errno));
34 return ERR_IO;
35 }
36
37 fseek(fp, 0, SEEK_END);
38 long file_size = ftell(fp);
39 fseek(fp, 0, SEEK_SET);
40
41 if (file_size < 0) {
42 fclose(fp);
43 return ERR_IO;
44 }
45
46 char *buf = malloc((size_t)file_size + 1);
47 if (!buf) {
48 fclose(fp);
49 return ERR_OUT_OF_MEM;
50 }
51
52 size_t nread = fread(buf, 1, (size_t)file_size, fp);
53 fclose(fp);
54
55 if ((long)nread != file_size) {
56 free(buf);
57 return ERR_IO;
58 }
59
60 buf[nread] = '\0';
61 *out_buf = buf;
62 *out_len = nread;
63 return ERR_OK;
64}

The goto cleanup pattern is the standard way to handle multiple cleanup requirements in C. Despite its reputation, goto is the correct tool for this:

goto_cleanup.c
C
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4
5/* The goto cleanup pattern — recommended by Linux kernel style */
6int handle_request(const char *user, const char *action, const char *data) {
7 char *processed = NULL;
8 char *response = NULL;
9 FILE *log = NULL;
10 int status = -1;
11
12 if (!user || !action || !data) {
13 status = -1;
14 goto done;
15 }
16
17 processed = malloc(strlen(data) + 1);
18 if (!processed) {
19 fprintf(stderr, "malloc failed\n");
20 goto done;
21 }
22 strcpy(processed, data);
23
24 /* Simulate some processing */
25 if (strcmp(action, "create") == 0) {
26 if (create_resource(processed) != 0) {
27 goto done;
28 }
29 } else if (strcmp(action, "delete") == 0) {
30 if (delete_resource(processed) != 0) {
31 goto done;
32 }
33 } else {
34 fprintf(stderr, "unknown action: %s\n", action);
35 goto done;
36 }
37
38 log = fopen("audit.log", "a");
39 if (log) {
40 fprintf(log, "user=%s action=%s\n", user, action);
41 fclose(log);
42 log = NULL;
43 }
44
45 status = 0; /* success */
46
47done:
48 /* Cleanup always runs — regardless of success or failure */
49 if (log) fclose(log);
50 free(response);
51 free(processed);
52 return status;
53}

best practice

Use goto cleanup for multi-resource cleanup, not for general control flow. The label should always be at the bottom of the function, and cleanup should always free in reverse allocation order.
Performance Best Practices

Premature optimization is the root of all evil — but profiling and applying targeted optimizations is engineering. Always measure before optimizing.

performance.c
C
1#include <stddef.h>
2
3/* Compiler optimization flags (in Makefile):
4 * gcc -O2 -o program main.c (safe, recommended)
5 * gcc -O3 -o program main.c (aggressive, may increase binary size)
6 * gcc -Os -o program main.c (size-optimized, good for embedded)
7 * gcc -march=native -O2 -o program main.c (optimize for your CPU)
8 */
9
10/* BRANCH PREDICTION HINTS — use __builtin_expect for hot paths */
11#define LIKELY(x) __builtin_expect(!!(x), 1)
12#define UNLIKELY(x) __builtin_expect(!!(x), 0)
13
14int fast_path_example(int *data, size_t len) {
15 int sum = 0;
16 for (size_t i = 0; i < len; i++) {
17 /* Errors are rare — tell the compiler to optimize for the common case */
18 if (LIKELY(data[i] > 0)) {
19 sum += data[i];
20 }
21 }
22 return sum;
23}
24
25/* RESTRICT POINTERS — promise the compiler no aliasing */
26void add_arrays(int *restrict dst, const int *restrict a,
27 const int *restrict b, size_t n) {
28 /* Compiler can vectorize this more aggressively because it knows
29 dst, a, and b do not overlap */
30 for (size_t i = 0; i < n; i++) {
31 dst[i] = a[i] + b[i];
32 }
33}
34
35/* CACHE-FRIENDLY ACCESS — iterate in memory order */
36/* BAD: column-major iteration of a row-major array */
37void sum_columns_bad(int *matrix, int rows, int cols) {
38 int sum = 0;
39 for (int c = 0; c < cols; c++) {
40 for (int r = 0; r < rows; r++) {
41 sum += matrix[r * cols + c]; /* jumps by cols elements each time */
42 }
43 }
44}
45
46/* GOOD: process one row at a time, accumulate in a separate array */
47void sum_columns_good(int *matrix, int rows, int cols, int *col_sums) {
48 memset(col_sums, 0, cols * sizeof(int));
49 for (int r = 0; r < rows; r++) {
50 for (int c = 0; c < cols; c++) {
51 col_sums[c] += matrix[r * cols + c]; /* sequential access */
52 }
53 }
54}
🔥

pro tip

Use -march=native when building for a specific machine. It enables SIMD instructions (SSE, AVX, NEON) that can speed up tight loops by 4-8x. Never use it for distribution binaries — only for local builds.
OptimizationTechniqueImpact
Compiler flags-O2 -march=native2-5x speedup over -O0
Branch prediction__builtin_expect10-20% on branch-heavy code
Restrict pointersrestrict keywordEnables auto-vectorization
Cache accessSequential memory access10-100x vs random access
AllocationAvoid allocations in hot loopsmalloc/free are expensive
Common Mistakes

These are the subtle pitfalls that even experienced C developers fall into. Each one leads to bugs that can be extremely difficult to diagnose:

common_mistakes.c
C
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4
5/* MISTAKE 1: Array-pointer confusion */
6void array_decay_example(void) {
7 int arr[10];
8 int *ptr = arr; /* arrays decay to pointers — sizeof differences */
9
10 printf("sizeof(arr) = %zu\n", sizeof(arr)); /* 40 (10 * 4) */
11 printf("sizeof(ptr) = %zu\n", sizeof(ptr)); /* 8 (pointer size) */
12}
13
14/* MISTAKE 2: Signed vs unsigned comparison */
15void signed_unsigned_trap(void) {
16 size_t len = 5; /* unsigned */
17 int i = -1; /* signed */
18
19 /* BUG: -1 is implicitly converted to a huge unsigned value */
20 if (i < len) {
21 printf("This never prints!\n");
22 }
23
24 /* CORRECT: both operands should be the same signedness */
25 if ((int)len >= 0 && i < (int)len) {
26 printf("This prints correctly\n");
27 }
28}
29
30/* MISTAKE 3: sizeof on a pointer vs pointed-to type */
31void sizeof_misconception(int *arr, size_t n) {
32 /* sizeof(arr) is sizeof(int*), NOT sizeof the array */
33 size_t total = n * sizeof(arr[0]); /* correct: use element size */
34 int *copy = malloc(total);
35 if (copy) {
36 memcpy(copy, arr, total);
37 }
38}
39
40/* MISTAKE 4: Struct padding in binary I/O */
41typedef struct {
42 char a; /* 1 byte + 3 bytes padding */
43 int b; /* 4 bytes */
44 char c; /* 1 byte + 3 bytes padding */
45} PackedTrick; /* sizeof = 12, NOT 6! */
46
47void binary_io_mistake(void) {
48 PackedTrick val = {'x', 42, 'y'};
49 /* fwrite(&val, sizeof(val), 1, fp) writes 12 bytes, not 6 */
50 /* If you need exact layout, use __attribute__((packed)) or #pragma pack */
51}
52
53/* MISTAKE 5: Modifying string literals */
54void string_literal_trap(void) {
55 char *str = "hello"; /* str points to read-only memory */
56 /* str[0] = 'H'; /* UNDEFINED BEHAVIOR: segfault */
57 char mutable[] = "hello"; /* CORRECT: array copies to writable memory */
58 mutable[0] = 'H'; /* This is fine */
59}
60
61/* MISTAKE 6: Implicit int assumptions */
62void int_size_mistake(void) {
63 /* int is NOT guaranteed to be 32 bits! */
64 /* Use fixed-width types when size matters */
65 int32_t guaranteed_32 = 1000000;
66 int64_t guaranteed_64 = 1000000000000LL;
67
68 /* For sizes, use size_t. For file offsets, use off_t or int64_t */
69}
70
71/* MISTAKE 7: Missing break in switch */
72void switch_fallthrough_trap(int value) {
73 switch (value) {
74 case 1:
75 printf("one\n");
76 /* missing break — falls through to case 2! */
77 case 2:
78 printf("two\n");
79 break;
80 default:
81 break;
82 }
83}

warning

Always use -Wswitch and -Wimplicit-fallthrough (part of -Wextra in modern GCC) to catch missing break statements. GCC 7+ recognizes /* fallthrough */ comments to document intentional fallthrough.
Project Structure

A well-organized C project scales from a single-file prototype to a large multi-module codebase without requiring a rewrite. These conventions work for projects of any size.

project_layout.txt
TEXT
1project/
2├── include/ # Public headers (.h files)
3│ └── project/
4│ ├── module_a.h
5│ ├── module_b.h
6│ └── project.h # Umbrella header
7├── src/ # Implementation files (.c files)
8│ ├── module_a.c
9│ ├── module_b.c
10│ └── main.c
11├── tests/ # Unit and integration tests
12│ ├── test_module_a.c
13│ ├── test_module_b.c
14│ └── test_main.c
15├── lib/ # Third-party libraries
16│ └── some_lib/
17├── docs/ # Documentation
18│ └── api.md
19├── build/ # Build output (git-ignored)
20│ ├── debug/
21│ └── release/
22├── CMakeLists.txt # Build system (or Makefile)
23├── .gitignore
24├── README.md
25└── LICENSE

A minimal but complete CMakeLists.txt for a C project:

CMakeLists.txt
CMAKE
1cmake_minimum_required(VERSION 3.14)
2project(myapp VERSION 1.0.0 LANGUAGES C)
3
4set(CMAKE_C_STANDARD 11)
5set(CMAKE_C_STANDARD_REQUIRED ON)
6set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
7
8# Debug build: cmake -DCMAKE_BUILD_TYPE=Debug ..
9# Release build: cmake -DCMAKE_BUILD_TYPE=Release ..
10
11add_compile_options(-Wall -Wextra -pedantic -Werror)
12
13# Library
14add_library(mylib STATIC src/module_a.c src/module_b.c)
15target_include_directories(mylib PUBLIC include)
16
17# Executable
18add_executable(myapp src/main.c)
19target_link_libraries(myapp PRIVATE mylib)
20
21# Tests
22enable_testing()
23add_executable(test_all tests/test_module_a.c tests/test_module_b.c)
24target_link_libraries(test_all PRIVATE mylib)
25add_test(NAME unit_tests COMMAND test_all)

Essential .gitignore for C projects:

.gitignore
TEXT
1# Build outputs
2*.o
3*.a
4*.so
5*.dylib
6*.exe
7build/
8out/
9
10# Debug
11core
12core.*
13*.dSYM/
14vgcore.*
15
16# IDE
17.vscode/
18.idea/
19*.swp
20*.swo
21*~
22
23# Coverage
24*.gcda
25*.gcno
26*.gcov
27coverage/
Recommended Tools

The right toolchain makes C development dramatically safer and more productive. Here is the essential stack:

CategoryToolNotes
CompilersGCC, Clang, MSVCClang has best diagnostics; GCC most portable
Static Analyzerscppcheck, clang-tidyRun on every commit via CI
Dynamic AnalyzersValgrind, ASan, UBSanASan for dev, Valgrind for deep analysis
Build SystemsCMake, Meson, MakeCMake dominates; Meson is modern alternative
EditorsVS Code + clangdclangd gives IDE-quality completions in C
Formattersclang-formatEnforce consistent style automatically
DocumentationDoxygenStandard for C API documentation

A practical development workflow that catches bugs at every stage:

dev_workflow.sh
Bash
1# 1. Format code automatically
2clang-format -i src/*.c include/*.h
3
4# 2. Static analysis — catch bugs without running code
5cppcheck --enable=all --error-exitcode=1 src/
6
7# 3. Build with all warnings and sanitizers (debug)
8cmake -DCMAKE_BUILD_TYPE=Debug \
9 -DCMAKE_C_FLAGS="-fsanitize=address,undefined" ..
10make
11
12# 4. Run tests
13ctest --output-on-failure
14
15# 5. Run Valgrind on test suite
16valgrind --leak-check=full --error-exitcode=1 ./test_all
17
18# 6. Build release binary
19cmake -DCMAKE_BUILD_TYPE=Release ..
20make

best practice

Integrate cppcheck and clang-tidy into your CI pipeline. A static analysis pass that runs on every pull request catches entire categories of bugs before they reach review. Use clang-format to eliminate style debates entirely.
Summary of Rules

These are the non-negotiable rules for production C code. Print this, tape it to your monitor, make it a team charter:

#RuleWhy
1Always compile with -Wall -Wextra -WerrorWarnings are bugs in disguise
2Never use gets(), sprintf(), strcpy()Buffer overflow sources
3Always check malloc() return valueNULL dereference on OOM
4Free memory in reverse allocation orderSimplifies cleanup logic
5Set pointers to NULL after freeingPrevents use-after-free
6Use snprintf() not sprintf()Bounds checking on output
7Validate all external inputTrust nothing, sanitize everything
8Use goto cleanup for multi-resource functionsClean, correct resource management
9Run ASan + UBSan during developmentCatch memory and UB bugs instantly
10Write tests for every moduleRegression prevention
$Blueprint — Engineering Documentation·Section ID: C-BEST-PRACTICES·Revision: 1.0