Storage Classes · Macros · Enums in C
0%
C Language Fundamentals  ·  Chapter

Storage Classes,
Macros & Enums

Three foundational C concepts in one chapter — where and how long variables live in memory, how the preprocessor replaces code before compilation, and how to give names to sets of integer constants.

🗄️
Storage Classes
auto · register · static · extern
⚙️
Macros
#define · object-like · function-like · guards
🏷️
Enums
enum keyword · custom values · switch usage
🗄️
Storage Classes
Control where a variable lives in memory, how long it survives, and who can see it
Part 1

What is a Storage Class?

Every variable in C has two attributes beyond its data type: lifetime (how long it exists in memory) and scope (which parts of the program can see it). A storage class keyword controls both. There are four storage classes in C: auto, register, static, and extern.

Lifetime is either automatic (created on function entry, destroyed on return) or static (created once at program start, persists until the program exits). Scope is either block (visible only inside the curly braces where declared) or file/global (visible across functions or files).

auto
scope: block · lifetime: automatic
Default for all local variables. Stack-allocated. Destroyed when the block exits. No initialisation — holds garbage if not set.
register
scope: block · lifetime: automatic
Hint to the compiler to store in a CPU register for speed. Cannot take its address with &. Modern compilers ignore the hint.
static
scope: block/file · lifetime: program
Inside a function: retains value across calls. At file level: restricts visibility to that file only. Initialised to zero by default.
extern
scope: global · lifetime: program
Declares a variable defined in another file. No memory is allocated — just a declaration that tells the linker where to find it.
where variables live in process memory
auto / register
Stack
← fast, small, grows downward, freed on return
static (local)
BSS / Data
← persists for program lifetime, zero-init if no value given
static (global)
Data segment
← file-private global, linker won't export it
extern
Elsewhere
← declaration only, definition lives in another .c file
Storage class code example
The program below puts all four storage classes side by side. Watch how the static local counter keeps its value between calls while the auto counter resets every time. The register variable behaves just like auto — the difference is only a compiler hint. The extern declaration shows how a global variable defined at the top of the file is visible to all functions.
storage_classes.c
C
#include <stdio.h>

/* ── Global variable (extern linkage by default) ── */
int globalScore = 100;

/* ── File-private global (static at file scope) ── */
static int filePrivate = 42;

void demonstrate() {
    /* auto: default storage class — re-created each call */
    auto int autoVar = 0;
    autoVar++;
    printf("  auto    autoVar   = %d  (always resets)\n", autoVar);

    /* static local: survives across calls — initialised once */
    static int callCount = 0;
    callCount++;
    printf("  static  callCount = %d  (keeps growing)\n", callCount);

    /* register: hint to use CPU register — acts like auto */
    register int regSum = 0;
    for (register int i = 1; i <= 5; i++) regSum += i;
    printf("  register regSum  = %d  (1+2+3+4+5)\n", regSum);

    /* extern: using the global variable from file scope */
    extern int globalScore;   /* re-declaration (optional here) */
    globalScore += 10;
    printf("  extern  globalScore = %d  (modified globally)\n",
           globalScore);

    printf("  static  filePrivate = %d  (file-only global)\n",
           filePrivate);
    printf("  ---\n");
}

int main() {
    printf("=== Storage Classes Demo ===\n\n");

    printf("Call 1:\n"); demonstrate();
    printf("Call 2:\n"); demonstrate();
    printf("Call 3:\n"); demonstrate();

    printf("\nFinal globalScore = %d\n", globalScore);
    return 0;
}
output
=== Storage Classes Demo ===

Call 1:
  auto    autoVar   = 1  (always resets)
  static  callCount = 1  (keeps growing)
  register regSum   = 15  (1+2+3+4+5)
  extern  globalScore = 110  (modified globally)
  static  filePrivate = 42  (file-only global)
  ---
Call 2:
  auto    autoVar   = 1  (always resets)
  static  callCount = 2  (keeps growing)
  register regSum   = 15  (1+2+3+4+5)
  extern  globalScore = 120  (modified globally)
  static  filePrivate = 42  (file-only global)
  ---
Call 3:
  auto    autoVar   = 1  (always resets)
  static  callCount = 3  (keeps growing)
  register regSum   = 15  (1+2+3+4+5)
  extern  globalScore = 130  (modified globally)
  static  filePrivate = 42  (file-only global)
  ---

