프로그래밍/C++ 문법

Operator Overloading in C++

studylida 2023. 2. 23. 21:00

If you're familiar with C++ programming, you know that it's a language that provides a lot of flexibility in terms of defining your own custom data types and classes. One powerful feature of C++ is its ability to overload operators, which allows you to use standard operators (such as +, -, *, /, etc.) with your own custom objects and classes. This feature can make your code more concise, more readable, and more expressive.
 
Operator overloading in C++ can seem intimidating at first, but once you understand the basics, you'll find that it's a valuable tool in your programming arsenal. In this comprehensive guide, we'll explore the different types of operators that can be overloaded, how to overload them, and how to use them in your code.
 
So, whether you're a seasoned C++ developer or just starting out, this guide will provide you with the knowledge and skills you need to take your programming to the next level.
 

Table of Contents

  1. Basics of Operator Overloading
  2. Overloading Unary Operators
  3. Overloading Binary Operators
  4. Overloading Stream Operators
  5. Overloading Subscripting Operator
  6. Overloading Function Call Operator
  7. Appendix. When global overloading should be used.

Section 1: Basics of Operator Overloading

In C++, operators are symbols that represent operations, such as addition (+), subtraction (-), multiplication (*), division (/), and so on. In traditional C++ programming, these operators are used with built-in data types, such as int, float, and double.
However, in object-oriented programming, we often want to use these same operators with our own custom data types and classes. This is where operator overloading comes in.

What is Operator Overloading?

Operator overloading is the process of defining new meanings for existing operators in C++. With operator overloading, you can use these operators with your own custom data types and classes, making your code more concise, more readable, and more expressive. However, operator function excluding new and delete should be used as non-static.
 
For example, imagine you have a class called Complex that represents complex numbers, with a real part and an imaginary part. You can define an operator overload for the addition operator (+) to allow you to add two Complex objects together, like this:

class Complex {
public:
    double real;
    double imag;
    
    Complex operator+(const Complex& other) const {
        return Complex(real + other.real, imag + other.imag);
    }
};

With this operator overload, you can now add two Complex objects together using the + operator, like this:

Complex c1(1.0, 2.0);
Complex c2(3.0, 4.0);
Complex c3 = c1 + c2;

Types of Operators that Can be Overloaded

Not all operators in C++ can be overloaded. Only a specific set of operators can be overloaded, including:

  • Arithmetic operators: +, -, *, /, %
  • Relational operators: ==, !=, <, >, <=, >=
  • Logical operators: !, &&, ||
  • Bitwise operators: ~, &, |, ^, <<, >>
  • Assignment operators: =, +=, -=, *=, /=, %=, <<=, >>=, &=, ^=, |=
  • Unary operators: +, -, *, &, ++, --, !, ~
  • Subscripting operator: []
  • Function call operator: ()
  • Member selection operator: ->

Section 2: Overloading Unary Operators

Unary operators are operators that operate on a single operand, such as the increment operator (++), decrement operator (--), and the unary minus operator (-). In this section, we'll take a closer look at how to overload unary operators in C++.

Overloading Unary Operators

To overload a unary operator in C++, you need to define a member function in your class with the following syntax:

return_type operator@(argument_list) const;

where @ is the unary operator you want to overload, return_type is the return type of the operator overload, and argument_list is the argument list for the operator overload (which is usually empty for unary operators).
 
For example, let's say we have a class called MyInt that represents integers with an additional negate() method that negates the integer value. We can overload the unary minus operator (-) to call the negate() method and return the negated integer value, like this:

class MyInt {
private:
    int value;
public:
    MyInt(int v) : value(v) {}
    
    MyInt operator-() const {
        return MyInt(-value);
    }
    
    void negate() {
        value = -value;
    }
};

With this operator overload, we can now use the unary minus operator (-) with MyInt objects, like this:

MyInt x(5);
MyInt y = -x;
y.negate(); // equivalent to x.negate()

Overloading Increment and Decrement Operators

The increment (++) and decrement (--) operators are also unary operators that can be overloaded in C++. However, there are two ways to overload these operators: as prefix operators or as postfix operators.
 
To overload the prefix increment and decrement operators (++i and --i), you need to define the following member functions in your class:

return_type operator++();
return_type operator--();​

To overload the postfix increment and decrement operators (i++ and i--), you need to define the following member functions instead:

return_type operator++(int);
return_type operator--(int);

Note that the postfix increment and decrement operators take an additional int argument, which is a dummy argument used to distinguish them from the prefix increment and decrement operators.
 
For example, let's say we have a class called Counter that counts the number of times its increment() method is called. We can overload the prefix and postfix increment operators to call the increment() method and return the updated Counter object, like this:

class Counter {
private:
    int count;
public:
    Counter(int c = 0) : count(c) {}
    
    Counter operator++() {
        ++count;
        return *this;
    }
    
    Counter operator++(int) {
        Counter temp(*this);
        ++count;
        return temp;
    }
    
