All is well

[YYBASIC0307/얌얌코딩] 연산자 오버로딩(Operator Overloading) 본문

C++/YYBASIC

[YYBASIC0307/얌얌코딩] 연산자 오버로딩(Operator Overloading)

D0YUN 2025. 3. 8. 14:37

연산자 오버로딩

C++에서 연산자는 기본적으로 정수, 실수와 같은 기본 타입에 대해서만 정의되어 있습니다. 하지만 사용자가 정의한 클래스에도 연산자를 적용할 수 있도록 연산자 오버로딩(Operator Overloading)을 지원합니다.

 

연산자 오버로딩을 사용하면, 객체끼리 `+`, `-`, `==`, `<<`, `[]` 등의 연산을 수행할 수 있습니다. 예를 들어, 두 개의 Vector 객체를 `+` 연산자로 더할 수 있다면 코드가 훨씬 직관적이고 가독성이 좋아집니다.


연산자 오버로딩 문법

연산자 오버로딩은 `operator` 키워드를 사용하여 구현할 수 있습니다.

class ClassName {
public:
    반환형 operator연산자(매개변수) {
        // 연산 수행
    }
};

멤버 함수 vs. 비멤버 함수

연산자 오버로딩을 할 때, 멤버 함수로 오버로딩할지 비멤버 함수(전역 함수)로 오버로딩할지 선택해야 합니다.

멤버 함수로 연산자 오버로딩

멤버 함수로 연산자 오버로딩을 하면 왼쪽 피연산자(객체) 가 `this` 포인터를 통해 자동으로 접근됩니다. 즉, 연산자 왼쪽에 있는 객체멤버 함수의 주체가 됩니다.

// 멤버 함수의 호출 방식

lhs.operator+(rhs);

위 코드는 우리가 흔히 사용하는 `lhs + rhs` 를 내부적으로 해석한 것입니다.

// 멤버 함수 오버로딩의 특징

  • 왼쪽 피연산자(`lhs`)가 `this` 포인터를 통해 자동으로 접근
  • 첫 번째 피연산자(`lhs`)가 클래스의 멤버여야
  • 오른쪽 피연산자(`rhs`)는 함수의 매개변수로 전달됨

// ex) `+` 연산자 오버로딩 (멤버 함수)

#include <iostream>
class Vector {
public:
    int x, y;

    Vector(int x = 0, int y = 0) : x(x), y(y) {}

    // 멤버 함수로 + 연산자 오버로딩
    Vector operator+(const Vector& other) const {
        return Vector(x + other.x, y + other.y);
    }
};

int main() {
    Vector v1(3, 4);
    Vector v2(1, 2);
    Vector result = v1 + v2;  // v1.operator+(v2)로 해석됨

    std::cout << result.x << ", " << result.y << std::endl;  // 출력: 4, 6
    return 0;
}

 

비멤버 함수(전역 함수)로 연산자 오버로딩

비멤버 함수로 연산자 오버로딩을 하면 연산자 왼쪽과 오른쪽을 대등한 관계로 다룰 수 있습니다. 즉, `lhs`와 `rhs`가 동일한 방식으로 처리됩니다.

// 비멤버 함수의 호출 방식

operator+(lhs, rhs);

이 방식은 연산자의 좌항과 우항을 대등한 관계로 처리해야 할 때 유용합니다.

// 비멤버 함수 오버로딩의 특징

  • 두 피연산자가 동등한 관계로 처리됨 (`lhs`와 `rhs` 모두 매개변수로 전달됨)
  • 클래스 내부에 없는 연산자도 구현 가능 (`ostream <<` 같은 연산자)
  • `friend` 키워드를 사용하면 private 멤버에도 접근 가능

ex) `+` 연산자 오버로딩 (비멤버 함수)

#include <iostream>
class Vector {
public:
    int x, y;
    Vector(int x = 0, int y = 0) : x(x), y(y) {}

    // friend 함수로 + 연산자 오버로딩
    friend Vector operator+(const Vector& lhs, const Vector& rhs);
};

