Skip to content

Module 2 — Concepts and Explanations

Read this with a compiler open. Every section ends with code you can type, and the code is the point — the paragraphs tell you what to look for in it.


1. What is a class, and what is an object?

A class is a type you define. An object is one instance of it.

class Book {
public:
    Book(int id, const std::string& title) : id_(id), title_(title) {}
    int id() const { return id_; }
    const std::string& title() const { return title_; }
private:
    int id_;
    std::string title_;
};

That declaration says four things, and it is worth naming each one:

Part What it declares
class Book a new type, usable anywhere int is usable
public: the part a caller may use — the interface
private: the part a caller must ignore — the representation
Book(int, const std::string&) how one of these comes into existence

Now make two:

Book primer(1001, "C++ Primer");
Book clean(1003, "Clean Code");

primer and clean are separate objects. Each has its own id_ and title_. Writing to one cannot possibly affect the other, because they are two different blocks of memory — exactly like two ints.

std::cout << primer.id() << ' ' << clean.id() << '\n';   // 1001 1003

This is worth stating because one of the most common beginner questions is "where do the different values go?" Answer: into the object. There is no shared storage unless you deliberately ask for it (a static member — Module 5).

A type is a promise, not a folder. When you declare Book, you are telling the compiler and every future reader: these operations are the only way to work with a book. That is the whole idea, and everything else in this course is about keeping the promise.


2. class and struct — the only real difference

In C++, class and struct are the same construct with two different defaults:

struct class
Default member access public private
Default inheritance mode public private (Module 6)

That is the entire language difference. There is no performance difference, no "structs are values and classes are objects", no rule about size. Both may have member functions, constructors, destructors and inheritance.

So why does the course use class almost everywhere? Because the convention is useful:

struct Rgb {          // a plain bundle of data, no rules: struct reads better
    int red;
    int green;
    int blue;
};

class Catalogue {     // data with rules and behaviour: class states the intent
public:
    bool add(Book book);
private:
    std::vector<Book> books_;
};

Choose by asking: does anything have to be true about this data? If not, a struct with public members is honest and short. If yes — a count that cannot go negative, a title that cannot be empty — use a class, keep the data private, and put the rule in the operations.

A warning about habits from other languages

In Java or C#, a struct is a different concept (a value type). In C++ it is not. If you write C++ as if struct meant "no methods allowed", you are carrying a rule from a language you are not writing.


3. Writing a type: interface first

Here is the order that produces good types, and the order most beginners never try:

  1. Write the invariant, if there is one, in one sentence: "a book always has a non-empty title and a positive identifier."
  2. Write the operations the rest of the program needs, as if the objects already existed and worked perfectly.
  3. Only then decide what data those operations need.

Applied to a library item:

class Book {
public:
    // Step 2: what does the rest of the program need to do?
    Book(int id, const std::string& title);     // create a valid one
    int id() const;                              // identify it
    const std::string& title() const;            // display it
    bool hasId(int id) const;                    // find it
    bool isLongRead() const;                     // a question about itself
private:
    // Step 3: what did those operations turn out to need?
    int id_;
    std::string title_;
    int pages_;
};

Notice what step 2 produced: no setTitle, no getPages, no setPages. Not because setters are forbidden, but because nothing in a well-designed program needs to reach in and rename a book that has already been catalogued.

The test at the end of this section. Can you describe what this type is responsible for in one sentence, without the word "and"? "A Book is a catalogued item with an identifier and a title." If your sentence needs "and", you very probably have two types badly glued together.


4. Where does a member function's body go?

Two places, and both are legitimate:

class Loan {
public:
    // Defined inside the class body. Implicitly `inline`, so it is fine in a
    // header — every translation unit gets its own copy and the linker merges them.
    int days() const { return days_; }

    // Declared here, defined elsewhere (see section 8).
    std::string summary() const;

private:
    int days_;
};
// ...and defined here, in a .cpp file:
std::string Loan::summary() const {
    return "loan of " + std::to_string(days_) + " days";
}

Loan:: in front of the name is a scope qualifier. It does two things: it tells the compiler which class this belongs to, and it puts the class's members in scope inside the body, which is why days_ can be written without qualification.

Rules of thumb, and the reason behind each:

Keep in the class body Move to a source file
One-liners: id(), hasId() Anything longer than a couple of lines
Templates and other generics (Module 12) Bodies that need #include <iostream>
When the clarity is worth it Anything that would leak an implementation detail into every file that includes your header

That last row is the real reason. A header is included by many files; a header that includes <iostream> drags an entire library into each of them.


5. Public and private: who may use a member?

Three access specifiers, one of which belongs to a later module:

Specifier Who may use the member
public anybody — this is the type's interface
private only the class's own member functions (and friends — Module 4)
protected the class and its derived classes — Module 6, and only then

The prototype of this section is the module's first program, examples/module-02/01-book-class/main.cpp. Try to break it:

Book primer(1001, "C++ Primer", 976);
std::cout << primer.id_;        // error: 'int Book::id_' is private within this context

