Introduction
You now know how to write working C programs. This lesson focuses on writing good C programs: readable, maintainable, and robust. We’ll cover:
- Practical naming conventions for variables, functions, and files
- How to structure projects using header files for modular programming
- Commenting strategies that clarify (not clutter) code
- Defensive programming techniques to catch bugs early
- Memory management practices that prevent leaks and crashes
Naming Conventions in C
Clear names make code self-explanatory and reduce the need for comments and debugging.
General Guidelines
- Use lowercase_with_underscores for variables and functions:
int total_count;double compute_average(int sum, int n);
- Use UPPERCASE_WITH_UNDERSCORES for macros and constants:
#define BUFFER_SIZE 1024const int MAX_USERS = 100;
- Use nouns for variables and verbs for functions:
- Variables:
user_age,file_handle,error_code - Functions:
read_config(),parse_line(),log_error()
- Variables:
- Avoid abbreviations unless they’re standard (
idx,len,ptrare usually OK).
Example: Consistent Naming
#include <stdio.h>
#define MAX_LINE_LENGTH 256
int read_line(FILE *input, char *buffer, int max_length);
int main(void) {
char line_buffer[MAX_LINE_LENGTH];
int line_length;
line_length = read_line(stdin, line_buffer, MAX_LINE_LENGTH);
if (line_length > 0) {
printf("Read %d characters: %s\n", line_length, line_buffer);
}
return 0;
}
int read_line(FILE *input, char *buffer, int max_length) {
if (fgets(buffer, max_length, input) == NULL) {
return 0; // indicate no characters read
}
int length = 0;
while (buffer[length] != '\0') {
length++;
}
return length;
}
// Output (example)
// Read 5 characters: hello
Explanation:
- Function
read_lineclearly states what it does. line_buffer,line_length,MAX_LINE_LENGTHare descriptive and consistent.
Modular Programming with Header Files
You’ve already used header files; now we’ll focus on best practices when splitting code into multiple files.
Organizing Code into Modules
- Each logical component gets:
- A header file (
module.h) with:- Type definitions (
struct,enum) - Function declarations (prototypes)
- Macros and constants
- Type definitions (
- A source file (
module.c) with:- Function definitions
- A header file (
- Only expose what other files need. Keep internal helpers
staticin the.cfile.
Example: Simple Logger Module
logger.h
#ifndef LOGGER_H
#define LOGGER_H
#include <stdio.h>
void log_info(const char *message);
void log_error(const char *message);
#endif // LOGGER_H
logger.c
#include "logger.h"
static void log_with_prefix(const char *prefix, const char *message) {
fprintf(stderr, "%s: %s\n", prefix, message);
}
void log_info(const char *message) {
log_with_prefix("INFO", message);
}
void log_error(const char *message) {
log_with_prefix("ERROR", message);
}
main.c
#include "logger.h"
int main(void) {
log_info("Application started");
log_error("Unable to open configuration file");
return 0;
}
// Output
// INFO: Application started
// ERROR: Unable to open configuration file
Key points:
log_with_prefixisstaticand hidden from other files.logger.hexposes only the public API.
Commenting for Readability
Comments should explain why, not restate what the code already says.
Good Commenting Practices
- Document the purpose, inputs, and outputs of functions.
- Explain non-obvious logic, algorithms, or workarounds.
- Avoid commenting every line or describing trivial operations.
Example: Helpful vs. Redundant Comments
#include <stdio.h>
/*
* Returns 1 if year is a leap year according to the Gregorian calendar,
* otherwise returns 0.
*/
int is_leap_year(int year) {
// Year divisible by 400 is always a leap year
if (year % 400 == 0) return 1;
// Year divisible by 100 but not 400 is not a leap year
if (year % 100 == 0) return 0;
// Year divisible by 4 but not 100 is a leap year
if (year % 4 == 0) return 1;
// All other years are not leap years
return 0;
}
int main(void) {
int year = 2000;
if (is_leap_year(year)) {
printf("%d is a leap year\n", year);
} else {
printf("%d is not a leap year\n", year);
}
return 0;
}
// Output
// 2000 is a leap year
Notice there are no comments like // check if divisible by 400 right next to year % 400 == 0—the code is clear enough.
Defensive Programming Techniques
Defensive programming means assuming things will go wrong and handling them gracefully.
Core Techniques
- Validate all inputs (function parameters, user input, file handles).
- Check return values from library functions.
- Use
assertfor conditions that must never be false in a correct program. - Initialize variables before use.
Example: Validating Inputs and Using assert
#include <stdio.h>
#include <assert.h>
int safe_divide(int numerator, int denominator, int *result) {
if (result == NULL) {
return 0; // invalid output pointer
}
if (denominator == 0) {
return 0; // cannot divide by zero
}
*result = numerator / denominator;
return 1; // success
}
int main(void) {
int value;
int ok;
ok = safe_divide(10, 2, &value);
assert(ok); // should be true in this scenario
printf("10 / 2 = %d\n", value);
ok = safe_divide(10, 0, &value);
if (!ok) {
printf("Error: division by zero\n");
}
ok = safe_divide(10, 2, NULL);
if (!ok) {
printf("Error: result pointer is NULL\n");
}
return 0;
}
// Output
// 10 / 2 = 5
// Error: division by zero
// Error: result pointer is NULL
Practices shown:
- Validating pointers and parameters.
- Returning status codes instead of silently failing.
- Using
assertin places that should never fail in normal use.
Memory Management Best Practices
In C, memory errors are a major source of bugs. Good habits dramatically reduce leaks and crashes.
Key Rules
- Pair every
mallocwith exactly onefree. - Set pointers to
NULLafter freeing them; check forNULLbefore use. - Avoid returning pointers to local (stack) variables.
- Prefer smaller, well-defined lifetimes for dynamically allocated memory.
Example: Safe Allocation and Cleanup
#include <stdio.h>
#include <stdlib.h>
int read_integers(const char *filename, int **out_array, size_t *out_count) {
if (filename == NULL || out_array == NULL || out_count == NULL) {
return 0;
}
FILE *file = fopen(filename, "r");
if (file == NULL) {
return 0;
}
size_t capacity = 4;
size_t count = 0;
int *data = malloc(capacity * sizeof(int));
if (data == NULL) {
fclose(file);
return 0;
}
while (1) {
int value;
int read = fscanf(file, "%d", &value);
if (read != 1) break;
if (count == capacity) {
size_t new_capacity = capacity * 2;
int *temp = realloc(data, new_capacity * sizeof(int));
if (temp == NULL) {
free(data);
fclose(file);
return 0;
}
data = temp;
capacity = new_capacity;
}
data[count++] = value;
}
fclose(file);
*out_array = data;
*out_count = count;
return 1;
}
int main(void) {
int *numbers = NULL;
size_t count = 0;
if (!read_integers("numbers.txt", &numbers, &count)) {
printf("Failed to read integers.\n");
return 1;
}
printf("Read %zu integers:\n", count);
for (size_t i = 0; i < count; ++i) {
printf("%d ", numbers[i]);
}
printf("\n");
free(numbers);
numbers = NULL;
return 0;
}
// Example numbers.txt
// 1 2 3 4 5
// Output
// Read 5 integers:
// 1 2 3 4 5
Good practices illustrated:
- Checking allocation results (
malloc,realloc). - Cleaning up on every error path.
- Freeing memory in
mainafter use. - Using output parameters (
out_array,out_count) safely.
Summary / Key Takeaways
In this lesson on Best Coding Practices in C, you have learned:
- Consistent Naming Matters: Use clear, descriptive names with consistent casing to make code self-documenting.
- Modular Design with Headers: Separate interfaces into header files and implementations into source files, exposing only what’s necessary.
- Purposeful Comments: Comment the “why” and non-obvious “how”, and document function behavior instead of restating obvious code.
- Defensive Programming: Validate all inputs, check return values, and use assertions to detect impossible states during development.
- Safe Memory Management: Always match allocations with frees, handle allocation failures, and avoid using freed or uninitialized memory.
- Encapsulation of Internals: Hide helper functions inside modules using
staticto reduce coupling and prevent accidental misuse. - Error Reporting Conventions: Prefer returning status codes and using clear error messages instead of silently ignoring failures.
- Resource Cleanup Discipline: Ensure files, memory, and other resources are reliably released on both success and error paths.