C Preprocessor Directives, Macros & Macro Functions Tutorial

In this C tutorial we learn how to tell the compiler to apply transformations in our code with preprocessor directives and macros.

We cover how to create macro functions as well as the #include, #define, #undef and #error directives.

We also cover the #ifdef, ifndef, #if, #elif, #else and #endif conditional directives.

What is a preprocessor directive

A preprocessor directive allows us to transform the code in our program. It tells the compiler that it must pre-process these transformations before compiling the program.

When we compile the program, the preprocessor will process the directives that are associated with it, and then compile the program.

The C preprocessor is not a part of the compiler, and it uses a different syntax from normal statements.

C gives us access to the following preprocessor directives:

  • #include
  • #define
  • #undef
  • #ifdef
  • #ifndef
  • #if
  • #else
  • #elif
  • #endif
  • #error
  • #pragma

In this tutorial, we will explore the preprocessor directives above.

How to include files with the #include directive

We can import the code from other source code files (like .h files) into our document.

Throughout the course tutorials, we’ve included several files we needed at that time. These includes essentially copy all the code from that file, into our current document, making the code available to us to use.

Before we proceed, let’s create another file to include:

  1. In Codeblocks, go to File > New > Empty file.
  2. A dialog box will popup asking you to include the file in the project by saving it, choose Yes.
  3. Save the file as “myfile.h” and choose Save.
  4. In the next dialog box, choose OK.

This should have created a separate folder called “Headers” in your project. The myfile.h will be stored in there.

In myfile.h, initialize a variable called message, with some text.

Example: myfile.h
 char message[] = "Hello from another file\n";

Now we will use the #include preprocessor directive to include the myfile.h into our main.c file.

