4. Object-Oriented Programming (OOP)

Module 4 • Comprehensive Structural Paradigm Guide

4.1. Classes and Objects

In C++, classes and objects are fundamental concepts of Object-Oriented Programming (OOP), which focuses on organizing code into reusable and modular structures. A class is a blueprint for creating objects, which are instances of the class. The class defines the properties (attributes) and behaviors (methods) that the objects will have.

4.1.1. Classes in C++

A class is a user-defined data type that represents a collection of related data and functions. The data is called member variables (attributes), and the functions are called member functions (methods). Classes provide a way to structure the data and the operations that can be performed on that data.

Syntax of a class:

class ClassName {
   private:
      // Private members (cannot be accessed directly from outside the class)
      dataType memberVariable;

   public:
      // Public members (can be accessed from outside the class)
      void memberFunction() {
         // function definition
      }
};

2. Objects in C++

An object is an instance of a class. When you create an object of a class, the class defines the properties (variables) and methods (functions) that the object will have. Objects allow you to store data and interact with it using the methods defined in the class.

Syntax for creating objects:

ClassName objectName;  // Object creation

3. Example of Classes and Objects in C++

Let’s take a simple example where we define a Car class and then create objects (specific cars) from it.

#include <iostream>
using namespace std;

// Define the Car class
class Car {
   // Private members
   private:
      string brand;
      string model;
      int year;

   // Public members
   public:
      // Constructor to initialize the object
      Car(string b, string m, int y) {
         brand = b;
         model = m;
         year = y;
      }

      // Member function to display car details
      void displayDetails() {
         cout << "Car brand: " << brand << endl;
         cout << "Car model: " << model << endl;
         cout << "Car year: " << year << endl;
      }

      // Getter functions
      string getBrand() {
         return brand;
      }

      string getModel() {
         return model;
      }

      int getYear() {
         return year;
      }
};

int main() {
   // Create objects of the Car class
   Car car1("Toyota", "Corolla", 2020);  // Initialize car1 with brand, model, and year
   Car car2("Honda", "Civic", 2021);     // Initialize car2 with brand, model, and year

   // Calling member function using objects
   car1.displayDetails();
   car2.displayDetails();

   // Accessing object properties through getter functions
   cout << "Brand of car1: " << car1.getBrand() << endl;
   cout << "Year of car2: " << car2.getYear() << endl;

   return 0;
}

Explanation of the Example:

4.1.2. Access Modifiers in C++

Access modifiers determine the visibility of class members (attributes and methods). C++ supports three access modifiers:

4.2. Constructor and Destructor

Example of constructor and destructor:

class Car {
   private:
      string brand;
      string model;
      int year;

   public:
      // Constructor
      Car(string b, string m, int y) {
         brand = b;
         model = m;
         year = y;
         cout << "Car object created!" << endl;
      }

      // Destructor
      ~Car() {
         cout << "Car object destroyed!" << endl;
      }

      // Function to display car details
      void displayDetails() {
         cout << "Car brand: " << brand << endl;
         cout << "Car model: " << model << endl;
         cout << "Car year: " << year << endl;
      }
};

6. Other Key Points:

Conclusion: Classes and objects in C++ provide a powerful way to organize and manage code in a structured, reusable way. By using classes, we can define complex types, encapsulate data, and provide methods to interact with the data, which is essential for implementing Object-Oriented Programming principles.

4.2.1. Default, parameterized, and copy constructors

In C++, constructors are special member functions that are automatically called when an object of a class is created. Their primary purpose is to initialize the object. Constructors have the same name as the class and do not return any type, not even void.

There are three types of constructors in C++:

1. Default Constructor

A default constructor is a constructor that does not take any arguments. If you don't define any constructor in a class, C++ will automatically generate a default constructor for you. However, if you define any constructor, the default constructor is not generated automatically, unless explicitly defined.

A default constructor initializes the object with default values. If no explicit values are provided for the attributes of an object, the default constructor initializes them to some predefined values (like 0 for integers, null for pointers, etc.).

Syntax of a Default Constructor:

class ClassName {
public:
    ClassName() {
        // Initialization of member variables (optional)
    }
};

Example of Default Constructor:

#include <iostream>
using namespace std;

class Car {
private:
    string brand;
    string model;
    int year;

public:
    // Default Constructor
    Car() {
        brand = "Unknown";
        model = "Unknown";
        year = 0;
    }

