프로그래밍/C++

C++ 연산자 오버로딩: 산술 및 관계 연산자 오버로딩의 이해와 활용

shimdh 2025. 2. 3. 13:29
728x90

1. 산술 연산자 오버로딩

1.1 산술 연산자란?

산술 연산자는 숫자를 다룰 때 사용하는 기본적인 연산자입니다. C++에서는 다음과 같은 산술 연산자를 제공합니다:

  • 덧셈(+)
  • 뺄셈(-)
  • 곱셈(*)
  • 나눗셈(/)
  • 모듈러(%)

이러한 연산자를 오버로딩하면, 사용자 정의 객체 간의 산술 연산을 직관적으로 구현할 수 있습니다. 연산자 오버로딩은 클래스의 멤버 함수로 정의하거나, 전역 함수로 정의할 수 있습니다. 이번 예제에서는 멤버 함수를 사용하여 연산자를 오버로딩하는 방법을 살펴보겠습니다.

1.2 예제: 복소수 클래스

복소수는 실수부와 허수부로 구성된 수학적 개념입니다. 이를 클래스로 표현하고, 덧셈과 뺄셈 연산자를 오버로딩해 보겠습니다.

#include <iostream>

class Complex {
private:
    double real; // 실수부
    double imag; // 허수부

public:
    // 생성자
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}

    // 덧셈 연산자 오버로딩
    Complex operator+(const Complex& other) {
        return Complex(real + other.real, imag + other.imag);
    }

    // 뺄셈 연산자 오버로딩
    Complex operator-(const Complex& other) {
        return Complex(real - other.real, imag - other.imag);
    }

    // 곱셈 연산자 오버로딩 (추가 예제)
    Complex operator*(const Complex& other) {
        return Complex(real * other.real - imag * other.imag,
                       real * other.imag + imag * other.real);
    }

    // 나눗셈 연산자 오버로딩 (추가 예제)
    Complex operator/(const Complex& other) {
        double denominator = other.real * other.real + other.imag * other.imag;
        return Complex((real * other.real + imag * other.imag) / denominator,
                       (imag * other.real - real * other.imag) / denominator);
    }

    // 출력 함수
    void display() const {
        std::cout << real << " + " << imag << "i" << std::endl;
    }
};

int main() {
    Complex c1(3.0, 2.5); // 복소수 3 + 2.5i
    Complex c2(1.5, 4.5); // 복소수 1.5 + 4.5i

    // 덧셈 연산
    Complex sum = c1 + c2;
    std::cout << "Sum: ";
    sum.display(); // 출력: 4.5 + 7i

    // 뺄셈 연산
    Complex difference = c1 - c2;
    std::cout << "Difference: ";
    difference.display(); // 출력: 1.5 - 2i

    // 곱셈 연산
    Complex product = c1 * c2;
    std::cout << "Product: ";
    product.display(); // 출력: -6.75 + 17.25i

    // 나눗셈 연산
    Complex quotient = c1 / c2;
    std::cout << "Quotient: ";
    quotient.display(); // 출력: 0.676471 + -0.205882i

    return 0;
}

코드 설명

  1. Complex 클래스는 실수부(real)와 허수부(imag)를 멤버 변수로 갖습니다.
  2. operator+, operator-, operator*, operator/를 오버로딩하여 두 복소수 객체 간의 덧셈, 뺄셈, 곱셈, 나눗셈을 정의했습니다.
  3. display() 함수를 통해 복소수를 출력합니다.

활용 사례

  • 수학적 계산이 필요한 프로그램(예: 공학 계산기, 물리 시뮬레이션)에서 복소수 연산을 쉽게 구현할 수 있습니다.
  • 코드의 가독성을 높이고, 유지보수를 용이하게 합니다.

1.3 추가 설명: 전역 함수로 연산자 오버로딩

