Inheritance Basics
Inheritance serves as a mechanism for code reuse, allowing new classes to acquire properties and behaviors from existing classes. The class being inherited from is called the base class (or parent class), while the new class is termed the derived class (or child class).
class Entity {
private:
char gender;
};
class Player : public Entity {
private:
std::string username;
std::string contact;
};
Access Specifiers in Inheritance
In C++, there are three types of inheritance access: public, protected, and private. The accessibility of inherited members depends on both the access specifier in the base class and the inheritance type used.
Object Slicing
When a derived class object is assigned to a base class object, pointer, or reference, a type conversion occurs. This phenomenon is known as object slicing, where the derived class-specific portion is "sliced off."
class Parent {
public:
Parent(int val = 10) : value(val) {}
void display() { std::cout << value << std::endl; }
int value;
};
class Child : public Parent {
public:
Child(int x = 20) : extra(x) {}
int extra;
};
int main() {
Child childObj(5);
Parent parentObj;
// Derived to base assignment works
parentObj = childObj;
Parent* ptr = &childObj;
Parent& ref = childObj;
// Base to derived assignment is not allowed
// childObj = parentObj; // Error
// Casting base pointer to derived pointer is risky
Child* riskyPtr = (Child*)ptr;
// riskyPtr->extra; // Potential undefined behavior
return 0;
}
Name Hiding (Redefinition)
When a derived class defines a member with the same name as one in the base class, the derived class member hides the base class member. This occurs regardless of parameter differences since the members exist in different scopes.
class Parent {
public:
Parent(int val = 10) : value(val) {}
void show() { std::cout << value << std::endl; }
int value;
};
class Child : public Parent {
public:
Child(int x = 20) : extra(x) {}
void show(int param) { // Hides Parent::show()
std::cout << value << std::endl;
std::cout << extra << std::endl;
}
double value = 15.5; // Hides Parent::value
int extra;
};
int main() {
Parent p;
p.show();
Child c;
c.show(0);
c.Parent::show(); // Access hidden base member using scope resolution
}
Note that functions with the same name in base and derived classes do not constitute function overloading, as overloading requires functions to be in the same scope.
Special Member Functions in Inheritance
Constructors
Derived class constructors must initialize base class members by calling the base class constructor. If the base class lacks a default constructor, explicit initialization is required in the derived class constructor's initialization list.
class Parent {
public:
Parent(int val) : value(val) {} // No default constructor
int value;
};
class Child : public Parent {
public:
Child(int x) : Parent(100), extra(value) { // Must explicitly call Parent constructor
}
int extra;
};
Destructors
Destructor calls occur in reverse order: derived class destructor executes first, followed by the base class destructor automatically. The compiler ensures the base destructor is called, so explicit calls should be avoided to prevent double destruction.
class Child : public Parent {
public:
~Child() {
// Base destructor called automatically after this
std::cout << "~Child()" << std::endl;
}
};
Copy Constructor and Assignment Opertaor
The derived class copy constructor and assignment operator must explicitly call their base class counterparts to properly copy base class members.
Friendship and Static Members
Friendship is not inherited. A friend of the base class does not have access to private members of the derived class. Static members, however, are shared across the entire inheritance hierarchy—there exists only one instance of a static member regardless of how many derived classes exist.
Diamond Inheritance Problem
Diamond inheritance occurs when a class inherits from two classes that both inherit from a common base class, creating ambiguity and data redundancy.
class Base {
public:
int id;
};
class Left : public Base {
public:
int leftData;
};
class Right : public Base {
public:
int rightData;
};
class Diamond : public Left, public Right {
public:
int diamondData;
};
In this scenario, Diamond contains two copies of Base::id, leading to ambiguity when accessing id and wasting memory.
Virtual Inheritance Solution
Virtual inheritance resolves the diamond problem by ensuring only one copy of the base class subobject exists.
class Base {
public:
int id;
};
class Left : virtual public Base {
public:
int leftData;
};
class Right : virtual public Base {
public:
int rightData;
};
class Diamond : public Left, public Right {
public:
int diamondData;
};
int main() {
Diamond d;
d.id = 42; // No ambiguity
return 0;
}
Virtual inheritance works through virtual base pointers and virtual base tables, which store the offset needed to locate the shared base class subobject.
Inheritance vs Composition
Inheritance (Is-A Relationship)
class Engine {
public:
int power;
};
class Car : public Engine { // Is-A: Car is an Engine? (poor design)
public:
int wheels;
};
Composition (Has-A Relationship)
class Engine {
public:
int power;
};
class Car { // Has-A: Car has an Engine (better design)
public:
Engine motor;
int wheels;
};
Composition is generally preferred over inheritance because:
- Inheritance represents "white-box" reuse with lower encapsulation—derived classes have visibility into base class implementation details.
- Composition represents "black-box" reuse with higher encapsulation—objects are used through their public interfaces without knowledge of internal implementation.
- Composition results in looser coupling between classes, making code more maintainable and flexible.