연산자 재정의

연산자 오버로딩은 C++에서 사용자 정의 타입에 대해 연산자의 동작을 재정의하는 기능입니다.

일반 함수로 구현

  // 컴파일러가 p1 + p2를 operator+(p1, p2)로 변환
Point operator+(const Point& p1, const Point& p2) {
    // 구현부
}

// private 멤버 접근을 위해 friend로 등록
friend Point operator+(const Point& p1, const Point& p2);
  

멤버 함수로 구현

  // 컴파일러가 p1 + p2를 p1.operator+(p2)로 변환
Point operator+(const Point& p2) {
    // 구현부
}
  

연산자 오버로딩 제약사항

  • 인자가 모두 기본 타입(primitive)인 경우는 오버로딩 불가
  • =, (), [], -> 네 가지는 반드시 멤버 함수로만 오버로딩 가능
  • 새로운 연산자 생성 불가
  • 연산자의 인자 개수 변경 불가
  • 연산자 우선순위 변경 불가
  • 디폴트 파라미터 사용 불가

연산자 오버로딩 예시

1. 스트림 출력 연산자 오버로딩

ostream과 cout

  • coutostream 타입의 객체
  • <<operator<<로 재정의된 것
  • C++98 표준화 이후 basic_ostream<> 클래스 템플릿으로 유니코드 출력 가능

사용자 정의 타입 출력

  // std::cout << pt 분석
// 컴파일러는 operator<<(cout, pt)를 탐색
std::ostream& operator<<(std::ostream& os, const Point& pt) {
    os << "(" << pt.x << ", " << pt.y << ")";
    return os;
}

// private 멤버 접근을 위해 friend로 선언
friend std::ostream& operator<<(std::ostream& os, const Point& pt);
  

endl의 원리

  • cout은 객체로 .put, .flush 등 다양한 멤버 함수 사용 가능
  • endl(cout) == cout << endl
  • endl은 함수인데 cout.operator<<(함수포인터) 버전이 오버로딩되어 있어서 가능

사용자 정의 조작자

  // cout << tab
ostream& tab(ostream& os) {
    os << '\t';
    return os;
}
  

2. 증가/감소 연산자 오버로딩

사용 예시

  • STL 반복자

전위형과 후위형 구분

  ++pt;  // p.operator++() 호출
pt++;  // p.operator++(int) 호출 - 컴파일러는 int 매개변수 있는 함수를 탐색
  

구현 예시

  // 전위형 (++pt)
Point& operator++() {
    ++x;
    ++y;
    return *this;
}

// 후위형 (pt++)
Point operator++(int) {
    Point temp(*this);
    ++(*this);  // 전위형 연산자 재활용
    return temp;
}
  

유의사항:

  • 참조로 리턴하지 않을 시 ++++pt에서 두 번째 ++가 반환된 임시객체의 값을 증가시키는 문제 발생
  • 일반적으로 전위형이 더 효율적(후위형은 복사본 생성)이지만, 최적화될 수 있음

3. 대입 연산자

  // p2 = p1 => p2.operator=(p1)
Point& operator=(const Point& p) {
    if (this != &p) {  // 자기 대입 검사
        x = p.x;
        y = p.y;
    }
    return *this;
}
  

4. 스마트 포인터 연산자

스마트 포인터는 객체지만 포인터처럼 동작하는 객체입니다.

구현 예시

  template<class T>
class Ptr {
    T* pObj;
public:
    Ptr(T* p = 0): pObj(p) {}
    
    T* operator->() { 
        return pObj; 
    }
    
    T& operator*() { 
        return *pObj; 
    }
};

int main() {
    Ptr<Car> p = new Car;
    p->Go();    // p.operator->()->Go()
    (*p).Go();  // (p.operator*()).Go()
}
  

장점

  • 포인터가 아니라 객체이므로 생성/복사/대입/파괴 모든 과정에 원하는 작업 수행 가능

shared_ptr

  • 표준 스마트 포인터 (C++11)
  • shared_ptr의 생성자는 explicit이어서:
  std::shared_ptr<Car> p1 = new Car;    // 오류
std::shared_ptr<Car> p1(new Car);     // 정상