Final globalScore = 130
static local variables are initialised exactly once — the first time the function is called. On every subsequent call, the variable retains the value it had when the function last returned. This makes static locals perfect for counters, caches, and "first-call" flags that a function needs to remember between invocations.
Never use register in modern C code. The keyword is a leftover from the 1970s. Today's compilers are far better at register allocation than any human hint, and they are free to ignore register entirely. It is kept in the standard for backward compatibility only. More importantly, you cannot take the address of a register variable — &regVar is a compile error.
static at file scope means the variable or function is file-private — the linker will not export it to other translation units. This is the C equivalent of private in object-oriented languages, and it is the correct way to hide implementation details inside a .c file. Marking helper functions static prevents name collisions across large projects.
quick-reference: all four storage classes compared
Keyword
auto
register
static
extern
Default init
garbage
garbage
zero
defined elsewhere
Lifetime
block
block
program
program
Can take &addr?
YES
NO
YES
YES
part 2 — macros
⚙️
Macros & the C Preprocessor
Text substitution that runs before the compiler even sees your code — constants, function-like macros, and header guards
Part 2

What is a Macro?

A macro is a preprocessor directive that performs text substitution before compilation. When the C preprocessor sees #define NAME value, it replaces every occurrence of NAME in the source file with value before the compiler runs. The compiler never sees the macro name — only the substituted text.

There are two kinds of macros: object-like macros (simple name-to-value substitution, used for constants) and function-like macros (take parameters, behave like inline functions). Macros are also used for include guards that prevent header files from being included more than once.

Key rule: macros are not variables. They have no type, no address, no scope in the C sense. They are pure text replacement. This power comes with pitfalls — operator precedence bugs are common in function-like macros if parameters are not wrapped in parentheses.

Types of macros
macros_demo.c
C
#include <stdio.h>

/* ─── 1. Object-like macros: simple constants ─── */
#define PI          3.14159265
#define MAX_STUDENTS 50
#define SCHOOL_NAME "Ananta Academy"
#define NEWLINE     '\n'

/* ─── 2. Function-like macros: parameterised ─── */
#define SQUARE(x)        ((x) * (x))
#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(v, lo, hi) ((v) < (lo) ? (lo) : (v) > (hi) ? (hi) : (v))
#define SWAP(T, a, b)    do { T _t=(a); (a)=(b); (b)=_t; } while(0)

/* ─── 3. Stringification and token pasting ─── */
#define STRINGIFY(x)  #x
#define PASTE(a, b)   a##b

/* ─── 4. Conditional compilation ─── */
#define DEBUG 1
#ifdef DEBUG
  #define LOG(msg) printf("[DEBUG] %s\n", msg)
#else
  #define LOG(msg)   /* nothing in release build */
#endif

int main() {
    printf("=== Macros Demo ===\n\n");

    /* Object-like */
    printf("School      : %s\n",   SCHOOL_NAME);
    printf("Max students: %d\n",   MAX_STUDENTS);
    printf("PI          : %.8f\n\n", PI);

    /* Function-like macros */
    int a = 7, b = 12;
    printf("a=%d  b=%d\n", a, b);
    printf("SQUARE(a)   = %d\n",    SQUARE(a));
    printf("MAX(a,b)    = %d\n",    MAX(a, b));
    printf("MIN(a,b)    = %d\n",    MIN(a, b));
    printf("ABS(-99)    = %d\n",    ABS(-99));
    printf("CLAMP(150,0,100) = %d\n", CLAMP(150, 0, 100));

    /* SWAP using do-while(0) trick */
    printf("\nBefore SWAP: a=%d b=%d\n", a, b);
    SWAP(int, a, b);
    printf("After  SWAP: a=%d b=%d\n", a, b);

    /* Stringification */
    printf("\nSTRINGIFY(PI)   = \"%s\"\n", STRINGIFY(PI));
    printf("STRINGIFY(MAX_STUDENTS) = \"%s\"\n",
           STRINGIFY(MAX_STUDENTS));

    /* Token pasting: PASTE(var, 1) creates the token 'var1' */
    int PASTE(var, 1) = 999;
    printf("\nPASTE(var,1) creates var1 = %d\n", var1);

    /* Conditional LOG (active because DEBUG=1) */
    LOG("Main function reached end");
    return 0;
}
output
=== Macros Demo ===

School      : Ananta Academy
Max students: 50
PI          : 3.14159265

a=7  b=12
SQUARE(a)   = 49
MAX(a,b)    = 12
MIN(a,b)    = 7
ABS(-99)    = 99
CLAMP(150,0,100) = 100

Before SWAP: a=7 b=12
After  SWAP: a=12 b=7

STRINGIFY(PI)   = "PI"
STRINGIFY(MAX_STUDENTS) = "MAX_STUDENTS"

