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

Classes & Encapsulation

Bundle data with the code that operates on it, and hide the internals behind a clean interface: the C struct-plus-functions pattern, made safe by the compiler.

22 min read

In this lesson:


From struct-plus-functions to a class

In C you model a peripheral as a struct plus a set of functions that take a pointer to it:

typedef struct { volatile uint32_t *base; uint32_t baud; } Uart;
void uart_init(Uart *self, uint32_t baud);
void uart_send(Uart *self, uint8_t byte);

Nothing stops a caller from reaching into self->baud and corrupting it, or calling uart_send before uart_init. A class bundles the data and the functions together and lets you hide the internals:

class Uart {
    volatile uint32_t *base_;   // private by default
    uint32_t baud_;
public:
    void init(uint32_t baud);   // the public interface
    void send(uint8_t byte);
};

The data members (base_, baud_) and member functions (init, send) live in one type. Callers use uart.send(0x41) instead of uart_send(&uart, 0x41).


public vs private: a contract the compiler enforces

Encapsulation means exposing a small, deliberate interface and hiding the implementation. Members after private: are accessible only from inside the class's own methods; members after public: are the interface everyone may use.

class RingBuffer {
    uint8_t data_[64];     // private: nobody outside can touch the storage
    uint8_t head_ = 0;
    uint8_t tail_ = 0;
public:
    bool push(uint8_t b);  // public: the only way in
    bool pop(uint8_t *out);
    bool empty() const;
};
 
RingBuffer rb;
rb.push(0x41);             // OK
rb.head_ = 99;             // COMPILE ERROR β€” head_ is private

The win over C: the compiler enforces the boundary. A caller cannot accidentally desynchronize head_/tail_ by poking the fields, because they're unreachable. The invariant "head and tail are only modified by push/pop" is guaranteed, not just documented.


The implicit this pointer

A non-static method receives a hidden first parameter, this, a pointer to the object it was called on. Inside send, base_ means this->base_:

void Uart::send(uint8_t byte) {
    while (!(this->base_[0] & TX_READY)) { }   // 'this->' is implicit
    base_[1] = byte;                            // same as this->base_[1]
}

This is why a method costs nothing extra over a C function: uart.send(b) compiles to passing &uart as the implicit this plus b, identical to uart_send(&uart, b). The object's memory layout is just its data members; methods aren't stored per-object.

 sizeof(Uart) == sizeof(base_) + sizeof(baud_)   // methods add ZERO bytes
 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 β”‚  base_ (ptr) β”‚  baud_   β”‚     ← one Uart object in memory
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Header / source split

Idiomatic C++ declares the class in a .hpp header and defines the methods in a .cpp. The ClassName::method syntax says "this is the definition of that class's method":

// uart.hpp
class Uart {
    volatile uint32_t *base_;
    uint32_t baud_;
public:
    void init(uint32_t baud);
    void send(uint8_t byte);
};
// uart.cpp
#include "uart.hpp"
 
void Uart::init(uint32_t baud) {   // Uart:: = "this belongs to Uart"
    baud_ = baud;
    // ... configure registers ...
}
 
void Uart::send(uint8_t byte) {
    while (!(base_[0] & TX_READY)) { }
    base_[1] = byte;
}

Short, hot methods can instead be defined inline inside the class body, which hints the compiler to inline them, useful for one-line accessors like bool empty() const { return head_ == tail_; }.


const methods

A method that doesn't modify the object should be marked const, it promises not to change any data member, and it's the only kind of method callable on a const object:

bool RingBuffer::empty() const {   // promises not to mutate *this
    return head_ == tail_;
}

This is const-correctness (from the C lesson) applied to objects: query methods are const, mutating methods aren't. The compiler rejects an accidental write inside a const method.


struct vs class

In C++ they're nearly identical, the only difference is the default access level: struct members are public by default, class members are private. Convention: use struct for passive data aggregates (a plain bundle of fields, like a register map or a config POD), and class when you have invariants to protect behind methods.

struct Point { int x; int y; };     // just data β€” public is fine
class Motor { /* hidden state + methods enforcing safe control */ };

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

C++ vs C for EmbeddedConstructors, Destructors & RAII
References vs Pointers
const Correctness & constexpr
Function & Operator Overloading
Templates: The Basics