C++ Primer, Chapter 7

Classes

Data abstraction, encapsulation, constructors, and the this pointer. How C++ lets you define your own types.

Prerequisites: Chapter 2 (Variables) + Chapter 6 (Functions).
9
Chapters
4+
Simulations

Chapter 0: Why Classes?

You're building a bookstore application. Each book has an ISBN, a number of units sold, and total revenue. You could use three separate variables — but then every function needs three parameters, and keeping them in sync is a nightmare. What if you could bundle them into one thing?

A class lets you define your own type. It bundles data (the ISBN, units sold, revenue) and operations (compute average price, combine two sales records) into a single entity. Users of the class work with the operations; the data stays hidden inside.

The core idea: A class separates interface (what you can do with the type) from implementation (how it stores and computes). This is data abstraction — the foundation of C++ design.
struct vs Class

A struct exposes everything by default. A class hides everything by default. Click to toggle between them and see which members are accessible.

Using: struct (all public)
What is the key difference between a struct and a class in C++?

Chapter 1: The this Pointer

When you call a member function on an object, how does the function know which object's data to use? Every member function receives a hidden parameter called this — a pointer to the object the function was called on.

c++
struct Sales_data {
    string isbn() const { return bookNo; }
    // compiler sees this as:
    // string isbn(const Sales_data *const this) { return this->bookNo; }

