icon
9장 : 객체 지향 프로그래밍 기초

연산자 오버로딩 기초

C++ 객체 지향 프로그래밍의 또 다른 강력한 기능인 연산자 오버로딩(Operator Overloading) 에 대해 알아보겠습니다.

연산자 오버로딩은 사용자 정의 타입(클래스 객체)에도 +, -, *, /와 같은 기존 연산자를 사용할 수 있도록 확장하는 기능입니다.

이를 통해 코드의 가독성을 높이고, 객체 간의 연산을 더욱 직관적으로 만들 수 있습니다.


연산자 오버로딩이란?

일반적으로 C++의 연산자(예: +, -, *, /, ==, [], <<, >> 등)는 기본 데이터 타입(int, double 등)에 대해서만 미리 정의된 동작을 수행합니다.

예를 들어 두 개의 int 값을 더하는 a + b는 잘 작동합니다.

하지만 우리가 정의한 PointVector와 같은 클래스 객체를 + 연산자로 직접 더하려고 하면 어떻게 될까요?

Point 클래스 + 연산?
Point p1(1, 2);
Point p2(3, 4);
// Point p3 = p1 + p2; // 컴파일 오류! Point 타입에 대한 + 연산자가 정의되어 있지 않음

이런 연산은 컴파일 오류를 발생시킵니다.

왜냐하면 컴파일러는 Point 객체에 대해 + 연산자를 어떻게 처리해야 할지 모르기 때문입니다.

연산자 오버로딩은 바로 이 문제를 해결합니다.

클래스에 대해 특정 연산자가 사용될 때, 해당 연산자가 어떤 작업을 수행할지 우리가 직접 정의하는 것입니다.

이를 통해 사용자 정의 타입도 마치 기본 타입처럼 연산자를 사용하여 처리할 수 있게 됩니다.

장점

  • 가독성 향상: p1.add(p2) 대신 p1 + p2처럼 직관적인 코드를 작성할 수 있습니다.
  • 직관적인 표현: 수학적 또는 논리적인 개념을 코드에 더 자연스럽게 반영할 수 있습니다.

연산자 오버로딩의 기본 형식

연산자 오버로딩은 operator 키워드를 사용하여 멤버 함수 또는 비멤버 함수(전역 함수)로 정의할 수 있습니다.

연산자 오버로딩 형식
// 멤버 함수로 오버로딩
반환타입 operator연산자(매개변수1, 매개변수2, ...) {
    // 연산자 동작 정의
}

// 비멤버 함수 (전역 함수)로 오버로딩
반환타입 operator연산자(매개변수1, 매개변수2, ...) {
    // 연산자 동작 정의
}
  • operator 키워드 뒤에 오버로딩할 연산자 기호(예: +, -, ==, [] 등)를 붙입니다.
  • 매개변수의 개수와 타입은 오버로딩할 연산자의 종류(단항 연산자, 이항 연산자)에 따라 달라집니다.

이항 연산자 오버로딩 (+ 연산자 예시)

두 개의 피연산자를 가지는 이항 연산자(예: +, -, *, /, ==, !=)를 오버로딩하는 가장 흔한 방법은 다음과 같습니다.

  • 멤버 함수로 오버로딩

    • 좌측 피연산자가 호출되는 객체(암시적 this 포인터)가 됩니다.
    • 우측 피연산자 하나를 매개변수로 받습니다.
    • 반환 타입은 연산 결과의 타입입니다.
  • 비멤버 함수 (전역 함수)로 오버로딩

    • 두 피연산자를 모두 매개변수로 받습니다.
    • 클래스의 private 멤버에 접근해야 하는 경우, friend 키워드를 사용하여 프렌드 함수(Friend Function) 로 선언해야 합니다.

예시 1: Point 클래스에 + 연산자 오버로딩

Point.h
#pragma once
#include <iostream>

class Point {
private:
    int x;
    int y;

public:
    Point(int _x = 0, int _y = 0) : x(_x), y(_y) {} // 기본값 인자를 가진 생성자

    // Getter 함수 (const 멤버 함수)
    int getX() const { return x; }
    int getY() const { return y; }

    // 1. 멤버 함수로 + 연산자 오버로딩
    // Point 객체를 더하면 새로운 Point 객체를 반환
    Point operator+(const Point& other) const; // const는 현재 객체의 상태를 변경하지 않음을 의미

    // 2. << 연산자 오버로딩을 위한 friend 선언 (뒤에서 다시 다룸)
    friend std::ostream& operator<<(std::ostream& os, const Point& p);
};
Point.cpp
#include "Point.h"

// 1. 멤버 함수로 + 연산자 오버로딩 정의
// 현재 Point 객체(this)와 other Point 객체를 더하여 새로운 Point 객체를 반환
Point Point::operator+(const Point& other) const {
    // 새로운 Point 객체를 생성하여 x, y 좌표를 더한 값으로 초기화
    return Point(this->x + other.x, this->y + other.y);
}

