Volume 10 Intermediate 3 sub-modules ~25 min read

Embedded C++ Interview Vault

The last volume turns the course into interview preparation. It gathers every rule and every measured number onto one revision sheet, answers thirty questions that embedded C++ interviews really ask, and ends with code-reading drills - each answer produced by compiling and running the code.

You will learn
  • The rules and the measured costs of the whole course, on one sheet
  • Model answers to thirty embedded C++ interview questions
  • How to read code for aliasing, construction order, virtual calls and sizes
You need
  • The rest of this course. Each answer points back to the volume that explains it.

10.1 Revision sheet

The whole course on a few screens: every rule worth remembering, and every number this course measured rather than repeated from rumour.

The rules

Topic The rule Volume
The subset Use classes, references, templates and constexpr freely; switch off exceptions and RTTI 00
References Pass structs by const reference; use a reference when "nothing" is not a valid answer 01
extern "C" Every interrupt handler and every function shared with C needs a plain C name 01
Classes Keep data private; mark every read-only member function const 02
Constructors Mark one-argument constructors explicit; declare members in the order they are set 02
RAII Give every resource to a guard object; a guard needs a name 03
Copying A class that gives something back deletes its copies, or writes them; aim for the rule of zero 03
Virtual Use it when the choice is made while running; mark overrides override; destructors virtual 04
Compile time Prefer constexpr tables and static_assert checks to start-up code and comments 05
Memory No heap: static storage, std::array, fixed-capacity containers, placement new 06
Start-up Globals shared between files are constinit, or set up on first use 06
Modern C++ enum class, lambdas for callbacks, optional, span, moves, [[nodiscard]] 07
Structure Layers, with registers only in the lowest; hand classes their hardware 08
Testing Test logic on a PC with fakes; let CI run every test on every change 09

The numbers

Every number below was measured by this course's harness, with g++ 15.2 on a 64-bit PC. A 32-bit microcontroller gives different absolute sizes - its pointers are 4 bytes, not 8 - but the comparisons hold.

What was measured Result Volume
Exceptions switched on, one small function 152 bytes more: 12 of code, 140 of tables 00
RTTI for three small classes 6 type-information objects, 97 bytes 00
std::cout against printf, statically linked 1,877,656 bytes of code against 699,251 00
--gc-sections on a program with one unused function 586 bytes removed 00
A reference against a pointer The same 4 bytes of machine code 01
static_cast against a C-style cast The same 13 bytes of machine code 01
A member function against a C function taking a struct pointer The same 5 bytes of machine code 02
An RAII lock against unlocks written on each path 39 bytes against 44: smaller 03
A class with virtual functions A hidden 8-byte pointer per object, a 40-byte vtable per class 04
A virtual call, against final, plain and CRTP An indirect jump, against the answer itself 04
A typed register field against a hand-written mask The same 11 bytes of machine code 05
A constexpr 256-byte table against one built at start-up 256 bytes of flash, against 497 of flash and 256 of RAM 05
A static local with a run-time constructor 131 bytes of text, 90 with -fno-threadsafe-statics 06
std::function holding a lambda with 20 bytes of captures 32 bytes, and a call to operator new 07
std::optional of int16_t, and std::span of bytes 4 bytes, and 16 bytes 07

Flashcards

16 cards
01 What do -fno-exceptions and -fno-rtti do?
Switch exceptions and run-time type information off, and turn any use of throw, try, dynamic_cast or typeid into a build error.
02 Why must an interrupt handler written in C++ be extern "C"?
Without it the name is mangled, the vector table finds only the weak default handler, and your handler never runs - with no warning.
03 Can a reference be null, or be moved to another variable?
No and no. It is set once, to a real variable, for its whole life.
04 What is the only difference between struct and class?
The starting access: public for struct, private for class.
05 In what order are members initialised?
The order they are declared in the class, whatever order the initialiser list uses.
06 What does RAII guarantee?
A resource taken in a constructor is given back by the destructor on every way out of the scope, early returns included.
07 What is wrong with BusLock(); on a line of its own?
It makes a temporary that is destroyed at the end of that line, before the work it should protect.
08 What does a class need if its destructor gives something back?
A decision about copying: usually delete the copy constructor and copy assignment (the rule of three).
09 What does a virtual function cost?
A hidden pointer in each object, a vtable per class, and an indirect call that cannot be inlined.
10 Why call a function from a constructor with care?
During construction the object is only its base part, so a virtual call runs the base version.
11 constexpr or consteval?
constexpr may run while compiling or while running; consteval must run while compiling.
12 Where does a constexpr table go on a chip?
Into read-only data in flash, with no RAM and no start-up code.
13 What does std::array::at() do with a bad index in a -fno-exceptions build?
The library still throws, nothing catches it, and std::terminate aborts the program.
14 Which lambdas fit a plain C callback?
Only those that capture nothing.
15 What does std::move do by itself?
Nothing at run time: it is a cast that allows the move constructor or move assignment to be chosen.
16 What makes a class testable on a PC?
Being handed its hardware through its constructor, so a test can hand it fakes.
Quick check

