Lifetime, visibility and the preprocessor
Interviewers check that you know where variables live, who can see them, what const and volatile promise, and how the preprocessor transforms code before the compiler sees it.
- Storageauto, static, extern and register.
- Qualifiersconst with pointers and volatile.
- PreprocessorMacros, include guards and #if.
Storage classes and keywords decide where a variable lives, how long it lasts and who can see it — the details embedded and systems interviews care about. The preprocessor questions test whether you know what happens before compilation.
1. Explain the storage classes: auto, static, extern and register.
- auto — default for local variables: stack, lives only during the block.
- static — a local static keeps its value between calls; a global static is visible only in its file.
- extern — declares a variable defined in another file.
- register — a historic hint to keep a variable in a CPU register; compilers ignore it, and you cannot take its address.
void count_calls(void) {
static int calls = 0; // initialised once, keeps its value
calls++;
printf("called %d times\n", calls);
}
/* config.c */ int max_users = 100;
/* main.c */ extern int max_users; // refers to the one in config.c2. What does static mean for functions and global variables?
It gives them internal linkage: the name is private to that source file, which prevents clashes between files and hides helper functions — C's version of “private”.
/* payments.c */
static int validate(const char *card) { return card && strlen(card) == 16; } // private helper
int charge(const char *card, int amount) { return validate(card) ? 0 : -1; }3. What does const mean with pointers?
Read the declaration right to left. const int *p — pointer to const int (cannot change the value through p). int *const p — const pointer (cannot point elsewhere). const int *const p — neither. Use const in parameters to promise a function will not modify the data.
size_t count_vowels(const char *s); // promises not to modify s
int x = 5, y = 7;
const int *a = &x; // *a = 6 is an error; a = &y is fine
int *const b = &x; // *b = 6 is fine; b = &y is an error4. What is the volatile keyword for?
It tells the compiler that a variable can change outside the program's visible flow — a hardware register, a variable changed by an interrupt handler or a signal — so every access must really read or write memory. Without it the compiler may cache the value and your loop never sees the change. It does not make code thread-safe.
volatile int data_ready = 0; // set by an interrupt handler
void wait_for_data(void) {
while (!data_ready) { } // re-read on every iteration
}5. Macros vs functions — pros and cons?
Macros are text substitution done by the preprocessor: no call overhead and work with any type, but no type checking, hard to debug, and arguments may be evaluated twice. Always bracket macro parameters and the whole body. Prefer static inline functions and const/enum constants in modern C.
#define SQUARE_BAD(x) x * x
#define SQUARE(x) ((x) * (x))
SQUARE_BAD(1 + 2); // 1 + 2 * 1 + 2 = 5, not 9
SQUARE(i++); // i incremented twice — undefined behaviour
static inline int square(int x) { return x * x; } // type-safe alternative6. What are include guards?
They stop a header from being included twice in one translation unit, which would cause redefinition errors. Use the classic #ifndef pattern or the widely supported #pragma once.
#ifndef ORDER_H
#define ORDER_H
typedef struct {
int id;
double total;
} Order;
int order_save(const Order *o);
#endif /* ORDER_H */7. How does conditional compilation work?
#if, #ifdef, #ifndef, #elif and #endif include or exclude code before compilation — for debug logging, platform differences and feature flags. Pass definitions from the build with -DDEBUG.
#ifdef DEBUG
#define LOG(fmt, ...) fprintf(stderr, "[debug] " fmt "\n", __VA_ARGS__)
#else
#define LOG(fmt, ...) ((void)0)
#endif
#if defined(_WIN32)
/* Windows-specific code */
#else
/* POSIX code */
#endif8. What does typedef do, and how does it differ from #define?
typedef creates a real type alias understood by the compiler (scoped and type-checked); #define is blind text replacement. With pointers the difference shows: typedef char *str; str a, b; makes two pointers, but #define STR char * then STR a, b; makes one pointer and one char.
Remember
Key points
- static local = remembered
static global = file-private.
- Read const right to left
Pointer to const vs const pointer.
- volatile ≠ thread-safe
It only forces real memory access.
- Bracket macro arguments
Or use static inline functions.
Interview drill
Follow-up questions
What the interviewer asks after your first answer. Turn on practice mode, answer each one aloud, then reveal.
-
1 Can a variable be both const and volatile? Advanced
Yes — a read-only hardware status register: your code must not write it (const), but it can change at any time (volatile).
-
2 What is the default value of a static or global variable? Basic
Zero (or NULL for pointers). Local automatic variables are not initialised and hold garbage.
-
3 What does the ## operator do in macros? Advanced
Token pasting — it joins two tokens into one, for example to generate function names.
#turns an argument into a string literal. -
4 What is the difference between declaration and definition? Basic
A declaration introduces a name and type (extern int x;); a definition also allocates storage or provides the body (int x = 5;). A variable can be declared many times but defined once.
-
5 What are predefined macros like __FILE__ and __LINE__ used for? Basic
They expand to the current file name and line number — useful in logging and assert messages.
-
6 What is an enum and why is it better than #define constants? Basic
An enum groups related integer constants under a type, is visible to the debugger and is scoped by the compiler, unlike blind text macros.