UP: C14 Explained for Beginners
Let’s go through Chapter 14 slowly, as if you’re a beginner who wants to understand deeply, not just memorize.
We’ll cover:
Static members (shared between all objects)
The
Consoleclass with static functionsFriend functions
Operator overloading (
+,-,++,<,==,<<,>>, …)Modules (
export,import, visibility vs reachability)
1. Static Members – “Shared Stuff” for the Whole Class
Intuition
Normally, each object has its own data:
Product p1;
Product p2;
p1.priceis separate fromp2.price.
But what if you want something that is shared between all Products?
Example:
Count how many
Productobjects have been created:Product::object_count.
This is what static members are:
One variable shared by all objects of that class.
Static Data Member Example (from the Product class)
In the header (Product.h):
class Product {
private:
double price;
int discount_percent;
static int object_count; // static data member
public:
std::string name;
Product(std::string name = "", double price = 0.0, int discount_percent = 0);
// ... other functions ...
static int get_object_count() { // static member function
return object_count;
}
};
In the .cpp file:
int Product::object_count = 0; // define + initialize
Product::Product(string name_param, double price, int discount_pct) {
name = name_param;
set_price(price);
set_discount_percent(discount_pct);
++object_count; // increment when each Product is created
}
Why do we initialize object_count in the .cpp file?
Because:
staticdata members exist once, not per object.The compiler needs one definition where memory is actually allocated.
The class declaration only declares it; the
.cppfile defines it.
If you try to initialize it in the class (like non-const static ints), in older standards you’d get linker errors; the book uses the traditional way for clarity.
Static Member Functions
The function:
static int get_object_count();
belongs to the class, not to a specific object.
cannot use non-static members (like
priceordiscount_percent) because it doesn’t have athispointer.
If you wrote:
static int get_object_count() {
return discount_percent; // ERROR: non-static member
}
This fails because discount_percent only exists inside an object, but this function has no specific object.
How to Access Static Members
You can access public static members in two ways:
1. Through the class (recommended)
int c = Product::get_object_count();
This is clear: you’re using a class-level thing.
2. Through an object (allowed but less clear)
Product p;
int c = p.get_object_count();
This works, but it may confuse readers into thinking get_object_count() depends on that object — it doesn’t.
Best practice:
Access static stuff using the class: ClassName::member.
2. The Console Class – Static Utility Functions
Earlier in your book, console input functions were in a namespace called console. Now they’re moved into a class with static methods.
Why Put Them in a Class?
To avoid name collisions like with namespace.
To group related functionality (
get_int,get_double,get_char) together.To show you that classes + static functions can be used like a mini “library”.
Console Class Header
class Console {
public:
Console() {} // default constructor, does nothing
static double get_double(std::string prompt,
double min = std::numeric_limits<double>::min(),
double max = std::numeric_limits<double>::max());
static int get_int(std::string prompt,
int min = std::numeric_limits<int>::min(),
int max = std::numeric_limits<int>::max());
static char get_char(std::string prompt, bool add_blank_line = true);
};
Implementation uses helpers (like discard_remaining_chars) but those helpers are not in the header, so they’re private to the .cpp file.
Using the Console Class – Two Ways
1. Call static methods directly from the class (clearer)
double mi = Console::get_double("Monthly investment: ", 0, 10000);
int years = Console::get_int("Years: ", 0, 100);
char choice = Console::get_char("Continue? (y/n): ");
2. Call via an object (shorter but not necessary)
Console c;
double mi = c.get_double("Monthly investment: ", 0, 10000);
int years = c.get_int("Years: ", 0, 100);
char choice = c.get_char("Continue? (y/n): ");
Both work because static functions can be called on instances, but they don’t depend on instance data.
When to make a function static?
If the function:
doesn’t need to access object member variables, and
conceptually belongs to the class,
then static is a good choice.
Examples:
parsing
general utilities related to the class
factory methods (like
Product::from_string())
3. Friend Functions – Letting Outsiders See Private Data
Problem
You sometimes want a non-member function to:
access private fields of a class,
without writing tons of getters/setters.
Example from the chapter: FuelTank and FuelCan.
We want a free function:
void pour(FuelTank& tank, FuelCan& can);
that:
adds
can.gallonsintotank.gallonssets
can.gallonsto 0
But gallons is private in both classes.
Solution: friend
In FuelCan.h:
class FuelTank; // forward declaration
class FuelCan {
private:
double gallons = 0;
public:
FuelCan(double gallons_param = 0) { gallons = gallons_param; }
double get_gallons() const { return gallons; }
friend void pour(FuelTank& tank, FuelCan& can);
};
In FuelTank.h:
#include "FuelCan.h"
class FuelTank {
private:
double gallons;
public:
FuelTank(double gallons_param = 0) { gallons = gallons_param; }
void set_gallons(double gallons_param) { gallons = gallons_param; }
double get_gallons() const { return gallons; }
friend void pour(FuelTank& tank, FuelCan& can);
};
In the .cpp file:
void pour(FuelTank& tank, FuelCan& can) {
tank.gallons += can.gallons; // uses private members
can.gallons = 0;
}
Because both classes declared friend void pour(...), the function is allowed to touch their private data.
When to Use Friend Functions
Good when:
Two classes need to cooperate tightly.
You’re writing operators that naturally go outside the class (like
<<and>>).You want some performance or convenience without tons of boilerplate getters.
Avoid when:
You can reasonably express the behavior with public methods.
You’d be exposing too much internal detail.
Think of friend as a controlled exception to encapsulation.
4. Operator Overloading
This is a big chunk of the chapter.
You’ll see:
Binary arithmetic operators (
+,-)Unary arithmetic operators (
++)Relational operators (
<,>,==)Stream operators (
<<,>>)
All using FuelTank as the main example.
4.1 Binary Operators (+, -) – Creating New Objects
Example: define + for FuelTank.
Declaration in FuelTank.h
FuelTank operator+(const FuelTank& right);
FuelTank operator-(const FuelTank& right);
Definition in FuelTank.cpp
FuelTank FuelTank::operator+(const FuelTank& right) {
FuelTank t;
t.set_gallons(gallons + right.gallons);
return t;
}
FuelTank FuelTank::operator-(const FuelTank& right) {
FuelTank t;
t.set_gallons(gallons - right.gallons);
return t;
}
Notice:
rightis passed byconst FuelTank&so we don’t copy or modify it.We create a new
FuelTankobject and return it.We do not modify
*this(the object on the left).
Usage
FuelTank tank1(100);
FuelTank tank2(200);
FuelTank tank3 = tank2 + tank1; // tank3 has 300 gallons
FuelTank tank4 = tank2 - tank1; // tank4 has 100 gallons
You can also write:
FuelTank tank3 = tank2.operator+(tank1); // equivalent, less readable
Rule of thumb:
Arithmetic operators like + and - are expected to not modify their operands; instead they produce a new value. Follow that expectation.
4.2 Unary Operators (++, --) – Prefix vs Postfix
The chapter focuses on ++ but the same idea applies to --.
Prefix ++x
Declaration:
FuelTank& operator++(); // no parameters
Definition:
FuelTank& FuelTank::operator++() {
++gallons; // increment this object's gallons
return *this;
}
Modifies the current object.
Returns a reference to the modified object.
Usage:
FuelTank tank1(100);
++tank1;
cout << tank1.get_gallons() << endl; // 101
You can also chain:
(++tank1).get_gallons();
Postfix x++
Declaration:
FuelTank operator++(int unused_param); // int is a dummy parameter
Definition:
FuelTank FuelTank::operator++(int unused_param) {
FuelTank temp = *this; // copy current state
++gallons; // increment this object
return temp; // return old version
}
Usage:
FuelTank tank1(100);
cout << tank1++.get_gallons() << endl; // prints 100
cout << tank1.get_gallons() << endl; // now 101
Why do we use that dummy int parameter?
It’s just to distinguish postfix
++from prefix++in the function signatures.The compiler uses the presence of that dummy
intto know “this is the postfix version”.
Performance note:
Prefix ++ (++x) is usually more efficient than postfix x++ because postfix has to make a copy. Prefer ++x unless you specifically need old value semantics.
4.3 Relational Operators (<, >, ==)
We want to compare two FuelTanks based on their gallons.
Declarations
bool operator<(const FuelTank& right);
bool operator>(const FuelTank& right);
bool operator==(const FuelTank& right);
Definitions
bool FuelTank::operator<(const FuelTank& right) {
return gallons < right.gallons;
}
bool FuelTank::operator>(const FuelTank& right) {
return gallons > right.gallons;
}
bool FuelTank::operator==(const FuelTank& right) {
return gallons == right.gallons;
}
All:
take
constreferences (no copy, no modification).return
bool.
Usage:
if (tank1 < tank2) {
cout << "Tank1 has less fuel than tank2.\n";
} else if (tank1 > tank2) {
cout << "Tank1 has more fuel than tank2.\n";
} else if (tank1 == tank2) {
cout << "Tank1 has the same fuel as tank2.\n";
}
4.4 Stream Operators (<<, >>) – Printing and Reading Objects
These operators are special because the left operand is a stream (std::ostream or std::istream), not your class. So they are typically friend functions, not member functions.
In FuelTank.h
friend std::ostream& operator<<(std::ostream& out, const FuelTank& tank);
friend std::istream& operator>>(std::istream& in, FuelTank& tank);
Definitions in .cpp
std::ostream& operator<<(std::ostream& out, const FuelTank& tank) {
out << "Gallons: " << tank.gallons << '\n'
<< "Liters: " << tank.get_liters() << '\n' << '\n';
return out; // allow chaining
}
std::istream& operator>>(std::istream& in, FuelTank& tank) {
cout << "Enter gallons: ";
in >> tank.gallons;
return in; // allow chaining
}
Note:
<<usesconst FuelTank&since it doesn’t modify the tank.>>usesFuelTank&(non-const) because it does modify the object.Both return their stream (
outorin) by reference so you can chain calls.
Chaining Usage
FuelTank tank4, tank5;
cin >> tank4 >> tank5; // read two tanks
cout << "TANK4\n" << tank4
<< "TANK5\n" << tank5; // print headings and objects
Chaining works because:
operator>>returnsistream&, so the next>>still sees a stream.operator<<returnsostream&, so multiple<<calls can be linked.
5. Modules – A Modern Alternative to Headers
This is the C++20 feature part of the chapter.
Why Modules?
Headers (.h + #include) have problems:
Multiple inclusion complexity, need include guards
Can easily leak implementation details
Can slow down compilation
Modules aim to:
give you clean interfaces with
exportseparate what is visible from how it is implemented
speed up builds
5.1 A Simple Module Exporting Functions
Module file (e.g., murach_temp.ixx):
export module murach_temp; // name the module
export double to_celsius(double fahrenheit) {
double celsius = (fahrenheit - 32.0) * 5.0 / 9.0;
return celsius;
}
export double to_fahrenheit(double celsius) {
double fahrenheit = celsius * 9.0 / 5.0 + 32.0;
return fahrenheit;
}
export module murach_temp;defines the module’s name.exportbefore a function makes it available to importers.
main.cpp:
import murach_temp; // instead of #include "murach_temp.h"
#include <iostream>
using namespace std;
int main() {
double c = to_celsius(32);
cout << "Celsius: " << c << endl;
double f = to_fahrenheit(0);
cout << "Fahrenheit: " << f << endl;
}
5.2 Modules Exporting Namespaces
You can also export an entire namespace:
export module murach_temp;
export namespace temp {
double to_celsius(double f) {
return (f - 32.0) * 5.0 / 9.0;
}
double to_fahrenheit(double c) {
return c * 9.0 / 5.0 + 32.0;
}
}
Then in main.cpp:
import murach_temp;
#include <iostream>
using namespace std;
using namespace temp; // use exported namespace
int main() {
double c = to_celsius(32);
double f = to_fahrenheit(0);
}
5.3 Modules Exporting Classes (with Global Module Fragment)
Example: murach_dice module, with Die and Dice classes.
At the top:
module; // start global module fragment
#include <cstdlib>
#include <ctime>
#include <vector>
// end global module fragment
export module murach_dice;
export class Die {
private:
int value;
public:
Die() {
srand(time(nullptr));
value = 1;
}
void roll() {
value = rand() % 6 + 1;
}
int get_value() const {
return value;
}
};
export class Dice {
private:
std::vector<Die> dice;
public:
Dice() {}
void add_die(Die die) {
dice.push_back(die);
}
void roll_all() {
for (Die& die : dice) {
die.roll();
}
}
std::vector<Die> get_dice() const {
return dice;
}
};
Note the global module fragment:
module;
#include <cstdlib>
#include <ctime>
#include <vector>
You put #includes there because:
Preprocessor should run before the module interface.
These headers are internal to the module.
main.cpp:
import murach_dice;
#include <iostream>
using namespace std;
int main() {
Dice dice;
// use dice...
}
5.4 Using export for Access Control – Visibility vs Reachability
You don’t have to export everything.
Example: only Dice is exported, Die is hidden:
module;
#include <cstdlib>
#include <ctime>
#include <vector>
export module murach_dice;
class Die { // not exported
// ...
};
export class Dice { // exported
private:
std::vector<Die> dice;
public:
Dice() {}
void add_die() {
Die die;
dice.push_back(die);
}
void roll_all() { /*...*/ }
std::vector<Die> get_dice() const { return dice; }
};
In main.cpp:
import murach_dice;
#include <iostream>
using namespace std;
int main() {
Dice dice;
for (int i = 0; i < 2; ++i) {
dice.add_die(); // creates Die internally
}
dice.roll_all();
// Can't name the Die type, it's not exported:
// for (Die d : dice.get_dice()) { } // wrong
// But you *can* use auto:
for (auto die : dice.get_dice()) {
// die.get_value() is allowed
cout << die.get_value() << " ";
}
}
Key concepts:
Dieis not visible outside the module (cannot be named in code).But it is reachable through exported members (like
get_dice()).Once you have an object (via
auto), you can call its public methods.
So:
visible = can you refer to the type by name?
reachable = can you interact with objects of that type via exported APIs?
5.5 Using import Instead of #include for Libraries
The chapter notes that in C++20, standard libraries can eventually be imported like:
import <string>;
import <stdexcept>;
instead of:
#include <string>
#include <stdexcept>
But compiler support is still incomplete, so they often mix:
import murach_movie;
import <string>; // where supported
#include <iostream> // use #include if import not supported yet
#include <vector>
Best practice:
When modules are fully supported, try to use import consistently for headers that support it.
What You Should Be Able to Do After Mastering This Chapter
Static members
Explain difference between object data and static/class data.
Write and initialize a static data member and call a static function properly.
Console-like utility classes
Design a class whose methods are all static and use it without creating objects.
Friend functions
Explain what
frienddoes and when it’s appropriate.Write a friend function that uses private members of one or more classes.
Operator overloading
Implement
+,-,<,>,==,++(prefix & postfix) and know when to return by value vs reference.Implement
<<and>>as friend functions and chain them.
Modules
Create a module that exports functions, namespaces, or classes.
Use a global module fragment appropriately.
Explain visible vs reachable members, and why
autocan be needed.
Quick Check Questions (to see where to focus next)
If you want, you can answer these, and I’ll target any weak spots:
For a class
Counter, how would you:declare and define a static
int count,increment it in the constructor, and
provide a static
get_count()?
Why can’t a static function use
this?For a
Matrixclass, would it make sense foroperator+to modify*thisand return*this? Why or why not?What does this function signature represent semantically?
MyClass& MyClass::operator++();In a module
shapes, why might you exportclass Circlebut not exportclass CircleHelper? How could users still “reach”CircleHelperobjects?