What This Chapter Added to a Plain Class

Before this chapter, a class was data members, a constructor, and member functions called on an object. Every feature you met since then answers a question that simple form leaves open: which object am I inside, where does class-wide data live, who is allowed to look at private members, and what runs when the object goes away.

Rather than replay the lessons one by one, this review groups them by the question they answer.

Lesson Question it answers The line to carry forward
The this pointer and method chaining Which object was this called on? this is a const pointer holding the address of the current implicit object
Organizing class definitions Which file does each piece go in? Class definition in the header, non-trivial member functions in the matching source file
Defining types within classes Where does a helper type belong? Nest it in the class and name it Outer::Inner from outside
Resource cleanup with destructors What runs on the way out? One destructor per class, no parameters, called automatically at end of life
Class templates with member functions How are generic members written? Members inside the template use the template parameters directly; members outside must resupply them
Static member variables Where does class-wide data live? One shared copy with static duration that exists even with zero objects
Static member functions How do I reach that data without an object? A member function with no this, called as ClassName::function()
Granting external function access Can a free function read private data? Only if the class names it in a friend declaration
Cross-class access with friend declarations Can another class read it? Same mechanism, granted to a whole class or to one member function of a class
Overloading on value category Does it matter that the object is a temporary? & and && ref-qualifiers overload on the implicit object's value category

Most of the Chapter in One Class

A dive log entry uses six of those features at once. Read it once, then read the notes that follow.

#include <iostream>
#include <string>
#include <string_view>

class DiveEntry
{
public:
    enum class Gas
    {
        air,
        nitrox32,
        trimix,
    };

private:
    std::string m_site{ "unlogged" };
    int m_depth{};
    int m_minutes{};
    Gas m_mix{ Gas::air };

    static inline int s_entriesFiled{ 0 };

public:
    DiveEntry() { ++s_entriesFiled; }

    DiveEntry& atSite(std::string_view site) { m_site = site; return *this; }
    DiveEntry& toDepth(int metres) { m_depth = metres; return *this; }
    DiveEntry& forMinutes(int span) { m_minutes = span; return *this; }
    DiveEntry& breathing(Gas mix) { m_mix = mix; return *this; }

    std::string_view mixLabel() const
    {
        switch (m_mix)
        {
        case Gas::air:      return "air";
        case Gas::nitrox32: return "nitrox 32";
        case Gas::trimix:   return "trimix";
        }

        return "unrecorded";
    }

    void write() const
    {
        std::cout << m_site << ": " << m_depth << " m for " << m_minutes
                  << " min on " << mixLabel() << '\n';
    }

    static int entriesFiled() { return s_entriesFiled; }

    friend bool needsSafetyStop(const DiveEntry& entry);
};

bool needsSafetyStop(const DiveEntry& entry)
{
    return entry.m_depth >= 12 && entry.m_minutes >= 20;
}

int main()
{
    DiveEntry kelpWall{};
    kelpWall.atSite("Kelp Wall").toDepth(28).forMinutes(41).breathing(DiveEntry::Gas::nitrox32);
    kelpWall.write();

    DiveEntry shallows{};
    shallows.atSite("Harbour Steps").toDepth(6).forMinutes(35);
    shallows.write();

    std::cout << std::boolalpha;
    std::cout << "Kelp Wall needs a stop: " << needsSafetyStop(kelpWall) << '\n';
    std::cout << "Harbour Steps needs a stop: " << needsSafetyStop(shallows) << '\n';
    std::cout << "Entries filed: " << DiveEntry::entriesFiled() << '\n';

    return 0;
}

Output:

