In C++, operators are categorized based on the number of operands they work with. Unary operators work with a single operand, while binary operators work with two operands. Let’s explore how to overload both types in detail.
Unary Operators
Unary operators are operators that work with a single operand. Examples include:
- Increment (
++) - Decrement (
--) - Unary minus (
-) - Logical NOT (
!) - Address-of (
&) - Dereference (
*)
Syntax for Overloading Unary Operators
As a member function:
return_type operator op(); // For prefix operators
return_type operator op(int); // For postfix increment/decrement
As a friend function:
friend return_type operator op(class_name &obj); // For prefix operators
friend return_type operator op(class_name &obj, int); // For postfix
Example 1: Overloading Unary Minus (-)
#include <iostream>
using namespace std;
class Distance {
private:
int feet;
int inches;
public:
// Constructor
Distance(int f = 0, int i = 0) : feet(f), inches(i) {}
// Display distance
void display() {
cout << feet << " feet " << inches << " inches" << endl;
}
// Overload unary minus (-) operator
Distance operator-() {
return Distance(-feet, -inches);
}
};
int main() {
Distance d1(5, 8);
cout << "Original distance: ";
d1.display();
// Use overloaded unary minus
Distance d2 = -d1;
cout << "Negated distance: ";
d2.display();
return 0;
}
Output:
Original distance: 5 feet 8 inches
Negated distance: -5 feet -8 inches
Example 2: Overloading Increment (++) and Decrement (—) Operators
The increment and decrement operators each have two forms:
- Prefix form:
++objor--obj(increment/decrement, then return) - Postfix form:
obj++orobj--(return, then increment/decrement)
To distinguish between these forms, the postfix version takes a dummy int parameter.
#include <iostream>
using namespace std;
class Counter {
private:
int count;
public:
// Constructor
Counter(int c = 0) : count(c) {}
// Display counter value
void display() {
cout << "Count: " << count << endl;
}
// Prefix increment operator (++obj)
Counter& operator++() {
++count;
return *this;
}
// Postfix increment operator (obj++)
Counter operator++(int) {
Counter temp = *this; // Save current state
++count; // Increment
return temp; // Return saved state
}
// Prefix decrement operator (--obj)
Counter& operator--() {
--count;
return *this;
}
// Postfix decrement operator (obj--)
Counter operator--(int) {
Counter temp = *this; // Save current state
--count; // Decrement
return temp; // Return saved state
}
};
int main() {
Counter c1(5);
cout << "Initial value: ";
c1.display();
// Use prefix increment
++c1;
cout << "After prefix increment: ";
c1.display();
// Use postfix increment
Counter c2 = c1++;
cout << "Result of postfix increment: ";
c2.display();
cout << "After postfix increment: ";
c1.display();
// Use prefix decrement
--c1;
cout << "After prefix decrement: ";
c1.display();
// Use postfix decrement
Counter c3 = c1--;
cout << "Result of postfix decrement: ";
c3.display();
cout << "After postfix decrement: ";
c1.display();
return 0;
}
Output:
Initial value: Count: 5
After prefix increment: Count: 6
Result of postfix increment: Count: 6
After postfix increment: Count: 7
After prefix decrement: Count: 6
Result of postfix decrement: Count: 6
After postfix decrement: Count: 5
Important Points for Unary Operators
-
Return Types:
- For prefix operators, return a reference to allow chaining (e.g.,
++(++a)) - For postfix operators, return by value (a copy) since the original object changes
- For prefix operators, return a reference to allow chaining (e.g.,
-
Parameter Differences:
- Prefix version takes no parameters
- Postfix version takes a dummy int parameter (never used, just for differentiation)
-
Implementation Differences:
- Prefix: Modify the object, then return reference to modified object
- Postfix: Save original state, modify object, return the saved state
Binary Operators
Binary operators are operators that work with two operands. Examples include:
- Arithmetic operators (
+,-,*,/,%) - Relational operators (
==,!=,<,>,<=,>=) - Logical operators (
&&,||) - Bitwise operators (
&,|,^,<<,>>) - Assignment operators (
=,+=,-=,*=,/=, etc.)
Syntax for Overloading Binary Operators
As a member function:
return_type operator op(const parameter_type& param);
As a friend function:
friend return_type operator op(const class_name& obj1, const class_name& obj2);
Example 3: Overloading Arithmetic Operators (+, -)
#include <iostream>
using namespace std;
class Vector {
private:
float x, y, z;
public:
// Constructor
Vector(float a = 0, float b = 0, float c = 0) : x(a), y(b), z(c) {}
// Display vector
void display() {
cout << "(" << x << ", " << y << ", " << z << ")" << endl;
}
// Overloading + operator (member function)
Vector operator+(const Vector& v) {
return Vector(x + v.x, y + v.y, z + v.z);
}
// Overloading - operator (member function)
Vector operator-(const Vector& v) {
return Vector(x - v.x, y - v.y, z - v.z);
}
};
int main() {
Vector v1(1.5, 2.5, 3.5);
Vector v2(0.5, 1.0, 1.5);
cout << "Vector v1: ";
v1.display();
cout << "Vector v2: ";
v2.display();
// Use overloaded + operator
Vector v3 = v1 + v2;
cout << "v1 + v2: ";
v3.display();
// Use overloaded - operator
Vector v4 = v1 - v2;
cout << "v1 - v2: ";
v4.display();
return 0;
}
Output:
Vector v1: (1.5, 2.5, 3.5)
Vector v2: (0.5, 1, 1.5)
v1 + v2: (2, 3.5, 5)
v1 - v2: (1, 1.5, 2)
Example 4: Overloading Comparison Operators (==, <)
#include <iostream>
using namespace std;
class Time {
private:
int hours;
int minutes;
int seconds;
public:
// Constructor
Time(int h = 0, int m = 0, int s = 0) : hours(h), minutes(m), seconds(s) {}
// Display time
void display() {
cout << hours << ":" << minutes << ":" << seconds << endl;
}
// Convert time to seconds (for easy comparison)
int toSeconds() const {
return hours * 3600 + minutes * 60 + seconds;
}
// Overload equality operator (==)
bool operator==(const Time& t) const {
return toSeconds() == t.toSeconds();
}
// Overload less than operator (<)
bool operator<(const Time& t) const {
return toSeconds() < t.toSeconds();
}
};
int main() {
Time t1(2, 30, 45);
Time t2(2, 30, 45);
Time t3(3, 15, 0);
cout << "Time t1: ";
t1.display();
cout << "Time t2: ";
t2.display();
cout << "Time t3: ";
t3.display();
// Use overloaded == operator
if (t1 == t2) {
cout << "t1 is equal to t2" << endl;
} else {
cout << "t1 is not equal to t2" << endl;
}
// Use overloaded < operator
if (t1 < t3) {
cout << "t1 is less than t3" << endl;
} else {
cout << "t1 is not less than t3" << endl;
}
return 0;
}
Output:
Time t1: 2:30:45
Time t2: 2:30:45
Time t3: 3:15:0
t1 is equal to t2
t1 is less than t3
Example 5: Overloading the Subscript Operator ([])
#include <iostream>
#include <stdexcept> // For std::out_of_range
using namespace std;
class IntArray {
private:
int* data;
int size;
public:
// Constructor
IntArray(int sz) {
size = sz;
data = new int[size];
for (int i = 0; i < size; i++) {
data[i] = 0;
}
}
// Destructor
~IntArray() {
delete[] data;
}
// Overload subscript operator
int& operator[](int index) {
if (index < 0 || index >= size) {
throw out_of_range("Index out of range");
}
return data[index];
}
// Display array
void display() {
cout << "Array contents: ";
for (int i = 0; i < size; i++) {
cout << data[i] << " ";
}
cout << endl;
}
};
int main() {
IntArray arr(5);
// Use overloaded [] operator to set values
try {
arr[0] = 10;
arr[1] = 20;
arr[2] = 30;
arr[3] = 40;
arr[4] = 50;
// This would cause an exception
// arr[5] = 60;
arr.display();
// Use overloaded [] operator to get values
cout << "Element at index 2: " << arr[2] << endl;
}
catch (const out_of_range& e) {
cout << "Error: " << e.what() << endl;
}
return 0;
}
Output:
Array contents: 10 20 30 40 50
Element at index 2: 30
Example 6: Using Friend Functions for Operator Overloading
Sometimes it’s necessary to use friend functions for operator overloading, especially when:
- The left operand is not an object of your class
- You need to modify both operands
- You’re overloading I/O operators (
<<,>>)
#include <iostream>
using namespace std;
class Complex {
private:
double real;
double imag;
public:
// Constructor
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// Display complex number
void display() {
cout << real;
if (imag >= 0) {
cout << " + " << imag << "i";
} else {
cout << " - " << -imag << "i";
}
cout << endl;
}
// Friend function to overload + operator
friend Complex operator+(const Complex& c1, const Complex& c2);
// Friend function to overload * operator
friend Complex operator*(const Complex& c, double scalar);
// Friend function to overload * operator (for scalar * complex)
friend Complex operator*(double scalar, const Complex& c);
// Friend functions for I/O operators
friend ostream& operator<<(ostream& os, const Complex& c);
friend istream& operator>>(istream& is, Complex& c);
};
// Implement the + operator as a friend function
Complex operator+(const Complex& c1, const Complex& c2) {
return Complex(c1.real + c2.real, c1.imag + c2.imag);
}
// Implement the * operator for complex * scalar
Complex operator*(const Complex& c, double scalar) {
return Complex(c.real * scalar, c.imag * scalar);
}
// Implement the * operator for scalar * complex
Complex operator*(double scalar, const Complex& c) {
// Reuse the other multiplication operator
return c * scalar;
}
// Implement the << operator for output
ostream& operator<<(ostream& os, const Complex& c) {
os << c.real;
if (c.imag >= 0) {
os << " + " << c.imag << "i";
} else {
os << " - " << -c.imag << "i";
}
return os;
}
// Implement the >> operator for input
istream& operator>>(istream& is, Complex& c) {
cout << "Enter real part: ";
is >> c.real;
cout << "Enter imaginary part: ";
is >> c.imag;
return is;
}
int main() {
Complex c1(3.0, 4.0);
Complex c2(1.5, -2.5);
cout << "First complex number: " << c1 << endl;
cout << "Second complex number: " << c2 << endl;
// Using overloaded + operator (friend function)
Complex c3 = c1 + c2;
cout << "Sum: " << c3 << endl;
// Using overloaded * operator (complex * scalar)
Complex c4 = c1 * 2.0;
cout << "c1 * 2.0: " << c4 << endl;
// Using overloaded * operator (scalar * complex)
Complex c5 = 3.0 * c2;
cout << "3.0 * c2: " << c5 << endl;
// Using overloaded >> operator
Complex c6;
cin >> c6;
cout << "You entered: " << c6 << endl;
return 0;
}
Member Functions vs. Friend Functions
When to Use Member Functions
- When the operator naturally acts on the left operand (e.g.,
obj += value). - When the operator needs to modify the left operand.
- When the left operand must always be an object of your class.
When to Use Friend Functions
- When the left operand might not be an object of your class (e.g.,
2 * obj). - When overloading binary operators where both operands should be treated equally.
- When overloading I/O operators (
<<,>>).
Guidelines for Operator Overloading
-
Follow conventions: Overloaded operators should behave as expected based on their built-in behavior.
-
Be consistent: If you overload
+, consider overloading+=too. If you overload==, also overload!=. -
Choose the right return type:
- For assignment and compound assignment operators (
=,+=, etc.), return a reference. - For comparison operators (
==,<, etc.), return a boolean. - For arithmetic operators (
+,-, etc.), return by value.
- For assignment and compound assignment operators (
-
Avoid side effects: Operators should behave predictably without unexpected side effects.
-
Consider const-correctness: Use const parameters when they shouldn’t be modified.
-
Use initialization lists: When creating new objects in operator functions, use initialization lists for efficiency.
Common Errors in Operator Overloading
-
Forgetting to return a value/reference: Always ensure your operators return the appropriate value or reference.
-
Incorrect return types: Using the wrong return type can lead to unexpected behavior.
-
Not checking for self-assignment: For assignment operators, always check for self-assignment to prevent issues.
-
Inconsistent behavior: Operators should behave consistently with their built-in counterparts.
-
Inefficient implementations: Avoid unnecessary object creation or copying.
Summary
Overloading unary and binary operators in C++ allows you to create intuitive and readable code for working with user-defined types. By following the proper syntax and guidelines, you can make your classes behave like built-in types, enhancing code clarity and maintainability.
Remember that while operator overloading is powerful, it should be used judiciously. Only overload operators when it makes the code more natural and readable, and always follow established conventions for operator behavior.