OOP Unit 3 Notes
OOP Unit 3 Notes
Unit – III
Polymorphism
1. Polymorphism
The word “polymorphism” means having many forms. In simple words, we can define
polymorphism as the ability of a message to be displayed in more than one form. A real-life
example of polymorphism is a person who at the same time can have different characteristics.
A man at the same time is a father, a husband, and an employee. So, the same person exhibits
different behavior in different situations. This is called polymorphism. Polymorphism is
considered one of the important features of Object-Oriented Programming.
C++ allows you to specify more than one definition for a function name or an operator in the
same scope, which is called function overloading and operator overloading respectively.
An overloaded declaration is a declaration that is declared with the same name as a previously
declared declaration in the same scope, except that both declarations have different arguments
and obviously different definition (implementation).
2|Page
When you call an overloaded function or operator, the compiler determines the most
appropriate definition to use, by comparing the argument types you have used to call the
function or operator with the parameter types specified in the definitions. The process of
selecting the most appropriate overloaded function or operator is called overload resolution.
Operator overloading in C++ allows you to redefine how operators work for user-defined
types, such as classes and structures. By overloading operators, you can provide custom
implementations for operations such as addition, subtraction, comparison, etc., on objects of a
class, much like how they work for built-in types like integers or floating-point numbers.
To overload an operator, you define a function where the keyword operator is followed by
the symbol of the operator you wish to overload. The function must be a member function of a
class or a friend function, depending on the context.
3|Page
#include <iostream>
using namespace std;
class Complex {
private:
int real, imag; // Private members for real and imaginary parts
public:
// Constructor to initialize complex numbers
Complex(int r = 0, int i = 0) : real(r), imag(i) {}
int main() {
Complex c1(10, 5); // First complex number
Complex c2(4, 2); // Second complex number
return 0;
}
1.5 Overloading Unary Operators
Overloading of unary operators in C++ allows you to define custom behavior for unary
operators when they are applied to objects of a class. Unary operators operate on a single
operand and include operators such as - (negation), ++ (increment), -- (decrement), ! (logical
NOT), and ~ (bitwise NOT).
When overloaded as member functions, they don't require parameters since the operand
is the object itself (*this).
Syntax:
#include <iostream>
using namespace std;
class Counter {
private:
int value;
public:
5|Page
int main() {
Counter count1(10);
return 0;
}
Output:
Using overloaded '--' operator (Member function):
Value: 9
#include <iostream>
6|Page
class Counter {
private:
int value;
public:
// Constructor to initialize the counter
Counter(int v = 0) : value(v) {}
int main() {
Counter count2(20);
return 0;
}
Output:
Value: 21
Binary operators are those that operate on two operands, such as +, -, *, /, ==, and !=. When
overloading a binary operator in C++, you define how the operator should behave when used
between two objects of a class (or between an object and a built-in type, in some cases).
The operator must take one argument if it is a member function (the other operand is
implicitly the object invoking the operator).
Syntax:
#include <iostream>
#include <cstring> // For strcmp function
using namespace std;
class String {
private:
char* str; // Pointer to dynamically store the string
8|Page
public:
// Constructor to initialize string
String(const char* s = "") {
str = new char[strlen(s) + 1]; // Allocate memory
strcpy(str, s); // Copy the input string
}
int main() {
String s1("Hello");
String s2("Hello");
String s3("World");
cout << "s1 and s2 are equal." << endl; // Output: s1 and s2
are equal.
} else {
cout << "s1 and s2 are not equal." << endl;
}
if (s1 == s3) {
cout << "s1 and s3 are equal." << endl;
} else {
cout << "s1 and s3 are not equal." << endl; // Output: s1
and s3 are not equal.
}
return 0;
}
Example 2: Program to overload insertion (<<) and extraction (>>) operator:
#include <iostream>
using namespace std;
class Complex
{
private:
int real, imag;
public:
Complex(int r = 0, int i =0)
{ real = r; imag = i; }
friend ostream & operator << (ostream &out, const Complex &c);
friend istream & operator >> (istream &in, Complex &c);
};
int main(){
Complex c1;
cin >> c1;
cout << "The complex number is ";
cout << c1;
return 0;
}
1.7 Data Conversion - Type Casting (implicit and explicit)
Type casting in C++ refers to the process of converting one data type to another. This
conversion can be done implicitly (automatically) by the compiler or explicitly (manually) by
the programmer. Type casting is important to ensure that the right data type is being used for
operations, especially in expressions involving mixed data types.
Definition: Implicit type casting, also known as automatic type conversion, occurs when the
compiler automatically converts one data type to another without explicit instructions from the
11 | P a g e
programmer. This usually happens in expressions where different data types are involved, and
the compiler promotes the smaller or less precise type to a larger or more precise type to avoid
data loss.
Examples:
Definition: Explicit type casting, also known as type casting or manual conversion, occurs
when the programmer explicitly specifies the type to which a value should be converted. This
is often necessary when converting from a larger data type to a smaller data type, as it may lead
to data loss.
Syntax:
Example:
Ambiguity: Overloaded operators may lead to confusion if their behavior is not intuitive or
clear to users.
Performance Issues: Complex overloaded operators can introduce overhead, affecting the
performance of time-critical applications.
Inconsistent Behavior: Overloaded operators may not behave consistently with built-in types,
leading to unexpected results.
Unintended Side Effects: Operators may modify object states, leading to unintended
consequences if not carefully implemented.
Reduced Readability: Code can become less readable if overloaded operators are not familiar
or clear, making it harder for other developers to understand.
Loss of Information: Converting from a larger to a smaller type can result in data loss or
precision issues.
Reduced Type Safety: Overusing implicit conversions can lower type safety, making it
difficult to track variable types and their usage.
13 | P a g e
In C++, keywords serve different purposes, primarily related to data members of classes and
constructors, respectively.
Purpose: The mutable keyword allows a member variable of a class to be modified even if
the containing object is declared as const. This is useful for situations where you want to keep
some internal state mutable without affecting the overall immutability of the object.
Usage: You typically declare a data member as mutable when you need to change its value
within a const member function.
Example:
#include <iostream>
using namespace std;
class MyClass {
private:
mutable int counter; // mutable member
public:
MyClass() : counter(0) {}
Purpose: The explicit keyword is used in constructors to prevent implicit conversions and
copy-initialization. When a constructor is marked as explicit, it cannot be used for implicit
type conversions, which helps to avoid unintentional conversions that can lead to errors.
Usage: You typically use explicit when defining constructors that take a single argument or
when defining conversion operators.
Example:
#include <iostream>
using namespace std;
class MyClass {
private:
int value;
public:
// Constructor marked as explicit
explicit MyClass(int v) : value(v) {}
int main() {
MyClass obj1(5); // OK: Direct initialization
cout << "Value: " << obj1.getValue() << endl; // Output: Value: 5
15 | P a g e
return 0;
}
1.10 Function Overloading
Function Overloading in C++ allows you to define multiple functions with the same name
but different parameters (i.e., different types or numbers of parameters). This feature enables
the same function name to be used for different types of operations, enhancing code readability
and usability.
Same Name: All overloaded functions must have the same name.
Different Parameters: They must differ in the type or number of their parameters
(called the function signature).
Return Type: The return type is not considered part of the function signature, so it
cannot be used to distinguish between overloaded functions.
Example:
#include <iostream>
using namespace std;
// Function to add two integers
int add(int a, int b) {
return a + b;
}
return a + b;
}
int main() {
// Testing overloaded functions
int intResult = add(5, 10); // Calls the int version
float floatResult = add(5.5f, 3.2f); // Calls the float version
double doubleResult = add(5.5, 3.3); // Calls the double version
return 0;
}
2. Run-Time Polymorphism
Run-time polymorphism, also known as dynamic polymorphism, is a feature in C++ that
allows a program to determine which function to call at runtime based on the type of the object
being referred to, rather than the type of the pointer or reference. This is typically achieved
through the use of virtual functions and inheritance.
In C++, pointers to base classes are used to refer to objects of derived classes through a base
class pointer. This concept is fundamental in achieving polymorphism, allowing a single
interface to represent different underlying data types.
17 | P a g e
Key Concepts
o A base class is a general class that can be extended to create derived classes,
which inherit its properties and behaviors.
o Derived classes can override or extend the functionality of the base class.
o A pointer of the base class type can hold the address of an object of a derived
class. This enables the base class pointer to invoke overridden functions in the
derived class, achieving run-time polymorphism.
3. Virtual Functions:
o When a base class function is declared as virtual, the appropriate derived class
function is called based on the actual object type that the base class pointer is
pointing to, not the type of the pointer itself.
Example:
#include <iostream>
using namespace std;
// Base class
class Shape {
public:
virtual void draw() { // Virtual function
cout << "Drawing Shape" << endl;
}
};
// Derived class
class Circle : public Shape {
public:
void draw() override { // Override base class function
18 | P a g e
int main() {
Shape* shapePtr; // Base class pointer
return 0;
}
2.2 Virtual Base Class
A virtual base class in C++ is used in cases of multiple inheritance to prevent the duplication
of base class members. When a class is inherited by more than one derived class, and both
19 | P a g e
derived classes are inherited by another class (diamond problem), using a virtual base class
ensures that only one instance of the base class is created and shared by all derived classes.
If Class A contains some members, Class D will inherit Class A's members twice (once via
Class B and once via Class C), leading to ambiguity and duplication. This is known as the
diamond problem in multiple inheritance.
By declaring Class A as a virtual base class when inherited by Class B and Class C, only one
instance of Class A will be created and shared among the derived classes, resolving the
ambiguity.
#include <iostream>
using namespace std;
// Base class
class A {
public:
int value;
A() {
value = 10;
cout << "Constructor of A called" << endl;
}
};
int main() {
D obj;
cout << "Value from class A: " << obj.value << endl; // No
ambiguity
return 0;
}
2.3 Virtual Function and its Significance in C++
A virtual function in C++ is a member function that is declared within a base class and is
intended to be overridden in derived classes. When a function is marked as virtual in a base
class, C++ enables run-time polymorphism, which allows the program to determine which
21 | P a g e
function to invoke (the base class or the derived class version) at runtime, based on the actual
object type that the pointer or reference is pointing to.
The virtual keyword is used in the base class to indicate that the function can be overridden in
derived classes:
class Base {
public:
virtual void show() {
cout << "Base class show function" << endl;
}
};
Key Characteristics of Virtual Functions:
1. Dynamic Binding:
o A derived class can override the virtual function of a base class, providing its
own specific implementation.
o Virtual functions are primarily useful when a base class pointer or reference is
used to point to a derived class object.
o The compiler creates a virtual table (v-table) for each class with virtual
functions. This table holds pointers to the actual functions that should be called
for each object.
o In modern C++, the override keyword is used in the derived class to explicitly
state that the function is intended to override a virtual function.
A pure virtual function in C++ is a virtual function that has no implementation in the base
class and is declared using the = 0 syntax. This concept is fundamental in defining abstract
classes. An abstract class cannot be instantiated directly and serves as a blueprint for derived
classes, which must provide concrete implementations of the pure virtual functions.
Key Points:
class AbstractClass {
public:
virtual void pureVirtualFunction() = 0; // Pure virtual function
};
2. Abstract Class: A class containing at least one pure virtual function is called an abstract
class. This class cannot be instantiated, meaning you cannot create objects of the abstract class
directly.
3. Implementation Requirement: Derived classes must override all pure virtual functions to
become concrete classes that can be instantiated.
4. Enforcing Interface: Pure virtual functions enforce that certain functions must be
implemented in derived classes, thus providing a common interface while allowing flexibility
in the implementation.
A virtual table (or v-table) is a mechanism used by C++ to support dynamic (run-time)
polymorphism through virtual functions. It is an internal structure that stores pointers to the
virtual functions of a class, allowing the correct function to be called at runtime based on the
actual object type.
23 | P a g e
Key Points:
1. Structure: Each class that has virtual functions has its own v-table. The v-table contains
pointers to the virtual functions that are defined in that class.
2. Object Representation: Each object of a class with virtual functions typically contains a
pointer (often called the vptr) to its class's v-table. This pointer allows the program to access
the correct function implementations at runtime.
3. Dynamic Dispatch: When a virtual function is called through a base class pointer, the
program uses the vptr of the actual object to look up the appropriate function address in the
v-table and execute that function. This process is known as dynamic dispatch.
4. Overriding Functions: If a derived class overrides a virtual function, the v-table for that
derived class will point to the overridden function instead of the base class function.
#include <iostream>
using namespace std;
class Base {
public:
virtual void func() {
cout << "Base function" << endl;
}
};
int main() {
24 | P a g e
A virtual destructor in C++ is a destructor declared with the virtual keyword in a base class.
It ensures that the correct destructor is called for derived class objects when they are deleted
through a base class pointer. This is crucial for proper resource management and memory
cleanup, especially in inheritance hierarchies.
1. Purpose: Virtual destructors prevent resource leaks by ensuring that when an object of a
derived class is deleted through a base class pointer, the destructor for the derived class is
executed before the base class destructor.
2. Declaration: A virtual destructor is declared in the base class like any other virtual function:
class Base {
public:
virtual ~Base() {
// Cleanup code for Base class
}
};
3. Behavior: When a derived class overrides a destructor, if the base class destructor is virtual,
it guarantees that the derived class destructor will be invoked first, followed by the base class
destructor.
4. Memory Management: Using a virtual destructor is essential when working with dynamic
memory allocation and polymorphism to ensure that all allocated resources are properly
released.
25 | P a g e
5. Destructor Chaining: The virtual destructor mechanism ensures that destructors are called
in the correct order (derived class first, then base class) during object destruction.
Example:
#include <iostream>
using namespace std;
// Base class
class Base {
public:
// Virtual destructor
virtual ~Base() {
cout << "Base destructor called" << endl;
}
};
// Derived class
class Derived : public Base {
public:
~Derived() override {
cout << "Derived destructor called" << endl;
}
};
int main() {
Base* b = new Derived(); // Base pointer to Derived object
delete b; // Correctly calls Derived destructor, then Base
destructor
return 0;
}
2.6 Abstract Base Class
26 | P a g e
An abstract class in C++ is a class that cannot be instantiated (i.e., you cannot create objects
of an abstract class). It is primarily used as a base class for other classes to inherit from. An
abstract class is declared when it contains at least one pure virtual function.
A pure virtual function is a virtual function that has no definition in the base class. It is meant
to be overridden by derived classes, and its syntax is defined using = 0 at the end of the function
declaration.
class AbstractClass {
public:
virtual void pureVirtualFunction() = 0; // Pure virtual function
};
Example:
Suppose you are creating a program to model different types of employees in a company. The
base class Employee could be an abstract class because every employee should have a function
to calculate their salary, but the specific implementation of that function will differ for different
types of employees like Manager, Engineer, etc.
#include <iostream>
using namespace std;
// Derived class
class Manager : public Employee {
public:
void calculateSalary() override {
cout << "Calculating salary for Manager." << endl;
27 | P a g e
}
};
// Derived class
class Engineer : public Employee {
public:
void calculateSalary() override {
cout << "Calculating salary for Engineer." << endl;
}
};
int main() {
// Employee emp; // Error: Cannot instantiate an abstract class
Manager m;
Engineer e;
return 0;
}