awesome-lists/c-programming-guidelines.md

16 KiB

C Programming Guidelines

1. Naming Conventions

1.1 Use snake_case for variables and functions

  • Names should be descriptive but concise
  • Use lowercase with underscores for separation

Good:

int user_count = 0;
float calculate_average(int *values, int count);
char *get_user_input(void);

Bad:

int UserCount = 0;      // Uses PascalCase instead of snake_case
float calcAvg(int *v, int c);  // Unclear abbreviated names
char *input(void);      // Too vague

1.2 Use UPPERCASE for constants and macros

  • All capitals with underscores for separation

Good:

#define MAX_BUFFER_SIZE 1024
#define PI 3.14159
const int ERROR_CODE_FILE_NOT_FOUND = -1;

Bad:

#define maxbuffersize 1024  // Should be uppercase with underscores
#define Pi 3.14159          // Should be all uppercase
const int errorCode = -1;   // Should be uppercase with underscores

1.3 Use PascalCase for types

  • First letter of each word should be capitalized
  • This applies to struct, enum, union, and typedef names

Good:

typedef struct {
    int x;
    int y;
} Point;

typedef enum {
    FileStatusOk,
    FileStatusNotFound,
    FileStatusPermissionDenied
} FileStatus;

Bad:

typedef struct {
    int x;
    int y;
} point;  // Should start with capital P

typedef enum {
    FILE_STATUS_OK,  // Should use PascalCase, not screaming snake case for enum values
    FILE_STATUS_NOT_FOUND,
    FILE_STATUS_PERMISSION_DENIED
} file_status;  // Should start with capital F

2. Code Formatting

2.1 Use consistent indentation

  • 4 spaces is the most common choice
  • Be consistent throughout your project

Good:

if (condition) {
    printf("Condition is true\n");
    if (another_condition) {
        printf("Both conditions are true\n");
    }
}

Bad:

if (condition) {
  printf("Condition is true\n");
    if (another_condition) {
     printf("Both conditions are true\n");
        }
}

2.2 Use consistent brace style

  • K&R style (opening brace on same line) is common in C
  • Whatever style you choose, be consistent

Good (K&R style):

if (condition) {
    statement1;
    statement2;
} else {
    statement3;
    statement4;
}

Also Good (Allman style, if used consistently):

if (condition)
{
    statement1;
    statement2;
}
else
{
    statement3;
    statement4;
}

Bad (mixing styles):

if (condition) {
    statement1;
    statement2;
}
else {
    statement3;
    statement4;
}

2.3 Use proper spacing around operators and keywords

  • Add spaces after commas and around operators
  • Add space after keywords like if, while, for, but not after function names

Good:

for (int i = 0; i < 10; i++) {
    result = calculate(a + b, c * d);
    if (result > threshold) {
        break;
    }
}

Bad:

for(int i=0;i<10;i++){
    result=calculate(a+b,c*d);
    if(result>threshold){
        break;
    }
}

3. Function Guidelines

3.1 Keep functions short and focused

  • Each function should have a single responsibility
  • Aim for 20-30 lines maximum when possible

Good:

int validate_input(const char *input) {
    if (input == NULL) {
        return ERROR_NULL_INPUT;
    }
    
    if (strlen(input) > MAX_INPUT_LENGTH) {
        return ERROR_INPUT_TOO_LONG;
    }
    
    if (!contains_valid_chars(input)) {
        return ERROR_INVALID_CHARS;
    }
    
    return INPUT_VALID;
}

Bad:

int process_input(const char *input) {
    // 100+ lines of code that:
    // - validates input
    // - parses input
    // - processes data
    // - updates database
    // - generates report
    // - handles errors
    // All mixed together
}

3.2 Use clear parameter names and document function behavior

  • Input parameters should come first, output parameters last
  • Use consistent return value semantics

Good:

/**
 * Splits a string into tokens based on a delimiter.
 *
 * @param str String to tokenize (modified by this function)
 * @param delim Characters that serve as delimiters
 * @return Pointer to the next token or NULL if no more tokens found
 */
char *tokenize_string(char *str, const char *delim) {
    // Implementation
}

Bad:

// No documentation, unclear parameter names
char *ts(char *s, const char *d) {
    // Implementation
}

3.3 Check return values and handle errors

  • Always check return values of functions that can fail
  • Have explicit error paths

Good:

FILE *file = fopen("data.txt", "r");
if (file == NULL) {
    fprintf(stderr, "Error opening file: %s\n", strerror(errno));
    return ERROR_FILE_OPEN_FAILED;
}

// Use the file

fclose(file);

Bad:

FILE *file = fopen("data.txt", "r");
// No error checking!

// Use the file - potential NULL pointer dereference

fclose(file);

4. Memory Management

4.1 Always pair allocation with deallocation

  • Every malloc() needs a matching free()
  • Check for allocation failures

Good:

int *data = (int *)malloc(size * sizeof(int));
if (data == NULL) {
    fprintf(stderr, "Memory allocation failed\n");
    return ERROR_MEMORY_ALLOCATION;
}

// Use the allocated memory

free(data);
data = NULL;  // Prevent use after free

Bad:

int *data = (int *)malloc(size * sizeof(int));
// No NULL check!

// Use the allocated memory

// No free - memory leak!

4.2 Use consistent resource management patterns

  • Release resources in reverse order of acquisition
  • Consider using goto for error cleanup in C

Good:

int process_data(void) {
    FILE *input_file = NULL;
    FILE *output_file = NULL;
    char *buffer = NULL;
    int result = SUCCESS;
    
    buffer = (char *)malloc(BUFFER_SIZE);
    if (buffer == NULL) {
        result = ERROR_MEMORY_ALLOCATION;
        goto cleanup;
    }
    
    input_file = fopen("input.txt", "r");
    if (input_file == NULL) {
        result = ERROR_INPUT_FILE_OPEN;
        goto cleanup;
    }
    
    output_file = fopen("output.txt", "w");
    if (output_file == NULL) {
        result = ERROR_OUTPUT_FILE_OPEN;
        goto cleanup;
    }
    
    // Process data using all resources
    
cleanup:
    // Clean up in reverse order
    if (output_file != NULL) fclose(output_file);
    if (input_file != NULL) fclose(input_file);
    free(buffer);
    
    return result;
}

Bad:

int process_data(void) {
    // Inconsistent error handling and resource cleanup
    char *buffer = (char *)malloc(BUFFER_SIZE);
    FILE *input_file = fopen("input.txt", "r");
    
    if (input_file == NULL) {
        free(buffer);
        return ERROR_INPUT_FILE_OPEN;
    }
    
    FILE *output_file = fopen("output.txt", "w");
    // Forgot to check if output_file is NULL
    
    // Process data
    
    fclose(input_file);
    fclose(output_file);  // Might be NULL!
    // Forgot to free buffer - memory leak!
    
    return SUCCESS;
}

5. Header Files

5.1 Always use include guards

  • Prevent multiple inclusion with include guards
  • Use a unique name based on file path

Good:

#ifndef PROJECT_MODULE_HEADER_H
#define PROJECT_MODULE_HEADER_H

// Header content

#endif /* PROJECT_MODULE_HEADER_H */

Bad:

// No include guard - can cause multiple definition errors

typedef struct {
    int x;
    int y;
} Point;

void draw_point(Point p);

5.2 Organize includes consistently

  • Include system headers first, then project headers
  • Group related declarations

Good:

/* System includes */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Project includes */
#include "project/common.h"
#include "project/utils.h"

/* Module-specific includes */
#include "module/internal.h"

Bad:

#include "module/internal.h"
#include <string.h>
#include "project/common.h"
#include <stdio.h>
#include "project/utils.h"
#include <stdlib.h>

5.3 Declare what you use, use what you declare

  • Include only what you need
  • Don't rely on indirect includes

Good:

// In file.h
#ifndef FILE_H
#define FILE_H

#include <stddef.h>  // For size_t

// Declare only what other modules need
size_t read_file(const char *filename, char *buffer, size_t buffer_size);

#endif /* FILE_H */

// In file.c
#include "file.h"
#include <stdio.h>   // For FILE, fopen, etc.
#include <string.h>  // For strerror

size_t read_file(const char *filename, char *buffer, size_t buffer_size) {
    // Implementation using stdio functions
}

Bad:

// In file.h
#ifndef FILE_H
#define FILE_H

#include <stdio.h>    // Exposing implementation details
#include <string.h>   // Not needed in the interface
#include <stdlib.h>   // Not needed in the interface

// Using stdio in the interface
FILE *open_file(const char *filename);
size_t read_file(FILE *file, char *buffer, size_t buffer_size);

#endif /* FILE_H */

6. Comments and Documentation

6.1 Document function interfaces

  • Describe purpose, parameters, return values, and side effects
  • Document error conditions

Good:

/**
 * Searches for a substring within a string.
 *
 * @param haystack  The string to search in
 * @param needle    The substring to search for
 * @return Pointer to the first occurrence of needle in haystack,
 *         or NULL if needle is not found
 */
char *find_substring(const char *haystack, const char *needle) {
    // Implementation
}

Bad:

// Finds a string
char *find_substring(const char *s1, const char *s2) {
    // Implementation
}

6.2 Comment complex or non-obvious code

  • Explain the "why" not the "what"
  • Keep comments up to date with code changes

Good:

// Use binary search to find insertion point while maintaining ordering
// This is O(log n) rather than the O(n) approach of linear search
int insertion_index = find_insertion_point(array, value, 0, array_size - 1);

// Special case: The XDR protocol requires 4-byte alignment for data
// so we pad the buffer with zeros as needed
size_t padding = (4 - (buffer_size % 4)) % 4;
memset(buffer + buffer_size, 0, padding);

Bad:

// This function finds the substring
char *p = strstr(s, sub);

// Increment i
i++;

// Too much detail about "what" the code does, not "why"
// Loop through the array and add each element to sum
int sum = 0;
for (int i = 0; i < size; i++) {
    sum += array[i];  // Add element to sum
}

7. Error Handling

7.1 Use consistent error reporting

  • Choose one approach (return codes, errno, error objects) and stick with it
  • Document error codes

Good:

/**
 * Error codes for the file module
 */
#define FILE_ERROR_NONE              0
#define FILE_ERROR_NOT_FOUND        -1
#define FILE_ERROR_PERMISSION       -2
#define FILE_ERROR_INVALID_FORMAT   -3

/**
 * Opens a configuration file and validates its format.
 *
 * @param filename The path to the config file
 * @return FILE_ERROR_NONE on success, or appropriate error code on failure
 */
int open_config_file(const char *filename) {
    if (access(filename, F_OK) != 0) {
        return FILE_ERROR_NOT_FOUND;
    }
    
    if (access(filename, R_OK) != 0) {
        return FILE_ERROR_PERMISSION;
    }
    
    // Check file format
    if (!is_valid_config_format(filename)) {
        return FILE_ERROR_INVALID_FORMAT;
    }
    
    return FILE_ERROR_NONE;
}

Bad:

// Inconsistent error reporting
int open_file(const char *filename) {
    FILE *file = fopen(filename, "r");
    if (file == NULL) {
        printf("Error opening file\n");
        return -1;  // Generic error code
    }
    
    if (!is_valid_format(file)) {
        fprintf(stderr, "Invalid format\n");
        fclose(file);
        exit(1);  // Abrupt program termination!
    }
    
    // No error, but doesn't return the file handle!
    fclose(file);
    return 0;
}

7.2 Validate function inputs

  • Check parameters for validity at the beginning of functions
  • Return early for invalid inputs

Good:

