연산자 오버로딩

김펭귄·2026년 4월 22일

Today What I Learned (TIL)

목록 보기
113/139

1. 연산자 오버로딩 (Operator Overloading)

반환타입 operator연산자 (매개변수) 
{
    // 로직
}
  • 기존의 연산자(+, -, *, << 등)가 사용자 정의 자료형(클래스나 구조체)에 대해서도 동작할 수 있게 하는 방법
class Point 
{
private:
    int x, y;
public:
    Point(int x = 0, int y = 0) : x(x), y(y) {}

    // + 연산자 오버로딩
    Point operator+(const Point& other) 
    {
        return Point(this->x + other.x, this->y + other.y);
    }
};

int main
{
	Point A(1, 2);
    Point B(4, 3);
    Point C;
    C = A + B;		// A.operator+(B)가 호출되어 오버로딩한 함수를 사용
}
  • 이렇듯, 간단하면서도 직관적인 작업을 기존 연산자에 오버로딩하여 사용하는 방법

1.1. 주의사항

  • 새로운 연산자(@**)를 만들 수는 없음

  • 피연산자 개수 변경 불가: +는 무조건 2개, !는 1개가 필요

  • 연산자 우선순위 유지: *+보다 먼저 계산되는 규칙은 변하지 않음

  • 오버로딩 불가능한 연산자

    • . (멤버 선택)

    • .* (멤버 포인터 선택)

    • :: (범위 지정)

    • ? : (삼항 연산자)

    • sizeof, typeid

  • 단순하고 직관적인 기능에 맞게만 사용하기를 추천

1.2. 증감 연산자 (++ / --)

  • 전위: Point& operator++()
    본인인 L-value를 반환해야하므로, 참조자로 반환

  • 후위: Point operator++(int)
    괄호 안에 의미 없는 int를 넣어 구분해주고, R-value이므로 새 객체를 반환

1.3. ! 연산자

  • 주로 if (!obj)와 같은 조건문에서 객체의 유효성을 검사하는 용도로 자주 사용

  • 관습적으로 bool 타입을 반환

class Player 
{
private:
    string Name;
    int Level;
public:
    // 논리 NOT (!) 연산자 오버로딩
    bool operator!() const 
    {
        return Name.empty() ? true : false;
    }
};

int main() 
{
	Player Character;
    
	if (!Character) 
    { 
    	// ...
    }
}

1.4. 대입 연산자 (=)

  • 한 객체의 값을 다른 객체에 복사할 때 사용

  • 특히 동적 할당을 사용하는 클래스에서 깊은 복사를 위해 자주 사용

Person& operator=(const Person& other) 	// 참조자를 반환
{
    if (this != &other) 	// 자기 자신 대입 방지
    { 
        this->name = other.name;
        // ... 
    }
    
    return *this;
}

1.5. 비교 연산자 (== / != / < )

  • 두 객체의 데이터가 같은지, 혹은 정렬을 위해 크기를 비교할 때 사용

  • STL 컨테이너(set, map 등)를 쓸 때 < 연산자 오버로딩이 자주 필요

bool operator==(const Point& other) const 
{
    return (x == other.x && y == other.y);
}

1.6. 인덱스 ( [ ] )

  • 객체를 배열처럼 다룰 수 있게 해줌

  • 리스트나 행렬(Matrix) 클래스를 만들 때 내부 데이터에 접근하는 용도로 사용

  • 두 가지 버젼으로 만들어줘야함

int& operator[](int index) 			// 참조자를 반환하는 버젼
{
    return data[index];
}

int operator[](int index) const		// 값만 반환하는 버젼
{
	return data[index];
}
  • 이유는, a[1] = 10처럼 사용할 수 있으므로 참조자를 반환하는 버젼이 필요하고, void foo(const Person& p)처럼 const객체로 받았을 경우엔 const함수만 사용가능하므로 값만 반환하는 버젼이 필요함

2. 입출력 연산자 (<< / >>)

  • 지금까지의 연산자 오버로딩과는 조금 결이 다름

  • 보통 p1 + p2를 하면 p1.operator+(p2)가 호출됨. 즉, 왼쪽 객체가 주인공

  • 그런데 출력을 할 때는 std::cout << p1; 처러 사용. 즉, std::cout이 주인공 (cin도 마찬가지)

  • 그러나, std::cout은 표준 라이브러리 클래스이므로 오버로딩하고자 수정할 순 없음

  • 따라서, 클래스 외부에서 따로 함수를 만들어 주는 방식을 사용