PASTE(var,1) creates var1 = 999
[DEBUG] Main function reached end
Always wrap macro parameters in parentheses. Without them, operator precedence causes bugs. Compare: #define BAD_SQ(x) x*x — calling BAD_SQ(3+2) expands to 3+2*3+2 = 11, not 25. The safe form #define SQUARE(x) ((x)*(x)) expands to ((3+2)*(3+2)) = 25. This is the most common macro bug in C.
The do { ... } while(0) trick is the standard way to write multi-statement function-like macros. It wraps multiple statements in a block that behaves correctly inside if/else without braces: if (cond) SWAP(int, a, b); works perfectly. Without the do-while wrapper, the two statements inside SWAP would break the if/else pairing.
Include guards — prevent double inclusion
mymath.h (header with include guard)
C header
/* Traditional #ifndef guard */
#ifndef MYMATH_H
#define MYMATH_H

#define PI   3.14159265
#define TAU  (2 * PI)

static double circleArea(double r) { return PI * r * r; }
static double circumference(double r) { return TAU * r; }

#endif  /* MYMATH_H */

/* Modern alternative — one line, same effect: */
/* #pragma once  */
Include guards prevent "redefinition" errors when a header is included from multiple files. The first time the preprocessor sees #ifndef MYMATH_H, the symbol is not defined, so it processes the contents and defines MYMATH_H. Every subsequent include finds the symbol already defined and skips the entire block. #pragma once is a shorter, non-standard alternative supported by all major compilers.
macro expansion — what the preprocessor does before the compiler runs
Source code
SQUARE(a + 1)
← what you write
After expansion
((a + 1) * (a + 1))
← what compiler sees
Bad macro
a + 1 * a + 1
← missing parens — wrong answer!
Compile step
Preprocess
Compile
Link
← macros gone before compile
part 3 — enums
🏷️
Enumerations (enum)
Give readable names to sets of integer constants — make code self-documenting and type-safer than bare numbers
Part 3

What is an enum?

An enumeration is a user-defined type that consists of a set of named integer constants called enumerators. Instead of writing if (status == 2) and guessing what 2 means, you write if (status == FAILED) — the code becomes self-documenting. Under the hood, each enumerator is just an int.

By default, enumerators are assigned values starting from 0 and incrementing by 1. You can override any value explicitly — subsequent enumerators continue from the overridden value. You can also assign non-sequential or negative values, or use bit-shifted values to create flag sets (bitmasks).

Enums are the preferred alternative to magic numbers and #define integer constants because they are scoped to the enum type, visible to the debugger (you see names, not numbers), and work naturally with switch statements — the compiler warns if a case is missing.

Enum syntax and usage — three patterns
enums_demo.c
C
#include <stdio.h>

/* ─── Pattern 1: Default values (0, 1, 2, …) ─── */
typedef enum {
    MON, TUE, WED, THU, FRI, SAT, SUN
} Day;

const char *dayName(Day d) {
    const char *names[] = {
        "Monday","Tuesday","Wednesday","Thursday",
        "Friday","Saturday","Sunday"
    };
    return names[d];
}

/* ─── Pattern 2: Custom values ─── */
typedef enum {
    HTTP_OK        = 200,
    HTTP_CREATED   = 201,
    HTTP_NOT_FOUND = 404,
    HTTP_SERVER_ERR= 500
} HttpStatus;

const char *httpMsg(HttpStatus s) {
    switch (s) {
        case HTTP_OK:         return "200 OK";
        case HTTP_CREATED:    return "201 Created";
        case HTTP_NOT_FOUND:  return "404 Not Found";
        case HTTP_SERVER_ERR: return "500 Internal Server Error";
        default:             return "Unknown Status";
    }
}

/* ─── Pattern 3: Bit-flag enum (powers of 2) ─── */
typedef enum {
    PERM_NONE    = 0,        /* 0000 */
    PERM_READ    = 1 << 0,   /* 0001 */
    PERM_WRITE   = 1 << 1,   /* 0010 */
    PERM_EXECUTE = 1 << 2,   /* 0100 */
    PERM_ALL     = PERM_READ | PERM_WRITE | PERM_EXECUTE
} Permission;

void showPerms(int perms) {
    printf("  Permissions: %s%s%s\n",
           (perms & PERM_READ)    ? "READ "    : "",
           (perms & PERM_WRITE)   ? "WRITE "   : "",
           (perms & PERM_EXECUTE) ? "EXECUTE"  : "");
}

