Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Archives
Today
Total
관리 메뉴

nomad-programmer

[Programming/C++] 연산자 오버로딩 본문

Programming/C++

[Programming/C++] 연산자 오버로딩

scii 2023. 1. 19. 18:15

단항 연산자 오버로딩

오버로딩이 가능한 단항 연산자는 !, &, ~, *, +, -, ++, -- 형 변환 연산자이다. 

++, --연산자로 단항 연산자 오버로딩에 대해 알아보자.

++, -- 연산자 오버로딩

++ 연산자는 전위 ++ 연산자와 후위 ++ 연산자가 있습니다. 컴파일러와 약속된 함수는 operator++() 와 operator++(int) 입니다.

#include <iostream>

using namespace std;

class Point {
public:
	Point(const int x, const int y);
	~Point();
	void Print() const;

	// 전위 ++, --
	const Point& operator++();
	const Point& operator--();
	// 후위 ++, --
	const Point operator++(int);
	const Point operator--(int);

private:
	int x;
	int y;
};

Point::Point(const int x=0, const int y=0) { this->x = x; this->y = y; }
Point::~Point() {}

void Point::Print() const {
	cout << "x: " << x << ", " << "y: " << y << endl;
}

// 전위 ++
const Point& Point::operator++() {
	++x;
	++y;
	return *this;
}

// 전위 --
const Point& Point::operator--() {
	--x;
	--y;
	return *this;
}

// 후위 ++
const Point Point::operator++(int) {
	Point pt(x, y);
	// 내부 구현이므로 멤버 변수는 전위 ++ 연산을 사용해도 무방하다.
	++x;
	++y;
	return pt;
}

// 후위 --
const Point Point::operator--(int) {
	Point pt(x, y);
	// 내부 구현이므로 멤버 변수는 전위 -- 연산을 사용해도 무방하다.
	--x;
	--y;
	return pt;
}

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

	// p1.operator++(); 와 같다.
	result = ++p1;
	p1.Print();
	result.Print();

	// p2.operator++(0); 와 같다.
	result = p2++;
	p2.Print();
	result.Print();

	return 0;
}


// 결과
// 전위 ++ 연산자는 연산한 후의 값이므로 result의 결과는 3, 4이다.
// 후위 ++ 연산자는 연산 전의 값이므로 result의 결과는 2, 3이다.
x: 3, y: 4
x: 3, y: 4
x: 3, y: 4
x: 2, y: 3
// 중복 코드를 피하는 방법으로 다음과 같은 코드를 사용할 수 있다.
const Point operator++(int) // 후위 ++
{
    Point tmp = *this;
    ++(*this);
    return tmp;
}

++p1과 p2++ 객체는 각각 p1.operator++()와 p2.operator(0)으로 해석된다. 전위 ++ 연산자와 후위 ++ 연산자는 모두 operator++() 멤버 함수를 호출한다. ++ 연산자를 구분하기 위해 후위 ++연산자는 operator++() 멤버 함수 호출 시 의미 없는 (dummy) 정수형 인자 0을 전달한다.

-- 연산자는 ++ 연산자와 동일하다. 그래서 생략!


이항 연산자 오버로딩

오버로딩이 가능한 이항 연산자로는 +, -, *, /, ==, !=, <, <= 등이 있다.

==, != 연산자 오버로딩

==, != 연산자는 비교 연산으로 true 혹은 false로 결과가 반환되는 bool 타입이다.

#include <iostream>

using namespace std;

class Point {
public:
	Point(const int x, const int y);
	~Point();
	void Print() const;
	bool operator==(const Point& ref) const;
	bool operator!=(const Point& ref) const;


private:
	int x;
	int y;
};

Point::Point(const int x=0, const int y=0) { this->x = x; this->y = y; }
Point::~Point() {}

void Point::Print() const {
	cout << "x: " << x << ", " << "y: " << y << endl;
}

bool Point::operator==(const Point& ref) const {
	return x == ref.x && y == ref.y ? true : false;
}

bool Point::operator!=(const Point& ref) const {
	// !((*this).operator==(ref)); 와 같다.
	return !(*this == ref);
}


