C++ OOP Cheatsheet 💀
C++ is not a fully object-oriented language like Java—it gives you the choice of using OOP, procedural, or even template-based metaprogramming. However, when you do use OOP, C++ expects you to take responsibility (i.e., manual memory management, virtual destructors, and explicit inheritance rules).
Basic OOP Program
#include <iostream>
#include <string>
class Car {
private:
std::string brand;
int speed;
public:
// Constructor
Car(std::string b, int s) : brand(b), speed(s) {
std::cout << brand << " Car is being created.\n";
}
// Virtual destructor
virtual ~Car() {
std::cout << brand << " Car is being destroyed.\n";
}
// Method
void accelerate() {
speed += 10;
std::cout << brand << " is going " << speed << " km/h.\n";
}
// Getter for brand
std::string getBrand() const {
return brand;
}
// Getter for speed
int getSpeed() const {
return speed;
}
// Operator overloading
friend std::ostream& operator<<(std::ostream& os, const Car& c) {
os << c.brand << " at " << c.speed << " km/h";
return os;
}
};
// Single inheritance
class SportsCar : public Car {
public:
SportsCar(std::string b, int s) : Car(b, s) {
std::cout << b << " SportsCar is being created.\n";
}
void turboBoost() {
std::cout << "Boosting the " << getBrand() << "!\n";
}
// Destructor
~SportsCar() {
std::cout << getBrand() << " SportsCar is being destroyed.\n";
}
};
int main() {
Car myCar("beetle", 50);
myCar.accelerate();
myCar.accelerate();
SportsCar mySportsCar("Ferrari", 100);
mySportsCar.accelerate();
mySportsCar.turboBoost();
// Using the overloaded << operator to print Car objects
std::cout << myCar << std::endl;
std::cout << mySportsCar << std::endl;
return 0;
}
Basic Class Definition: Car
Key Takeaways
- Encapsulation
- brand and speed are private, meaning they cannot be accessed outside of the class.
- Access is provided through getter functions (getBrand(), getSpeed()).
- Constructors & Initialization Lists
- Car(std::string b, int s) : brand(b), speed(s) {} → This is a constructor with an initializer list.
- Instead of assigning values inside {} manually, we use direct initialization, which is faster and cleaner.
- Virtual Destructor (~Car())
- Why virtual? So that if you delete a SportsCar through a Car*, it calls the correct destructor.
- Without virtual, only Car's destructor would run, and SportsCar's destructor wouldn’t execute.
- Operator Overloading (<<)
- We define friend std::ostream& operator<<(...) to allow std::cout << myCar.
- Friend functions allow non-member functions to access private class members.
Inheritance: SportsCar Extends Car
Key Takeaways
- Single Inheritance (class SportsCar : public Car)
- SportsCar inherits all properties of Car but can add new behavior (turboBoost()).
- SportsCar(std::string b, int s) : Car(b, s) calls the base class (Car) constructor.
- Destructor Handling
- Why is the destructor not virtual? Since Car already has a virtual destructor, it ensures that deleting a SportsCar object correctly calls both destructors.
- Destruction order: SportsCar's destructor runs first, then Car's.
The main() Function
Key Takeaways
- Automatic Object Lifetime Management
- myCar and mySportsCar are stack-allocated.
- No need for delete because destructors run automatically when they go out of scope.
- Calling Methods on Objects
- myCar.accelerate(); increases the speed by 10.
- mySportsCar.turboBoost(); is only available in SportsCar.
- Overloaded << Operator Works as Expected
std::cout << myCar << std::endl;
→ Calls the overloaded << function.
OOP with Parameterized Constructors & Operator Overloading in C++
This program introduces parameterized constructors and expands on operator overloading, reinforcing core C++ OOP concepts. While it follows the same fundamental principles as the Car example, the use of constructors and the + operator overload adds complexity.
#include <iostream>
using namespace std;
class Point {
private:
double x, y;
public:
// Default constructor
Point() : x(0.0), y(0.0) {}
// Parameterized constructor
Point(double xVal, double yVal) : x(xVal), y(yVal) {}
// Getter for x
double getX() const {
return x;
}
// Setter for x
void setX(double v) {
x = v;
}
// Getter for y
double getY() const {
return y;
}
// Setter for y
void setY(double v) {
y = v;
}
// Overload the + operator
Point operator+ (const Point& p) const {
return Point(x + p.x, y + p.y);
}
// Overload the << operator for output
friend ostream& operator << (ostream& out, const Point& p) {
out << "(" << p.x << ", " << p.y << ")";
return out;
}
};
int main() {
Point a(3.5, 2.5), b(2.5, 4.5), c;
cout << "a = " << a << " b = " << b << endl;
c = a + b;
cout << "sum = " << c << endl;
return 0;
}
Class Definition: Point
Key Takeaways
Point() : x(0.0), y(0.0) {}
- Default Constructor (Point())
- This constructor initializes x and y to 0.0 by default.
- Why use an initializer list (: x(0.0), y(0.0)) instead of assignment in {}?
- Faster: Directly initializes members instead of assigning values later.
- Mandatory for const or reference members (not in this example, but useful elsewhere).
- Parameterized Constructor (Point(double xVal, double yVal))
Point(double xVal, double yVal) : x(xVal), y(yVal) {}
Allows creating a Point object with custom values.
- Getter & Setter Methods
double getX() const { return x; }
void setX(double v) { x = v; }
- const after getX() → Ensures getX() does not modify the object.
- Setters allow modification, while getters ensure read-only access.
- Operator Overloading
- Overloading the + Operator
Point operator+ (const Point& p) const {
return Point(x + p.x, y + p.y);
}
- Allows adding Point objects like a + b.
- Why return a Point object?
- Instead of modifying
this
, it creates a new Point.
- Instead of modifying
Example:
c = a + b;
Equivalent to c = Point(a.getX() + b.getX(), a.getY() + b.getY());
- Overloading the << Operator (Friend Function)
friend ostream& operator << (ostream& out, const Point& p) {
out << "(" << p.x << ", " << p.y << ")";
return out;
}
Allows printing Point objects using:
cout << a; // Outputs (3.5, 2.5)
Why is operator<< a friend function?
- It needs access to private members (x and y).
- Cannot be a member function because the first parameter must be ostream&.
The main() Function
int main() {
Point a(3.5, 2.5), b(2.5, 4.5), c;
cout << "a = " << a << " b = " << b << endl;
c = a + b;
cout << "sum = " << c << endl;
return 0;
}
- What Happens Here?
- Objects are created:
Point a(3.5, 2.5), b(2.5, 4.5), c;
- a is initialized with (3.5, 2.5), b with (2.5, 4.5), and c is (0.0, 0.0) by default.
Operator overloading is used:
c = a + b;
-
Triggers operator+() and stores the result in c.
-
Objects are printed using operator<<:
cout << "sum = " << c << endl;
- Calls operator<<() to format the output.
Output:
a = (3.5, 2.5) b = (2.5, 4.5)
sum = (6, 7)
Deep Copy Constructor
C++ has a doubly linked list in the std library, but you should know how they are implemented under the hood.
Why Do We Need a Deep Copy Constructor?
By default, when you copy an object in C++, the compiler performs a shallow copy, meaning:
- It copies the memory addresses instead of duplicating the data itself.
- If the copied object modifies the data, the original object also changes because they share the same memory.
- When one object is destroyed, the other might point to an invalid memory location (dangling pointers).
To avoid this nightmare, we manually implement a deep copy constructor. This ensures:
- Each object gets its own separate copy of the data.
- Deleting one object doesn’t affect the others.
- No accidental shared memory corruption.
#include <iostream>
using namespace std;
class list_element {
public:
int d;
list_element* next;
list_element(int n = 0, list_element* ptr = nullptr) : d(n), next(ptr) {}
};
class list {
public:
list() : head(nullptr), cursor(nullptr) {}
~list(); // Destructor to free memory
list(const list& lst); // Copy constructor
void prepend(int n); // Insert at front value n
int get_element() { return cursor->d; }
void advanced() { cursor = cursor->next; }
void print();
private:
list_element* head;
list_element* cursor;
};
// Destructor implementation
list::~list() {
while (head != nullptr) {
list_element* temp = head;
head = head->next;
delete temp;
}
}
// Deep copy constructor
list::list(const list& lst) {
if (lst.head == nullptr) {
head = nullptr;
cursor = nullptr;
} else {
cursor = lst.head;
list_element* h = new list_element(cursor->d);
list_element* previous = h;
head = h;
cursor = cursor->next;
while (cursor != nullptr) {
h = new list_element(cursor->d);
previous->next = h;
previous = h;
cursor = cursor->next;
}
cursor = head;
}
}
void list::prepend(int n) {
if (head == nullptr) // empty list case
cursor = head = new list_element(n, head);
else // add to front-chain
head = new list_element(n, head);
}
void list::print() {
list_element* h = head;
while (h != nullptr) {
cout << h->d << ", ";
h = h->next;
}
cout << "###" << endl;
}
int main() {
list a, b;
a.prepend(9); a.prepend(8);
cout << "list a" << endl;
a.print();
// Use the copy constructor
list c = a;
cout << "list c (copy of a)" << endl;
c.print();
for (int i = 0; i < 40; ++i)
b.prepend(i * i);
cout << "list b" << endl;
b.print();
return 0;
}
Class Definition
#include <iostream>
using namespace std;
class list_element {
public:
int d;
list_element* next;
list_element(int n = 0, list_element* ptr = nullptr) : d(n), next(ptr) {}
};
Each list_element stores:
- d (data).
- next (pointer to the next element).
The list
class
class list {
public:
list() : head(nullptr), cursor(nullptr) {}
~list(); // Destructor to free memory
list(const list& lst); // Copy constructor
void prepend(int n); // Insert at front value n
int get_element() { return cursor->d; }
void advanced() { cursor = cursor->next; }
void print();
private:
list_element* head;
list_element* cursor;
};
- Encapsulates a linked list.
- Implements deep copy via list(const list& lst).
- Destructor (~list()) manually frees memory.
Destructor: Preventing Memory leaks
list::~list() {
while (head != nullptr) {
list_element* temp = head;
head = head->next;
delete temp; // Free each element
}
}
- Ensures no memory leaks when an object is destroyed.
- Deletes every node one by one.
- Without this, dynamically allocated list_elements would never be freed.
The Deep Copy Constructor
list::list(const list& lst) {
if (lst.head == nullptr) {
head = nullptr;
cursor = nullptr;
} else {
cursor = lst.head;
list_element* h = new list_element(cursor->d); // Create first element
list_element* previous = h;
head = h;
cursor = cursor->next;
while (cursor != nullptr) {
h = new list_element(cursor->d); // Deep copy each node
previous->next = h;
previous = h;
cursor = cursor->next;
}
cursor = head;
}
}
How It Works
- Creates a new list_element for each node in the original list.
- Does NOT reuse pointers from the old list.
- Ensures the new object is an independent copy. What Happens If We Didn’t Do This?
list c = a; // Calls copy constructor
- Without a deep copy, c would just reuse a's pointers.
- Modifying c would modify a, which is unintended behavior.
- Deleting c would leave a with dangling pointers, causing undefined behavior.
Other Methods
- Prepend (Insert at Front)
void list::prepend(int n) {
if (head == nullptr) // Empty list case
cursor = head = new list_element(n, head);
else // Add new element at the front
head = new list_element(n, head);
}
-
Creates a new list_element and links it to the front of the list.
-
If the list is empty, initializes cursor.
-
Print the List
void list::print() {
list_element* h = head;
while (h != nullptr) {
cout << h->d << ", ";
h = h->next;
}
cout << "###" << endl;
}
-
Iterates over the list and prints each value.
-
Stops when nullptr is reached.
-
The main() Function
int main() {
list a, b;
a.prepend(9); a.prepend(8);
cout << "list a" << endl;
a.print();
// Use the copy constructor
list c = a;
cout << "list c (copy of a)" << endl;
c.print();
for (int i = 0; i < 40; ++i)
b.prepend(i * i);
cout << "list b" << endl;
b.print();
return 0;
}
- Creates multiple lists (a, b, c).
- Copies a into c using the deep copy constructor.
- Adds elements to b and prints everything.
Expected Output
list a
8, 9, ###
list c (copy of a)
8, 9, ###
list b
1521, 1444, 1369, ..., 0, ###
Why is list c identical to list a?
- Because it was deep copied, not shallow copied.
- Modifying c will not affect a, proving that they are independent lists.
C++ Rule of Three (or Five)
If a class manages dynamic memory, you MUST define these manually:
- Copy Constructor (list(const list&))
- Copy Assignment Operator (operator=)
- Destructor (~list())
- (Optional) Move Constructor (list(list&&))
- (Optional) Move Assignment Operator (operator=(list&&))
Without these, you get shallow copies, which can lead to:
- Memory leaks
- Double deletion errors
- Undefined behavior