연산자 오버로딩은 클래스의 멤버 함수로 정의하는 것 외에도 전역 함수로 정의할 수 있습니다. 전역 함수로 정의할 경우, 첫 번째 피연산자가 클래스 객체가 아니어도 연산자를 오버로딩할 수 있습니다.

// 전역 함수로 덧셈 연산자 오버로딩
Complex operator+(const Complex& lhs, const Complex& rhs) {
    return Complex(lhs.getReal() + rhs.getReal(), lhs.getImag() + rhs.getImag());
}

이 경우, Complex 클래스에 getReal()getImag()와 같은 접근자 함수가 필요합니다.


2. 관계 연산자 오버로딩

2.1 관계 연산자란?

관계 연산자는 두 값을 비교할 때 사용하는 연산자입니다. 주요 관계 연산자는 다음과 같습니다:

  • == (동등)
  • != (불일치)
  • < (작다)
  • > (크다)
  • <= (작거나 같다)
  • >= (크거나 같다)

이러한 연산자를 오버로딩하면, 사용자 정의 객체 간의 비교를 직관적으로 수행할 수 있습니다.

2.2 예제: Point 클래스

2D 평면상의 점을 나타내는 Point 클래스를 만들고, ==< 연산자를 오버로딩해 보겠습니다.

#include <iostream>

class Point {
public:
    int x, y;

    // 생성자
    Point(int xVal, int yVal) : x(xVal), y(yVal) {}

    // 동등성 검사
    bool operator==(const Point& other) const {
        return this->x == other.x && this->y == other.y;
    }

    // 작음 검사
    bool operator<(const Point& other) const {
        if (this->x != other.x)
            return this->x < other.x; // x좌표 비교
        return this->y < other.y; // y좌표 비교
    }

    // 크기 비교 (추가 예제)
    bool operator>(const Point& other) const {
        if (this->x != other.x)
            return this->x > other.x; // x좌표 비교
        return this->y > other.y; // y좌표 비교
    }
};

int main() {
    Point p1(1, 2); // 점 (1, 2)
    Point p2(1, 3); // 점 (1, 3)

    // 동등성 비교
    if (p1 == p2)
        std::cout << "p1은 p2와 같습니다." << std::endl;
    else
        std::cout << "p1은 p2와 다릅니다." << std::endl;

    // 크기 비교
    if (p1 < p2)
        std::cout << "p1은 p2보다 작습니다." << std::endl;

    // 크기 비교 (추가 예제)
    if (p1 > p2)
        std::cout << "p1은 p2보다 큽니다." << std::endl;

    return 0;
}

코드 설명

  1. Point 클래스는 xy 좌표를 멤버 변수로 갖습니다.
  2. operator==를 오버로딩하여 두 점이 동일한지 비교합니다.
  3. operator<operator>를 오버로딩하여 두 점의 크기를 비교합니다. 먼저 x 좌표를 비교하고, x가 같으면 y 좌표를 비교합니다.

활용 사례

  • 정렬: std::sort와 같은 알고리즘을 사용할 때, 관계 연산자를 오버로딩하면 사용자 정의 객체를 쉽게 정렬할 수 있습니다.
    std::vector<Point> points = {Point(3, 4), Point(1, 5), Point(0, -1)};
    std::sort(points.begin(), points.end()); // '<' 연산자를 사용하여 정렬
  • 조건문: 객체 간의 비교를 직관적으로 표현할 수 있습니다.
    if (pointA < pointB) {
        // 처리 로직...
    }

3. 연산자 오버로딩의 장점과 주의사항

3.1 장점

  1. 코드 가독성 향상: 사용자 정의 객체에 대해 직관적인 연산을 정의할 수 있어 코드가 더 읽기 쉬워집니다.
  2. 유지보수 용이성: 연산자 오버로딩을 통해 객체 간의 관계를 명확히 표현할 수 있어 유지보수가 쉬워집니다.
  3. 재사용성: 연산자 오버로딩을 통해 정의한 로직은 다른 프로젝트에서도 재사용할 수 있습니다.

