UML Class Diagrams¶
Code shows you one class at a time, from the inside. A UML diagram shows a whole design from above: which classes exist, what each one holds, and how they connect. UML ("Unified Modeling Language") is the box-and-arrow shorthand engineers use to sketch a design on a whiteboard before writing it, or to explain an existing one to a colleague.
There are many kinds of UML diagram. This page covers the one you will actually meet and use: the class diagram. It is the notation behind every classDiagram figure in this book (the Polymorphism chapter, for example), and the kind CLion can generate from your code.
You do not need to memorise the whole language. The goal here is narrow: read a class diagram, and sketch a simple one yourself, with each symbol tied back to the C++ it stands for.
Why a picture?¶
Imagine ten classes spread across twenty files. The relationships — who owns whom, who inherits from whom, who merely talks to whom — are real, but they are scattered across constructors, members, and #includes. A class diagram pulls all of that onto one page.
A diagram is cheap. Three boxes and an arrow on a whiteboard can settle an argument about a design in two minutes. It is a thinking and communication tool, not a deliverable — do not gold-plate it.
Anatomy of a class¶
A single class is drawn as a box with up to three stacked compartments: the name, the data members (attributes), and the operations (member functions).
That box is exactly this class:
class BankAccount {
public:
explicit BankAccount(double initial) : balance_(initial) {}
void deposit(double amount);
bool withdraw(double amount);
double balance() const;
private:
double balance_;
};
The middle and bottom compartments are often dropped when you only care about how classes connect — a box with just a name is a perfectly good class in a diagram.
Visibility¶
The +, -, and # in front of each member are the visibility markers. They map straight onto the access specifiers from Classes:
| Symbol | UML term | C++ keyword |
|---|---|---|
+ |
public | public: |
- |
private | private: |
# |
protected | protected: |
A quick note on the small stuff so it does not trip you up later: tools vary in how they print a member's type. The textbook UML order is name : type (as in the box above); other tools — including the Mermaid diagrams later on this page — mirror C++ and put the type first (double balance_). Both mean the same thing.
Relationships between classes¶
The arrows are where class diagrams earn their keep. Each arrow style means something specific, and each one corresponds to a concrete C++ construct. This is the part worth learning well.
Inheritance — "is-a"¶
A solid line with a hollow triangle pointing at the base class. Read it as "is-a": a Car is a Vehicle.
%%{init: {'class': {'hideEmptyMembersBox': true}}}%%
classDiagram
direction LR
Vehicle <|-- Car
Vehicle <|-- Truck
The triangle always points at the base (more general) class.
class Vehicle { /* ... */ };
class Car : public Vehicle { /* ... */ };
class Truck : public Vehicle { /* ... */ };
See Polymorphism for what inheritance buys you.
Interfaces — "implements"¶
A dashed line with a hollow triangle means a class implements an interface. In C++ an "interface" is an abstract base class with pure virtual functions; the marker <<interface>> (a stereotype) labels it as one.
%%{init: {'class': {'hideEmptyMembersBox': true}}}%%
classDiagram
direction LR
class Logger {
<<interface>>
+log(message) void
}
Logger <|.. ConsoleLogger
Logger <|.. FileLogger
Solid triangle line = inherit from a concrete class; dashed triangle line = implement an interface. The C++ looks the same — you derive and override — but the dashed line signals "this base is pure interface, no implementation of its own":
class Logger {
public:
virtual ~Logger() = default;
virtual void log(const std::string& message) = 0; // pure virtual
};
class ConsoleLogger : public Logger {
public:
void log(const std::string& message) override;
};
Composition — "owns a"¶
A filled diamond on the owner. Composition means the part is owned by the whole and shares its lifetime: destroy the Car and its Engine goes with it.
%%{init: {'class': {'hideEmptyMembersBox': true}}}%%
classDiagram
direction LR
Car *-- Engine : owns
In C++ this is a value member, or a std::unique_ptr member — something the class owns outright:
Aggregation — "uses a"¶
A hollow diamond on the holder. Aggregation is a looser "has-a": the class refers to something whose lifetime is managed elsewhere. Park the Car somewhere else and it still exists after the Garage is gone.
%%{init: {'class': {'hideEmptyMembersBox': true}}}%%
classDiagram
direction LR
Garage o-- Car : holds
In C++ this is a reference, a raw pointer, or a std::shared_ptr — the class points at something it does not own:
The composition/aggregation distinction is just ownership. Filled diamond = "I own this, I clean it up" (a value or
unique_ptrmember). Hollow diamond = "I only borrow this, someone else owns it" (a reference or non-owning pointer). This is the same ownership question from Values, References & Pointers and Memory Management, drawn as a picture.
Association — "knows about"¶
A plain open arrow. The weakest lasting link: one class keeps a reference to another so it can talk to it, with no implied ownership.
%%{init: {'class': {'hideEmptyMembersBox': true}}}%%
classDiagram
direction LR
Driver --> Car : drives
In practice association and aggregation overlap a great deal; do not agonise over which to draw. Both say "this object holds onto that one."
Dependency — "temporarily uses"¶
A dashed open arrow. The most fleeting link: a class uses another only briefly — typically as a function parameter or a local variable — without storing it.
%%{init: {'class': {'hideEmptyMembersBox': true}}}%%
classDiagram
direction LR
Car ..> FuelPump : refuel(pump)
The arrows at a glance¶
| In the diagram | Reads as | In C++ |
|---|---|---|
| inheritance — "is-a" | class Car : public Vehicle |
|
| interface — "implements" | derive from an abstract base, override |
|
| composition — "owns; dies with me" | value member or std::unique_ptr<Part> |
|
| aggregation — "uses; owned elsewhere" | reference, raw pointer, or std::shared_ptr |
|
| association — "knows about" | a member that refers to another object | |
| dependency — "temporarily uses" | a function parameter or local variable |
Multiplicity: how many?¶
Numbers at the ends of a line say how many objects take part. A Library has one-to-many Books:
%%{init: {'class': {'hideEmptyMembersBox': true}}}%%
classDiagram
direction LR
Library "1" --> "*" Book : contains
| Notation | Meaning |
|---|---|
1 |
exactly one |
0..1 |
zero or one (optional) |
* |
zero or more |
1..* |
one or more |
The mapping to C++ is direct: a "one" end is usually a single member; a "many" end is a container.
Putting it together¶
Here is a small monitoring system that uses most of the vocabulary at once: a Monitor that owns several SensorLogs, uses a Logger to report, and classifies each reading into a Status as it goes.
%%{init: {'class': {'hideEmptyMembersBox': true}}}%%
classDiagram
class Monitor {
+addReading(name, value) void
}
class SensorLog {
-vector~double~ readings_
+add(value) void
+max() double
}
class Logger {
<<interface>>
+log(message) void
}
class Status {
<<enumeration>>
Ok
Warning
Critical
}
Monitor "1" *-- "1..*" SensorLog : owns
Monitor o-- Logger : uses
Logger <|.. ConsoleLogger
Logger <|.. FileLogger
Monitor ..> Status : addReading()
Read straight off the picture: the Monitor owns one-or-more SensorLogs (filled diamond), it uses a Logger it does not own (hollow diamond), ConsoleLogger and FileLogger implement the Logger interface (dashed triangles), and Monitor depends on the Status enumeration — addReading() runs the new reading through classify() and logs the result, using the Status only as a local value (dashed arrow). The C++ skeleton falls out almost mechanically:
enum class Status { Ok, Warning, Critical };
Status classify(double reading); // maps a single reading to a status
class SensorLog {
public:
void add(double value);
double max() const;
private:
std::vector<double> readings_;
};
class Logger {
public:
virtual ~Logger() = default;
virtual void log(const std::string& message) = 0;
};
class Monitor {
public:
explicit Monitor(Logger& logger) : logger_(logger) {}
// Record a reading, classify it, and log its status.
void addReading(const std::string& name, double value);
private:
std::map<std::string, SensorLog> logs_; // owns (filled diamond)
Logger& logger_; // uses (hollow diamond)
};
classify is a free helper function rather than a class, so it gets no box of its own — a class diagram shows types and how they relate, not every function. The diamonds, meanwhile, chose the member types for you: the owned SensorLogs are stored by value in the map, while the borrowed Logger is held by reference.
A glance at behaviour: sequence diagrams¶
A class diagram shows structure — what exists. When you need to show behaviour — who calls whom, and in what order — UML offers the sequence diagram. Time runs downward; each arrow is a call.
sequenceDiagram
participant main
participant mon as Monitor
participant sl as SensorLog
participant lg as Logger
main->>mon: addReading("temp", 91.0)
mon->>sl: add(91.0)
mon->>mon: classify(91.0)
mon->>lg: log("temp Critical: 91.0")
You will not need to draw these often in this course, but they are invaluable for explaining a tricky interaction at a glance.
When to reach for UML¶
- To design before you code. Sketch the classes and arrows; spotting an awkward relationship on paper is far cheaper than discovering it after you have written it.
- To understand code you did not write. CLion can generate a diagram from existing source — right-click a class and choose Diagrams ▸ Show Diagram. It is the fastest way to get the shape of an unfamiliar codebase.
- To explain a design to someone else. A picture in a pull request or a report says in seconds what a wall of prose cannot.
And when not to: do not turn a quick sketch into a formal artefact you have to maintain. The code is the source of truth. A diagram is worth drawing only as long as it is helping you think or communicate.
Summary¶
- A class diagram shows the shape of a design: the classes and how they relate.
- A class is a box of up to three compartments — name, data members, operations — with
+-#for public/private/protected. - The arrows each map to C++: hollow triangle = inheritance (solid) or interface (dashed); filled diamond = composition (you own it, by value or
unique_ptr); hollow diamond = aggregation (you borrow it, by reference or pointer); open arrows = looser "knows about" / "temporarily uses". - Composition vs. aggregation is simply the ownership question from Memory Management, drawn as a diamond.
- Multiplicity (
1,*,1..*) tells you single member vs. container. - UML is a tool for thinking and communicating, not a deliverable. Sketch freely; do not gold-plate.