Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
Symbolic Constants in C
Name your numbers with #define, const and enum, avoid macro traps, and configure builds with -D. Part of our free embedded systems course.
Imagine a program where the number 64 appears in forty places, and one day the buffer must grow to 128. Hunting through the code is slow and error-prone. Symbolic constants in C solve this by giving a value a meaningful name that you define exactly once. This lecture of our free embedded systems course covers the three ways to do it, #define, const and enum, plus the traps that make macros a favourite interview topic.
Register maps in the free Linux device drivers course, build options in the free embedded Linux course, and the flag definitions all over the free Linux kernel development course are written with exactly these tools.
What You Will Learn
By the end of this lecture you can:
- Define and use symbolic constants with
#define,constandenum - Explain what the preprocessor really does and inspect it with
gcc -E - Avoid the missing-parentheses macro bug that returns 11 instead of 25
- Choose the right tool for each job using a clear comparison table
- Use
#ifndefand-Dto configure a build without editing code - Answer interview questions on macros versus constants
Prerequisites
Lectures 1 to 3 on integer, floating, character and string constants. You will also see the idea of “compiling in stages”; we explain it here as needed.
What Are Symbolic Constants in C?
A symbolic constant (also called a named constant) is a name that stands for a fixed value, such as MAX_STUDENTS for 5 or PI for 3.14159265. The value can be numeric, a character or a string. Using names instead of raw numbers, sometimes called removing “magic numbers”, makes code readable, because for (i = 0; i < MAX_STUDENTS; i++) explains itself, and maintainable, because changing the value in one place updates every use.
Defining Symbolic Constants in C with #define
The classic tool is the preprocessor directive #define NAME value. By convention names are upper case with underscores. There is no equals sign and no semicolon at the end. The directive is not a C statement at all; it is an instruction to a text-replacement tool that runs before the compiler sees your code.
#define MAX_STUDENTS 5
#define PI 3.14159265
#define GRADE_PASS 'P'
#define COURSE "Embedded C"
The compiler never sees the name. Macro names do not appear in your debugger either.
You can watch the preprocessor work with gcc -E. For the program in Example 2 below it prints these final lines:
int main(void)
{
printf("SQUARE_BAD(2 + 3) = %d\n", 2 + 3 * 2 + 3);
printf("SQUARE_OK(2 + 3) = %d\n", ((2 + 3) * (2 + 3)));
return 0;
}
Because it is plain text substitution, a macro has no type, no memory address and no scope; it simply exists from the line of definition until #undef or the end of the file.
Symbolic Constants in C: The Macro Parentheses Trap
Since the preprocessor pastes text, operator precedence can betray you. If you write #define OFFSET 4+2 and later use OFFSET * 2, the compiler sees 4+2*2, which is 8 and not 12. The fix is a habit: wrap expressions in parentheses, and wrap every macro parameter too.
The intended answer was 25. Parentheses around each x and around the whole body fix it.
const and enum: Typed Alternatives to #define
Modern C offers safer options. A const variable has a real type, a real address, obeys scope rules and appears in the debugger. An enum constant gives you named integers that the compiler treats as true constant expressions. C23 adds constexpr objects, which combine the benefits of both.
| Feature | #define | const object | enum constant |
|---|---|---|---|
| Has a type | No | Yes | int |
| Uses memory | No (text only) | Usually yes (may be optimised away) | No |
| Obeys scope | No, file-wide until #undef | Yes | Yes |
| Visible in debugger | No | Yes | Yes |
| Usable as array size in C | Yes | No (file scope) / makes a VLA (block scope) | Yes |
Usable in #if | Yes | No | No |
| Can hold strings or floats | Yes | Yes | No, integers only |
Rule of thumb
Use enum for related integer codes and array sizes, const for typed values like a timeout or a float gain, and #define for conditional compilation switches, header guards and values that must work inside #if.
Configuring Builds with Symbolic Constants in C
A very practical use of macros is letting the build command choose a value. The pattern is #ifndef NAME followed by a default #define, so a -D flag on the compiler line can override it. Embedded projects use this for board revision, log level, feature switches and clock speed, with no source edits.
Hands-On: Four Programs on Symbolic Constants in C
Example 1: Named constants of every kind
Goal
Define numeric, character and string symbolic constants and use them in a small calculation.
Save this as named_consts.c:
#include <stdio.h>
#define MAX_STUDENTS 5
#define PI 3.14159265
#define GRADE_PASS 'P'
#define COURSE "Embedded C"
int main(void)
{
double r = 2.0;
int total = 0;
printf("Course : %s, grade code : %c\n", COURSE, GRADE_PASS);
printf("Area of circle r=%.1f is %.4f\n", r, PI * r * r);
for (int i = 1; i <= MAX_STUDENTS; i++)
total += i;
printf("Sum of 1..%d = %d\n", MAX_STUDENTS, total);
return 0;
}
Build and run:
$ gcc -Wall -Wextra -std=c11 -o named_consts named_consts.c
$ ./named_consts
Interactive: Predict the output first, then click to check yourself.
Expected output:
Course : Embedded C, grade code : P
Area of circle r=2.0 is 12.5664
Sum of 1..5 = 15
- A single edit to
MAX_STUDENTSchanges the loop and the printed message together. - The area is 3.14159265 × 2 × 2 = 12.5664 after
%.4frounding. COURSEandGRADE_PASSare replaced by the string and character text before compilation.
Example 2: The square macro bug
Goal
Compare a careless macro with a correctly parenthesised one.
Save this as macro_trap.c:
#include <stdio.h>
#define SQUARE_BAD(x) x * x
#define SQUARE_OK(x) ((x) * (x))
int main(void)
{
printf("SQUARE_BAD(2 + 3) = %d\n", SQUARE_BAD(2 + 3));
printf("SQUARE_OK(2 + 3) = %d\n", SQUARE_OK(2 + 3));
return 0;
}
Build and run:
$ gcc -Wall -Wextra -std=c11 -o macro_trap macro_trap.c
$ ./macro_trap
Interactive: Predict the output first, then click to check yourself.
Expected output:
SQUARE_BAD(2 + 3) = 11
SQUARE_OK(2 + 3) = 25
The bad version expands to 2 + 3 * 2 + 3, giving 11. The good version expands to ((2 + 3) * (2 + 3)), giving 25. Even the fixed macro evaluates its argument twice, so SQUARE_OK(i++) would increment twice. For function-like behaviour prefer a static inline function.
Example 3: const, enum and define together
Goal
Size an array with an enum, keep a typed retry count as a const, and mix them in an expression.
Save this as three_ways.c:
#include <stdio.h>
#define TIMEOUT_MS 250
enum { BUF_SIZE = 8 };
const int retries = 3;
int main(void)
{
int buf[BUF_SIZE];
printf("buffer holds %zu ints\n", sizeof(buf) / sizeof(buf[0]));
printf("worst wait = %d ms\n", TIMEOUT_MS * retries);
return 0;
}
Build and run:
$ gcc -Wall -Wextra -std=c11 -o three_ways three_ways.c
$ ./three_ways
Interactive: Predict the output first, then click to check yourself.
Expected output:
buffer holds 8 ints
worst wait = 750 ms
BUF_SIZEis an enum constant, so it can size the array andsizeof(buf) / sizeof(buf[0])yields 8.TIMEOUT_MSis a macro;retriesis a const int with an address and a type.- 250 × 3 gives 750 milliseconds of worst-case waiting.
- Computing the element count with sizeof means you only edit one number if the buffer grows.
Example 4: Overriding a default with -D
Goal
Build the same file twice and watch the behaviour change without touching the code.
Save this as log_level.c:
#include <stdio.h>
#ifndef LOG_LEVEL
#define LOG_LEVEL 1
#endif
int main(void)
{
printf("LOG_LEVEL = %d\n", LOG_LEVEL);
#if LOG_LEVEL >= 2
printf("verbose logging is ON\n");
#endif
return 0;
}
Build and run:
$ gcc -Wall -std=c11 -o log_level log_level.c
$ ./log_level
$ gcc -Wall -std=c11 -DLOG_LEVEL=3 -o log_level log_level.c
$ ./log_level
Interactive: Predict the output first, then click to check yourself.
Expected output:
$ ./log_level
LOG_LEVEL = 1
$ ./log_level
LOG_LEVEL = 3
verbose logging is ON
The first build uses the default LOG_LEVEL of 1, so the verbose line is compiled out entirely: it is not just skipped at run time, it does not exist in the binary. The second build passes -DLOG_LEVEL=3, the #if test succeeds and the extra message appears. This is how production firmware ships with logging off and debug builds turn it on.
Common Mistakes and Troubleshooting
| Mistake | What happens | Fix |
|---|---|---|
#define MAX 100; | The semicolon becomes part of the replacement text and breaks expressions like arr[MAX] | No semicolon after a #define |
#define MAX = 100 | Replacement text is = 100, causing odd syntax errors | No equals sign |
| Unparenthesised expression or parameter | Wrong precedence, as in 11 instead of 25 | Parenthesise the body and every parameter |
| Side effects in macro arguments | The argument is evaluated more than once | Use a static inline function |
Using const int n as a file-scope array size | Compile error in C, because it is not a constant expression | Use enum or #define |
| Lower-case macro names | Collide with variables and confuse readers | Use UPPER_CASE for macros |
Best Practices, Performance and Security
- One definition, one place. Put project constants in a header so every file sees the same value.
- Prefix names with the module, such as
UART_BAUD_RATE, to avoid clashes in big projects. - Prefer typed options (
enum,const,static inline) when you do not need preprocessor features; you get type checking and debugger visibility. - Performance: macros and enums cost no run-time memory or lookups. A const object may occupy flash, but optimising compilers usually fold its value into the code.
- Security: unsafe macros can hide double evaluation and integer overflow. Keep size-related constants unsigned and wrap them fully in parentheses, for example
(64u * 1024u).
Quiz: Symbolic Constants in C
Try each question on paper first. Click a question to reveal the answer.
Q1. Is there a semicolon at the end of #define LIMIT 10?
Answer: No. It is a preprocessor directive, not a C statement, and a semicolon would become part of the replacement.
Q2. What does SQUARE_BAD(2 + 3) return with #define SQUARE_BAD(x) x * x?
Answer: 11, since it expands to 2 + 3 * 2 + 3.
Q3. With #define OFFSET 4+2, what is OFFSET * 2?
Answer: 8, because it expands to 4+2*2. Write (4+2) to get 12.
Q4. Which of #define, const and enum has no type of its own?
Answer: #define. It is pure text replacement.
Q5. Can const int n = 10; at file scope be used to size a global array in C?
Answer: No. In C a const object is not a constant expression, so use an enum or a macro.
Q6. What command shows you the output of the preprocessor?
Answer: gcc -E file.c (add -P to suppress line markers).
Q7. How do you set LOG_LEVEL to 3 without editing the source?
Answer: Compile with -DLOG_LEVEL=3, provided the code uses #ifndef LOG_LEVEL for its default.
Q8. Why are macro names written in upper case?
Answer: It is a convention that warns the reader the identifier is a macro, with textual substitution and no scope.
Interview Questions: Symbolic Constants in C
These come up in embedded and product-company interviews. Answer out loud, then click.
Q1. What is the difference between #define and const?
Answer: #define is text substitution with no type, no address and no scope. const creates a typed object that obeys scope and shows up in debuggers, but in C it cannot be used where a constant expression is required.
Q2. Why must macro parameters be parenthesised?
Answer: Because the argument text is pasted unchanged, so operators in it can combine with operators in the macro body in unintended ways, such as 2 + 3 * 2 + 3.
Q3. What is the danger of #define MAX(a,b) ((a)>(b)?(a):(b))?
Answer: Each argument is evaluated twice, so MAX(i++, j) can increment i twice. A static inline function evaluates once.
Q4. How do embedded teams switch features without code edits?
Answer: With conditional compilation: a default under #ifndef and overrides through -D in the Makefile or CMake file.
Q5. What does enum { SIZE = 8 }; give you that const does not?
Answer: A true integer constant expression usable for array sizes and case labels, with no storage and proper scope.
Q6. What is #undef for?
Answer: It removes a macro definition so you can redefine it or stop later code from seeing it.
Summary and Key Takeaways
- Symbolic constants replace magic numbers with meaningful names and a single point of change.
#defineis text substitution done by the preprocessor, with no type and no scope.- Always parenthesise macro bodies and parameters; use inline functions when you can.
enumandconstare typed, scoped, debugger-friendly alternatives.#ifndefplus-Dlets the build choose values without editing code.gcc -Eshows exactly what the compiler receives.
Conclusion
Symbolic constants in C are one of the simplest tools that make programs readable and easy to change. You now know three ways to name a value, what the preprocessor does behind the scenes, and how a missing pair of parentheses turns 25 into 11. You also saw how real embedded projects flip behaviour with a single compiler flag.
With all four kinds of constants covered, the final lecture of this chapter introduces variables: named storage that can change, and the rules for declaring and initialising them.
Frequently Asked Questions
1. Should I use #define or const in modern C?
Answer: Prefer enum or const when you only need a value, and keep #define for conditional compilation and header guards. C23 also adds constexpr.
2. Does a #define use memory?
Answer: No. It is replaced by text before compilation, so the name does not exist at run time.
3. Why does my macro give the wrong answer?
Answer: Almost always missing parentheses. Print the expansion with gcc -E to see the real text.
4. Is this free embedded systems course enough for embedded interviews?
Answer: It covers the C fundamentals interviewers test first. Combine it with the free embedded Linux course and the device driver lectures for depth.
5. Can a #define span several lines?
Answer: Yes, end each line except the last with a backslash. Wrap multi-statement macros in do { … } while (0).
6. How are symbolic constants used in the Linux kernel?
Answer: Register offsets, bit masks and flags are macros, often built with helpers like BIT(n). You will meet them in the free Linux kernel development course.
Next: Variables in C
Finish this chapter of the free embedded systems course with variables, then continue toward the free Linux device drivers course. Try rewriting an old program so every magic number gets a name.
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments