Part of C in 100s

C in 100 Seconds: Constants and Macros | Episode 14

Celest KimCelest Kim

Video: C in 100 Seconds: #define const enum — When to Use Each | Episode 14 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

C Constants: #define, const, enum

#define PI 3.14159 for preprocessor constants. const int x = 10 for 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.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.