Derived Class and Base Class in C++

The Relationship Between Base and Derived Classes

In object-oriented programming with C++, the relationship between base classes and derived classes establishes a hierarchy where derived classes inherit properties and behaviors from base classes. This relationship is fundamental to inheritance and allows for code reuse and the extension of existing functionality.

Understanding Base Classes

A base class (also called a parent class or superclass) is a class that provides properties and methods that can be inherited by other classes. It defines the common attributes and behaviors that all its derived classes will share.

Characteristics of Base Classes

  1. Foundation for derived classes: Provides a template of attributes and methods
  2. Can be abstract or concrete: An abstract base class cannot be instantiated directly
  3. Encapsulates common functionality: Centralizes shared code
  4. Supports polymorphic behavior: Enables runtime determination of object types

Creating a Base Class

#include <iostream>
using namespace std;

// Base class
class Shape {
protected:
    double width;
    double height;
    
public:
    // Constructor
    Shape(double w = 0, double h = 0) : width(w), height(h) {}
    
    // Member functions
    void setWidth(double w) {
        width = w;
    }
    
    void setHeight(double h) {
        height = h;
    }
    
    // This function might be overridden by derived classes
    virtual double area() {
        cout << "Parent class area function" << endl;
        return 0;
    }
    
    // Display dimensions
    void displayDimensions() {
        cout << "Width: " << width << ", Height: " << height << endl;
    }
};

Understanding Derived Classes

A derived class (also called a child class or subclass) is a class that inherits properties and methods from a base class. It can extend the base class by adding new functionality or modifying inherited functionality.

Characteristics of Derived Classes

  1. Inherits features from base class: Gets access to base class members based on inheritance mode
  2. Can add new members: Extends functionality with additional properties and methods
  3. Can override base class methods: Customizes behavior inherited from the base class
  4. Is a specialization of the base class: Represents a more specific type

Creating a Derived Class

// Derived class - Rectangle
class Rectangle : public Shape {
public:
    // Constructor that calls base class constructor
    Rectangle(double w = 0, double h = 0) : Shape(w, h) {}
    
    // Override area function
    double area() override {
        return width * height;
    }
};

// Another derived class - Triangle
class Triangle : public Shape {
public:
    // Constructor that calls base class constructor
    Triangle(double w = 0, double h = 0) : Shape(w, h) {}
    
    // Override area function
    double area() override {
        return (width * height) / 2;
    }
};

Comparing Base and Derived Class Functionality

int main() {
    Shape* shape;      // Base class pointer
    Rectangle rect(5, 4);
    Triangle tri(5, 4);
    
    // Assigning Rectangle object to Shape pointer
    shape = &rect;
    // Accessing Rectangle object through Shape pointer
    cout << "Rectangle area: " << shape->area() << endl;
    
    // Assigning Triangle object to Shape pointer
    shape = &tri;
    // Accessing Triangle object through Shape pointer
    cout << "Triangle area: " << shape->area() << endl;
    
    return 0;
}

Output:

Rectangle area: 20
Triangle area: 10

How Base and Derived Classes Work Together

1. The “is-a” Relationship

Inheritance establishes an “is-a” relationship between the derived class and base class. For example, a Rectangle “is a” Shape.

Rectangle rect(10, 5);
Shape* shapePtr = &rect;  // Valid because a Rectangle is a Shape

2. Object Construction and Destruction Order

When a derived class object is created:

  1. The base class constructor executes first
  2. Then the derived class constructor executes

When a derived class object is destroyed:

  1. The derived class destructor executes first
  2. Then the base class destructor executes
#include <iostream>
using namespace std;

class Base {
public:
    Base() {
        cout << "Base constructor" << endl;
    }
    
    ~Base() {
        cout << "Base destructor" << endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        cout << "Derived constructor" << endl;
    }
    
    ~Derived() {
        cout << "Derived destructor" << endl;
    }
};

int main() {
    {
        cout << "Creating a Derived object:" << endl;
        Derived d;
        cout << "Derived object goes out of scope:" << endl;
    }
    
    return 0;
}

Output:

Creating a Derived object:
Base constructor
Derived constructor
Derived object goes out of scope:
Derived destructor
Base destructor

3. Derived Classes and Base Class Constructors

Derived classes can call specific base class constructors using initializer lists:

#include <iostream>
using namespace std;

class Base {
private:
    int value;
    
public:
    Base() : value(0) {
        cout << "Base default constructor" << endl;
    }
    
    Base(int v) : value(v) {
        cout << "Base parameterized constructor with value " << value << endl;
    }
    
    int getValue() const {
        return value;
    }
};

