In this lesson:
- a class as "a struct that also owns the functions acting on it"
publicvsprivate, enforcing an interface the compiler checks- the implicit
thispointer (and why a method has zero memory overhead) - declaration in a header, definition in a
.cpp structvsclass, and why encapsulation matters for drivers
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 privateThe 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
- Forgetting the access specifier defaults. A
classwith nopublic:label has everything private, callers can't touch anything and you get confusing errors.structdefaults to public. Be explicit. - Defining a method twice. If you define a non-inline method in a header included by multiple
.cpps, you get "multiple definition" at link time, same rule as C functions. Declarations in the header, definitions in one.cpp(or mark inline). - Forgetting
conston query methods. A non-constempty()can't be called on aconst RingBuffer&, which spreads friction through your API. Mark every non-mutating methodconst. - Encapsulation isn't security.
privateis a compile-time check, not memory protection, a determined cast or a pointer can still reach the bytes. It prevents accidents, not a malicious actor.
TL;DR
- A class bundles data members with the methods that act on them, replacing C's struct-plus-free-functions pattern.
privatehides implementation;publicis the interface, and the compiler enforces the boundary, guaranteeing invariants rather than just documenting them.- A method has an implicit
thispointer, soobj.method()β‘func(&obj), methods add zero bytes per object and zero runtime cost. - Declare classes in headers, define methods (
Class::method) in.cppfiles; mark non-mutating methodsconst. structandclassdiffer only in default access (public vs private); usestructfor plain data,classwhen you have invariants to protect.