int main() {
	Point p1(2, 3), p2(5, 5), p3(2, 3);

	if (p1 == p2)	// p1.operator==(p2); 와 같다.
		cout << "p1 == p2" << endl;
	if (p1 == p3)	// p1.operator==(p3); 와 같다.
		cout << "p1 == p3" << endl;
	if (p1 != p2)	// p1.operator!=(p2); 와 같다.
		cout << "p1 != p2" << endl;
	if (p1 != p3)	// p1.operator!=(p3); 와 같다.
		cout << "p1 != p3" << endl;

	return 0;
}


// 결과
p1 == p3
p1 != p2

+, - 연산자 오버로딩

#include <iostream>

using namespace std;

class Point {
public:
	Point(const int x, const int y);
	~Point();
	void Print() const;
	const Point operator+(const Point& ref) const;
	const Point operator-(const Point& ref) const;

private:
	int x;
	int y;
};

Point::Point(const int x=0, const int y=0) { this->x = x; this->y = y; }
Point::~Point() {}

void Point::Print() const {
	cout << "x: " << x << ", " << "y: " << y << endl;
}

const Point Point::operator+(const Point& ref) const {
	Point pt;
	pt.x = x + ref.x;
	pt.y = y + ref.y;
	return pt;
}

const Point Point::operator-(const Point& ref) const {
	Point pt;
	pt.x = x - ref.x;
	pt.y = y - ref.y;
	return pt;
}


int main() {
	Point pt1(5, 7), pt2(1, 2);
	Point res;

	res = pt1 - pt2;
	res.Print();

	res = pt1 + pt2;
	res.Print();

	return 0;
}


// 결과
x: 4, y: 5
x: 6, y: 9

멤버 함수로 정의할 수 없는 경우

연산자를 오버로딩할 때 반드시 일반 함수를 사용해야 하는 경우가 있다. 그것은 오른쪽 피연산자가 객체인 경우가 그렇다. 
예를 들어 다음과 같이 사용할 수 있게 << 연산자를 오버로드하는 경우를 생각해보자.

Complex x(10, 5);
cout << c;        // cout.operator<<(c); 와 같이 됨

cout은 이미 스탠다드 라이브러리에 존재하는 객체로 cout.operator<<(c); 와 같이 실행되도록 수정할 수 없다. 때문에 일반 함수로 오버로딩해야 한다.
멤버 함수를 사용해서 위와 같은 상황을 만들어야 한다면 어떻게 해야 할까? 다음과 같은 두 가지 후보를 생각해볼 수 있지만 두 가지 모두 원하는 결과를 얻을 수 없다.

// 왼쪽 피연산자는 Complex, 오른쪽 피연산자는 cout인 경우에 호출된다.
void Complex::operator<<(ostream& o);

// 양쪽 피연산자 모두 Complex인 경우에 호출된다.
void Complex::operator<<(const Complex& right);

결국 두 개의 피연산자 중 오른쪽 피연산자만 Complex인 경우는 멤버 함수를 사용해서 구현할 수 없다. 이는 비단 << 연산자 뿐만 아니라 다른 연산자의 경우에도 모두 마찬가지로 작용하는 규칙이다.

일반 함수를 사용하는 경우라면 함수의 원형을 이렇게 만들면 될 것이다. 일단 << 연산자를 오버로드하는 것이므로 함수의 이름은 operator<<가 되야 한다. 왼쪽 피연산자는 cout 객체이므로 cout 객체의 타입인 ostream 타입을 사용해야 stream& 처럼 만들어야 한다. 오른쪽 피연산자는 Complex 객체이므로 const Complex& 처럼 만들어야 한다.

ostream& operator<<(ostream& o, const Complex& right)

다음 예제는 << 연산자를 오버로딩해서 Complex 객체를 cout으로 보낼 수 있게 만들었다.

#pragma warning(disable: 4996)

#include <iostream>

using namespace std;

class Complex {
public:
    Complex(int realpart, int imaginarypart)
        :real(realpart), imaginary(imaginarypart) {}

    int Real(int realpart) {
        real = realpart;
        return real;
    }
    int Imaginary(int imaginarypart) {
        imaginary = imaginarypart;
        return imaginary;
    }

    int Real() const { return real; }
    int Imaginary() const { return imaginary; }

private:
    int real;
    int imaginary;
};