class Derived : public Base {
private:
    int data;
    
public:
    // Using Base default constructor
    Derived() : data(0) {
        cout << "Derived default constructor" << endl;
    }
    
    // Using Base parameterized constructor
    Derived(int b, int d) : Base(b), data(d) {
        cout << "Derived parameterized constructor with data " << data << endl;
    }
    
    void display() {
        cout << "Base value: " << getValue() << ", Derived data: " << data << endl;
    }
};

int main() {
    cout << "Creating Derived object with default constructor:" << endl;
    Derived d1;
    d1.display();
    
    cout << "\nCreating Derived object with parameterized constructor:" << endl;
    Derived d2(100, 200);
    d2.display();
    
    return 0;
}

Output:

Creating Derived object with default constructor:
Base default constructor
Derived default constructor
Base value: 0, Derived data: 0

Creating Derived object with parameterized constructor:
Base parameterized constructor with value 100
Derived parameterized constructor with data 200
Base value: 100, Derived data: 200

4. Method Overriding vs. Method Hiding

Method Overriding: When a derived class provides a specific implementation for a method that is already defined in the base class. Requires the virtual keyword in the base class for runtime polymorphism.

Method Hiding: When a derived class method has the same name but different parameters than a base class method. This is function overloading, not overriding.

#include <iostream>
using namespace std;

class Base {
public:
    // Virtual function - can be overridden
    virtual void show() {
        cout << "Base show()" << endl;
    }
    
    // Non-virtual function - can be hidden
    void display() {
        cout << "Base display()" << endl;
    }
    
    // Overloaded function
    void print(int x) {
        cout << "Base print(int): " << x << endl;
    }
};

class Derived : public Base {
public:
    // Overrides Base::show()
    void show() override {
        cout << "Derived show()" << endl;
    }
    
    // Hides Base::display()
    void display() {
        cout << "Derived display()" << endl;
    }
    
    // Hides Base::print(int)
    void print(double x) {
        cout << "Derived print(double): " << x << endl;
    }
};

int main() {
    Base baseObj;
    Derived derivedObj;
    
    cout << "Direct calls:" << endl;
    baseObj.show();      // Calls Base::show()
    derivedObj.show();   // Calls Derived::show()
    
    baseObj.display();   // Calls Base::display()
    derivedObj.display(); // Calls Derived::display()
    
    baseObj.print(10);   // Calls Base::print(int)
    // derivedObj.print(10);  // Error: Base::print(int) is hidden
    derivedObj.print(10.5); // Calls Derived::print(double)
    
    cout << "\nPolymorphic calls:" << endl;
    Base* ptr = &derivedObj;
    ptr->show();     // Calls Derived::show() - polymorphism
    ptr->display();  // Calls Base::display() - no polymorphism
    ptr->print(10);  // Calls Base::print(int) - no polymorphism
    
    return 0;
}

Output:

Direct calls:
Base show()
Derived show()
Base display()
Derived display()
Base print(int): 10
Derived print(double): 10.5

Polymorphic calls:
Derived show()
Base display()
Base print(int): 10

5. Function Overriding with Covariant Return Types

C++ allows a derived class to override a virtual function with a different return type if the return type of the derived class method is a pointer or reference to a class that is derived from the return type of the base class method.

#include <iostream>
using namespace std;

class Base {
public:
    virtual Base* getData() {
        cout << "Base::getData()" << endl;
        return this;
    }
};

class Derived : public Base {
public:
    // Covariant return type - returns Derived* instead of Base*
    Derived* getData() override {
        cout << "Derived::getData()" << endl;
        return this;
    }
};

int main() {
    Base baseObj;
    Derived derivedObj;
    
    Base* basePtr = baseObj.getData();     // Returns Base*
    Derived* derivedPtr = derivedObj.getData(); // Returns Derived*
    
    // Polymorphic behavior
    Base* polyPtr = &derivedObj;
    Base* returnedPtr = polyPtr->getData(); // Calls Derived::getData()
    
    return 0;
}

Output:

Base::getData()
Derived::getData()
Derived::getData()

Access Control and Member Visibility

The access control in derived classes depends on the access specifier of the base class member and the inheritance mode:

#include <iostream>
using namespace std;

class Base {
public:
    int publicVar;
    
protected:
    int protectedVar;
    
private:
    int privateVar;
    
public:
    Base() : publicVar(1), protectedVar(2), privateVar(3) {}
    
    void displayBase() {
        cout << "Base - publicVar: " << publicVar 
             << ", protectedVar: " << protectedVar 
             << ", privateVar: " << privateVar << endl;
    }
};

class PublicDerived : public Base {
public:
    void displayPublicDerived() {
        cout << "PublicDerived - publicVar: " << publicVar 
             << ", protectedVar: " << protectedVar << endl;
        // privateVar is not accessible here
    }
};