// 2. << 연산자 오버로딩 정의 (friend 함수이므로 클래스 외부에서 정의)
// Point::operator<< 가 아님!
std::ostream& operator<<(std::ostream& os, const Point& p) {
    os << "(" << p.x << ", " << p.y << ")"; // private 멤버 x, y에 직접 접근 가능 (friend라서)
    return os;
}
main.cpp
#include "Point.h" // Point 클래스 헤더 포함

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

    // + 연산자 오버로딩 사용
    Point p3 = p1 + p2; // p1.operator+(p2) 와 동일

    std::cout << "p1: " << p1 << std::endl; // 출력: p1: (1, 2)
    std::cout << "p2: " << p2 << std::endl; // 출력: p2: (3, 4)
    std::cout << "p1 + p2 = p3: " << p3 << std::endl; // 출력: p1 + p2 = p3: (4, 6)

    Point p4 = p3 + Point(10, 20); // 임시 객체와도 연산 가능
    std::cout << "p3 + (10,20) = p4: " << p4 << std::endl; // 출력: p3 + (10,20) = p4: (14, 26)

    return 0;
}

단항 연산자 오버로딩 (- (부정) 연산자 예시)

하나의 피연산자를 가지는 단항 연산자(예: +, -, !, ++, --)는 멤버 함수로 오버로딩할 때 매개변수가 필요 없습니다.

예시 2: Point 클래스에 단항 - (부정) 연산자 오버로딩

Point.h
// ... (기존 Point 클래스 내용) ...
class Point {
    // ... (기존 멤버 변수 및 생성자, getter) ...
public:
    // 멤버 함수로 + 연산자 오버로딩
    Point operator+(const Point& other) const;

    // 단항 - 연산자 오버로딩 (멤버 함수)
    Point operator-() const; // 매개변수 없음
    
    friend std::ostream& operator<<(std::ostream& os, const Point& p);
};
Point.cpp
#include "Point.h"

// 단항 - 연산자 오버로딩 정의
// 현재 Point 객체의 각 좌표를 음수로 바꾸어 새로운 Point 객체를 반환
Point Point::operator-() const {
    return Point(-this->x, -this->y);
}

// ... (기존 + 연산자 및 << 연산자 정의) ...
main.cpp
#include "Point.h"

int main() {
    Point p1(1, 2);
    std::cout << "p1: " << p1 << std::endl; // 출력: p1: (1, 2)

    Point p_neg = -p1; // 단항 - 연산자 오버로딩 사용 (p1.operator-() 와 동일)
    std::cout << "-p1: " << p_neg << std::endl; // 출력: -p1: (-1, -2)

    return 0;
}

입출력 연산자 오버로딩 (<<, >> 연산자)

스트림 입출력 연산자 (<< 출력, >> 입력)는 특별한 경우입니다.

이 연산자들은 항상 비멤버 함수(전역 함수) 로 오버로딩되어야 합니다.

그 이유는 좌측 피연산자가 std::ostream (for <<) 또는 std::istream (for >>) 객체이기 때문입니다.

멤버 함수로 오버로딩하면 좌측 피연산자가 항상 클래스 객체 자신(this)이 되어야 하므로 std::cout << myObject와 같은 문법을 지원할 수 없습니다.

<< 연산자 오버로딩은 일반적으로 friend 함수로 선언하여 클래스의 private 멤버에 접근할 수 있도록 합니다.

입출력 연산자 오버로딩 형식
// << (출력) 연산자 오버로딩
std::ostream& operator<<(std::ostream& os, const 클래스이름& 객체) {
    // os에 객체의 내용을 출력
    return os; // 연속적인 출력을 위해 os 참조자를 반환
}

// >> (입력) 연산자 오버로딩
std::istream& operator>>(std::istream& is, 클래스이름& 객체) {
    // is에서 데이터를 읽어 객체에 저장
    return is; // 연속적인 입력을 위해 is 참조자를 반환
}

Point 클래스의 Point.hPoint.cpp에 이미 << 연산자 오버로딩이 구현되어 있습니다.


연산자 오버로딩 시 주의사항

  • 모든 연산자를 오버로딩할 수 있는 것은 아닙니다. (. 멤버 접근, :: 스코프 결정, sizeof, ?: 삼항 연산자 등은 오버로딩 불가)
  • 연산자의 피연산자 개수(arity)를 변경할 수 없습니다. 예를 들어, 이항 연산자를 단항 연산자로 오버로딩할 수 없습니다.
  • 연산자의 우선순위나 결합 법칙을 변경할 수 없습니다.
  • 의미를 명확히 유지: 연산자를 오버로딩할 때는 원래 연산자가 가지는 일반적인 의미를 벗어나지 않도록 주의해야 합니다. 예를 들어, + 연산자가 두 수를 더하는 의미가 아니라, 두 객체를 전혀 관계없는 방식으로 "빼기"를 수행하도록 오버로딩하면 코드의 가독성과 이해도를 해칠 수 있습니다.
  • 일관성 유지: 연관된 연산자들(예: ++=, ==!=)은 일관성 있게 동작하도록 구현해야 합니다.
  • 성능 고려: 불필요한 객체 복사를 피하기 위해 인자를 const & (상수 참조)로 받고, 불필요한 오버로딩은 피하는 것이 좋습니다.