Skip to content
Home » Embedded Systems » Embedded C » #define Macros in C: Constants, Function Macros, and Best Practices

#define Macros in C: Constants, Function Macros, and Best Practices

Circuit board with C Programming hexagon, illustrating #define macro concepts in C programming for embedded systems developm…
Embedded Systems Learning Path
Part 32 of 129View Full Path →

KEY TAKEAWAYS

  • Macros are text replacements handled by the preprocessor before compilation
  • Object-like macros define constants; function-like macros can accept parameters
  • Always wrap macro parameters and the full expression in parentheses to avoid operator precedence bugs
  • Macros have no type checking — prefer const variables or inline functions when possible

#define in C is a preprocessor directive used to create constants and function-like macros that expand during compilation.

The #define directive in C is one of the most powerful preprocessor features for creating constants and inline functions.

The #define directive in C is a fundamental preprocessor feature that enables developers to create constants, function-like macros, and powerful code generation patterns.

Macros are a type of pre-processor. Pre-processors begin with the “#” symbol and are also called  directives. They are of great convenience to the programmers and are used frequently in most of the C programs. They can be placed anywhere in the program. But they are usually placed at the beginning of the program before any variable/function declarations.

The first step of a C compilation process is the pre-processing. You can read about the C Compilation Process here. The output of the pre-processing stage is a “.i” file.

Preprocessing does the following things:

  1. Evaluate all Preprocessor Directives.
  2. Remove the Comments

Types of pre-processor directives

There are many types of preprocessors in C.

  1. Macros
  2. File Inclusions
  3. Conditional Compilation
  4. Other directives

The following video gives a good explaination of preprocessors and the different types. In this post, we will cover Macros.

Macro Expansion

Macros are also called #define in C. That is because, to define a macro, we use the statement “#define

Let us look at macro expansion through an example. Consider the following example. Assume that a class has 30 students. And the program is written to calculate the average mark obtained by each student.

[c]int main ()

{
int avg = 0;
int sub1_arr[30] = {24,54,74,14,….}; // upto 30 students
int sub2_arr[30] = {43,44,32,58……}; // upto 30 students

for (int i= 0;i < 30;i++)
{
avg = (sub1_arr[i] + sub2_arr[i])/2;
printf (“Student %d has an average of %dn”,i+1,avg);
}
return 0;
}[/c]

Using Macros in C

The above code prints the average of all the 30 students. As the number of students in the class can vary by time, assume there are ‘x’ new students joining the class, every month. In such situations,

  • we will have to manually replace the value ’30’ everywhere in the program. Missing even one place will result in misbehavior.
  • Moreover, the number ’30’ is not intuitive. Imagine a larse code where such numbers appear frequently. It will be a nightmare for the people making changes to the code or maintaining it.

And, wouldn’t it be good if we could give a name to this number? Then, then name becomes more intuitive and is easily understood.

This is where the Macros come in. They let us define names and give values to those names. Now, we can use these names in the program instead of the values. Let’s look at an example that makes use of a macro and then see the benefits of it.

[c] #define NO_OF_STUDENTS 40

int main()
{
int avg = 0;
int sub1_arr[NO_OF_STUDENTS] = {24,54,74,14,….}; // upto 40 students
int sub2_arr[NO_OF_STUDENTS] = {43,44,32,58……}; // upto 40 students

for (int i= 0;i < NO_OF_STUDENTS;i++)
{
avg = (sub1_arr[i] + sub2_arr[i])/2;
printf (“Student %d has an average of %dn”,i+1,avg);
}
return 0;
}[/c]

In the above code, what we have is a name for the value, for the number of students. Now, anytime the number of students change, we just have to make the change at one place and it will be reflected everywhere. Thus, we avoid the problem of missing out update at some places and the code also becomes to maintain and work upon.

Such statements are often called as macro definitions or simply macros. During pre-processing, the program replaces all NO_OF_STUDENTS by the number defined with it.

The following video will help you understand Macros with the help of its use in a program.

In the following video, you can see the preprocessed file:

Let us consider another example.

[c]
#define PI 3.141

int main()
{
float radius = 3.3; float area = 0;
area = PI*radius*radius;
printf(“The area of circle is %fn”, area);
return 0;
}[/c]

In the above program, since the value of PI is universally same, it can be defined with the help of pre-processor.

The program replaces the instances of the word PI in program by the number 3.141. Today you may limit the value of pi to 3.141, but might want to change it tomorrow to 3.141592653589 for more accuracy. Macros make it easy to replace the required value. Whenever the program sees a #define directive, it searches for the macro template over the entire program, and replaces with the physical value there. Generally for macros, use of Capital Letters makes programmers easy to pick while going through the program.

Macros with Arguments in C

We can also pass arguments to macros. Consider the example below:

[c]
#define AREA_OF_CIRCLE(r) (3.141*r*r)

main()
{
float ra = 5.7;
printf(“The Area of circle with radius %f is %fn”, r, AREA_OF_CIRCLE(ra));
}[/c]