int main() {
    printf("=== Enums Demo ===\n\n");

    /* Pattern 1: Day of week */
    printf("--- Day enum (auto 0..6) ---\n");
    for (Day d = MON; d <= SUN; d++)
        printf("  %s = %d%s\n", dayName(d), d,
               (d == SAT || d == SUN) ? "  ← weekend" : "");

    /* Pattern 2: HTTP status codes */
    printf("\n--- HTTP status enum (custom values) ---\n");
    HttpStatus codes[] = { HTTP_OK, HTTP_CREATED,
                           HTTP_NOT_FOUND, HTTP_SERVER_ERR };
    for (int i = 0; i < 4; i++)
        printf("  codes[%d] = %s\n", i, httpMsg(codes[i]));

    /* Pattern 3: Bit-flag permissions */
    printf("\n--- Permission flags (bit-field enum) ---\n");
    int userPerm  = PERM_READ | PERM_WRITE;
    int guestPerm = PERM_READ;
    int rootPerm  = PERM_ALL;

    printf("User  (val=%d): ", userPerm);  showPerms(userPerm);
    printf("Guest (val=%d): ", guestPerm); showPerms(guestPerm);
    printf("Root  (val=%d): ", rootPerm);  showPerms(rootPerm);

    /* Revoke write from user */
    userPerm &= ~PERM_WRITE;
    printf("User after revoking WRITE (val=%d): ", userPerm);
    showPerms(userPerm);

    return 0;
}
output
=== Enums Demo ===

--- Day enum (auto 0..6) ---
  Monday    = 0
  Tuesday   = 1
  Wednesday = 2
  Thursday  = 3
  Friday    = 4
  Saturday  = 5  ← weekend
  Sunday    = 6  ← weekend

--- HTTP status enum (custom values) ---
  codes[0] = 200 OK
  codes[1] = 201 Created
  codes[2] = 404 Not Found
  codes[3] = 500 Internal Server Error

--- Permission flags (bit-field enum) ---
User  (val=3): READ WRITE
Guest (val=1): READ
Root  (val=7): READ WRITE EXECUTE
User after revoking WRITE (val=1): READ
enum integer values — what the compiler assigns under the hood
Day (default)
MON=0
TUE=1
WED=2
THU=3
FRI=4
SAT=5
SUN=6
HttpStatus
200
201
404
500
← explicit custom values
Permission bits
NONE=0
READ=1
WRITE=2
EXEC=4
ALL=7
← powers of 2 for bitmask
User perm
R=001
W=010
X=000
← 001 | 010 = 011 = 3
Prefer typedef enum over plain enum so you can use the type name directly without the enum keyword each time. Instead of enum Day d = MON, you write the cleaner Day d = MON. This is the standard convention in all modern C code.
Use bit-shifted enums instead of #define for flags. The pattern 1 << 0, 1 << 1, 1 << 2 creates non-overlapping bits so you can combine any set of flags with | and test any flag with &. Revoking a flag uses bitwise AND with the complement: perms &= ~PERM_WRITE. This is exactly how Unix file permissions (rwx), CSS class flags, and OS capability bits work.
Enum values are just int — there is no type safety. C will let you assign Day d = 999 with no error, even though 999 is not a valid Day. For stronger type checking, use a dedicated struct wrapper or move to C++ where enum classes provide proper scoping and type safety.
quick-reference comparison
storage classes vs macros vs enums — when to use which
For a constant
#define (ok)
const int (better)
enum (best for sets)
For a counter
static local int
persists across calls
For inline code
#define macro
static inline fn (safer)
For named states
#define ints (bad)
enum (always prefer)
For bit flags
enum with 1<<n values
combine with |
checklist
  • auto — default for local variables. Stack-allocated. No default init (holds garbage). Destroyed when its block exits. You almost never need to write auto explicitly.
  • register — a compiler hint to use a CPU register. Acts like auto but cannot take its address with &. Modern compilers ignore the hint; avoid in new code.
  • static (local) — allocated once at program start, survives across function calls, initialised to zero by default. Perfect for counters, caches, first-call flags. static (global) restricts a variable or function to file scope.
  • extern — a declaration (not a definition). Tells the linker the variable is defined in another translation unit. No memory allocated at the point of declaration.
  • Macros — preprocessor text substitution before compilation. Always wrap parameters in parentheses: #define SQ(x) ((x)*(x)). Use do{...}while(0) for multi-statement macros. Use #ifndef guards in every header file.
  • enum (default) — enumerators start at 0, increment by 1. Custom values: assign explicit integers. Bit flags: use 1<<0, 1<<1, 1<<2 for combinable flags. Always use typedef enum for cleaner type names.