강께르의 개발일지

[C++] 연산자 오버로딩에 대해_2 본문

프로그래밍/C++

[C++] 연산자 오버로딩에 대해_2

강께르 2021. 6. 17. 16:35

- 앞의 내용에 이어서 연산자 오버로딩에 대해 계속해서 정리하려고 한다.

 

1. 단항 연산자의 오버로딩

- 앞에서 했던 것은 이항 연산자라고 한다. 지금부터 할 것은 단항 연산자의 오버로딩이다. 대표적인 단항 연산자는 증감 연사자가 있을 것이다.

++pos  // 증가 연산자 전위 연산
pos--; // 감소 연산자 후위 연산

- 이것은 어떻게 구현할까? 연산자의 이름은 우리가 알고 있고, 단항 연산자이니 객체가 호출하는 멤버함수의 형태로 오버로딩을 한다면 다음과 같은 형태를 띄게 될 것이다.

pos.operator++();

- 전달할 인자가 없는 것은 단항 연산자이기 때문에, 그나마 있는 피연산자 하나는 멤버함수의 호출 용도로 사용하고 있다. 전역함수로 선언한다면 피연산자 하나가 매개변수로 전달되어 다음과 같은 형태를 띠게 될 것이다.

operator++(pos);

- 연산자 오버로딩 첫 번째 글에서 사용했던 Point 클래스를 참고해서 증감연산자를 어떻게 클래스의 멤버함수 그리고 전역 함수로 구현할 수 있을까? 아래 코드는 예시이다.

class Point
{
private:
	int xPos, yPos;
public:
	Point& operator++()
	{
		xPos += 1;
		yPos += 1;
		return *this;
	}
	friend Point& operator--(Point& pos);
};

Point& operator--(Point& pos)
{
	pos.xPos -= 1;
	pos.yPos -= 1;
	return pos;
}

- 생성자를 정의하여 멤버 변수를 초기화하는 일은 이 코드에선 생략하도록 하자. 여기서 주목할 것은 연산자 오버로딩한 멤버함수의 반환형이다.

- 반환형으로 객체의 참조 값을 요구하고 있다. 함수 내에 return문을 보면 *this를 반환하는 것을 볼 수 있다.

- 여기서 짤막하게 this를 요약해서 정리하자면, this는 멤버 함수를 호출한 객체 자신의 포인터 주소를 의미하고, *this는 그 포인터 주소의 역참조이니, 객체 자기 자신을 의미한다고 볼 수 있다.

- *this로 반환한다는 것은 객체 자신의 복사본을 만들어서 반환한다는 것이다. 이를 이해한다면 아래와 같은 코드도 정상적으로 작동할 것이다.

++(++pos); // 이것을 아래 코드로 풀어 쓸 수 있다.
++(pos.operator++()); // 이것을 다시 함수의 결과인 반환값으로 풀어 쓴다면 아래와 같을 것이다.
++(pos의 참조 값); // 이것을 다시 전위 연산 증가 연산자를 풀어쓴다면 아래와 같을 것이다.
(pos의 참조 값).operator++();

- 증가 연산자의 반환값은 객체 자신의 참조 값이기에 그 참조값인 객체 자신을 통해 다시 함수를 호출하는 모양으로 사용할 수 있다는 이야기이다.

 

2. 전위연산과 후위연산의 구분

- 전위연산에 대한 코드는 위에서부터 계속 사용하고 있는 연산자 오버로딩을 그대로 익히고 사용하면 된다.

- 하지만 후위연산은 어떻게 할까? 그에 대한 코드는 아래와 같다.

++pos;
pos.operator++();
// 전위 연산

pos++;
pos.operator++(int);
// 후위 연산

- 후위연산을 보면 매개변수가 들어간 자리에 int 자료형이 들어가 있는 것을 볼 수 있다. 이는 int형 자료형 매개변수를 전달한다는 의미가 아니라, 후위연산을 구분하는 목적으로 저 자리에 int가 들어간 것이다. 절대 int형 값을 매개변수로 전달하는 것이 아니다.

- 만약 구현한다면 아래와 같은 코드가 될 것이다. 앞 뒤 문맥을 생략하고 Point 객체 내에서 오버로딩한다는 가정 하에 코드를 적어보겠다.

Point& operator++(int)
{
        const Point temp(xPos, yPos); // 호출한 객체의 원래 멤버변수 값을
                                      // 새로운 임시 객체에 저장하고
	xPos += 1;                    // 객체의 값을 수정한 다음
	yPos += 1;
	return temp;                  // 수정하지 않은 값이 담긴 임시 객체를 반환한다. 
}

- 이렇게 연산자 오버로딩을 하면 다른 곳에서 pos++과 같이 후위연산이 가능하다.

- 여기서 후위연산이 갖는 의미가 무엇인가? pos++;을 예로 설명하자.

