C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
69: Function-Like Macros
A lot of people coming into C think that function-like macros are just a shorthand for writing small, inline functions. They see #define MAX(a,b) ((a) > (b) ? (a) : (b)) and assume the compiler is treating it as a tiny function call that happens to be fast. It looks like a function, it takes arguments, and it returns a value. But here is the thing: the compiler never even sees your macro.
The "Macros are just fast functions" fallacy
The preprocessor is essentially a sophisticated "find and replace" tool. It doesn't understand C types, scope, or logic; it just shuffles text around before the actual compilation begins. If you treat a macro like a function, you're going to run into some truly bizarre bugs that are a nightmare to debug because the code you're looking at in your editor isn't the code the compiler is actually processing.
Take this seemingly innocent macro for squaring a number:
#define SQUARE(x) x * x
int main() {
int result = SQUARE(1 + 1);
// You expect 4, but you get 3.
}
Why 3? Because the preprocessor does a literal substitution. The line becomes int result = 1 + 1 * 1 + 1;. Following the standard order of operations, multiplication happens first: 1 + (1 * 1) + 1, which equals 3. I've spent more time than I'd like to admit chasing bugs exactly like this because I forgot that macros are just text manipulation.
Parenthesize everything to survive the preprocessor
To make a macro behave predictably, you have to be paranoid. You must wrap every single argument in parentheses, and then wrap the entire expression in another set of parentheses. This forces the order of operations to remain what you intended, regardless of what the caller passes in.
Here is the professional way to write that same macro:
#define SQUARE(x) ((x) * (x))
Now, SQUARE(1 + 1) expands to ((1 + 1) * (1 + 1)), which correctly evaluates to 4. It looks ugly and cluttered, but in the world of macros, "ugly" is usually synonymous with "safe."
The double-evaluation trap
Even with parentheses, macros have a dangerous quirk that actual functions don't: they evaluate their arguments every time they appear in the macro body. If you pass an expression with a side effect—like an increment operator—you're in for a surprise.
Consider this scenario:
#define SQUARE(x) ((x) * (x))
int a = 5;
int result = SQUARE(a++);
// You might expect result to be 25 and a to be 6.
// In reality, result is 30 (5 * 6) and a is 7.
Because a++ is substituted twice, the code becomes ((a++) * (a++)). The variable is incremented twice. This is one of the primary reasons why, in modern C, I generally prefer static inline functions over macros whenever possible. Inline functions provide the same performance benefit but obey the rules of the C language, including type checking and single-evaluation of arguments.
Use macros when you absolutely have to—like when you need to use stringification (#) or token pasting (##), or when you're doing something that must happen before the compiler starts its work. Otherwise, stick to functions.
📋 Practical Task
Fixing the Volatile Buffer Boundary Macro
You've inherited a legacy driver for a hardware buffer. The previous engineer wrote a macro to calculate the offset of a memory address, but it's causing sporadic crashes because it doesn't handle operator precedence correctly and it suffers from the double-evaluation problem.
Your task:
- Rewrite the
GET_OFFSETmacro so that it is safe from operator precedence issues (e.g., it should work if the user passesbase + 1). - Modify the
mainfunction to avoid the double-evaluation bug whereptr++was being passed into the macro.
#include <stdio.h>
// BUGGY MACRO: Needs parentheses!
#define GET_OFFSET(base, index) base + (index * 4)
int main() {
int buffer[10] = {0};
int *ptr = buffer;
// This currently expands to: ptr++ + (2 * 4)
// It increments ptr twice or behaves unpredictably.
int offset = GET_OFFSET(ptr++, 2);
printf("Offset: %d\n", offset);
return 0;
}There are no comments for now.