What Is Extending a Derived Class?

A derived class starts life with everything its base class offers, and extending it means declaring additional member variables and member functions that only the derived class has. The base class is untouched, the inherited members keep working exactly as before, and the derived class ends up as a strict superset: everything the base could do, plus whatever you added.

That last point is worth pausing on, because it is the practical reason inheritance is a code reuse tool at all. You are not copying the base class and editing the copy. You are declaring the difference, and the compiler assembles the rest.

Adding Members Is Ordinary Class Declaration

There is no special syntax for this. Whatever you would write inside a standalone class, you write inside the derived class.

Start with a base class that models a package arriving at a depot. It knows its weight and how to record itself on intake:

#include <iostream>

class Shipment
{
public:
    Shipment(double weightKg)
        : m_weightKg{ weightKg }
    {
    }

    void logIntake() const { std::cout << "recorded at the depot" << '\n'; }

protected:
    double m_weightKg{};
};

A derived class for temperature-controlled freight begins with nothing but a constructor, which forwards the weight up to the base class constructor through its member initializer list:

class ChilledShipment : public Shipment
{
public:
    ChilledShipment(double weightKg)
        : Shipment{ weightKg }
    {
    }
};

At this point ChilledShipment can already do everything Shipment can do, and nothing more. The extension is the next step: the hold temperature needs somewhere to live, and a chilled shipment needs to answer questions a plain shipment cannot. Those go straight into the class body:

class ChilledShipment : public Shipment
{
public:
    ChilledShipment(double weightKg, double holdCelsius)
        : Shipment{ weightKg }, m_holdCelsius{ holdCelsius }
    {
    }

    double weightKg() const { return m_weightKg; }
    double holdCelsius() const { return m_holdCelsius; }

    std::string_view coolant() const
    {
        return m_holdCelsius < -10.0 ? "dry ice" : "gel packs";
    }

private:
    double m_holdCelsius{};
};

Three kinds of addition appear here, and all three are declared the same ordinary way. m_holdCelsius is new state. holdCelsius() and coolant() are new behaviour built on that new state. weightKg() is new behaviour built on inherited state, which is the case worth studying next.

What the New Members Can Reach

weightKg() is a function declared in ChilledShipment that reads m_weightKg, a variable declared in Shipment. That works because of one word in the base class: protected.

Base class member is Code inside the derived class can use it Code outside, through a derived object
public yes yes
protected yes no
private no no

protected exists precisely for this situation. It opens a member to the class family without opening it to the rest of the program. Had m_weightKg been private, ChilledShipment::weightKg() would not compile, and the derived class would have to reach the weight through some public or protected function the base class chose to provide.

Best Practice
Favour private data with protected accessor functions over protected data. Protected member variables become part of the contract you owe every derived class, so changing how the base class stores something later can break code you do not own. A protected accessor lets the representation change while the derived classes keep compiling.
Note
Adding an accessor in the derived class, as we did with weightKg(), is not the usual arrangement. Normally that accessor belongs in the base class where every derived class can benefit from it. It sits in ChilledShipment here only to show that a newly added function can work with inherited data.

A Complete Example

Here is the whole thing running, with a second derived class alongside it so you can see two different extensions of the same base:

#include <iostream>
#include <string_view>

class Shipment
{
public:
    Shipment(double weightKg)
        : m_weightKg{ weightKg }
    {
    }

    void logIntake() const { std::cout << "recorded at the depot" << '\n'; }

protected:
    double m_weightKg{};
};

class ChilledShipment : public Shipment
{
public:
    ChilledShipment(double weightKg, double holdCelsius)
        : Shipment{ weightKg }, m_holdCelsius{ holdCelsius }
    {
    }

    double weightKg() const { return m_weightKg; }
    double holdCelsius() const { return m_holdCelsius; }

    std::string_view coolant() const
    {
        return m_holdCelsius < -10.0 ? "dry ice" : "gel packs";
    }

private:
    double m_holdCelsius{};
};

