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 much prefer it to any other system programming language alternatives.
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.
- Create heap allocated resources by returning a pointer to them. The function should be called
fun_create
and freed withfun_destroy
. - Use typedefs on function pointers or objects that are suppossed to be opaque from the user perspective.
- Declare each variable in a separate line.
- Use braces even when optional.
- Indent with 4 spaces.
- When writing function implementations, put return argument argumenets on a separate line from function name to be able to query the codebase for
^fun_name
. - Use double slash comments when possible
//
. I find these much easier to read and write, and they are supported sinceC99
. I tend to comment a lot, sometimes a bit too much, but this is because I normally start writing new functions by outlining the desired behaviour with comments and then implement it step by step. Future-me likes past-me when reading code that has been pushed off the mental cache. - Don’t be afraid of using long variable names. It’s good to try to be as concise as possible, but clarity is very important to me, so I would favour
int node_count
overint nc
. The exception to this are indexes, where normally I will usei, j, k, n, m, z, p...
.
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
- static global: Used as a global makes the variable/function only visible for that translation unit. If a function is made static, we ensure that the identifier name will not conflict with others outside of its translation unit. For example, if on
head.h
I declare astatic int do_the_thing(void)
function and onkazoo.h
I declare a function with the same type signature and name, both will be valid, as long as I’m not trying to make use one in place of another. - static local: Used inside a function will make it persist, but will only be visible from within the function. This means that we have a persistent global state, but no one can touch it except its creator.
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>
- Try to use
strtok_r
instead ofstrtok
for string tokenization, as the former doesn’t have global state (It is reentrant). strtok_r
andstrtok
can take multiple delimiters as a string (",:"
will tokenize by comma or colon).strtok_r
andstrtok
will treat multiple empty delimiters as one instead of returning an empty string. For example"hey,hello,,world"
will be tokenized as["hey", "hello", "world"]
instead of["hey", "hello", "", "world"]
. If we need empty strings as delimiters we can roll our own tokenization or use non standardstrsep
.strtok_r
,strtok
andstrsep
are destructive operations. Make sure tomemcpy
orstrdup
the original string if you care for it.
// 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>
- Don’t take the address or try to set manually a FILE pointer. Some implementations rely on magic to make them work.
- After writing, call fseek(), fsetpos(), rewind(), or fflush() before reading.
- After reading, call fseek(), fsetpos(), or rewind() before writing unless the file is at EOF: a read that hits EOF can be followed immediately by a write.
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
- MMAP an entire large file into memory (StackOverflow)
- The lost art of structure packing
- How to Map Files into Memory in C (mmap) (Video)
- How do I measure how much memory my program is using? (getrusage) (Video)
- Writing a Simple Garbage Collector in C
Libraries/Tools
Debugging/profiling
- Flamegraphs
- The poor man’s profiler
- Tracy: A real time, nanosecond resolution, remote telemetry frame profiler
- RMS’s gdb debugger tutorial
- GDB tutorial
- Debugging with Core Dumps (Video)
- Debugging with GDB
Networking
UI
Other
- Why move away from C++? (C++ FQA)
- ##C wiki
- C FAQ
- C11 Standard
- C99 Standard
- Why Aren’t There C Conferences?
- Render Multimedia in Pure C, by Chris Wellons
- Interactive Programming in C, by Chris Wellons
- Portable Makefiles, by Chris Wellons
- Looking for entropy in all the wrong places, by Chris Wellons
- Small-Size Optimization in C, by Chris Wellons
- Handles are the better pointers by @flohofwoe
- Modern C for C++ Peeps by @flohofwoe
- Inside the C standard library by @begriffs
- What a C programmer should know about memory
- Disch’s tutorial to good binary files
- Speed Limits
- Understanding strict aliasing (Mike Acton)
- Using locks in real-time audio processing, safely (C++)
- Load CGLTF files in C
- Signal processing in C
- The basic ontology of a C program
Books
- The C programming language, by K&R (2nd edition)
- The Art of Computer Programming, by Donald Knuth
- C interfaces and implementations, by David R. Hanson
Talks
- CppCon 2014: Chandler Carruth “Efficiency with Algorithms, Performance with Data Structures”
- CppCon 2014: Mike Acton “Data-Oriented Design and C++”
- C++Now 2018: You Can Do Better than std::unorderedmap: New Improvements to Hash Table Performance
- Bloom filters, by Dr. Rob Edwards
- Modern C and what can we learn from it - Luca Sas (ACCU 2021)