C in 100 Seconds: Constants and Macros | Episode 14
Video: C in 100 Seconds: #define const enum — When to Use Each | Episode 14 by Taught by Celeste AI - AI Coding Coach
C Constants: #define, const, enum
#define PI 3.14159for preprocessor constants.const int x = 10for type-safe constants.enum Color { RED, GREEN, BLUE }for related groups. Three tools, different jobs.
C has multiple ways to name a constant value. Each has trade-offs in type safety, scope, and debug visibility.
The basic shape
#include <stdio.h>
#define PI 3.14159
#define MAX 100
enum Color { RED, GREEN, BLUE };
int main() {
const int year = 2026;
printf("PI = %f\n", PI);
printf("MAX = %d\n", MAX);
printf("Year = %d\n", year);
enum Color c = GREEN;
printf("Color = %d\n", c);
return 0;
}
Output:
PI = 3.141590
MAX = 100
Year = 2026
Color = 1
#define — preprocessor substitution
#define PI 3.14159
#define MAX 100
#define NAME value is a preprocessor macro. Before compilation, the preprocessor literally substitutes every occurrence of NAME in your source with value.
So printf("%f", PI) becomes printf("%f", 3.14159) before the compiler sees it.
Pros:
- No memory allocated.
- Works with any value (numbers, strings, expressions).
- Can be used at compile time (e.g., int arr[MAX]).
Cons:
- No type checking. #define X 5 then int *p = X; becomes int *p = 5; — silent.
- Hard to debug (text substitution; not visible to debuggers as a named symbol).
- Scope is "from #define to end of file (or #undef)" — no block scoping.
const — typed constants
const int year = 2026;
const prefix says "this variable can't be reassigned." It's a real variable with a type — the compiler enforces the constness.
Pros:
- Type-checked. const int y = "hello" errors.
- Block-scoped (or wherever it's declared).
- Visible to debuggers.
Cons: - Takes memory (most compilers fold simple consts to immediates, but it's not guaranteed). - Initialization expression must be evaluable when used (some restrictions for use as array size pre-C99).
For C, const int N = 5; int arr[N]; works in C99+ as a variable-length array (VLA). Pre-C99, this wasn't allowed — #define was the only way to size arrays at compile time.
enum — related named constants
enum Color { RED, GREEN, BLUE };
enum Color c = GREEN;
enum introduces a new type and a set of named integer constants. By default, RED = 0, GREEN = 1, BLUE = 2 — sequential from 0.
You can specify values:
enum Status {
OK = 0,
ERROR = -1,
PENDING = 100,
};
Pros:
- Self-documenting (names tied to a group).
- Type provides some semantic clarity (enum Color c vs int c).
- Switch statements get warnings for missing cases (-Wswitch).
Cons:
- Underlying type is int — can be assigned arbitrary integers (enum Color c = 999; compiles).
- C's enums are not strict like C++ enum classes — no namespacing.
When to use which
| Use case | Best tool |
|---|---|
Mathematical constant (PI, E) |
const double (or #define) |
| Buffer size, array dimension | #define (pre-C99) or const int (C99+) |
| Related set of states | enum |
| String constant | const char * or #define |
| Compile-time expression | #define |
For modern C, prefer const and enum over #define when possible. Reserve #define for:
- Conditional compilation (
#define DEBUG). - Macros with arguments (
#define MAX(a,b) ((a) > (b) ? (a) : (b))). - Stringification or token pasting.
Macros with arguments
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = MAX(5, 10); // expands to: int x = ((5) > (10) ? (5) : (10));
Useful when you can't use a function (e.g., need to work with multiple types). Wrap arguments in parens to avoid surprises:
#define BAD_MAX(a, b) a > b ? a : b
int z = BAD_MAX(1+2, 3); // expands to 1+2 > 3 ? 1+2 : 3 → 3 (wrong!)
BAD_MAX(1+2, 3) becomes 1+2 > 3 ? 1+2 : 3. Operator precedence makes that 1 + (2 > 3 ? 1+2 : 3) = 1 + 3 = 4. Wrong answer.
MAX(1+2, 3) with parens becomes ((1+2) > (3) ? (1+2) : (3)) = 3. Correct.
For function-like behavior, prefer real functions in modern C; or use static inline for the small-and-fast pattern.
const in function parameters
void print(const char *s) {
printf("%s\n", s);
// s[0] = 'X'; // would be ERROR — s is const
}
const char *s says "the function won't modify the string." Useful for documentation and as a contract — strcpy, strcmp, etc. take const char * for the source.
enum vs #define for state codes
// With #define
#define STATE_IDLE 0
#define STATE_RUNNING 1
#define STATE_DONE 2
int state = STATE_IDLE;
// With enum
enum State { IDLE, RUNNING, DONE };
enum State state = IDLE;
The enum version:
- Groups related constants by name (enum State).
- The compiler can warn on switch statements missing a case.
- IDE auto-complete groups them together.
Generally preferred. Use #define when you specifically need to be able to use the value in #ifdef or array dimensions on old compilers.
Common mistakes
Forgetting parens on macro args. #define DOUBLE(x) x*2 then DOUBLE(3+1) is 3+1*2 = 5, not 8. Parens: #define DOUBLE(x) ((x)*2).
Using #define for type-strict things. #define MAX 100 plus if (val > MAX) works — but if val is unsigned and MAX is signed... mismatch. const int MAX = 100 keeps types clean.
Modifying a const variable via pointer. const int x = 5; int *p = (int*)&x; *p = 10; — undefined behavior. Sometimes works; sometimes crashes.
Enum values overlapping. enum {A=1, B=1}; compiles but probably wrong. If you want explicit values, double-check.
Macro semicolon trap. #define LOG(msg) printf(msg); plus if (cond) LOG("hi") else ... — the ; from the macro creates if (cond) printf("hi"); ; else — error. Avoid trailing ; in macros, or wrap in do { ... } while(0).
What's next
Episode 15: type casting. (int)3.7 → 3. (double)7 / 2 → 3.5. The bridge between the integer and floating-point worlds.
Recap
#define NAME value for preprocessor substitution — no type, no memory. const TYPE name = value for typed constants — block-scoped, debugger-visible. enum Name { A, B, C } for related integer constants — type-named, switch-friendly. Modern C: prefer const and enum; reserve #define for conditional compilation and macros with arguments. Always wrap macro args in parens.
Next episode: type casting.