2.1. 사용 방법

  • 출력은 ostream, 입력은 istream
#include <iostream>

class Point 
{
private:
    int x, y;

public:
    Point(int x = 0, int y = 0) : x(x), y(y) {}

    // 출력 연산자 선언 (cout)
    friend std::ostream& operator<<(std::ostream& os, const Point& p);
    // 입력 연산자 선언 (cin)
    friend std::istream& operator>>(std::istream& is, Point& p);
};

// 출력 연산자 정의
std::ostream& operator<<(std::ostream& os, const Point& p) 
{
    os << "X: " << p.x << ", Y: " << p.y;
    return os;
}
// 입력 연산자 정의
std::istream& operator>>(std::istream& is, Point& p) 
{
    is >> p.x >> p.y; 
    if (is.fail()) 		// 입력이 잘못되었다면?
    { 
        p = Point(0, 0); // 예외 처리 가능
    }
    return is; 
}

int main() 
{
    Point p;    
    std::cin >> p; 
    std::cout << "p : " << p << std::endl;
}
  • 먼저 friend키워드를 통해 클래스 외부에서 선언된 함수가 Pointprivate변수에 접근 가능하도록 선언

  • 함수 반환 타입은 참조자로 하여, 결과값으로 다시 입출력 가능하게 함 (연쇄 법칙)

  • 출력은 const참조자로 매개변수를 받고, 입력은 수정해야하므로 그냥 참조자로 받음

  • cout / cin은 복사 불가능한 인스턴스라 참조자로 받아야함

2.2. 전역범위에 선언한 이유

  • 주인공은 Point객체가 아니라, cout / cin이라 하였음

  • 하지만 이 객체 안에 수정은 불가능

  • 그러면 얘네 입장에선, <<가 호출되었는데 피연산자 Point를 받는 오버로딩된 함수가 없음

  • 그래서 자동으로 범위를 밖으로 확장하여 맞는 함수를 찾음. ADL(Argument Dependent Lookup)

  • 전역범위에 오버로딩된 함수가 있게 되어 이 함수가 호출되는 것

  • 이는 다른 연산자 오버로딩도 동일하며, 객체 내에 오버로딩하는 경우와 외부에 오버로딩하는 경우가 나뉨

2.3. 객체 내부에 오버로딩하는 연산자

  • 객체의 고유한 행위로, 전역 함수로 만들 수 없게 막아놓은 연산자들 (강제)

    • = (대입 연산자)

    • [] (배열 인덱스 연산자)

    • () (함수 호출 연산자)

    • -> (멤버 접근 포인터 연산자)

  • 객체의 상태를 직접 바꾸는 연산자들도 객체 내부에 하면 좋다 (추천)

    • 복합 대입 연산자 (+=, -=, *=)

    • 증감 연산자 (++, --)

2.4. 전역 범위에 선언하는 연산자

  • 교환법칙같은 대칭성이 필요할 때 (Point + 3, 3 + Point)

  • 근데, 교환법칙하려 했더니 왼쪽 피연산자가 내가 만든 클래스가 아닐 때 (cout, int, double 등)

  • 이럴 때 전역범위에 해주면 유연성이 생기게 된다

// 전역범위
Point operator*(double d, const Point& p) 
{
    return Point(p.x * d, p.y * d);
}

p2 = 3 * p1;
  • 산술 연산자 (+, -, *, /)

  • 비교 연산자 (==, !=, <, >)

  • 입출력 연산자 (<<, >>)

2.5. 전역 범위 선언의 단점

  • 전역에 선언했기에 friend를 사용하면서 캡슐화가 약해지고, 결합도가 높아짐

  • 클래스 내부에도 friend 함수 선언이 많아져 클래스 내용보다 친구 목록이 더 길어지는 상황 발생 가능

  • 전역 범위에 함수를 너무 많이 만들면 관리가 힘들어짐

profile
반갑습니다

0개의 댓글