연산자 오버로딩 기초
C++ 객체 지향 프로그래밍의 또 다른 강력한 기능인 연산자 오버로딩(Operator Overloading) 에 대해 알아보겠습니다.
연산자 오버로딩은 사용자 정의 타입(클래스 객체)에도 +
, -
, *
, /
와 같은 기존 연산자를 사용할 수 있도록 확장하는 기능입니다.
이를 통해 코드의 가독성을 높이고, 객체 간의 연산을 더욱 직관적으로 만들 수 있습니다.
연산자 오버로딩이란?
일반적으로 C++의 연산자(예: +
, -
, *
, /
, ==
, []
, <<
, >>
등)는 기본 데이터 타입(int, double 등)에 대해서만 미리 정의된 동작을 수행합니다.
예를 들어 두 개의 int
값을 더하는 a + b
는 잘 작동합니다.
하지만 우리가 정의한 Point
나 Vector
와 같은 클래스 객체를 +
연산자로 직접 더하려고 하면 어떻게 될까요?
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
클래스에 +
연산자 오버로딩
#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);
};
#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;
}
#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 클래스 내용) ...
class Point {
// ... (기존 멤버 변수 및 생성자, getter) ...
public:
// 멤버 함수로 + 연산자 오버로딩
Point operator+(const Point& other) const;
// 단항 - 연산자 오버로딩 (멤버 함수)
Point operator-() const; // 매개변수 없음
friend std::ostream& operator<<(std::ostream& os, const Point& p);
};
#include "Point.h"
// 단항 - 연산자 오버로딩 정의
// 현재 Point 객체의 각 좌표를 음수로 바꾸어 새로운 Point 객체를 반환
Point Point::operator-() const {
return Point(-this->x, -this->y);
}
// ... (기존 + 연산자 및 << 연산자 정의) ...
#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.h
와 Point.cpp
에 이미 <<
연산자 오버로딩이 구현되어 있습니다.
연산자 오버로딩 시 주의사항
- 모든 연산자를 오버로딩할 수 있는 것은 아닙니다. (
.
멤버 접근,::
스코프 결정,sizeof
,?:
삼항 연산자 등은 오버로딩 불가) - 연산자의 피연산자 개수(arity)를 변경할 수 없습니다. 예를 들어, 이항 연산자를 단항 연산자로 오버로딩할 수 없습니다.
- 연산자의 우선순위나 결합 법칙을 변경할 수 없습니다.
- 의미를 명확히 유지: 연산자를 오버로딩할 때는 원래 연산자가 가지는 일반적인 의미를 벗어나지 않도록 주의해야 합니다. 예를 들어,
+
연산자가 두 수를 더하는 의미가 아니라, 두 객체를 전혀 관계없는 방식으로 "빼기"를 수행하도록 오버로딩하면 코드의 가독성과 이해도를 해칠 수 있습니다. - 일관성 유지: 연관된 연산자들(예:
+
와+=
,==
와!=
)은 일관성 있게 동작하도록 구현해야 합니다. - 성능 고려: 불필요한 객체 복사를 피하기 위해 인자를
const &
(상수 참조)로 받고, 불필요한 오버로딩은 피하는 것이 좋습니다.