C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
28: Inheritance and Access Control
When you first encounter inheritance, the goal usually seems simple: you have a base class with some common logic, and you want a derived class to reuse that logic. The struggle starts when the derived class needs to touch the data inside the base class. I've seen a lot of developers fall into a trap here where they treat access modifiers as a nuisance to be bypassed rather than a tool for design.
The temptation of the public floodgate
Imagine we're building a simple game engine. We have a GameObject class that tracks a position in a 2D world. The naive approach is to make everything public just so the subclasses—like a Player or an Enemy—can easily update their coordinates. It looks something like this:
class GameObject {
public:
float x, y;
int id;
};
class Player : public GameObject {
public:
void move(float dx, float dy) {
x += dx;
y += dy;
}
};
On the surface, this works. It's concise. But as a software engineer, you have to ask: who else can see x and y? In this setup, any part of your program—a UI script, a physics solver, or a random sound manager—can reach into your Player object and teleport them to coordinates (9999, 9999) without the Player class ever knowing. You've traded encapsulation for a tiny bit of convenience in the subclass. You've basically told the rest of the world, "I don't care who touches my internals."
The "Family Secret" approach with protected
You might then think, "Okay, I'll just make them private." But the moment you do that, your Player::move method will throw a compiler error. C++ is strict: private means private, even to your own children. This is where protected comes in. I like to think of protected as a family secret: the general public can't know it, but the kids inherit the knowledge.
class GameObject {
protected:
float x, y;
int id;
public:
GameObject(int id) : id(id), x(0), y(0) {}
};
class Player : public GameObject {
public:
Player(int id) : GameObject(id) {}
void move(float dx, float dy) {
x += dx; // This works now!
y += dy;
}
};
This is significantly better because the GameObject's position is now hidden from the rest of the application, but the Player can still manipulate it. However, there's a hidden cost here that I want you to be aware of: the Fragile Base Class problem. When you make data protected, you're creating a tight coupling between the base and the derived class. If you later decide that x and y should be replaced by a Vector2 object to support better math, you have to hunt down every single subclass that touched those floats and rewrite them. You've leaked the implementation details of the base class into the children.
Finding the balance with controlled access
The professional way to handle this is to keep your data private and provide protected methods for modification. This gives the base class a "gatekeeper" role. Instead of letting the Player class just overwrite the coordinates, you give them a method that can include validation—like checking for map boundaries.
class GameObject {
private:
float x, y;
int id;
protected:
void setPosition(float newX, float newY) {
// I can add logic here to ensure the object doesn't leave the map
x = newX;
y = newY;
}
public:
GameObject(int id) : id(id), x(0), y(0) {}
void getPosition() const { /* ... */ }
};
class Player : public GameObject {
public:
Player(int id) : GameObject(id) {}
void move(float dx, float dy) {
// We use the protected setter instead of touching raw data
setPosition(x + dx, y + dy);
}
};
By doing this, you've achieved the best of both worlds. The rest of the program is locked out, the subclasses have the power they need, and the base class retains control over how its data is changed. If you ever change how positions are stored internally, you only have to update the setPosition method in one place, and all your derived classes will keep working perfectly. It's a bit more typing upfront, but it saves you from a massive refactoring headache six months down the line.
📋 Practical Task
Refactoring the Vehicle Telemetry System
You have been handed a legacy piece of code for a vehicle tracking system. Currently, it uses a naive inheritance structure where all telemetry data is public, allowing any part of the system to accidentally modify a vehicle's fuel level or odometer reading.
Your goal: Refactor the following code to ensure that fuelLevel and mileage are encapsulated. They should be inaccessible to the rest of the program, but the Truck subclass should be able to update them using protected methods provided by the Vehicle base class.
#include <iostream>
#include <string>
class Vehicle {
public:
std::string licensePlate;
float fuelLevel;
float mileage;
Vehicle(std::string plate) : licensePlate(plate), fuelLevel(100.0f), mileage(0.0f) {}
};
class Truck : public Vehicle {
public:
Truck(std::string plate) : Vehicle(plate) {}
void drive(float distance, float fuelConsumed) {
mileage += distance;
fuelLevel -= fuelConsumed;
}
};
int main() {
Truck myTruck("ABC-123");
myTruck.drive(50.0f, 10.0f);
// This line represents a bug in the system where
// external code accidentally resets the fuel.
// Your refactor should make this line cause a compiler error.
myTruck.fuelLevel = 100.0f;
return 0;
}
Requirements:
- Move
fuelLevelandmileageto aprivatesection inVehicle. - Create
protectedsetter methods inVehiclethat allow derived classes to update these values. - Update the
Truck::drivemethod to use these setters instead of direct variable access. - Verify that the line
myTruck.fuelLevel = 100.0f;inmainno longer compiles.
There are no comments for now.