Which of these did the course measure as costing nothing at all when the program runs?

Show the answer

Answer: D. A reference and a pointer compiled to the same 4 bytes of machine code. Exceptions added 152 bytes even with nothing thrown, a virtual call is indirect, and std::function was 32 bytes and called operator new.

10.2 30 interview questions

Thirty questions that embedded C++ interviews really ask. Say your answer out loud before you open each model answer - explaining is the skill being tested.

Each answer is short on purpose: an interviewer wants the idea, a reason, and one piece of evidence. Where a number appears, it is one this course measured.

From C to C++

1. Why would you use C++ instead of C on a microcontroller?

"The same machine code for the same work, with more checking. Classes keep data private, references cannot be null, RAII releases resources on every path, templates and constexpr move work to compile time, and enum class stops type mix-ups. I switch off exceptions and RTTI and avoid the heap, so the program stays small and predictable."

2. What is the difference between a pointer and a reference?

"A reference is another name for an existing variable: set once, never null, never moved, used without *. A pointer is a variable holding an address: it can be null, changed and used for arithmetic. Compiled, a reference parameter is passed as an address, so the machine code is the same."

3. What does extern C do, and where does firmware need it?

"It gives a function its plain C name instead of a mangled C++ one. Firmware needs it for interrupt handlers, which the vector table finds by name, and for code shared with C. A handler without it links cleanly and never runs, because the weak default handler keeps the plain name."

4. Why nullptr rather than NULL?

"NULL is an integer zero underneath, so it can pick an int overload or be ambiguous. The keyword nullptr has its own type, which becomes any pointer and never a number."

5. When would you not use auto?

"When the exact type matters. Adding two uint8_t values with auto gives an int holding 300, not a byte that wraps to 44. For register values, protocol fields and anything whose width matters, I write the type."

6. Why are C-style casts discouraged?

"A C cast can do any conversion without saying which. The C++ casts each do one kind, so the compiler refuses the wrong kind and reviewers can search for the risky ones. They compile to the same code - I measured the same 13 bytes as a C cast."

Classes and RAII

7. What is the difference between struct and class?

"Only the starting access: public for struct, private for class. I use struct for plain data and class for a type with rules to keep."

8. How does a member function know which object it belongs to?

"The object's address is passed as a hidden argument called this. A member function compiles like a C function taking a struct pointer - the same 5 bytes in the course's test."

9. Why mark a member function const?

"It promises not to change the object, the compiler enforces it, and only const functions can be called through a const reference or on a const object."

10. How can a constructor report an error without exceptions?

"It cannot, so I design around it. I can make the object harmless when its arguments are bad, or split construction from an init() that returns a status. Or I can use a factory that returns an optional."

11. What is RAII, and where does firmware use it?

"Taking a resource in a constructor and giving it back in the destructor, so every path out of a scope releases it. I use it for interrupt locks, chip selects, bus locks and peripheral clocks."

12. How would you write a critical-section guard for a Cortex-M?

"Save PRIMASK and disable interrupts in the constructor, write the saved value back in the destructor, and delete the copy functions. It must restore rather than enable, because critical sections nest."

13. Explain the rule of zero, three and five.

"If a class needs its own destructor, it needs a decision about copying - that is three. Moves add two more - five. Best of all is zero: build classes from members that already behave correctly, and write none."

Polymorphism

14. What does a virtual function cost?

"A hidden pointer per object, a vtable per class in flash, and an indirect call that cannot be inlined unless the compiler can prove the type. I measured an 8-byte pointer on a PC and a 40-byte vtable for a small class."

15. Why does a base class need a virtual destructor?

"If an object is destroyed through a pointer to its base, a non-virtual destructor skips the derived part's clean-up, which is undefined behaviour. The -Wnon-virtual-dtor warning enforces it."

16. What does override protect you from?

"A function that looks like a replacement but does not match - a missing const, say. With override, that is a build error instead of a silent second function."

17. What happens if a constructor calls a virtual function?

"The base-class version runs. While the base part is being built, the object is only a base object, so the derived override is not yet reachable."

18. What is CRTP?

"A class inheriting from a template of itself, so the base can call the derived class directly. It shares code without a vtable or hidden pointer, and every call can be inlined. The cost is no common base type."

19. How do you replace dynamic_cast when RTTI is off?

"Usually by redesigning, so the base class offers the function the caller needs. When the kind really must be asked, a virtual function returning an enum class does it at the cost of one vtable entry."

Compile time and memory

