Base and derived classes
Inheritance, operators, and templates12 min readLesson 1 of 5
1.1 An is-a relationship
Public inheritance models an is-a relationship. A Dog is an Animal, so code that works with an Animal should also accept a Dog. The derived class inherits the base class's members, and it can add members of its own.
The syntax class Dog : public Animal places Animal as a public base. The public keyword keeps the base's public members public in the derived class and its protected members protected. Private members of the base stay inaccessible to the derived class, although they still exist in every derived object.
#include <iostream>
#include <string>
class Animal {
public:
explicit Animal(std::string name) : name_(name) {}
void eat() const { std::cout << name_ << " eats\n"; }
protected:
std::string name_;
};
class Dog : public Animal {
public:
Dog() : Animal("Dog") {}
void fetch() const { std::cout << name_ << " fetches\n"; }
};
int main() {
Dog dog;
dog.eat(); dog.fetch();
return 0;
}Output
Dog eats Dog fetches
1.1.1 Construction runs from the base down
When a derived object is created, the base part is constructed first, then the derived class's members, then the derived constructor body. Destruction reverses that order. A derived constructor passes arguments to the base constructor in its initializer list.
If the base has no default constructor, every derived constructor must name one. Dog() : Animal("Dog") {} calls the base constructor with a string. Without that call, the compiler tries to use a base default constructor and reports an error when none exists.
#include <iostream>
class Vehicle {
public:
explicit Vehicle(int wheels) : wheels_(wheels) {
std::cout << "vehicle with " << wheels_ << " wheels\n";
}
protected:
int wheels_;
};
class Bike : public Vehicle {
public:
Bike() : Vehicle(2) { std::cout << "bike ready\n"; }
};
int main() {
Bike bike;
return 0;
}Output
vehicle with 2 wheels bike ready
1.1.2 Protected members
A protected member is visible to the class and its derived classes, and hidden from the rest of the program. It lets a derived class reuse base state without exposing that state as part of the public interface.
Protected data is still shared state. A derived class can change it and break a base class rule, and every derived class becomes coupled to the base representation. Prefer private data with protected member functions when derived classes need controlled access. Use protected data for small hierarchies where the base is designed for extension.
#include <iostream>
class Account {
protected:
void set_balance(int balance) { balance_ = balance; }
public:
int balance() const { return balance_; }
private:
int balance_ = 0;
};
class Savings : public Account {
public:
void deposit(int amount) { set_balance(balance() + amount); }
};
int main() {
Savings account;
account.deposit(40);
std::cout << "Balance: " << account.balance() << "\n";
return 0;
}Output
Balance: 40
1.1.3 Name hiding
A member function in a derived class hides base functions with the same name, even when the parameter lists differ. If Logger has log(int) and TimedLogger declares log(const std::string&), then timed.log(7) does not compile, because the derived name hides the whole base overload set.
Call the hidden function with a qualified name: timed.Logger::log(7). A using Logger::log; declaration inside the derived class brings the base overloads back into the derived scope. Hiding is easy to miss because the base function still exists; the compiler simply stops looking for it once it finds the derived name.
#include <iostream>
#include <string>
class Logger {
public:
void log(int code) const { std::cout << "code " << code << "\n"; }
};
class TimedLogger : public Logger {
public:
void log(const std::string& message) const {
std::cout << "[time] " << message << "\n";
}
};
int main() {
TimedLogger logger;
logger.log("ready");
logger.Logger::log(7);
return 0;
}Output
[time] ready code 7
1.1.4 Composition and inheritance
Inheritance answers an is-a question. Composition answers a has-a question: a Car has an Engine, so an Engine member makes more sense than an Engine base. Composition reuses a class without claiming that the outer type is a kind of the inner type.
Composition also keeps interfaces independent. An Engine can change its functions, and Car decides which of them to expose. Inheritance ties the derived class to the base interface, and a change to the base reaches every derived class. When both designs seem possible, composition is usually easier to change later.
#include <iostream>
class Engine {
public:
void start() const { std::cout << "engine started\n"; }
};
class Car {
public:
void start() const {
engine_.start();
std::cout << "car ready\n";
}
private:
Engine engine_;
};
int main() {
Car car;
car.start();
return 0;
}Output
engine started car ready
1.1.5 Common mistakes
- Forgetting
publicin the base list, which makes aclassuse private inheritance. - Expecting a derived class to reach private base members.
- Forgetting to call a base constructor that has no default constructor.
- Assuming a derived name overloads a base function when it hides it.
- Using inheritance for code reuse when the types have no is-a relationship.
- Destroying a derived object through a base pointer that has no virtual destructor.
1.1.6 Practice
- Write an
Animalbase with a name and aneatfunction, then aCatclass that addspurr. - Give the base a constructor that takes a string and call it from the derived constructor.
- Move a base member from protected data to a protected function and update the derived class.
- Replace an inheritance relationship with a member object and compare the two call sites.
- Explain why
timed.log(7)fails when the derived class declareslog(const std::string&).