    int get_count() const {
        return count;
    }
};

With these operator overloads, we can now use the prefix and postfix increment operators with Counter objects, like this:

Counter c1(0);
++c1;
Counter c2 = c1++;
int count = c2.get_count(); // count == 1

 

Section 3: Overloading Binary Operators

In addition to unary operators, binary operators can also be overloaded in C++. Binary operators take two operands, one on the left and one on the right, and return a result. Some examples of binary operators include + (addition), - (subtraction), * (multiplication), / (division), % (modulus), == (equality), != (inequality), < (less than), > (greater than), <= (less than or equal to), and >= (greater than or equal to).
 
The syntax for overloading binary operators is similar to that for unary operators. We define a function with the operator keyword followed by the symbol for the operator we want to overload, and then implement the function like any other function.

class MyClass {
public:
    // Overloading the + operator
    MyClass operator+(const MyClass& rhs) const {
        MyClass result;
        // Perform addition on the data members of this and rhs
        // and store the result in result
        return result;
    }
};

In the example above, we overload the + operator for the MyClass class. The function takes a constant reference to another MyClass object as its argument, performs addition on the data members of the two objects, and returns a new MyClass object that represents the result of the addition.
 
Note that the overloaded operator function is a member function of the class, and takes one argument of the same type as the class. However, since we want to overload a binary operator, we also need to specify another operand of the same type, which is done using the regular function parameter syntax.
 
Here's an example of overloading the * operator for a Complex class that represents complex numbers:

class Complex {
public:
    double real, imag;

    // Constructor
    Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}

    // Overloading the * operator
    Complex operator*(const Complex& other) const {
        Complex result;
        result.real = real * other.real - imag * other.imag;
        result.imag = real * other.imag + imag * other.real;
        return result;
    }
};

int main() {
    Complex a(1.0, 2.0);
    Complex b(3.0, 4.0);
    Complex c = a * b; // Overloaded operator
    return 0;
}

In the example above, we define a Complex class with a constructor that takes two double values representing the real and imaginary parts of the complex number. We then overload the * operator for this class, implementing the multiplication of two complex numbers using the standard formula. Finally, we create two Complex objects a and b, and multiply them using the overloaded operator to obtain a new Complex object c.
 
Overall, operator overloading is a powerful feature in C++ that allows you to define custom behavior for operators that is specific to your own data types. However, it should be used with care and only when it makes sense in the context of your program. When used appropriately, operator overloading can make your code more concise, intuitive, and expressive.

Section 4: Overloading Stream Operators

C++ provides two stream classes, istream for input operations and ostream for output operations. The stream classes are very powerful and can handle various types of data. C++ allows you to overload the << and >> operators for input and output operations respectively.

Overloading the << operator

The << operator is used for output operations. We can overload this operator to customize the output of our custom class. When overloading the << operator for a custom class, we need to define a friend function that takes the ostream object as the first parameter and the custom class object as the second parameter.
Here's an example of how to overload the << operator for a custom class called Person:

#include <iostream>

class Person {
public:
    Person(std::string name, int age) : name_(name), age_(age) {}
    std::string GetName() const { return name_; }
    int GetAge() const { return age_; }
    friend std::ostream& operator <<(std::ostream& os, const Person& person);
private:
    std::string name_;
    int age_;
};

std::ostream& operator<<(std::ostream& os, const Person& person) {
    os << "Name: " << person.GetName() << ", Age: " << person.GetAge();
    return os;
}

int main() {
    Person p("John", 30);
    operator<<(std::cout, p).operator<<(std::endl);
    return 0;
}

Output:

Name: John, Age: 30

In the example above, we overload the << operator for the Person class to output the name and age of the person. We define a friend function that takes the ostream object as the first parameter and the Person object as the second parameter. In the overloaded function, we use the os object to output the name and age of the person and return the ostream object.

Overloading the >> operator

The >> operator is used for input operations. We can overload this operator to customize the input of our custom class. When overloading the >> operator for a custom class, we need to define a friend function that takes the istream object as the first parameter and the custom class object as the second parameter.
 
Here's an example of how to overload the >> operator for a custom class called Fraction:

#include <iostream>

class Fraction {
public:
    Fraction(int num, int denom) : numerator_(num), denominator_(denom) {}
    int GetNumerator() const { return numerator_; }
    int GetDenominator() const { return denominator_; }
	friend std::istream & operator >> (std::istream& is, Fraction& fraction);
private:
    int numerator_;
    int denominator_;
};

std::istream& operator>>(std::istream& is, Fraction& fraction) {
    is >> fraction.numerator_ >> fraction.denominator_;
    return is;
}

int main() {
    Fraction f(0, 0);
    std::cout << "Enter a fraction (numerator denominator): ";
    std::cin >> f;
    std::cout << "You entered: " << f.GetNumerator() << "/" << f.GetDenominator() << std::endl;
    return 0;
}

Output:

Enter a fraction (numerator denominator): 3 4
You entered: 3/4