class FragileShipment : public Shipment
{
public:
    FragileShipment(double weightKg, int maxStack)
        : Shipment{ weightKg }, m_maxStack{ maxStack }
    {
    }

    double grossWeight() const { return m_weightKg; }
    int stackLimit() const { return m_maxStack; }

private:
    int m_maxStack{};
};

int main()
{
    ChilledShipment vaccineCrate{ 12.5, -18.0 };
    FragileShipment glassware{ 40.0, 3 };

    vaccineCrate.logIntake();
    std::cout << "packed with " << vaccineCrate.coolant() << '\n';

    glassware.logIntake();
    std::cout << "gross: " << glassware.grossWeight() << " kg" << '\n';
    std::cout << "stack limit: " << glassware.stackLimit() << '\n';

    return 0;
}
recorded at the depot
packed with dry ice
recorded at the depot
gross: 40 kg
stack limit: 3

Both derived classes call logIntake() without either of them declaring it, and both store a weight without either of them declaring m_weightKg. That is the reuse. What each one adds on top is entirely its own business, and the two lists of additions have nothing to do with each other.

Additions Only Travel Downward

Inheritance is a one-way relationship. ChilledShipment is-a Shipment, so it can see into Shipment. The reverse does not hold, and the compiler is blunt about it. This version does not compile:

int main()
{
    Shipment pallet{ 40.0 };

    pallet.logIntake();
    std::cout << pallet.coolant() << '\n';

    return 0;
}

GCC rejects the second call with error: 'class Shipment' has no member named 'coolant', and it is right to. coolant() was declared in ChilledShipment, and pallet is not a ChilledShipment. A base class is written without any knowledge that derived classes will ever exist, so it cannot possibly know what they added.

The same restriction applies sideways. FragileShipment inherits from Shipment, not from ChilledShipment, so weightKg() is not available to it. That is exactly why it had to declare grossWeight() for itself even though both functions do the same trivial job. Additions flow down a single branch of the hierarchy, never up and never across.

When to Extend Instead of Editing the Base

In the example above we own Shipment, so we could have dropped coolant() straight into it. Sometimes that is the right call, and sometimes it is not. The deciding question is not whether you can edit the base class, but who the change is for.

Situation Where the new code belongs
Every current and future shipment needs it the base class
Only refrigerated freight needs it the derived class
The base class ships from a vendor who will send updates the derived class
The base class comes from the standard library, or as a header plus a precompiled binary the derived class, because there is no other option

The two rows about code you did not write are the ones people underestimate. Editing a vendor's source is not a one-time cost, it is a cost you pay again at every update, either by losing your changes or by hand-merging them back in. And you simply cannot edit std::string or a library you only received as a header and a .lib file. Deriving sidesteps both problems: your additions live in your own file, in your own class, and the next vendor update drops in cleanly underneath them.

Warning
Deriving purely to bolt a helper function onto a class is a common misuse of inheritance. Inheritance should mean is-a. If your derived class is not genuinely a kind of the base class, a free function taking the base type, or a class that holds one as a member, is usually the better design.

Summary

Extending a derived class: A derived class inherits the full public and protected interface of its base class and can then declare additional member variables and member functions of its own. No special syntax is involved: new members are declared in the derived class exactly as they would be in any other class.

Access to inherited data: New derived class functions can use public and protected members of the base class directly, but not private ones. protected is what makes a base class member usable by the family without exposing it to the rest of the program, and favouring private data with protected accessors keeps the base class free to change how it stores things.

One-way relationship: Derived classes see their base classes; base classes never see their derived classes, and sibling derived classes never see each other. Calling a derived class function on a base class object is a compile error, because the base class knows nothing about what was added below it.

Extend or edit: Put a change in the base class when it belongs to every derived class, and in a derived class when it belongs to only one of them. When the base class comes from a vendor or from the standard library, deriving is the only approach that survives an update, and sometimes the only approach available at all.