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).
&. Modern compilers ignore the hint.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.
#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; }
=== 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.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 — ®Var 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.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.
#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; }
=== 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
#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.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./* 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 */
#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.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.
#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; }
=== 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
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.#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.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.- auto — default for local variables. Stack-allocated. No default init (holds garbage). Destroyed when its block exits. You almost never need to write
autoexplicitly. - 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)). Usedo{...}while(0)for multi-statement macros. Use#ifndefguards 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<<2for combinable flags. Always usetypedef enumfor cleaner type names.