What are Symbolic Constants in C- C Programming For Beginners

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

Elements of C · Lecture 4

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.

4
Runnable programs
8
Quiz questions
6
Interview questions
Free
Forever

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, const and enum
  • 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 #ifndef and -D to 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"
What happens to a #define before compilation
Your source with names like MAX_STUDENTS
▼
Preprocessor: replaces each name with its text, removes comments, handles #include
▼
Compiler: sees only plain numbers and strings
▼
Assembler and linker: build the final program

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.

Macro expansion of SQUARE_BAD(2 + 3)
Source code :  SQUARE_BAD(2 + 3)
Definition :  #define SQUARE_BAD(x) x * x
After cpp :  2 + 3 * 2 + 3
Evaluated as:  2 + 6 + 3 = 11

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#defineconst objectenum constant
Has a typeNoYesint
Uses memoryNo (text only)Usually yes (may be optimised away)No
Obeys scopeNo, file-wide until #undefYesYes
Visible in debuggerNoYesYes
Usable as array size in CYesNo (file scope) / makes a VLA (block scope)Yes
Usable in #ifYesNoNo
Can hold strings or floatsYesYesNo, 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.

How -D overrides a default
gcc -DLOG_LEVEL=3 …
▼
Macro is already defined, so #ifndef is false and the default is skipped
▼
Code compiled with LOG_LEVEL = 3

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_STUDENTS changes the loop and the printed message together.
  • The area is 3.14159265 × 2 × 2 = 12.5664 after %.4f rounding.
  • COURSE and GRADE_PASS are 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_SIZE is an enum constant, so it can size the array and sizeof(buf) / sizeof(buf[0]) yields 8.
  • TIMEOUT_MS is a macro; retries is 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

MistakeWhat happensFix
#define MAX 100;The semicolon becomes part of the replacement text and breaks expressions like arr[MAX]No semicolon after a #define
#define MAX = 100Replacement text is = 100, causing odd syntax errorsNo equals sign
Unparenthesised expression or parameterWrong precedence, as in 11 instead of 25Parenthesise the body and every parameter
Side effects in macro argumentsThe argument is evaluated more than onceUse a static inline function
Using const int n as a file-scope array sizeCompile error in C, because it is not a constant expressionUse enum or #define
Lower-case macro namesCollide with variables and confuse readersUse 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.
  • #define is text substitution done by the preprocessor, with no type and no scope.
  • Always parenthesise macro bodies and parameters; use inline functions when you can.
  • enum and const are typed, scoped, debugger-friendly alternatives.
  • #ifndef plus -D lets the build choose values without editing code.
  • gcc -E shows 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

Leave a Reply

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