Assignment - 6
Assignment - 6
Q1. Define operator overloading. What are the rules for operator
overloading in C++?
Operator Overloading: Operator overloading is a type of polymorphism in C++ that allows you
to redefine the way operators work for user-defined data types (classes). It enables operators to
have special meanings when applied to objects of a class, making code more intuitive and
readable by allowing operations to be performed on objects using familiar operator syntax.
1. Only Existing Operators Can Be Overloaded: You can only overload existing operators in
C++. You cannot create new operators.
2. Precedence and Associativity Cannot Change: The precedence and associativity of an
operator cannot be changed through overloading.
3. Arity Cannot Change: The number of operands an operator takes (its arity) cannot be
changed. For example, a binary operator (like +) cannot be overloaded to work with
three operands.
4. Cannot Overload Certain Operators: Some operators cannot be overloaded:
○ . (dot operator)
○ .* (pointer-to-member operator)
○ :: (scope resolution operator)
○ ?: (ternary conditional operator)
○ sizeof (sizeof operator)
○ typeid (RTTI operator)
5. Member Function vs. Non-Member (Friend) Function:
○ Binary operators (except (), [], ->, =, and conversion operators) can be
overloaded either as a member function or a non-member (friend) function.
○ Unary operators can be overloaded as member functions (taking no arguments)
or non-member (friend) functions (taking one argument).
○ Operators (), [], ->, and = must be overloaded as member functions.
○ Conversion operators must be overloaded as member functions.
○ The << and >> operators (for I/O) are typically overloaded as non-member friend
functions because their left-hand operand is usually an ostream or istream object,
not an object of the user-defined class.
6. At Least One Operand Must Be User-Defined: When overloading an operator, at least
one of its operands must be an object of a user-defined class (or a reference to it). You
cannot overload operators for built-in data types alone (e.g., you cannot change how int
+ int works).
7. No Default Arguments: Overloaded operators cannot have default arguments.
8. Inheritance: Overloaded operators are not automatically inherited by derived classes.
Each derived class must explicitly overload operators if needed.
Q2. What is the difference between overloading unary and binary
operators in C++?
The difference between overloading unary and binary operators lies in their arity (number of
operands) and how they are typically implemented as member functions versus non-member
(friend) functions.
1. Code Reusability: This is the primary advantage. Instead of writing the same code
(attributes and methods) for multiple classes, you can define common functionalities in a
base class and reuse them in derived classes. This saves development time and effort.
2. Reduced Code Duplication: By reusing code, the amount of redundant code in a
system is significantly reduced. This makes the codebase smaller, cleaner, and easier to
manage.
3. Improved Maintainability: When common logic is centralized in a base class, changes
or bug fixes need to be made in only one place (the base class). These changes
automatically propagate to all derived classes, simplifying maintenance and reducing the
risk of inconsistencies.
4. Enhanced Extensibility: Inheritance makes it easier to extend the functionality of an
application. New features or specialized behaviors can be added by creating new
derived classes without modifying the existing base class, adhering to the Open/Closed
Principle (open for extension, closed for modification).
5. Polymorphism: Inheritance is a prerequisite for achieving run-time polymorphism
(dynamic polymorphism) through virtual functions. This allows objects of different derived
classes to be treated as objects of a common base class, enabling more flexible and
generic code.
6. Better Code Organization and Structure: Inheritance helps in creating a clear,
hierarchical structure among classes. This logical organization makes the program
easier to understand, design, and manage, reflecting real-world relationships more
naturally.
7. Data Hiding (Encapsulation): While derived classes inherit members, access specifiers
(private, protected, public) still control visibility. protected members allow derived
classes to access them while keeping them hidden from external users, maintaining
encapsulation.
8. Abstraction: Inheritance can be used in conjunction with abstract classes and pure
virtual functions to define interfaces and enforce certain behaviors among derived
classes, pushing common design to the base class while leaving specific implementation
details to derived classes.
Q4. What are virtual functions? How do they support runtime
polymorphism?
Virtual Functions: A virtual function is a member function of a base class that you expect to be
redefined (overridden) in derived classes. When you call a virtual function through a pointer or
reference to a base class object, the specific version of the function that is executed depends on
the actual type of the object being pointed to or referenced at runtime, rather than the type of
the pointer/reference itself.
To make a function virtual, you use the virtual keyword before its declaration in the base class. If
a derived class explicitly redefines a virtual function, it's good practice to use the override
keyword (C++11 onwards) to indicate that it's overriding a base class virtual function.
Runtime polymorphism, also known as dynamic polymorphism or late binding, is the ability of an
object to take on many forms at runtime. Virtual functions are the core mechanism in C++ that
enables this:
1. Base Class Pointer/Reference: The key to runtime polymorphism is to use a pointer or
a reference to the base class to point to or refer to an object of a derived class.
Animal* myAnimal = new Dog();
2. Dynamic Dispatch (V-table): When a virtual function is called through a base class
pointer or reference, C++ uses a mechanism called "dynamic dispatch" or "late binding."
This typically involves a virtual table (v-table).
○ Every class that has virtual functions (or inherits them) gets a v-table, which is a
table of function pointers.
○ Every object of such a class gets a hidden pointer, often called a v-pointer (vptr),
which points to its class's v-table.
○ When a virtual function is called via a base pointer/reference, the compiler
doesn't directly call a specific function. Instead, it looks at the object's vptr, finds
the corresponding v-table, and then looks up the correct function address in that
v-table based on the function's signature.
○ Because the vptr points to the v-table of the actual object type (e.g., Dog's v-table
even if the pointer is Animal*), the correct overridden function in the derived class
is invoked at runtime.
#include<iostream>
Using namespace std;
class Animal {
public:
virtual void makeSound() {
cout << "Animal makes a sound" << endl;
}
};
class Dog : public Animal {
public:
void makeSound() override {
cout << "Dog barks" << endl;
}
};
class Cat : public Animal {
public:
void makeSound() override {
cout << "Cat meows" << endl;
}
};
int main() {
Animal* animals[3];
animals[0] = new Animal();
animals[1] = new Dog();
animals[2] = new Cat();
for (int i = 0; i < 3; ++i) {
animals[i]->makeSound();
return 0;
}
Output:
Animal makes a sound
Dog barks
Cat meows
In this example, even though animals is an array of Animal pointers, the makeSound() call for
each element correctly dispatches to the makeSound() of Animal, Dog, or Cat based on the
actual object type at runtime. This dynamic behavior is the essence of runtime polymorphism
supported by virtual functions.
Q5. How do new and delete operators work in C++? Write a
program to demonstrate their usage.
new and delete Operators in C++:
new and delete are operators in C++ used for dynamic memory allocation and deallocation at
runtime, primarily on the heap (or free store).
● new Operator:
○ The delete operator is used to deallocate memory that was previously allocated
using new.
○ It takes a pointer to the memory block as an operand.
○ Deallocating memory frees it up, making it available for other programs or
subsequent allocations.
○ If the memory being deallocated was for a class object, delete automatically calls
the destructor of that class before deallocating the memory.
○ It's crucial to delete memory once it's no longer needed to prevent memory leaks.
○ Deleting a nullptr is safe and does nothing. Deleting memory that was not
allocated with new or deleting the same memory twice leads to undefined
behavior.
○ Syntax: delete pointer_variable; or delete[] pointer_variable; (for arrays)
Function Templates:
A function template is a blueprint for creating functions that can operate on different data types
without being rewritten for each type. Instead of writing separate functions for int, float, double,
etc., you write one function template that takes a type parameter. The compiler then generates
specific versions of the function for the data types used in the calls.
Syntax:
template <typename T>
Example:
#include <iostream>
using namespace std;
template <typename T>
T findMaximum(T a, T b) {
return (a > b) ? a : b;
}
int main() {
cout << "Max of 5 and 10 (int): " << findMaximum(5, 10) << endl;
cout << "Max of 3.5 and 7.2 (double): " << findMaximum(3.5, 7.2) << endl;
cout << "Max of 'Z' and 'A' (char): " << findMaximum('Z', 'A') << endl;
return 0;
}
Class Templates:
A class template is a blueprint for creating classes that can work with different data types. It
allows you to define a class structure (data members and member functions) once, and then
use that structure with any data type. This is particularly useful for generic data structures like
stacks, queues, lists, or pairs, where the underlying type of elements can vary.
Syntax:
template <typename T>
class ClassName {
// class members using T
};
Example:
#include <iostream>
using namespace std;
template <typename T>
class Pair {
private:
T first;
T second;
public:
Pair(T f, T s) : first(f), second(s) {}
void display() {
cout << "First: " << first << ", Second: " << second << endl;
}
T getMax() {
return (first > second) ? first : second;
}
};
int main() {
Pair<int> intPair(10, 20);
cout << "Int Pair: ";
intPair.display();
cout << "Max in Int Pair: " << intPair.getMax() << endl;
Pair<double> doublePair(15.5, 12.3);
cout << "Double Pair: ";
doublePair.display();
cout << "Max in Double Pair: " << doublePair.getMax() << endl;
Pair<char> charPair('X', 'Y');
cout << "Char Pair: ";
charPair.display();
cout << "Max in Char Pair: " << charPair.getMax() << endl;
return 0;
}
Q7. Define exception handling. What are the basics of exception
handling in C++?
Exception Handling: Exception handling in C++ is a powerful mechanism that allows
programs to detect and respond to exceptional conditions (errors or unusual events) that occur
during runtime, without crashing or terminating abruptly. It separates the error-handling code
from the normal program logic, making the code cleaner, more robust, and easier to maintain.
○ When an error condition is detected within a try block (or a function called from
within a try block), an exception is thrown using the throw keyword.
○ The throw statement can throw any type of data, including primitive types (like int,
char*, double) or objects of user-defined classes.
○ When an exception is thrown, the program control immediately transfers from the
throw point to the appropriate catch handler. The normal execution flow is
interrupted, and the stack is unwound (destructors of local objects are called).
○ Syntax: throw exception_object; or throw value;
○ The catch block (also known as an exception handler) is where the exception is
caught and handled.
○ A catch block immediately follows a try block. It specifies the type of exception it
can handle in its parameter list.
○ If the type of the thrown exception matches the type in a catch block's parameter,
that catch block is executed.
○ Multiple catch blocks can be associated with a single try block to handle different
types of exceptions.
○ A catch(...) block (ellipsis) can be used to catch any type of exception (a
"catch-all" handler). It should be placed last.
○ Syntax:
catch (ExceptionType e) {
// Code to handle the exception
}
catch (...) { // Catch-all
// Handle any other exception
}
Basic Flow:
1. Code within a try block executes.
2. If an abnormal condition occurs, a throw statement sends an exception object.
3. The C++ runtime system looks for a catch block that can handle the type of the thrown
exception.
4. If a matching catch block is found, control jumps to that catch block. The code inside the
catch block executes, handling the error.
5. After the catch block completes, execution resumes at the statement immediately
following the catch block (or try-catch construct).
6. If no matching catch block is found after searching through the current function's catch
blocks and unwinding the call stack, the program terminates (often by calling
std::terminate()).
Advantages:
● Separation of Concerns: Separates error-handling code from normal logic.
● Robustness: Allows graceful handling of errors, preventing program crashes.
● Clarity: Makes the code more readable by centralizing error management.
Q8. Write a program to overload the << and >> operators for input
and output in a class.
Code:
#include <iostream>
#include <string>
using namespace std;
class Book {
private:
string title;
string author;
int publicationYear;
public:
Book() : title(""), author(""), publicationYear(0) {}
Book(string t, string a, int y) : title(t), author(a), publicationYear(y) {}
friend ostream& operator<<(ostream& os, const Book& book);
friend istream& operator>>(istream& is, Book& book);
};
ostream& operator<<(ostream& os, const Book& book) {
os << "Title: \"" << book.title << "\", Author: " << book.author << ", Year: " <<
book.publicationYear;
return os;
}
istream& operator>>(istream& is, Book& book) {
cout << "Enter Title: ";
getline(is >> ws, book.title);
cout << "Enter Author: ";
getline(is >> ws, book.author);
cout << "Enter Publication Year: ";
is >> book.publicationYear;
return is;
}
int main() {
Book book1("The Great Gatsby", "F. Scott Fitzgerald", 1925);
cout << "Book 1 Details: " << book1 << endl;
Book book2;
cout << "\nEnter details for Book 2:" << endl;
cin >> book2;
cout << "\nBook 2 Details: " << book2 << endl;
return 0;
}
Output:
Book 1 Details: Title: "The Great Gatsby", Author: F. Scott Fitzgerald, Year: 1925
Book 2 Details: Title: "My New Book", Author: Jane Doe, Year: 2023
Q9. Write a program to overload binary + and unary - operators.
Code:
#include <iostream>
using namespace std;
class Vector2D {
private:
double x;
double y;
public:
Vector2D(double valX = 0.0, double valY = 0.0) : x(valX), y(valY) {}
Vector2D operator+(const Vector2D& other) const {
return Vector2D(x + other.x, y + other.y);
}
Vector2D operator-() const {
return Vector2D(-x, -y);
}
void display() const {
cout << "(" << x << ", " << y << ")" << endl;
}
};
int main() {
Vector2D v1(2.5, 3.0);
Vector2D v2(1.0, -1.5);
cout << "Vector 1: ";
v1.display();
cout << "Vector 2: ";
v2.display();
Vector2D sum = v1 + v2;
cout << "Sum (V1 + V2): ";
sum.display();
Vector2D negativeV1 = -v1;
cout << "Negative V1 (-V1): ";
negativeV1.display();
Vector2D negativeV2 = -v2;
cout << "Negative V2 (-V2): ";
negativeV2.display();
return 0;
}
Output:
Vector 1: (2.5, 3)
Vector 2: (1, -1.5)
Sum (V1 + V2): (3.5, 1.5)
Negative V1 (-V1): (-2.5, -3)
Negative V2 (-V2): (-1, 1.5)
Q10. Write a program demonstrating multilevel and multiple
inheritance.
Code:
#include <iostream>
#include <string>
using namespace std;
class Grandparent {
public:
string grandName;
Grandparent(string gn) : grandName(gn) {
cout << "Grandparent constructor: " << grandName << endl;
}
void displayGrandparent() {
cout << "Grandparent Name: " << grandName << endl;
}
~Grandparent() {
cout << "Grandparent destructor: " << grandName << endl;
}
};
class Parent : public Grandparent {
public:
string parentName;
Parent(string gn, string pn) : Grandparent(gn), parentName(pn) {
cout << "Parent constructor: " << parentName << endl;
}
void displayParent() {
displayGrandparent();
cout << "Parent Name: " << parentName << endl;
}
~Parent() {
cout << "Parent destructor: " << parentName << endl;
}
};
class Child : public Parent {
public:
string childName;
Child(string gn, string pn, string cn) : Parent(gn, pn), childName(cn) {
cout << "Child constructor: " << childName << endl;
}
void displayChild() {
displayParent();
cout << "Child Name: " << childName << endl;
}
~Child() {
cout << "Child destructor: " << childName << endl;
}
};
class SkillA {
public:
string skillAName;
SkillA(string sn) : skillAName(sn) {
cout << "SkillA constructor: " << skillAName << endl;
}
void showSkillA() {
cout << "Has Skill A: " << skillAName << endl;
}
~SkillA() {
cout << "SkillA destructor: " << skillAName << endl;
}
};
class SkillB {
public:
string skillBName;
SkillB(string sn) : skillBName(sn) {
cout << "SkillB constructor: " << skillBName << endl;
}
void showSkillB() {
cout << "Has Skill B: " << skillBName << endl;
}
~SkillB() {
cout << "SkillB destructor: " << skillBName << endl;
}
};
class Expert : public Child, public SkillA, public SkillB {
public:
string expertName;
Expert(string gn, string pn, string cn, string sa, string sb, string en)
: Child(gn, pn, cn), SkillA(sa), SkillB(sb), expertName(en) {
cout << "Expert constructor: " << expertName << endl;
}
void displayExpert() {
cout << "\n--- Expert Details (" << expertName << ") ---" << endl;
displayChild();
showSkillA();
showSkillB();
}
~Expert() {
cout << "Expert destructor: " << expertName << endl;
}
};
int main() {
cout << "--- Demonstrating Multilevel Inheritance ---" << endl;
Child myChild("Grand John", "Parent Jane", "Child Jim");
myChild.displayChild();
cout << "\n--- Demonstrating Multiple Inheritance ---" << endl;
Expert professional("Grand Alice", "Parent Bob", "Child Charlie", "Coding", "Design", "Ava
The Expert");
professional.displayExpert();
cout << "\n--- Program End ---" << endl;
return 0;
}
Output:
#include <iostream>
#include <string>
using namespace std;
class Shape {
public:
string name;
Shape(string n) : name(n) {
cout << "Shape constructor: " << name << endl;
}
virtual double calculateArea() = 0;
virtual void displayInfo() {
cout << "This is a " << name << "." << endl;
}
virtual ~Shape() {
cout << "Shape destructor: " << name << endl;
}
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : Shape("Circle"), radius(r) {
cout << " Circle constructor: radius " << radius << endl;
}
double calculateArea() override {
return 3.14159 * radius * radius;
}
void displayInfo() override {
cout << "This is a Circle with radius " << radius << "." << endl;
}
~Circle() override {
cout << " Circle destructor: radius " << radius << endl;
}
};
class Rectangle : public Shape {
private:
double length;
double width;
public:
Rectangle(double l, double w) : Shape("Rectangle"), length(l), width(w) {
cout << " Rectangle constructor: length " << length << ", width " << width << endl;
}
double calculateArea() override {
return length * width;
}
void displayInfo() override {
cout << "This is a Rectangle with length " << length << " and width " << width << "." <<
endl;
}
~Rectangle() override {
cout << " Rectangle destructor: length " << length << ", width " << width << endl;
}
};
int main() {
Shape* shapePtr1 = new Circle(5.0);
Shape* shapePtr2 = new Rectangle(4.0, 6.0);
cout << "\n--- Displaying Info and Area ---" << endl;
shapePtr1->displayInfo();
cout << "Area: " << shapePtr1->calculateArea() << endl;
shapePtr2->displayInfo();
cout << "Area: " << shapePtr2->calculateArea() << endl;
cout << "\n--- Cleaning up ---" << endl;
delete shapePtr1;
shapePtr1 = nullptr;
delete shapePtr2;
shapePtr2 = nullptr;
cout << "\n--- Program End ---" << endl;
return 0;
}
Output:
#include <iostream>
#include <string>
using namespace std;
template <typename T>
T findMax(T a, T b) {
return (a > b) ? a : b;
}
int main() {
cout << "Maximum of 10 and 20 (int): " << findMax(10, 20) << endl;
cout << "Maximum of 3.14 and 2.71 (double): " << findMax(3.14, 2.71) << endl;
cout << "Maximum of 'A' and 'Z' (char): " << findMax('A', 'Z') << endl;
string s1 = "apple";
string s2 = "banana";
cout << "Maximum of \"" << s1 << "\" and \"" << s2 << "\" (string): " << findMax(s1, s2) <<
endl;
return 0;
}
Output:
#include <iostream>
#include <string>
using namespace std;
double divide(int numerator, int denominator) {
if (denominator == 0) {
throw "Division by zero is not allowed!";
}
if (numerator < 0) {
throw string("Numerator cannot be negative for this operation.");
}
if (numerator > 1000) {
throw 999;
}
return static_cast<double>(numerator) / denominator;
}
int main() {
cout << "--- Demonstrating Exception Handling ---" << endl;
try {
cout << "Attempting division (10, 2): " << divide(10, 2) << endl;
cout << "Attempting division (5, 0): ";
cout << divide(5, 0) << endl;
}
catch (const char* msg) {
cout << "Caught C-style string exception: " << msg << endl;
}
try {
cout << "\nAttempting division (-10, 2): ";
cout << divide(-10, 2) << endl;
}
catch (const string& msg) {
cout << "Caught std::string exception: " << msg << endl;
}
try {
cout << "\nAttempting division (1500, 50): ";
cout << divide(1500, 50) << endl;
}
catch (int errorCode) {
cout << "Caught int exception (Error Code): " << errorCode << endl;
}
try {
cout << "\nAttempting division (25, 5): " << divide(25, 5) << endl;
}
catch (...) {
cout << "Caught an unknown exception type." << endl;
}
cout << "\n--- Program continues after exceptions ---" << endl;
return 0;
}
Output:
Attempting division (-10, 2): Caught std::string exception: Numerator cannot be negative for this
operation.
Attempting division (1500, 50): Caught int exception (Error Code): 999
Output:
Code:
#include <iostream>
#include <string>
using namespace std;
class CustomDivideByZeroException {
private:
string message;
public:
CustomDivideByZeroException(string msg) : message(msg) {}
const char* what() const {
return message.c_str();
}
};
double safeDivideCustom(double numerator, double denominator) {
if (denominator == 0) {
throw CustomDivideByZeroException("Custom Error: Cannot divide by zero!");
}
return numerator / denominator;
}
int main() {
cout << "--- User-Defined Exception Class Demo ---" << endl;
double num1 = 10.0;
double den1 = 2.0;
double num2 = 15.0;
double den2 = 0.0;
double num3 = 20.0;
double den3 = 0.0;
try {
cout << num1 << " / " << den1 << " = " << safeDivideCustom(num1, den1) << endl;
cout << num2 << " / " << den2 << " = ";
cout << safeDivideCustom(num2, den2) << endl;
}
catch (const CustomDivideByZeroException& e) {
cout << "Caught CustomDivideByZeroException: " << e.what() << endl;
}
cout << "\nAttempting another division by zero with user-defined exception:" << endl;
try {
cout << num3 << " / " << den3 << " = ";
cout << safeDivideCustom(num3, den3) << endl;
}
catch (const CustomDivideByZeroException& e) {
cout << "Caught CustomDivideByZeroException: " << e.what() << endl;
}
cout << "\nProgram continues after handling custom exceptions." << endl;
return 0;
}
Output: