C++/C++ : Study

8. 연산자 오버로딩 (3) - 단항 연산자 오버로딩 멤버 함수로의 구현

더블유제이플로어 2025. 6. 10. 06:11

Unary operator, as Member Function

◆  단항(Unary) 연산자의 멤버 함수로의 선언 ( ++ , -- , -,!)

   ●  선언 형태

    Point operator-() const;
    Point operator++(); // pre-increment
    Point operator++(int); // post-increment
    bool operator!() const;

   ●  사용 예시

    Point p1{ 10,20 };
    Point p2 = -p1; // p1.operator-();
    p2 = ++p1; // p1.operator++();
    p2 = p1++; // p1.operator++(int);

✅ 단항 연산자 오버로딩 정리

🔹 단항 연산자란?

단항(Unary) 연산자는 피연산자가 하나만 필요한 연산자를 말한다.
대표적인 예시는 다음과 같다:

연산자 의미
-a 부호 반전 (음수로 바꾸기)
++a, a++ 증가
--a, a-- 감소
!a 논리 부정 (true ↔ false)

🔹 단항 vs 이항 연산자의 차이

int a = 10;
int b = 5;
표현식  의미  연산자 종류 
-a a의 부호를 반전시켜 -10으로 만듦 단항 연산자
a - b a에서 b를 뺌 → 5 이항 연산자
-는 같은 기호지만, 피연산자 개수에 따라 동작이 달라지는 연산자다.
단항/이항 모두에서 사용 가능하지만
오버로딩 방식은 다르다.

🔹 단항 연산자의 오버로딩

클래스에서 단항 연산자를 오버로딩하려면 피연산자 하나만 있는 형태로 멤버 함수를 정의해야 한다.

예시: -p1 (부호 반전)

class Point {
private:
    int xPos;
    int yPos;

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

    // 단항 - 연산자 오버로딩
    Point operator-() const {
        return Point{-xPos, -yPos};
    }

    void Print() const {
        std::cout << "(" << xPos << ", " << yPos << ")\n";
    }
};
int main() {
    Point p1{3, -5};
    Point p2 = -p1;   // p1.operator-()
    p2.Print();       // (-3, 5)
}

🔹 단항 vs 이항 오버로딩 구조 차이

연산자 형태  함수 시그니처  인자 
-p1 Point operator-() const 없음
p1 + p2 Point operator+(const Point& rhs) const rhs 1개
단항 연산자는 인자가 없고, 이항 연산자는 인자가 1개라는 점이 큰 차이이다.

🔹 전위/후위 증감자 차이 (간단 정리)

형태  시그니처  특징
++a (전위) Point& operator++() 값을 먼저 증가시킴
a++ (후위) Point operator++(int) 값을 사용한 뒤 증가 (더미 int 매개변수로 구분)

✅요약 문장

단항 연산자는 피연산자가 하나인 연산자로, -, ++, --,! 등이 있다.
클래스에서 이를 오버로딩하려면 멤버 함수로 operator-() 같은 함수를 정의하면 된다.
단항 연산자 오버로딩은 인자가 없으며, 호출 시 obj.operator-() 형태로 해석된다.

🔹 단항 - 연산자 오버로딩

Point 클래스에서 단항 - 연산자(부호 반전)를 사용하려면,
해당 연산자에 대한 오버로딩을 직접 구현해야 한다.

Point p3 = -p1; // 실제로는 p1.operator-() 호출

위 코드는 컴파일 시 다음처럼 해석된다:

Point p3 = p1.operator-();

하지만 operator-() 함수가 정의되어 있지 않다면 컴파일러는 에러를 발생시킨다.

🔹 오버로딩 함수 정의

class Point {
private:
    int xPos;
    int yPos;

public:
    // 단항 - 연산자 오버로딩
    Point operator-() const {
        return Point{-xPos, -yPos};
    }
};
  • operator-()는 인자를 받지 않는 단항 연산자다.
  • 반환형은 Point 객체다:
    왜냐하면 -p1의 결과를 다른 Point 객체(p3)에 저장하려면
    새로운 객체를 반환해야 하기 때문이다.
  • const 멤버 함수인 이유는:
    • p1 객체의 멤버 변수는 변경되지 않아야 하며
    • const Point p1 객체에서도 호출 가능해야 하기 때문이다.

🔹 const의 의미 요약

  • const Point p1 → 읽기 전용 객체
  • const 멤버 함수만 호출 가능
    → operator-()에 const가 없으면 호출 자체가 불가능
📌 주의: const 멤버 함수는 const가 아닌 객체에서도 호출 가능하다.
반대는 불가능하다.

🔹 참고: 다른 단항 연산자의 오버로딩 시그니처

연산자  함수 시그니처 예시
++ (전위) Point& operator++();
-- (전위) Point& operator--();
! (논리 부정) bool operator!() const;
- (부호 반전) Point operator-() const;
단항 연산자는 대부분 인자가 없고, const 멤버 함수로 만드는 것이 일반적이다.

✅ 2. 최종 요약 문장

단항 - 연산자는 클래스 내부에서 operator-() 멤버 함수로 오버로딩할 수 있다.
이 함수는 인자를 받지 않으며, 보통 const 멤버 함수로 선언된다.
이는 연산 결과를 새로운 객체로 반환하면서 원본 객체는 변경하지 않기 때문이다.
또한, const 객체에서도 해당 연산자를 사용할 수 있게 하기 위해 const를 붙인다.

 ●  단항 - 연산자의 오버로딩 동작 방식의 이해

    Point operator-() const;
    
    Point p3 = -p2; // p2.operator-();
     ...