    // Function to display car details
    void displayDetails() {
        cout << "Car brand: " << brand << endl;
        cout << "Car model: " << model << endl;
        cout << "Car year: " << year << endl;
    }
};

int main() {
    Car car1; // Object created with default constructor
    car1.displayDetails(); // Displays default values
    return 0;
}

Explanation:

2. Parameterized Constructor

A parameterized constructor is a constructor that takes one or more arguments. It allows you to initialize an object with specific values at the time of its creation. This provides flexibility, as the object can be initialized with different values based on the parameters passed.

Syntax of a Parameterized Constructor:

class ClassName {
public:
    ClassName(type parameter1, type parameter2) {
        // Initialize members with parameters
    }
};

Example of Parameterized Constructor:

#include <iostream>
using namespace std;

class Car {
private:
    string brand;
    string model;
    int year;

public:
    // Parameterized Constructor
    Car(string b, string m, int y) {
        brand = b;
        model = m;
        year = y;
    }

    // Function to display car details
    void displayDetails() {
        cout << "Car brand: " << brand << endl;
        cout << "Car model: " << model << endl;
        cout << "Car year: " << year << endl;
    }
};

int main() {
    // Creating objects with different initial values using parameterized constructor
    Car car1("Toyota", "Corolla", 2020);
    Car car2("Honda", "Civic", 2021);

    car1.displayDetails();
    car2.displayDetails();

    return 0;
}

Explanation:

3. Copy Constructor

A copy constructor is a special constructor that initializes an object using another object of the same class. It creates a new object as a copy of an existing object, and it is invoked when:

The copy constructor performs a shallow copy or deep copy depending on the type of members (whether they are simple data types or complex types like pointers).

Syntax of a Copy Constructor:

class ClassName {
public:
    ClassName(const ClassName& otherObject) {
        // Copy constructor code to copy data from otherObject
    }
};

Example of Copy Constructor:

#include <iostream>
using namespace std;

class Car {
private:
    string brand;
    string model;
    int year;

public:
    // Parameterized Constructor
    Car(string b, string m, int y) {
        brand = b;
        model = m;
        year = y;
    }

    // Copy Constructor
    Car(const Car& other) {
        brand = other.brand;
        model = other.model;
        year = other.year;
        cout << "Copy constructor called!" << endl;
    }

    // Function to display car details
    void displayDetails() {
        cout << "Car brand: " << brand << endl;
        cout << "Car model: " << model << endl;
        cout << "Car year: " << year << endl;
    }
};

int main() {
    Car car1("Toyota", "Corolla", 2020);
    Car car2 = car1;  // Copy constructor is called here

    car1.displayDetails();
    car2.displayDetails();

    return 0;
}

Explanation:

Key Points about Copy Constructor:

Example of a Deep Copy Constructor (in case of pointers):

class Car {
private:
    string* brand;
    string* model;

public:
    // Parameterized Constructor
    Car(string b, string m) {
        brand = new string(b);
        model = new string(m);
    }

    // Copy Constructor (Deep Copy)
    Car(const Car& other) {
        brand = new string(*(other.brand));  // Create new memory for brand
        model = new string(*(other.model));  // Create new memory for model
    }

    // Destructor to release dynamically allocated memory
    ~Car() {
        delete brand;
        delete model;
    }

    // Function to display car details
    void displayDetails() {
        cout << "Car brand: " << *brand << endl;
        cout << "Car model: " << *model << endl;
    }
};

Conclusion:

Understanding constructors is crucial for managing object initialization and ensuring the correct handling of resources in object-oriented programming.

4.2.2. Destructor in C++ Programming

A destructor in C++ is a special member function of a class that is executed when an object of that class goes out of scope or is explicitly deleted. It is used to release any resources (such as memory, file handles, etc.) that were acquired during the lifetime of an object.

Key Features of Destructors:

Purpose of Destructor

The primary purpose of a destructor is to perform cleanup tasks for the object before it is destroyed. This includes:

Syntax of Destructor

The syntax of a destructor is as follows:

~ClassName()
{
    // code to release resources
}

Example: Destructor in Action

Here's a simple example demonstrating the use of destructors:

Example 1: Basic Destructor Example

#include <iostream>

class MyClass
{
public:
    MyClass() { 
        std::cout << "Constructor called!" << std::endl; 
    }
    
    ~MyClass() { 
        std::cout << "Destructor called!" << std::endl; 
    }
};

int main() {
    MyClass obj; // Constructor is called here
    // Destructor will be called automatically when 'obj' goes out of scope
    return 0;
}

