Operator Overloading in C++
When you write 2 + 3, C++ knows how to add integers. When you write "Hello" + " World" using std::string, C++ also knows how to concatenate — because the + operator was overloaded for std::string by the standard library authors. Operator overloading lets you extend that same expressiveness to your own classes.
For engineering candidates preparing for Wipro Elite, TCS NQT, or FAANG interviews, operator overloading is a reliable topic that appears in both written and practical coding rounds.
Which Operators Can Be Overloaded?
Most C++ operators can be overloaded. A handful cannot.
| Can Overload | Cannot Overload |
|---|---|
+ - * / % | :: (scope resolution) |
== != < > <= >= | . (member access) |
<< >> (stream) | .* (pointer-to-member) |
++ -- (pre and post) | ?: (ternary) |
[] () -> | sizeof |
= += -= *= /= | typeid |
new delete | static_cast and related casts |
Key rule: Operator overloading can change what an operator does for your type, but it cannot change the operator's arity (number of operands), precedence, or associativity.
Member Function vs Friend Function
This is the most common conceptual question in C++ interviews. You have two ways to overload an operator.
As a Member Function
The left-hand operand is always the calling object (*this). This is natural for operators like +=, [], and ().
class Vector2D {
public:
double x, y;
Vector2D(double x, double y) : x(x), y(y) {}
// Member function: left operand is *this
Vector2D operator+(const Vector2D& other) const {
return Vector2D(x + other.x, y + other.y);
}
};
int main() {
Vector2D a(1.0, 2.0), b(3.0, 4.0);
Vector2D c = a + b; // Calls a.operator+(b)
return 0;
}
As a Friend Function
Friend functions have access to private members but are not member functions themselves. They are preferred for operators where the left operand might be a built-in type (like int or ostream), because built-in types cannot have member functions.
class Vector2D {
public:
double x, y;
Vector2D(double x, double y) : x(x), y(y) {}
// Friend declaration
friend ostream& operator<<(ostream& os, const Vector2D& v);
};
// Definition outside the class
ostream& operator<<(ostream& os, const Vector2D& v) {
os << "(" << v.x << ", " << v.y << ")";
return os;
}
int main() {
Vector2D a(1.0, 2.0);
cout << a << endl; // Calls operator<<(cout, a)
return 0;
}
You cannot make operator<< a member of Vector2D because the left operand (cout) is of type ostream, not Vector2D.
Comparison Guide: Member vs Friend
| Scenario | Preferred Approach |
|---|---|
Left operand is always *this | Member function |
| Left operand is a built-in or standard type | Friend function |
Symmetric binary operators (+, ==) | Friend function (more symmetric) |
Assignment-like operators (=, +=) | Member function (required for =) |
Stream operators << and >> | Friend function (required) |
Overloading Arithmetic Operators
#include <iostream>
using namespace std;
class Complex {
double real, imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// Unary minus
Complex operator-() const {
return Complex(-real, -imag);
}
// Binary plus as friend
friend Complex operator+(const Complex& a, const Complex& b);
friend Complex operator-(const Complex& a, const Complex& b);
friend Complex operator*(const Complex& a, const Complex& b);
void print() const {
cout << real << " + " << imag << "i" << endl;
}
};
Complex operator+(const Complex& a, const Complex& b) {
return Complex(a.real + b.real, a.imag + b.imag);
}
Complex operator-(const Complex& a, const Complex& b) {
return Complex(a.real - b.real, a.imag - b.imag);
}
Complex operator*(const Complex& a, const Complex& b) {
return Complex(
a.real * b.real - a.imag * b.imag,
a.real * b.imag + a.imag * b.real
);
}
int main() {
Complex c1(3.0, 4.0), c2(1.0, 2.0);
(c1 + c2).print(); // 4 + 6i
(c1 - c2).print(); // 2 + 2i
(c1 * c2).print(); // -5 + 10i
return 0;
}
Overloading Comparison Operators
class Student {
string name;
double cgpa;
public:
Student(string n, double g) : name(n), cgpa(g) {}
bool operator==(const Student& other) const {
return name == other.name && cgpa == other.cgpa;
}
bool operator!=(const Student& other) const {
return !(*this == other);
}
bool operator<(const Student& other) const {
return cgpa < other.cgpa;
}
bool operator>(const Student& other) const {
return other < *this;
}
// Useful when storing in std::sort or std::set
bool operator<=(const Student& other) const {
return !(other < *this);
}
bool operator>=(const Student& other) const {
return !(*this < other);
}
};
Notice the pattern: once you define < and ==, you can express all others in terms of those two. This avoids code duplication.
Overloading Stream Operators
Stream operators are the most practically useful overloads — they let your class work naturally with cout, cin, and file streams.
#include <iostream>
#include <string>
using namespace std;
class Employee {
string name;
int id;
double salary; // in lakhs per annum
public:
Employee() : name(""), id(0), salary(0) {}
Employee(string n, int i, double s) : name(n), id(i), salary(s) {}
// Output stream operator
friend ostream& operator<<(ostream& os, const Employee& e) {
os << "ID: " << e.id
<< " | Name: " << e.name
<< " | Salary: Rs." << e.salary << " LPA";
return os;
}
// Input stream operator
friend istream& operator>>(istream& is, Employee& e) {
cout << "Enter name, ID, salary (LPA): ";
is >> e.name >> e.id >> e.salary;
return is;
}
};
int main() {
Employee e1("Rohan", 1001, 12.5);
cout << e1 << endl;
Employee e2;
cin >> e2;
cout << e2 << endl;
return 0;
}
Both operators return a reference to the stream, which is what allows chaining like cout << a << b << endl.
Overloading Pre and Post Increment
The pre-increment and post-increment operators have the same name (++) but different signatures. C++ distinguishes them with a dummy int parameter for the post-increment version.
class Counter {
int value;
public:
Counter(int v = 0) : value(v) {}
// Pre-increment: ++c
Counter& operator++() {
++value;
return *this; // Return reference to modified object
}
// Post-increment: c++
// The dummy int parameter signals this is post-increment
Counter operator++(int) {
Counter old = *this; // Save old state
++value;
return old; // Return copy of old state
}
int get() const { return value; }
};
int main() {
Counter c(5);
Counter a = ++c; // c becomes 6, a gets 6
cout << "Pre: a=" << a.get() << " c=" << c.get() << endl;
Counter b = c++; // b gets 6, c becomes 7
cout << "Post: b=" << b.get() << " c=" << c.get() << endl;
return 0;
}
Output:
Pre: a=6 c=6
Post: b=6 c=7
Pre-increment is more efficient because it avoids creating a temporary copy. Prefer ++i over i++ in loops.
Worked Example: The Fraction Class
Let us build a complete Fraction class that supports all arithmetic operators, comparisons, and stream I/O. This is a classic C++ interview problem given at companies like Adobe India and Amazon Bangalore.
#include <iostream>
#include <stdexcept>
#include <numeric> // for gcd (C++17) or __gcd
using namespace std;
class Fraction {
long long num, den; // numerator, denominator
void simplify() {
if (den < 0) { num = -num; den = -den; }
long long g = __gcd(abs(num), abs(den));
num /= g;
den /= g;
}
public:
Fraction(long long n = 0, long long d = 1) : num(n), den(d) {
if (d == 0) throw invalid_argument("Denominator cannot be zero");
simplify();
}
// --- Arithmetic ---
Fraction operator+(const Fraction& other) const {
return Fraction(num * other.den + other.num * den, den * other.den);
}
Fraction operator-(const Fraction& other) const {
return Fraction(num * other.den - other.num * den, den * other.den);
}
Fraction operator*(const Fraction& other) const {
return Fraction(num * other.num, den * other.den);
}
Fraction operator/(const Fraction& other) const {
return Fraction(num * other.den, den * other.num);
}
// Unary minus
Fraction operator-() const {
return Fraction(-num, den);
}
// Compound assignment
Fraction& operator+=(const Fraction& other) {
*this = *this + other;
return *this;
}
// --- Comparison ---
bool operator==(const Fraction& other) const {
return num == other.num && den == other.den;
}
bool operator!=(const Fraction& other) const { return !(*this == other); }
bool operator<(const Fraction& other) const {
return num * other.den < other.num * den;
}
bool operator>(const Fraction& other) const { return other < *this; }
bool operator<=(const Fraction& other) const { return !(*this > other); }
bool operator>=(const Fraction& other) const { return !(*this < other); }
// --- Stream Operators ---
friend ostream& operator<<(ostream& os, const Fraction& f) {
if (f.den == 1) os << f.num;
else os << f.num << "/" << f.den;
return os;
}
friend istream& operator>>(istream& is, Fraction& f) {
char slash;
is >> f.num >> slash >> f.den;
f.simplify();
return is;
}
// Convert to double for display
double toDouble() const { return static_cast<double>(num) / den; }
};
int main() {
Fraction a(1, 2); // 1/2
Fraction b(1, 3); // 1/3
Fraction c(2, 4); // Simplifies to 1/2
cout << a << " + " << b << " = " << (a + b) << endl;
cout << a << " - " << b << " = " << (a - b) << endl;
cout << a << " * " << b << " = " << (a * b) << endl;
cout << a << " / " << b << " = " << (a / b) << endl;
cout << "\na == c? " << (a == c ? "true" : "false") << endl;
cout << "a < b? " << (a < b ? "true" : "false") << endl;
Fraction sum(0, 1);
sum += a;
sum += b;
cout << "\nRunning sum: " << sum << " = " << sum.toDouble() << endl;
return 0;
}
Output:
1/2 + 1/3 = 5/6
1/2 - 1/3 = 1/6
1/2 * 1/3 = 1/6
1/2 / 1/3 = 3/2
a == c? true
a < b? false
Running sum: 5/6 = 0.833333
The simplify() method inside the constructor ensures all fractions are stored in their lowest terms, so 2/4 and 1/2 compare as equal.
Common Pitfalls
-
Forgetting to return
*thisfrom compound assignments. Operators like+=,-=, and=must return a reference to*thisto allow chaining:a += b += c. -
Post-increment returning a reference. Post-increment must return a copy (value), not a reference, because it returns the old value before the increment. Returning a reference to a local variable is undefined behaviour.
-
Not handling self-assignment in
operator=. Always guard againsta = awithif (this == &other) return *this;before doing any cleanup. -
Forgetting
conston the right-hand operand. Binary operators like+and==should take the right-hand operand asconstreference. Withoutconst, you cannot call them on const objects. -
Overloading
&&and||. Overloading these breaks short-circuit evaluation — both operands are always evaluated. Avoid overloading logical operators. -
Ambiguous conversions. If you provide implicit conversion constructors alongside overloaded operators, the compiler may generate multiple valid interpretations and refuse to compile. Prefer
explicitconstructors.
Practice Exercises
-
Add a
%(modulo) operator to theFractionclass that computes the fractional remainder of dividing one fraction by another. -
Build a
Matrix2x2class that overloads+,-,*(matrix multiplication),==, and<<. Test it with rotation matrices used in 2D graphics. -
Write a
BigIntegerclass that overloads+for adding integers of arbitrary length stored as strings (a common problem in competitive programming contests on platforms like Codeforces). -
Implement a
Dateclass with++(advance by one day, handling month and year rollovers),--,-(difference in days between two dates), and<<for printing in DD/MM/YYYY format. -
Extend the
Employeeclass from earlier with<and>operators that compare by salary. Use these withstd::sortto sort a vector of employees from highest to lowest salary — practical for payroll systems at firms like TCS HR software.
Summary
- Operator overloading lets you give user-defined types natural mathematical and I/O syntax.
- Most operators can be overloaded;
::,.,.*,?:,sizeof, and casts cannot. - Use member functions when the left operand is always
*this; use friend functions for symmetric binary operators and stream operators. operator<<andoperator>>must be friend (or non-member) functions because the left operand isostreamoristream.- Pre-increment returns
*thisby reference; post-increment saves a copy, increments, and returns the copy by value. - Always keep
operator!=consistent with==, and>,<=,>=consistent with<. - The
Fractionclass demonstrates complete arithmetic, comparison, and stream operator support, simplified via GCD on construction.