    string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

When you write total.isbn(), the compiler passes &total as the this pointer. Inside isbn, using bookNo really means this->bookNo.

this is always a const pointer — you cannot change the address it holds. It always points to "this" object. But whether the object itself is const depends on whether the member function is declared as a const member function (next chapter).

Returning *this

Member functions can return *this to enable chaining:

c++
Sales_data& combine(const Sales_data &rhs) {
    units_sold += rhs.units_sold;
    revenue += rhs.revenue;
    return *this;   // return the object this function was called on
}
// enables: total.combine(trans1).combine(trans2);
What is this inside a member function?

Chapter 2: const Member Functions

A member function with const after the parameter list promises not to modify the object. The compiler enforces this by making this a pointer to const:

c++
struct Sales_data {
    string isbn() const { return bookNo; }
    // this has type: const Sales_data *const
    // can read members but cannot modify them
};

Why does this matter? If you have a const Sales_data object, you can only call const member functions on it. Without the const qualifier, isbn() would be uncallable on const objects.

Rule of thumb: Any member function that doesn't modify the object should be declared const. This makes the function usable on both const and non-const objects — maximum flexibility.

Overloading on const

You can overload a member function based on whether the object is const:

c++
class Screen {
public:
    Screen &display(ostream &os)       { do_display(os); return *this; }
    const Screen &display(ostream &os) const { do_display(os); return *this; }
private:
    void do_display(ostream &os) const { os << contents; }
};
Can a const object call a non-const member function?

Chapter 3: Encapsulation

Encapsulation hides a class's implementation from its users. Users interact only through the public interface. The implementation can change without breaking any code that uses the class.

Access Specifiers

SpecifierEffect
publicAccessible to all code
privateAccessible only by member functions and friends
protectedAccessible by members, friends, and derived classes (Ch 15)
c++
class Sales_data {
public:                       // interface
    Sales_data() = default;
    string isbn() const { return bookNo; }
    Sales_data &combine(const Sales_data&);
private:                      // implementation
    string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};
Why bother? Imagine you later change the internal representation — store revenue as cents instead of dollars, or replace the string ISBN with a custom type. If the data is private, you only update the class implementation. If it's public, you must find and fix every line of code that touches the data directly.
What is the purpose of making data members private?

Chapter 4: Constructors

A constructor initializes a newly created object. It has the same name as the class and no return type. Every class needs at least one. If you don't write any, the compiler generates a synthesized default constructor.

c++
class Sales_data {
public:
    Sales_data() = default;                   // synthesized default
    Sales_data(const string &s): bookNo(s) {} // initializer list
    Sales_data(const string &s, unsigned n, double p):
        bookNo(s), units_sold(n), revenue(p * n) {}
    Sales_data(istream &is);                   // reads from stream
private:
    string bookNo;
    unsigned units_sold = 0;  // in-class initializer
    double revenue = 0.0;
};

Constructor Initializer List

The colon after the parameter list introduces the constructor initializer list. Each member is initialized directly with the given value. This is more efficient than assigning inside the body, because it initializes rather than default-constructs-then-assigns.

Critical rule: Members are initialized in the order they appear in the class definition, not the order in the initializer list. If one member's initial value depends on another, the order in the class matters.

= default

= default tells the compiler to generate the default constructor even though you've defined other constructors. Without it, defining any constructor suppresses the synthesized default constructor.

In what order are members initialized in a constructor initializer list?

Chapter 5: Friends

Sometimes a non-member function or another class needs access to private members. Instead of making the data public, you can declare that function or class as a friend:

c++
class Sales_data {
    friend Sales_data add(const Sales_data&, const Sales_data&);
    friend ostream &print(ostream&, const Sales_data&);
    friend istream &read(istream&, Sales_data&);
public:
    // ... constructors and member functions ...
private:
    string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};
A friend declaration only grants access. It is NOT a function declaration in the usual sense. The friend functions still need to be declared (or defined) outside the class for code to call them.

Friend Classes

You can also declare an entire class as a friend, giving all of its member functions access to the private members of the granting class:

c++
class Screen {
    friend class Window_mgr;  // Window_mgr can access Screen's privates
    // ...
};
Use friends sparingly. Every friend is a potential break in encapsulation. If you find yourself needing many friends, it may be a sign that the interface needs rethinking.
What does a friend declaration do?

Chapter 6: Class Memory Layout

Let's visualize how a class object actually lives in memory. A Sales_data object occupies a contiguous block on the stack (or heap). Each data member is laid out in order, with potential padding bytes added by the compiler for alignment.

Object Memory Layout

See how Sales_data's members are laid out in memory. The string occupies 32 bytes (internal pointer, size, capacity), followed by padding, then the unsigned int and double.

sizeof(Sales_data) = 48 bytes
Key observation: Member functions don't take space in the object. They live in the code segment, shared by all objects of the class. Only data members contribute to sizeof. That's why calling isbn() needs the this pointer — the function is separate from the object.

Chapter 7: static Members

Sometimes a piece of data belongs to the class itself, not to any particular object. A static member exists once, regardless of how many objects exist. Think of it as a class-wide variable.

c++
class Account {
public:
    void calculate() { amount += amount * interestRate; }
    static double rate() { return interestRate; }
    static void rate(double);
private:
    string owner;
    double amount;
    static double interestRate;  // shared by ALL Account objects
    static double initRate();
};

Key Differences from Regular Members

Regular memberstatic member
One copy per objectOne copy for entire class
Accessed via object: obj.xAccessed via class: Account::rate()
Has this pointerNo this pointer
Defined in classMust be defined outside class (usually in .cpp)
static members are not part of any object. They don't have a this pointer, so static member functions cannot access non-static members. They exist independently of any object — you can call them even before any objects are created.
How many copies of a static data member exist if you create 100 objects of the class?

Chapter 8: Beyond — Connections

This chapter introduced the mechanics of classes: how the this pointer connects member functions to objects, how access specifiers enforce encapsulation, how constructors initialize objects, and how friends and static members extend the design.

This ChapterBuilds Toward
ConstructorsCopy constructors, move constructors (Ch 13)
EncapsulationInheritance and access control (Ch 15)
this pointerVirtual functions and dynamic binding (Ch 15)
Member functionsOperator overloading (Ch 14)
static membersClass-level constants, singleton patterns
Lippman's advice: "Think of a class as a type. If the class does not have a natural set of operations, it probably should not be a class." Every class should model a coherent concept with a clear interface.

Continue Reading

Next up: Chapter 9: Sequential Containers — the library types that use classes to manage data for you.