Inheritance in C++: Protected Access and Constructor Order
A C++ derived class contains its base and inherits its members. Public vs private inheritance, protected access, construction order and name hiding.
- Course: C++ study plan
- Module: Inheritance and polymorphism
- Kind: Lesson
- Reading time: 14 min
- Runtime: Clang 18 · C++20
How does inheritance work in C++?
In C++, class Derived : public Base makes every Derived object contain a complete Base subobject and gives it the base's public and protected members, so a Derived can be used wherever a Base& or Base* is expected. Write public explicitly: a class inherits privately by default. Constructors, the destructor and assignment operators are not inherited; the base constructor is called from the derived initialiser list.
Lesson
A class can be built on another: class Car : public Vehicle makes every Car contain a Vehicle and gives it that class's members. Java has the same idea with extends; C++ differs in the details that matter — an access specifier on the base itself, a protected level between public and private, a construction sequence the compiler fixes and you cannot reorder, constructors that are not inherited unless you ask, and a name-lookup rule that hides base functions wholesale. This lesson settles those mechanics. The next lesson adds virtual, which is where inheritance stops being code reuse and becomes polymorphism.
Deriving a class
#include <iostream>
#include <string>
class Vehicle {
public:
explicit Vehicle(std::string name) : name_(std::move(name)) {}
const std::string& name() const { return name_; }
int wheels() const { return wheels_; }
protected:
int wheels_ = 4;
private:
std::string name_;
};
class Car : public Vehicle {
public:
Car(std::string name, int doors) : Vehicle(std::move(name)), doors_(doors) {}
int doors() const { return doors_; }
private:
int doors_;
};
int main() {
Car car("mini", 3);
std::cout << car.name() << ' ' << car.wheels() << ' ' << car.doors() << '\n'; // mini 4 3
return 0;
}
Car is the derived class and Vehicle its base — C++ says base and derived where Java says super and sub. A Car object contains a complete Vehicle subobject (on this platform the base's members are laid out first, then the derived class's own), and every public member of Vehicle is callable on a Car as though Car had declared it. What is inherited: data members, member functions including static ones, and nested types. What is not: constructors, the destructor and the assignment operators — the derived class gets its own, which call the base's — and friendships. private members are present in the object but inaccessible to the derived class's code.
The word public before Vehicle is the inheritance access specifier, and in practice it is not optional. class Car : Vehicle means private inheritance — the default for class, just as private is the default for members — which makes every inherited member private to Car and stops a Car from being passed where a Vehicle& is expected. struct defaults to public. Write public every time you mean is-a; lesson 6 covers the rare uses of the private form.
A derived object converts implicitly to a reference or pointer to its base — const Vehicle& v = car; compiles because a Car is a Vehicle. That upcast is what makes the next lesson possible. A conversion in the other direction never happens by itself.
Three levels of access
protected sits between public and private: a protected member is accessible inside the class, inside its friends, and inside member functions of derived classes — but only through an object of the derived type. wheels_ above may be read by Car's member functions; a free function or main cannot touch it, and Car's functions cannot read wheels_ through a plain Vehicle& either, only through a Car.
| Specifier | Inside the class | In a derived class | Everywhere else |
|---|---|---|---|
public | yes | yes | yes |
protected | yes | yes | no |
private | yes | no | no |
Protected data is a weak form of public: every derived class becomes a client of the base's representation, and changing that representation breaks them all. Prefer private data with protected functions when derived classes need a controlled way in; keep protected data for small, stable hierarchies where the convenience is worth it.
Construction order
A derived class's constructor runs in three fixed stages, whatever order the initialiser list happens to write them in:
- The base class subobject is constructed — by the constructor named in the initialiser list, or by the base's default constructor when none is named.
- The derived class's members are constructed in declaration order — the same rule as a class with no base (Module 8, lesson 2).
- The constructor body runs.
struct Engine { Engine() { std::cout << "Engine "; } ~Engine() { std::cout << "~Engine "; } };
struct Base { Base() { std::cout << "Base "; } ~Base() { std::cout << "~Base "; } };
struct Derived : Base {
Engine engine;
Derived() { std::cout << "Derived "; }
~Derived() { std::cout << "~Derived "; }
};
int main() {
Derived d; // Base Engine Derived
} // ~Derived ~Engine ~Base
Destruction is the exact reverse: the derived destructor's body, then the derived members in reverse declaration order, then the base. That symmetry is why a derived destructor may still use the base's members — the base stays alive until the derived body has finished — and why a base destructor must never rely on derived state, which is already gone. It also means the base is fully built before the derived body starts, so a derived constructor can call base member functions in its body safely.
Calling the base constructor
The base constructor is called from the initialiser list, and only from there:
class Car : public Vehicle {
public:
Car(std::string name, int doors) : Vehicle(std::move(name)), doors_(doors) {}
};
If the list does not name the base, the compiler inserts Vehicle(). When the base has no default constructor — Vehicle has only Vehicle(std::string) — that is a compile error: no matching function for call to 'Vehicle::Vehicle()'. Writing Vehicle(name); inside the body instead of the list is legal C++ that does something else entirely: it constructs a temporary Vehicle and throws it away, after the base subobject was already default-constructed (or failed to be). The base always goes in the list, and it goes first by rule even when you write it second.
Inheriting constructors
Constructors are not inherited: a Car needs its own, even one that only forwards. When the derived class adds no state that needs arguments, C++11 lets you pull the base's constructors in wholesale:
class Taxi : public Vehicle {
public:
using Vehicle::Vehicle; // Taxi(std::string) now exists and forwards to Vehicle(std::string)
int fares = 0; // derived members get their default member initialisers
};
Taxi t("cab-7");
using Base::Base; declares a constructor in Taxi for every constructor of Vehicle, each forwarding its arguments to the base and leaving the derived members to their in-class initialisers. It is the right tool for thin wrappers and tag types. A derived class with members that must be set from arguments still writes its own constructor.
Name hiding
struct Printer {
void print(int v) { std::cout << "int " << v << '\n'; }
void print(double v) { std::cout << "double " << v << '\n'; }
};
struct Fancy : Printer {
void print(const std::string& s) { std::cout << "string " << s << '\n'; }
};
Fancy f;
f.print("hi"); // string hi
f.print(3); // error: no conversion from int to std::string
Declaring print in Fancy hides every print in Printer, not only the one with matching parameters. Name lookup stops at the first class, walking upward, that declares the name; overload resolution then runs on what it found there and nothing else. Java merges overloads across the hierarchy; C++ does not. Two fixes: using Printer::print; inside Fancy brings the base overloads into the derived scope so all three overload together, or call the hidden one explicitly as f.Printer::print(3). The same qualified form, Printer::print(v), is how a derived function calls the base version of the function it replaces — a habit that becomes essential the moment functions are virtual.
Pitfalls
| Mistake | What happens |
|---|---|
class D : B | Private inheritance; D is not usable as a B outside itself. |
| Base has no default constructor and the list omits it | Compile error at the derived constructor. |
Base(x); in the constructor body | A temporary; the base subobject was default-constructed already. |
| Relying on initialiser-list order | Ignored: base first, then members in declaration order. |
A derived f(double) next to a base f(int) | The base overload is hidden; add using Base::f;. |
Reading a protected member through a Base& inside Derived | Not allowed; only through a Derived object. |
| Using derived state in the base destructor | Already destroyed — the reverse order guarantees it. |
Key takeaways
class D : public Bembeds a completeBin everyDand exposesB's public members; writepublicexplicitly or you get private inheritance.protectedis visible to derived classes and friends; prefer protected functions over protected data.- Construction is base, then members in declaration order, then the body; destruction is the exact reverse.
- The base constructor is named in the initialiser list; omit it and the base's default constructor is used or the build fails.
using B::B;inherits constructors for a derived class that adds nothing needing arguments.- A derived name hides every base overload of that name;
using B::f;orB::f()gets them back.
Common questions
In what order are constructors and destructors called in C++ inheritance?
Construction runs the base class constructor first, then the derived class's members in declaration order, then the derived constructor body. Destruction is the exact reverse: the derived destructor body, then its members in reverse order, then the base destructor. The initialiser list's order changes nothing.
What is the difference between protected and private in C++?
A private member is accessible only inside the class and its friends. A protected member is also accessible in member functions of derived classes, but only through an object of the derived type. Prefer private data with protected functions; protected data couples every derived class to the base's representation.
Why is a base class function hidden in the derived class?
Declaring a function named print in a derived class hides every print in the base, not only the one with the same parameters, because name lookup stops at the first class that declares the name. Add using Base::print; to bring the base overloads back, or call Base::print(x) explicitly.
What is the difference between public and private inheritance in C++?
Public inheritance means is-a: the base's public members stay public and a derived object converts to a Base&. Private inheritance, the default for class, makes every inherited member private and hides the relationship, so outside code cannot use the derived class as the base; it means "implemented in terms of".
How do I inherit constructors in C++?
Write using Base::Base; in the derived class (C++11). It declares a derived constructor for each base constructor, forwarding the arguments, while the derived members take their default member initialisers. A derived class with members that need arguments still writes its own constructor.
Exercises
Construction and destruction tracer
Complete a three-level hierarchy whose constructors and destructors announce themselves, then let the scaffold build the objects and tear them down.
Vehicle holds a protected std::string name_ and prints Vehicle(<name>) when constructed and ~Vehicle(<name>) when destroyed. Car : public Vehicle adds an Engine member (which prints Engine(<name>) / ~Engine(<name>)) and prints Car(<name>) / ~Car(<name>). SportsCar : public Car adds a Turbo member (Turbo(<name>) / ~Turbo(<name>)) and prints SportsCar(<name>) / ~SportsCar(<name>). Every constructor takes the name and passes it to its base in the initialiser list; the members are initialised from the same name.
The starter's build function constructs the objects one per recursive call, so they are all alive at once and die in reverse order when the calls return. It prints -- all built -- between the two phases. Your job is the classes; the order is the language's.
Input: n, then n lines of kind name where kind is vehicle, car or sportscar. Output: the construction trace, the separator, the destruction trace.
2
car mini
sportscar zed
prints
Vehicle(mini)
Engine(mini)
Car(mini)
Vehicle(zed)
Engine(zed)
Car(zed)
Turbo(zed)
SportsCar(zed)
-- all built --
~SportsCar(zed)
~Turbo(zed)
~Car(zed)
~Engine(zed)
~Vehicle(zed)
~Car(mini)
~Engine(mini)
~Vehicle(mini)Plain account, fee account
Write Account with a protected long long balance_, a constructor taking the opening balance, void deposit(long long), bool withdraw(long long) (returns false and changes nothing when the amount exceeds the balance) and long long balance() const.
Write FeeAccount : public Account that inherits the constructor with using Account::Account;, keeps a class constant kFee = 2, and declares its own bool withdraw(long long amount) — which hides the base's — implemented as one call to the base version: Account::withdraw(amount + kFee). A withdrawal that would overdraw after the fee is rejected and nothing changes.
The program opens one of each with the same opening balance and applies every command to both.
Input: the opening balance, then lines of deposit x or withdraw x until the input ends. Output: after each command, plain=<balance> fee=<balance>; at the end, rejected plain=<count> fee=<count>.
100
deposit 50
withdraw 20
withdraw 130
withdraw 128
prints
plain=150 fee=150
plain=130 fee=128
plain=0 fee=128
plain=0 fee=128
rejected plain=1 fee=2In this module: Inheritance and polymorphism
- Inheritance basics — deriving, protected and the order of construction (this lesson)
- Virtual functions — static type, dynamic type and override
- Abstract classes and interfaces — programming to a contract
- Virtual destructors and slicing — the two ways a hierarchy loses data
- RTTI and the vtable — how dispatch works and when to ask an object its type
- Composition versus inheritance — is-a, has-a and the shapes to avoid
- Checkpoint — Inheritance and polymorphism
← Checkpoint — Copies, moves and the rule of five · Virtual functions — static type, dynamic type and override →