0% found this document useful (0 votes)
4 views18 pages

CPP Lab Programs

The document provides a series of C++ programming experiments focusing on object-oriented concepts such as constructors, destructors, function overloading, and polymorphism. Each experiment includes explanations of key concepts, code examples, and their outputs, demonstrating practical applications of these principles. Topics covered include deep vs shallow copy, RAII, dynamic binding, multilevel inheritance, and abstract classes.

Uploaded by

oneadityasinha
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
4 views18 pages

CPP Lab Programs

The document provides a series of C++ programming experiments focusing on object-oriented concepts such as constructors, destructors, function overloading, and polymorphism. Each experiment includes explanations of key concepts, code examples, and their outputs, demonstrating practical applications of these principles. Topics covered include deep vs shallow copy, RAII, dynamic binding, multilevel inheritance, and abstract classes.

Uploaded by

oneadityasinha
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd

C++ Object-Oriented Programming

Lab Programs – Complete Solutions with Beginner-Friendly Explanations


Experiment 1: Student Class – Constructors, Destructor & Deep vs Shallow
Copy

📘 Concept Explanation
💡 Constructors are special functions that run automatically when an object is created. A 'default constructor' takes no
arguments. A 'parameterized constructor' accepts values. A 'copy constructor' creates a new object from an existing
one. Deep copy duplicates the actual data; shallow copy only copies the pointer (dangerous with dynamic memory).

💻 Program Code
#include <iostream>
#include <cstring>
using namespace std;

class Student {
char* name; // pointer – will be stored on the heap
int rollNo;
float marks;

public:
// 1. Default Constructor
Student() {
name = new char[20];
strcpy(name, "Unknown");
rollNo = 0;
marks = 0.0f;
cout << "Default constructor called\n";
}

// 2. Parameterized Constructor
Student(const char* n, int r, float m) {
name = new char[strlen(n) + 1];
strcpy(name, n);
rollNo = r;
marks = m;
cout << "Parameterized constructor called\n";
}

// 3. Deep Copy Constructor


Student(const Student& other) {
name = new char[strlen([Link]) + 1]; // allocate NEW memory
strcpy(name, [Link]);
rollNo = [Link];
marks = [Link];
cout << "Copy constructor (deep copy) called\n";
}

// Destructor – frees heap memory


~Student() {
cout << "Destructor called for: " << name << "\n";
delete[] name;
}
void display() const {
cout << "Name: " << name
<< ", Roll: " << rollNo
<< ", Marks: " << marks << "\n";
}

void setName(const char* n) {


strcpy(name, n);
}
};

int main() {
Student s1; // default
Student s2("Alice", 101, 95.5f); // parameterized
Student s3(s2); // deep copy

cout << "\n--- Student Details ---\n";


[Link]();
[Link]();
[Link]();

// Prove deep copy: changing s3's name does NOT affect s2


[Link]("Bob");
cout << "\nAfter changing s3's name:\n";
[Link](); // still "Alice"
[Link](); // now "Bob"

return 0;
}

Experiment 2: Box Class – Multiple Constructors, Validation & Initialiser


Lists

📘 Concept Explanation
💡 Constructor initialiser lists (the colon syntax after the constructor name) are the most efficient way to initialise
member variables—they set values before the constructor body runs. Input validation ensures we never create a Box
with negative dimensions.

💻 Program Code
#include <iostream>
using namespace std;

class Box {
double length, breadth, height;

// Private helper to validate


static double validate(double v) {
if (v < 0) {
cout << "Warning: negative value set to 0\n";
return 0;
}
return v;
}

public:
// 1. Default – unit cube
Box() : length(1), breadth(1), height(1) {
cout << "Default constructor: unit cube\n";
}

// 2. Three-parameter constructor (uses initialiser list)


Box(double l, double b, double h)
: length(validate(l)), breadth(validate(b)), height(validate(h)) {
cout << "Parameterized constructor called\n";
}

// 3. Single-parameter – cube (all sides equal)


explicit Box(double side)
: length(validate(side)), breadth(validate(side)), height(validate(side))
{
cout << "Cube constructor called\n";
}

double volume() const { return length * breadth * height; }


double surfaceArea() const {
return 2 * (length*breadth + breadth*height + height*length);
}

void display() const {


cout << "Dimensions: " << length << " x "
<< breadth << " x " << height << "\n";
cout << "Volume : " << volume() << "\n";
cout << "Surface Area: " << surfaceArea() << "\n";
}
};

int main() {
Box b1; // unit cube
Box b2(4.0, 3.0, 2.0); // custom box
Box b3(5.0); // cube of side 5
Box b4(-1.0, 3.0, 2.0); // negative – triggers validation

cout << "\n--- Box 1 (default) ---\n"; [Link]();


cout << "\n--- Box 2 (4x3x2) ---\n"; [Link]();
cout << "\n--- Box 3 (cube 5) ---\n"; [Link]();
cout << "\n--- Box 4 (negative l) ---\n"; [Link]();

return 0;
}
Experiment 3: FileHandler – RAII, Static Object Counter & Scoped Lifecycle

📘 Concept Explanation
💡 RAII (Resource Acquisition Is Initialisation) means acquiring resources (memory, files) in the constructor and releasing
them in the destructor. A static variable belongs to the class, not any single object—perfect for counting how many
objects are alive at any moment.

💻 Program Code
#include <iostream>
using namespace std;

class FileHandler {
char* buffer; // simulated file data
int id;
static int activeCount; // shared by ALL objects

public:
FileHandler(int fileId) : id(fileId) {
buffer = new char[256];
activeCount++;
cout << "FileHandler #" << id << " opened. Active: " << activeCount << "\
n";
}

~FileHandler() {
delete[] buffer;
activeCount--;
cout << "FileHandler #" << id << " closed. Active: " << activeCount << "\
n";
}

static int getActive() { return activeCount; }

void write(const char* data) {


// In a real program this would write to an actual file
cout << "FileHandler #" << id << " writing: " << data << "\n";
}
};

// Define (not just declare) the static variable outside the class
int FileHandler::activeCount = 0;

int main() {
cout << "=== Outer scope begins ===\n";
FileHandler f1(1);

{
cout << "\n--- Inner scope begins ---\n";
FileHandler f2(2);
FileHandler f3(3);
[Link]("Hello from f2");
[Link]("Hello from f3");
cout << "Active inside inner scope: " << FileHandler::getActive() << "\
n";
cout << "--- Inner scope ends ---\n";
} // f2 and f3 destructors called automatically here

cout << "\nActive after inner scope: " << FileHandler::getActive() << "\n";
cout << "=== Outer scope ends ===\n";
return 0; // f1 destructor called here
}

Experiment 4: Function Overloading – area() for Square, Rectangle & Circle

📘 Concept Explanation
💡 Function overloading lets you use the same function name with different parameter lists. The compiler picks the right
version based on the types and number of arguments you pass. Using distinct types (int vs double) prevents ambiguity.

💻 Program Code
#include <iostream>
#include <cmath>
using namespace std;

const double PI = 3.14159265358979;

// area of a square (int side)


int area(int side) {
return side * side;
}

// area of a rectangle (double length, double breadth)


double area(double length, double breadth) {
return length * breadth;
}

// area of a circle (float radius) – note: float distinguishes from double


overload
float area(float radius) {
return static_cast<float>(PI * radius * radius);
}

int main() {
// Square
int sq = area(5);
cout << "Area of square (side=5) : " << sq << "\n";

// Rectangle
double rect = area(4.5, 3.2);
cout << "Area of rectangle (4.5 x 3.2) : " << rect << "\n";
// Circle
float circ = area(7.0f);
cout << "Area of circle (radius=7.0) : " << circ << "\n";

return 0;
}

Experiment 5: Function Overloading – sum() with Default Arguments &


Ambiguity

📘 Concept Explanation
💡 Default arguments allow callers to omit certain parameters. But mixing overloaded functions and default arguments
can confuse the compiler (ambiguity error) when multiple overloads match the same call. The example shows how to
spot and resolve this.

💻 Program Code
#include <iostream>
using namespace std;

// Overload 1: sum of two integers


int sum(int a, int b) {
cout << "[2-int version] ";
return a + b;
}

// Overload 2: sum of three integers


int sum(int a, int b, int c) {
cout << "[3-int version] ";
return a + b + c;
}

// Overload 3: sum of two floats


float sum(float a, float b) {
cout << "[2-float version] ";
return a + b;
}

/* ─── Ambiguity demonstration ───────────────────────────────────────────


If we wrote:
int sum(int a, int b = 0) { ... } // default arg
and kept:
int sum(int a, int b) { ... } // no default
calling sum(3, 4) would match BOTH → compiler error!

The safe approach below avoids that by never mixing defaults with
identical base signatures.
─────────────────────────────────────────────────────────────────── */
int main() {
cout << "sum(3, 4) = " << sum(3, 4) << "\n";
cout << "sum(1, 2, 3) = " << sum(1, 2, 3) << "\n";
cout << "sum(1.5f, 2.5f) = " << sum(1.5f, 2.5f) << "\n";

// Explicit cast resolves potential ambiguity with literals


cout << "sum((float)2, (float)3) = " << sum((float)2, (float)3) << "\n";

return 0;
}