- 표면적으로 사용할 때 느끼기엔 pos가 원래 저장하고 있는 값으로 다른 연산을 먼저 하고 마지막으로 ++연산을 하는 느낌이다. 이를 연산자 오버로딩으로 어떻게 풀어 작성할 수 있을까?

- 위의 코드에서 설명할 수 있는 후위연산은 함수 내에서 지역 변수인 객체를 하나 만들어 그곳에 원래의 값을 저장하고 있는다. 그리고 객체 자신의 멤버 변수의 값을 연산하고 아무것도 건드리지 않은 지역 변수인 객체를 반환한다.

- 이렇게 되면 함수 내부의 코드를 모르는 사용자는 그저 "아~ 먼저 다른 연산에 변수가 쓰이고 나서 값이 증가되는구나~"와 같은 느낌을 받을 수 있을 것이다.

- 하지만 함수 내부로 보면 객체 원래 수정하지 않은 값은 임시적으로 만든 다른 객체에게 값을 복사하여 전달하고, 객체 자신의 값을 증가시키고 임시로 만든 그 객체를 반환하는 것이다. 사실은 반환값을 받았을 때부터 이미 객체 자신의 값은 변했다는 것이다.

 

3. 이항 연산자의 교환 법칙에 대한 문제와 그 해결

- 이항 연산자는 교환 법칙이 적용된다. x + y를 더하거나, y + x를 더하거나 결과는 똑같다.

- 하지만 객체와 정수와의 연산과 같은 문제는 어떨까? 다시 말해, 자료형이 다른 두 피연산자를 대상으로 하는 연산 말이다.

- 문제를 풀어서 설명하겠다. 우리는 여태까지 객체와 객체끼리의 연산을 연산자 오버로딩을 해왔다.

- 하지만 객체와 정수와 같은 연산도 필요할 수 있지 않겠는가? 그러면 이렇게 묘사할 수 있을 것이다.

Point pos1(2, 4); // 멤버변수 xPos에 2, yPosd에 4로 초기화

Point pos2 = pos1 * 3; // 멤버변수의 값을 3배로

- 이런 연산을 원할 수 있다. 이런 연산 같은 경우엔 pos1이 lvalue, 즉 곱셈 연산자보다 앞에 있기 때문에 다음과 같은 구문으로 표현할 수 있을 것이다.

Point pos2 = pos1.operator*(3);

- Point 객체인 pos1의 operator* 멤버 함수를 호출해 매개변수를 정수형 값 3을 전달해준 모습과 똑같다. 이 모양은 멤버 함수 내에서 매개변수를 달리 정의해 함수 몸체를 구현할 수 있을 것이다.

- 그럼 pos1이 곱셈 연산자보다 뒤에 있다면 어떨까? 아래 코드를 보자.

Point pos2 = 3 * pos1;

pos2 = 3.operator*(pos1); // 3은 정수형 상수이다. pos1과 같이
                          // 멤버함수를 가진 객체가 아니다.

- 위와 같은 구문은 교환법칙에 의해서 가능하기도 하고 반환값도 교환하기 전이랑 동일할 거라고 기대되는 것도 있다.

- 하지만 이를 풀어서 아래에 쓴 것처럼 정수형 상수 3이 객체인 것 마냥 멤버함수를 호출하는 모습이 되었다. 이는 말이 안 되지 않나. 그럼 어떻게 교환법칙을 구현할 수 있을까?

pos2 = operator*(3, pos1);

- 전역함수를 사용해서 이에 대한 연산자 오버로딩을 하면 된다. 전역함수를 호출하여 피연산자 두 개를 매개변수로 전달하면 된다. 이와 같이 구현된 모습은 아래와 같다.

class Point
{
private:
	int xPos, yPos;
public:
	Point(int x, int y) : xPos(x), yPos(y) { }
	friend Point& operator*(int times, Point& ref);
};

Point& operator*(int times, Point& ref)
{
	Point pos(ref.xPos * times, ref.yPos * times);
	return pos;
}

- 이렇게 하면 새로 생성된 객체에 정수 곱셈 연산을 처리하고 그 객체 자신을 반환할 것이다.

 

- 연산자 오버로딩에 대해서는 이야기할 것이 더 있는 것 같다. 열혈 C++ 프로그래밍을 정리하면서 그 내용을 봤을 때는, cout과 cin에 대한 이야기를 하며 다른 연산자의 오버로딩을 다루는 것을 이어간다.

- 하지만 지금 배우고 있는 단계에선 그 정도의 연산자 오버로딩을 필요로 하지 않은 것 같다. 정리할 것이 다른 것도 많으니 다음에 정리할 필요를 느끼거나 시간이 있을 때, 3번째 혹은 4번째까지 정리를 이어나가도록 하겠다.