16 KiB
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 matchingfree() - 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;
}