// 전역 함수로 + 연산자 오버로딩
Vector operator+(const Vector& lhs, const Vector& rhs) {
    return Vector(lhs.x + rhs.x, lhs.y + rhs.y);
}

int main() {
    Vector v1(3, 4);
    Vector v2(1, 2);
    Vector result = v1 + v2;  // operator+(v1, v2)로 해석됨

    std::cout << result.x << ", " << result.y << std::endl;  // 출력: 4, 6
    return 0;
}

 

멤버 함수 사용 vs 비멤버 함수 사용

구분 멤버 함수 오버로딩 비멤버 함수(전역 함수) 오버로딩
호출 방식 `lhs.operator+(rhs)` `operator+(lhs, rhs)`
첫 번째 피연산자 항상 클래스의 객체(`this`) 두 개의 피연산자가 동등
일반적인 사용 예시 `+=`, `-=`, `*=`, `/=` 등 `+`,`-`, `==`, `!=`, `<<`, `>>` 등
클래스 외부 연산 가능 여부 불가 (클래스 내부에서만 동작) 가능 (클래스 외부에서도 가능)
출력 연산자(<<) 오버로딩 가능? 불가 가능

 

특별한 경우: `<<` 연산자 오버로딩

출력 연산자(`<<`)는 `std::cout`과 객체를 함께 사용하기 때문에 비멤버 함수(전역 함수)로 오버로딩해야 합니다.

// 비멤버 함수로 구현해야 하는 이유

  • `std::cout`은 `ostream` 클래스의 객체이므로, 이를 `Vector`의 멤버 함수로 만들 수 없습니다.

ex) `<< `연산자 오버로딩 (비멤버 함수)

#include <iostream>
class Vector {
public:
    int x, y;
    Vector(int x = 0, int y = 0) : x(x), y(y) {}

    // friend 함수로 << 연산자 오버로딩
    friend std::ostream& operator<<(std::ostream& os, const Vector& v);
};

// 전역 함수로 << 연산자 오버로딩
std::ostream& operator<<(std::ostream& os, const Vector& v) {
    os << "(" << v.x << ", " << v.y << ")";
    return os;
}

int main() {
    Vector v(3, 4);
    std::cout << v << std::endl;  // (3, 4)
    return 0;
}

 


연산자 오버로딩에서 주의할 점

// 반환형을 고려할 것

  • `operator+`와 같은 연산은 새로운 객체를 반환하는 것이 일반적입니다.
  • `operator+=`와 같은 연산은 자기 자신을 변경하고 `this`를 반환하는 것이 좋습니다.

// 객체의 원본을 변경하지 않도록 const 사용

  • `operator+`는 매개변수에 `const`를 붙여 원본이 변경되지 않도록 설계합니다.

// 연산자의 의미를 유지할 것

  • 연산자가 직관적인 의미를 가지도록 구현합니다.
    • 예를 들어 `==`는 값 비교, `<<`는 출력 용도로 활용합니다.

// 비효율적인 연산 방지

  • 불필요한 복사 연산을 줄이기 위해 참조(&)와 이동 연산자(std::move)를 고려합니다.

연산자 오버로딩을 통한 커스텀 Vector2 자료형의 사칙 연산 구현

아래 예제에서는 `Vector2`라는 구조체를 정의하고, 기본적인 사칙 연산(`+`, `-`, `*`, `/`) 및 비교 연산(`<`)을 오버로딩하여 사용합니다.

 

이때 매개변수를 값으로 전달하면 불필요한 복사 연산이 발생하여 성능이 저하될 수 있습니다.

이를 방지하기 위해 `const &`(`const` 참조) 키워드를 사용하여 최적화된 코드를 구현했습니다.

// YYBASIC03_07
#include <iostream>  // 입출력을 위한 헤더 파일
using namespace std;

// 2차원 벡터(Vector2) 구조체 정의
struct Vector2
{
    int x;  // x 좌표
    int y;  // y 좌표

    // + 연산자 오버로딩: 두 Vector2 객체를 더하는 기능
    Vector2 operator+(const Vector2& other)
    {
        Vector2 rslt;
        rslt.x = x + other.x;
        rslt.y = y + other.y;

        return rslt;
    }

