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:
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.
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:
- Write the invariant, if there is one, in one sentence: "a book always has a non-empty title and a positive identifier."
- Write the operations the rest of the program needs, as if the objects already existed and worked perfectly.
- 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.
Because it returns a reference to this same object, calls can be chained:
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:
const Loan&avoids a copy (no object is duplicated).conston the reference means the function cannot callrenew.- Inside, only
constmember functions may be called — which is whydaysAllowed()must be markedconst.
Now the payoff. If you forget the const on daysAllowed():
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:
- Mark the function
const— if it genuinely does not modify anything, this is the correct fix and the compiler just told you the truth. - Change the parameter — if the function really does need to modify the object,
it should not have promised
const. 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'smutable.
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:
Omit Book.cpp and you learn the most important error in C++:
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 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:
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:
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.
- List three things a class declaration can say, and say which one a caller is allowed to depend on. (§1 and §5.)
- What is the only language difference between
classandstruct? Which would you use for aDatethat must never hold month 13, and why? (§2.) - Predict: does this compile? If not, why not, and what is the smallest fix?
(§7:
class Counter { private: int value_ = 0; public: int value() const { return value_; } }; int main() { Counter c; c.value() = 5; }value()returns anintby value, so this is assigning to a temporary — and it is not assignable. The compiler refuses. Explain in your own words why a function returningint&would be the wrong fix.) - Why does
printSummary(const Loan&)fail to compile ifsummary()is not markedconst, and why isconst_castthe wrong response? (§7.) - 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.) - Give one rule from
examples/module-02/04-objects-interacting/main.cppthat belongs toCourseand one that belongs toStudent, and say what would go wrong if the second moved into the first. (§9.) - Predict: what does
fillWithoutAsking(physics)change aboutphysics? Why? (§9 — and Module 3 gives the machinery.)
Next: Worked examples — four annotated programs, each with a prediction prompt. · All modules