C++ Inheritance Mechanics: Access Modifiers, Construction Order, and Name Hiding
Inheritance Access Control and Rules
In C++, inheritance facilitates code reuse at the structural level. A derived class embeds the member variables of its base class rather than duplicating the logic. The syntax specifies the derived class, the inheritance mode, and the base class: class Derived : public Base.
Access Specifiers vs. Inheritance Modes
The accessibility of an inherited member is determined by the most restrictive qualifier between the base class member's access specifier and the inheritance mode. The hierarchy of accessibility from most to least permissive is public, protected, and private. For instance, if a public member is inherited using protected inheritance, it becomes protected in the derived class.
#include <iostream>
class BaseModule {
public:
int publicId = 10;
protected:
int internalState = 20;
private:
int secretKey = 30;
};
class DerivedModule : public BaseModule {
public:
void display() {
std::cout << publicId << " " << internalState << std::endl;
}
int extraValue = 40;
};
int main() {
DerivedModule dm;
dm.display();
return 0;
}Although private members of the base class are inherited and occupy memory within the derived class object, they are strictly inaccessible to the derived class. A derived class cannot modify or read these members directly.
Friendship and Inheritance
Friendship is not inherited. If a function or class is declared as a friend of the base class, it does not gain access to the private or protected members of the derived class unless explicitly granted friendship there.
Differentiating Protected and Private
The protected specifier exists specifically for inheritance hierarchies. Both protected and private members are inaccessible from outside the class, but protected members remain visible to derived classes, whereas private members are completely hidden from all derived classes.
Default Inheritance Modes
If the inheritance mode is omitted, the default depends on the keyword used to define the derived class. The class keyword defaults to private inheritance, while the struct keyword defaults to public inheritance.
Constructor and Destructor Behavior in Inheritance
Initializing Base Class Members
When constructing a derived object, the base class portion must be initialized using the base class's constructor. You cannot directly initialize inherited base members in the derived class initializer list. The base subobject is treated as a single entity.
#include <iostream>
class NetworkNode {
public:
NetworkNode(int ip, int port) : ipAddress(ip), portNumber(port) {}
void printStatus() { std::cout << ipAddress << ":" << portNumber << std::endl; }
int ipAddress;
protected:
int portNumber;
};
class SecureNode : public NetworkNode {
public:
SecureNode() : NetworkNode(192, 8080), encryptionLevel(5) {}
void printStatus() { std::cout << "Encrypted:" << encryptionLevel << std::endl; }
int encryptionLevel;
};
int main() {
SecureNode node;
node.NetworkNode::printStatus();
node.printStatus();
return 0;
}Function Hiding (Redefinition)
When a derived class defines a function with the same name as a function in the base class, the base class function is hidden. This is not function overloading, because overloading requires both functions to exist within the same scope. Because the functions reside in different class scopes, the derived class's version hides all base class versions with the same name, regardless of parameter differences. To invoke the hidden base class function, the scope resolution operator must be used (e.g., Base::functionName()).
Construction and Destruction Order
Construction always occurs from the base class to the derived class (base first, derived second). Destruction follows the reverse order (derived first, base second). This order guarantees that base class members remain valid while the derived class is being constructed or destructed.
Internally, destructors are mangled to the same name (often destructor), which causes the base class destructor to be hidden by the derived class destructor. However, you must never explicitly call the base class destructor (e.g., Base::~Base()) within the derived class destructor. The compiler automatically invokes the base destructor after the derived destructor completes. Explicitly calling it results in double deletion and undefined behavior.
#include <iostream>
class ResourceHolder {
public:
int* buffer = new int(100);
virtual ~ResourceHolder() { delete buffer; std::cout << "Base destroyed" << std::endl; }
};
class ExtendedHolder : public ResourceHolder {
public:
ExtendedHolder() : ptr(buffer) {}
~ExtendedHolder() {
// Explicit call to base destructor is erroneous and causes use-after-free
// ResourceHolder::~ResourceHolder();
*ptr = 50; // Base must still be alive here
std::cout << "Derived destroyed" << std::endl;
}
int* ptr;
};
int main() {
ExtendedHolder ex;
return 0;
}