Experiment 6: Abstract Base Class Vehicle – Pure Virtual Functions &


Runtime Polymorphism

📘 Concept Explanation
💡 A pure virtual function (= 0) makes a class abstract—you can't create objects of it directly. Derived classes MUST
provide their own version of the function. When you call via a base class pointer, C++ decides at RUNTIME which version
to use—this is runtime polymorphism.

💻 Program Code
#include <iostream>
using namespace std;

// Abstract base class


class Vehicle {
protected:
int speed; // km/h
float fuel; // litres

public:
Vehicle(int s, float f) : speed(s), fuel(f) {}

// Pure virtual – must be overridden


virtual void display() const = 0;

// Virtual destructor (good practice)


virtual ~Vehicle() {}
};

class Car : public Vehicle {


int doors;
public:
Car(int s, float f, int d) : Vehicle(s, f), doors(d) {}

void display() const override {


cout << "[Car] Speed: " << speed << " km/h"
<< ", Fuel: " << fuel << " L"
<< ", Doors: " << doors << "\n";
}
};

class Bike : public Vehicle {


bool hasSidecar;
public:
Bike(int s, float f, bool sc) : Vehicle(s, f), hasSidecar(sc) {}

void display() const override {


cout << "[Bike] Speed: " << speed << " km/h"
<< ", Fuel: " << fuel << " L"
<< ", Sidecar: " << (hasSidecar ? "Yes" : "No") << "\n";
}
};

int main() {
// Base class pointers pointing to derived objects
Vehicle* v1 = new Car(120, 45.0f, 4);
Vehicle* v2 = new Bike(80, 12.5f, false);

cout << "=== Vehicle Display (via base pointer) ===\n";


v1->display(); // calls Car::display() at runtime
v2->display(); // calls Bike::display() at runtime

delete v1;
delete v2;
return 0;
}

Experiment 7: Dynamic Binding – Loop Over Base Pointers

📘 Concept Explanation
💡 Storing different derived-class objects in a base-class pointer array and looping over them is the classic pattern for
dynamic binding. The correct overridden function is chosen at runtime for each object—even though all pointers are the
same type.

💻 Program Code
#include <iostream>
using namespace std;

class Animal {
public:
virtual void speak() const {
cout << "[Animal] Generic sound\n";
}
virtual ~Animal() {}
};
class Dog : public Animal {
public:
void speak() const override {
cout << "[Dog] Woof! Woof!\n";
}
};

class Cat : public Animal {


public:
void speak() const override {
cout << "[Cat] Meow~\n";
}
};

class Duck : public Animal {


public:
void speak() const override {
cout << "[Duck] Quack!\n";
}
};

int main() {
// Array of base-class pointers – each points to a different derived type
Animal* zoo[4];
zoo[0] = new Dog();
zoo[1] = new Cat();
zoo[2] = new Duck();
zoo[3] = new Dog();

cout << "=== Animals speaking (dynamic binding) ===\n";


for (int i = 0; i < 4; i++) {
zoo[i]->speak(); // correct version chosen at runtime
}

// Clean up
for (int i = 0; i < 4; i++) delete zoo[i];
return 0;
}

Experiment 8: Multilevel Inheritance – Shape → Polygon → Rectangle →


Square

📘 Concept Explanation
💡 Multilevel inheritance chains derived classes one after another. Each level can override virtual functions. When called
through a base pointer, the most-derived version runs—demonstrating polymorphism across four levels.
💻 Program Code
#include <iostream>
using namespace std;

class Shape {
public:
virtual double area() const {
cout << "[Shape] area() – no formula\n";
return 0;
}
virtual ~Shape() {}
};

class Polygon : public Shape {


protected:
int sides;
public:
Polygon(int s) : sides(s) {}
double area() const override {
cout << "[Polygon] area() – general polygon\n";
return 0;
}
};

class Rectangle : public Polygon {


protected:
double length, width;
public:
Rectangle(double l, double w) : Polygon(4), length(l), width(w) {}
double area() const override {
double a = length * width;
cout << "[Rectangle] area() = " << a << "\n";
return a;
}
};

class Square : public Rectangle {


public:
Square(double side) : Rectangle(side, side) {}
double area() const override {
double a = length * length;
cout << "[Square] area() = " << a << "\n";
return a;
}
};