In the above program, the preprocessor finds the instances of AREA_OF_CIRCLE(r) and expands it by its formula (3.141*r*r). This makes some simple calculations to be expressed in one single line. The benefit of doing this is that if, for some reason, you want to update the formula for the calculation, you can just change it in the line where you defined it.

Note:

    1. Do not leave a blank space while defining a macro and its argument. Eg: AREA_OF_CIRCLE (r) (3.14*r*r) – This will result in a wrong interpretation. No space in between AREA_OF_CIRCLE and (r).
    2. Enclose the Macro expansions within brackets.

[c]#define SQUARE(n) n*n

main()
{
int i;
i = 64/SQUARE(4);
printf(“i=%d”,i);
}[/c]

The output expected from this program is i = 4, but the output would be i = 64.

The pre-processed output of the expansion is as follows:

[c]
main()
{
int i;
i = 64/4*4;
printf(“i=%d”,i);
}[/c]

The statement i = 64/4*4; is evaluated as

64/4*4 = 16*4 = 64

Always enclose the expansion is brackets, as #define SQUARE(n) (n*n)

The following video will help you understand better. It shows the expansion in a program.

Split Macros in C into multiple line

We can split Macros in C into multiple lines if the expansion has multiple statements

[c]#define HORLINE for( i = 0 ; i < 79 ; i++ )
printf ( “%c”, 196 ) ;
main( )
{
int i;
clrscr();
HORLINE
}[/c]

This program draws a horizontal line on the screen.

Why not use a “const” Variable instead of Macros?

The reasons for this are:

  1. The simple reason for this is that #define Macros are replaced in the code during preprocessing. But const variables are part of the final compiled program.
  2. Since #define macros are replaced during run time, they do not occupy additional memory which the const variables will occupy.

Want to learn more about C and Embedded C? Follow the tutorial Series on YouTube – Master C and Embedded C Programming

Join the Course on Udemy to get a Certificate of Completion for C and Embedded C Programming.

Object-Like Macros: Constants and Configuration

Object-like macros (without parentheses) define constants and configuration values. The preprocessor performs text substitution before compilation — the compiler never sees the macro name.

// Hardware configuration constants
#define UART_BAUD_RATE    115200
#define SPI_CLOCK_HZ      1000000
#define SENSOR_COUNT      8
#define ADC_RESOLUTION    12
#define ADC_MAX_VALUE     ((1 << ADC_RESOLUTION) - 1)  // 4095

// Buffer sizes #define RX_BUFFER_SIZE 256 #define TX_BUFFER_SIZE 128 #define LOG_ENTRIES_MAX 64

// Register addresses #define GPIO_BASE_ADDR 0x40020000 #define UART1_BASE_ADDR 0x40011000

// Feature flags #define ENABLE_LOGGING 1 #define ENABLE_WATCHDOG 1 #define DEBUG_MODE 0 “`

Best practices for object-like macros:

  • Use ALL_CAPS naming convention to distinguish macros from variables
  • Parenthesize expressions: #define AREA (WIDTH * HEIGHT) not #define AREA WIDTH * HEIGHT
  • Use const variables instead of macros when type safety matters (C99+)
  • Group related macros with a common prefix (e.g., UART_, SPI_, ADC_)

Function-Like Macros: Inline Code Generation

Function-like macros generate inline code at the call site. They’re faster than function calls (no overhead) but have pitfalls that you must understand.

// Simple function-like macros
#define MAX(a, b)      ((a) > (b) ? (a) : (b))
#define MIN(a, b)      ((a) < (b) ? (a) : (b))
#define ABS(x)         ((x) < 0 ? -(x) : (x))
#define CLAMP(x, lo, hi)  (MIN(MAX(x, lo), hi))

// Bit manipulation macros — extremely common in embedded C #define BIT(n) (1U << (n)) #define SET_BIT(reg, n) ((reg) |= BIT(n)) #define CLEAR_BIT(reg, n) ((reg) &= ~BIT(n)) #define TOGGLE_BIT(reg, n) ((reg) ^= BIT(n)) #define CHECK_BIT(reg, n) (((reg) & BIT(n)) != 0)

// Array utilities #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))

// Conversion macros #define MS_TO_TICKS(ms) ((ms) * TICK_RATE_HZ / 1000) #define ADC_TO_VOLTAGE(adc) ((adc) * 3.3f / 4095.0f) “`

The critical rules for function-like macros:

1. Parenthesize every parameter — prevents operator precedence bugs:

// BAD: no parentheses around parameters
#define SQUARE(x) x * x
int result = SQUARE(2 + 3);  // Expands to: 2 + 3 * 2 + 3 = 11, not 25!

// GOOD: fully parenthesized #define SQUARE(x) ((x) * (x)) int result = SQUARE(2 + 3); // Expands to: ((2 + 3) * (2 + 3)) = 25 “`

2. Beware of double evaluation — macro arguments are evaluated each time they appear:

#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = MAX(i++, j++);  // i or j gets incremented TWICE!

// Solution: use a statement expression (GCC extension) #define MAX_SAFE(a, b) ({ __typeof__(a) _a = (a); __typeof__(b) _b = (b); _a > _b ? _a : _b; }) “`

