상세 컨텐츠

본문 제목

C++ 연산자 함수

C++/C++98

by deulee 2023. 8. 5. 15:29

본문

대망의 연산자 오버로딩이다.

 

연산자 오버로딩은 말 그대로 연산자를 함수로 오버로딩이 가능하다는 것을 의미한다.

 

다음의 예시 있다고 가정해보자.

Num A;
Num B;
Num C;

A = B + C;

 

이 전에 클래스는 타입이라고 얘기했던 것을 기억해보자.

 

즉, 기본적인 자료형이 제공하는 모든 연산이 클래스에서도 가능하다는 것인데 그 중에는 사칙연산도 포함이 된다.

 

그럼 이것이 어떻게 가능할까? 막상 저대로 컴파일을 한다면 에러가 나올 것이다.

 

이를 해결하기 위해 나온게 바로 연산자 오버로딩이다.

 

#include <iostream>

class Num
{
private:
	int x;
public:
	Num() {}
	Num(int _x) : x(_x)
	{}
	const Num operator+(const Num& right) const
	{
	}
};

int main(void)
{
	Num A(3), B(4);

	Num C = A + B;
	Num D = A.operator+(B); // 같은 의미
	return 0;
}

위의 예시를 보면 operator+() 라는 생소한 표현이 있을 것이다.

 

그렇다. 이것이 연산자를 오버로딩하기 위해 사용되는 함수의 이름이라고 생각하면 된다.

 

이 함수는 어떻게 호출할 수 있을까?

 

A + BA.operator+(B)로 호출할 수 있는데 이 둘은 표현만 다른 같은 구문이다. 사실 A + B가 A.operator(B)로 자동으로 바뀐다고 생각해도 좋을 것이다.

 

그럼 이렇게 했을 경우 장점이 뭐가 있을까?

 

우선 보기 편하고 직관적이다. 그리고 사칙연산의 우선순위를 자동으로 실행해주기 때문에 그 또한 편하다.

 

그럼 본격적으로 탐방해보도록 하자.

 

연산자 함수의 형식

연산자 함수를 정의하는 방법은 두 가지가 있다.

 

1. 클래스의 멤버 함수로 작성한다.

 

2. 전역 함수로 작성한다. - 나중에 알아보도록 하자.

 

멤버 연산자 함수의 기본 형식은 다음과 같다.

 

리턴타입 Class::operator 연산자(매개 변수) // 클래스 내부에서 정의하는 경우 :: 연산자를 사용하지 않아도 된다.
{
	함수 본체
}

 

연산자 자리에는 대부분의 기호가 올 수 있다. (몇가지 빼고)

 

예시로 완전한 형태의 연산자 함수의 형태를 보도록 하자.

 

class Num
{
private:
	int x;
public:
	Num() {}
	Num(int _x) : x(_x)
	{}
	const Num operator+(const Num& right) const
	{
		Num ret;
		ret.x = x + right.x;
		return ret;
	}
};

외부에서 정의한다면 어느 소속의 함수인지 밝혀주는 :: 연산자를 꼭 붙이는 것을 잊지 말자.

 

위의 형태로 작성한데는 여러 가지가 있는데 차근차근 알아가보자.

 

인수의 타입

 

연산자 함수의 인수는 자기 자신(this) + 함수로 전달되는 인수가 연산 대상이다.

 

A(좌변) + B(우변)  <--> A.operator(B);

 

우선 객체와 연산 가능한 대상만 인수로 쓴다는 것은 인지하고 있어야 한다.

 

우선 왜 call by reference인지 부터 확인해보도록 하자.

 

객체는 call by value를 할 때마다 생성자를 호출하기 때문에 비효율적이라 포인터나 참조자로 호출하는 것이 효율적이다.

 

그럼 왜 포인터로 안하고 참조자로 했을까?

 

그 이유는 간단하다.

Num operator+(const Num* right) const; // 선언부