Output:

Constructor called!
Destructor called!

In this example:

Example 2: Destructor with Dynamic Memory Management

The real usefulness of destructors appears when they handle dynamic memory allocated using new or malloc. If an object allocates memory dynamically, its destructor should deallocate that memory to avoid memory leaks.

#include <iostream>

class MyClass
{
private:
    int* data; // Pointer to dynamically allocated memory
    
public:
    MyClass(int size) {
        data = new int[size]; // Allocate memory dynamically
        std::cout << "Memory allocated!" << std::endl;
    }
    
    ~MyClass() {
        delete[] data;  // Deallocate memory in the destructor
        std::cout << "Memory deallocated!" << std::endl;
    }
};

int main() {
    MyClass obj(5); // Constructor is called, memory allocated for 5 integers
    // Destructor will be called automatically, and memory will be freed
    return 0;
}

Output:

Memory allocated!
Memory deallocated!

Here:

Example 3: Destructor with File Handling

Destructors are also useful for closing files or releasing other system resources.

#include <iostream>
#include <fstream>

class FileHandler {
private:
    std::ofstream file;
    
public:
    FileHandler(const std::string& filename) {
        file.open(filename);  // Open the file
        if (file.is_open()) {
            std::cout << "File opened!" << std::endl;
        }
    }
    
    ~FileHandler() {
        if (file.is_open()) {
            file.close();  // Close the file in the destructor
            std::cout << "File closed!" << std::endl;
        }
    }
};

int main() {
    FileHandler fileHandler("example.txt"); // File opened here
    // File will be automatically closed when the object goes out of scope
    return 0;
}

Output:

File opened!
File closed!

In this example:

Destructor and Dynamic Memory Management

When using dynamic memory allocation (i.e., memory allocated using new), it's important to ensure that the memory is properly deallocated to avoid memory leaks. The destructor is typically used for this purpose.

Example 4: Using delete and delete[]

#include <iostream>

class MyClass
{
private:
    int* data; // Pointer to dynamically allocated memory
    
public:
    MyClass() {
        data = new int(10);  // Allocates a single integer on the heap
        std::cout << "Single integer allocated!" << std::endl;
    }
    
    ~MyClass() {
        delete data;  // Deallocate memory
        std::cout << "Memory deallocated!" << std::endl;
    }
};

int main() {
    MyClass obj; // Constructor allocates memory
    // Destructor will deallocate memory automatically when the object goes out of scope
    return 0;
}

Output:

Single integer allocated!
Memory deallocated!

In this example, new allocates memory for a single integer, and the destructor releases it using delete.

Destructor for Base and Derived Classes (Polymorphism)

When using inheritance, it's important to ensure that the destructor of the base class is virtual if you are dealing with polymorphic objects. This ensures that the correct destructor is called for objects created with base class pointers that point to derived class objects.

Example 5: Virtual Destructor for Inheritance

#include <iostream>

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

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

int main() {
    Base* obj = new Derived();  // Base pointer points to Derived object
    delete obj; // Correct destructor (Derived's) will be called
    return 0;
}

Output:

Base class constructor!
Derived class constructor!
Derived class destructor!
Base class destructor!

In this example:

Conclusion: Destructors are an essential feature in C++ for resource management, particularly for cleaning up dynamically allocated memory or other resources (such as file handles, network connections, etc.). They are automatically called when objects go out of scope, and their proper use prevents memory leaks and ensures the safe release of resources.

4.3. Inheritance

Inheritance is one of the key features of Object-Oriented Programming (OOP) in C++. It allows one class to inherit properties and behaviors (i.e., data members and member functions) from another class. This mechanism promotes code reuse and creates a relationship between classes, where one class (derived class) is a specialized version of another class (base class).

Key Concepts in Inheritance:

Types of Inheritance:

Syntax of Inheritance:

class BaseClass {
  // Members and methods of the base class
};

class DerivedClass : accessSpecifier BaseClass {
  // Members and methods of the derived class
};

Access Specifiers in Inheritance:

Example of Inheritance in C++

1. Single Inheritance

#include <iostream>
using namespace std;

class Animal {
public:
    void speak() {
        cout << "Animal speaks" << endl;
    }
};

class Dog : public Animal {  // Dog is derived from Animal
public:
    void bark() {
        cout << "Dog barks" << endl;
    }
};

int main() {
    Dog myDog;
    myDog.speak();  // Inherited method
    myDog.bark();   // Dog's own method
    return 0;
}