Multi-Line Macros and do-while(0) Pattern

Complex macros that contain multiple statements must use the do { ... } while(0) idiom to work correctly in all contexts:

// WRONG: breaks when used in an if-else
#define LOG_ERROR(msg) printf("ERROR: "); printf(msg); printf("n");

if (error) LOG_ERROR(“something failed”); // Only first printf is in the if block! else do_something(); // Compiler error: else without matching if

// CORRECT: do-while(0) wraps everything in a single statement #define LOG_ERROR(msg) do { printf(“ERROR: “); printf(msg); printf(“n”); } while(0)

if (error) LOG_ERROR(“something failed”); // Works correctly — entire macro is one statement else do_something(); “`

The do-while(0) pattern is standard practice in Linux kernel, FreeRTOS, and all major embedded codebases. It ensures the macro behaves syntactically like a single statement, including requiring a trailing semicolon.

Another multi-line pattern using comma operator (for expression macros):

// Using comma operator — returns the last expression's value
#define INIT_SENSOR(id) (sensor_reset(id), sensor_configure(id), sensor_enable(id))

// Can be used in expressions: if (INIT_SENSOR(0)) { printf(“Sensor 0 initializedn”); } “`

Stringification and Token Pasting

The preprocessor provides two special operators for macro arguments:

# (stringification) — converts a macro argument to a string literal:

#define STRINGIFY(x) #x
#define TO_STRING(x) STRINGIFY(x)  // Two-level for macro expansion

printf(STRINGIFY(hello)); // Prints: hello printf(TO_STRING(__LINE__)); // Prints: “42” (the actual line number)

// Useful for debug logging #define ASSERT(expr) do { if (!(expr)) { printf(“Assertion failed: %s at %s:%dn”, #expr, __FILE__, __LINE__); while(1); } } while(0)

ASSERT(x > 0); // If fails, prints: “Assertion failed: x > 0 at main.c:42” “`

## (token pasting) — concatenates two tokens into one:

#define GPIO_PIN(port, pin) GPIO##port->ODR |= (1 << pin)
GPIO_PIN(A, 5);  // Expands to: GPIOA->ODR |= (1 << 5)

// Create register access functions for multiple peripherals #define UART_INIT(n) void uart##n##_init(void) { UART##n->BRR = SYSTEM_CLOCK / BAUD_RATE; UART##n->CR1 |= USART_CR1_UE | USART_CR1_TE | USART_CR1_RE; }

UART_INIT(1) // Generates uart1_init() for UART1 UART_INIT(2) // Generates uart2_init() for UART2 UART_INIT(3) // Generates uart3_init() for UART3 “`

This technique is heavily used in STM32 HAL, FreeRTOS port layers, and driver frameworks to generate repetitive code for multiple peripheral instances.

Variadic Macros and Debug Logging

C99 introduced variadic macros with __VA_ARGS__, enabling printf-style debug macros:

// Basic debug print macro
#define DEBUG_PRINT(fmt, ...) 
    printf("[DEBUG %s:%d] " fmt "n", __FILE__, __LINE__, ##__VA_ARGS__)

// Level-based logging #define LOG_LEVEL_ERROR 0 #define LOG_LEVEL_WARN 1 #define LOG_LEVEL_INFO 2 #define LOG_LEVEL_DEBUG 3

#define CURRENT_LOG_LEVEL LOG_LEVEL_INFO

#define LOG(level, fmt, …) do { if (level <= CURRENT_LOG_LEVEL) { const char *names[] = {“ERROR”, “WARN”, “INFO”, “DEBUG”}; printf(“[%s] ” fmt “n”, names[level], ##__VA_ARGS__); } } while(0)

#define LOG_ERROR(fmt, …) LOG(LOG_LEVEL_ERROR, fmt, ##__VA_ARGS__) #define LOG_WARN(fmt, …) LOG(LOG_LEVEL_WARN, fmt, ##__VA_ARGS__) #define LOG_INFO(fmt, …) LOG(LOG_LEVEL_INFO, fmt, ##__VA_ARGS__) #define LOG_DEBUG(fmt, …) LOG(LOG_LEVEL_DEBUG, fmt, ##__VA_ARGS__)

// Usage LOG_ERROR(“Sensor %d failed with code 0x%02X”, sensor_id, error_code); LOG_INFO(“System started, %d sensors active”, active_count); “`

The ##__VA_ARGS__ (GCC extension) removes the trailing comma when no variadic arguments are provided. Without ##, calling LOG_ERROR("simple message") would produce a syntax error due to the dangling comma.

To completely remove debug logging from release builds (zero code size and runtime overhead):

#ifdef DEBUG
    #define DBG(fmt, ...) printf("[DBG] " fmt "n", ##__VA_ARGS__)
#else
    #define DBG(fmt, ...) ((void)0)  // Compiles to nothing
#endif

📖 Related: Inline Functions and Macros in Embedded C: When to Use EachHow to Modularize C Programs into Multiple Files

Related on this site

Leave a Reply

Your email address will not be published. Required fields are marked *