ostream& operator<<(ostream& o, const Complex& ref) {
    // showpos를 cout 객체에 보냈는데, 이렇게 하면 허수 부분의 값이 양수인 경우에도 +5처럼 기호를 출력한다.
    // noshowpos를 cout 객체에 보내서 다시 원래의 상태로 되돌린다.
    o << ref.Real() << showpos << ref.Imaginary() << "i" << noshowpos << endl;
    return o;   // 인자로 받은 cout 객체를 다시 반환.
}

int main(void) {
    Complex c1(10, 5), c2(55, 100);

    cout << c1 << c2 << endl;

    return 0;
}


// 결과
10+5i
55+100i

반환형을 ostream&로 하는 이유는, << 연산자를 여러 개 붙여서 사용할 수 있도록 만들기 위함이다.


연산자 오버로딩의 규칙

다음은 오버로딩이 가능한 연산자의 리스트이다.

# 피연산자가 하나인 연산자 (단항 연산자)

! 논리 not 연산
& 주소를 얻는 연산
~ 비트 단위 not 연산
* 역참조 연산 (Dereferencing)
+ +5 처럼 값에 + 부호를 붙여주는 연산
++ 1 증가시키는 연산
- -5 처럼 값에 - 부호를 붙여주는 연산
-- 1 감소시키는 연산
형변환 연산자 타입을 변환시키는 연산

# 피연산자가 두 개인 연산자 (이항 연산자)

, != % %= & && &= *
*= + += - -= -> ->* /
/= < << <<= <= = == >
>= >> >>= ^ ^= | |= ||
( ) [ ]            

# 기타 연산자

( ) 함수 호출 연산
[ ] 배열의 원소를 가져오는 연산(Subscription operator)
new 객체를 생성하는 연산
new[ ] 객체의 배열을 생성하는 연산
delete 객체를 해제하는 연산
delete[ ] 객체의 배열을 해제하는 연산

이 중에서 다음의 연산자들은 반드시 멤버 함수를 사용해서 오버로드해야 한다.

= ( ) [ ] ->

그리고 다음과 같은 연산자들은 오버로딩할 수 없다.

. subnet.name 처럼 멤버를 선택하는 연산
.* (subnet.*name)() 처럼 멤버에 대한 포인터를 선택하는 연산
:: Subnet::MAX_NUMBER 처럼 영역을 선택하는 연산
?: a > b ? a : b처럼 사용하는 삼항 연산자
전처리기에서 인자를 문자열로 바꾸는 연산
## 전처리기에서 문자열을 결합하는 연산

기존 연산 방법을 바꿀 수 없다.

연산자 오버로딩을 할 때 피연산자들 중에 적어도 하나는 객체가 되야 한다. 예를 들어 다음과 같은 함수는 만들 수가 없다.

int operator+(int a, int b);

만약 이런 함수를 만들 수 있다면 정수의 덧셈 방법을 변경할 수 있는 셈이므로 심각한 혼란을 초래할 수 있다. 

또한 연산자의 우선 순위나 계산 순서를 변경하는 것도 불가능하다. 그렇기 때문에 오버로드된 함수의 반환 값을 결정할 때는 기존 연산 방식을 고려할 필요가 있다. << 연산자를 오버로드할 때 cout 객체를 반환하는 것이나 a = b = c = 100 처럼 대입 연산자의 경우에도 반환 값에 신경을 써줄 필요가 있다.


기존 연산자의 의미를 해치지 말아야 한다.

기존 연산자의 의미에서 크게 벗어나지 않게 만드는 것도 중요한 일이다. 예를 들어 + 연산자를 오버로드한 다음 뺄셈을 수행한다고 해보자. 매우 이해하기 힘든 코드가 될 것이 분명하다. 연산자를 오버로드하는 목적은 어디까지나 클래스의 편리한 사용과 이해하기 쉬운 코드 생산에 있는 것이다. 필요 없이 연산자오버로딩을 남용하는 것은 그리 좋은 습관이 아니다.

또한 모든 클래스들이 연산자 오버로딩을 사용해야 하는 것은 아니다. 문자열 클래스처럼 간편하게 사용할 수 있는 유틸리티 성격의 클래스들에게는 매우 유용하지만 일반적인 클래스들은 연산자 오버로딩을 필요로 하지 않는 경우가 대부분이다.

Comments