Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 36

Debug Macro: Print Variable Name and Value

Recommended resources

Introduction

In our last lesson, we built a flexible debugging system using conditional compilation. By defining macros like LOG_INT("Damage", damage), we could toggle logging on or off with a simple compiler flag. This was a significant step forward, but it still involves some repetitive work—we had to manually type the name of the variable as a string each time. What if a macro could be smart enough to figure out the variable's name on its own?

This lesson will show you how to create just that: a powerful debug macro that automatically prints a variable's name and its value, along with the precise location in the code where it was called. To achieve this, we will explore some of the C preprocessor's more advanced capabilities, including the "stringification" operator and a set of useful predefined macros. This will complete our tour of the preprocessor for now, leaving you with a professional-grade tool for tracing and debugging your game logic.


From Argument to String: The Stringification Operator

The C preprocessor has a special operator, the hash symbol (#), which, when used inside a macro definition, converts the token that follows it into a string literal. This is often called "stringification" or the "stringizing operator." It's the key to making our macro automatically print a variable's name.

For instance, if we have a macro PRINT_VAR(x), the preprocessor can take the x, and if we write #x in the macro's body, it will be replaced not by the value of the variable passed, but by the literal string "x".

Let's watch a short video that demonstrates this concept clearly.

Macros with parameters in C

This clip from the CodeVault channel shows exactly how to use the # operator to take a macro parameter and use it as a string.

Watch from this segment on printing the variable name. The key moment is when the code is changed from printing a static string like "value is" to printing the name of the variable itself by using the # operator on the macro's parameter. Notice how the preprocessor expansion effectively becomes printf("a is ...", ...) when the macro is called with the variable a.

This technique is incredibly powerful. As you saw, a macro defined as #define PRINT_INT_VAL(x) printf("%s = %d\n", #x, x) allows a call like PRINT_INT_VAL(player_health); to be expanded by the preprocessor into printf("%s = %d\n", "player_health", player_health);. The compiler then executes this printf call, printing the variable's name and its current value.


Adding Location, Location, Location: Predefined Macros

Knowing a variable's value is good, but in a large project with hundreds of files, knowing where that value was logged is critical. The C standard provides several built-in macros that give us this contextual information. The most useful for debugging are:

  • __FILE__: Expands to a string literal containing the name of the current source file.
  • __LINE__: Expands to an integer constant representing the current line number in the source file.
  • __func__: (Available in C99 and later) Expands to a string literal containing the name of the current function.

To see these in action, let's turn to another quick video tutorial.

Predefined Preprocessor Macros + Log File Use Case | C Programming Tutorial

This video from Portfolio Courses introduces the standard predefined macros and immediately connects them to their primary use case: logging.

Please watch the following two parts: Introduction to Macros: Watch from this section, which introduces __FILE__, __DATE__, __TIME__, __LINE__, and __STDC__. Pay special attention to __FILE__ and __LINE__. Logging Use Case: Continue through the brief explanation from this segment on how these macros are used to implement logging in larger C programs. This reinforces why we are learning this.

For a quick reference, the excellent Beej's Guide to C provides a clear table of these mandatory macros.

19 The C Preprocessor - Beej's Guide to C Programming

This resource provides a concise, text-based summary of the predefined macros.

Read the section titled "19.4.1 Mandatory Macros". You can find it by searching for that title. It begins with the text "These are all defined" and includes a table summarizing __FILE__, __LINE__, __func__, and others. This serves as a great cheat sheet.


Building a Robust, Multi-Statement Macro

Now we have all the ingredients to build our ultimate debug macro: a way to print a variable's name, its value, and the location of the call. This will require our macro to expand into several statements, so we'll need to define it over multiple lines. As you might have seen in some resources, this is done by placing a backslash (\) at the end of each line to "escape" the newline character.

However, creating multi-statement macros introduces a subtle but dangerous pitfall, especially when used with if-else statements. This is why we use the do { ... } while(0) idiom, which the previous lesson hinted at. This construct wraps our macro's code in a block that is executed exactly once, and it correctly "absorbs" the semicolon that follows the macro call, making it behave exactly like a single, safe function call.

A detailed explanation on a Stack Overflow thread is one of the best available.

c preprocessor - #define macro for debug printing in C?

This section of a highly-rated answer explains precisely why the do-while(0) trick is essential for robust macros.

Please read the section titled "Why the do-while loop?" It starts with the text "You want to be able to use the macro". It demonstrates with clear code examples how a seemingly correct macro can break an if-else structure and how do { ... } while(0) solves the problem elegantly.

With this final piece of the puzzle, we can construct our macro. We will define it conditionally using #ifdef DEBUG, wrap it in do { ... } while(0), and use __FILE__, __LINE__, __func__, and the stringification operator (#) to produce a rich, informative debug message. A common practice for such diagnostic messages is to print them to the "standard error" stream (stderr) instead of "standard output" (stdout), so that debug information can be separated from the program's normal output.




#ifdef DEBUG
    #define DUMP_INT(var) \
        do { \
            fprintf(stderr, "[DEBUG] %s:%d:%s(): %s = %d\n", \
                    __FILE__, __LINE__, __func__, #var, var); \
        } while (0)



#else
    #define DUMP_INT(var) do {} while (0) // Expands to nothing in release builds
#endif

This is a professional-grade debug macro. It's safe, informative, and has zero performance cost in release builds.


Refactoring the Game with Our New Macro

Let's integrate this powerful new tool into our combat game. We'll replace the old LOG_INT call with our new DUMP_INT macro. Notice how much cleaner the call becomes—we no longer need to pass the variable's name as a string.

Below is the refactored code.




#include <stdio.h>
#include <string.h>
#include <stdbool.h>

// --- Game Parameters ---



#define MAX_NAME_LENGTH 50
#define PLAYER_INITIAL_HEALTH 100
#define PLAYER_ATTACK 15
#define PLAYER_DEFENSE 5
#define ENEMY_INITIAL_HEALTH 50
#define ENEMY_ATTACK 10
#define ENEMY_DEFENSE 2

// --- Advanced Debugging Macro ---



#ifdef DEBUG
    // In debug mode, this macro prints variable name, value, file, line, and function.
    // It prints to stderr to separate debug output from program output.



    #define DUMP_INT(var) \
        do { \
            fprintf(stderr, "[DEBUG] %s:%d:%s(): %s = %d\n", \
                    __FILE__, __LINE__, __func__, #var, var); \
        } while (0)



#else
    // In release mode, this macro expands to an empty, safe statement.



    #define DUMP_INT(var) do {} while (0)
#endif

// A simple logging macro for messages remains useful.



#ifdef DEBUG
    #define LOG(msg) fprintf(stderr, "[DEBUG] %s\n", msg)
#else
    #define LOG(msg) do {} while(0)
#endif


// --- Data Structures ---
typedef struct {
    char name[MAX_NAME_LENGTH];
    int health;
    int max_health;
    int attack;
    int defense;
    bool autoPilot;
} Character;

// --- Function Prototypes ---
void display_status(const Character *c);
int fight_loop(Character *player, Character *enemy);
void swap_pointers(Character **a, Character **b);

// --- Main Program ---
int main() {
    LOG("Initializing characters.");
    Character hero = {"Hero", PLAYER_INITIAL_HEALTH, PLAYER_INITIAL_HEALTH, PLAYER_ATTACK, PLAYER_DEFENSE, false};
    Character goblin = {"Goblin", ENEMY_INITIAL_HEALTH, ENEMY_INITIAL_HEALTH, ENEMY_ATTACK, ENEMY_DEFENSE, true};

    printf("A wild %s appears!\n\n", goblin.name);
    
    LOG("Starting fight loop.");
    int result = fight_loop(&hero, &goblin);

    LOG("Fight loop ended.");
    printf("\n--- Battle Over ---\n");
    if (result == 1) {
        printf("%s is victorious!\n", hero.name);
    } else {
        printf("%s has been defeated.\n", hero.name);
    }

    return 0;
}

// --- Function Definitions ---
int fight_loop(Character *player, Character *enemy) {
    Character *attacker = player;
    Character *target = enemy;

    while (player->health > 0 && enemy->health > 0) {
        LOG("--- New Turn ---");
        display_status(player);
        display_status(enemy);
        printf("\nIt is %s's turn.\n", attacker->name);

        int choice = 1;
        if (attacker->autoPilot == false) {
            printf("Choose your action:\n  1) Attack\n>> ");
            char line[10];
            fgets(line, sizeof(line), stdin);
            sscanf(line, "%d", &choice);
        }

        switch (choice) {
            case 1: {
                int damage = attacker->attack - target->defense;
                if (damage < 0) damage = 0;

                // Our new, improved macro in action!
                DUMP_INT(damage);
                
                printf("%s attacks %s for %d damage!\n", attacker->name, target->name, damage);
                target->health -= damage;
                break;
            }
            default:
                printf("%s is confused and does nothing.\n", attacker->name);
                break;
        }

        if (target->health <= 0) {
            target->health = 0;
            LOG("Target defeated.");
            break;
        }
        
        printf("\n");
        LOG("Swapping attacker and target pointers.");
        swap_pointers(&attacker, &target);
    }
    
    display_status(player);
    display_status(enemy);

    return player->health > 0;
}

void display_status(const Character *c) {
    printf("%s | Health: %d/%d\n", c->name, c->health, c->max_health);
}

void swap_pointers(Character **a, Character **b) {
    Character *temp = *a;
    *a = *b;
    *b = temp;
}

Compile and run this code with your debug flag (gcc -DDEBUG combat_game.c -o game_debug -std=c99) and you should see a detailed log message for the damage variable. Then, compile without the flag to see it disappear completely.


Conclusion

You have now constructed a truly powerful and reusable debugging tool. By combining several features of the C preprocessor, you've created a macro that provides rich, contextual information with minimal effort at the call site. This is a common pattern in large-scale C and C++ projects, and mastering it is a mark of a programmer moving from basic to intermediate-level skills.

Key Takeaways:

  • The stringification operator (#) is a preprocessor feature that turns a macro argument into a string literal. This is the key to automatically printing a variable's name.
  • Predefined macros like __FILE__, __LINE__, and __func__ provide invaluable context (file, line number, and function name) for logging and assertions.
  • The do { ... } while(0) construct is the standard, safe way to wrap multi-statement macros to ensure they behave predictably in all grammatical contexts, especially if-else blocks.
  • Printing diagnostic messages to stderr is a best practice that separates your debugging output from your program's primary output.

So far in our project, our game's inventory has been a fixed-size array. This is simple, but inflexible. What if we want an inventory that can grow or shrink as the player picks up or drops items? To do that, we need to manage memory ourselves. Our journey now takes us from the preprocessor to the core of C's power: manual memory management.

In the next lesson, we will begin Module 7 by exploring the fundamental concepts of stack and heap memory, setting the stage for learning how to allocate memory dynamically using malloc() and free().