Concept Q&A
20 questions
Comprehensive

C/C++ Embedded Interview Questions

Essential C/C++ interview questions for embedded systems: volatile, const, static, pointers, bit manipulation, memory layout, structs, macros, and more.

Study the fundamentals first

Read the C Programming topic pages for in-depth concepts before practicing Q&A

Keywords: volatile, const, static

QWhat does the keyword volatile mean? Give three examples of its use.

volatile tells the compiler that a variable's value may change at any time without any action being taken by the surrounding code. The compiler must reload the variable from memory on every access — it cannot cache it in a register, reorder accesses around it, or eliminate reads that appear redundant.

Three mandatory use cases in embedded systems:

  • Hardware registers — a status register can change between reads because the peripheral updates it independently of the CPU.
  • Variables modified inside an ISR — the main loop reads a flag that an interrupt handler sets; without volatile, the compiler may optimize the re-read into a single load before the loop and never check again.
  • Variables shared between RTOS tasks — multiple threads can modify the same variable concurrently (though volatile alone does not make access atomic — you still need a mutex or critical section).

Failure to use volatile causes bugs that only appear at certain optimization levels — code works in debug (-O0) but breaks in release (-O2).

QCan a variable be both const and volatile? Give a real example.

Yes. A const volatile variable is one the program must not write to, but whose value can still change externally. The classic example is a read-only hardware status register: it is const because the software should never write to it (and the compiler will error if you try), and volatile because the hardware updates it asynchronously between reads.

c
const volatile uint32_t * const STATUS = (const volatile uint32_t *)0x40021000;
// Read-only from software, changes every time hardware updates it
while (!(*STATUS & READY_BIT)) { /* poll */ }

This is one of the most commonly asked C interview questions. Candidates who say "const and volatile are contradictory" reveal a misunderstanding of what const means in C — it means "read-only from this code's perspective," not "the value never changes."

QDoes volatile make a variable access atomic?

No. volatile only prevents the compiler from optimizing away or reordering accesses. It says nothing about atomicity at the hardware level. On a 32-bit ARM Cortex-M, a volatile uint32_t read is atomic because the bus performs a single 32-bit load. But a volatile uint64_t requires two separate 32-bit loads — an interrupt between them can corrupt the value.

For true atomicity, you need either: (1) disabling interrupts around the access, (2) using architecture-specific atomic instructions (LDREX/STREX on ARM), or (3) using a mutex in an RTOS context. volatile is necessary but not sufficient for shared-variable safety.

QWhat are the three uses of the keyword static in C?

static has three distinct meanings depending on context:

  1. Inside a function — the variable retains its value between calls. It is allocated in .bss or .data (not on the stack) and persists for the lifetime of the program. This is useful for counters, state machines, and cached values, but it makes the function non-reentrant — if called from two threads or from an ISR and main simultaneously, the shared static variable becomes a race condition.
  2. At file scope (outside functions) — restricts visibility of the variable to the current translation unit (.c file). This is C's mechanism for creating "private" globals, preventing name collisions across files.
  3. Applied to a function — restricts the function's linkage to the current translation unit, making it invisible to other files. This enables encapsulation in C, where there are no classes or namespaces.
QExplain the different positions of const with pointers.

The position of const relative to * determines what is read-only. Read declarations right-to-left:

c
const int *p; // pointer to const int — can't modify *p, can reassign p
int * const p; // const pointer to int — can modify *p, can't reassign p
const int * const p;// const pointer to const int — can't modify either

In embedded systems, const int * is used for function parameters to promise the function won't modify the caller's data. int * const is used for memory-mapped register pointers that always point to the same address. const also helps the compiler place data in flash (.rodata) instead of RAM, saving precious RAM on small MCUs.

Memory Layout

QWhat are the memory sections in a C program, and what goes where?

A typical embedded C program has these memory sections:

  1. .text — compiled machine instructions, placed in flash (read-only).
  2. .rodataconst globals, string literals, and lookup tables, also in flash.
  3. .data — global and static variables with nonzero initializers. Their initial values are stored in flash and copied to RAM at boot by the C runtime.
  4. .bss — global and static variables that are zero-initialized or have no explicit initializer. Zeroed at boot by the C runtime. No flash storage needed for values — only the section size.
  5. Stack — local variables, function arguments, and return addresses. Grows downward from the top of RAM. Size is fixed at link time.
  6. Heap — dynamically allocated memory (malloc/free). Grows upward. Many embedded systems avoid it entirely.

The critical interview point: only .data and .bss are initialized by the startup code. Local (stack) variables are NOT initialized — they contain whatever garbage was in that memory location. This is why int sum; for(...) sum += x; is a bug.

QWhat is the difference between .data and .bss, and why does it matter?

