Catto / Topic Map · Computing for Electronics I Year 4
Computing for Electronics I · 72 h · Topic 3 of 3

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.

Programming Classes and objects Inheritance Polymorphism Drivers

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.

FeatureWhat it is forDoes it cost resources?
Classes and objectsGroup data together with the functions that manipulate it.No
Constructors / destructorsGuarantee that an object is born initialized and releases whatever it took.No
OverloadingSeveral functions with the same name and different parameters.No
References (&)Pass-by-reference that is more readable than with pointers.No
InheritanceReuse and specialize existing code.No
Virtual functionsPolymorphism: choose the implementation at run time.Little
Templates (generic programming)Generic code that works for any type.Little
ExceptionsError handling kept separate from the normal flow.Yes
Standard library (STL)Ready-made containers and algorithms.Yes
“You don’t pay for what you don’t use”

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

rojoprivate datapin = 8estado = falsepublic functionsencender() apagar()verdeprivate datapin = 9estado = falsepublic functionsencender() apagar()Led rojo(8), verde(9);Two objects of the same class. Each one has its own copy of the data.rojoprivate datapin = 8estado = truepublic functionsencender() apagar()verdeprivate datapin = 9estado = falsepublic functionsencender() apagar()rojo.encender();The function works on the data of rojo, unaware that verde exists.rojoprivate datapin = 8estado = truepublic functionsencender() apagar()verdeprivate datapin = 9estado = truepublic functionsencender() apagar()verde.encender();Now both are on, and each object still remembers its own state.rojoprivate datapin = 8estado = falsepublic functionsencender() apagar()verdeprivate datapin = 9estado = truepublic functionsencender() apagar()rojo.apagar();Only one is switched off. Without objects you would have to keep a state variable by hand for every LED.A class is the mold; each object is a piece made with that mold, with its own data.
Figure 1. An object is data plus functions, animated: two LEDs of the same class, each with its own pin and its own state. The function always works on the data of its own object.

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();
Encapsulation

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.

The difference from a C struct

A 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

Constructor

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.

Destructor

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
RAII: the most useful pattern in C++

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) { ... }
In moderation

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.

Sensorvirtual float leer() = 0;SensorNTCreads a thermistorSensorPT100reads a PT100SensorDS18B20reads over 1-WireSensorNTCreads a thermistors->leer() calls the SensorNTC versionSensorPT100reads a PT100s->leer() calls the SensorPT100 versionSensorDS18B20reads over 1-Wires->leer() calls the SensorDS18B20 versionThe program holds a pointer to Sensor and always calls leer(). Which of the three runs is decided by the real object,not the pointer type: that is polymorphism, and it lets you add a new sensor without touching the rest of the program.
Figure 2. Inheritance and polymorphism, animated: the program always calls 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;
    }
};
VisibilityWho can access
privateOnly the class itself. It is the default.
protectedThe class itself and its derived classes.
publicAnyone.

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
How it works inside

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.

The destructor of a base class must be virtual

If 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");
}
Why this matters in electronics

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();
}
On microcontrollers they are usually disabled

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

Safe to use
  • Classes, encapsulation, methods.
  • Constructors and destructors (RAII).
  • Function and operator overloading.
  • References instead of pointers.
  • const y constexpr.
  • Single inheritance and virtual functions, with judgment.
  • Templates, as long as the number of instances does not explode.
  • enum class for states and modes.
Avoid
  • Exceptions (-fno-exceptions).
  • RTTI and dynamic_cast (-fno-rtti).
  • new and delete: same reason as malloc, RAM gets fragmented.
  • std::vector, std::string and, in general, the STL with dynamic memory.
  • Multiple inheritance and deep hierarchies.
  • iostream: adds tens of KB.
Arduino is C++

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

Lab 1 · First class

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.

Lab 2 · Constructor, destructor and RAII

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.

Lab 3 · Inheritance and polymorphism

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.

Lab 4 · Display interface

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

MistakeConsequence
Forgetting the ; after the closing brace of a classA cascade of incomprehensible compile errors on the following lines.
Base destructor not virtualWhen 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 polymorphicThe base-class version is always called. The program compiles and does what you do not want.
Members not initialized in the constructorGarbage values. It is best to use the initializer list (: pin(p), estado(false)).
Using new without deleteMemory leak. On a micro with 2 KB of RAM, it runs out in minutes.
Building with the STL on a small microcontrollerThe program does not fit in flash, or it fits and fails when RAM runs out.
Overloading operators in a non-intuitive wayCode that is impossible to read. Overloading is justified only if the meaning is obvious.
Making everything publicEncapsulation 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.

Development of the topic “The C++ language” of Computing for Electronics I (Year 4), based on the “Curriculum Proposal – Second Cycle of the Technical-Vocational Track, Secondary Education – Electronics,” Ministry of Education of the Province of Córdoba, DGETyFP. Back to the Topic Map · catto.ar