That error is not an obstacle placed in your way. It is the compiler refusing to let you depend on something you were not supposed to depend on — and the reason that error is valuable is that changing a private member is now guaranteed to be safe. No caller can have written code against it.

What private does not do. It does not make your class well-designed. A class where every private field has a public getter and a public setter has the syntax of encapsulation and none of the benefit, because callers can still reach the representation — just through a function call. The module after this one is entirely about that distinction.


6. What is this?

Inside any non-static member function there is a hidden parameter: a pointer to the object the function was called on. It is called this.

class Counter {
public:
    void increment() {
        ++value_;
        // ...which is exactly:  ++this->value_;
    }
    int value() const { return value_; }
private:
    int value_ = 0;
};

You need this explicitly in exactly two situations.

1. When a parameter name hides a member name. This is the one that bites:

class Book {
public:
    Book(int id, const std::string& title) {
        id = id;              // ✗ assigns the parameter to itself
        title = title;        // ✗ same
    }
private:
    int id;
    std::string title;
};

Both lines are legal, both compile, and both do nothing. The compiler cannot know which id you meant, so it assumes the innermost — the parameter. this->id = id; breaks the tie. (The course's own style is to name members with a trailing underscore, id_, so no tie arises; you will meet both conventions in real code, and this-> is also used in many codebases.)

2. When you want to return the object itself.

Book& withPages(int pages) {
    pages_ = pages;
    return *this;             // the object, not a pointer to it
}

Because it returns a reference to this same object, calls can be chained:

book.withPages(976).tagged("reference");

That works, and the object is unchanged in count — one object, two calls. Returning *this is the mechanism behind std::cout << a << b << c, and you will see it again for operator= in Module 5 and operator<< in Module 5.

Where this is not needed. If you find yourself writing this-> in front of every member, you have a convention, not a need — usually inherited from a language where it is required. It is not wrong; it is just noise unless it resolves something.


7. const member functions: a promise the compiler enforces

This is the most valuable twenty minutes of Module 2, so do not skim it.

class Loan {
public:
    int daysAllowed() const { return daysAllowed_; }   // promises not to modify
    void renew(int extraDays) { daysAllowed_ += extraDays; }  // modifies
private:
    int daysAllowed_;
};

The trailing const means: calling this function will not modify the object. It is a promise made to callers, and the compiler checks it rather than trusting you.

What it buys you is the ability to write functions that accept objects cheaply without handing over the right to modify them:

void printSummary(const Loan& loan) {
    std::cout << loan.daysAllowed() << '\n';
}
  • const Loan& avoids a copy (no object is duplicated).
  • const on the reference means the function cannot call renew.
  • Inside, only const member functions may be called — which is why daysAllowed() must be marked const.

Now the payoff. If you forget the const on daysAllowed():

error: passing 'const Loan' as 'this' argument discards qualifiers

Read that message literally and it explains itself: inside a const Loan, this is a const Loan*, so a function that might modify the object cannot be called. The three ways to respond, in order of preference:

  1. Mark the function const — if it genuinely does not modify anything, this is the correct fix and the compiler just told you the truth.
  2. Change the parameter — if the function really does need to modify the object, it should not have promised const.
  3. const_cast — almost always a bug or a design smell. The one legitimate use sits behind an abstraction where the object is logically const but physically mutable; that is Module 4's mutable.

Why this matters immediately. Every const object, every const reference, and every object inside a const container can only call const member functions. Without const-correctness, your types cannot be used in the places they most need to be used. The module's second program, examples/module-02/02-const-members/main.cpp, is nothing but this idea, executed.


8. One type, two files: the header/source split

So far the class and main have shared one file. Real programs do not work that way, and the reason is mechanical: the definition of a function must exist exactly once in the final program, but its declaration must be visible everywhere it is used.

That splits a type in two:

File Contains Why
Book.h (header) declarations: the class, its members, its function signatures every file that uses Book includes this
Book.cpp (source) definitions: the function bodies compiled once, so each function exists once

examples/module-02/header-source/ is exactly this, and building it is one command:

g++ -std=c++17 -Wall -Wextra main.cpp Book.cpp -o book_demo

Omit Book.cpp and you learn the most important error in C++:

undefined reference to `Book::describe() const'

That is a link error, not a compile error (Module 1, §2). main.cpp compiled perfectly, because it saw a declaration — a promise that the function exists. The linker then discovered nobody kept the promise. The fix is to add the missing file to the command, or to write the missing definition.

One file must include the header, not copy it. Book.cpp starts with #include "Book.h", and main.cpp does too. Neither repeats the class. A header is the single place a type is described.

Include guards. A file can include the same header twice — directly, and through another header that also includes it. Without protection the class definition appears twice in one translation unit, and the compiler reports a redefinition. The course uses:

#pragma once          // the first line of every header

#pragma once is not in the C++ standard, but every compiler in ordinary use supports it and it cannot be misspelled the way an include guard can. The portable alternative is the include guard:

#ifndef LIBRARY_BOOK_H
#define LIBRARY_BOOK_H
// ... the header ...
#endif

You will meet both. Neither is wrong; the important thing is that your headers have one of them.

Where the #includes go. Rule: a header includes only what its own declarations need. Book.h needs <string> because it declares a std::string member. It does not need <iostream>, because nothing in its declarations mentions std::cout — even though Book::describe() prints. That #include belongs in Book.cpp. Getting this right keeps compile times down and is one of the visible marks of a well-organised project in Module 16.


9. Objects talking to objects

A class on its own is only half of object orientation. Programs are made of several kinds of object sending messages to each other, and the design question is always which object owns each rule.

examples/module-02/04-objects-interacting/main.cpp has a Course with a capacity and a Student who can enrol. The enrolment is the interesting part:

bool Student::enrolIn(Course& course) {
    if (courseCount_ >= kMaxCourses) {
        return false;                 // the Student's own rule
    }
    if (!course.reserveSeat(name_)) {
        return false;                 // the Course's rule
    }
    courses_[courseCount_] = course.title();
    ++courseCount_;
    return true;
}

Three things to notice:

  • The Student asks; the Course decides. The Student never reads a "number of seats" and never compares it to anything. If it did, the capacity rule would exist in two places, and the two would eventually disagree.
  • Two rules, two owners. "At most three courses per student" belongs to the Student. "At most three students per course" belongs to the Course. Neither class knows the other's limit.
  • The objects cannot disagree. The Student records the course only if the Course agreed to take it, so it is impossible to have a Student who thinks they are enrolled in a Course that has never heard of them.

This is the whole of object-oriented design in miniature, and it is why Module 9 exists: once objects cooperate this way, the next question is who owns whom, and how long each one lives.

And a warning from the same program. One function in it takes its parameter by value:

void fillWithoutAsking(Course copy) {     // ← a copy, not the course itself
    while (copy.hasSpace()) {
        copy.reserveSeat("a passing stranger");
    }
}

It compiles. It seems to work. And it fills the copy, which is destroyed on return, so the caller's course is untouched. Module 3 explains exactly when the copy is made and destroyed, and Module 10 explains how to control it.


10. Aggregate initialisation, and what a constructor will replace

A simple struct with no constructors, no private members, no base classes and no virtual functions is an aggregate, and it can be initialised directly:

struct Point {
    int x;
    int y;
};

Point origin{0, 0};        // aggregate initialisation: members in declaration order
Point corner{5, 12};

Aggregate initialisation is exactly the mechanism that constructors exist to replace. Point is an aggregate because it declares no constructor, and the compiler generates the "assign each member in order" behaviour for you. The Book from §1 is not an aggregate — it declares a constructor and has private members — so this brace initialisation instead calls that constructor:

Book primer{1001, "C++ Primer"};    // calls Book::Book(int, const std::string&)

That is the trade in one line of code. Point{0, 0} cannot fail, because there is nothing about the data that could be wrong. Book{-1, ""} is wrong — no identifier, no title — and the only way to refuse it is to write the constructor that checks. That constructor is Module 3; this module is about the type it belongs to.

Brace initialisation also prevents one specific bug. These two lines look alike and do completely different things:

Book primer(1001, "C++ Primer");    // calls a constructor
Book primer{1001, "C++ Primer"};    // also calls a constructor, but refuses
                                    // narrowing conversions

With {}, a double silently becoming an int is an error rather than a conversion you did not notice. Prefer braces.

The most confusing sentence in C++

std::vector<int> v(); declares a function returning a vector, not an empty vector. It is called the most vexing parse, and braces fix it: std::vector<int> v{};. You do not need to memorise the rule; you need to recognise the symptom — a variable you are sure you created that the compiler treats as a function.


Checkpoint

Answer these before the worked examples. If an answer is not immediate, re-read the section beside it.

  1. List three things a class declaration can say, and say which one a caller is allowed to depend on. (§1 and §5.)
  2. What is the only language difference between class and struct? Which would you use for a Date that must never hold month 13, and why? (§2.)
  3. Predict: does this compile? If not, why not, and what is the smallest fix?
    class Counter {
    private:
        int value_ = 0;
    public:
        int value() const { return value_; }
    };
    int main() {
        Counter c;
        c.value() = 5;
    }
    
    (§7: value() returns an int by value, so this is assigning to a temporary — and it is not assignable. The compiler refuses. Explain in your own words why a function returning int& would be the wrong fix.)
  4. Why does printSummary(const Loan&) fail to compile if summary() is not marked const, and why is const_cast the wrong response? (§7.)
  5. In examples/module-02/header-source/, which file holds the declarations, which holds the definitions, and what error appears if you forget the second? (§8.)
  6. Give one rule from examples/module-02/04-objects-interacting/main.cpp that belongs to Course and one that belongs to Student, and say what would go wrong if the second moved into the first. (§9.)
  7. Predict: what does fillWithoutAsking(physics) change about physics? Why? (§9 — and Module 3 gives the machinery.)

Next: Worked examples — four annotated programs, each with a prediction prompt. · All modules