Introduction to OOP
Object-Oriented Programming (OOP) is a programming paradigm that organizes code around objects rather than functions and logic. C++ is one of the most powerful OOP languages, allowing you to model real-world entities as classes and objects.
Object-Oriented Programming
Object-Oriented Programming (OOP) is a programming approach that bundles data (attributes) and the functions that operate on that data (methods) into units called objects. This mirrors how we think about real-world entities - a car has properties (color, speed) and behaviors (accelerate, brake).
Four Pillars of OOP: Encapsulation (bundling data and methods), Abstraction (hiding complexity), Inheritance (creating class hierarchies), Polymorphism (same interface, different implementations).
// Procedural approach - data and functions are separate
struct CarData {
string brand;
int speed;
};
void accelerate(CarData& car) {
car.speed += 10;
}
In procedural programming, we define data structures and functions separately. The CarData
struct holds the data, and the accelerate function operates on it. While this works,
it does not naturally group related functionality together, and anyone can directly access and modify
the data without any validation or control.
// Object-oriented approach - data and behavior together
class Car {
private:
string brand;
int speed;
public:
void accelerate() {
speed += 10;
}
int getSpeed() {
return speed;
}
};
With OOP, the Car class encapsulates both data (brand, speed)
and behavior (accelerate, getSpeed) in a single unit. The private
keyword protects the data from direct external access, while public methods provide
controlled ways to interact with the object. This is encapsulation in action.
// Creating and using objects
int main() {
Car myCar; // Create an object (instance) of Car
myCar.accelerate(); // Call method on the object
cout << "Speed: " << myCar.getSpeed() << endl;
return 0;
}
An object is an instance of a class - when you create myCar, you are creating a specific
car with its own data. You interact with the object through its public methods using dot notation.
Each object maintains its own state independently, so multiple Car objects can have
different speeds.
Why Use OOP?
Encapsulation
Bundle data and methods together, protecting internal state from unauthorized access
Reusability
Create classes once, use them throughout your codebase and in future projects
Inheritance
Create new classes based on existing ones, promoting code reuse and hierarchy
Maintainability
Changes to a class are localized, making code easier to update and debug
| Concept | Description | C++ Keyword |
|---|---|---|
| Class | Blueprint for creating objects | class |
| Object | Instance of a class | Variable of class type |
| Data Members | Variables inside a class | Declared in class body |
| Member Functions | Functions inside a class | Methods defined in class |
| Access Modifiers | Control visibility | public, private, protected |
Defining Classes
A class is a user-defined data type that serves as a blueprint for creating objects. It defines what data an object will hold and what operations can be performed on that data. Classes are the foundation of object-oriented design in C++.
Class
A class is a template that defines the properties (data members) and behaviors (member functions) that objects of that type will have. Think of a class as a cookie cutter - it defines the shape, but each cookie (object) is a separate entity.
Syntax: class ClassName { access_modifier: members }; - Note the semicolon after the closing brace!
// Basic class definition
class Student {
public:
string name;
int age;
double gpa;
};
This is the simplest form of a class. The class keyword is followed by the class name
(conventionally using PascalCase). Inside the curly braces, we declare member variables that every
Student object will have. The public: access modifier makes these members accessible
from outside the class. Note the required semicolon after the closing brace.
// Creating objects (instances)
int main() {
Student alice; // Object created on stack
alice.name = "Alice"; // Access member with dot operator
alice.age = 20;
alice.gpa = 3.8;
cout << alice.name << " has GPA: " << alice.gpa << endl;
return 0;
}
Creating an object is similar to declaring a variable - the class name is the type. The dot operator
(.) accesses members of the object. Each object has its own copy of the data members,
so alice.name and bob.name (another Student object) would be independent.
// Class with methods (member functions)
class Rectangle {
public:
double width;
double height;
// Member function defined inside class
double area() {
return width * height;
}
// Member function declared inside, defined outside
double perimeter();
};
// Define member function outside class using scope resolution
double Rectangle::perimeter() {
return 2 * (width + height);
}
Classes can contain both data and functions. Functions defined inside the class are implicitly
inline. For larger functions, declare them inside the class but define them outside using the
scope resolution operator (::). The Rectangle:: prefix tells the compiler
that perimeter belongs to the Rectangle class.
// Using the Rectangle class
int main() {
Rectangle room;
room.width = 10.5;
room.height = 8.0;
cout << "Area: " << room.area() << endl; // 84
cout << "Perimeter: " << room.perimeter() << endl; // 37
return 0;
}
Member functions have direct access to all data members of the object they are called on. When you
call room.area(), the function uses room.width and room.height
automatically. This is one of the key benefits of OOP - data and the functions that operate on it
are bundled together.
Practice Questions: Defining Classes
Task: Create a class called Book with public members: title (string) and pages (int). Create an object and print the book info.
Show Solution
#include <iostream>
#include <string>
using namespace std;
class Book {
public:
string title;
int pages;
};
int main() {
Book myBook;
myBook.title = "C++ Primer";
myBook.pages = 976;
cout << myBook.title << " has " << myBook.pages << " pages" << endl;
return 0;
}
Task: Create a Student class with name (string), rollNumber (int), and grade (char). Add a display() method to print all details.
Show Solution
#include <iostream>
#include <string>
using namespace std;
class Student {
public:
string name;
int rollNumber;
char grade;
void display() {
cout << "Name: " << name << endl;
cout << "Roll Number: " << rollNumber << endl;
cout << "Grade: " << grade << endl;
}
};
int main() {
Student s1;
s1.name = "Alice";
s1.rollNumber = 101;
s1.grade = 'A';
s1.display();
return 0;
}
Task: Create a Rectangle class with length and width. Add methods area() and perimeter() that return the calculated values. Test with a rectangle of 10x5.
Show Solution
#include <iostream>
using namespace std;
class Rectangle {
public:
double length;
double width;
double area() {
return length * width;
}
double perimeter() {
return 2 * (length + width);
}
};
int main() {
Rectangle rect;
rect.length = 10;
rect.width = 5;
cout << "Area: " << rect.area() << endl; // 50
cout << "Perimeter: " << rect.perimeter() << endl; // 30
return 0;
}
Task: Create a BankAccount class with accountNumber, holderName, and balance. Add deposit() and withdraw() methods that update the balance.
Show Solution
#include <iostream>
#include <string>
using namespace std;
class BankAccount {
public:
string accountNumber;
string holderName;
double balance;
void deposit(double amount) {
balance += amount;
cout << "Deposited: $" << amount << endl;
}
void withdraw(double amount) {
if (amount <= balance) {
balance -= amount;
cout << "Withdrawn: $" << amount << endl;
} else {
cout << "Insufficient funds!" << endl;
}
}
void showBalance() {
cout << "Current Balance: $" << balance << endl;
}
};
int main() {
BankAccount acc;
acc.accountNumber = "ACC001";
acc.holderName = "John Doe";
acc.balance = 1000;
acc.showBalance(); // $1000
acc.deposit(500); // Deposited: $500
acc.withdraw(200); // Withdrawn: $200
acc.showBalance(); // $1300
return 0;
}
Task: Create a Circle class with radius. Declare methods area(), circumference(), and diameter() inside the class but define them outside using scope resolution operator.
Show Solution
#include <iostream>
using namespace std;
const double PI = 3.14159;
class Circle {
public:
double radius;
// Method declarations
double area();
double circumference();
double diameter();
};
// Method definitions outside class
double Circle::area() {
return PI * radius * radius;
}
double Circle::circumference() {
return 2 * PI * radius;
}
double Circle::diameter() {
return 2 * radius;
}
int main() {
Circle c;
c.radius = 5;
cout << "Radius: " << c.radius << endl;
cout << "Diameter: " << c.diameter() << endl; // 10
cout << "Area: " << c.area() << endl; // 78.5398
cout << "Circumference: " << c.circumference() << endl; // 31.4159
return 0;
}
Access Modifiers
Access modifiers control the visibility of class members. They are fundamental to encapsulation, allowing you to hide implementation details while exposing a clean interface. C++ provides three levels of access: public, private, and protected.
Access Modifiers
Access modifiers determine which parts of your code can access class members.
public members are accessible everywhere, private members only within
the class itself, and protected members within the class and its derived classes.
Default Access: In a class, members are private by default. In a struct, they are public by default.
class BankAccount {
private:
double balance; // Only accessible within class
string accountNumber;
protected:
string ownerName; // Accessible in derived classes too
public:
void deposit(double amount); // Accessible everywhere
double getBalance();
};
The private section contains sensitive data like balance that should never
be directly modified from outside. The protected section contains data that derived
classes (like SavingsAccount) might need. The public section provides the interface -
the safe ways to interact with the account.
class BankAccount {
private:
double balance = 0;
public:
void deposit(double amount) {
if (amount > 0) { // Validation!
balance += amount;
}
}
bool withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return true;
}
return false; // Insufficient funds
}
double getBalance() {
return balance; // Read-only access
}
};
This demonstrates why private data with public methods is powerful. The deposit method
validates that amounts are positive. The withdraw method ensures you cannot withdraw
more than available. If balance were public, anyone could set it to any value, including
negative numbers, breaking your program logic.
int main() {
BankAccount myAccount;
myAccount.deposit(1000); // OK - public method
myAccount.withdraw(500); // OK - public method
cout << myAccount.getBalance(); // OK - prints 500
// myAccount.balance = 1000000; // ERROR! balance is private
return 0;
}
From outside the class, you can only use public members. Attempting to access balance
directly causes a compiler error. This is encapsulation - the internal state is hidden, and all
interactions go through controlled methods. This makes your code more robust and easier to maintain.
| Modifier | Same Class | Derived Class | Outside |
|---|---|---|---|
public |
Yes | Yes | Yes |
protected |
Yes | Yes | No |
private |
Yes | No | No |
Practice Questions: Access Modifiers
Task: Create a Person class with private age and public methods setAge() (validates age is 0-150) and getAge().
Show Solution
#include <iostream>
using namespace std;
class Person {
private:
int age;
public:
void setAge(int a) {
if (a >= 0 && a <= 150) {
age = a;
}
}
int getAge() {
return age;
}
};
int main() {
Person p;
p.setAge(25);
cout << "Age: " << p.getAge() << endl; // 25
p.setAge(-5); // Ignored due to validation
cout << "Age: " << p.getAge() << endl; // Still 25
return 0;
}
Task: Create an Employee class with private salary and public name. Add setSalary() that only accepts positive values and getSalary() to read it.
Show Solution
#include <iostream>
#include <string>
using namespace std;
class Employee {
private:
double salary;
public:
string name;
void setSalary(double s) {
if (s > 0) {
salary = s;
} else {
cout << "Invalid salary!" << endl;
}
}
double getSalary() {
return salary;
}
};
int main() {
Employee emp;
emp.name = "John";
emp.setSalary(50000);
cout << emp.name << " earns $" << emp.getSalary() << endl;
emp.setSalary(-1000); // Invalid salary!
cout << "Salary unchanged: $" << emp.getSalary() << endl;
return 0;
}
Task: Create a Password class with private password. Add setPassword() that only accepts passwords with at least 8 characters, and checkPassword() that returns true if input matches.
Show Solution
#include <iostream>
#include <string>
using namespace std;
class Password {
private:
string password;
public:
bool setPassword(string pwd) {
if (pwd.length() >= 8) {
password = pwd;
return true;
}
cout << "Password must be at least 8 characters!" << endl;
return false;
}
bool checkPassword(string input) {
return input == password;
}
};
int main() {
Password user;
user.setPassword("short"); // Too short
user.setPassword("mySecurePass123"); // OK
if (user.checkPassword("mySecurePass123")) {
cout << "Access granted!" << endl;
} else {
cout << "Access denied!" << endl;
}
return 0;
}
Task: Create a Temperature class with private celsius. Add setCelsius(), getCelsius(), setFahrenheit(), and getFahrenheit() methods that convert appropriately.
Show Solution
#include <iostream>
using namespace std;
class Temperature {
private:
double celsius;
public:
void setCelsius(double c) {
celsius = c;
}
double getCelsius() {
return celsius;
}
void setFahrenheit(double f) {
celsius = (f - 32) * 5.0 / 9.0;
}
double getFahrenheit() {
return celsius * 9.0 / 5.0 + 32;
}
};
int main() {
Temperature temp;
temp.setCelsius(100);
cout << "100C = " << temp.getFahrenheit() << "F" << endl; // 212F
temp.setFahrenheit(32);
cout << "32F = " << temp.getCelsius() << "C" << endl; // 0C
return 0;
}
Task: Create a SecureAccount class with private balance and transactionCount. Add deposit(), withdraw() (validates sufficient funds), getBalance(), and getTransactionCount().
Show Solution
#include <iostream>
using namespace std;
class SecureAccount {
private:
double balance;
int transactionCount;
public:
SecureAccount() {
balance = 0;
transactionCount = 0;
}
void deposit(double amount) {
if (amount > 0) {
balance += amount;
transactionCount++;
cout << "Deposited: $" << amount << endl;
}
}
bool withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
transactionCount++;
cout << "Withdrawn: $" << amount << endl;
return true;
}
cout << "Transaction failed!" << endl;
return false;
}
double getBalance() { return balance; }
int getTransactionCount() { return transactionCount; }
};
int main() {
SecureAccount acc;
acc.deposit(1000);
acc.deposit(500);
acc.withdraw(200);
acc.withdraw(2000); // Fails
cout << "Balance: $" << acc.getBalance() << endl; // 1300
cout << "Transactions: " << acc.getTransactionCount() << endl; // 3
return 0;
}
Constructors
Constructors are special member functions that initialize objects when they are created. They ensure that every object starts in a valid state. C++ provides several types of constructors for different initialization scenarios.
Constructor
A constructor is a special function that is automatically called when an object is created. It has the same name as the class, no return type (not even void), and can be overloaded to accept different parameters.
Key Point: If you do not define any constructor, C++ provides a default constructor that initializes members to default values (zero for numeric types).
class Player {
private:
string name;
int health;
int score;
public:
// Default constructor - no parameters
Player() {
name = "Unknown";
health = 100;
score = 0;
}
};
The default constructor takes no arguments and sets initial values for all members. It is called
when you create an object without any arguments: Player p;. Without this constructor,
members would have undefined values, potentially causing bugs. Always initialize all members.
class Player {
private:
string name;
int health;
int score;
public:
// Parameterized constructor
Player(string n, int h) {
name = n;
health = h;
score = 0;
}
};
A parameterized constructor accepts arguments to customize the object at creation time. Call it
with: Player hero("Hero", 150);. This is more convenient than creating an object and
then setting each property. You can have multiple constructors with different parameters (overloading).
class Player {
private:
string name;
int health;
int score;
public:
// Constructor with initializer list (preferred)
Player(string n, int h, int s) : name(n), health(h), score(s) {
// Constructor body - additional logic if needed
}
};
The initializer list (after the colon) directly initializes members, which is more efficient than
assigning in the body. For const members and references, this is the only way to
initialize them. The format is member(value) separated by commas. This is the preferred
style in modern C++.
class Player {
public:
string name;
int health;
int score;
// Multiple constructors (overloading)
Player() : name("Unknown"), health(100), score(0) {}
Player(string n) : name(n), health(100), score(0) {}
Player(string n, int h) : name(n), health(h), score(0) {}
Player(string n, int h, int s) : name(n), health(h), score(s) {}
};
Constructor overloading allows multiple ways to create objects. The compiler chooses the appropriate constructor based on the arguments provided. This provides flexibility while keeping the interface clean.
int main() {
Player p1; // Default: Unknown, 100, 0
Player p2("Alice"); // Just name: Alice, 100, 0
Player p3("Bob", 150); // Name and health: Bob, 150, 0
Player p4("Charlie", 200, 50); // All values: Charlie, 200, 50
return 0;
}
Each object is created using a different constructor based on the arguments. This is a key advantage of constructor overloading - users of your class can choose the most convenient way to create objects based on what information they have available.
Player p5 = p4; or Player p5(p4);.
You can override it for custom copying behavior.
Practice Questions: Constructors
Task: Create a Point class with x and y coordinates. Add a default constructor that sets both to 0, and a display() method.
Show Solution
#include <iostream>
using namespace std;
class Point {
private:
int x, y;
public:
Point() {
x = 0;
y = 0;
}
void display() {
cout << "(" << x << ", " << y << ")" << endl;
}
};
int main() {
Point p;
p.display(); // (0, 0)
return 0;
}
Task: Create a Car class with brand and year. Add a parameterized constructor to initialize both values and a showInfo() method.
Show Solution
#include <iostream>
#include <string>
using namespace std;
class Car {
private:
string brand;
int year;
public:
Car(string b, int y) {
brand = b;
year = y;
}
void showInfo() {
cout << year << " " << brand << endl;
}
};
int main() {
Car car1("Toyota", 2022);
Car car2("Honda", 2023);
car1.showInfo(); // 2022 Toyota
car2.showInfo(); // 2023 Honda
return 0;
}
Task: Create a Product class with name, price, and quantity. Add a default constructor and a parameterized constructor. Use initializer lists for both.
Show Solution
#include <iostream>
#include <string>
using namespace std;
class Product {
private:
string name;
double price;
int quantity;
public:
// Default constructor with initializer list
Product() : name("Unknown"), price(0.0), quantity(0) {}
// Parameterized constructor with initializer list
Product(string n, double p, int q) : name(n), price(p), quantity(q) {}
void display() {
cout << name << ": $" << price << " x " << quantity << endl;
}
};
int main() {
Product p1;
Product p2("Laptop", 999.99, 5);
p1.display(); // Unknown: $0 x 0
p2.display(); // Laptop: $999.99 x 5
return 0;
}
Task: Create a Time class with hours, minutes, seconds. Add three constructors: default (00:00:00), hours only, and all three parameters.
Show Solution
#include <iostream>
#include <iomanip>
using namespace std;
class Time {
private:
int hours, minutes, seconds;
public:
// Default constructor
Time() : hours(0), minutes(0), seconds(0) {}
// Hours only
Time(int h) : hours(h), minutes(0), seconds(0) {}
// All parameters
Time(int h, int m, int s) : hours(h), minutes(m), seconds(s) {}
void display() {
cout << setfill('0') << setw(2) << hours << ":"
<< setw(2) << minutes << ":"
<< setw(2) << seconds << endl;
}
};
int main() {
Time t1; // 00:00:00
Time t2(14); // 14:00:00
Time t3(9, 30, 45); // 09:30:45
t1.display();
t2.display();
t3.display();
return 0;
}
Task: Create a Date class with day, month, year. The constructor should validate that day is 1-31, month is 1-12. If invalid, set to 1/1/2000.
Show Solution
#include <iostream>
using namespace std;
class Date {
private:
int day, month, year;
bool isValid(int d, int m, int y) {
return (d >= 1 && d <= 31) && (m >= 1 && m <= 12) && (y > 0);
}
public:
Date(int d, int m, int y) {
if (isValid(d, m, y)) {
day = d;
month = m;
year = y;
} else {
cout << "Invalid date! Setting to default." << endl;
day = 1;
month = 1;
year = 2000;
}
}
void display() {
cout << day << "/" << month << "/" << year << endl;
}
};
int main() {
Date d1(15, 6, 2024); // Valid: 15/6/2024
Date d2(32, 13, 2024); // Invalid: 1/1/2000
d1.display();
d2.display();
return 0;
}
Destructors
Destructors are the opposite of constructors - they clean up when an object is destroyed. They are crucial for releasing resources like dynamic memory, file handles, or network connections that the object acquired during its lifetime.
Destructor
A destructor is a special function called automatically when an object goes out of scope or is explicitly deleted. It has the same name as the class prefixed with a tilde (~), takes no parameters, and cannot be overloaded.
Syntax: ~ClassName() { /* cleanup code */ }
class FileHandler {
private:
string filename;
bool isOpen;
public:
FileHandler(string name) : filename(name), isOpen(true) {
cout << "Opening file: " << filename << endl;
}
~FileHandler() {
if (isOpen) {
cout << "Closing file: " << filename << endl;
isOpen = false;
}
}
};
The destructor ~FileHandler() ensures the file is closed when the object is destroyed.
This pattern is called RAII (Resource Acquisition Is Initialization) - you acquire resources in
the constructor and release them in the destructor. This prevents resource leaks even if exceptions occur.
int main() {
cout << "Starting program" << endl;
{
FileHandler file("data.txt");
cout << "Working with file" << endl;
} // file goes out of scope here - destructor called
cout << "Back to main" << endl;
return 0;
}
When the inner block ends, file goes out of scope and its destructor is automatically
called. This is automatic cleanup - you do not need to remember to close the file. The output shows
the sequence: Starting, Opening, Working, Closing, Back to main.
class DynamicArray {
private:
int* data;
int size;
public:
DynamicArray(int s) : size(s) {
data = new int[size]; // Allocate memory
cout << "Allocated " << size << " integers" << endl;
}
~DynamicArray() {
delete[] data; // Free memory
cout << "Freed memory" << endl;
}
};
When working with dynamic memory (new), destructors are essential. The constructor
allocates memory with new, and the destructor frees it with delete[].
Without the destructor, this memory would leak every time an object is destroyed. This is one of
the most important uses of destructors.
new, your destructor must
use delete. Use delete[] for arrays allocated with new[].
Failing to do this causes memory leaks.
Practice Questions: Destructors
Task: Create a Message class that prints "Object created" in constructor and "Object destroyed" in destructor. Create an object and observe the output.
Show Solution
#include <iostream>
using namespace std;
class Message {
public:
Message() {
cout << "Object created" << endl;
}
~Message() {
cout << "Object destroyed" << endl;
}
};
int main() {
cout << "Before creating object" << endl;
Message m;
cout << "After creating object" << endl;
return 0;
} // Destructor called here automatically
Task: Create a Counter class with a static count. Increment in constructor, decrement in destructor. Print the count in both.
Show Solution
#include <iostream>
using namespace std;
class Counter {
private:
static int count;
public:
Counter() {
count++;
cout << "Object created. Count: " << count << endl;
}
~Counter() {
count--;
cout << "Object destroyed. Count: " << count << endl;
}
};
int Counter::count = 0;
int main() {
Counter c1;
{
Counter c2;
Counter c3;
} // c2 and c3 destroyed here
cout << "Back in main" << endl;
return 0;
} // c1 destroyed here
Task: Create a Logger class with a name. Print "Logger [name] started" in constructor and "Logger [name] stopped" in destructor.
Show Solution
#include <iostream>
#include <string>
using namespace std;
class Logger {
private:
string name;
public:
Logger(string n) : name(n) {
cout << "Logger [" << name << "] started" << endl;
}
~Logger() {
cout << "Logger [" << name << "] stopped" << endl;
}
void log(string message) {
cout << "[" << name << "] " << message << endl;
}
};
int main() {
{
Logger app("AppLog");
app.log("Application running");
} // Destructor called here
cout << "After logger scope" << endl;
return 0;
}
Task: Create a Timer class that records creation time in constructor and prints elapsed time in destructor using chrono.
Show Solution
#include <iostream>
#include <chrono>
#include <thread>
using namespace std;
using namespace chrono;
class Timer {
private:
steady_clock::time_point startTime;
string label;
public:
Timer(string l) : label(l) {
startTime = steady_clock::now();
cout << "Timer [" << label << "] started" << endl;
}
~Timer() {
auto endTime = steady_clock::now();
auto duration = duration_cast<milliseconds>(endTime - startTime);
cout << "Timer [" << label << "] ended: " << duration.count() << "ms" << endl;
}
};
int main() {
{
Timer t("SleepTest");
this_thread::sleep_for(milliseconds(500));
}
return 0;
}
Task: Create a DynamicArray class that allocates an int array in constructor and properly deallocates it in destructor. Add set() and get() methods.
Show Solution
#include <iostream>
using namespace std;
class DynamicArray {
private:
int* data;
int size;
public:
DynamicArray(int s) : size(s) {
data = new int[size];
for (int i = 0; i < size; i++) {
data[i] = 0;
}
cout << "Allocated array of size " << size << endl;
}
~DynamicArray() {
delete[] data;
cout << "Freed array memory" << endl;
}
void set(int index, int value) {
if (index >= 0 && index < size) {
data[index] = value;
}
}
int get(int index) {
if (index >= 0 && index < size) {
return data[index];
}
return -1;
}
};
int main() {
{
DynamicArray arr(5);
arr.set(0, 10);
arr.set(2, 30);
cout << "arr[0] = " << arr.get(0) << endl;
cout << "arr[2] = " << arr.get(2) << endl;
} // Destructor frees memory here
cout << "Memory freed safely" << endl;
return 0;
}
Member Functions
Member functions define the behavior of a class. They can access all class members directly and operate on the object's data. Understanding different types of member functions helps you design clean and efficient classes.
Member Functions
Member functions (also called methods) are functions declared within a class
that operate on the class's data. They have access to all members (public, private, protected)
and can be marked as const if they do not modify the object.
Types: Getters (accessors), Setters (mutators), Const member functions, Static member functions
class Circle {
private:
double radius;
public:
// Setter - modifies data
void setRadius(double r) {
if (r > 0) {
radius = r;
}
}
// Getter - reads data (const - does not modify)
double getRadius() const {
return radius;
}
// Calculation method (const - does not modify)
double area() const {
return 3.14159 * radius * radius;
}
};
Getters and setters provide controlled access to private data. The const keyword after
the parameter list indicates the function will not modify the object. This allows calling these
methods on const objects and communicates intent to other programmers. Always mark
functions as const when they do not modify state.
class Counter {
private:
int count;
static int totalCounters; // Shared by all objects
public:
Counter() : count(0) {
totalCounters++;
}
void increment() { count++; }
int getCount() const { return count; }
// Static function - called on class, not object
static int getTotalCounters() {
return totalCounters;
}
};
int Counter::totalCounters = 0; // Initialize static member
Static member functions belong to the class itself, not to any particular object. They can only
access static members. Call them using the class name: Counter::getTotalCounters().
Static members are useful for tracking class-wide data or providing utility functions related to
the class.
int main() {
Counter c1, c2, c3;
c1.increment();
c1.increment();
c2.increment();
cout << "c1: " << c1.getCount() << endl; // 2
cout << "c2: " << c2.getCount() << endl; // 1
cout << "c3: " << c3.getCount() << endl; // 0
// Static function called on class
cout << "Total counters: " << Counter::getTotalCounters() << endl; // 3
return 0;
}
Each object has its own count, but totalCounters is shared across all
objects. The static function getTotalCounters() is called using the class name and
scope resolution operator, not on an object. This is how you track information across all instances.
class Calculator {
public:
// Method chaining - return reference to *this
Calculator& add(int value) {
result += value;
return *this;
}
Calculator& multiply(int value) {
result *= value;
return *this;
}
int getResult() const { return result; }
private:
int result = 0;
};
Returning *this enables method chaining - calling multiple methods in sequence on the
same object. The this pointer is an implicit parameter to all non-static member functions
that points to the object the method was called on.
int main() {
Calculator calc;
// Method chaining - fluent interface
calc.add(5).multiply(3).add(10);
cout << "Result: " << calc.getResult() << endl; // 25
return 0;
}
Method chaining creates a fluent, readable interface. Each method returns the object itself, so you can immediately call another method on the result. This pattern is common in builder classes and configuration objects.
Practice Questions: Member Functions
Task: Create a Counter class with count value. Add increment(), decrement(), and getCount() const methods.
Show Solution
#include <iostream>
using namespace std;
class Counter {
private:
int count;
public:
Counter() : count(0) {}
void increment() {
count++;
}
void decrement() {
if (count > 0) count--;
}
int getCount() const {
return count;
}
};
int main() {
Counter c;
c.increment();
c.increment();
c.increment();
cout << "Count: " << c.getCount() << endl; // 3
c.decrement();
cout << "Count: " << c.getCount() << endl; // 2
return 0;
}
Task: Create a Circle class with radius. Add setRadius() and const methods getRadius(), area(), and circumference().
Show Solution
#include <iostream>
using namespace std;
const double PI = 3.14159;
class Circle {
private:
double radius;
public:
Circle() : radius(0) {}
void setRadius(double r) {
if (r > 0) radius = r;
}
double getRadius() const {
return radius;
}
double area() const {
return PI * radius * radius;
}
double circumference() const {
return 2 * PI * radius;
}
};
int main() {
Circle c;
c.setRadius(5);
cout << "Radius: " << c.getRadius() << endl;
cout << "Area: " << c.area() << endl;
cout << "Circumference: " << c.circumference() << endl;
return 0;
}
Task: Create an Employee class with a static employeeCount. Add a constructor that increments count, and a static method getTotalEmployees().
Show Solution
#include <iostream>
#include <string>
using namespace std;
class Employee {
private:
string name;
static int employeeCount;
public:
Employee(string n) : name(n) {
employeeCount++;
}
string getName() const {
return name;
}
static int getTotalEmployees() {
return employeeCount;
}
};
int Employee::employeeCount = 0;
int main() {
cout << "Employees: " << Employee::getTotalEmployees() << endl; // 0
Employee e1("Alice");
Employee e2("Bob");
Employee e3("Charlie");
cout << "Employees: " << Employee::getTotalEmployees() << endl; // 3
return 0;
}
Task: Create a MyString class with a string. Add methods: length(), toUpper(), toLower(), and reverse() that return new strings.
Show Solution
#include <iostream>
#include <string>
#include <algorithm>
#include <cctype>
using namespace std;
class MyString {
private:
string str;
public:
MyString(string s) : str(s) {}
int length() const {
return str.length();
}
string toUpper() const {
string result = str;
for (char& c : result) c = toupper(c);
return result;
}
string toLower() const {
string result = str;
for (char& c : result) c = tolower(c);
return result;
}
string reverse() const {
string result = str;
std::reverse(result.begin(), result.end());
return result;
}
string get() const { return str; }
};
int main() {
MyString s("Hello World");
cout << "Original: " << s.get() << endl;
cout << "Length: " << s.length() << endl;
cout << "Upper: " << s.toUpper() << endl;
cout << "Lower: " << s.toLower() << endl;
cout << "Reverse: " << s.reverse() << endl;
return 0;
}
Task: Create a Calculator class with a result. Add chainable methods: add(), subtract(), multiply(), divide(), clear(), and getResult() const.
Show Solution
#include <iostream>
using namespace std;
class Calculator {
private:
double result;
public:
Calculator() : result(0) {}
Calculator& add(double value) {
result += value;
return *this;
}
Calculator& subtract(double value) {
result -= value;
return *this;
}
Calculator& multiply(double value) {
result *= value;
return *this;
}
Calculator& divide(double value) {
if (value != 0) result /= value;
return *this;
}
Calculator& clear() {
result = 0;
return *this;
}
double getResult() const {
return result;
}
};
int main() {
Calculator calc;
// Method chaining: ((10 + 5) * 2 - 6) / 4 = 6
calc.add(10).add(5).multiply(2).subtract(6).divide(4);
cout << "Result: " << calc.getResult() << endl; // 6
calc.clear().add(100).divide(4);
cout << "Result: " << calc.getResult() << endl; // 25
return 0;
}
Object Instantiation
Object instantiation is the process of creating actual objects from a class blueprint. C++ offers multiple ways to create objects - on the stack, on the heap, as arrays, or as pointers. Understanding these options helps you manage memory efficiently and choose the right approach for your needs.
Object Instantiation
Object instantiation is creating a concrete instance of a class. When you instantiate an object, memory is allocated for its data members and its constructor is called to initialize the object.
Two Main Ways: Stack allocation (automatic, scoped lifetime) vs Heap allocation (manual, dynamic lifetime with new/delete).
Stack vs Heap Allocation
class Player {
public:
string name;
int health;
Player(string n, int h) : name(n), health(h) {
cout << name << " created" << endl;
}
~Player() {
cout << name << " destroyed" << endl;
}
};
This Player class has a constructor that announces creation and a destructor that announces
destruction. This helps us visualize when objects are created and destroyed based on how we instantiate them.
// Stack allocation - automatic memory management
int main() {
Player hero("Hero", 100); // Created on stack
cout << "Playing game..." << endl;
return 0;
} // hero automatically destroyed here
Stack-allocated objects are created when declared and automatically destroyed when they go out of scope. This is the simplest and safest way to create objects. The compiler handles all memory management for you, and there is no risk of memory leaks.
// Heap allocation - manual memory management
int main() {
Player* villain = new Player("Villain", 80); // Created on heap
cout << "Playing game..." << endl;
delete villain; // Must manually destroy!
return 0;
}
Heap-allocated objects use new to create and delete to destroy. The object
persists until explicitly deleted, regardless of scope. This gives you control over object lifetime
but requires careful memory management to avoid leaks.
| Feature | Stack | Heap |
|---|---|---|
| Syntax | Player p("A", 100); |
Player* p = new Player("A", 100); |
| Access | Dot: p.name |
Arrow: p->name |
| Destruction | Automatic (scope ends) | Manual (delete p;) |
| Size Limit | Limited (usually ~1MB) | Large (available RAM) |
| Speed | Faster | Slower (allocation overhead) |
Object Pointers
int main() {
Player hero("Hero", 100); // Stack object
Player* ptr = &hero; // Pointer to stack object
// Access via pointer using arrow operator
cout << ptr->name << endl; // Hero
cout << ptr->health << endl; // 100
// Equivalent to:
cout << (*ptr).name << endl; // Hero (dereference then dot)
return 0;
}
You can create pointers to stack objects using the address-of operator (&). Access members
through pointers using the arrow operator (->), which is shorthand for dereferencing and
then using dot ((*ptr).member).
// Dynamic object with pointer
int main() {
Player* enemy = new Player("Enemy", 50);
enemy->health -= 10; // Reduce health
cout << enemy->name << " health: " << enemy->health << endl;
delete enemy; // Free memory
enemy = nullptr; // Good practice: avoid dangling pointer
return 0;
}
After delete, set the pointer to nullptr to avoid dangling pointers. A dangling
pointer points to freed memory and using it causes undefined behavior. Setting to nullptr makes it safe
to check before use.
Arrays of Objects
class Enemy {
public:
string type;
int damage;
Enemy() : type("Goblin"), damage(10) {} // Default constructor required!
Enemy(string t, int d) : type(t), damage(d) {}
};
When creating arrays of objects, a default constructor is required because the array elements must be default-initialized first. If you only have parameterized constructors, you cannot create arrays using simple array syntax.
// Stack array of objects
int main() {
Enemy enemies[3]; // Creates 3 Enemy objects using default constructor
enemies[0].type = "Orc";
enemies[0].damage = 25;
for (int i = 0; i < 3; i++) {
cout << enemies[i].type << " deals " << enemies[i].damage << " damage" << endl;
}
return 0;
}
Stack arrays of objects are automatically created and destroyed. Each element is initialized with the default constructor. You can then modify individual objects as needed. The entire array is destroyed when it goes out of scope.
// Heap array of objects
int main() {
int size = 5;
Enemy* horde = new Enemy[size]; // Dynamic array on heap
horde[0] = Enemy("Dragon", 100);
horde[1] = Enemy("Troll", 50);
for (int i = 0; i < size; i++) {
cout << horde[i].type << ": " << horde[i].damage << endl;
}
delete[] horde; // Use delete[] for arrays!
return 0;
}
For heap arrays, use new Type[size] to allocate and delete[] to deallocate.
Using delete instead of delete[] for arrays causes undefined behavior - it
only destroys the first element. Always match new[] with delete[].
Passing Objects to Functions
// Pass by value - creates a copy
void levelUp(Player p) {
p.health += 50; // Modifies the copy only
cout << "Inside function: " << p.health << endl;
}
Passing by value creates a copy of the object. Changes inside the function do not affect the original. This is safe but can be slow for large objects because the entire object must be copied.
// Pass by reference - no copy, can modify
void heal(Player& p) {
p.health += 25; // Modifies the original!
}
// Pass by const reference - no copy, read-only
void display(const Player& p) {
cout << p.name << ": " << p.health << " HP" << endl;
// p.health = 0; // ERROR! Cannot modify const reference
}
Pass by reference avoids copying and allows modification. Pass by const reference avoids copying but prevents modification - best for read-only access to large objects. This is the preferred way to pass objects you do not need to modify.
// Pass by pointer
void damage(Player* p, int amount) {
if (p != nullptr) { // Always check for null!
p->health -= amount;
}
}
int main() {
Player hero("Hero", 100);
levelUp(hero); // By value: hero unchanged
cout << "After levelUp: " << hero.health << endl; // 100
heal(hero); // By reference: hero modified
cout << "After heal: " << hero.health << endl; // 125
display(hero); // Const ref: read-only
damage(&hero, 30); // By pointer: hero modified
cout << "After damage: " << hero.health << endl; // 95
return 0;
}
Pass by pointer is similar to pass by reference but requires explicit address-taking (&hero)
and can be null. Choose the method based on your needs: value for small types or when a copy is needed,
const reference for read-only access, reference or pointer when modification is required.
std::unique_ptr, std::shared_ptr)
over raw pointers for heap allocation. They automatically manage memory and prevent leaks.
Practice Questions: Object Instantiation
Task: Create a Book class with title and pages. Create one object on the stack and one on the heap. Print both and properly clean up.
Show Solution
#include <iostream>
#include <string>
using namespace std;
class Book {
public:
string title;
int pages;
Book(string t, int p) : title(t), pages(p) {}
void display() {
cout << title << " (" << pages << " pages)" << endl;
}
};
int main() {
// Stack object
Book stackBook("C++ Primer", 976);
stackBook.display();
// Heap object
Book* heapBook = new Book("Clean Code", 464);
heapBook->display();
delete heapBook; // Clean up heap object
return 0;
}
Task: Create a Student class with default constructor. Create an array of 3 students on the stack, set their names, and print all.
Show Solution
#include <iostream>
#include <string>
using namespace std;
class Student {
public:
string name;
int grade;
Student() : name("Unknown"), grade(0) {} // Default constructor required
};
int main() {
Student students[3]; // Array on stack
students[0].name = "Alice";
students[0].grade = 95;
students[1].name = "Bob";
students[1].grade = 87;
students[2].name = "Charlie";
students[2].grade = 92;
for (int i = 0; i < 3; i++) {
cout << students[i].name << ": " << students[i].grade << endl;
}
return 0;
}
Task: Create a Counter class with value. Write three functions: incrementByValue(), incrementByRef(), and incrementByPtr(). Test and show which ones modify the original.
Show Solution
#include <iostream>
using namespace std;
class Counter {
public:
int value;
Counter(int v) : value(v) {}
};
void incrementByValue(Counter c) {
c.value++; // Modifies copy only
}
void incrementByRef(Counter& c) {
c.value++; // Modifies original
}
void incrementByPtr(Counter* c) {
c->value++; // Modifies original
}
int main() {
Counter counter(10);
incrementByValue(counter);
cout << "After byValue: " << counter.value << endl; // 10
incrementByRef(counter);
cout << "After byRef: " << counter.value << endl; // 11
incrementByPtr(&counter);
cout << "After byPtr: " << counter.value << endl; // 12
return 0;
}
Task: Create a Product class. Dynamically allocate an array of 5 products on the heap, initialize them, display all, and properly deallocate.
Show Solution
#include <iostream>
#include <string>
using namespace std;
class Product {
public:
string name;
double price;
Product() : name("Unknown"), price(0) {}
Product(string n, double p) : name(n), price(p) {}
};
int main() {
int size = 5;
Product* products = new Product[size]; // Heap array
products[0] = Product("Laptop", 999.99);
products[1] = Product("Phone", 699.99);
products[2] = Product("Tablet", 499.99);
products[3] = Product("Watch", 299.99);
products[4] = Product("Earbuds", 149.99);
cout << "Product List:" << endl;
for (int i = 0; i < size; i++) {
cout << products[i].name << ": $" << products[i].price << endl;
}
delete[] products; // Use delete[] for arrays!
return 0;
}
Task: Create a Team class that holds a dynamic array of Player pointers. Implement addPlayer(), displayAll(), and proper destructor to clean up all players.
Show Solution
#include <iostream>
#include <string>
using namespace std;
class Player {
public:
string name;
int score;
Player(string n, int s) : name(n), score(s) {}
};
class Team {
private:
Player** players; // Array of Player pointers
int capacity;
int count;
public:
Team(int cap) : capacity(cap), count(0) {
players = new Player*[capacity];
}
~Team() {
for (int i = 0; i < count; i++) {
delete players[i]; // Delete each Player
}
delete[] players; // Delete the array
cout << "Team cleaned up" << endl;
}
bool addPlayer(string name, int score) {
if (count < capacity) {
players[count++] = new Player(name, score);
return true;
}
return false;
}
void displayAll() {
cout << "Team Roster:" << endl;
for (int i = 0; i < count; i++) {
cout << players[i]->name << ": " << players[i]->score << endl;
}
}
};
int main() {
Team team(5);
team.addPlayer("Alice", 100);
team.addPlayer("Bob", 85);
team.addPlayer("Charlie", 92);
team.displayAll();
return 0;
} // Destructor cleans up all memory
Key Takeaways
Classes are Blueprints
A class defines what data and behavior objects will have. Objects are instances created from classes.
Encapsulation Protects Data
Use private members with public getters/setters to control access and validate data changes.
Constructors Initialize
Constructors set up objects when created. Use initializer lists for efficient member initialization.
Destructors Clean Up
Destructors release resources when objects are destroyed. Essential for dynamic memory and RAII.
Const for Read-Only
Mark member functions as const when they do not modify the object. This enables use with const objects.
Static for Class-Wide
Static members belong to the class, not objects. Use for shared data or utility functions.
Knowledge Check
Quick Quiz
Test what you've learned about C++ Classes and Objects