The C++ language
C++ adds to C the ability to model objects: an LED, a sensor or a motor stop being a loose collection of variables and become entities that keep their data and their operations together. It is the language of Arduino and of a good part of the modern embedded industry.
01What C++ adds to C
C++ is, almost entirely, a superset of C: a C program compiles as C++. What it adds is a different way of organizing large programs.
| Feature | What it is for | Does it cost resources? |
|---|---|---|
| Classes and objects | Group data together with the functions that manipulate it. | No |
| Constructors / destructors | Guarantee that an object is born initialized and releases whatever it took. | No |
| Overloading | Several functions with the same name and different parameters. | No |
References (&) | Pass-by-reference that is more readable than with pointers. | No |
| Inheritance | Reuse and specialize existing code. | No |
| Virtual functions | Polymorphism: choose the implementation at run time. | Little |
| Templates (generic programming) | Generic code that works for any type. | Little |
| Exceptions | Error handling kept separate from the normal flow. | Yes |
| Standard library (STL) | Ready-made containers and algorithms. | Yes |
This is the C++ design principle, and it explains why the language is used in embedded systems. Classes, single inheritance and constructors generate exactly the same machine code as the equivalent C version: they are organization, not layers of runtime. What does cost something is exceptions, run-time type information and the STL with dynamic allocation — and those three are disabled in most microcontroller projects.
02Classes and objects
A class is a mold: it describes what data a thing has and what can be done with it. An object is a concrete instance created from that mold.
class Led {
private: // nobody outside can touch this
uint8_t pin;
bool encendido;
public: // the visible interface
Led(uint8_t p); // constructor
void on();
void off();
void toggle();
bool estado() const; // const: does not modify the object
};
// --- implementation ---
Led::Led(uint8_t p) : pin(p), encendido(false) {
pinMode(pin, OUTPUT);
digitalWrite(pin, LOW);
}
void Led::on() { digitalWrite(pin, HIGH); encendido = true; }
void Led::off() { digitalWrite(pin, LOW); encendido = false; }
void Led::toggle() { encendido ? off() : on(); }
bool Led::estado() const { return encendido; }
// --- usage ---
Led rojo(13); // two independent objects are created
Led verde(12); // each with its own pin and its own state
rojo.on();
verde.toggle();
The data goes in private and only the functions of the class can touch it. The rest
of the program communicates exclusively through the public interface. The concrete advantage: if
tomorrow the way the state is represented internally changes, nothing outside breaks, because
nobody depended on those details. And it is impossible to leave the object in an inconsistent state from
outside.
structA struct groups data; a class groups data and behavior, and also
controls access. In C you would write led_on(&rojo), passing the structure as the
first parameter; in C++ you write rojo.on() and that pointer to the object (called
this) is passed by the compiler on its own. It is literally the same call, with better
syntax.
03Constructors and destructors
It runs automatically when the object is created. It has the same name as the class and returns nothing. Its job is to leave the object ready to use.
There can be several (overloaded) with different parameters.
It runs automatically when the object is destroyed: on leaving its scope or when it is freed.
It is named ~Class() and takes no parameters.
Its job is to give back whatever the object has taken: memory, a file, a port.
class PuertoSerie {
int fd;
public:
PuertoSerie(uint32_t baudios) {
fd = abrir_uart(baudios); // takes the resource
}
~PuertoSerie() {
cerrar_uart(fd); // gives it back, no matter what
}
void enviar(const char *s);
};
void tarea() {
PuertoSerie p(9600); // constructor: opens
p.enviar("hello");
if (error) return; // even when leaving here...
...
} // ...the destructor closes the port
Resource Acquisition Is Initialization: the resource is acquired in the constructor and released in
the destructor. Because the destructor always runs when leaving the scope —by any
path— it becomes impossible to forget to release. In C, by contrast, every early return
is a chance to leave a resource held. In embedded systems it is applied to ports, SPI or I²C buses, and to
critical sections: an object that disables interrupts in the constructor and restores them in the
destructor makes it impossible to forget to re-enable them.
04Overloading
In C++ several functions can coexist with the same name if they differ in the number or the type of their parameters. The compiler picks the right one depending on how it is called.
void mostrar(int v);
void mostrar(float v);
void mostrar(const char *s);
void mostrar(int v, uint8_t base);
mostrar(42); // calls the first one
mostrar(3.14f); // the second
mostrar("done"); // the third
mostrar(255, 16); // the fourth: in hexadecimal
In C you would need four different names (mostrar_int,
mostrar_float…). This is exactly what Arduino’s Serial.print() does: it
accepts integers, decimals and strings, because it is really several overloaded functions.
Operator overloading
class Medicion {
int16_t mV;
public:
Medicion(int16_t v) : mV(v) {}
Medicion operator+(const Medicion &o) const {
return Medicion(mV + o.mV);
}
bool operator>(const Medicion &o) const { return mV > o.mV; }
};
Medicion a(1200), b(340);
Medicion c = a + b; // reads like math
if (a > b) { ... }
Overloading operators makes sense when the operation is obvious for that type: adding two
measurements, comparing two times, multiplying two matrices. Overloading + to do
something unexpected makes the code indecipherable. The rule is: if someone reading a + b
could be mistaken about what it does, do not overload it.
05Inheritance
A class can derive from another: it inherits its data and methods, and adds or modifies whatever it needs. It is the way to express an “is a” relationship.
leer(), and
which of the three versions runs is decided by the real object, not by the pointer type.class Sensor { // base class
protected: // visible to the derived classes
uint8_t pin;
public:
Sensor(uint8_t p) : pin(p) {}
virtual float leer() = 0; // = 0 : PURE virtual
virtual ~Sensor() {} // virtual destructor: mandatory
};
class SensorTemp : public Sensor { // derives from Sensor
public:
SensorTemp(uint8_t p) : Sensor(p) {}
float leer() override { // LM35: 10 mV per degree
return analogRead(pin) * 500.0f / 1024.0f;
}
};
| Visibility | Who can access |
|---|---|
private | Only the class itself. It is the default. |
protected | The class itself and its derived classes. |
public | Anyone. |
06Polymorphism and interfaces
It is the powerful consequence of inheritance: you can write code that works with the base class and works correctly with any derived class, even those that do not exist yet.
// This function neither knows nor cares which sensors there are
void registrar(Sensor *lista[], uint8_t n) {
for (uint8_t i = 0; i < n; i++) {
float v = lista[i]->leer(); // calls the CORRECT leer()
guardar(i, v);
}
}
SensorTemp t(0);
SensorLuz l(1);
SensorHum h(2);
Sensor *todos[] = { &t, &l, &h };
registrar(todos, 3); // each one is read the way it should be
A class with virtual functions carries a hidden pointer to a table of virtual
functions (vtable). When you call lista[i]->leer(), the program looks up that
table at run time and jumps to the implementation of the real object. The cost is one pointer per
object and one indirection per call: perfectly affordable even on an
8-bit microcontroller.
virtualIf a derived object is destroyed through a pointer to the base and the destructor is not virtual, only the base destructor runs: the derived one never runs and its resources stay held. It is a classic, silent error. The rule is simple: if a class has any virtual function, its destructor must be virtual too.
Interfaces: pure abstract classes
A class whose methods are all pure virtual (= 0) cannot be instantiated:
it defines only a contract. It is the mechanism used to declare “what a driver has to
be able to do,” without saying anything about how.
class IDisplay { // interface: just the contract
public:
virtual void limpiar() = 0;
virtual void escribir(uint8_t f, uint8_t c, const char *t) = 0;
virtual ~IDisplay() {}
};
class LCD1602 : public IDisplay { /* 4-bit parallel */ };
class OLED_I2C : public IDisplay { /* over the I2C bus */ };
class DisplayFalso : public IDisplay { /* prints to the console: for testing */ };
// The menu works with any of the three, without changing a line
void menu(IDisplay &d) {
d.limpiar();
d.escribir(0, 0, "Temperature");
}
It lets you write all the logic of the device before you have the hardware, testing it against a fake driver. And it lets you switch from an LCD to an OLED, or from one microcontroller to another, by touching a single class. It is the C++ version of the principle of separation into drivers seen in the previous topic.
07Exceptions
They let you separate error handling from the normal flow of the program: instead of every function returning an error code that has to be checked, an exception is thrown and caught wherever appropriate.
try {
float t = sensor.leer(); // may throw
procesar(t);
}
catch (const SensorDesconectado &e) {
alarma("sensor offline");
}
catch (...) { // any other
reiniciar();
}
Exceptions require stack-unwinding tables that take up several KB of flash, and their propagation
time is unbounded, which makes them unacceptable in real time. That is why almost
all embedded projects compile with -fno-exceptions and handle errors as in
C: with return codes or with an object that represents “value or error.” On a PC, by contrast,
exceptions are the normal and recommended way to handle errors.
08C++ on microcontrollers
- Classes, encapsulation, methods.
- Constructors and destructors (RAII).
- Function and operator overloading.
- References instead of pointers.
constyconstexpr.- Single inheritance and virtual functions, with judgment.
- Templates, as long as the number of instances does not explode.
enum classfor states and modes.
- Exceptions (
-fno-exceptions). - RTTI and
dynamic_cast(-fno-rtti). newanddelete: same reason asmalloc, RAM gets fragmented.std::vector,std::stringand, in general, the STL with dynamic memory.- Multiple inheritance and deep hierarchies.
iostream: adds tens of KB.
Even though it is not presented that way, the Arduino environment compiles C++ with avr-g++. Each
library is a class: Serial is an object of the class HardwareSerial,
Servo, LiquidCrystal and Wire are classes, and
Serial.print() is overloaded for all types. The .ino files are
concatenated, a main() is added that calls setup() and then
loop() inside a while(1), and everything is compiled as C++. Recognizing this lets you go
from “using Arduino” to “programming the microcontroller.”
09In the lab
Write a Contador (counter) class with a private value and the methods
incrementar(), decrementar(), reiniciar() and
valor(), with a maximum limit passed through the constructor. Create three objects with different
limits and check that each one keeps its own state. Try to access the private data from
outside and read the compiler error: that is encapsulation.
A Recurso (resource) class whose constructor prints “acquired” and whose destructor prints
“released.” Create objects inside { } blocks and at different points of a function with
several return statements. Observe the exact order in which the destructors run: always on
leaving the scope, and in the reverse order of creation.
An abstract base class Figura (shape) with virtual float area() = 0; and the derived classes
Circulo (circle), Rectangulo (rectangle) and Triangulo (triangle). Store pointers to the three in
an array of Figura* and loop through it adding up areas. Then remove the word
virtual and run it again: the base method is always called. It is the direct demonstration
of what virtual is for.
Define IDisplay as in the example and write two implementations: one that prints
to the console and another that simulates an LCD by drawing a 16×2-character box. Write a function
menu(IDisplay&) and run it with both, without modifying it. It is a direct rehearsal of
how device software is developed before the hardware is available.
10Common mistakes
| Mistake | Consequence |
|---|---|
Forgetting the ; after the closing brace of a class | A cascade of incomprehensible compile errors on the following lines. |
Base destructor not virtual | When destroying through a pointer to the base, the derived destructor does not run: a silent resource leak. |
Forgetting virtual on the method that is meant to be polymorphic | The base-class version is always called. The program compiles and does what you do not want. |
| Members not initialized in the constructor | Garbage values. It is best to use the initializer list (: pin(p), estado(false)). |
Using new without delete | Memory leak. On a micro with 2 KB of RAM, it runs out in minutes. |
| Building with the STL on a small microcontroller | The program does not fit in flash, or it fits and fails when RAM runs out. |
| Overloading operators in a non-intuitive way | Code that is impossible to read. Overloading is justified only if the meaning is obvious. |
Making everything public | Encapsulation is lost, and with it the whole advantage of using classes. |
11Self-assessment
What is the difference between a class and an object?
The class is the mold: it describes what data and methods an entity has. The
object is a concrete instance created from that mold, with its own values. From a
Led class you can create many objects, each with its own pin and state.
What does a program gain by declaring its data as private?
That nobody outside can leave the object in an inconsistent state, and that the internal representation can be changed without breaking the rest of the program, because nobody depended on it. That is encapsulation.
When does a destructor run, and what is it for?
Automatically, when the object is destroyed: on leaving the scope where it was created, or on
calling delete. It is used to release whatever the object has taken (memory, a port,
a file). Because it runs on every exit path, it makes forgetting impossible.
What is RAII and what problem does it solve?
Resource Acquisition Is Initialization: acquiring the resource in the constructor and releasing it in the destructor. It solves the problem of forgotten releases, especially when a function has several exit points.
What does the keyword virtual do?
It indicates that the method can be overridden in a derived class, and that the decision of which one to run is made at run time according to the real type of the object, not the type of the pointer. It is what makes polymorphism possible.
What is a pure virtual function, and what effect does it have on the class?
It is one declared with = 0: it has no implementation in the base. It makes
the class abstract, meaning it cannot be instantiated, and forces every concrete
derived class to implement it. If all of its methods are pure virtual, the class is an interface.
Why must the destructor of a base class be virtual?
So that, when a derived object is destroyed through a pointer to the base, the derived destructor runs as well. If it is not virtual, only the base one runs and the resources of the derived class are left unreleased.
Why are exceptions disabled on microcontrollers?
Because they require stack-unwinding tables that take up several KB of flash and
because their propagation time is unbounded, which makes them incompatible with real-time
requirements. You compile with -fno-exceptions and handle errors with return
codes.
What language is an Arduino program written in?
In C++. The environment concatenates the .ino files, adds a
main() that calls setup() and then loop() inside a
while(1), and compiles everything with avr-g++. Serial,
Servo and LiquidCrystal are C++ classes.