In the code above, we overload the '>>' operator for the Fraction class to allow input of fraction values from standard input. We define a friend function that takes the istream object as the first parameter and the Fraction object as the second parameter. In the overloaded function, we use the is object to input the numerator and denominator of the fraction and return the istream object.

Section 5: Overloading Subscripting Operators

In C++, arrays are accessed using the subscript operator []. It is also possible to overload the subscript operator to work with user-defined types. The subscripting operator allows the user to access the elements of an object as if they were in an array.

To overload the subscripting operator, we need to define a member function named operator[] in the class. The function takes an index as an argument and returns a reference to the element at that index. The index can be of any type, but it must be convertible to an integral type. The operator can also be overloaded as a const member function to allow access to elements of a const object.

 

Here's an example of how to overload the subscript operator for a custom class:

#include <iostream>

class MyArray {
public:
    MyArray(int size) : size_(size) {
        data_ = new int[size_];
    }
    int& operator[](int index) {
        return data_[index];
    }
    const int& operator[](int index) const {
        return data_[index];
    }
    ~MyArray() {
        delete[] data_;
    }
private:
    int* data_;
    int size_;
};

int main() {
    MyArray arr(5);
    for(int i = 0; i < 5; ++i) {
        arr[i] = i;
    }
    for(int i = 0; i < 5; ++i) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
    return 0;
}

In the example above, we define a custom class MyArray with a dynamically allocated integer array of size size_. We overload the [] operator as both a non-const and a const member function. The non-const version returns a reference to the element at the given index, allowing the user to modify it. The const version returns a const reference to the element, preventing modification of the object. In the main function, we create an instance of MyArray and use the overloaded [] operator to set and get its elements.

Section 6: Overloading Function Call Operators

In addition to the operators discussed in previous sections, C++ also allows overloading of the function call operator () for user-defined classes. The function call operator allows an object of a class to be invoked as if it were a function, with arguments passed in and a return value expected.

Syntax

The syntax for overloading the function call operator is as follows:

class MyClass {
public:
    ReturnType operator()(ArgType1 arg1, ArgType2 arg2, ...) {
        // Function body
    }
};

Here, ReturnType is the type of value returned by the function, and ArgType1, ArgType2, etc. are the types of the arguments passed in. The operator() function can take any number of arguments, including zero.

Example

Let's say we want to create a class that acts like a mathematical function, taking in a single argument and returning the value of the function at that point. We can overload the function call operator to achieve this:

class MyFunction {
public:
    MyFunction(double a, double b, double c) : a_(a), b_(b), c_(c) {}
    double operator()(double x) const {
        return a_ * x * x + b_ * x + c_;
    }
private:
    double a_, b_, c_;
};

int main() {
    MyFunction f(2.0, 3.0, 1.0);
    double result = f(4.0);
    std::cout << "f(4.0) = " << result << std::endl;
    return 0;
}

In this example, we create a MyFunction class that takes three coefficients a, b, and c, and calculates the value of the quadratic function ax^2 + bx + c. We overload the function call operator () to take a single argument x and return the value of the function at that point. In main(), we create an instance of MyFunction and call it like a regular function with argument 4.0.

Appendix. When global overloading should be used.

Let's take a look at the following code:

Integer operator +(int x, const Integer &y) {
	Integer temp;
	temp.SetValue(x + y.GetValue());
	return temp;
}

int main(void) {
	Integer a(1), b(3);
	Integer sum1 = a + 5; 
	Integer sum2 = 5 + a;
	return 0;
}

In this code, we have overloaded the + operator as a global function. We have also defined two Integer objects a and b, and used them to initialize two more Integer objects sum1 and sum2.

 

Now, the interesting part is that we can write Integer sum2 = 5 + a only because we have overloaded the + operator as a global function. If we hadn't done so, this line of code wouldn't work.

 

The reason for this is that in the expression 5 + a, the first operand is a primitive type (an int), and the second operand is an Integer object. When the compiler sees this expression, it first tries to find an overloaded + operator that takes an int and an Integer object as operands. If it can't find such an overloaded operator, it will try to find an overloaded + operator that takes two Integer objects as operands. Since we have only overloaded the + operator as a member function of the Integer class, the compiler won't find a suitable overloaded operator and will generate a compilation error.

 

To avoid this problem, we need to overload the + operator as a global function, as we did in the code above. This way, the compiler will find a suitable overloaded operator for the expression 5 + a, and the code will compile without errors.

 

In general, if the first operand of an overloaded operator has to be a primitive type, then the operator should be overloaded as a global function to avoid compilation errors.

'프로그래밍 > C++ 문법' 카테고리의 다른 글

Memory Management in C++ : Raw Pointers and Smart Pointers  (0) 2023.02.28
Type Conversions in C++  (0) 2023.02.27
Range-Based for Loops in C++  (0) 2023.02.22
Object-Oriented Programming in C++  (0) 2023.02.20
Function Pointers in C++  (0) 2023.02.07