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.
- 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
- 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
01 What do -fno-exceptions and -fno-rtti do?
02 Why must an interrupt handler written in C++ be extern "C"?
03 Can a reference be null, or be moved to another variable?
04 What is the only difference between struct and class?
05 In what order are members initialised?
06 What does RAII guarantee?
07 What is wrong with BusLock(); on a line of its own?
08 What does a class need if its destructor gives something back?
09 What does a virtual function cost?
10 Why call a function from a constructor with care?
11 constexpr or consteval?
12 Where does a constexpr table go on a chip?
13 What does std::array::at() do with a bad index in a -fno-exceptions build?
14 Which lambdas fit a plain C callback?
15 What does std::move do by itself?
16 What makes a class testable on a PC?
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."
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].
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
- The subset: classes, references, templates and constexpr freely; no exceptions, RTTI or heap.
- The measured costs: most features compile to the same code as C, and a few have a cost you can now name.
- Thirty questions, each answered with an idea, a reason and a number.
- Reading code: aliasing references, construction order, virtual calls in constructors, sizes and captures.
Key words from this volume
Every word below has a plain-English entry in the glossary.
- Reference
- extern "C"
- Class
- RAII
- Rule of zero
- Virtual function
- constexpr
- Static initialisation order
- Layered architecture
- Unit test
Practice
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.
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
- Make
bufferprivate, and consider astd::arraymember instead of a pointer, so nothing outside the class can corrupt it. - 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. - Make the constructor
constexprand the globalconstinitif 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
Ten one-line answers
Answer each in one line.
- Which flags switch off exceptions and RTTI?
- What does a const member function promise?
- What does RAII stand for?
- What does = delete do?
- What makes a class abstract?
- What does static_assert cost at run time?
- Where does a constexpr table live?
- What does std::span hold?
- What does std::move do by itself?
- What does a CI server look at to decide a build failed?
Show the solution
- The flags -fno-exceptions and -fno-rtti.
- Not to change the object.
- Resource acquisition is initialisation.
- It makes any use of that function a build error.
- At least one pure virtual function.
- Nothing.
- In flash, as read-only data.
- A pointer and a length.
- Nothing at run time: it is a cast.
- 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.