Output:

Animal speaks
Dog barks

In this example: Dog is a derived class that inherits the speak method from the Animal class. The Dog class also defines its own method bark.

2. Multiple Inheritance

#include <iostream>
using namespace std;

class Animal {
public:
    void speak() {
        cout << "Animal speaks" << endl;
    }
};

class Bird {
public:
    void fly() {
        cout << "Bird flies" << endl;
    }
};

class Bat : public Animal, public Bird {  // Inherits from both Animal and Bird
public:
    void hang() {
        cout << "Bat hangs upside down" << endl;
    }
};

int main() {
    Bat myBat;
    myBat.speak();   // Inherited from Animal
    myBat.fly();     // Inherited from Bird
    myBat.hang();    // Bat's own method
    return 0;
}

Output:

Animal speaks
Bird flies
Bat hangs upside down

In this example: Bat inherits from both Animal and Bird. The Bat class can access the methods from both base classes (speak from Animal and fly from Bird).

3. Multilevel Inheritance

#include <iostream>
using namespace std;

class Animal {
public:
    void speak() {
        cout << "Animal speaks" << endl;
    }
};

class Mammal : public Animal {
public:
    void walk() {
        cout << "Mammal walks" << endl;
    }
};

class Dog : public Mammal {  // Dog is derived from Mammal, which is derived from Animal
public:
    void bark() {
        cout << "Dog barks" << endl;
    }
};

int main() {
    Dog myDog;
    myDog.speak();  // Inherited from Animal
    myDog.walk();   // Inherited from Mammal
    myDog.bark();   // Dog's own method
    return 0;
}

Output:

Animal speaks
Mammal walks
Dog barks

In this example: Dog inherits from Mammal, which in turn inherits from Animal. Dog has access to all the methods of Animal and Mammal.

4. Hierarchical Inheritance

#include <iostream>
using namespace std;

class Animal {
public:
    void speak() {
        cout << "Animal speaks" << endl;
    }
};

class Dog : public Animal {
public:
    void bark() {
        cout << "Dog barks" << endl;
    }
};

class Cat : public Animal {
public:
    void meow() {
        cout << "Cat meows" << endl;
    }
};

int main() {
    Dog myDog;
    Cat myCat;
    myDog.speak();  // Inherited from Animal
    myDog.bark();   // Dog's own method
    myCat.speak();  // Inherited from Animal
    myCat.meow();   // Cat's own method
    return 0;
}

Output:

Animal speaks
Dog barks
Animal speaks
Cat meows

In this example: Both Dog and Cat inherit from the same Animal class. Each derived class (Dog and Cat) has its own method in addition to the inherited speak method.

Constructor and Destructor in Inheritance

Example with Constructors and Destructors:

#include <iostream>
using namespace std;

class Animal {
public:
    Animal() {
        cout << "Animal Constructor" << endl;
    }
    ~Animal() {
        cout << "Animal Destructor" << endl;
    }
};

class Dog : public Animal {
public:
    Dog() {
        cout << "Dog Constructor" << endl;
    }
    ~Dog() {
        cout << "Dog Destructor" << endl;
    }
};

int main() {
    Dog myDog; // Object creation will call constructors
    return 0;
}

Output:

Animal Constructor
Dog Constructor
Dog Destructor
Animal Destructor

Here, the constructor of the Animal class is called first, then the Dog constructor, and when the object goes out of scope, the destructors are called in reverse order.

Conclusion: Inheritance in C++ allows you to build relationships between classes, promote code reuse, and extend existing code with new functionality. The derived class inherits properties and behaviors from the base class and can override or extend them as needed.

4.4. Polymorphism

Polymorphism is one of the core concepts of Object-Oriented Programming (OOP), and it allows objects of different classes to be treated as objects of a common base class. The term polymorphism comes from the Greek words "poly" (many) and "morph" (form), meaning many forms. In C++, polymorphism enables a function or an operator to behave differently depending on the type of object that it is acting upon.

There are two types of polymorphism in C++:

Let's explore both in detail.

1. Compile-time Polymorphism (Static Polymorphism)

Compile-time polymorphism is resolved during the compilation process. This is primarily achieved through function overloading and operator overloading.

Function Overloading

Function overloading allows multiple functions with the same name but different parameters (either in number or type) to exist in the same scope. The compiler determines which function to call based on the arguments passed at compile time.

Example: Function Overloading

#include <iostream>
using namespace std;

class Printer {
public:
    void print(int i) {
        cout << "Printing integer: " << i << endl;
    }

