In C++, custom data types (such as classes and structures) can be used with operators (like +, -, *, <<, etc.) through operator overloading, enhancing the readability and usability of the code. Operator overloading is essentially function overloading, and thus must follow specific syntax and rules.
Basic Syntax of Operator Overloading
There are two forms of operator overloading: member functions and non-member functions (global functions).
1. Member Function Form
ReturnType operatorOperatorName(ParameterList) { // Implementation logic}
● When called: the left operand is the current object, and the right operand is the function parameter.
● Applicable to: Most binary operators (like +, –, *, /), unary operators (like ++, —, !), etc.
2. Non-Member Function Form (Usually Friend Functions)
ReturnType operatorOperatorName(Parameter1, Parameter2) { // Implementation logic}
● When called: both operands are function parameters.
● Applicable to: scenarios where the left operand is not a custom type (like cout << custom object), or for symmetric operators (like + allowing a + b and b + a, where a is a custom type and b is a built-in type).
Overloadable Operators
1. Arithmetic Operators
a. Binary: + (addition), – (subtraction), * (multiplication), / (division), % (modulus)
b. Unary: + (positive), – (negative), ++ (increment), — (decrement)
2. Assignment Operators
a. Basic assignment: = (note: generated by default, but must be manually overloaded if customized)
b. Compound assignment: +=, -=, =, /=, %=, &=, |=, ^=, <<=, >>=
3. Comparison Operators
a. == (equal)
b. != (not equal)
c. < (less than)
d. > (greater than)
e. <= (less than or equal)
f. >= (greater than or equal)
4. Logical Operators
a. ! (logical NOT)
b. && (logical AND)
c. || (logical OR)
⚠️ Note: After overloading, short-circuit evaluation is not supported (unlike built-in logical operators).
5. Bitwise Operators
a. Bitwise logic: & (bitwise AND), | (bitwise OR), ^ (bitwise XOR), ~ (bitwise NOT)
b. Shift: << (left shift), >> (right shift)
6. Member Access Operators
a. -> (member pointer access)
b. ->* (pointer to member access)
c. [] (subscript access, can only be overloaded as a member function)
7. Function Call Operator
a. () Function call, can be overloaded as a member function, allowing objects to be called like functions, i.e., “function objects”.
8. Memory Management Operators
a. new (dynamic memory allocation), delete (dynamic memory release);
b. new[] (array dynamic allocation), delete[] (array dynamic release).
9. Other Operators
a. , comma operator, changes its “sequence evaluation and returns the last value” semantics after overloading;
b. & address-of operator, can be overloaded as a member function, returning a custom pointer;
c. * dereference operator, often used with -> for smart pointers.
Overloadable operators cover most common operations, allowing custom types to be used more like built-in types (e.g., a + b, obj[i], etc.). However, it is important to note, when overloading operators, the semantics of the original operator should be followed (e.g., + should represent “addition”).
Non-Overloadable Operators
1. . (member access operator, like obj.member)
2. .* (member pointer access operator, like obj.*ptr)
3. :: (scope resolution operator, like std::cout)
4. ?: (ternary conditional operator, like a ? b : c)
5. sizeof (size of operator, like sizeof(int))
6. typeid (type information operator, used to get type information)
7. const_cast, static_cast, dynamic_cast, reinterpret_cast (type conversion operators)
Detailed Explanation of Common Operator Overloading
1. Arithmetic Operators (using + as an example)
Member Function Form (left operand is the current object)
class Point{public: Point(int x = 0, int y = 0) : _x(x), _y(y) {} // Member function: Point + Point Point operator+(const Point& other) const { // Return new object, does not modify original object return Point(_x + other._x, _y + other._y); }public: int _x; int _y;};// Call: Point c = a + b; (equivalent to a.operator+(b))
Non-Member Function Form (supports mixed types, like Point + int)
// Global function: Point + int (adding x coordinate)Point operator+(const Point& p, int val){ return Point(p._x + val, p._y);}// Global function: int + Point (symmetric overload)Point operator+(int val, const Point& p){ // Same logic as above, parameter order reversed return Point(p._x + val, p._y); }// Call: Point c = a + 5; or Point c = 5 + a;
2. Increment/Decrement Operators (++, –)
Need to distinguish between prefix (++a) and postfix (a++), distinguished by syntax (the postfix version has an additional int placeholder parameter).
Prefix ++ (Member Function)
class Counter{public: Counter(int c = 0) : _count(c) {} // Prefix++: returns modified self-reference Counter& operator++() { _count++; return *this; // Supports chaining, like ++(++a) }private: int _count;};// Counter c(1);// ++c; Prefix, calls operator++()
Postfix ++ (Member Function)
class Counter{public: Counter(int c = 0) : _count(c) {} // Postfix++: returns a copy before modification, parameter int is only for distinction, has no actual meaning Counter operator++(int) { Counter temp = *this; // Save current state _count++; // Modify self return temp; // Return copy before modification }private: int _count;};// Counter c(1);// c++; calls operator++(int)
3. Assignment Operators (=, +=, etc.)
By default, the compiler generates a default assignment operator (shallow copy), but when pointers are involved, it needs to be manually overloaded (to implement deep copy).
Assignment Operator = (Member Function, must be a member function)
class String{public: String(const char* s = "") { _data = new char[strlen(s) + 1]; strcpy(_data, s); } ~String() { delete[] _data; }public: // Overload assignment operator (deep copy) String& operator=(const String& other) { if (this != &other) { // Avoid self-assignment // Release original memory delete[] _data; _data = new char[strlen(other._data) + 1]; strcpy(_data, other._data); } // Support chained assignment, like a = b = c return *this; }private: char* _data;};
Compound Assignment Operator += (Member Function)
class String{public: String(const char* s = "") { _data = new char[strlen(s) + 1]; strcpy(_data, s); } ~String() { delete[] _data; }public: // Overload assignment operator (deep copy) String& operator+=(const String& other) { char* newData = new char[strlen(_data) + strlen(other._data) + 1]; strcpy(newData, _data); strcat(newData, other._data); delete[] _data; _data = newData; return *this; }private: char* _data;};
4. Comparison Operators (==, !=, etc.)
Usually overloaded as non-member functions to ensure symmetry (like a == 5 and 5 == a both work).
class Point{public: Point(int x = 0, int y = 0) : _x(x), _y(y) {} // Member function: Point + Point Point operator+(const Point& other) const { // Return new object, does not modify original object return Point(_x + other._x, _y + other._y); }public: int _x; int _y;};// Non-member function: check if two Points are equalbool operator==(const Point& a, const Point& b){ return a._x == b._x && a._y == b._y;}// Use == to implement !=bool operator!=(const Point& a, const Point& b){ return !(a == b);}// Call: if (a == b) { ... }// Call: if (a != b) { ... }
5. Logical Operators (&&, ||, !)
The operators &&, || lose their short-circuit evaluation property after overloading, because operator overloading is essentially implemented through function calls (the compiler will convert a && b to operator&&(a, b) or a.operator&&(b)).
class BitVector{public: BitVector(uint32_t bits = 0) : _bits(bits) {}public: uint32_t GetBits() const { return _bits; } // Logical NOT ! (Member Function) bool operator!() const { return _bits == 0; } // Logical AND && and Logical OR || must be non-member functions // Non-member functions can ensure symmetry in calls. friend bool operator&&(const BitVector& a, const BitVector& b); friend bool operator||(const BitVector& a, const BitVector& b);private: uint32_t _bits;};// Logical AND && (Non-member function)bool operator&&(const BitVector& a, const BitVector& b){ // Logical AND: both sides non-zero (logical true) returns true return (a._bits != 0) && (b._bits != 0);}// Logical OR || (Non-member function)bool operator||(const BitVector& a, const BitVector& b){ // Logical OR: at least one side non-zero (logical true) returns true return (a._bits != 0) || (b._bits != 0);}// Call// BitVector a(0b00001111);// BitVector b(0b00110011);// if(a && b) {...}// if(a || b) {...}
6. Bitwise Operators (&, |, ^, ~, <<, >>)
class BitVector{public: BitVector(uint32_t bits = 0) : _bits(bits) {}public: // Bitwise NOT ~ (Member Function) BitVector operator~() const { return BitVector(~_bits); } // Bitwise AND & (Member Function) BitVector operator&(const BitVector& other) const { return BitVector(_bits && other._bits); } // Bitwise OR | (Member Function) BitVector operator|(const BitVector& other) const { return BitVector(_bits | other._bits); } // Bitwise XOR ^ (Member Function) BitVector operator^(const BitVector& other) const { return BitVector(_bits ^ other._bits); } // Left Shift << (Member Function) BitVector operator<<(int n) const { return BitVector(_bits << n); } // Right Shift >> (Member Function) BitVector operator>>(int n) const { return BitVector(_bits >> n); }private: uint32_t _bits;};// Call// BitVector a(0b00001111);// a << 2;
7. Input and Output Operators (<<, >>)
Must be overloaded as non-member functions (because the left operand is ostream or istream, which cannot be a member of a custom class), usually declared as friends to access private members.
#include <iostream>using namespace std;class Point{public: Point(int x = 0, int y = 0) : _x(x), _y(y) {} // Member function: Point + Point Point operator+(const Point& other) const { // Return new object, does not modify original object return Point(_x + other._x, _y + other._y); }public: // Declare friends, allowing access to private members friend ostream& operator<<(ostream& os, const Point& p); friend istream& operator>>(istream& is, Point& p);public: int _x; int _y;};// Output operator: cout << Pointostream& operator<<(ostream& os, const Point& p){ os << "(" << p._x << ", " << p._y << ")"; // Supports chained output, like cout << a << b return os;}// Input operator: cin >> Pointistream& operator>>(istream& is, Point& p){ // Assume input format is "x y" is >> p._x >> p._y; // Supports chained input, like cin >> a >> b return is;}// Call:// Point p;// cin >> p; // Input: 3 4// cout << p; // Output: (3, 4)
8. Subscript Operator []
Usually overloaded as a member function for custom container classes (like array wrappers).
class Array{public: Array(int s = 0) : _size(s) { _data = new int[_size]; } // Overload[], return reference to support read/write int& operator[](int index) { if (index < 0 || index >= _size) { throw out_of_range("Index out of range"); } return _data[index]; } // Constant version for constant objects (read-only) const int& operator[](int index) const { if (index < 0 || index >= _size) { throw out_of_range("Index out of range"); } return _data[index]; }private: int* _data; int _size;};
// Call// Array arr(3);// Write operation, calls int& operator[]// arr[0] = 10;
// Read operation, calls int& operator[] // cout << arr[0] << endl; // const Array cArr(3);// Calls const int& operator[] (read-only)// cout << cArr[0] << endl;
9. Function Call Operator ()
After overloading, objects can be called like functions (“function objects” or “functors”).
class Add{public: Add(int b) : _base(b) {} // Overload(), implement base + x int operator()(int x) const { return _base + x; }private: int _base;};
// Call// Create function object// Add add5(5); // Equivalent to add5.operator()(3), outputs 8// cout << add5(3);
Conclusion
Rules for Operator Overloading
1. Cannot create new operators: can only overload existing operators (e.g., cannot overload @).
2. Priority and associativity remain unchanged: Overloading does not change the original priority and associativity of the operator (e.g., * still has higher priority than +).
3. Number of operands remains unchanged: Unary operators still require one operand, binary operators still require two.
4. The following operators are strongly bound to the current object, thus must be member functions:
a. Assignment operator =
b. Subscript operator []
c. Function call operator ()
d. Member access operator ->
5. Avoid excessive overloading: Ensure that the overloaded operator’s semantics are consistent with the original operator (e.g., + should represent “addition”, not some other logic), otherwise it will reduce code readability.
Operator overloading is an important feature of C++ object-oriented programming, and when used properly, it can make operations on custom types more natural. When overloading operators, it is necessary to choose appropriately based on the characteristics of the operator to implement it as a member function or non-member function, and to follow the principle of semantic consistency.