int calculate_average(const int *values, size_t count, double *result) {
    // Validate inputs
    if (values == NULL || result == NULL) {
        return ERROR_NULL_POINTER;
    }
    
    if (count == 0) {
        return ERROR_DIVISION_BY_ZERO;
    }
    
    // Calculation is safe now
    double sum = 0;
    for (size_t i = 0; i < count; i++) {
        sum += values[i];
    }
    
    *result = sum / count;
    return SUCCESS;
}

Bad:

double calculate_average(int *values, int count) {
    // No input validation!
    
    double sum = 0;
    for (int i = 0; i < count; i++) {
        sum += values[i];  // Potential NULL pointer dereference!
    }
    
    return sum / count;  // Potential division by zero!
}

8. Defensive Programming

8.1 Check array bounds

  • Never access arrays out of bounds
  • Use size parameters to track array sizes

Good:

/**
 * Finds the maximum value in an array.
 */
int find_maximum(const int *array, size_t size) {
    if (array == NULL || size == 0) {
        return INT_MIN;  // Error value
    }
    
    int max = array[0];
    for (size_t i = 1; i < size; i++) {
        if (array[i] > max) {
            max = array[i];
        }
    }
    
    return max;
}

Bad:

int find_maximum(int *array, int size) {
    // No validation on array or size
    
    int max = array[0];  // Crash if array is NULL or size is 0
    
    // Could go out of bounds if size is incorrect
    for (int i = 1; i <= size; i++) {  // Note: <= is wrong!
        if (array[i] > max) {
            max = array[i];
        }
    }
    
    return max;
}

8.2 Initialize variables before use

  • Always initialize variables
  • Don't rely on default initialization

Good:

void process_data(void) {
    int count = 0;
    double total = 0.0;
    char buffer[MAX_SIZE] = {0};  // Zero-initialize array
    
    // Variables are initialized before use
}

Bad:

void process_data(void) {
    int count;      // Uninitialized
    double total;   // Uninitialized
    char buffer[MAX_SIZE];  // Uninitialized
    
    // Using variables without initialization
    total = total + 10.0;  // Undefined behavior
}

9. Performance Considerations

9.1 Prefer readability unless performance is critical

  • Write clear, readable code first
  • Optimize only when necessary and based on profiling

Good:

// Clear and readable implementation
double calculate_average(const int *values, size_t count) {
    if (values == NULL || count == 0) {
        return 0.0;
    }
    
    double sum = 0.0;
    for (size_t i = 0; i < count; i++) {
        sum += values[i];
    }
    
    return sum / count;
}

Bad (premature optimization):

// Trying to be clever with unnecessary optimization
double calculate_average(const int *values, size_t count) {
    if (!values || !count) return 0.0;
    
    register double sum = 0.0;  // 'register' rarely helps modern compilers
    register size_t i = count;
    
    // Reversed loop for "performance" but harder to understand
    while (i--) {
        sum += values[i];
    }
    
    return sum / count;
}

9.2 Document performance-critical code

  • Explain the reasoning behind optimization
  • Note any assumptions that affect performance

Good:

/**
 * Fast string hash function optimized for short strings.
 * Uses FNV-1a algorithm which has good distribution and
 * is quick for strings under 100 bytes.
 * 
 * Time complexity: O(n) where n is string length
 * Performance assumption: Most strings are under 20 chars
 */
unsigned int hash_string(const char *str) {
    const unsigned int FNV_PRIME = 16777619;
    const unsigned int FNV_OFFSET_BASIS = 2166136261;
    
    unsigned int hash = FNV_OFFSET_BASIS;
    
    while (*str) {
        hash ^= (unsigned char)*str++;
        hash *= FNV_PRIME;
    }
    
    return hash;
}

Bad:

// No documentation of algorithm choice or performance characteristics
unsigned int hash_string(const char *str) {
    unsigned int hash = 2166136261;
    
    while (*str) {
        hash ^= (unsigned char)*str++;
        hash *= 16777619;
    }
    
    return hash;
}