Operator Overlading
◆ 연산자 오버로딩 : 클래스에 대한 연산자의 적용 방식을 사용자가 직접 오버로딩하여 구현할 수 있다.
◆ 멤버 한수인 연산자 오버로딩
◆ 전역 함수인 연산자 오버로딩
◆ 스트림 삽입 및 추출 연산자 오버로딩
◆ 대입 연산자 오버로딩
◆ 첨자 연상자 오버로딩
연산자 오버로딩에는 두 가지 방법이 있다.
멤버 함수로 정의하는 방법과
전역 함수(비멤버 함수)로 정의하는 방법이다.
이 두 가지 방식을 잘 구분하여 기억해 두자.
Operator Overlading as Member Function
◆ 이항(binary) 연산자의 멤버 함수로의 선언 (+, - , ==,!= , > , < , etc)
● 선언 형태
Point operator+(const Point& rhs) const;
Point operator-(const Point& rhs) const;
bool operator==(const Point& rhs) const;
Point operator<(const Point & rhs) const;
● 사용 예시
Point p1{ 10,20 };
Point p2{ 30,40 };
Point p3 = p1 + p2; // p1.operator+(p2);
p3 = p1 - p2; // p1.operator+(p2);
if (p1 == p2) // p1.operator==(p2);
이항 연산자를 기억하는가?
이번에는 + 연산자를 예로 들어보자.
1 + 2
이 연산에서 +는 연산자(operator)이고, 1과 2는 연산의 대상이 되는 피연산자(operand)이다.
+ 연산자는 두 개의 피연산자가 필요하므로, 이를 이항 연산자(binary operator)라고 부른다.
또 다른 예로!= 연산자를 보자:
if (a != b)
이 표현에서도 a와 b는 각각 연산자의 좌우에 위치한 피연산자이며,!=은 두 값이 서로 다른지 비교하는 이항 연산자이다.
즉, 이항 연산자란 좌우에 두 개의 피연산자가 필요한 연산자를 의미한다.
이제 이항 연산자를 클래스의 멤버 함수로 오버로딩하는 방법에 대해 알아보자.
● 선언 형태
앞서 자료와 같은 선언 형태를 그대로 외울 필요는 없다.
왜냐하면 연산자마다 피연산자의 타입이나 반환형이 다르기 때문이다.
다만 알아두어야 할 중요한 점은,
이 함수들의 이름이 모두 operator+, operator-, operator==와 같은 형태라는 것이다.
즉, 연산자를 오버로딩하려면 operator연산자기호 형식의 이름을 가진 함수를 정의해야 한다는 점만 꼭 기억하자.
● 사용 예시
이러한 연산이 가능해지려면, Point 클래스 내부에
operator+, operator-, operator==와 같은 연산자 오버로딩 함수가 정의되어 있어야 한다.
예를 들어 p1 + p2가 실행되면, 컴파일러는 이를 다음과 같이 해석한다:
p1.operator+(p2);
즉, 왼쪽 피연산자인 p1이 멤버 함수를 호출하고,
오른쪽 피연산자인 p2가 해당 함수의 인자로 전달되는 것이다.
따라서 연산자 오버로딩을 통해 Point 객체끼리의 연산도 일반 숫자처럼 자연스럽게 사용할 수 있게 된다.
중요한 Part이다.★★★★★
● + 연산자의 오버로딩 동작 방식의 이해
위와 같은 문장을 작성하면, 컴파일러는 p1 + p2를 주석에서와 같이 p1.operator+(p2) 형태로 변환한 뒤, 해당 함수가 존재하고 호출 가능한지를 확인한다.
변환된 결과를 보면 + 연산자 왼쪽에 있는 객체(p1)의 멤버 함수인 operator+에 오른쪽 객체(p2)가 인자로 전달되고 있다.
즉, p1 객체가 Point 클래스의 인스턴스라면, 이 클래스에는 반드시 operator+라는 멤버 함수가 정의되어 있어야 한다.
또한, 해당 함수는 Point 타입의 객체를 인자로 받아야 한다.
연산자 오버로딩에서 중요한 개념 중 하나는,
컴파일러가 연산식을 어떻게 해석하는지를 정확히 이해하는 것이다.
예를 들어 아래 코드를 보자
Point p3 = p1 + p2;
이 전체 한 줄은 **문장(statement)**이지만,
그 안에 있는 p1 + p2는 **표현식(expression)**이다.
컴파일러는 이 표현식을 보고, 해당 연산을 어떤 함수로 바꿀 수 있는지 판단한다.
즉, 다음과 같이 변환해 해석한다:
p1.operator+(p2);
+ 연산자를 기준으로 왼쪽에 있는 p1은 클래스 객체이므로,
컴파일러는 p1이 멤버 함수 operator+를 가지고 있는지 먼저 확인한다.
- 만약 p1 객체(즉 Point 클래스)에 operator+ 멤버 함수가 정의되어 있지 않다면, p1 + p2는 컴파일 오류가 발생한다.
- 반대로 operator+ 멤버 함수가 정의되어 있다면, 컴파일러는 이 함수를 호출하고 오른쪽 피연산자 p2를 인자로 전달한다.
Point 클래스에는 Point 객체를 인자로 받아 Point 객체를 반환하는 operator+ 멤버 함수를 정의할 수 있다.
Point operator+(Point p)
이렇게 정의하면 p1 + p2 형태의 연산이 가능해지며, 이는 컴파일러에 의해 다음과 같이 해석된다:
p1.operator+(p2)
여기서 p1은 operator+를 호출하는 객체이며, p2는 이 함수의 인자로 전달된다. 즉, 두 번째 피연산자(p2)는 operator+ 함수의 인자로 전달되어야 하므로, 해당 함수는 Point 타입의 인자를 받아야 한다.
참고로 함수 시그니처가 Point operator+(Point& p)처럼 참조 타입을 받는 것도 가능하지만, 복사본이 필요 없는 상황에서는 const Point&로 받는 것이 더 효율적이다.
왜 효율적일까?(Chat GPT)
🔍 1. 값 전달(Point p) vs 참조 전달(Point& p vs const Point& p)
✅ Point p → 값 복사
- 함수가 호출될 때 p2 객체의 복사본이 생성됨
- 복사 생성자(Point(const Point&))가 호출됨 → 비용 발생
- 값이 커지면(예: 큰 구조체) 성능 저하 가능
✅ Point& p → 참조 전달 (수정 가능)
- 복사가 일어나지 않음 → 성능 O
- 하지만 함수 내부에서 p의 값을 바꿀 수 있음
- 즉, 원래 객체인 p2의 값이 훼손될 수 있음
✅ const Point& p → 복사 없이 안전하게 전달
- 복사 X → 성능 O
- const → 값 수정 불가 → 안정성 O
- 연산자 함수처럼 "읽기만 하는" 함수에 적합
🎯 왜 연산자 오버로딩에서는 const Point&가 좋은가?
operator+는 보통 두 객체를 더해서 새로운 객체를 리턴할 뿐, 인자 객체 자체를 수정하지 않는다.
예:
Point p1{2, 3};
Point p2{4, 5};
Point result = p1 + p2;
이 상황에서 p1이나 p2가 수정되면 안 된다. 따라서:
- Point p → 불필요한 복사 (성능 낭비)
- Point& p → 수정 가능성 존재 (위험)
- ✅ const Point& p → 복사 없음 + 수정 불가 → 가장 안전하고 효율적
🧠 부연 설명 (고급 포인트)
- operator+는 비파괴적(non-mutating) 연산이므로 const가 원칙에 부합
- STL이나 프로 수준 C++ 코드에서도 거의 전부 const T&로 받음
C++에서는 + 연산자를 오버로딩하면, p1 + p2와 같은 표현이 내부적으로 다음과 같이 해석된다:
p1.operator+(p2);
이처럼 + 연산자는 왼쪽 피연산자(p1)가 클래스의 객체일 경우, 그 클래스에 정의된 operator+ 멤버 함수를 호출하는 방식으로 동작한다.
예시:
Point p1{2, 3};
Point p2{3, 4};
Point result = p1 + p2; // 실제로는 p1.operator+(p2) 호출
따라서 이 기능이 동작하려면 다음과 같은 조건이 필요하다:
- p1이 Point 클래스의 객체여야 하며,
- Point 클래스에는 Point operator+(const Point&) 형태의 멤버 함수가 정의되어 있어야 하고,
- 이 함수는 p2와 같은 타입의 인자를 받아야 한다.
📌 핵심 요약
- p1 + p2는 컴파일러가 자동으로 p1.operator+(p2)로 변환한다.
- 이 변환이 가능한 전제 조건은 Point 클래스에 operator+ 멤버 함수가 존재해야 한다는 점이다.
- 해당 함수가 없으면 컴파일러는 오류를 발생시키고, 연산자 오버로딩이 실패한다.
위와 같은 문장을 작성하면, 컴파일러는 p1 + p2를 주석에서와 같이 p1.operator+(p2) 형태로 변환한 뒤, 해당 함수가 존재하고 호출 가능한지를 확인한다.
변환된 결과를 보면 + 연산자 왼쪽에 있는 객체(p1)의 멤버 함수인 operator+에 오른쪽 객체(p2)가 인자로 전달되고 있다.
즉, p1 객체가 Point 클래스의 인스턴스라면, 이 클래스에는 반드시 operator+라는 멤버 함수가 정의되어 있어야 한다.
또한, 해당 함수는 Point 타입의 객체를 인자로 받아야 한다.
🧠 부연 설명
C++의 연산자 오버로딩은 문법적으로 일반 연산자 표현을 멤버 함수 호출로 바꿔주는 문법적 설탕(syntactic sugar) 역할을 한다. 따라서 a + b는 본질적으로 a.operator+(b)와 같다.
✅ 핵심 요지 요약
Point operator+(const Point& rhs) const;
이 함수 선언에는 세 가지 의미 있는 요소가 있다:
요소 이유
Point 반환형 | 두 좌표를 더한 새로운 객체를 반환하기 위해 |
const Point& rhs | 복사를 줄이고, 인자를 수정하지 않기 위해 |
const 멤버 함수 | operator+는 멤버 변수(xPos, yPos)를 수정하지 않기 때문에 |
강의 초반에는 연산자 오버로딩 함수를 다음과 같이 선언했다:
void operator+(Point p);
하지만 실제로는 다음과 같이 작성하는게 더 적절하다 :
Point operator+(const Point& rhs) const;
🔹 1) 반환형이 Point인 이유
operator+는 두 Point 객체를 더해 새로운 Point 객체를 반환하는 연산이다.
따라서 void를 반환하면 결과를 저장하거나 사용할 수 없기 때문에 적절하지 않다.
예를 들어 아래와 같은 코드에서:
Point p3 = p1 + p2;
p1 + p2는 새로운 Point 객체를 생성하고 그 값을 p3에 대입해야 한다.
이를 위해서 반환형이 반드시 Point여야 한다.
🔹 2) 인자를 const Point& rhs로 받는 이유
연산자 함수의 인자인 rhs는 단순히 값을 읽기 위한 용도다.
그런데 만약 아래처럼 작성하면:
Point operator+(Point rhs); // 값 전달
- rhs가 함수에 전달될 때 복사 생성자가 호출되어 비용이 발생하고,
- 실제 함수 내부에서 수정할 이유도 없으니 복사는 불필요하다.
그렇기 때문에 const Point& rhs 처럼 상수 참조자(const reference) 로 전달하는 것이
더 효율적이며, 인자 값이 함수 내부에서 실수로 수정되는 것도 방지할 수 있다.
🔹 3) 함수 끝에 const를 붙이는 이유
Point operator+(const Point& rhs) const;
여기서 const는 멤버 함수가 멤버 변수의 값을 변경하지 않음을 나타낸다.
즉, 이 함수 안에서 xPos, yPos를 수정하면 컴파일 오류가 난다.
이는 연산자 오버로딩 함수가 p1의 내부 상태를 변경하지 않아야 한다는 원칙에 부합한다.
예를 들어 p1 + p2를 수행한 후 p1이 바뀌면 안 되고, 결과는 새로운 객체(p3)에 저장되어야 한다:
Point p1{2, 3};
Point p2{3, 4};
Point p3 = p1 + p2; // p1과 p2는 그대로, p3 = {5, 7}
함수 끝에 const를 붙임으로써, p1의 값이 함수 실행 중에 변하지 않음이 보장된다.
🧠 보충 설명: "왜 중요한가?"
연산자 오버로딩 함수는 일반적으로 비파괴적(non-mutating) 이어야 한다.
즉, 연산을 수행한 결과는 새로운 값으로 주어져야 하며, 기존 값은 그대로 유지되어야 한다.
이를 통해 코드는 예측 가능하고, 부작용 없이 사용할 수 있다.
📌 전체 정리 문장
연산자 오버로딩에서 Point operator+(const Point& rhs) const;와 같은 선언은 다음 세 가지 이유로 가장 바람직하다:
- Point 반환형: 연산 결과를 새 객체로 반환하기 위해
- const Point& 인자: 복사 비용을 줄이고 안전하게 읽기 위해
- const 멤버 함수: 멤버 변수의 값을 변경하지 않도록 보장하기 위해
이러한 원칙을 지키면, p1 + p2와 같은 표현을 안정적이고 효율적으로 사용할 수 있다.
이제 Point operator+(const Point& rhs) const 함수가 실제로 어떤 동작을 해야 하는지 살펴보자.
이 함수는 두 Point 객체의 x 좌표와 y 좌표를 각각 더한 결과를 새로운 Point 객체로 반환해야 한다.
즉, p1 + p2 연산을 수행하면 p1과 p2의 좌표를 더한 새로운 좌표를 갖는 객체가 생성되는 것이다.
이를 구현하면 다음과 같다:
Point operator+(const Point& rhs) const
{
return Point{ xPos + rhs.xPos , yPos + rhs.yPos };
}
- rhs는 오른쪽 피연산자, 즉 p1 + p2에서 p2에 해당
- xPos, yPos는 왼쪽 피연산자인 p1 객체의 멤버 변수
- 이 둘의 좌표를 더해서 새로운 객체를 만들어 반환하는 것이 operator+의 역할
결과적으로 xPos + rhs.xPos는 p1.x + p2.x 연산을 의미하며,
return 문은 이 결과를 담은 새로운 Point 객체를 반환한다.
🧠 보충: 왜 ‘새 객체’를 만들어야 하나?
연산자 오버로딩에서 operator+는 기존 객체를 변경하지 않고,
항상 새로운 객체를 반환해야 한다는 것이 중요한 규칙이다.
그렇게 해야 예측 가능하고 안전한 코드가 된다.
📌 최종 요약
Point operator+(const Point& rhs) const 함수는,
왼쪽 객체의 멤버 변수(xPos, yPos)와
오른쪽 인자 객체(rhs)의 멤버 변수(rhs.xPos, rhs.yPos)를 더한 결과를
새로운 Point 객체로 만들어 반환한다.
기존 객체는 전혀 변경되지 않으며, 함수는 읽기 전용(const)으로 안전하게 동작한다.
Binary operator Overlading as Member Function
◆ 이항(binary) 연산자의 멤버 함수로의 선언 (+, - , == ,! , > , < , etc)
● + 연산자의 오버로딩 동작 방식의 구현
Point operator+(const Point& rhs) const
{
return Point{ xPos + rhs.xPos , yPos + rhs.yPos };
}
Point p3 = p1 + p2; // p1.operator+(p2);
위의 멤버 함수가 호출되면, p1과 p2의 xPos와 yPos 값을 각각 더한 결과로 새로운 Point 객체가 생성된다.
이렇게 만들어진 객체는 반환되어 p3에 복사된다.
✅ operator+ 멤버 함수에 const 를 써야하는 이유
const Point p1{2, 3};
Point p2{3, 4};
Point p3 = p1 + p2;
이 코드가 잘 작동하려면 어떤 조건이 필요할까?
🔍 핵심 개념: const 객체는 const 멤버 함수만 호출 가능
p1이 const Point 타입이기 때문에, p1.operator+(p2)를 하려면
👉 operator+는 const 멤버 함수여야 한다.
즉, 함수 선언이 다음과 같아야 한다:
Point operator+(const Point& rhs) const
// ↑ 이게 필수!
📌 이유 요약
const Point p1 | 멤버 함수를 호출할 때 읽기 전용 보장이 필요함 |
operator+ 뒤에 const | 멤버 변수(xPos, yPos)를 수정하지 않겠다는 약속 |
없으면? | ❌ const 아닌 객체만 사용할 수 있으므로 p1 + p2 컴파일 에러 발생 |
❌ 만약 const 안 붙이면?
class Point {
...
Point operator+(const Point& rhs); // const 없음
};
이 상태에서 const Point p1{2, 3};을 사용하면:
Point p3 = p1 + p2; // ❌ 컴파일 에러!
- 이유: p1이 const 객체이므로, const 멤버 함수가 아닌 operator+는 호출 불가능
✅ 요약 한 줄
operator+ 멤버 함수에 const를 붙이는 이유는,
const 객체에서도 해당 함수를 호출할 수 있도록 보장하기 위해서이다.
중요한 Part이다.★★★★★
● 비교 연산자의 오버로딩
Point operator==(const Point& rhs) const
{
if (xPos == rhs.xPos && yPos == rhs.yPos)
return true;
else
return false;
}
이 함수에서 p1은 멤버 함수를 호출하는 주체이므로, 해당 객체의 상태가 변경되지 않음을 보장하기 위해 함수 선언 뒤에 const를 붙여준다.
또한, 비교 대상인 p2 역시 이 함수 내에서 변경되지 않기 때문에, 매개변수 rhs에도 const를 붙여 const Point& rhs로 선언한다.
🔹 == 연산자 오버로딩의 필요성
C++에서 p1 == p2 구문은 두 객체가 같은지를 비교하는 표현이다.
이때 컴파일러는 다음과 같이 해석한다:
p1.operator==(p2);
하지만 Point 클래스에 operator== 멤버 함수가 정의되어 있지 않으면
컴파일 에러가 발생한다. IDE에서 p1 아래에 빨간 줄이 그어지는 이유도 이 때문이다.
🔹 if (p1 == p2) 문장에서 필요한 조건
if 문 안의 조건은 반드시 논리형(bool) 값이어야 한다.
따라서 operator== 함수는 반드시 bool을 반환해야 한다.
정리하면, 다음과 같이 오버로딩해야 한다:
bool operator==(const Point& rhs) const;
- const Point& rhs : 복사 방지 + 수정 방지
- const 멤버 함수 : 비교만 하고 p1을 수정하지 않기 때문
- bool 반환형 : if (p1 == p2)에서 논리 결과로 사용하기 위해
🔹 실제 구현 예시
bool operator==(const Point& rhs) const {
return xPos == rhs.xPos && yPos == rhs.yPos;
}
💡 위 코드에서 if 문 없이 바로 return 해도, 완전히 동일한 의미다.
훨씬 간결하고 효율적인 표현이다.
✅ 요약
- p1 == p2는 p1.operator==(p2)로 해석된다.
- operator==가 없으면 컴파일 에러 발생
- if (p1 == p2)가 동작하려면 bool 반환형이 필요하다
- 좌표가 같음을 판단하려면 xPos와 yPos를 각각 비교하면 된다
📌 최종 정리 문장
Point 클래스에 operator== 함수를 정의하면 두 점 객체를 == 연산자로 비교할 수 있다.
이 함수는 좌표 값이 모두 같을 때만 true를 반환하도록 작성해야 하며, 반환형은 bool이고,
인자는 const Point&, 함수 자체는 const 멤버 함수로 선언하는 것이 적절하다.
https://youtu.be/wPBVXJLwWE8?si=mHFlL5_i6lsNdyHd
이번 정리는 교수님 강의 듣고 chatGPT 에게 정리를 맡겨서 특유의 포멧이 생겼다.
그래도 이 정리법이 아니였다면 p1.operator+(p2) 부분에서 헷갈릴뻔하였다.
'C++ > C++ : Study' 카테고리의 다른 글
8. 연산자 오버로딩 (4) - 전역 함수로의 구현 (3) | 2025.06.15 |
---|---|
8. 연산자 오버로딩 (3) - 단항 연산자 오버로딩 멤버 함수로의 구현 (0) | 2025.06.10 |
8. 연산자 오버로딩 (1) - 소개 (3) | 2025.06.08 |
다형성(6) - 인터페이스 (2) | 2025.05.30 |
다형성(5) - 순수 가상 함수와 추상 클래스 (2) | 2025.05.29 |