.data holds globals/statics initialized to nonzero values — their initial values must be stored in flash and copied to RAM at boot. .bss holds globals/statics that are zero-initialized — only the section size is stored, and the startup code memsets it to zero.

The practical implication: a large array like uint8_t buf[4096] = {0}; goes in .bss and costs 4 KB of RAM but zero flash. The same array initialized to {1, 2, 3, ...} goes in .data and costs 4 KB of RAM plus 4 KB of flash for the initializers, plus boot time to copy it. On flash-constrained MCUs, this distinction matters significantly.

QWhat are the problems with dynamic memory allocation in embedded systems?

Dynamic allocation (malloc/free) is risky in embedded systems for several reasons:

  • Fragmentation — repeated alloc/free cycles fragment the heap into small unusable blocks. With limited RAM, this causes allocation failures even when total free memory is sufficient.
  • Non-deterministic timingmalloc execution time varies depending on heap state, violating real-time deadlines.
  • No garbage collection — every leak is permanent. In a device running for years, even tiny leaks are fatal.
  • Difficult to test — fragmentation failures depend on allocation patterns over time and are nearly impossible to reproduce.

Many embedded coding standards (MISRA C, NASA JPL, DO-178C) ban dynamic allocation entirely. Alternatives include static allocation, memory pools (fixed-size block allocators), and stack allocation for short-lived buffers.

Pointers and Arrays

QWhat happens when you pass an array to a function?

The array decays to a pointer to its first element. Inside the function, sizeof(arr) returns the size of a pointer (4 or 8 bytes), not the size of the original array. This is one of the most common C bugs — code that worked with a local array breaks when refactored into a function:

c
void process(uint8_t data[]) {
size_t len = sizeof(data); // BUG: returns sizeof(pointer), not array size
}

The fix is to always pass the array length as a separate parameter: void process(uint8_t *data, size_t len). There is no way to recover the original array size from a decayed pointer — the information is permanently lost at the function boundary.

QWhat is a dangling pointer and how do you prevent it?

A dangling pointer points to memory that has already been freed or gone out of scope. Dereferencing it causes undefined behavior — often a crash or silent data corruption.

Common causes: (1) returning a pointer to a local variable, (2) using a pointer after free(), (3) using a pointer to a variable whose scope has ended (e.g., a pointer to a block-scoped variable after the block exits).

Prevention: set pointers to NULL after freeing (free(ptr); ptr = NULL;), never return pointers to local variables, and in RTOS contexts, ensure that task-local data outlives any pointer to it. Static analysis tools (Coverity, Polyspace) can detect many dangling pointer patterns at compile time.

Structs, Unions, and Alignment

QGiven this struct, what is sizeof(s) and why?
c
struct example {
uint8_t a; // 1 byte
uint32_t b; // 4 bytes
uint8_t c; // 1 byte
};

On a 32-bit ARM, sizeof(struct example) is 12 bytes, not 6. The compiler inserts 3 bytes of padding after a to align b on a 4-byte boundary, and 3 bytes of padding after c to make the struct's total size a multiple of its largest member's alignment (4 bytes).

Reordering the fields — uint32_t b; uint8_t a; uint8_t c; — reduces the size to 8 bytes (4 + 1 + 1 + 2 padding). In embedded systems with limited RAM, this reordering can save significant memory when thousands of structs are allocated. The rule: order fields from largest to smallest to minimize padding.

QIs using a union for type punning defined behavior in C?

Yes, in C99 and later. The C standard explicitly allows reading a union member that was not the last one written to. This is commonly used to interpret a float as its raw bit pattern, or to overlay a register value with a struct of named bitfields:

c
union float_bits {
float f;
uint32_t u;
};
union float_bits x;
x.f = 3.14f;
uint32_t raw = x.u; // Defined in C99+

However, this is undefined behavior in C++. If your codebase must compile as both C and C++, use memcpy() instead — the compiler will optimize it to a single register move.

QWhy are bitfields problematic for hardware register access?

Bitfields have three major portability issues that make them unreliable for hardware registers:

  1. Bit ordering is implementation-defined — the compiler chooses whether the first field occupies the MSB or LSB. Different compilers (or even different versions of the same compiler) may lay out bits in the opposite order.
  2. Storage unit boundaries are compiler-dependent — a bitfield that spans a byte or word boundary may or may not be split across storage units.
  3. Read-modify-write behavior — the compiler may read the entire register word, modify the bitfield, and write back, potentially overwriting volatile status bits in other fields.

The portable alternative is explicit mask-and-shift: reg = (reg & ~MASK) | (value << SHIFT);. MISRA C Advisory Rule 6.1 discourages bitfields for any hardware-interfacing code.

Preprocessor and Inline

QWhat is the double-evaluation trap in macros?