    // - 연산자 오버로딩: 두 Vector2 객체를 빼는 기능
    Vector2 operator-(const Vector2& other)
    {
        Vector2 rslt;
        rslt.x = x - other.x;
        rslt.y = y - other.y;

        return rslt;
    }

    // * 연산자 오버로딩: 두 Vector2 객체를 곱하는 기능
    Vector2 operator*(const Vector2& other)
    {
        Vector2 rslt;
        rslt.x = x * other.x;
        rslt.y = y * other.y;

        return rslt;
    }

    // / 연산자 오버로딩: 두 Vector2 객체를 나누는 기능 (단, 0으로 나누는 예외 처리가 필요함)
    Vector2 operator/(const Vector2& other)
    {
        Vector2 rslt;
        rslt.x = x / other.x;
        rslt.y = y / other.y;

        return rslt;
    }

    // < 연산자 오버로딩: 두 Vector2 객체를 비교하는 기능
    bool operator<(const Vector2& other)
    {
        return (x < other.x && y < other.y); // 두 좌표가 모두 작을 때 true 반환
    }
};

int main(void)
{
    Vector2 p1;  // 첫 번째 벡터 객체 선언
    p1.x = 1;
    p1.y = 1;

    Vector2 p2;  // 두 번째 벡터 객체 선언
    p2.x = 3;
    p2.y = 2;

    Vector2 p3;  // 결과를 저장할 세 번째 벡터 객체 선언

    // 연산자 오버로딩을 사용하지 않고 p1 + p2 연산 수행
    p3.x = p1.x + p2.x;
    p3.y = p1.y + p2.y;
    cout << "p3 : (" << p3.x << " , " << p3.y << ")\\n";

    cout << "----- Initialize p3 -----\\n";

    // p3 좌표를 초기화
    p3.x = 0;
    p3.y = 0;
    cout << "p3 : (" << p3.x << " , " << p3.y << ")\\n";

    cout << "----- Use Operator Overloading -----\\n";

    // + 연산자 오버로딩 사용: p1 + p2 계산 (p1.operator+(p2)와 동일)
    p3 = p1 + p2;
    cout << "p3 : (" << p3.x << " , " << p3.y << ")\\n";

    cout << "----- Compare p1 & p2 with Operator Overloading -----\\n";

    // < 연산자 오버로딩 사용: p1이 p2보다 작은지 비교
    if (p1 < p2)
        cout << "p1 is smaller than p2\\n";

    // 문자열도 연산자 오버로딩이 구현되어 있어서 += 연산이 가능함
    string test = "test";
    test += " success";  // 문자열 덧붙이기 연산자 += 가 적용됨

    return 0;
}

// 실행 결과

 

 

const &를 사용한 최적화의 장점

  • 불필요한 복사 연산 방지 : `const Vector2&`를 사용하면 원본 객체를 직접 참조하므로 복사 비용을 줄일 수 있습니다.
  • 객체의 불변성 유지 : `const`를 추가하여 연산 중 객체가 변경되지 않도록 보호합니다.
  • 성능 향상 : 값 복사 없이 연산을 수행하여 빠른 실행 속도를 유지합니다.

 

C++ 표준 라이브러리에서의 연산자 오버로딩 활용

C++ 표준 라이브러리에서도 연산자 오버로딩이 적극적으로 활용됩니다. 대표적인 예로 `std::string`이 있으며, 문자열을 쉽게 이어 붙일 수 있도록 `+=` 연산자가 오버로딩되어 있습니다.

#include <iostream>
#include <string>

int main() {
    std::string test = "test";
    test += " success";  // += 연산자 오버로딩이 적용됨
    std::cout << test << std::endl;  // 출력: test success
    return 0;
}

 

 

위 코드에서 `test += " success";` 구문이 동작하는 이유는 `std::string` 클래스에 `operator+=`가 오버로딩되어 있기 때문입니다.

디버깅을 통해 확인해 보면 `operator+=`가 `const char*`을 매개변수로 받아 문자열을 결합하는 역할을 수행한다는 것을 알 수 있습니다.

 

Lv11 연산자 오버로딩(Operator Overloading)