Loading PDF…
Your browser or server settings are blocking the inline PDF viewer. Open it in a new tab or download it — it's the same guide!
Loading PDF…
Your browser or server settings are blocking the inline PDF viewer. Open it in a new tab or download it — it's the same guide!
Most frequently asked C programming questions — Structures, Macros, Compilation, Volatile/Const, and Interrupts.
The compiler decides the memory layout based on alignment rules of the target architecture. Each member is placed at an address that is a multiple of its own size (natural alignment). Padding bytes are inserted between members or at the end to satisfy these alignment requirements.
struct Example {
char a; // 1 byte → offset 0
// 3 bytes padding
int b; // 4 bytes → offset 4
char c; // 1 byte → offset 8
// 3 bytes padding (end padding)
};
// sizeof(Example) = 12 (not 6)
Padding is extra unused bytes inserted by the compiler between structure members (or at the end) to align each member to its natural alignment boundary. This makes memory access faster since CPUs can fetch aligned data in fewer operations.
The unused bytes are called holes in the structure.
Method 1 — Reorder members from largest to smallest to minimize padding:
// BAD (12 bytes due to padding)
struct Bad { char a; int b; char c; };
// GOOD (8 bytes — no wasted padding)
struct Good { int b; char a; char c; };
Method 2 — Use #pragma pack(1) or __attribute__((packed)) to force no padding (may hurt performance):
#pragma pack(1)
struct Packed { char a; int b; char c; };
// sizeof = 6 (no padding)
Method 3 — Use bit fields to pack multiple small values into one word.
| Aspect | Structure | Union |
|---|---|---|
| Memory | Separate memory for each member | All members share one memory location |
| Size | Sum of all members (+ padding) | Size of the largest member |
| Access | All members accessible simultaneously | Only one member valid at a time |
| Keyword | struct | union |
| Use case | Group related but independent data | Save memory when only one field needed at a time |
union Data {
uint32_t raw;
struct { uint8_t b0, b1, b2, b3; } bytes;
};
// Access raw 32-bit value OR individual bytes
Bitfields are structure members that specify the number of bits to allocate rather than a full data type size. They allow packing multiple flags or small values into a single word efficiently.
struct Flags {
unsigned int enable : 1; // 1 bit
unsigned int mode : 3; // 3 bits (0–7)
unsigned int speed : 4; // 4 bits (0–15)
unsigned int : 8; // 8 bits padding (unnamed)
};
// Total: fits in 16 bits
& operator not allowed).field[n] : 1 is illegal.sizeof() on a bitfield member.No. The C standard leaves several aspects implementation-defined:
int bitfields.For portable hardware register access, using explicit masks and bit shifts is preferred over bitfields.
A structure that contains another structure as a member is called a nested structure.
struct Address {
char city[30];
int pin;
};
struct Employee {
int id;
char name[50];
struct Address addr; // nested structure
};
struct Employee e;
e.addr.pin = 560001;
typedef creates an alias for a structure type so you don't have to write struct keyword every time you declare a variable.
// Without typedef
struct Point { int x, y; };
struct Point p1;
// With typedef
typedef struct { int x, y; } Point;
Point p1; // cleaner!
It also helps with self-referential structures (linked lists) and makes APIs cleaner and more readable.
| Aspect | Macro (#define) | const Variable |
|---|---|---|
| Stage | Preprocessor (text substitution) | Compiler (type-checked) |
| Type safety | None — no type checking | Fully type-safe |
| Memory | No memory allocated | Memory allocated (usually) |
| Debuggable | Not visible in debugger | Visible in debugger |
| Scope | From definition to end of file | Follows C scoping rules |
#define MAX 100 // macro — no type const int max = 100; // typed, debuggable
Macros that take arguments like functions but are expanded inline by the preprocessor — no function call overhead.
#define SQUARE(x) ((x) * (x)) #define MAX(a, b) ((a) > (b) ? (a) : (b)) int r = SQUARE(5); // expands to ((5) * (5)) = 25 int m = MAX(3, 7); // expands to ((3) > (7) ? (3) : (7)) = 7
Always wrap arguments and the whole expression in parentheses to avoid operator precedence bugs.
SQUARE(i++)).#define DOUBLE(x) x + x int r = DOUBLE(3) * 2; // Expands to: 3 + 3 * 2 = 9 (not 12!)
When a macro argument has a side effect (like i++), and the macro uses the argument more than once, the side effect happens multiple times — causing unexpected behavior.
#define SQUARE(x) ((x) * (x)) int i = 5; int r = SQUARE(i++); // Expands to: ((i++) * (i++)) // i++ executed TWICE → undefined behavior!
Solution: Use inline functions instead of macros for expressions with side effects.
static inline int square(int x) { return x * x; }
// i++ evaluated only ONCE
#ifdef checks if a macro is defined and includes the code block only if it is. Used for conditional compilation.
#define DEBUG
#ifdef DEBUG
printf("Debug: x = %d
", x); // compiled only if DEBUG defined
#endif
#ifndef RELEASE
// compiled only if RELEASE is NOT defined
#endif
Common uses: enabling debug logs, platform-specific code, feature toggles.
Conditional compilation allows selective inclusion of code blocks based on preprocessor conditions. The excluded code is never compiled — reducing executable size.
#if defined(ARM_PLATFORM)
init_arm();
#elif defined(X86_PLATFORM)
init_x86();
#else
#error "Unknown platform"
#endif
Used for: platform portability, debug vs release builds, feature flags.
| Form | Search Order | Use For |
|---|---|---|
#include <file.h> | System/compiler include paths only | Standard library headers |
#include "file.h" | Current directory first, then system paths | Project/user-defined headers |
A header guard prevents a header file from being included more than once in a compilation unit, avoiding duplicate declaration errors.
// myheader.h
#ifndef MYHEADER_H
#define MYHEADER_H
void myFunction(void);
typedef struct { int x; } Point;
#endif // MYHEADER_H
Alternative (non-standard but widely supported): #pragma once
No, not directly. Since macros are replaced by the preprocessor before compilation, the debugger sees the expanded code — not the macro name. You cannot step into a macro or inspect it by name in GDB.
To inspect macro expansion, use:
gcc -E source.c -o source.i # see preprocessed output # or gcc -dM -E source.c # list all defined macros
This is why inline functions are preferred — they are type-safe and fully debuggable.
Token Pasting (##) — concatenates two tokens into one during macro expansion:
#define CONCAT(a, b) a##b int CONCAT(my, Var) = 10; // becomes: int myVar = 10;
Stringification (#) — converts a macro argument to a string literal:
#define STRINGIFY(x) #x
printf("%s
", STRINGIFY(hello)); // prints: hello
printf("%s
", STRINGIFY(3+4)); // prints: 3+4
| Stage | Tool | Input → Output | Responsibility |
|---|---|---|---|
| 1. Preprocessing | cpp | .c → .i | Expand macros, include headers, strip comments |
| 2. Compilation | cc1 | .i → .s | Syntax/semantic check, generate assembly |
| 3. Assembly | as | .s → .o | Convert assembly to machine opcode (object file) |
| 4. Linking | ld | .o → executable | Link libraries, resolve symbols, create final binary |
#define macros are replaced with their values.#include files are copy-pasted in./* */ and // comments are stripped.#ifdef / #endif blocks resolved.#line directives processed.gcc -E main.c -o main.i # stop after preprocessing
An object file (.o) is the output of the assembler stage. It contains:
-g).Object files are not directly executable — they must be linked to resolve external symbol references.
.a) or shared (.so) libraries.Linker errors (undefined reference to...) occur when a function is called but never defined.
A multiple definition error occurs when the linker finds two or more definitions of the same symbol across object files.
// file1.c int counter = 0; // definition // file2.c int counter = 0; // another definition — LINKER ERROR! // Fix: declare in header, define in ONE .c file // header.h: extern int counter; // file1.c: int counter = 0;
A symbol table is a data structure maintained by the compiler and linker that maps each identifier (function name, variable name) to information about it: type, size, storage class, address/offset.
nm my_program.o # view symbol table of an object file
| Aspect | Static Linking | Dynamic Linking |
|---|---|---|
| When | At compile/link time | At runtime |
| Library included in exe? | Yes — library code copied into binary | No — binary references shared lib |
| Executable size | Larger | Smaller |
| Memory sharing | Each process has its own copy | One copy shared by all processes |
| Portability | Self-contained — no dependency | Requires correct .so on system |
| Extension | .a (archive) | .so / .dll |
A Makefile automates the build process — it defines rules for compiling source files, linking, and other build tasks. The make tool reads the Makefile and only rebuilds files whose dependencies have changed (incremental build).
CC = gcc CFLAGS = -Wall -O2 app: main.o utils.o $(CC) -o app main.o utils.o main.o: main.c $(CC) $(CFLAGS) -c main.c clean: rm -f *.o app
Incremental build means recompiling only the source files that have changed since the last build, rather than rebuilding everything. This saves significant build time in large projects.
Make achieves this by comparing the timestamps of source files and their corresponding object files — if the source is newer, it recompiles.
Compiler flags control how the compiler processes source code:
| Flag | Purpose |
|---|---|
-Wall -Wextra | Enable all warnings — catch bugs early |
-O0 / -O2 / -O3 | Optimization level (debug → release) |
-g | Include debug symbols for GDB |
-std=c99 | Enforce specific C standard |
-DDEBUG | Define a macro (conditional compilation) |
-I./include | Add header search path |
The volatile keyword tells the compiler that a variable's value can change at any time without any action by the code it can see — so the compiler must not cache it in a register or optimize away reads/writes.
volatile int sensor_data; // always read from memory
while (sensor_data == 0) {
// Without volatile, compiler might optimize this to infinite loop
// WITH volatile, it re-reads sensor_data from memory each iteration
}
#define UART_STATUS (*(volatile uint32_t *)0x40013800) // Hardware register — must always read actual hardware value
| Qualifier | Meaning | Use Case |
|---|---|---|
volatile | Value can change unexpectedly — always re-read | ISR variables, shared flags |
const volatile | Cannot be written by code AND must always be re-read | Read-only hardware status registers |
// Status register: hardware changes it, CPU should never write it const volatile uint32_t *STATUS_REG = (uint32_t *)0x40020010; // Read it — OK (volatile ensures fresh read) uint32_t val = *STATUS_REG; // Write to it — compiler ERROR (const prevents this) // *STATUS_REG = 0xFF; // ERROR!
When a variable is modified inside an ISR and read in main(), the compiler doesn't know the ISR can modify it. Without volatile, the compiler may cache the variable in a register and never re-read it from memory, causing the main code to see a stale value.
volatile int flag = 0; // MUST be volatile
void ISR_handler(void) {
flag = 1; // set by ISR
}
int main(void) {
while (!flag) { // must re-read from memory each time
// wait...
}
// process event
}
Without volatile, the compiler may:
Result: the program appears to hang, miss interrupts, or behave incorrectly — bugs that only appear with optimization enabled (-O1 or higher) and disappear in debug builds.
No. volatile only prevents compiler optimizations — it does not guarantee atomicity or provide any synchronization/locking mechanism.
_Atomic / __sync_*), or disabling interrupts.// volatile alone is NOT safe for 32-bit value on 8-bit CPU: volatile uint32_t counter; // read could be interrupted mid-way // Safe approach: disable interrupts or use atomic __disable_irq(); counter++; __enable_irq();
Memory-mapped I/O (MMIO) is a technique where hardware peripheral registers are mapped into the CPU's address space. The CPU reads and writes to them using normal memory load/store instructions — no special I/O instructions needed.
// STM32 GPIOA ODR register at address 0x40020014 #define GPIOA_ODR (*(volatile uint32_t *)0x40020014) GPIOA_ODR |= (1 << 5); // Set pin 5 HIGH GPIOA_ODR &= ~(1 << 5); // Set pin 5 LOW
The volatile qualifier is mandatory for MMIO registers to prevent the compiler from optimizing away the accesses.
Hardware registers are accessed by casting their fixed address to a volatile pointer and dereferencing it:
// Method 1: Macro (common in embedded)
#define REG_CONTROL (*(volatile uint32_t *)0x40021000)
REG_CONTROL = 0x01; // write
uint32_t val = REG_CONTROL; // read
// Method 2: Struct overlay (used in CMSIS / HAL)
typedef struct {
volatile uint32_t CR;
volatile uint32_t SR;
volatile uint32_t DR;
} UART_TypeDef;
#define UART1 ((UART_TypeDef *)0x40013800)
UART1->CR = 0x200C;
A read-only hardware status register should be declared const volatile because:
volatile — forces the compiler to always re-read from the actual hardware register (never cache).const — prevents your code from accidentally writing to it (a write could crash the system or be a no-op).// UART status register — hardware sets flags, software only reads
const volatile uint32_t *UART_SR = (uint32_t *)0x40013800;
if (*UART_SR & (1 << 5)) {
// TX empty — safe to send next byte
}
The C standard guarantees that volatile accesses are not reordered relative to each other by the compiler. However:
// These two volatile reads happen in order: uint32_t a = REG_A; // 1st uint32_t b = REG_B; // 2nd — compiler won't swap these // For hardware barriers (e.g., ARM): __DMB(); // data memory barrier — prevents CPU reordering
An ISR (Interrupt Service Routine) is a special function called automatically by the CPU when a hardware or software interrupt occurs. The CPU pauses normal execution, saves its state, runs the ISR, then resumes.
// ARM Cortex-M example (GCC)
void __attribute__((interrupt)) TIMER0_IRQHandler(void) {
TIMER0->SR &= ~(1 << 0); // clear interrupt flag
// handle timer event
}
ISRs are registered in the interrupt vector table at fixed memory addresses.
delay(), printf(), malloc(), or OS calls.Best practice: Set a flag or write to a ring buffer in ISR, then process in main loop (deferred processing).
A function is reentrant if it can be safely interrupted mid-execution and called again (recursively or from an ISR) without corrupting its state.
A reentrant function:
// Reentrant — uses only local variables
int add(int a, int b) { return a + b; }
// NOT reentrant — uses static variable
int counter(void) {
static int cnt = 0; // shared state — dangerous in ISR!
return ++cnt;
}
A race condition occurs when two or more execution contexts (threads, ISR + main) access shared data concurrently, and the final result depends on the timing/order of execution.
volatile int count = 0;
void ISR_handler(void) { count++; } // ISR
void main_task(void) { count++; } // main
// If both execute "count++" simultaneously:
// 1. Both read count (= 0)
// 2. Both add 1 → both write 1
// Result: count = 1 (should be 2!) — race condition!
Fix: disable interrupts or use atomic operations around the access.
A critical section is a block of code that accesses shared resources and must not be executed by more than one context at a time.
// Embedded: protect with interrupt disable/enable
__disable_irq(); // ENTER critical section
shared_counter++; // protected operation
__enable_irq(); // EXIT critical section
// RTOS: protect with mutex
xSemaphoreTake(mutex, portMAX_DELAY);
shared_resource++;
xSemaphoreGive(mutex);
volatile — prevents compiler from caching the variable._Atomic in C11) for single-variable updates.volatile uint8_t rx_flag = 0;
volatile uint8_t rx_data = 0;
void UART_ISR(void) {
rx_data = UART->DR; // read hardware register
rx_flag = 1; // signal main loop
}
int main(void) {
if (rx_flag) {
rx_flag = 0;
process(rx_data);
}
}
delay(), scanf(), mutex wait) — the ISR never returns, freezing the system.printf() uses malloc() internally and is not reentrant — can corrupt the heap if called from ISR.xQueueReceive(portMAX_DELAY) can deadlock the OS scheduler from within an ISR.Priority inversion occurs when a high-priority task is blocked by a low-priority task that holds a resource (mutex) the high-priority task needs. A medium-priority task can then preempt the low-priority one, effectively blocking the high-priority task indefinitely.
// Classic scenario: // Low task (L) locks mutex M // High task (H) tries to lock M → blocked // Medium task (Med) preempts L → H still blocked! // Med runs freely while H waits → inversion!
Solution: Priority Inheritance — temporarily raise L's priority to H's level while L holds M. Used in RTOS mutexes.
| Aspect | Polling | Interrupt |
|---|---|---|
| Mechanism | CPU continuously checks a flag/register | Hardware signals CPU only when event occurs |
| CPU usage | High — CPU busy even with no events | Low — CPU free until event fires |
| Latency | Depends on poll frequency | Fast — immediate response |
| Complexity | Simple | More complex (ISR, priority, shared data) |
| Power | High power (CPU always running) | CPU can sleep between events |
| Use case | Simple, fast loops; when event rate is high | Infrequent events, low-power systems |