int main() {
Shape* s;

Rectangle rect(5.0, 3.0);


Square sq(4.0);

cout << "=== Via base pointer (runtime binding) ===\n";


s = &rect; s->area();
s = &sq; s->area();

cout << "\n=== Direct calls ===\n";


[Link]();
[Link]();

return 0;
}

Experiment 9: Complex Number Class – Operator Overloading with Friend


Functions

📘 Concept Explanation
💡 Operator overloading lets you use +, -, ==, >>, << with your own classes just like built-in types. Friend functions can
access private members of a class. Overloading >> and << makes input/output as natural as using cin/cout with ints.

💻 Program Code
#include <iostream>
using namespace std;

class Complex {
double real, imag;

public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}

// Overload + using friend function


friend Complex operator+(const Complex& a, const Complex& b);

// Overload - using friend function


friend Complex operator-(const Complex& a, const Complex& b);

// Overload == using friend function


friend bool operator==(const Complex& a, const Complex& b);

// Overload << (output)


friend ostream& operator<<(ostream& out, const Complex& c);

// Overload >> (input)


friend istream& operator>>(istream& in, Complex& c);
};

Complex operator+(const Complex& a, const Complex& b) {


return Complex([Link] + [Link], [Link] + [Link]);
}

Complex operator-(const Complex& a, const Complex& b) {


return Complex([Link] - [Link], [Link] - [Link]);
}
bool operator==(const Complex& a, const Complex& b) {
return ([Link] == [Link]) && ([Link] == [Link]);
}

ostream& operator<<(ostream& out, const Complex& c) {


out << [Link];
if ([Link] >= 0) out << " + " << [Link] << "i";
else out << " - " << -[Link] << "i";
return out;
}

istream& operator>>(istream& in, Complex& c) {


cout << "Enter real part: "; in >> [Link];
cout << "Enter imag part: "; in >> [Link];
return in;
}

int main() {
Complex c1(3, 4), c2(1, -2);

cout << "c1 = " << c1 << "\n";


cout << "c2 = " << c2 << "\n";
cout << "c1 + c2 = " << (c1 + c2) << "\n";
cout << "c1 - c2 = " << (c1 - c2) << "\n";
cout << "c1 == c2? " << (c1 == c2 ? "Yes" : "No") << "\n";
cout << "c1 == c1? " << (c1 == c1 ? "Yes" : "No") << "\n";

Complex c3;
cin >> c3;
cout << "You entered: " << c3 << "\n";

return 0;
}

Experiment 10: Virtual Destructors – Why They Matter in Inheritance

📘 Concept Explanation
💡 When you delete a derived object through a base-class pointer, only the base destructor runs if it's NOT virtual—
causing a memory leak. Declaring the destructor virtual ensures the chain of destructors runs correctly from derived to
base.

💻 Program Code
#include <iostream>
using namespace std;

// ─── WITHOUT virtual destructor (incorrect) ──────────────────────────


class BaseWrong {
public:
BaseWrong() { cout << "[Wrong] Base constructor\n"; }
~BaseWrong() { cout << "[Wrong] Base destructor\n"; } // NOT virtual
};

class DerivedWrong : public BaseWrong {


int* data;
public:
DerivedWrong() {
data = new int[100];
cout << "[Wrong] Derived constructor (allocated memory)\n";
}
~DerivedWrong() {
delete[] data;
cout << "[Wrong] Derived destructor (memory freed)\n";
}
};

// ─── WITH virtual destructor (correct) ───────────────────────────────


class BaseRight {
public:
BaseRight() { cout << "[Right] Base constructor\n"; }
virtual ~BaseRight() { cout << "[Right] Base destructor\n"; } // VIRTUAL
};

class DerivedRight : public BaseRight {


int* data;
public:
DerivedRight() {
data = new int[100];
cout << "[Right] Derived constructor (allocated memory)\n";
}
~DerivedRight() {
delete[] data;
cout << "[Right] Derived destructor (memory freed)\n";
}
};

int main() {
cout << "=== Non-virtual destructor (memory leak!) ===\n";
BaseWrong* w = new DerivedWrong();
delete w; // only BaseWrong::~BaseWrong() runs – memory leaked!

cout << "\n=== Virtual destructor (correct) ===\n";


BaseRight* r = new DerivedRight();
delete r; // DerivedRight::~DerivedRight() then BaseRight::~BaseRight()

return 0;
}

Experiment 11: Compile-Time & Runtime Polymorphism Combined


📘 Concept Explanation
💡 Compile-time polymorphism (function overloading) is resolved before the program runs. Runtime polymorphism
(function overriding with virtual) is resolved while the program runs. This example uses both in the same class hierarchy
to show the difference clearly.

💻 Program Code
#include <iostream>
using namespace std;

