Skip to content
Laviyx Learn

Try

C course menu

Advanced C Interview Questions: UB, volatile, Memory Layout, Endianness and Embedded C

Advanced C interview questions with answers: undefined, unspecified and implementation-defined behavior, volatile, the memory layout of a C program, stack vs heap, structure padding, endianness, restrict, inline, setjmp and longjmp, bit fields, and interview questions for embedded systems roles.

Advanced10 min readUpdated
On this page

These questions appear in interviews for systems, embedded and experienced developer roles. They test whether you understand what the compiler and the machine actually do with your code.

1. What is undefined behavior, and how does it differ from unspecified and implementation-defined behavior?#

Show solution
  • Undefined: the standard sets no requirements. Examples: signed overflow, out-of-bounds access, null dereference, i = i++.
  • Unspecified: one of several outcomes, and the compiler need not document which. Example: the order in which function arguments are evaluated.
  • Implementation-defined: the compiler must choose and document. Examples: the size of int, right-shifting negative numbers.

Optimisers assume UB never happens, so UB can remove checks or change behavior between -O0 and -O2. See Undefined Behavior.

2. What does volatile do? When do you need it?#

Show solution

volatile tells the compiler that a variable can change in ways it cannot see, so every read and write must really happen in the order written. Without it, the compiler may keep the value in a register and skip "redundant" reads. Use it for:

  • memory-mapped hardware registers;
  • variables shared with an interrupt service routine;
  • a volatile sig_atomic_t flag set in a signal handler.
C
#define STATUS_REG (*(volatile uint32_t *)0x40021000u)   /* example address */
while ((STATUS_REG & 0x1u) == 0) {
    /* wait for the hardware ready bit */
}

volatile does not make code thread-safe: it gives no atomicity and no ordering between threads. Use C11 <stdatomic.h> or locks for that.

3. Describe the memory layout of a running C program.#

Show solution

From low to high addresses, typically:

SegmentContains
Text (code)Machine instructions; usually read-only
Read-only dataString literals, const globals
DataInitialised globals and static variables
BSSZero-initialised and uninitialised globals and static variables
Heapmalloc memory; grows upwards
StackFunction frames: locals, parameters, return addresses; grows downwards

The exact layout depends on the operating system and toolchain.

4. Stack versus heap?#

Show solution
StackHeap
AllocationAutomatic, on function entryManual: malloc / free
SpeedVery fast (move the stack pointer)Slower (the allocator searches for space)
SizeSmall, fixed (often 1–8 MB)Large
LifetimeUntil the function returnsUntil free
Typical errorsStack overflow from deep recursion or huge local arraysLeaks, double free, use after free

5. What is structure padding, and how can you reduce it?#

Show solution

Compilers align members to addresses that are multiples of their size, inserting unused bytes. Ordering members from largest to smallest usually reduces padding:

C
#include <stdio.h>

struct Bad { char a; double b; char c; };
struct Good { double b; char a; char c; };

int main(void) {
    printf("%zu %zu\n", sizeof(struct Bad), sizeof(struct Good));
    return 0;
}
Output
24 16

(Sizes from x86-64 GCC.) Compiler extensions such as __attribute__((packed)) remove padding, but unaligned access can be slower or even fault on some processors.

6. What is endianness? Write code to detect it.#

Show solution

Endianness is the order in which a multi-byte value's bytes are stored. Little-endian (x86, most ARM systems) stores the least significant byte first; big-endian stores the most significant byte first, and is also the standard "network byte order":

C
#include <stdio.h>
#include <stdint.h>

int main(void) {
    uint32_t x = 0x01020304;
    unsigned char *bytes = (unsigned char *)&x;
    printf("%02X %02X %02X %02X\n", bytes[0], bytes[1], bytes[2], bytes[3]);
    printf("%s-endian\n", bytes[0] == 0x04 ? "little" : "big");
    return 0;
}
Output
04 03 02 01
little-endian

This output is from an x86-64 machine. Inspecting an object's bytes through an unsigned char * is always allowed in C.

7. How do you swap the byte order of a 32-bit value?#

Show solution

Shift each byte to its new position and combine with OR:

C
#include <stdio.h>
#include <stdint.h>

uint32_t swap32(uint32_t x) {
    return (x >> 24) |
           ((x >> 8) & 0x0000FF00u) |
           ((x << 8) & 0x00FF0000u) |
           (x << 24);
}

int main(void) {
    printf("0x%08X\n", (unsigned)swap32(0x12345678u));
    return 0;
}
Output
0x78563412

On POSIX systems, htonl and ntohl convert between host and network byte order.

8. What are bit fields?#

Show solution

Struct members with an explicit width in bits, which pack several small values into one integer:

C
#include <stdio.h>

struct Flags {
    unsigned ready : 1;
    unsigned error : 1;
    unsigned mode  : 3;    /* 0..7 */
};

int main(void) {
    struct Flags f = { .ready = 1, .mode = 5 };
    printf("%u %u %u %zu\n", f.ready, f.error, f.mode, sizeof f);
    return 0;
}
Output
1 0 5 4

The order and layout of bit fields are implementation-defined, so for hardware registers and file formats, explicit masks and shifts are more portable.

9. What do inline and restrict do?#

Show solution
  • inline suggests that calls be replaced by the function body to avoid call overhead. In C it is only a hint, and static inline functions in headers are the usual safe pattern.
  • restrict on a pointer promises the object is accessed only through that pointer, which allows better optimisation. memcpy's parameters are restrict, which is why overlapping copies need memmove.

10. What are setjmp and longjmp?#

Show solution

setjmp saves the current execution context; a later longjmp jumps back to it, unwinding the stack without returning normally. Some C libraries use them for error recovery, similar to exceptions:

C
#include <setjmp.h>
#include <stdio.h>

static jmp_buf on_error;

void parse(int value) {
    if (value < 0) longjmp(on_error, 1);
    printf("parsed %d\n", value);
}

int main(void) {
    if (setjmp(on_error) == 0) {
        parse(5);
        parse(-1);
        printf("not reached\n");
    } else {
        printf("recovered from error\n");
    }
    return 0;
}
Output
parsed 5
recovered from error

Resources acquired between the two calls are not released automatically, and local variables changed after setjmp must be volatile to have reliable values. Use them sparingly.

11. Why is gets dangerous, and what replaced it?#

Show solution

gets reads a line into a buffer without knowing its size, so long input overflows the buffer, a classic security hole. It was removed in C11. Use fgets(buf, sizeof buf, stdin) and strip the trailing newline.

12. Embedded: how do you set, clear and test bit 5 of a register?#

Show solution
C
#include <stdio.h>
#include <stdint.h>

int main(void) {
    volatile uint8_t reg = 0x81;          /* stands in for a hardware register */
    reg |= (uint8_t)(1u << 5);            /* set */
    printf("0x%02X %u\n", reg, (reg >> 5) & 1u);
    reg &= (uint8_t)~(1u << 5);           /* clear */
    printf("0x%02X %u\n", reg, (reg >> 5) & 1u);
    return 0;
}
Output
0xA1 1
0x81 0

On real hardware the register is reached through a volatile pointer to its address, and a read-modify-write like |= may need interrupts disabled if an interrupt handler also writes that register.

13. What does this declaration mean: char ((*x)(void))[5]?#

Show solution

Read from the name outwards: x is a pointer to a function taking no arguments, returning a pointer to an array of 5 elements, each a pointer to char. In real code, build such types from typedefs:

C
typedef char *Row[5];          /* array of 5 char pointers */
typedef Row *(*Getter)(void);  /* pointer to function returning Row * */
Getter x;

Keep going#

Finished this lesson?