To include a file, we write an octothorp symbol ( # ), immediately followed by the keyword include . Then, we specify the file we want to include between a set of double quotes.

Syntax:
 #include "filename"

Note that we do not use angle brackets like we do normally when we import libraries. Angle brackets are only used for libraries.

Example: main.c
#include <stdio.h>
#include "myfile.h"

int main()
{
	return 0;
}

In the example above, we use the #include directive with double quotes to include myfile.h in our document.

By including the file, we make everything in that file available for us to use. As a demonstration, let’s print the message that we initialized in myfile.h.

We don’t have to do anything special, we can just use the variable as we normally would.

Example:
#include <stdio.h>
#include "myfile.h"

int main()
{
	printf(message);

	return 0;
}

When we compile and run the example above, the message prints to the console.

We can use the #include directive to include functions, structs, etc. Any file that we include does not need its own main() function.

How to define a macro with #define

A macro is, essentially, a preprocessor constant. It will find and replace any instance of the macro name, with code that we specify.

To create a macro, we use the #define preprocessor directive, with a name and a body.

Syntax:
 #define MACRO_NAME macroBody
Example:
#include <stdio.h>

// macro definitions
#define MESSAGE "Hello World"
#define TRUE    1
#define FALSE   0
#define SUM     (3 + 5)

int main()
{
    printf("String: %s\n", MESSAGE);
    printf("Custom boolean TRUE: %d\n", TRUE);
    printf("Custom boolean FALSE: %d\n", FALSE);
    printf("Arithmetic: 3+5=%d\n", SUM);

    return 0;
}

In the example above, any instance of the words MESSAGE, TRUE, FALSE and SUM will be replaced by whatever is in their body.

How to undefine a macro with #undef

We can remove, or redefine, a macro that we set up previously with the #undef directive.

Macro definition is typically done at the top of the document, but macro undefining and redefining is done inside the rest of the document.

Syntax:
 #undef MACRO_NAME
Example:
#include <stdio.h>

#define MESSAGE "Hello World"

int main()
{
    printf("String: %s\n", MESSAGE);

    // remove macro
    #undef MESSAGE

    // redefine macro
    #define MESSAGE "Hello there"

    printf("String: %s\n", MESSAGE);

    return 0;
}

In the example above, we change the words in the MESSAGE macro by undefining and then redefining it.

How to create and use parameterized macros (function macros)

C allows us to rewrite small functions by using macros to save execution time.

At every function call, the program’s execution will move between the function definition and the call, which consumes time. We use parameterized macros to save some of this time.

Syntax:
 #define(parameters) (logic)

Function macros only contain simple logic. Let’s compare a regular function with a function macro.

Example:
#define(a, b) (a + b)

// is the same as

int sum(int a, int b)
{
	return a + b;
}

The function can be a little more complex than the example above. It’s also a perfect situation to make use of the ternary operator to condense our code.

Example:
#include <stdio.h>

#define SUM(a, b) (a + b)
#define MIN(a, b) (a < b ? a : b)

int main()
{
    printf("3+5 = %d\n", SUM(3, 5));
    printf("The smallest value between 5 and 3 is: %d\n", MIN(3, 5));

    return 0;
}

In the example above, we use function macros to calculate the sum of two specified values. We also calculate which of two specified numbers is the smallest by using the ternary operator.

How to perform macro evaluations with #if, #elif, #else and #endif

C conditional preprocessor directives work similar to regular condition control statements . These conditional directives will compile multiple code blocks.

An important difference between regular conditional statements and conditional directives, is that any #if directive must be terminated with the #endif directive.

Syntax:
#if condition

// code to execute if
// condition is true

#endif

Another important difference, is that a regular else if statement is written as #elif for the conditional directive.

Syntax:
#if condition

// code

#elif condition

// code

#else

// code

#endif
Example:
#include <stdio.h>

#define APPLIED_PHYSICS 1
#define COMPUTER_SCIENCE 2
#define ECONOMICS 3
#define MATHEMATICS 4

#define MAJOR 2

int main()
{
    printf("Major: ");

    #if MAJOR == 1
        printf("Applied Physics\n");
    #elif MAJOR == 2
        printf("Computer Science\n");
    #elif MAJOR == 3
        printf("Economics\n");
    #elif MAJOR == 4
        printf("Mathematics\n");
    #else
        printf("Please choose a valid major\n");
    #endif // MAJOR

    return 0;
}

In the example above, we use conditional directives with macros to preprocess statements for our application.

How to test definitions with #ifdef and #ifndef

We can evaluate if a macro has been defined by using the conditional directives #ifdef and #ifndef .

So what is the difference between #ifdef vs #ifndef:

  • #ifdef will evaluate if a macro has been defined.
  • #ifndef will evaluate if a macro has not been defined.

Let’s look at an example.

Example:
#include <stdio.h>

#define MESSAGE "Hello there "

int main()
{
    #ifdef MESSAGE
        // message exists
        // print it
        printf("%s\n", MESSAGE);
    #endif

    #ifndef USER
        // user does not exist
        // ask for valid user
        printf("**Error: Please assign a valid user**\n");
    #endif

    return 0;
}

How to raise a compile time error with #error

We can generate a compile time error with the #error directive. It will display the error that we specify after the #error keyword and stop the compilation process.

Syntax:
 #error Error message to display

The #error directive is typically used with a conditional directive.

Example:
#include <stdio.h>

//#define MESSAGE "Hello World"

#ifndef MESSAGE
#error Please define MESSAGE before trying to access it
#endif

int main()
{
    printf("%s\n", MESSAGE);

    return 0;
}

In the example above, we check if the MESSAGE macro has not been defined. If it isn’t defined, raise an error message with #error and stop the compilation.

When we run the example, it will raise the error and stop the compilation because the MESSAGE macro definition is commented out.

If we remove the comment and allow MESSAGE to be defined, the program will not raise an error.

Example:
#include <stdio.h>

#define MESSAGE "Hello World"

#ifndef MESSAGE
#error Please define MESSAGE before trying to access it
#endif

int main()
{
    printf("%s\n", MESSAGE);

    return 0;
}

In the example above, the MESSAGE macro is now defined, so the #ifndef evaluates to false and the #error is never raised.

Further reading

For more information on the subject of preprocessors and macros, please see the links below.

Summary: Points to remember

  • A preprocessor directive tells the compiler to execute code before the compilation starts.
  • A macro is like a constant. The compiler will find any instance of the macro name and replace it with the macro body.
  • The #include directive imports other .c and .h files into our project, making their code available to us to use.
  • To create or recreate a standard or a function macro, we use the #define directive.
  • To undefine an already defined macro, we use the #undef directive.
  • If we want to conditionally perform preprocessing actions, we use the #if, #elif, #else and #endif conditionals.
  • If we want to check if a macro has been defined, we use the #ifdef conditional.
  • If we want to check if a macro has not been defined, we use the #ifndef conditionl.
    • All conditional directives are terminated with the #endif directive.
  • If we need to generate a compile time error and stop the compilation process, we use the #error directive.
  • For most traditional use cases of macros, there are better ways nowadays. If we don’t need to use a macro, we shouldn’t.