    void print(double d) {
        cout << "Printing double: " << d << endl;
    }

    void print(string s) {
        cout << "Printing string: " << s << endl;
    }
};

int main() {
    Printer printer;
    printer.print(10);         // Calls print(int)
    printer.print(3.14);       // Calls print(double)
    printer.print("Hello!");   // Calls print(string)

    return 0;
}

Output:

Printing integer: 10
Printing double: 3.14
Printing string: Hello!

In the above example, the print function is overloaded to accept different types of arguments. The compiler determines which version of the function to invoke based on the passed arguments.

Operator Overloading

Operator overloading allows you to define custom behaviors for operators when they are used with user-defined objects.

Example: Operator Overloading

#include <iostream>
using namespace std;

class Complex {
private:
    int real, imag;
    
public:
    Complex() : real(0), imag(0) {}

    Complex(int r, int i) : real(r), imag(i) {}

    // Overload the "+" operator to add two complex numbers
    Complex operator + (Complex const& obj) {
        return Complex(real + obj.real, imag + obj.imag);
    }

    void display() {
        cout << real << " + " << imag << "i" << endl;
    }
};

int main() {
    Complex c1(3, 4), c2(1, 2);
    Complex c3 = c1 + c2; // Using overloaded "+" operator
    c3.display(); // Outputs: 4 + 6i

    return 0;
}

Output:

4 + 6i

In this example, we overloaded the + operator for the Complex class to add two complex numbers.

2. Runtime Polymorphism (Dynamic Polymorphism)

Runtime polymorphism is achieved through inheritance and virtual functions. It allows a function to behave differently based on the type of object it is called on during execution, rather than at compile time.

Virtual Functions

In C++, a virtual function is a member function of a class that you expect to be overridden in derived classes. By marking a function as virtual, the C++ compiler ensures that the correct function is called for an object, even when the object is accessed through a pointer or reference to a base class.

Example: Runtime Polymorphism with Virtual Functions

#include <iostream>
using namespace std;

class Animal {
public:
    virtual void sound() { // Virtual function
        cout << "Animal makes a sound." << endl;
    }
};

class Dog : public Animal {
public:
    void sound() override { // Override the virtual function
        cout << "Dog barks." << endl;
    }
};

class Cat : public Animal {
public:
    void sound() override { // Override the virtual function
        cout << "Cat meows." << endl;
    }
};

int main() {
    Animal* animal;
    Dog dog;
    Cat cat;

    // Pointer of base class type can point to derived class objects
    animal = &dog;
    animal->sound();  // Outputs: Dog barks.

    animal = &cat;
    animal->sound();  // Outputs: Cat meows.

    return 0;
}

Output:

Dog barks.
Cat meows.

In this example, the sound function is declared as virtual in the Animal base class. When calling sound() through the base class pointer (animal), the actual function invoked depends on the type of object the pointer is pointing to (Dog or Cat). This is resolved at runtime, hence it's called runtime polymorphism.

Pure Virtual Functions and Abstract Classes

A pure virtual function is a function declared in a class that has no definition in that class. The class containing pure virtual functions is known as an abstract class. This forces derived classes to provide an implementation for that function.

Example: Abstract Class and Pure Virtual Function

#include <iostream>
using namespace std;

class Shape {
public:
    virtual void draw() = 0;  // Pure virtual function, makes Shape an abstract class
};

class Circle : public Shape {
public:
    void draw() override {
        cout << "Drawing a Circle." << endl;
    }
};

class Square : public Shape {
public:
    void draw() override {
        cout << "Drawing a Square." << endl;
    }
};

int main() {
    Shape* shape1 = new Circle();
    Shape* shape2 = new Square();

    shape1->draw();  // Outputs: Drawing a Circle.
    shape2->draw();  // Outputs: Drawing a Square.

    delete shape1;
    delete shape2;

    return 0;
}

Output:

Drawing a Circle.
Drawing a Square.

In this example, Shape is an abstract class with a pure virtual function draw(). Both Circle and Square provide implementations for draw(), and we can create pointers to Shape that can point to objects of either derived class.

Key Points of Polymorphism:

Benefits of Polymorphism:

In summary, polymorphism enhances the flexibility and maintainability of object-oriented systems by allowing functions to operate on different object types without needing to know their exact types at compile time.

Verify Comprehension: Technical Knowledge Assessment

Click your choice for each question to view feedback immediately. Complete all questions to evaluate your metric score.