This is where Object-Oriented Programming (OOP) begins.
At the heart of OOP lies the concept of the class, which allows us to create user-defined data types and model real-world or logical entities directly in code.
From Data Types to Objects
An object is an instance of a class.
int,bool, andstringdescribe _what kind of data_ we store.
- A class describes:
- what data an object owns
- what operations it can perform
- how it is created
- how it is destroyed
A class can contain:
- variables (data members)
- pointers and references
- functions (member functions / methods)
Each object created from the class gets its own copy of the data but shares the same behavior definition.
Class
A class in C++ is declared using the class keyword, followed by the class name and a body enclosed in curly braces.
class MyClass {
public:
int myNumber; // Public attribute
void myFunction(); // Public method declaration
};
This declaration defines a type, not an object.
No memory is allocated until an object is created. (Just the blue print)
Access Specifiers: Controlling Visibility
Encapsulation is one of the core principles of OOP.
C++ enforces encapsulation using access specifiers:
- public
- Accessible from anywhere using the
.operator
- private (default)
- Accessible only inside the class
- protected
- Accessible inside the class and by derived classes
class Example {
private:
int secret;
protected:
int inheritedValue;
public:
int visible;
};
Why this matters
Access control prevents invalid states, enforces invariants, and enables safe APIs.
Member Functions (Methods)
Functions defined inside a class are called member functions.
They describe the behavior of the object.
class MyClass {
public:
int myNumber;
void myFunction() {
cout << "Hello World!" << endl;
}
};
Member functions automatically have access to the object’s internal state.
Creating and Using Objects
Once a class is defined, we can create objects (instances) of it.
MyClass myObj;
myObj.myNumber = 15;
myObj.myFunction();
At this moment:
- memory is allocated
- the constructor is executed
- the object becomes usable
Constructors: Object Construction
A constructor defines how an object is initialized.
Rules:
- Same name as the class
- No return type
- Called automatically when the object is created
class MyClass {
public:
int myNumber;
MyClass() {
myNumber = 10;
}
};
Writing `void` as a return type is **not allowed** in C++ constructors.
Member Initialization List
Initialization lists initialize members before the constructor body runs.
class MyClass {
public:
int myNumber;
MyClass(int num) : myNumber(num) {}
};
Why use them
- Required for
constmembers and references - More efficient
- Ensures correct initialization order
Separating Declaration and Definition (`::`)
Large systems separate interface from implementation.
class Rectangle {
int length;
int width;
public:
void setLength(int l);
int area();
};
void Rectangle::setLength(int l) {
length = l;
}
int Rectangle::area() {
return length * width;
}
The scope resolution operator :: binds the function definition to the class.
Types of Constructors
To use the different types of constructors
// Default constructor
Example exmaple;
// Parameterized constructor
Example example1(1,2);
// Copy constructor
Example example2 = example1;
// Moce constructor
Example example3 = std::move(example1);
// Constructor delegation
Example example4(1);
// Exceplicit constructor
// this will work with implicit constructor, and it will not work with explicit
Example example5 = 1;
// only this will work
Example example6(1,2);
Default Constructor
class Example {
public:
Example() {}
};
If not defined, the compiler may generate one—but members are not initialized unless explicitly done.
Parameterized Constructor
class Example {
public:
Example(int x, int y) {}
};
Used when object creation requires initial data.
Copy Constructor
Creates a new object from an existing one.
class Example {
public:
Example(const Example& obj) {}
};
Triggered when:
- Passing by value
- Returning by value
- Explicit copy
Move Constructor
Transfers ownership from a temporary object.
class Example {
public:
Example(int x) : Example(x, 0) {}
Example(int x, int y) {}
};
Why
- Avoids deep copies
- Enables high-performance code
Constructor Delegation
One constructor calls another.
class Example {
public:
Example(int x) : Example(x, 0) {}
Example(int x, int y) {}
};
Reduces duplication and enforces consistency.
Explicit Constructor
Prevents implicit conversions.
class Example {
public:
explicit Example(int x) {}
};
Example e1 = 1; // not allowed
Example e2(1); // allowed
The `this` Pointer
this points to the current object.
class Date {
int day, month, year;
public:
Date(int day, int month, int year) {
this->day = day;
this->month = month;
this->year = year;
}
};
Used to:
- resolve name conflicts
- return the object itself
- chain calls
Destructor: Object Destruction
The destructor is called when an object:
- goes out of scope
- is deleted
class Example {
public:
~Example() {
// cleanup
}
};
Every class that manages resources must define a destructor.
`default` and `delete`
`= default`
Normally the compiler will generate the constructor and the destructor automatically if it is not defined by the user, but you can control this behaviour by using default and delete keywords.
- The
defaultkeyword is used when you want the compiler to generate a default implementation of a constructor (or other special member functions like the copy constructor, move constructor, copy assignment operator, or move assignment operator).
#include <iostream>
class Example {
public:
int x;
// Default constructor using 'default'
Example() = default;
// Parameterized constructor
Example(int value) : x(value) {}
// Default copy constructor using 'default'
Example(const Example&) = default;
};
int main() {
// Calls the default constructor
Example e1;
// Calls the parameterized constructor
Example e2(10);
// Calls the default copy constructor
Example e3 = e2;
std::cout << "e1.x = " << e1.x << "\n"; // Undefined value (not initialized)
std::cout << "e2.x = " << e2.x << "\n"; // Output: e2.x = 10
std::cout << "e3.x = " << e3.x << "\n"; // Output: e3.x = 10
return 0;
}
Tells the compiler to generate the function.
`= delete`
The delete keyword is used when you want to prevent the compiler from generating a default implementation of a constructor (or other special member functions). It essentially disables the use of that function, preventing objects from being created or copied in a certain way.
#include <iostream>
class Example {
public:
int x;
// Default constructor
Example() : x(0) {}
// Parameterized constructor
Example(int value) : x(value) {}
// Delete the copy constructor
Example(const Example&) = delete;
// Delete the assignment operator
Example& operator=(const Example&) = delete;
};
int main() {
// Calls the default constructor
Example e1;
// Calls the parameterized constructor
Example e2(10);
// Error: Copy constructor is deleted
// Example e3 = e2;
// Error: Assignment operator is deleted
// e1 = e2;
std::cout << "e1.x = " << e1.x << "\n"; // Output: e1.x = 0
std::cout << "e2.x = " << e2.x << "\n"; // Output: e2.x = 10
return 0;
}
Prevents copying or assignment.
Special Member Function Generation Rules
The behavior of the compiler varies based on what special members the user has defined. We can find details in the table by Howard Hinnant below:
| Default Constructor | Destructor | Copy Constructor | Copy Assignment | Move Constructor | Move Assignment | |
|---|---|---|---|---|---|---|
| User Declares Nothing | default | default | default | default | default | default |
| Any Constructor | Not declared | default | default | default | default | default |
| Default Constructor | User declared | default | default | default | default | default |
| Destructor | default | User declared | default | default | Not declared | Not declared |
| Copy Constructor | Not declared | default | User declared | default | Not declared | Not declared |
| Copy Assignment | default | default | default | User declared | Not declared | Not declared |
| Move Constructor | Not declared | default | deleted | deleted | User declared | Not declared |
| Move Assignment | default | default | deleted | deleted | Not declared |
Rule of 0 / 3 / 5 (Core of Modern C++)
Rule of 0 (Preferred)
If your class does **not** manage resources, do not write any special member functions.
#include <vector>
#include <string>
class User {
public:
std::string name;
std::vector<int> scores;
};
If your class does NOT directly own a resource, let the compiler do everything for you.
If it does, you must clearly define ownership behavior.
Use the Rule of 0 when:
- Your class does not directly manage a resource
- You rely on RAII types (
std::vector,std::string,std::unique_ptr, etc.) - Copying and moving “just work”
Because it is:
- Compiler-generated functions are correct
- Less code → fewer bugs
- Maximum optimization opportunities
Rule of 3 (Legacy / Manual Ownership)
If you manage a resource manually, you must define:
- Destructor
- Copy constructor
- Copy assignment operator
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t s) : size(s), data(new int[s]) {}
// 1. Destructor
~Buffer() {
delete[] data;
}
// 2. Copy constructor
Buffer(const Buffer& other) : size(other.size), data(new int[other.size]) {
std::copy(other.data, other.data + size, data);
}
// 3. Copy assignment
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete[] data;
size = other.size;
data = new int[size];
std::copy(other.data, other.data + size, data);
}
return *this;
}
};
// call them
Buffer b1{"test", 5};
Buffer b2 {b1}; // copy using the copy constractor
Buffer b3 = b1; // copy using the copy assignment
Use the Rule of 3 when:
- You manage a resource manually
- Your class defines any one of:
- Destructor
- Copy constructor
- Copy assignment operator
- You need deep copy semantics
- You are in pre-C++11 code or interacting with C APIs
Why Rule of 3 is needed
- Raw pointer ownership
- Deep copy required
- Compiler defaults would cause double delete
Still error-prone, Harder to maintain
Rule of 5 (Modern C++)
Add move semantics:
- Move constructor
- Move assignment operator
class Buffer {
public:
~Buffer();
Buffer(const Buffer&); // copy
Buffer& operator=(const Buffer&); // copy assignment
Buffer(Buffer&&); // move
Buffer& operator=(Buffer&&); // move assignment
};
Use the Rule of 5 when:
- You manage a resource manually and
- You want efficient moves
- Your class is used in containers (
std::vector,std::map) - Performance matters
Why it exists
Copying is expensive.
Moving allows transfer of ownership instead of duplication.
- Avoids expensive copies
- Enables container reallocation optimizations
- Essential for high-performance code
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t s) : size(s), data(new int[s]) {}
// 1. Destructor
~Buffer() {
delete[] data;
}
// 2. Copy constructor
Buffer(const Buffer& other) : size(other.size), data(new int[other.size]) {
std::copy(other.data, other.data + size, data);
}
// 3. Copy assignment
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete[] data;
size = other.size;
data = new int[size];
std::copy(other.data, other.data + size, data);
}
return *this;
}
// 4. Move constructor
Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
}
// 5. Move assignment
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}
};
Why This Rule Exists
Because ownership must be unambiguous.
If you don’t explicitly define behavior, the compiler will—sometimes incorrectly for your intent.
Rule of 3 vs Rule of 5 vs Rule of 0 — Summary
| Rule | When to use | Owns resource? | Move support | Modern C++ |
|---|---|---|---|---|
| Rule of 0 | Default | ❌ No | ✔ Yes | ✔ Best |
| Rule of 3 | Legacy/manual | ✔ Yes | ❌ No | ⚠ Rare |
| Rule of 5 | Manual + performance | ✔ Yes | ✔ Yes | ✔ Yes |
Decision Tree (Simple)
Does your class own a resource?
├─ No → Rule of 0
└─ Yes
├─ Can you use std::unique_ptr / std::vector?
│ └─ Yes → Rule of 0
└─ No
├─ Need performance? → Rule of 5
└─ Otherwise → Rule of 3
Operator Overloading in C++
Making User-Defined Types Behave Like Built-In Types
After learning how to define classes and create objects, the next logical question is:
_How do we make our objects interact naturally with operators like `+`, `==`, `[]`, or `<<`?_
This is where operator overloading becomes essential.
Operator overloading allows user-defined types (classes and structs) to behave like built-in types, while still preserving type safety, performance, and readability.
Why Operator Overloading Exists
Built-in types already know how to behave:
int a = 3 + 5;
double x = a * 2.5;
But classes are just user-defined data structures.
Without operator overloading, this is what interaction looks like:
Vector2 a(1, 2);
Vector2 b(3, 4);
Vector2 c = add(a, b); // works, but not natural
With operator overloading:
Vector2 c = a + b; // expressive, readable, intuitive
Why this matters
- Code becomes self-explanatory
- Mathematical and logical models become accurate
- APIs feel native, not artificial
The Core Rule of Operator Overloading
**Operators can only be overloaded for user-defined types**
That means:
- ✅ Classes
- ✅ Structs
- ❌ Built-in types (
int,float,double, pointers)
This is intentionally restricted to protect the language from abuse.
❌ Not allowed
int operator+(int a, int b) {
return a - b;
}
✅ Allowed
class Number {
public:
int value;
Number operator+(const Number& other) const {
return Number{value + other.value};
}
};
The Hidden Contract: At Least One Operand Must Be a Class
This is a critical language rule:
✔ Allowed:
Number a{3};
Number b{5};
a + b;
✔ Also allowed:
a + 5; // if explicitly defined
❌ Not allowed:
3 + 5; // both operands are built-in
C++ enforces this to keep core arithmetic unchangeable.
Operator Overloading as Member Functions
The most common and beginner-friendly approach is member function overloading.
Example:
class Rational {
int n {0};
int d {1};
public:
Rational(int numerator = 0, int denominator =1): n(numerator), d(denominator) {}
Rational(const Rational & rhs) : n(rhs.n), d(rhs.d) {} // copy constractor
~Rational();
int numerator() const { return n; };
int denominator() const { return d; };
Rational& operator = (const Rational&) { // assignment
if (this != &rhs) {
n = rhs.numerator();
d = rhs.denominator();
}
return *this;
}
Rational operator + (const Rational&) const {
return Rational((n * rhs.d) + (d * rhs.n), d * rhs.d);
}
Rational operator - (const Rational&) const {
return Rational((n * rhs.d) - (d * rhs.n), d * rhs.d);
}
Rational operator * (const Rational&) const {
return Rational((n * rhs.d) * (d * rhs.n), d * rhs.d);
}
Rational operator / (const Rational&) const {
return Rational((n * rhs.d) + (d * rhs.n), d * rhs.d);
}
};
Usage:
Vector2 a(1, 2);
Vector2 b(3, 4);
Vector2 c = a + b;
Why const matters
- Ensures no mutation
- Enables use with const objects
- Signals intent clearly
Overloading Comparison Operators
Comparison operators allow objects to be compared logically.
Example: Equality
class User {
public:
int id;
bool operator==(const User& other) const {
return id == other.id;
}
};
Usage:
User a{1};
User b{1};
if (a == b) {
// same user
}
Why this matters
- Enables STL algorithms
- Supports containers like
std::set,std::map - Makes domain logic expressive
Overloading Indexing (`[]`)
Some operators must be member functions.
[] is one of them.
Example: Custom Array
class IntArray {
int data[10];
public:
int& operator[](size_t index) {
return data[index];
}
const int& operator[](size_t index) const {
return data[index];
}
};
Usage:
IntArray arr;
arr[0] = 42;
This mirrors how built-in arrays work.
Operators That MUST Be Members
C++ enforces this for safety and correctness:
| Operator |
| -------- |
| = |
| [] |
| () |
| -> |
These operators directly affect object identity or behavior, so they cannot be free functions.
Operators That Can NEVER Be Overloaded
Some operators are language-level constructs:
|Operator|
|---|
|.|
|.*|
|::|
|?:|
|sizeof|
|typeid|
They define how C++ itself works, not how objects behave.
Operator Overloading and Performance
A common misconception:
_Operator overloading is slow_
Reality:
- Operator overloading is compile-time resolved
- No runtime penalty
- Inlined aggressively by compilers
Bad performance comes from:
- Unnecessary temporaries
- Copying instead of referencing
- Poor ownership design
Good operator overloading improves clarity without cost.
When Operator Overloading Is a Bad Idea
Avoid operator overloading when:
- Meaning is unclear
- Behavior surprises readers
- Function names are clearer
❌ Bad:
fileA + fileB;
✅ Better:
mergeFiles(fileA, fileB);
Operator overloading should model real-world intuition, not creativity.
How This Builds on Classes
This topic naturally extends C++ classes:
Classes define:
- Data
- Behavior
- Ownership
Operator overloading defines:
- How objects interact
- How they behave in expressions
- How natural your API feels