✅ 단항 연산자 오버로딩의 동작 방식

단항 연산자는 피연산자가 하나만 있는 연산자이다.
예를 들어, 아래 코드는 단항 연산자 -를 사용한 것이다:

Point p3 = -p2; // 내부적으로는 p2.operator-();

이때 컴파일러는 -p2 를 다음처럼 해석한다.

p2.operator-(); // 단항 - 연산자 호출
💡 즉, 연산자는 p2 한 개의 객체에만 적용하며, 추가적인 인자는 필요하지 않는다.

🔍 단항 연산자 vs 이항 연산자 – 구조 차이

구분  예시  내부 동작 방식  인자 
단항 연산자 -p2 p2.operator-() 없음
이항 연산자 p1 + p2 p1.operator+(p2) 있음 (1개)
단항 연산자는 멤버 함수에 인자가 없는 것이 핵심적인 차이다.
그 외의 구현 방식은 이항 연산자와 유사하다.

📌 핵심 요약 문장

단항 연산자는 피연산자가 하나뿐인 연산자로, operator-()처럼 인자 없이 오버로딩한다.
반면, 이항 연산자는 operator+(const Point& rhs)처럼 다른 객체를 인자로 받는다는 점에서 차이가 있다.
이 차이만 명확히 이해하면, 나머지 오버로딩 방식은 유사하게 작성할 수 있다.

중요한 Part이다.

Limitation of Member Function

◆  멤버 함수로 선언 시 한계점

   ●  교환 법칙이 성립하도록 구현이 불가능할 수 있다.
   ●  예를 들어 자료형이 다른 경우, 3(int) , p1(Point)를 가정해 보면,
   ●  p1 * 3 은 함수 호출 가능, 3 * p1 은 함수 호출 불가!

    Point p1{ 10,20 };
    Point p2{ 30,40 };
    
    Point p3 = p1 * 3; // p1.operator*(3)
    
    Point p4 = 3 * p1; // 3.operator*(p1) <- ???

✅ 연산자 오버로딩: 멤버 함수 방식의 한계와 해결책

🔹 연산자 오버로딩 방식: 2가지

C++에서 연산자 오버로딩은 두 가지 방식으로 구현할 수 있다:

  1. 멤버 함수로 오버로딩
  2. 전역(비멤버) 함수로 오버로딩

이 중 멤버 함수 방식은 직관적이지만, 명확한 한계점이 존재한다.
대표적인 예가 바로 교환 법칙(commutativity) 문제다.

🔹 교환 법칙이 깨지는 사례

Point p1{10, 20};

Point p3 = p1 * 3; // 가능: p1.operator*(3)
Point p4 = 3 * p1; // 에러: 3.operator*(p1) → 불가능
  • p1 * 3은 p1이 클래스 객체이므로 operator*(int) 멤버 함수가 호출되어 정상 작동한다.
  • 하지만 3 * p1은 왼쪽 피연산자가 int 타입이기 때문에
    3.operator*(p1) 형태로 해석되고, 이는 문법적으로 불가능하다.
    리터럴(3)은 멤버 함수가 없기 때문!

즉, 피연산자의 순서가 바뀌면 작동하지 않는다는 것이 멤버 함수 방식의 근본적인 한계다.

🔹 정리: 멤버 함수 방식의 한계

문제 설명
교환 법칙 불가 p1 * 3은 되지만, 3 * p1은 안 됨
리터럴은 멤버 함수 없음 3.operator*(p1) 같은 해석은 불가능
표현 제약 항상 클래스 객체가 왼쪽 피연산자여야 함

✅ 해결 방법: 전역 함수로 연산자 오버로딩

이 문제를 해결하려면 전역 함수(non-member) 방식으로 오버로딩을 구현하면 된다.
전역 함수는 왼쪽 피연산자도 우리가 제어 가능하므로, 3 * p1처럼 좌우 순서를 자유롭게 구현할 수 있다.

📌 최종 요약 문장

멤버 함수로 연산자 오버로딩을 구현하면, 클래스 객체가 왼쪽에 있을 때만 연산이 가능하다.
반면 3 * p1처럼 상수가 왼쪽인 경우는 멤버 함수 방식으로 구현할 수 없으며,
이를 해결하기 위해서는 전역 함수 기반 오버로딩이 필요하다.

Operator Overloading

◆  연산자 오버로딩 : 클래스에 대한 연산자의 적용 방식을 사용자가 직접 오버로딩하여 구현할 수 있다.

◆  멤버 한수인 연산자 오버로딩 : 클래스의 멤버함수로 operatorX()라는 이름을 갖는 함수를 구현하여 연산자를 오버로딩 할 수 있다. 이때 이항 연산자의 경우 우측 피연산자는 인자로 넘어온다.
◆  전역 함수인 연산자 오버로딩
◆  스트림 삽입 및 추출 연산자 오버로딩
◆  대입 연산자 오버로딩
◆  첨자 연상자 오버로딩

 

https://youtu.be/SJlIg2LJ7NE?si=-lMTfm3yGU4xkzH6

다음 강의 전역 함수 방식으로 오버로딩 구현이 기대된다.