20. Do templates bloat code?

"They cost one copy per type used, the same as writing those functions by hand. The risk is a big template used with many types; I keep templates small and move type-independent code out of them."

21. What is the difference between const, constexpr and consteval?

"A const value cannot change once set, but may be set while running. A constexpr one can be worked out while compiling, and a consteval function must be. A constexpr table lands in flash with no RAM or start-up code."

22. What would you check with static_assert?

"Register layouts against the datasheet, template parameters such as power-of-two sizes, and calculations such as baud-rate error - all for free, on every build."

23. How do you avoid the heap in C++ firmware?

"Static storage and the stack only: std::array, fixed-capacity containers, placement new or std::optional for objects built later, and status values instead of exceptions. The map file shows every byte."

24. What is the static initialisation order problem?

"Globals that need start-up code are set up file by file in no fixed order, so one can read another while it is still zero. Use constinit or constexpr for shared globals, or a function with a static local."

25. What does a static local object cost?

"A guard variable, and by default calls that make its first-time construction thread-safe. The course measured 131 bytes of text, 90 with -fno-threadsafe-statics - which is only safe if nothing can call it concurrently."

Modern C++ and design

26. Why enum class rather than enum?

"Scoped names, no quiet conversion to int, and a chosen size. With -Wall, a switch that misses a value is flagged, so adding a state finds every switch to update."

27. Why avoid std::function in firmware?

"It is 32 bytes, and it calls operator new when the callable does not fit inside it. A template parameter or a plain function pointer costs nothing, and a lambda that captures nothing fits a C callback."

28. When would you use std::optional, and when a status code?

"optional when the only failure is 'no value'. A status when there are several failures to tell apart, such as timeout and bad checksum. Either way, mark it nodiscard."

29. How would you design a HAL for testability?

"Layers, with registers only in the lowest. Classes are handed their hardware through their constructors, and a board file wires the real objects. Tests wire fakes, so logic runs on a PC."

30. Walk me through an interrupt-driven UART transmit path.

"The write function copies bytes into a ring buffer inside a critical section, and enables the TXE interrupt. Each TXE interrupt moves one byte to the data register. When the buffer is empty the handler disables TXE, or it fires for ever. And the write function must enable the interrupt even when the buffer fills part-way."

Quick check

An interviewer asks what a virtual function costs. Which answer covers it?

Show the answer

Answer: B. Those three are the whole cost, and the course measured each one. Code is never copied into objects, no heap is involved, and the compiler removes a virtual call only when it can prove the type - with final, for example.

10.3 Code-reading drills

Interviews often hand you code and ask what it does. Read each program, decide what it prints - or why it will not compile - and only then open the answer.

Every answer was produced by compiling and running the program with the course's flags.

Drill 1: two names for one variable


// drill_alias.cpp - drill: two references to the same variable
#include <cstdio>

static void bump(int &a, int &b) {
    a = a + 1;
    b = b + 10;
}

int main() {
    int x = 1;
    bump(x, x);
    std::printf("x = %d\n", x);
    return 0;
}
Answer

x = 12

Both references name x, so the first line makes it 2 and the second makes it 12. Functions that take two references and assume they are different are a classic source of this surprise.

Drill 2: the order of construction


// drill_order.cpp - drill: a base class, a member, and the class itself
#include <cstdio>

struct Buffer {
    Buffer() { std::printf("buffer made\n"); }
    ~Buffer() { std::printf("buffer gone\n"); }
};

struct Driver {
    Driver() { std::printf("driver base made\n"); }
    ~Driver() { std::printf("driver base gone\n"); }
};

struct UartDriver : Driver {
    Buffer rx;
    UartDriver() { std::printf("uart driver made\n"); }
    ~UartDriver() { std::printf("uart driver gone\n"); }
};

int main() {
    UartDriver uart;
    return 0;
}
Answer

driver base made
buffer made
uart driver made
uart driver gone
buffer gone
driver base gone

The base part first, then the members, then the class's own constructor body. Destruction runs in exactly the reverse order.

Drill 3: a virtual call during construction


// drill_ctor_virtual.cpp - drill: a virtual call made inside a constructor
#include <cstdio>

struct Sensor {
    Sensor() { std::printf("while constructing: %s\n", name()); }
    virtual ~Sensor() = default;
    virtual const char *name() const { return "Sensor"; }
};

struct Thermometer : Sensor {
    const char *name() const override { return "Thermometer"; }
};

int main() {
    Thermometer t;
    std::printf("afterwards: %s\n", t.name());
    return 0;
}
Answer

while constructing: Sensor
afterwards: Thermometer

While Sensor's constructor runs, the Thermometer part does not exist yet, so the virtual call goes to Sensor::name. Afterwards the same call reaches the override. Never rely on a derived class's behaviour from a base constructor.