Kelp Wall: 28 m for 41 min on nitrox 32
Harbour Steps: 6 m for 35 min on air
Kelp Wall needs a stop: true
Harbour Steps needs a stop: false
Entries filed: 2
  • Gas is a nested type. It exists only in the scope of DiveEntry, which is why callers write DiveEntry::Gas::nitrox32. Enumerations, type aliases, and whole classes can all be nested this way, they obey the access specifier they sit under, and they are conventionally declared at the top of the class so the members below can use them.
  • s_entriesFiled is a static member variable. There is exactly one of them no matter how many entries exist, and it was already alive before main() started. Marking it inline is what allows the initializer to sit inside the class definition.
  • entriesFiled() is a static member function. It reads class-wide data without needing an entry to call it on.
  • atSite(), toDepth(), forMinutes(), and breathing() each return *this by reference, so the four calls chain into one expression.
  • mixLabel() and write() are ordinary const member functions, reaching their data through the hidden this pointer.
  • needsSafetyStop() is not a member at all. It reads m_depth and m_minutes only because the class granted it a friend declaration.

Instance Members and Static Members Side by Side

Most confusion in this chapter comes from mixing these two columns up.

Non-static member Static member
How many exist one per object one per class, shared by every object
Lifetime tied to the object static duration, alive from before main() until after it returns
Exists with zero objects no yes
Function has a this pointer yes no
Function may touch static and non-static members static members only
Preferred call syntax kelpWall.write() DiveEntry::entriesFiled()
Where a variable is initialized member initializer list or default member initializer inline (or constexpr) inside the class, otherwise one definition outside it
Best Practice
Reach static members through the class name and the scope resolution operator (DiveEntry::entriesFiled()). Calling them through an object compiles, but it suggests the object matters when it does not.

The "static members only" row is a hard compiler rule, not a style preference. The following class is broken on purpose to show the diagnostic:

#include <iostream>

class Logbook
{
private:
    int m_pages{ 48 };
    static inline int s_booksIssued{ 3 };

public:
    static int booksIssued() { return s_booksIssued; }
    static int pagesLeft() { return m_pages; }
};

int main()
{
    std::cout << Logbook::booksIssued() << '\n';
    std::cout << Logbook::pagesLeft() << '\n';

    return 0;
}
s.cpp: In static member function 'static int Logbook::pagesLeft()':
s.cpp:11:37: error: invalid use of member 'Logbook::m_pages' in static member function
   11 |     static int pagesLeft() { return m_pages; }
      |                                     ^~~~~~~
s.cpp:6:9: note: declared here
    6 |     int m_pages{ 48 };
      |         ^~~~~~~

m_pages belongs to some particular logbook, and a static member function was never told which one. booksIssued() compiles because s_booksIssued belongs to the class itself.

What Returning *this Buys You

Inside a non-static member function, this is a const pointer to the object the function was called on, so m_depth and this->m_depth mean the same thing. Dereferencing it gives you the object, and returning that by reference is the whole trick behind chaining.

#include <iostream>

class TripSheet
{
private:
    int m_dives{};
    int m_totalMinutes{};

public:
    TripSheet& add(int span)
    {
        ++m_dives;
        m_totalMinutes += span;
        return *this;
    }

    TripSheet& clear()
    {
        *this = {};
        return *this;
    }

    void report() const
    {
        std::cout << m_dives << " dives, " << m_totalMinutes << " minutes\n";
    }
};

int main()
{
    TripSheet week{};
    week.add(41).add(35).add(52);
    week.report();

    week.clear().add(19).add(23);
    week.report();

    return 0;
}

Output:

3 dives, 128 minutes
2 dives, 42 minutes

Two details are worth pinning down. The return type is TripSheet&, not TripSheet: returning by value would hand each link in the chain a fresh copy and quietly throw the modifications away. And clear() shows the reset idiom, where *this = {} value-initializes a temporary and copy-assigns it over the current object, so you never have to remember to zero each member by hand.

On a const object, this is a pointer to const, which is exactly why a const object can only call const member functions.

Destructors Close the Other End

A constructor runs when an object comes into existence; a destructor runs when it goes out. It is named ~ClassName, takes no parameters, returns nothing, and you never call it yourself.

#include <iostream>
#include <string>
#include <string_view>

class SurfaceSlate
{
private:
    std::string m_label{};

public:
    SurfaceSlate(std::string_view label) : m_label{ label }
    {
        std::cout << "slate " << m_label << " signed out\n";
    }

    ~SurfaceSlate()
    {
        std::cout << "slate " << m_label << " wiped clean\n";
    }
};

