C storage classes and macros interview questions, with answers
Storage classes and the preprocessor decide things about a C program that its statements never mention: how long a variable lives, which files can see a name, and what text the compiler actually receives. Interviewers probe them because the rules are easy to state and easy to misapply, from a variable that keeps its value between calls to a macro that expands into the wrong expression. Every answer below comes with code whose output was produced by compiling and running it.
The questions cover the storage classes, sharing variables across files, macros and their alternatives, conditional compilation, and the qualifier interviewers save for last, volatile. Then take the free C diagnostic — ten questions across every C topic in the bank — to see which of these you can explain but not yet predict.
The questions, with answers
1.What are the storage classes in C, and what does each control?
In short: auto, register, static and extern, plus _Thread_local, decide a variable's lifetime and linkage: automatic locals die with their block, static ones live for the whole program.
Every variable has a scope, where its name is visible, a storage duration, how long it lives, and a linkage, which files can refer to it. auto is the default for locals: automatic storage, created on entry to the block and destroyed on exit; the keyword is never written in C, and C23 reuses it for type inference. register asks for fast access and forbids taking the variable's address, as the code shows, though compilers ignore the hint itself. static on a local gives it static duration, so it is initialised once and keeps its value between calls, and on a file-scope name it gives internal linkage, private to its file. extern declares a name defined elsewhere, and C11's _Thread_local gives each thread its own copy.
register int r = 5; int *p = &r; // does not compile printf("%d\n", r); // 5
2.How do you share a global variable between two C files?
In short: Define it once, without extern, in one .c file, and declare it with extern in a header that every file using it includes.
A definition allocates the variable; a declaration only announces its name and type. For a global shared across files, exactly one source file must contain the definition, such as int counter = 0;, and every other file must see a declaration, extern int counter;, which in practice lives in a header. Two definitions in different files break the one-definition rule, and since gcc 10 the default -fno-common turns that into a multiple-definition link error instead of silently merging them; forgetting the definition gives an undefined-reference error at link time. The code below shows the same split inside one file: the extern declaration lets main use a variable whose definition comes later. Functions work the same way, except that a function declaration is extern by default.
extern int limit; int main(void) { printf("%d\n", limit); } int limit = 64; // 64
3.What is the difference between a macro and an inline function in C?
In short: A macro is text substituted by the preprocessor, untyped and pasting its argument wherever it appears; an inline function is a real, type-checked function the compiler may expand.
#define MAX(a, b) ((a) > (b) ? (a) : (b)) is expanded as text before compilation, so it works for any type but checks none, it can evaluate an argument twice, and it never appears in the debugger. In the code, MAX(i++, j) increments i twice, while the inline function increments it once. static inline int max(int a, int b) is an ordinary function: each argument is evaluated exactly once, arguments are type-checked and converted, it has a scope, and the compiler may expand it at the call site or not, as it judges best. Prefer inline functions and enums, and keep macros for what only the preprocessor can do: include guards, conditional compilation, and generating code with # and ##.
#define MAX(a, b) \ ((a) > (b) ? (a) : (b)) static inline int max(int a, int b) { return a > b ? a : b; } int main(void) { int i = 5, j = 3; int m = MAX(i++, j); printf("%d %d\n", m, i); i = 5; m = max(i++, j); printf("%d %d\n", m, i); } // 6 7 // 5 6
4.What is the difference between #define and const for constants in C?
In short: #define is textual replacement with no type or scope; a const variable is typed and scoped but is not a constant expression in C, so an enum is often the better choice.
#define SIZE 8 replaces every later SIZE with 8 before compilation: no type, no scope, nothing in the debugger, and a chance of colliding with any identifier. const int size = 8; is a real variable with a type and a scope, which the compiler can check and a debugger can show. The C-specific catch interviewers look for is that a const int is not an integer constant expression, so it cannot size an array at file scope, label a case or set a bit-field width, as the rejected case label below shows, where C++ would accept it. An enumeration constant, enum { SIZE = 4 };, is both an int and a true constant expression, which makes it the idiomatic replacement for integer #defines; C23's constexpr now fills the same gap.
enum { SIZE = 4 }; const int n = 4; int v = 4; switch (v) { case SIZE: puts("enum works"); break; case n: // does not compile break; } // enum works
5.What are #if, #ifdef and #ifndef used for in C?
In short: They keep or drop blocks of source before compilation, for debug-only code, platform differences, feature switches, and the include guards around headers.
The preprocessor evaluates #if on integer constant expressions, #ifdef NAME when a macro is defined and #ifndef NAME when it is not, and keeps or discards everything up to the matching #else, #elif or #endif, so the compiler never sees the dropped code. Common uses are debug logging that disappears from release builds, platform-specific code selected by predefined macros such as _WIN32 or __linux__, feature switches passed on the command line with -DNAME, and include guards, which stop a header's contents from being processed twice. defined(NAME) lets #if combine several conditions, and #error stops the build with a message. assert is built the same way: defining NDEBUG removes every assertion, and this build does not define it, so the second line prints.
#define LEVEL 2 int main(void) { #if LEVEL >= 2 puts("verbose"); #endif #ifndef NDEBUG puts("asserts on"); #endif } // verbose // asserts on
6.What do the # and ## operators do in a C macro?
In short: # turns a macro argument into a string literal, and ## pastes two tokens into one, which lets a macro print expressions and build identifiers.
Inside a function-like macro, #x replaces the argument with a string literal of its spelling, so SHOW(var_1 * 2) can print the expression's text next to its value, which is how assert reports the condition that failed. a ## b joins two tokens into one, so VAR(1) below becomes the identifier var_1, a technique used to generate families of related functions or variables. Both operate on the argument's spelling before any macro inside it is expanded, which is why turning the value of another macro into a string needs two levels of macros, the STR and XSTR idiom. Adjacent string literals are joined by the compiler, which is what lets #x sit directly next to a format string.
#define SHOW(x) \ printf(#x " = %d\n", (x)) #define VAR(n) var_ ## n int main(void) { int VAR(1) = 7; SHOW(var_1 * 2); } // var_1 * 2 = 14
7.What does the volatile keyword do in C, and does it make code thread-safe?
In short: volatile makes every read and write of an object really happen, for hardware registers and signal flags; it gives no atomicity and no ordering between threads.
The compiler normally keeps a value in a register and drops reads and writes it can prove redundant. volatile forbids that for the object: each access in the source becomes an access in the machine code, in order relative to other volatile accesses. That is exactly what memory-mapped hardware registers need, where each read may return a new value, and what a flag set by a signal handler needs, declared volatile sig_atomic_t. It is not a synchronisation tool: volatile does not make x++ atomic, and it does not stop the compiler or the CPU from reordering other memory accesses around it, so data shared between threads needs C11's <stdatomic.h> atomics or a mutex. Java's volatile is a different, stronger guarantee, which is why the two are often confused.
How the diagnostic asks it
One question from the C bank, exactly as a sitting would show it. The bank has 4 on storage classes & macros and 30 across C.
What does this C program print?
#include <stdio.h> #define SQUARE(x) x * x int main(void) { printf("%d\n", SQUARE(2 + 3)); }
- 111correct
- 225
- 313
- 4It does not compile
A macro is textual substitution, done before compilation. SQUARE(2 + 3) becomes 2 + 3 * 2 + 3, and multiplication binds tighter than addition, so it is 2 + 6 + 3 = 11. 25 is what a function would return, and what the macro gives once every use of the parameter and the whole body are parenthesised: #define SQUARE(x) ((x) * (x)). 13 evaluates 2 + 3 * 2 + 3 strictly left to right, ignoring precedence. It compiles: a macro argument can be any sequence of tokens. Even the parenthesised macro evaluates its argument twice, so SQUARE(i++) increments i twice; an inline function avoids both traps.
Measure it
Reading answers tells you what’s true. A diagnostic tells you what you get wrong.
10 C questions across its topics, easy to hard, about fifteen minutes. You get a readiness figure with the arithmetic shown, the topics you missed named, and a practice set sized for today. Free: 1 diagnostic a month and 15 problems a day. No card.