3.2 주의사항

  1. 과도한 사용 금지: 모든 연산자를 오버로딩할 필요는 없습니다. 필요한 경우에만 사용해야 합니다.
  2. 의미에 맞는 구현: 연산자의 의미를 벗어나는 구현은 피해야 합니다. 예를 들어, 덧셈 연산자를 오버로딩할 때 뺄셈을 구현하는 것은 혼란을 초래할 수 있습니다.
  3. 성능 고려: 연산자 오버로딩은 내부적으로 함수 호출을 포함하므로, 성능에 영향을 미칠 수 있습니다. 특히 복잡한 연산을 수행할 때는 성능을 고려해야 합니다.

4. 더 복잡한 예제: 행렬 클래스

4.1 행렬 클래스 정의

행렬은 수학에서 매우 중요한 개념입니다. 행렬 클래스를 정의하고, 덧셈과 곱셈 연산자를 오버로딩해 보겠습니다.

#include <iostream>
#include <vector>

class Matrix {
private:
    std::vector<std::vector<int>> data;
    int rows, cols;

public:
    // 생성자
    Matrix(int r, int c, std::vector<std::vector<int>> d) : rows(r), cols(c), data(d) {}

    // 덧셈 연산자 오버로딩
    Matrix operator+(const Matrix& other) {
        std::vector<std::vector<int>> result(rows, std::vector<int>(cols));
        for (int i = 0; i < rows; ++i) {
            for (int j = 0; j < cols; ++j) {
                result[i][j] = data[i][j] + other.data[i][j];
            }
        }
        return Matrix(rows, cols, result);
    }

    // 곱셈 연산자 오버로딩
    Matrix operator*(const Matrix& other) {
        std::vector<std::vector<int>> result(rows, std::vector<int>(other.cols, 0));
        for (int i = 0; i < rows; ++i) {
            for (int j = 0; j < other.cols; ++j) {
                for (int k = 0; k < cols; ++k) {
                    result[i][j] += data[i][k] * other.data[k][j];
                }
            }
        }
        return Matrix(rows, other.cols, result);
    }

    // 출력 함수
    void display() const {
        for (int i = 0; i < rows; ++i) {
            for (int j = 0; j < cols; ++j) {
                std::cout << data[i][j] << " ";
            }
            std::cout << std::endl;
        }
    }
};

int main() {
    std::vector<std::vector<int>> data1 = {{1, 2}, {3, 4}};
    std::vector<std::vector<int>> data2 = {{5, 6}, {7, 8}};

    Matrix m1(2, 2, data1);
    Matrix m2(2, 2, data2);

    // 덧셈 연산
    Matrix sum = m1 + m2;
    std::cout << "Sum: " << std::endl;
    sum.display();

    // 곱셈 연산
    Matrix product = m1 * m2;
    std::cout << "Product: " << std::endl;
    product.display();

    return 0;
}

코드 설명

  1. Matrix 클래스는 2차원 벡터를 사용하여 행렬을 표현합니다.
  2. operator+operator*를 오버로딩하여 행렬의 덧셈과 곱셈을 정의했습니다.
  3. display() 함수를 통해 행렬을 출력합니다.

활용 사례

  • 수학적 계산이 필요한 프로그램(예: 선형 대수, 그래픽 처리)에서 행렬 연산을 쉽게 구현할 수 있습니다.
  • 코드의 가독성을 높이고, 유지보수를 용이하게 합니다.

5. 결론

연산자 오버로딩은 C++에서 매우 강력한 기능입니다. 이를 통해 사용자 정의 타입에 대해 직관적이고 자연스러운 연산을 정의할 수 있으며, 코드의 가독성과 유지보수성을 크게 향상시킬 수 있습니다. 산술 연산자와 관계 연산자를 오버로딩하는 방법을 익히면, 복잡한 문제를 더욱 효율적으로 해결할 수 있습니다.

728x90