Drill 4: a static local


// drill_static.cpp - drill: a static local, called three times
#include <cstdio>

static int next_id() {
    static int id = 100;
    id = id + 1;
    return id;
}

int main() {
    int a = next_id();
    int b = next_id();
    int c = next_id();
    std::printf("%d %d %d\n", a, b, c);
    return 0;
}
Answer

101 102 103

The static local is set to 100 once, on the first call, and keeps its value between calls. The three calls are on separate lines on purpose: inside one printf, the order the arguments are worked out in is not fixed.

Drill 5: what makes an object bigger?


// drill_sizes.cpp - drill: what does each of these add to an object?
#include <cstdint>
#include <cstdio>

struct Plain {
    uint8_t x;
};

struct WithFunction {
    uint8_t x;
    uint8_t get() const { return x; }
};

struct WithStatic {
    uint8_t x;
    static inline uint32_t count = 0;
};

struct WithVirtual {
    uint8_t x;
    virtual ~WithVirtual() = default;
};

int main() {
    std::printf("Plain %zu, WithFunction %zu, WithStatic %zu, WithVirtual %zu\n", sizeof(Plain),
                sizeof(WithFunction), sizeof(WithStatic), sizeof(WithVirtual));
    return 0;
}
Answer

Plain 1, WithFunction 1, WithStatic 1, WithVirtual 16

Member functions and static members are not stored in objects, so the first three are all 1 byte. The virtual destructor adds the hidden vtable pointer - 8 bytes on this PC - and padding rounds the object up to 16.

Drill 6: why will this not compile?


// drill_capture.cpp - drill: why does this lambda not compile?
int count_presses() {
    int presses = 0;
    auto on_press = [presses]() { presses = presses + 1; };
    on_press();
    return presses;
}
Answer

drill_capture.cpp: In lambda function:
drill_capture.cpp:4:43: error: assignment of read-only variable 'presses'

A capture by value is a copy, and a lambda's copies are read-only unless it is marked mutable. Even if it were, it would change the copy, and count_presses would still return 0. The fix is to capture by reference: [&presses].

Quick check

In drill 3, why does the constructor print "Sensor" rather than "Thermometer"?

Show the answer

Answer: C. During the base constructor, the derived part does not exist yet, so a virtual call reaches the base version. The same call made afterwards printed "Thermometer".

What you learned

Key words from this volume

Every word below has a plain-English entry in the glossary.

Practice

Practice 1

Explain it in one minute

Pick three questions from the second sub-module that you found hardest. For each, say your answer out loud in under a minute, then compare it with the model answer. What did you leave out?

Show the solution

Most first attempts leave out the evidence. "Virtual functions are slow" is weaker than "a virtual call is an indirect jump that cannot be inlined, plus a hidden pointer per object and a vtable per class". The course has measured almost everything it claims, so you can quote a number - that is what makes an answer convincing.

Practice 2

Review this code

A colleague's driver has class Uart { public: Uart(); void write(uint8_t b); uint8_t *buffer; };, a global Uart console; in one file, and void USART2_IRQHandler() { console.handle(); } in a .cpp file. List three things you would ask them to change, with a reason for each.

Show the solution
  1. Make buffer private, and consider a std::array member instead of a pointer, so nothing outside the class can corrupt it.
  2. Add extern "C" to the handler. Without it, its name is mangled, the vector table keeps the weak default, and the handler never runs - with no warning.
  3. Make the constructor constexpr and the global constinit if it can be, so the driver is ready before any start-up code runs and cannot be caught by the initialisation-order problem.

Rapid fire

Interview question 1

Ten one-line answers

Answer each in one line.

  1. Which flags switch off exceptions and RTTI?
  2. What does a const member function promise?
  3. What does RAII stand for?
  4. What does = delete do?
  5. What makes a class abstract?
  6. What does static_assert cost at run time?
  7. Where does a constexpr table live?
  8. What does std::span hold?
  9. What does std::move do by itself?
  10. What does a CI server look at to decide a build failed?
Show the solution
  1. The flags -fno-exceptions and -fno-rtti.
  2. Not to change the object.
  3. Resource acquisition is initialisation.
  4. It makes any use of that function a build error.
  5. At least one pure virtual function.
  6. Nothing.
  7. In flash, as read-only data.
  8. A pointer and a length.
  9. Nothing at run time: it is a cast.
  10. The exit status of each step.

The end of the course

You started with the claim that C++ can make firmware safer without making it bigger or slower, if you know which parts to use. Every volume since has tested that claim with a compiler, and the numbers are in the first sub-module of this one.

The natural next steps are the other courses in the Academy. Embedded C from Zero goes deeper into the hardware itself. And the planned Microcontrollers and RTOS course will put these drivers to work under a real-time operating system.