class ProtectedDerived : protected Base {
public:
    void displayProtectedDerived() {
        cout << "ProtectedDerived - publicVar: " << publicVar 
             << ", protectedVar: " << protectedVar << endl;
        // privateVar is not accessible here
    }
};

class PrivateDerived : private Base {
public:
    void displayPrivateDerived() {
        cout << "PrivateDerived - publicVar: " << publicVar 
             << ", protectedVar: " << protectedVar << endl;
        // privateVar is not accessible here
    }
    
    // Expose base method
    void callBaseDisplay() {
        displayBase();
    }
};

int main() {
    PublicDerived pubObj;
    ProtectedDerived protObj;
    PrivateDerived privObj;
    
    // Access via PublicDerived (public inheritance)
    pubObj.publicVar = 10;  // OK
    // pubObj.protectedVar = 20;  // Error: protected
    // pubObj.privateVar = 30;    // Error: private
    pubObj.displayBase();      // OK
    pubObj.displayPublicDerived();
    
    // Access via ProtectedDerived (protected inheritance)
    // protObj.publicVar = 40;  // Error: protected in derived class
    // protObj.protectedVar = 50;  // Error: protected
    // protObj.privateVar = 60;    // Error: private
    // protObj.displayBase();      // Error: protected in derived class
    protObj.displayProtectedDerived();
    
    // Access via PrivateDerived (private inheritance)
    // privObj.publicVar = 70;  // Error: private in derived class
    // privObj.protectedVar = 80;  // Error: private in derived class
    // privObj.privateVar = 90;    // Error: private
    // privObj.displayBase();      // Error: private in derived class
    privObj.displayPrivateDerived();
    privObj.callBaseDisplay();  // OK, through derived class public method
    
    return 0;
}

Type Relationships and Conversions

Implicit Conversions

A derived class object can be implicitly converted to a base class object (upcasting):

void processShape(Shape& shape) {
    cout << "Shape area: " << shape.area() << endl;
}

int main() {
    Rectangle rect(5, 3);
    Triangle tri(5, 3);
    
    // Implicit conversion from Rectangle to Shape
    processShape(rect);
    
    // Implicit conversion from Triangle to Shape
    processShape(tri);
    
    return 0;
}

Explicit Downcasting

Converting a base class pointer or reference to a derived class pointer requires explicit casting:

#include <iostream>
using namespace std;

class Base {
public:
    virtual void show() {
        cout << "Base class show()" << endl;
    }
    
    virtual ~Base() {}  // Virtual destructor for safe polymorphic deletion
};

class Derived : public Base {
public:
    void show() override {
        cout << "Derived class show()" << endl;
    }
    
    void derivedOnly() {
        cout << "Function only in Derived class" << endl;
    }
};

int main() {
    Base* basePtr;
    Derived derivedObj;
    Derived* derivedPtr;
    
    // Upcasting (implicit conversion)
    basePtr = &derivedObj;
    basePtr->show();  // Calls Derived::show()
    
    // basePtr->derivedOnly();  // Error: Base doesn't have this method
    
    // Downcasting (requires explicit cast)
    derivedPtr = static_cast<Derived*>(basePtr);
    derivedPtr->derivedOnly();  // OK now
    
    // Safe downcasting with dynamic_cast
    Base baseObj;
    basePtr = &baseObj;
    
    // This will result in nullptr because basePtr points to a Base object
    derivedPtr = dynamic_cast<Derived*>(basePtr);
    
    if (derivedPtr) {
        derivedPtr->derivedOnly();
    } else {
        cout << "Conversion failed: not a Derived object" << endl;
    }
    
    return 0;
}

Output:

Derived class show()
Function only in Derived class
Conversion failed: not a Derived object

Best Practices for Base and Derived Classes

  1. Use clear “is-a” relationships: A derived class should logically be a specialized type of its base class.

  2. Make base class destructors virtual: This ensures proper cleanup when deleting derived objects through base class pointers.

  3. Use override keyword: When overriding virtual functions, use the override keyword to catch errors.

  4. Avoid deep inheritance hierarchies: Prefer flatter hierarchies for better maintainability.

  5. Initialize base classes properly: Always use the constructor initialization list to pass parameters to base class constructors.

  6. Consider protected versus private members: Use protected for members that derived classes need to access.

  7. Follow the Liskov Substitution Principle: A derived class should be substitutable for its base class without affecting program correctness.

Summary

The relationship between base and derived classes forms the foundation of inheritance in C++. Base classes provide shared functionality, while derived classes extend or specialize this functionality. Understanding how to properly design and use base and derived classes is essential for creating effective object-oriented code that is both maintainable and extensible.