badd10de.dev

C Programming


C is on of the most used systems programming languages in the world. Your OS will probably a good chunk of C in it. After some years of C++ programming I wanted to expore the more minimal core of C99 and C11, and I’m having a blast with it.

Style guide

This is my personal style guide for writing C code. Some more or less objective rules of thumb and my own biased preferences. Everyone has their own preferences, and one should try to respect the style guide if working with other people on different projects. Consistency is key.

Resources

Here are some style guides that I’ve read from other people. I agree with some parts of these and disagree with others. Ultimately some choices are philosophical and other are more or less practical, so pick your own style and try to stick to it. If working with other people try to agree on some guidelines, use the project existing style or use some autoformatters to avoid pointless discussions.

Notes on the language

The static keyword

The inline keyword

The inline keyword can be thought as a hint to the compiler to increase the likelihood of a function to be inlined. In order for that to work, it needs to be used with static as well, otherwise, inline can be used as an alternative implementation for an existing function. For example if we have an int fun() in translation unit fun.c and inline fun() in bar.c, and we call fun() from bar.c, if the function gets inlined, the inline fun() will be used instead of the one in fun.c. This can be a big problem if the functions don’t behave exactly the same, and it will increase the burden of maintaining two different versions.

Inline functions should be both declared and defined in header files unless those functions have internal linkage. In C, if also want non inline for the same funcion we would add a definition on a single .c file.

//
// max_val.h
//

inline int
max_val(int a, int b) {
    return a > b ? a : b;
}

//
// max_val.c
//

#include "max_val.h"

int
max_val(int a, int b);

// or

extern int
max_val(int a, int b);

// or

extern inline int
max_val(int a, int b);

// this doesn't work here!
inline int
max_val(int a, int b);

We can also force the inlining by passing a compiler parameter, i.e. __atribute__((always_inline)) inline int fun(), but in general we can trust that the compiler will optimize the inlines as needed.

The <stdarg.h> header

Contains macros to access variadic functions. Use the following example as a guide:

#include <stdarg.h>

int
sum_numbers(int num, ...) {
    va_list args;
    va_init(args, num);
    int sum = 0;
    for (size_t i; i < num; ++i) {
        sum += va_arg(args, int);
    }
    va_end(args);
    return sum;
}

Strings with <string.h>

// Split file into words.
char *token = webster;
char **all_tokens = NULL;
for (size_t i = 0; i < n_words; ++i) {
    // Limit number of words
    if (i >= n_words) {
        break;
    }
    if (i == 0) {
        token = strtok(token, " \n\t\r\v\f");
    } else {
        token = strtok(NULL, " \n\t\r\v\f");
    }
    if (token == NULL) {
        break;
    }
    dyn_push(all_tokens, token); // Dynamic array from my own library.
}

Reference

char *strtok(char *str, const char *delim);
char *strtok_r(char *str, const char *delim, char **saveptr);

About const and pointers

Here is an example of const pointers usage depending on what do we want to make const, taken from Correct usage of const with pointers:

// Neither the data nor the pointer are const
char* ptr = "just a string";

// Constant data, non-constant pointer
const char* ptr = "just a string";

// Constant pointer, non-constant data
char* const ptr = "just a string";

// Constant pointer, constant data
const char* const ptr = "just a string";

IO with <stdio.h>

Read an entire file into memory

char *
read_file(char *file_name) {
    FILE *file = fopen(file_name, "r");
    if (!file) {
        fprintf(stderr, "couldn't open file: %s\n", file_name);
        exit(-1);
    }

    fseek(file, 0, SEEK_END);          // Set file pointer to the end of file
    size_t file_size = ftell(file);    // Store the current position in file (in Bytes)
    fseek(file, 0, SEEK_SET);          // Set file pointer to beginning.
    char *str = malloc(file_size + 1); // Allocate memory for file.
    fread(str, 1, file_size, file);    // Copy 1 (Byte) * file_size from file into str.
    str[file_size] = 0;                // Set the null terminator.

    fclose(file);
    return str; // NOTE: We are returning a pointer, caller must free it!
}

Extra printf/scanf options

// Print 10 characters
printf("%.10s", string);

// Print n characters
printf("%.*s", n, string);

// Format a numeric value as hex with up to 4 leading zeros.
printf("0x%04x\n", number); // Lowercase
printf("0x%04X\n", number); // Uppercase

// Read 10 characters
scanf("%10s", input);

// Read 10 vowels using scansets
scanf("%10[aeiou]s", input);

Read characters until end of file

int8_t c;
while ((c = getchar()) != EOF) {
    // do stuff...
}

Binary shifts

We can create an M cirular shift of an Nbit unsigned integer by doing:

uintN_t x = 1231215198;
uintN_t shift_M_left = (x << M) | (x >> (x - N));
uintN_t shift_M_right = (x >> M) | (x << (x - N));

Assertions/Tests with <assert.h>

We can quickly create a minimal test suite using asserts. If we want to display a message about the test, we can do so with:

assert(1 == 0 && "Message goes here");

Assertions will be removed when compiled with -DNDEBUG (They become NOPs). They also call abort(), which will dump the core if possible for further debugging.

Complex numbers <complex.h>

We have support for complex numbers with the _Complex values, but this header adds some nice typedefs. Arithmetic, equality, assignment and compoound assignment work with complex numbers.

double complex a = 2;   // 2 + 0i
double imaginary = 2;   // 2i
double complex = 6 * I; // 6i
d = a + b * c;
printf("%g + %gi\n", creal(d), cimag(d));

Designated initializers

One of the main reasons for me to use C99. They give a lot of utility for initializing structures and arrays. You can even use these to pass pointers to structs without creating a temporary. You can use this also for arrays by specifying the desired position.

struct Foo {
    int a;
    int b;
    int c;
};

// Direct structure initialization. Not given fields will be zero initialized.
struct Foo foo = (struct Foo){.a = 1, .b = 2};

// Zero initialization except for index 3 and 7
int arr[10] = {[3] = 1, [7] = 2};

// Take address of temporary.
// void add_vectors(struct Vec3 *, struct Vec3 *)
add_vectors(&(struct Vec3){.x = 2, .y = 3}, &(struct Vec3){.x = 1, .y = 2});

Flexible Array Members

The last field in a structure can be a variable array, for example if we have a packet structure such as this:

struct Packet {
    header h;
    data d[];
};

We can allocate the memory for it as follows:

Packet *p = malloc(sizeof(Packet) + n * sizeof(data));

This solves potential issues with padding. If so, sizeof will return the size of the package up to but not including the data member (Including padding).

Resources

Parallelism/Concurrency

Memory and OS resources

Libraries/Tools

Debugging/profiling

Networking

UI

Other

Books

Talks