a + b is fixed for integers, but a Complex class has no natural addition until you define one. The difficult part is not spelling operator+; it is preserving the operator's familiar meaning, const-correctness, return type, and operand behaviour. In C++, operator overloading gives an existing operator meaning for a user-defined type without creating new symbols or changing built-in integer operations. The Coding & DSA Courses for Placements category places this class-design skill alongside the data structures that use it.
Operator overloading in C++: what the compiler actually calls
An overloaded operator is a function with a special name such as operator+. The compiler selects it only when at least one operand has a user-defined type. This differs from function overloading: print(int) and print(double) provide several versions of print, while Complex::operator+ defines addition for Complex objects.
Take Complex a{3, 4}, representing 3 + 4i, and Complex b{2, -1}, representing 2 - 1i. Their sum must be 5 + 3i: the real part is 3 + 2 = 5, and the imaginary part is 4 + (-1) = 3. With a member overload, you can mentally translate a + b into a.operator+(b). The source stays readable while the compiler resolves a normal function call.
C++ operator-overloading syntax and rules to know first
A binary operator can be written in either of these common forms:
form | declaration | operands |
|---|---|---|
member |
| the left operand is |
non-member |
| both operands are explicit parameters |
A friend declaration lets a non-member function access private members, but friendship is not required for every overload. The stable rules are:
At least one operand must have a user-defined type.
You cannot create a new operator such as
**.Overloading cannot change arity, precedence, or associativity.
An all-built-in expression such as
2 + 3still produces5.::,.,.*, and?:cannot be overloaded.
Thus, a + b * c groups the multiplication before the addition even when both operators are overloaded. Use an operator only when its familiar meaning remains unsurprising for the class.
Runnable binary operator+ example with Complex(3, 4) and Complex(2, -1)
Adding two Complex values combines their corresponding real and imaginary parts:
#include <iostream>
class Complex {
private:
int real_;
int imag_;
public:
Complex(int real, int imag) : real_{real}, imag_{imag} {}
Complex operator+(const Complex& other) const {
return {real_ + other.real_, imag_ + other.imag_};
}
int real() const { return real_; }
int imag() const { return imag_; }
};
int main() {
const Complex a{3, 4};
const Complex b{2, -1};
const Complex sum = a + b;
std::cout << sum.real() << ' ' << sum.imag() << '\n';
return 0;
}Compile it on macOS or Linux with g++ -std=c++17 -Wall -Wextra main.cpp -o operator_demo, then run ./operator_demo. On Windows, run the corresponding .exe. The exact output is:
5 3Inside a.operator+(b), this refers to a, so real_ is 3 and imag_ is 4. The parameter other refers to b, so other.real_ is 2 and other.imag_ is -1. The return expression builds {3 + 2, 4 + (-1)}, which is {5, 3}. Returning this new object by value is correct. Neither a nor b is changed.

Non-member and friend overloads: make std::cout << value work
In std::cout << sum, the stream must be the left operand. Because std::cout is not a Complex, define operator<< as a non-member. Add this declaration inside the class:
friend std::ostream& operator<<(std::ostream& out, const Complex& value);Then define the function outside it:
std::ostream& operator<<(std::ostream& out, const Complex& value) {
out << '(' << value.real_;
if (value.imag_ >= 0) {
out << " + " << value.imag_;
} else {
out << " - " << -value.imag_;
}
return out << "i)";
}The return type is std::ostream&, which enables chained insertions. With std::cout << a << " + " << b << " = " << sum << '\n';, the exact output is (3 + 4i) + (2 - 1i) = (5 + 3i). Friendship is used only to read private members. A non-member formatter using public getters would not need to be a friend.
Prefix and postfix operator++ are different overloads
Prefix increment updates the object and returns it. Postfix must preserve and return the earlier state:
class Counter {
int value_;
public:
explicit Counter(int value) : value_{value} {}
Counter& operator++() { ++value_; return *this; }
Counter operator++(int) {
Counter old = *this;
++value_;
return old;
}
int value() const { return value_; }
};Start with Counter c{7}. The expression (++c).value() increments first and yields 8. Next, Counter old = c++ copies the current value 8, changes c to 9, and returns the copy. Printing old.value() and c.value() gives 8 9. The unused int parameter distinguishes postfix syntax; the caller supplies no integer. Prefix safely returns Counter& because the updated object still exists. Postfix returns by value because its local old is destroyed on return.

Comparison overload with exact fractions and no floating-point conversion
Suppose a Fraction class maintains a positive denominator. Its less-than operator can compare cross-products:
bool operator<(const Fraction& other) const {
return numerator_ * other.denominator_
< other.numerator_ * denominator_;
}For Fraction x{2, 3} and Fraction y{3, 4}, evaluating x < y gives a left cross-product of 2 * 4 = 8 and a right cross-product of 3 * 3 = 9. Since 8 < 9 is true, std::cout << std::boolalpha << (x < y) prints true. This matches ordinary fraction ordering without converting this small example to double.
A production class must reject a zero denominator, normalise signs, and choose integer widths or checked arithmetic suitable for its range. Do not add every relational operator merely for completeness. Each overload should preserve clear value semantics.
Operator-overloading mistakes and how the idea is tested
Some bad overloads compile, which makes interface checks as important as syntax checks.
mistake | failure | repair |
|---|---|---|
Return | the reference dangles after return | return the new result by value |
Omit the trailing | a const left operand cannot use it | mark a non-mutating member overload |
Accept | const arguments and temporaries are rejected | accept |
Make | addition gains a surprising side effect | build and return a new value |
Expect overloaded && or || to act like built-ins | ordinary overload calls do not preserve built-in short-circuit behaviour | use named operations when evaluation order matters |
Assessments usually ask you to predict an overloaded expression's output, choose a valid member or non-member signature, distinguish prefix from postfix, identify an operator that cannot be overloaded, or repair a const-correctness or lifetime defect. Work through three exact checkpoints:
Time{1, 50} + Time{2, 25}gives3hours and75minutes. Normalising75minutes to1hour15minutes producesTime{4, 15}.Unary minus on
Point{4, -3}must producePoint{-4, 3}.Fraction{5, 8} < Fraction{2, 3}istruebecause5 * 3 = 15,2 * 8 = 16, and15 < 16.
KnowledgeGate has about 10 Function and Operator Overloading practice questions for further checking. The ideas also support readable class interfaces in Stacks and Queues: LIFO vs FIFO, Postfix, Circular Queue and meaningful comparisons in Sorting Algorithms: Complexity, Stability, n log n Bound. Those lessons apply the surrounding data-structure concepts; they do not teach operator overloading itself.
Operator overloading in C++: the short version and next step
Use an existing operator, keep its familiar meaning, preserve its arity and precedence, prefer const-correct parameters, and return new arithmetic results by value. Distinguish prefix from postfix, and never return a reference to a local object. The addition example prints 5 3; after postfix increment, old = 8 and c = 9. Continue with the focused C++ Programming Course, or choose Coding for Placements for broader practice across C, C++, Java, and Python.