C = A.operator+(&B); // 주소 연산자를 붙여줘야함.
C = A + &B; // 이것과 같은 표현

 

보기만해도 이상함을 느낄 수 있을 것이다. 일반적인 표기법에 어긋나고 호출 구문이 이상해지기 때문이다.

 

인수와 함수의 상수성

피연산자로 전달되는 인수는 보통 읽기만 한다. 그렇기 때문에 인수로 넘기는 피연산자는 const로 지정해주는 것이 바람직하다.

 

그리고 피연산자의 인수가 const이어야 하는 또 다른 이유는 상수 객체도 인수로 넘겨야 하기 때문이다.

 

그리고 함수 또한 상수성을 띄우는 것이 좋다.

 

다음을 보도록 하자.

 

const int i = 4;
int j = 2, k;
k = i + j;

 

이때 i의 값이 바뀌어야 하는가?

 

전혀 그렇지 않다. 즉, 상수 i의 값이 바뀌지 않아야 한다는 보장이 있어야 한다.

 

그래서 대부분의 연산자 함수에는 뒤에 const 를 붙여 함수의 상수성을 보장하는 것이 옳다.

 

하지만 객체의 값을 직접 변경하는 연산자const로 지정하면 안된다. 대표적인 예로는 대입 연산자증감 연산자가 있다.

 

임시 객체의 사용

 

위 예제의 operator+ 연산자 본체를 보면 Num 형의 임시 객체 ret을 선언하고 호출 객체와 피연산자 right를 더한 결과를 ret에 작성한 후 임시 객체 ret를 리턴하고 있다.

 

이때 ret은 잠시 값을 저장하기 위한 용도라고 생각하면 된다.

 

이를 하지 않고 this를 통해서 하게 된다면 호출 객체의 값이 바뀌게 됨으로써 위의 함수의 상수성을 지키지 못하게 된다.

 

그렇기 때문에 잠시 결과값을 저장할 임시 객체가 필요하고 리턴되는 값을 같은 타입의 다른 객체에 즉시 대입해야 한다.

 

그렇지 않으면 이 값은 버려진다.

 

A(바로 더해진 값 대입해주기) = B + C

 

리턴 타입의 상수성

 

리턴 타입도 상수로 설정하는 것도 때에 따라 다르지만 보통은 상수 객체를 리턴해야 한다.

 

int i = 3, j = 4, k;
k = i + j;

 

이 값을 보면 i + j의 결과로 리턴되어야 하는 값은 7이라는 정수 상수이지 정수 변수가 아니다.

 

그렇기 때문에 클래스에서도 B + C는 덧셈을 한 객체일 뿐 여기에 어떤 변경을 가할 수는 없어야 하며 만약 이를 허용하면 잠시 후에 사라질 임시 객체를 변경하는 쓸데없는 짓을 하게 되는 것이다.

 

생성자의 활용

 

굳이 임시 객체를 생성해서 하나하나 값을 대입하는 것보다 생성자를 이용하여 리턴값에 바로 보내면 코드가 더 짧고 간략해 보인다. 또한, 컴파일러의 리턴값 최적화(RVO: Return Value Optimization) 기능의 도움도 받을 수 있어 훨씬 더 유리하다.

 

이 말은 호출원의 대입되는 좌변에 대해 곧바로 생성자를 호출하여 불필요한 임시 객체를 만들지 않음으로써 훨씬 더 작고 빠른 코드를 생성한다.

const Num operator+(const Num& right) const
{
	return Num(x + right.x);
}

 

다음 글에서는 대표적으로 사용하는 연산자 오버로딩에 대해서 알아보도록 하겠다.

'C++ > C++98' 카테고리의 다른 글

C++ 다양한 연산자 오버로딩의 예시  (0) 2023.08.05
C++ 전역 연산자 함수  (0) 2023.08.05
C++ 상수 멤버, const  (0) 2023.08.05
C++ 정적 멤버, static  (0) 2023.08.05
C++ this  (0) 2023.08.05

관련글 더보기