Unit 4.0: Overloading, Templates & Inheritance 🔧
1. Operator Overloading
- Giving existing C++ operators new meaning for user-defined types. You cannot create new operators or change the number of operands.
Syntax
return_type operator symbol (parameters) {
// implementation
}Overloading Arithmetic Operators
#include <iostream>
using namespace std;
class Vector2D {
public:
double x, y;
Vector2D(double x = 0, double y = 0) : x(x), y(y) {}
// + operator
Vector2D operator+(const Vector2D& other) const {
return Vector2D(x + other.x, y + other.y);
}
// - operator
Vector2D operator-(const Vector2D& other) const {
return Vector2D(x - other.x, y - other.y);
}
// * scalar
Vector2D operator*(double scalar) const {
return Vector2D(x * scalar, y * scalar);
}
// == comparison
bool operator==(const Vector2D& other) const {
return (x == other.x && y == other.y);
}
// != comparison
bool operator!=(const Vector2D& other) const {
return !(*this == other);
}
// Unary minus
Vector2D operator-() const {
return Vector2D(-x, -y);
}
void display() const {
cout << "(" << x << ", " << y << ")" << endl;
}
};
int main() {
Vector2D v1(3.0, 4.0);
Vector2D v2(1.0, 2.0);
Vector2D sum = v1 + v2;
sum.display(); // (4, 6)
Vector2D diff = v1 - v2;
diff.display(); // (2, 2)
Vector2D scaled = v1 * 2.0;
scaled.display(); // (6, 8)
cout << (v1 == v2) << endl; // 0 (false)
Vector2D neg = -v1;
neg.display(); // (-3, -4)
return 0;
}Overloading << and >> (Stream Operators)
#include <iostream>
#include <string>
using namespace std;
class Complex {
public:
double real, imag;
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}
// << must be a friend (non-member) to access private data
friend ostream& operator<<(ostream& out, const Complex& c) {
out << c.real;
if (c.imag >= 0) out << " + " << c.imag << "i";
else out << " - " << (-c.imag) << "i";
return out;
}
// >> must be a friend too
friend istream& operator>>(istream& in, Complex& c) {
in >> c.real >> c.imag;
return in;
}
};
int main() {
Complex c1(3.0, 4.0);
Complex c2(1.0, -2.0);
cout << c1 << endl; // 3 + 4i
cout << c2 << endl; // 1 - 2i
cout << c1 + c2 << endl; // 4 + 2i
Complex c3;
cout << "Enter real and imaginary: ";
// cin >> c3;
return 0;
}Overloading ++ and — (Increment/Decrement)
#include <iostream>
using namespace std;
class Counter {
int count;
public:
Counter(int c = 0) : count(c) {}
// Prefix ++ (++obj)
Counter& operator++() {
++count;
return *this;
}
// Postfix ++ (obj++) — dummy int parameter to distinguish
Counter operator++(int) {
Counter old = *this;
++count;
return old; // return value BEFORE increment
}
// Prefix --
Counter& operator--() {
--count;
return *this;
}
int getCount() const { return count; }
};
int main() {
Counter c(5);
cout << (++c).getCount() << endl; // 6 (pre-increment)
cout << (c++).getCount() << endl; // 6 (post-increment: old value)
cout << c.getCount() << endl; // 7
return 0;
}Overloading [] (Subscript)
#include <iostream>
#include <stdexcept>
using namespace std;
class SafeArray {
int data[100];
int size;
public:
SafeArray(int s) : size(s) {
for (int i = 0; i < s; i++) data[i] = 0;
}
// Non-const version (can modify)
int& operator[](int index) {
if (index < 0 || index >= size)
throw out_of_range("Index out of bounds!");
return data[index];
}
// Const version (read-only)
const int& operator[](int index) const {
if (index < 0 || index >= size)
throw out_of_range("Index out of bounds!");
return data[index];
}
};
int main() {
SafeArray arr(5);
arr[0] = 10;
arr[2] = 30;
arr[4] = 50;
for (int i = 0; i < 5; i++)
cout << arr[i] << " "; // 10 0 30 0 50
cout << endl;
// arr[10] = 5; // throws out_of_range exception
return 0;
}Operators That CAN and CANNOT Be Overloaded
| Can Overload | Cannot Overload |
|---|---|
+ - * / % | :: (scope resolution) |
== != < > <= >= | . (member access) |
<< >> (stream) | .* (pointer to member) |
[] () -> | ?: (ternary) |
++ -- ! ~ | sizeof typeid |
= += -= etc. | static_cast etc. |
2. Function Overloading
(Detailed coverage in Unit 3. Here is a quick recap plus advanced aspects.)
Overload Resolution Rules
#include <iostream>
using namespace std;
void print(int x) { cout << "int: " << x << endl; }
void print(double x) { cout << "double: " << x << endl; }
void print(char x) { cout << "char: " << x << endl; }
void print(int x, int y) { cout << "two ints: " << x << " " << y << endl; }
int main() {
print(42); // int: 42
print(3.14); // double: 3.14
print('A'); // char: A
print(1, 2); // two ints: 1 2
print(3.14f); // float → promoted to double
return 0;
}3. Function Templates
A function template lets you write a single function that works with any data type. The compiler generates the actual function for each type used.
Basic Function Template
#include <iostream>
using namespace std;
// T is a type parameter (placeholder)
template <typename T>
T findMax(T a, T b) {
return (a > b) ? a : b;
}
// Template with multiple type parameters
template <typename T1, typename T2>
void display(T1 a, T2 b) {
cout << a << " and " << b << endl;
}
int main() {
cout << findMax(3, 7) << endl; // int: 7
cout << findMax(3.14, 2.71) << endl; // double: 3.14
cout << findMax('a', 'z') << endl; // char: z
cout << findMax(string("hello"), string("world")) << endl; // world
display(42, "hello"); // 42 and hello
display(3.14, true); // 3.14 and 1
return 0;
}Practical Function Templates
#include <iostream>
using namespace std;
// Generic swap
template <typename T>
void swapValues(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
// Generic array print
template <typename T>
void printArray(const T arr[], int size) {
cout << "[ ";
for (int i = 0; i < size; i++) {
cout << arr[i];
if (i < size - 1) cout << ", ";
}
cout << " ]" << endl;
}
// Generic binary search
template <typename T>
int binarySearch(const T arr[], int size, T target) {
int low = 0, high = size - 1;
while (low <= high) {
int mid = (low + high) / 2;
if (arr[mid] == target) return mid;
else if (arr[mid] < target) low = mid + 1;
else high = mid - 1;
}
return -1;
}
int main() {
int a = 5, b = 10;
swapValues(a, b);
cout << a << " " << b << endl; // 10 5
double x = 1.1, y = 2.2;
swapValues(x, y);
cout << x << " " << y << endl; // 2.2 1.1
int nums[] = {1, 3, 5, 7, 9, 11};
printArray(nums, 6); // [ 1, 3, 5, 7, 9, 11 ]
cout << binarySearch(nums, 6, 7) << endl; // 3 (index)
cout << binarySearch(nums, 6, 4) << endl; // -1 (not found)
return 0;
}Template Specialization
#include <iostream>
#include <cstring>
using namespace std;
// General template
template <typename T>
T findMax(T a, T b) {
return (a > b) ? a : b;
}
// Specialization for C-strings
template <>
const char* findMax<const char*>(const char* a, const char* b) {
return (strcmp(a, b) > 0) ? a : b;
}
int main() {
cout << findMax(10, 20) << endl; // 20
cout << findMax("banana", "apple") << endl; // banana
return 0;
}4. Class Templates
A class template creates a blueprint for a class that works with any type.
Stack Implementation (Class Template)
#include <iostream>
#include <stdexcept>
using namespace std;
template <typename T, int MAX_SIZE = 100>
class Stack {
private:
T data[MAX_SIZE];
int top;
public:
Stack() : top(-1) {}
void push(const T& value) {
if (top >= MAX_SIZE - 1)
throw overflow_error("Stack overflow!");
data[++top] = value;
}
T pop() {
if (isEmpty())
throw underflow_error("Stack underflow!");
return data[top--];
}
T peek() const {
if (isEmpty())
throw underflow_error("Stack is empty!");
return data[top];
}
bool isEmpty() const { return top == -1; }
int size() const { return top + 1; }
};
int main() {
// Integer stack
Stack<int> intStack;
intStack.push(10);
intStack.push(20);
intStack.push(30);
cout << intStack.pop() << endl; // 30
cout << intStack.peek() << endl; // 20
// String stack
Stack<string> strStack;
strStack.push("Hello");
strStack.push("World");
cout << strStack.pop() << endl; // World
// Stack with custom size
Stack<double, 5> smallStack;
smallStack.push(1.1);
smallStack.push(2.2);
cout << smallStack.size() << endl; // 2
return 0;
}Pair Class Template
#include <iostream>
#include <string>
using namespace std;
template <typename T1, typename T2>
class Pair {
public:
T1 first;
T2 second;
Pair(T1 f, T2 s) : first(f), second(s) {}
void display() const {
cout << "(" << first << ", " << second << ")" << endl;
}
// Swap the pair
Pair<T2, T1> swapped() const {
return Pair<T2, T1>(second, first);
}
};
int main() {
Pair<int, string> p1(1, "Alice");
Pair<double, bool> p2(3.14, true);
p1.display(); // (1, Alice)
p2.display(); // (3.14, 1)
return 0;
}5. Single Inheritance
A derived class inherits from one base class. It gets all public/protected members of the base class.
Syntax
class Derived : access_specifier Base {
// additional members
};Inheritance Access Specifiers
| Base member | public inheritance | protected inheritance | private inheritance |
|---|---|---|---|
public | public | protected | private |
protected | protected | protected | private |
private | inaccessible | inaccessible | inaccessible |
Basic Single Inheritance
#include <iostream>
#include <string>
using namespace std;
class Vehicle {
protected:
string brand;
int year;
double speed;
public:
Vehicle(string b, int y, double s) : brand(b), year(y), speed(s) {}
void accelerate(double amount) { speed += amount; }
void brake(double amount) { speed = max(0.0, speed - amount); }
virtual void display() const {
cout << brand << " (" << year << ") | Speed: " << speed << " km/h" << endl;
}
virtual ~Vehicle() {}
};
class Car : public Vehicle {
private:
int doors;
string fuelType;
public:
Car(string brand, int year, int doors, string fuel)
: Vehicle(brand, year, 0), doors(doors), fuelType(fuel) {}
void display() const override {
Vehicle::display(); // call base class version
cout << " Doors: " << doors << " | Fuel: " << fuelType << endl;
}
};
class Motorcycle : public Vehicle {
private:
bool hasSidecar;
public:
Motorcycle(string brand, int year, bool sidecar = false)
: Vehicle(brand, year, 0), hasSidecar(sidecar) {}
void display() const override {
Vehicle::display();
cout << " Sidecar: " << (hasSidecar ? "Yes" : "No") << endl;
}
};
int main() {
Car car("Toyota", 2022, 4, "Petrol");
car.accelerate(60);
car.display();
Motorcycle moto("Harley-Davidson", 2021, true);
moto.accelerate(100);
moto.display();
return 0;
}Calling Base Class Constructor
#include <iostream>
using namespace std;
class Shape {
protected:
string color;
public:
Shape(string c) : color(c) {
cout << "Shape constructor: " << color << endl;
}
virtual ~Shape() {
cout << "Shape destructor" << endl;
}
};
class Circle : public Shape {
double radius;
public:
Circle(string color, double r)
: Shape(color), radius(r) { // explicitly call base constructor
cout << "Circle constructor: r=" << radius << endl;
}
~Circle() {
cout << "Circle destructor" << endl;
}
double area() const { return 3.14159 * radius * radius; }
};
int main() {
Circle c("Red", 5.0);
cout << "Area: " << c.area() << endl;
return 0;
}
/*
Shape constructor: Red
Circle constructor: r=5
Area: 78.5397
Circle destructor
Shape destructor
*/6. Multiple Inheritance
A derived class inherits from two or more base classes.
class Derived : public Base1, public Base2 { ... };Example
#include <iostream>
#include <string>
using namespace std;
class Flyable {
public:
void fly() const {
cout << "Flying at altitude 10,000 ft" << endl;
}
virtual ~Flyable() {}
};
class Swimmable {
public:
void swim() const {
cout << "Swimming at 5 knots" << endl;
}
virtual ~Swimmable() {}
};
class Walkable {
public:
void walk() const {
cout << "Walking on land" << endl;
}
virtual ~Walkable() {}
};
// Duck inherits from all three
class Duck : public Flyable, public Swimmable, public Walkable {
string name;
public:
Duck(string n) : name(n) {}
void display() const {
cout << name << " can:" << endl;
fly();
swim();
walk();
}
};
int main() {
Duck donald("Donald");
donald.display();
return 0;
}Ambiguity in Multiple Inheritance
#include <iostream>
using namespace std;
class A {
public:
void show() { cout << "A::show()" << endl; }
};
class B {
public:
void show() { cout << "B::show()" << endl; }
};
class C : public A, public B {
public:
void show() {
A::show(); // resolve ambiguity with scope ::
B::show();
}
void showA() { A::show(); }
void showB() { B::show(); }
};
int main() {
C obj;
obj.show(); // calls C's version
// obj.A::show(); // directly call A's version
// obj.B::show(); // directly call B's version
return 0;
}7. Virtual Base Class
Solves the Diamond Problem in multiple inheritance (where a base class is inherited twice).
The Diamond Problem
Animal
/ \
Dog Cat
\ /
DogCat ← Animal is inherited TWICE → ambiguity!
Without Virtual (Problem)
class Animal { public: string name = "Animal"; };
class Dog : public Animal {};
class Cat : public Animal {};
class DogCat : public Dog, public Cat {};
// DogCat obj;
// obj.name; // ERROR: ambiguous — Dog::Animal::name or Cat::Animal::name?With Virtual Base Class (Solution)
#include <iostream>
#include <string>
using namespace std;
class Animal {
public:
string name;
int age;
Animal(string n = "Animal", int a = 0) : name(n), age(a) {}
void breathe() const {
cout << name << " is breathing." << endl;
}
};
class Dog : virtual public Animal { // virtual inheritance
public:
Dog(string n, int a) : Animal(n, a) {}
void bark() const { cout << name << " says: Woof!" << endl; }
};
class Cat : virtual public Animal { // virtual inheritance
public:
Cat(string n, int a) : Animal(n, a) {}
void meow() const { cout << name << " says: Meow!" << endl; }
};
// Now only ONE copy of Animal
class DogCat : public Dog, public Cat {
public:
DogCat(string n, int a) : Animal(n, a), Dog(n, a), Cat(n, a) {}
// Must initialize Animal directly in most-derived class
};
int main() {
DogCat hybrid("Hybrid", 3);
hybrid.breathe(); // No ambiguity!
hybrid.bark(); // Woof!
hybrid.meow(); // Meow!
cout << hybrid.name << endl; // Hybrid (single copy)
return 0;
}8. Abstract Class
A class that cannot be instantiated — it exists only to be a base class. Contains at least one pure virtual function (= 0).
#include <iostream>
#include <string>
#include <cmath>
using namespace std;
// Abstract base class
class Shape {
protected:
string color;
public:
Shape(string c = "White") : color(c) {}
// Pure virtual functions — MUST be overridden in derived classes
virtual double area() const = 0;
virtual double perimeter() const = 0;
virtual string name() const = 0;
// Regular virtual function — CAN be overridden
virtual void display() const {
cout << name() << " [" << color << "]"
<< " | Area: " << area()
<< " | Perimeter: " << perimeter() << endl;
}
virtual ~Shape() {}
};
class Circle : public Shape {
double radius;
public:
Circle(double r, string c = "White") : Shape(c), radius(r) {}
double area() const override { return 3.14159 * radius * radius; }
double perimeter() const override { return 2 * 3.14159 * radius; }
string name() const override { return "Circle"; }
};
class Rectangle : public Shape {
double l, w;
public:
Rectangle(double l, double w, string c = "White") : Shape(c), l(l), w(w) {}
double area() const override { return l * w; }
double perimeter() const override { return 2 * (l + w); }
string name() const override { return "Rectangle"; }
};
class Triangle : public Shape {
double a, b, c;
public:
Triangle(double a, double b, double c, string col = "White")
: Shape(col), a(a), b(b), c(c) {}
double area() const override {
double s = (a + b + c) / 2;
return sqrt(s * (s-a) * (s-b) * (s-c)); // Heron's formula
}
double perimeter() const override { return a + b + c; }
string name() const override { return "Triangle"; }
};
int main() {
// Shape s; // ERROR! Cannot instantiate abstract class
Shape* shapes[] = {
new Circle(5.0, "Red"),
new Rectangle(4.0, 6.0, "Blue"),
new Triangle(3.0, 4.0, 5.0, "Green")
};
for (Shape* s : shapes) {
s->display();
}
for (Shape* s : shapes) delete s;
return 0;
}9. Pointer and Inheritance
Pointers to base classes are fundamental to polymorphism in C++.
Base Class Pointer to Derived Object
#include <iostream>
using namespace std;
class Animal {
public:
virtual void sound() const { cout << "..."; }
virtual string type() const { return "Animal"; }
virtual ~Animal() {}
};
class Dog : public Animal {
public:
void sound() const override { cout << "Woof!" << endl; }
string type() const override { return "Dog"; }
};
class Cat : public Animal {
public:
void sound() const override { cout << "Meow!" << endl; }
string type() const override { return "Cat"; }
};
class Bird : public Animal {
public:
void sound() const override { cout << "Tweet!" << endl; }
string type() const override { return "Bird"; }
};
int main() {
// Array of base class pointers pointing to derived objects
Animal* zoo[] = {
new Dog(),
new Cat(),
new Bird(),
new Dog()
};
for (Animal* a : zoo) {
cout << a->type() << ": ";
a->sound();
}
for (Animal* a : zoo) delete a;
return 0;
}dynamic_cast for Safe Downcasting
#include <iostream>
using namespace std;
class Base {
public:
virtual void info() const { cout << "Base" << endl; }
virtual ~Base() {}
};
class Derived : public Base {
public:
void info() const override { cout << "Derived" << endl; }
void specialFunction() const { cout << "Special!" << endl; }
};
int main() {
Base* bptr = new Derived();
// Upcast: always safe (implicit)
// Derived → Base
// Downcast: use dynamic_cast for safety
Derived* dptr = dynamic_cast<Derived*>(bptr);
if (dptr) {
dptr->specialFunction(); // Safe!
} else {
cout << "Cast failed!" << endl;
}
// Unsafe C-style cast (avoid!)
// Derived* d2 = (Derived*)bptr; // not checked at runtime
delete bptr;
return 0;
}Polymorphic Container Example
#include <iostream>
#include <vector>
#include <memory> // for smart pointers
using namespace std;
class Employee {
protected:
string name;
double baseSalary;
public:
Employee(string n, double s) : name(n), baseSalary(s) {}
virtual double calculatePay() const = 0;
virtual void display() const {
cout << name << " | Pay: $" << calculatePay() << endl;
}
virtual ~Employee() {}
};
class FullTime : public Employee {
public:
FullTime(string n, double s) : Employee(n, s) {}
double calculatePay() const override { return baseSalary; }
};
class PartTime : public Employee {
int hoursWorked;
public:
PartTime(string n, double rate, int hours)
: Employee(n, rate), hoursWorked(hours) {}
double calculatePay() const override { return baseSalary * hoursWorked; }
};
class Commission : public Employee {
double sales, rate;
public:
Commission(string n, double s, double sales, double rate)
: Employee(n, s), sales(sales), rate(rate) {}
double calculatePay() const override { return baseSalary + sales * rate; }
};
int main() {
// Smart pointers — no manual delete needed!
vector<unique_ptr<Employee>> staff;
staff.push_back(make_unique<FullTime>("Alice", 5000));
staff.push_back(make_unique<PartTime>("Bob", 20, 80));
staff.push_back(make_unique<Commission>("Carol", 2000, 10000, 0.05));
double total = 0;
for (const auto& emp : staff) {
emp->display();
total += emp->calculatePay();
}
cout << "Total payroll: $" << total << endl;
return 0;
}10. Overloading Member Functions
Member functions can be overloaded within a class, and inherited ones can be overridden in derived classes.
Overloading Inside a Class
#include <iostream>
#include <string>
using namespace std;
class Printer {
public:
// Overloaded print — same name, different types
void print(int x) {
cout << "Integer: " << x << endl;
}
void print(double x) {
cout << "Double: " << x << endl;
}
void print(const string& s) {
cout << "String: " << s << endl;
}
void print(int x, int y) {
cout << "Two ints: " << x << ", " << y << endl;
}
void print(const string& label, int value) {
cout << label << ": " << value << endl;
}
};
int main() {
Printer p;
p.print(42);
p.print(3.14);
p.print(string("Hello"));
p.print(10, 20);
p.print("Score", 95);
return 0;
}Overriding vs Overloading
#include <iostream>
using namespace std;
class Base {
public:
void display() { // version 1
cout << "Base::display()" << endl;
}
void display(int x) { // version 2 — overloaded in Base
cout << "Base::display(int): " << x << endl;
}
virtual void info() const {
cout << "Base::info()" << endl;
}
};
class Derived : public Base {
public:
using Base::display; // bring Base::display into scope
void display(double d) { // new overload in Derived
cout << "Derived::display(double): " << d << endl;
}
void info() const override { // OVERRIDING — replaces Base::info
cout << "Derived::info()" << endl;
}
};
int main() {
Derived d;
d.display(); // Base::display() (inherited via using)
d.display(10); // Base::display(int)
d.display(3.14); // Derived::display(double)
d.info(); // Derived::info() (overriding)
Base* bptr = &d;
bptr->info(); // Derived::info() — virtual dispatch
return 0;
}Quick Reference Summary
Template Syntax
// Function template
template <typename T>
T funcName(T param) { ... }
// Class template
template <typename T>
class ClassName {
T data;
public:
void method(T val) { ... }
};
// Usage
ClassName<int> obj1;
ClassName<string> obj2;Inheritance Cheat Sheet
class Derived : public Base {}; // public: most common
class Derived : protected Base {}; // protected: members become protected
class Derived : private Base {}; // private: members become private
// Multiple
class Derived : public A, public B {};
// Virtual (diamond fix)
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};
// Abstract class
class Abstract {
virtual void method() = 0; // pure virtual
};Operator Overloading Rules
| Rule | Detail |
|---|---|
| Cannot create new operators | Only overload existing ones |
| Cannot change precedence | * still has higher precedence than + |
| Cannot change associativity | Left-to-right stays left-to-right |
| At least one operand must be user-defined type | Cannot redefine int + int |
<< and >> must be friends | Need access to private data |
= () [] -> must be members | Cannot be free functions |