int main()
{
    SurfaceSlate boat{ "boat" };

    {
        SurfaceSlate buddy{ "buddy" };
        std::cout << "descending\n";
    }

    std::cout << "back on deck\n";

    return 0;
}

Output:

slate boat signed out
slate buddy signed out
descending
slate buddy wiped clean
back on deck
slate boat wiped clean

buddy is destroyed at the closing brace of the inner block, and boat at the end of main(), so destruction runs in reverse order of construction. A class with no destructor of its own gets an implicit one that does nothing beyond destroying its members, which is all a class of int and std::string members ever needs.

Warning
Calling std::exit() terminates the program without unwinding the stack, so destructors for local objects never run. Anything you were relying on a destructor to release is simply lost.

Class Templates: Where the Member Functions Go

Inside a class template, member functions can use the class's template parameters directly, and the bare class name is shorthand for the full specialization (DepthTrack means DepthTrack<T>). Outside the class, both of those conveniences disappear.

#include <iostream>

template <typename T>
class DepthTrack
{
private:
    T m_deepest{};
    int m_samples{};

public:
    void record(T reading);
    DepthTrack& merge(const DepthTrack& other);

    T deepest() const { return m_deepest; }
    int samples() const { return m_samples; }
};

template <typename T>
void DepthTrack<T>::record(T reading)
{
    if (reading > m_deepest)
        m_deepest = reading;

    ++m_samples;
}

template <typename T>
DepthTrack<T>& DepthTrack<T>::merge(const DepthTrack<T>& other)
{
    if (other.m_deepest > m_deepest)
        m_deepest = other.m_deepest;

    m_samples += other.m_samples;
    return *this;
}

int main()
{
    DepthTrack<double> saturday{};
    saturday.record(11.5);
    saturday.record(27.4);

    DepthTrack<double> sunday{};
    sunday.record(18.2);

    saturday.merge(sunday);

    std::cout << "deepest " << saturday.deepest() << " m across "
              << saturday.samples() << " samples\n";

    return 0;
}

Output:

deepest 27.4 m across 3 samples

Each out-of-class definition repeats template <typename T> and qualifies the function with DepthTrack<T>::. Note also that merge() reads other.m_deepest even though other is a different object: access control is per class, not per object.

Reminder
A class template's member functions are only compiled when instantiated, so the definitions must be visible wherever the template is used. Keep them in the same file, just below the class template definition, rather than in a separate source file.

Who Is Allowed to See Private Data

Four routes lead into a class's private and protected members, and they differ in who grants access and whether the code doing the reading is a member.

Route How it is written What it can see Has a this pointer?
Member function declared in the class every member of its own class yes
Static member function static in the class static members of its own class no
Friend non-member function friend bool needsSafetyStop(const DiveEntry&); private and protected members of the granting class, through an object passed to it no, it is not a member
Friend member function of another class friend void GearDesk::inspect(Cylinder&); the same, but only inside that one function yes, pointing at the other class's object
Friend class friend class SafetyReview; the same, from every member of that class not applicable

The class-wide grant on the last row looks like this:

#include <iostream>

class Cylinder
{
private:
    int m_barStart{};
    int m_barEnd{};

public:
    Cylinder(int barStart, int barEnd) : m_barStart{ barStart }, m_barEnd{ barEnd } {}

    friend class SafetyReview;
};

class SafetyReview
{
public:
    static void flag(const Cylinder& tank)
    {
        std::cout << "used " << (tank.m_barStart - tank.m_barEnd) << " bar, ";

        if (tank.m_barEnd < 50)
            std::cout << "reserve breached\n";
        else
            std::cout << "reserve intact\n";
    }
};

int main()
{
    Cylinder kelpWall{ 232, 38 };
    Cylinder shallows{ 207, 96 };

    SafetyReview::flag(kelpWall);
    SafetyReview::flag(shallows);

    return 0;
}

Output:

used 194 bar, reserve breached
used 111 bar, reserve intact

