We’re Officially Live β€” Lifetime Founding Membership Available for a Limited Time
Free preview

C++ vs C for Embedded

What C++ adds over C on a microcontroller: classes, RAII, templates, references, and the zero-overhead principle that makes those abstractions free.

20 min read

In this lesson:


C++ is (mostly) C plus abstractions

You already know C. C++ began as "C with classes" and stays almost a superset: most valid C compiles as C++. On top of C it adds classes, templates, references, namespaces, operator overloading, constexpr, RAII, and a stronger type system.

The fear embedded engineers have, "C++ is bloated and slow", is mostly about features, not the language. Used as an embedded subset, C++ compiles to machine code as tight as hand-written C, while giving you safer, more expressive abstractions.


The zero-overhead principle

Bjarne Stroustrup's design rule for C++: what you don't use, you don't pay for; and what you do use, you couldn't hand-code any better. A class method isn't a heavyweight construct, it's a function with an implicit this pointer.

A GPIO toggle in C and in C++ compile to identical instructions:

/* C: a function operating on a struct */
typedef struct { volatile uint32_t *odr; uint32_t pin; } Led;
 
void led_toggle(Led *self) {
    *self->odr ^= (1u << self->pin);
}
 
led_toggle(&led);   /* explicit self */
// C++: a method on a class β€” same machine code
class Led {
    volatile uint32_t *odr_;
    uint32_t pin_;
public:
    void toggle() { *odr_ ^= (1u << pin_); }   // 'this' is the implicit self
};
 
led.toggle();       // 'this' passed implicitly

led.toggle() and led_toggle(&led) generate the same instructions, the C++ version just moves the self pointer from explicit argument to implicit this. No virtual dispatch, no overhead. That's the whole bargain: better organization, identical cost.


The embedded-appropriate subset

Not all of C++ is MCU-friendly. The split:

Use freely (zero/low overhead) Avoid on MCUs (cost or non-determinism)
Classes, methods, encapsulation Exceptions (throw/try), code size + non-determinism
RAII (deterministic cleanup) RTTI (dynamic_cast, typeid), metadata bloat
Templates (compile-time generics) Dynamic allocation (new/delete), fragmentation
References <iostream>, enormous code size
const / constexpr Most of std:: that allocates (std::vector, std::string)
enum class Heavy standard library / std::function

This module teaches the left column and dedicates a full lesson to why the right column is dangerous on constrained targets. The good news: the valuable abstractions (classes, RAII, templates) are exactly the cheap ones.


Compiling and linking: g++, mangling, extern "C"

C++ is compiled with g++ (or arm-none-eabi-g++) and source files end in .cpp. The pipeline is the same four stages as C, with one twist that bites at link time: name mangling.

Because C++ allows overloading (two functions named init with different parameters), the compiler encodes the parameter types into the symbol name, init(int) might become _Z4initi. C does no such thing; its symbol is just init. So a C++ file calling a C function, or vice versa, needs extern "C" to disable mangling for that interface:

// In a C++ file, tell the compiler these use C linkage (no mangling),
// so they link against C object files and the linker can find them.
extern "C" {
    #include "uart.h"        // C header
    void HardFault_Handler(void);   // ISR the C startup code references by name
}

This is essential in real firmware: vendor HALs, RTOS kernels, and startup code are usually C, and your extern "C" wrappers let C++ application code link against them. Interrupt handlers in particular must keep their unmangled names so the vector table (C) finds them.


Stricter types catch more bugs

C++ tightens several of C's loose rules, which means some valid C is rejected by C++, usually because it was risky:

// 1. void* does NOT implicitly convert to a typed pointer in C++.
int *p = malloc(n * sizeof(int));   // ERROR in C++ (OK in C)
int *p = static_cast<int*>(malloc(n * sizeof(int)));  // explicit cast required
 
// 2. 'bool' is a real type, not a typedef'd int.
bool ready = true;                  // distinct type, size 1
 
// 3. enum class is strongly typed β€” no implicit int conversion.
enum class Mode { Idle, Run };
Mode m = Mode::Run;                 // can't accidentally compare to 7
 
// 4. stricter const rules and function prototypes

These catch real defects at compile time, the kind of implicit-conversion bugs that slip through C. The tradeoff is you write a few more casts; the payoff is the compiler rejecting nonsense.


Gotchas


TL;DR

This is just the start

Sign up free to track progress on this lesson, get coding problems with the Run button, and unlock the full interview Q&A.

More in C++ for Embedded

Classes & EncapsulationConstructors, Destructors & RAII
References vs Pointers
const Correctness & constexpr
Function & Operator Overloading
Templates: The Basics