Chapter 2 introduced user-defined types and Chapter 4 showed how exceptions and RAII keep
constructors honest. This chapter is where those threads meet: classes are
the central language feature of C++, the tool we use to define our own types that behave "as
real as int and float." Stroustrup organizes class design into three
kinds — concrete types (represented directly, like built-ins), abstract types (pure
interfaces), and class hierarchies (families of related types built on virtual functions).
Along the way you'll meet the two resource-management ideas that make C++ memory-safe in
practice: constructors/destructors pairs (RAII) and unique_ptr. By the end, the
Vector from Chapter 4 and the Shape hierarchy below are the same
pattern seen from two sides: acquisition in the constructor, release in the destructor,
interface up top, implementation below.
Why classes matter now: everything you've seen so far —
complex, Vector, string, vector — is a
class. This chapter gives you the vocabulary to read and write them: concrete vs. abstract
types, virtual functions and the vtbl, hierarchies and dynamic_cast. The
resource management half (RAII, unique_ptr) is what makes exceptions
safe, and it's the single most important C++ idiom you'll carry forward.
The central language feature of C++ is the class — a user-defined type.
Classes provide "a structure for creating objects that can hold data and define operations on
it," and they let us represent concepts directly in our code rather than through ad-hoc
combinations of built-in types. This is the heart of the idea that a user-defined type should
be "as real as int and float" — the quote is from Doug McIlroy, and
it's the standard by which Stroustrup judges every class design.
Class design has a small set of recurring questions: where does the representation live (inside the object, or hidden behind an interface)? Is there a family of related types that should share a common interface? The answers produce the three kinds of classes covered in this chapter:
| Kind of class | Representation lives | Used via | Typical example |
|---|---|---|---|
| Concrete type (§5.2) | in the object itself | directly (stack, members) | complex, Vector |
| Abstract type (§5.3) | behind a pure interface | pointers and references | Container |
| Class hierarchy (§5.5) | spread across a family of derived classes | pointers and references | Shape → Circle → Smiley |
The simplest kind of class is a concrete type: one whose representation is part of its definition. That makes them behave much like built-in types — you can place them on the stack, refer to them directly (not through pointers or references), copy and move them freely, and initialize them immediately and completely. The price of this simplicity is that if the representation changes in any significant way, code using it must be recompiled. But for small, hot, performance-critical types that trade is exactly right.
The classic example is a complex number type. Because it's small and its
operations are fundamental, it must behave like a built-in — no indirection, no heap
allocation, no surprises:
class complex {
double re, im; // representation: two doubles
public:
complex(double r = 0, double i = 0) : re{r}, im{i} {} // build two doubles
complex(double r) : re{r}, im{0} {} // build from a double
complex& operator+=(const complex& z) { re += z.re; im += z.im; return *this; }
double real() const { return re; } // read out
double imag() const { return im; }
// ...
};
Three design details are worth noticing:
complex(double r = 0, double i = 0) doubles as a default constructor, so
complex z1;, complex z2 = 2.0;, and
complex z3{1,2}; all work. Every variable is initialized — no uninitialized
doubles to surprise you later.
const member functions. The const after
real() declares "this function does not modify the object's state." That's what
lets you read the real part of a const complex. Without it,
const objects would have no way to be inspected at all.
operator+= is a member
because it must modify the object. But the symmetric operators +,
-, *, /, == are defined as
non-member functions — so that 2+z and z+2 both work (a
member operator+ would require the left operand to be a
complex already). The rule of thumb:
a function is a member only if it needs direct access to the representation.
complex operator+(complex a, complex b) { return a += b; } // symmetric: 2+z works
bool operator==(complex a, complex b) { return a.real()==b.real() && a.imag()==b.imag(); }
A container holds a collection of elements — and unlike
complex, its representation lives partly on the free store. That introduces the
resource-management problem this chapter is really about:
who acquires the memory, and who releases it? The answer, as Chapter 4 promised, is
the constructor/destructor pair:
class Vector {
public:
Vector(int s) : elem{new double[s]}, sz{s} {} // constructor: acquire resources
~Vector() { delete[] elem; } // destructor: release resources
double& operator[](int i) { return elem[i]; }
int size() const { return sz; }
private:
double* elem; // pointer to the elements
int sz; // the number of elements
};
The constructor acquires the array (new double[s]) and the destructor releases it
(delete[] elem). Because the destructor is called automatically when a
Vector goes out of scope — and, crucially,
even when an exception unwinds through it — the memory is released exactly once, on
every path:
void fct(int n) {
Vector v(n); // constructor allocates
// ... use v ...
} // destructor deallocates — even if an exception was thrown above
This is RAII (Resource Acquisition Is Initialization): acquire in the
constructor, release in the destructor, and the language guarantees the rest. The
Vector object itself is a small handle (two words: pointer + size); the
actual data lives on the free store. That split — handle on the stack, data on the heap — is
the pattern behind every standard container. The design rule to carry away:
if a constructor acquires a resource, its class needs a destructor to release it, and you
should avoid "naked" new and delete in application code entirely.
Raw new double[s] is awkward for everyday use. We want to write
Vector v = {1, 2, 3, 4}; — so the class gets an
initializer-list constructor that takes a
std::initializer_list<double>:
Vector::Vector(std::initializer_list<double> lst) // initialize from a list
: elem{new double[lst.size()]}, sz{static_cast<int>(lst.size())} {
std::copy(lst.begin(), lst.end(), elem); // copy the elements in
}
Note the static_cast<int>(lst.size()): the list's
size() returns a size_t (unsigned), and converting to the signed
int we chose for sz
needs an explicit cast — implicit narrowing would be a compile error inside a brace
initializer. Casts are a code smell at high levels of abstraction, but here the conversion is
legitimate and checked.
Once a Vector supports range-for (Chapter 2 style) and push_back(),
it becomes genuinely convenient:
Vector read(istream& is) {
Vector v; // empty Vector
for (double d; is >> d; ) // read floating-point values
v.push_back(d); // grow the Vector
return v; // return by value
}
For that return-by-value to be cheap (no copying of the whole array),
Vector needs a move constructor, which transfers the handle —
pointer and size — rather than copying elements:
Vector::Vector(Vector&& a) // move constructor
: elem{a.elem}, sz{a.sz} { // steal the representation
a.elem = nullptr; // leave a in an empty state
a.sz = 0;
}
A abstract type is a type that completely insulates a user from the implementation details. The representation is not part of the definition — in fact, an abstract type has no data members at all. It specifies an interface as a set of pure virtual functions, and it is the job of derived classes to supply the implementations:
class Container {
public:
virtual double& operator[](int) = 0; // pure virtual function
virtual int size() const = 0; // const member function
virtual ~Container() {} // virtual destructor
};
The = 0 marks a function as pure virtual — there is no
definition in Container itself, and a class with any pure virtual function cannot
be instantiated. You cannot write Container c;. What you can do is implement the
interface in a concrete class:
class Vector_container : public Container { // implements Container
Vector v; // Vector from §5.2.2 as the data
public:
Vector_container(int s) : v(s) {}
Vector_container(std::initializer_list<double> lst) : v(lst) {}
~Vector_container() {} // implicitly releases v's memory
double& operator[](int i) override { return v[i]; }
int size() const override { return v.size(); }
};
Now a user can write a function against the interface, and any implementation of
Container will work — without the compiler knowing anything about
Vector_container or the alternative List_container:
void use(Container& c) {
const int sz = c.size();
for (int i = 0; i != sz; ++i)
cout << c[i] << '\n'; // works for any Container implementation
}
The cost of this flexibility: you can only access abstract types through pointers or
references, and objects must be allocated on the free store. The override keyword
on the derived functions makes the intent explicit and lets the compiler catch spelling or
signature mistakes. And note the virtual destructor — it's essential, because
deleting a Container* must run the most-derived destructor, and only a virtual
destructor guarantees that.
Container is a polymorphic type: it has at least one virtual function,
so objects of different derived classes can be treated uniformly through the interface — the
same use() call dispatches to different implementations at run time.
How does use(Container&) call the right operator[]() when it
doesn't know the object's concrete type? The answer is the vtbl (virtual
function table). Each object of a polymorphic class carries a hidden pointer to a table of
function pointers for its class, and a call through a virtual function becomes an indirection
through that table:
// Conceptual layout:
// Vector_container object: [ vptr ] --> [ Vector_container::vtbl ]
// |-- operator[] : &Vector_container::operator[]
// |-- size() : &Vector_container::size()
// `-- ~Container : &Vector_container::~Vector_container
The price of run-time dispatch is small and bounded: one pointer per object, one table per
class, and a call that is roughly within 25% of a direct call on most hardware. That overhead
is the deliberate trade — you pay a little for the ability to extend the program with new
Container implementations without recompiling the code that uses the
interface.
When several classes share a common interface and a common structure, we organize
them into a class hierarchy — a family of classes ordered by derivation. The
classic example: a Shape base class with derived classes like
Circle and Smiley:
class Shape {
public:
virtual Point center() const = 0; // pure virtuals: interface only
virtual void move(Point to) = 0;
virtual void draw() const = 0;
virtual void rotate(int angle) = 0;
virtual ~Shape() {} // virtual destructor
};
class Circle : public Shape {
public:
Circle(Point p, int r);
Point center() const override { return x; }
void move(Point to) override { x = to; }
void draw() const override; // draw on some Canvas
void rotate(int) override {} // nice simple algorithm
private:
Point x; // center
int r; // radius
};
class Smiley : public Circle { // uses Circle's data and operations
public:
Smiley(Point p, int r) : Circle{p, r}, mouth{nullptr} {}
void draw() const override; // draw the face, then the eyes and mouth
void rotate(int) override; // rotate the eyes and mouth too
void add_eye(Shape* s) { eyes.push_back(s); }
void set_mouth(Shape* s);
virtual void wink(int i); // "feel free to override"
private:
std::vector<Shape*> eyes; // usually two
Shape* mouth;
};
Then a general function can treat every shape uniformly — this is exactly the interface-inheritance idea from §5.3, applied to a family:
void rotate_all(std::vector<Shape*>& v, int angle) {
for (auto p : v)
p->rotate(angle); // virtual call: the right rotate() runs for each shape
}
Objects are constructed bottom-up (base first, then derived) and destroyed
top-down (derived first, then base). A virtual destructor is what makes top-down
destruction reach Smiley's destructor — and thereby delete the
eyes and mouth it owns — when you delete through a
Shape*.
Hierarchies give us two distinct forms of inheritance, and Stroustrup's advice is to keep them separate in your mind:
Container and
Shape are interfaces — rotate_all() doesn't need to know which
concrete shapes it holds.
Smiley inherits Circle's data
(x, r) and operations, then adds its own. This is a convenience —
a way to reuse code — not a promise about the interface.
The design question is always: is the base class something users will program against (interface), or a helper for the classes above it (implementation)? Keeping the two distinct prevents the classic hierarchy smell where a "base class" is really just shared code, and users end up depending on implementation details.
Sometimes you need to get from a base pointer back to a derived object — for example, to call
wink() on a shape you know is a Smiley. That's what
dynamic_cast
is for: a run-time check of "is this object of type X?"
Shape* ps {read_shape(cin)};
if (Smiley* p = dynamic_cast<Smiley*>(ps)) { // does ps point to a Smiley?
p->wink(1); // yes: wink
} else {
// not a Smiley: do nothing
}
The pointer form returns nullptr when the cast fails, so the condition fails and
the else handles the "not a Smiley" case. The reference form
dynamic_cast<Smiley&& has no null value to return, so it throws
std::bad_cast instead. The advice: use pointer casts when failure is a valid
alternative (test it), reference casts when failure is an error (let the exception propagate).
And use dynamic_cast with restraint — code that type-tests its way down a
hierarchy is often better redesigned to use virtual functions, which are simpler, faster, and
checked at compile time.
Look at Smiley again: it owns eyes (a
vector<Shape*>) and mouth (a Shape*), so its
destructor must delete them:
Smiley::~Smiley() {
delete mouth;
for (auto p : eyes)
delete p;
}
This is exactly the "naked new/delete" pattern §5.2.2 told you to avoid. It works, but it puts
a burden on every owner: remember to delete, delete exactly once, and make sure no
exception can leak the objects in between. The modern alternative is to use a smart pointer
for owned resources — unique_ptr deletes its object automatically when it goes
out of scope:
class Smiley : public Circle {
// ...
std::unique_ptr<Shape> mouth; // owns the mouth
std::vector<std::unique_ptr<Shape>> eyes; // owns the eyes
// no destructor needed: unique_ptr members clean up automatically
};
Now Smiley needs no user-declared destructor at all — the compiler generates one
that destroys the members, which destroys the mouth and each eye. No hand-written
delete, no way to forget one, and if an exception is thrown mid-construction, the
members that were already built are still cleaned up. This is the RAII philosophy applied to
members:
own resources with objects that know how to release them, and let the language do the
rest.
Here is a summary of the guidance from this chapter. All 24 items, with the section where each is introduced. The C++ Core Guidelines link each item to its recommended practice.
| # | Guideline | § |
|---|---|---|
| 1 | Express ideas directly in code. | 5.1 |
| 2 | A concrete type is the simplest kind of class. Where applicable, prefer a concrete type over more complicated classes and over plain data structures. | 5.2 |
| 3 | Use concrete classes to represent simple concepts. | 5.2 |
| 4 | Prefer concrete classes over class hierarchies for performance-critical components. | 5.2 |
| 5 | Define constructors to handle initialization of objects. | 5.2.1, 6.1.1 |
| 6 | Make a function a member only if it needs direct access to the representation of a class. | 5.2.1 |
| 7 | Define operators primarily to mimic conventional usage. | 5.2.1 |
| 8 | Use nonmember functions for symmetric operators. | 5.2.1 |
| 9 |
Declare a member function that does not modify the state of its object const.
|
5.2.1 |
| 10 | If a constructor acquires a resource, its class needs a destructor to release the resource. | 5.2.2 |
| 11 | Avoid "naked" new and delete operations. |
5.2.2 |
| 12 | Use resource handles and RAII to manage resources. | 5.2.2 |
| 13 | If a class is a container, give it an initializer-list constructor. | 5.2.3 |
| 14 | Use abstract classes as interfaces when complete separation of interface and implementation is needed. | 5.3 |
| 15 | Access polymorphic objects through pointers and references. | 5.3 |
| 16 | An abstract class typically doesn't need a constructor. | 5.3 |
| 17 | Use class hierarchies to represent concepts with inherent hierarchical structure. | 5.5 |
| 18 | A class with a virtual function should have a virtual destructor. | 5.5 |
| 19 | Use override to make overriding explicit in large class hierarchies. |
5.3 |
| 20 | When designing a class hierarchy, distinguish between implementation inheritance and interface inheritance. | 5.5.1 |
| 21 | Use dynamic_cast where class hierarchy navigation is unavoidable. |
5.5.2 |
| 22 |
Use dynamic_cast to a reference type when failure to find the required class
is considered a failure.
|
5.5.2 |
| 23 |
Use dynamic_cast to a pointer type when failure to find the required class is
considered a valid alternative.
|
5.5.2 |
| 24 |
Use unique_ptr or shared_ptr to avoid forgetting to delete
objects created using new.
|
5.5.3 |
What is the defining property of a concrete type like complex or
Vector?
Why does Stroustrup define operator+ for complex as a
nonmember function?
What does the const on double real() const promise?
A Vector constructor does elem{new double[s]}. When is
delete[] elem guaranteed to run?
Why can't you write Container c;?
What goes wrong if Shape's destructor is NOT virtual, and you
delete a Smiley through a Shape*?
You write dynamic_cast<Smiley*>(ps) and ps actually points
to a Circle that is not a Smiley. What happens?
Why does Smiley need no user-declared destructor when its
members are std::unique_ptrs?
class ⇒ user-defined type ; central feature of C++
3 kinds ⇒ concrete | abstract | hierarchy
goal ⇒ as real as int and float (eg McIlroy)
concrete ⇒ representation part of definition → behaves like built-in
stack / direct access / copy / move ⇒ all allowed
price ⇒ representation change → recompile all users
default args ctor ⇒ no uninitialized variables
const member fn ⇒ doesn't modify object → callable on const objects
member fn ⇒ only if it needs direct representation access
symmetric operators ⇒ nonmember (eg +, ==) ; conversion on both sides
constructor ⇒ acquires resource (eg new double[s])
destructor ⇒ releases resource (eg delete[] elem)
RAII ⇒ acquire in ctor, release in dtor → safe on every path incl. exceptions
handle-to-data ⇒ small object on stack, data on free store
initializer-list ctor ⇒ {1,2,3,4} →
std::initializer_list
push_back ⇒ append element at end
move ctor ⇒ steals handle → cheap return by value
static_cast ⇒ explicit narrowing (eg size_t → int) ; use sparingly
abstract ⇒ interface only ; no representation
pure virtual ⇒ = 0 ; no definition in base ; class cannot be
instantiated
access ⇒ pointers/refs only ; objects on free store
override ⇒ explicit ; compiler catches signature mistakes
polymorphic ⇒ same interface, many implementations (eg use(Container&))
vtbl ⇒ table of function pointers ; per class
object ⇒ one hidden vptr → call goes through the table
cost ⇒ 1 ptr/object + 1 table/class ; ≈ within 25% of direct call
payoff ⇒ new implementations without recompiling users
interface inheritance ⇒ base = interface → any derived usable (eg Shape)
implementation inheritance ⇒ base = shared code/data (eg Smiley : Circle)
construction ⇒ bottom-up ; destruction ⇒ top-down
virtual destructor ⇒ delete via base ptr → derived dtor runs
dynamic_cast ⇒ run-time "is it a kind of X?"
pointer form ⇒ failure → nullptr ; test it
reference form ⇒ failure → throws std::bad_cast
restraint ⇒ prefer virtual fns over type-testing
naked ptr + hand delete × easy to forget / double-delete
unique_ptr ⇒ owns ; auto-deletes at scope end
members as unique_ptr → no user destructor needed
Primary source: Stroustrup, B. (2022). A Tour of C++, 3rd ed.,
Chapter 5: "Classes." Addison-Wesley.
Reference:
Chapter 1–5 Quick Reference & Glossary —
keep it beside you while you study.
Recommended supplement: cppreference on
classes,
virtual functions,
dynamic_cast, and
unique_ptr.
Questions? Ask your agent — your teacher — about anything unclear: when a class hierarchy is
worth its overhead over a concrete type, why
override catches real bugs, or how the move constructor makes
push_back and return-by-value cheap. Follow-ups are expected, not optional.