class Printer {
public:
// ── COMPILE-TIME polymorphism (overloading) ──
void print(int x) {
cout << "[Overload] Printing int: " << x << "\n";
}
void print(double x) {
cout << "[Overload] Printing double: " << x << "\n";
}
void print(const string& s) {
cout << "[Overload] Printing string: " << s << "\n";
}

// ── RUNTIME polymorphism (virtual overriding) ──


virtual void describe() const {
cout << "[Runtime] I am a generic Printer\n";
}
virtual ~Printer() {}
};

class LaserPrinter : public Printer {


public:
void describe() const override {
cout << "[Runtime] I am a LaserPrinter\n";
}
};

class InkjetPrinter : public Printer {


public:
void describe() const override {
cout << "[Runtime] I am an InkjetPrinter\n";
}
};

int main() {
cout << "=== Compile-Time Polymorphism (Overloading) ===\n";
Printer p;
[Link](42);
[Link](3.14);
[Link]("Hello C++");

cout << "\n=== Runtime Polymorphism (Overriding via base pointer) ===\n";
Printer* printers[3];
printers[0] = new Printer();
printers[1] = new LaserPrinter();
printers[2] = new InkjetPrinter();
for (int i = 0; i < 3; i++) {
printers[i]->describe();
delete printers[i];
}

return 0;
}

Experiment 12: Banking System – Account, SavingsAccount &


CurrentAccount

📘 Concept Explanation
💡 Real-world systems use inheritance and polymorphism to handle many types of objects uniformly. Here, a base
Account class defines common behaviour (deposit, withdraw, display). Derived classes customise interest calculation
while the main program treats all accounts through base pointers.

💻 Program Code
#include <iostream>
using namespace std;

class Account {
protected:
string owner;
double balance;
int accountNo;

public:
Account(const string& name, int no, double initial)
: owner(name), accountNo(no), balance(initial) {}

virtual void deposit(double amount) {


if (amount <= 0) { cout << "Invalid deposit amount\n"; return; }
balance += amount;
cout << "Deposited " << amount << ". Balance: " << balance << "\n";
}

virtual void withdraw(double amount) {


if (amount > balance) { cout << "Insufficient funds\n"; return; }
balance -= amount;
cout << "Withdrawn " << amount << ". Balance: " << balance << "\n";
}

virtual void calculateInterest() = 0; // pure virtual

virtual void display() const {


cout << "Account #" << accountNo
<< " | Owner: " << owner
<< " | Balance: $" << balance << "\n";
}

virtual ~Account() {}
};

class SavingsAccount : public Account {


double interestRate; // annual rate, e.g. 0.05 = 5%

public:
SavingsAccount(const string& name, int no, double bal, double rate)
: Account(name, no, bal), interestRate(rate) {}

void calculateInterest() override {


double interest = balance * interestRate;
balance += interest;
cout << "[Savings] Interest added: $" << interest
<< " | New balance: $" << balance << "\n";
}

void display() const override {


cout << "[Savings] ";
Account::display();
cout << " Rate: " << (interestRate * 100) << "%\n";
}
};

class CurrentAccount : public Account {


double overdraftLimit;

public:
CurrentAccount(const string& name, int no, double bal, double odLimit)
: Account(name, no, bal), overdraftLimit(odLimit) {}

void withdraw(double amount) override {


if (amount > balance + overdraftLimit) {
cout << "Exceeds overdraft limit!\n"; return;
}
balance -= amount;
cout << "[Current] Withdrawn " << amount
<< ". Balance: $" << balance << "\n";
}

void calculateInterest() override {


// Current accounts typically don't earn interest
cout << "[Current] No interest for current accounts\n";
}

void display() const override {


cout << "[Current] ";
Account::display();
cout << " Overdraft limit: $" << overdraftLimit << "\n";
}
};

int main() {
Account* accounts[2];
accounts[0] = new SavingsAccount("Alice", 1001, 5000.0, 0.04);
accounts[1] = new CurrentAccount("Bob", 2001, 3000.0, 1000.0);

cout << "=== Initial State ===\n";


for (auto* a : accounts) a->display();

cout << "\n=== Transactions ===\n";


accounts[0]->deposit(1000);
accounts[0]->withdraw(200);
accounts[1]->deposit(500);
accounts[1]->withdraw(4000); // uses overdraft

cout << "\n=== Calculate Interest ===\n";


for (auto* a : accounts) a->calculateInterest();

cout << "\n=== Final State ===\n";


for (auto* a : accounts) a->display();

for (auto* a : accounts) delete a;


return 0;
}

You might also like