Not Essentials But Must Haves

The topics below are really not essential to C programming but are heavily used in and out of this class. If you want to be a good C programmer, these are must haves.

Typedef


A handy concept for shortening down type definitions. Notice how you need to type out struct node every time you declare a variable. This get's really annoying after a while especially if you malloc structs constantly or write a lot of functions using structs. But it really helps in code readability.

Aside: Linus Torvalds begs to differ.. Pepper ain't he? Tl;dr - don't hide things unnecessarily behind typedefs. Particularly pointers; never do that.

//you can typedef immediately in a struct definition
typedef struct node{
    int data;
    struct node next;
}node_t; //I'm pretty sure the _t is just convention for structs and integers - Linus is still not a fan

//alternatively
typedef struct node node_t;

//you can also typedef primitives
typedef unsigned int my_uint32;

Macros


My favorite thing about C. You can do so many things with macros (I made a generic typed minheap library using only macros - it was kind of annoying though for reasons outlined below).

The first thing about macros - they are precompiled meaning your compiler itself doesn't check their syntax (GCC has a precompiler). This is because macros are essentially just find and replace definitions. Any instance of a macro name will be replaced with the macro so you should be careful in naming your macros or using common names as variables. In general a good practice is to capitalize all of the letters in macros.

The second thing about macros - they're fast, but only if you use them correctly. Macros are a decent way to avoid function calls which have the disadvantage of creating stack frames. If you call a function a lot in a loop, maybe it's best to use a macro instead. If you have a function that's only one line or has small functionality (for example bit shifting/masking is really fast and doesn't require that much code) you might want to use a macro instead.

Macros aren't for everything though. If your macro is just calling a function, you lose the benefits of using a macro. You could also use inline functions instead of macros but I don't have much experience in using them.

Third thing about about macros - you can't debug them. Yep, if you use a debugger like GDB and run into a macro, GDB will just skip over the code in the macro. This is what made making the minheap super annoying - but it's fast so I guess there's that. There's a compiler flag you can set to prevent this but generally it isn't worth it (and your macros shouldn't be written in an error prone manner anyway).

General good practice with macros - K.I.S.S. (keep it stupid simple). Just a good thing to remember in operating systems sometimes.

//simple macros
#define N 15
#define KiB 1024 //kibibyte
#define MiB KiB*1024 //Mibibyte

//functional macros
#define SUM(A, B) A+B
#define TOGGLE(X, SHIFT) X ^= 0x1 << SHIFT

//multiline functional macros
#define DEBUG(X, ERR)   \
do{                     \
    if(ERR){            \
        printf(X, ERR); \
    }else{              \
        printf("PASS"); \
    }                   \
}while(0);

The multiline macro might need some explaining. First, you don't need to line up the \, it just looks neater. Second, the do-while is generally good to include in every macro. There's a stackoverflow answer on why but in short, it's so that the macro expands properly in code blocks (conditionals, loops, etc.) with or without brackets surrounding the block.

The semi-colon at the end (while(0);) isn't necessary and in the stackoverflow answer it says it defeats the purpose of the expansion, but I put it there anyway just in case I forget to put a ; after my macro calls (though in generall you shouldn't put semi-colons in macros, otherwise you can't use them as arguments for functions).

You should always surround your macro definitions with conditional checks. If you don't you will override any existing macros with the same name.

#ifndef
#define MACRO1 something
#define MACRO2 something2
...
#endif

In your makefile you can also define which macro definitions to push to your code. This is useful for debugging - sometimes you don't want debug messages to show up and other times you do. You'll see an example of this in some of the project makefiles where we push certain macros to enable/disable certain blocks of code.

Header Files


The all important header file. This can get its own document with how much you can do with it, but we'll simplify it here.

First, if you have any functions that you want to make globally available to any source file that includes your header file, put your function prototypes in that header file - you don't have to put the prototypes in the .c file. The same goes with macros, structs, typedefs, or any other packages you want to include.

It's best to keep common packages out of the header file. The precompiler just takes you header file and basically copy pastes it to the top of your source file (not exactly but close enough for what we need it for) so any packages included in your header, will be included in every file that includes your header file. Try to be wary of this so you don't repeat common definitions, includes, or end up with circular dependencies.

Second, always surround the header with conditional checks. Having multiple conditional checks might be better but you won't have to worry about that in this class. You'll see them in the example.

Header File (test.h)

#ifndef _TEST_H //this is just checking on a variable name _TEST_H. Is used to avoid getting included again.
//ifndef -> if not defined, ifdef -> if defined 

/*include any necessary packages for your .c file that other people might also want to use (but not explicitly include).
Generally speaking, you don't need to include the packages that are in this example in the header file - let the user
decide that - but you might want to include header files that might contain some definitions needed to call the functions
in this header file (macros or typedefs from other header files for example). This decision is a little bit of a
headache and a lot of the time you'll see common packages included in header files anyway.*/
#include<pthread.h>

#define MACRO1 ...
...

typedef struct X{
    int member1;
    char* member2;
}Y_t;

void function1(int, int);
int function2(char*, void*);
#endif

Your C library file (test.c)

#include<stdio.h>
#include<stdlib.h>
#include "path/to/test.h" //make sure to include the headerfile in your library file

void function1(int a, int b){ //do something 
}
int function2(char* a, void* b){ //return something 
}

The C file using your library file (a.c)

#include<stdio.h>
#include<stdlib.h>
#include "path/to/test.h"

int main(...){
    int x = MACRO1;
    Y_t* z = (Y_t*) malloc(sizeof(Y_t)); //poor naming choice, I know
    function1(1, 2);
    return 0;
}