Friendship is granted, never taken: the access appears inside the class that owns the data, so encapsulation stays under that class's control. It is also one-directional, so Cylinder gains nothing from SafetyReview in return. And because a friend is not a member, it has no this pointer of the granting class and is called like any ordinary function.

Best Practice
Prefer a non-friend, non-member function built on the class's public interface. Reach for friend only when the operation genuinely needs the internals, since every friend is one more piece of code you must revisit when the private members change.

Ref-Qualifiers, the Optional Corner

A member function can be overloaded on the value category of the object it is called on. Add & to the overload that should match named objects and && to the one that should match temporaries.

#include <iostream>
#include <string>
#include <string_view>

class RentalSlip
{
private:
    std::string m_stamp{};

public:
    RentalSlip(std::string_view stamp) : m_stamp{ stamp } {}

    const std::string& stamp() const &
    {
        std::cout << "[lvalue overload] ";
        return m_stamp;
    }

    std::string stamp() const &&
    {
        std::cout << "[rvalue overload] ";
        return m_stamp;
    }
};

RentalSlip issueSlip(std::string_view stamp)
{
    return RentalSlip{ stamp };
}

int main()
{
    RentalSlip mine{ "AL80-4471" };
    std::cout << mine.stamp() << '\n';

    std::cout << issueSlip("LP85-2213").stamp() << '\n';

    return 0;
}

Output:

[lvalue overload] AL80-4471
[rvalue overload] LP85-2213

The named object gets a cheap reference to the member; the temporary gets a copy, because a reference into a temporary would dangle the moment the full expression ends. The cost is that ref-qualifying one overload of a function forces you to ref-qualify all of them, which is why this feature stays rare in practice.

Where Each Piece of the Class Lives

Piece Goes in Why
Class definition DiveEntry.h, named after the class one place to look, and the header guard or #pragma once keeps repeated inclusion legal
Trivial member functions (empty constructors, one-line accessors) inside the class definition short enough that inlining them costs nothing in readability
Non-trivial member functions DiveEntry.cpp, named after the class editing a body then recompiles one source file instead of every file that includes the header
Member functions of a class template the header, below the class template the definition must be visible at instantiation
inline or constexpr static member variables inside the class definition the initializer is allowed there, and no separate definition is needed
Other static member variables one definition in the matching source file they need exactly one definition in the whole program

A member function defined inside the class definition is implicitly inline; one defined outside it is not. That is why a function defined outside the class but still in a header needs an explicit inline, or two translation units including that header will collide.

Key Terminology

  • Implicit object: the object a non-static member function was called on
  • this pointer: the hidden const pointer to that implicit object, available in every non-static member function
  • Method chaining: writing several calls in one expression, made possible by each function returning *this by reference
  • Nested type (member type): a type declared inside a class and named Outer::Inner from outside
  • Destructor: the ~ClassName member function that runs automatically when an object's lifetime ends
  • Static member variable: a single class-wide variable with static duration, shared by all objects and alive without any
  • Static member function: a member function with no implicit object and therefore no this pointer
  • Friend declaration: the friend statement a class uses to grant outside code access to its private and protected members
  • Friend function: a function, member or non-member, holding that grant
  • Friend class: a class whose every member function holds that grant
  • Ref-qualifier: the & or && suffix that overloads a member function on the implicit object's value category

Summary

The through line of this chapter is scope: which object, which class, which file, which piece of code. this answers which object. Static members answer what belongs to the class rather than to any object, and static member functions are how you reach that data with no object in hand. Nested types keep a helper type inside the only class that needs it. Friend declarations let a class deliberately open a window that data hiding would otherwise close, and ref-qualifiers let one function behave differently for named objects and temporaries. Destructors and the header/source split answer when the object's work ends and where its code lives.

Looking Forward

Every feature here becomes machinery for the class designs ahead. Static members and static member functions are the parts a factory function or a shared registry is built from. Destructors are the foundation of resource-owning types, where acquiring in the constructor and releasing in the destructor makes leaks structurally impossible. Friend non-member functions become the standard shape for operator overloads that need both operands. Keep the reflex you built here of asking who owns a piece of data and who is allowed to touch it, because that question only gets more valuable as the classes get larger.