Function-like macros expand their arguments textually, so any argument with side effects is evaluated multiple times:

c
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = MAX(i++, j++); // i or j incremented TWICE

The winning argument is evaluated once in the comparison and once in the result — causing the increment to happen twice. This is why static inline functions are preferred when possible — they evaluate each argument exactly once while still being inlined by the compiler. The macro version is still necessary for type-generic operations in C (before C11's _Generic).

QWhen would you use a macro instead of an inline function?

Prefer static inline functions for most cases — they provide type checking, evaluate arguments once, and are debuggable. Use macros only when you need something inline functions cannot do:

  • Type-generic operations — a single MAX macro works with int, float, uint32_t without overloading (C has no function overloading).
  • Stringification and token pasting# and ## operators for generating code, enum-to-string tables (X-macros), or logging with __FILE__ and __LINE__.
  • Compile-time constants#define ARRAY_SIZE 64 for array dimensions (can't use a variable for static array sizes in C89).
  • Conditional compilation#ifdef, #ifndef, include guards.

MISRA C Rule 4.9 advises: use inline functions instead of function-like macros wherever possible.

Bit Manipulation

QHow do you set, clear, and toggle a specific bit in a register?
c
#define BIT(n) (1U << (n))
reg |= BIT(3); // Set bit 3
reg &= ~BIT(3); // Clear bit 3
reg ^= BIT(3); // Toggle bit 3

Use |= to set (OR with the mask), &= ~ to clear (AND with the inverted mask), and ^= to toggle (XOR with the mask). Always use 1U (unsigned) rather than 1 to avoid undefined behavior when shifting into the sign bit. For multi-bit fields, use mask-and-shift: reg = (reg & ~(0x7 << 4)) | (value << 4); to write a 3-bit field at position 4.

Never use direct assignment (reg = BIT(3)) — this clears all other bits in the register.

Interrupts

QCritique this ISR. What is wrong with it?
c
__interrupt double compute_area(double radius) {
double area = PI * radius * radius;
printf("Area = %f", area);
return area;
}

This ISR has four fundamental problems:

  • ISRs cannot return values — the hardware invokes the ISR via the interrupt vector; there is no caller to receive a return value.
  • ISRs cannot take parameters — they are triggered by hardware, not called with arguments.
  • Floating-point in an ISR — FP operations use shared FPU registers that may not be saved/restored, and they are slow. ISRs should be fast and minimal.
  • printf() in an ISR — it is slow, typically not reentrant, and may rely on interrupts itself (UART TX), causing deadlocks.

A proper ISR: takes no parameters, returns void, does minimal work (set a flag, copy data to a buffer), and defers heavy processing to the main loop or an RTOS task.

Type Promotion

QWhat does the comparison `(int)-1 > (unsigned)0` evaluate to?

It evaluates to true. When a signed and unsigned integer appear in the same expression, C's implicit conversion rules promote the signed value to unsigned. The value -1 is reinterpreted as UINT_MAX (4294967295 on a 32-bit system), which is far greater than 0.

This is one of the most dangerous C traps in embedded code, where unsigned types are used extensively for registers, counters, and sizes. The defense: never mix signed and unsigned in comparisons without an explicit cast, and enable -Wsign-compare and -Wconversion compiler warnings.

Function Pointers and Callbacks

QWhat is a callback and how is it used in embedded systems?

A callback is a function passed by pointer to be called later when an event occurs. In embedded systems, callbacks are the primary mechanism for decoupling drivers from application logic:

c
typedef void (*uart_rx_cb_t)(uint8_t byte);
void uart_register_rx_callback(uart_rx_cb_t cb);

The UART driver calls cb(received_byte) whenever a byte arrives. The application registers its handler without the driver needing to know what it does. Common uses: timer expiration, DMA complete, protocol parsers, state machine transitions.

Safety consideration: always null-check before calling (if (cb) cb(data);) and place constant dispatch tables in flash using const to prevent corruption.

Alignment and Endianness

QWhat happens on an unaligned memory access, and how do you avoid it?

An unaligned access occurs when a multi-byte value is read from or written to an address that is not a multiple of its size (e.g., reading a uint32_t from address 0x03 instead of 0x04). The behavior depends on the architecture:

  • Cortex-M0/M0+: generates a HardFault (crash). No recovery.
  • Cortex-M3/M4/M7: handles it in hardware but with a performance penalty (extra bus cycles). Some accesses to certain memory regions still fault.
  • x86: always handles it, with a small performance cost.

Common causes: casting a uint8_t* buffer to a uint32_t* to "read 4 bytes at once," packed structs, and deserializing network data by casting. The safe approach is memcpy() — the compiler optimizes it to an aligned load when possible, and handles unaligned cases correctly on all architectures.