개발하는 리프터 꽃게맨입니다.

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

언어/C, C++

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

파워꽃게맨 2024. 1. 6. 19:12

오버로딩

이름은 같지만 매개변수가 다르며 같은 유효 범위 내에 있는 함수를 '오버로딩'한다고 합니다.

 

다음 코드를 볼까요?

 

void SayHello(int num)
{
	cout << "Hello Int " << num << endl;
}

void SayHello(float num)
{
	cout << "Hello Float " << num << endl;
}

void SayHello(string str)
{
	cout << "Hello String " << str << endl;
}

int main()
{
	SayHello(10);
	SayHello(1.23f);
	SayHello("CrabMan");
}

 

같은 'SayHello'가 존재하지만, 다른 매개변수가 들어가 있으므로

호출하는 함수에 어떤 매개변수를 넣는가에 따라서 호출되는 함수가 달라집니다.

 

 

오버로딩을 할 때 주의점은

반드시 비슷한 일을 수행하는 함수에 대해서만 오버로딩을 수행해야 한다는 것입니다.

 

만약, 같은 이름의 함수를 오버로딩 했는데, 서로 하는 일이 다르다면 분명히 나중에 사용함에 있어서

예상치 못한 실행이 발생할 수밖에 없죠.

 

연산자 오버로딩

1. 개요

 

연산자 오버로딩이란 객체들끼리의 연산을 정의하는 것입니다.

예를 들어서 2차원 좌표를 나타내는 Point 클래스가 있다고 가정합니다.

 

class Point
{
public:
	Point(int x, int y)
		: _x(x), _y(y) {};

private:
	int _x;
	int _y;
};

 

만약, 2개의 Point를 연산하려면 어떻게 해야 할까요? 

가장 단순한 방법은 아마 이런 식으로 하는 것입니다.

 

int main()
{
	Point p1(10, 20);
	Point p2(15, 5);

	Point p3(0, 0);
	p3._x = p1._x + p2._x;
	p3._y = p1._y + p2._y;
}

일단 이 코드에는 문제점이 있습니다.

 

1) _x, _y가 private로 지정돼 있으면 불가능한 방법입니다.

Get, Set 함수를 지정해서 해결할 수 있습니다만, 가독성이 그렇게 좋지는 않을 것 같습니다.

 

2) 직관적이지 않습니다.

 

p3 = p1 + p2와 같이 직관적이게 정의할 수 있다면, 너무나도 좋겠죠.

근데 C++에서는 기본적인 연산자를 오버로딩할 수 있도록 지원합니다.

 

	Point operator+(const Point& other) const
	{
		int x = _x + other._x;
		int y = _y + other._y;

		return Point(x, y);
	}

 

Point 클래스 내부에 이런 oprator 오버로딩 함수를 정의하면

마치 기본적인 사칙연산을 하는 것처럼 클래스를 다룰 수 있게 됩니다.

 

	Point p1(10, 20);
	Point p2(15, 5);

	Point p3 = p1 + p2;

 

 

2. 정의하는 방법

Point operator+(피연산자 매개변수)
{
	/* 코드 */
}

 

기본적으로는 이런 꼴로 생겼습니다.

 

반환형은 함수의 목적에 따라 달라질 것이고,

operator 뒤에 오버로딩 하고픈 연산자를 붙이면 됩니다.

그리고 반드시 '매개변수'가 필요합니다,

 

존재하는 연산자만 오버로딩 할 수 있으므로

** 나 # 같은 기호를 사용할 수는 없습니다.

 

참고로, 매개변수는 개발자 재량껏 어떤 방식으로 받을지 정할 수 있는데,

보통은 안전성과 공간절약을 위해 

const Point& other

처럼 const 참조 형식으로 받습니다.

 

그리고 + 연산처럼, 클래스 내부 멤버 변수를 바꾸지 않는다면

const 함수를 사용하는 것이 좋습니다.

 

아래 여러 가지 연산자 오버로딩의 예시를 소개하겠습니다.

 

연산자 오버로딩의 다양한 예시

1. Point 좌표 클래스에 대한 연산자 오버로딩 예시

class Point
{
public:

	Point(int x, int y)
		: _x(x), _y(y) {};

	Point operator+(const Point& other) const
	{
		int x = _x + other._x;
		int y = _y + other._y;

		return Point(x, y);
	}

	Point operator-(const Point& other) const
	{
		int x = _x - other._x;
		int y = _y - other._y;

		return Point(x, y);
	}

	void operator+=(const Point& other) /*const를 사용하지 않음*/
	{
		_x += other._x;
		_y += other._y;
	}

	void operator-=(const Point& other) /*const를 사용하지 않음*/
	{
		_x -= other._x;
		_y -= other._y;
	}

	bool operator==(const Point& other) const
	{
		return _x == other._x && _y == other._y;
	}

	bool operator!=(const Point& other) const
	{
		return _x != other._x || _y != other._y;
	}

private:
	int _x;
	int _y;
};

 

단순하게 좌표 계산을 지원하는 연산자 오버로딩에 대한 예시입니다.

이 정도만 해줘도 코딩에 있어서 가독성 및 편의성이 배로 늘어납니다.

 

2. 비교 연산자 오버로딩

class Goods
{
public:
	Goods(int price, string name)
		: price(price), name(name) {}

	bool operator>(const Goods& other) const { return price > other.price; }
	bool operator<(const Goods& other) const { return price < other.price; }

public:
	int price;
	string name;
};

int main()
{
	Goods g1(1500, "Cookies");
	Goods g2(800, "Chocolate");

	if (g1 > g2)
	{
		cout << g1.name << " is more expensive.";
	}else
		cout << g2.name << " is more expensive.";

}

 

위 예시가 좋은 예시인지는 모르겠지만

예를 들어 상점에 갔을 때 물품을 비교할 때 가격으로 비교하고 싶다면,

위 예시처럼 비교 연산자를 오버로딩해서 간편하게 사용할 수 있습니다.

 

이런 비교연산자 오버로딩은 구조체나 클래스를 Priority_queue를 통해 관리하고자 할 때,

사용되는 기법입니다.

이런 상황뿐만 아니라 다양한 경우에서 사용될 수 있습니다.

 

3. [] 오버로딩

#include <iostream>
using namespace std;

class IntArray
{
public:
	
	int& operator[](int idx)
	{
		return elements[idx];
	}

public:
	int elements[100];
};

int main()
{
	IntArray arr;
	arr[0] = 10;
	arr[1] = 70;
	arr[5] = 40;

	cout << arr[0] << endl;
	cout << arr[1] << endl;
	cout << arr[5] << endl;

}

 

만약 사용자 지정 자료구조를 만들 경우, 배열처럼 [index]를 통해 요소에 접근하고 싶다면

[] 연산자 오버로딩을 통해서 위와 같이 접근할 수 있습니다.

 

참조& 형태로 반환하기에, 진짜 배열처럼 인덱스에 원하는 값을 대입할 수도 있습니다.

 

6. 비멤버 함수 연산자 오버로딩

class Point
{
	friend ostream& operator<<(ostream& os, const Point& other);

public:

	Point() {}

	Point(int x, int y)
		: _x(x), _y(y) {};

private:
	int _x;
	int _y;
};

ostream& operator<<(ostream& os, const Point& other)
{
	os << "(" << other._x << ", " << other._y << ")";

	return os;
}

int main()
{
	Point p(10, 20);
	cout << p << endl;
}

 

외부 함수로 연산자 오버로딩을 할 수도 있습니다.

위 함수는 

cout << 객체

형태로 바로 출력할 수 있도록 오버로딩한 예시입니다.

 

단, 외부 함수의 경우 private 멤버에 접근하지 못하므로

friend를 이용해서 접근 권한을 열어줘야만 합니다.

 

friend는 다른 클래스가 자신의 private, protected 멤버에 접근할 수 있도록 허용해 주는 문법인데,

자세한 것은 다른 포스팅에서 다루도록 하겠습니다.

 

7. 대입 연산자

#include <iostream>
using namespace std;

class Point
{
public:

	Point() {}

	Point(const Point& other)
		: _x(other._x)
		, _y(other._y)
	{
	
		cout << "난 복사야" << endl;
	}

	Point(int x, int y)
		: _x(x), _y(y) {};

	Point operator=(const Point& other)
	{
		_x = other._x;
		_y = other._y;
		cout << "난 대입이야" << endl;

		return Point(_x, _y);
	}

private:
	int _x;
	int _y;
};

int main()
{
	Point p1(10, 20);
	Point p2 = p1; //복사 생성자 호출
	Point p3;
	p3 = p2; //대입 연산자 호출
}

 

복사 생성자와 대입 연산자는 생긴 모습이 비슷하기 때문에 많이 혼돈하실 수 있습니다.

복사 생성자는 생성자이기에 '초기화' 할 때 호출되는 문법이고,

대입 연산자는 '대입' 할 때 호출되는 문법입니다.

 

대입 연산자 역시 복사 생성자처럼 컴파일러가 자동으로 생성해 줍니다.

단, 대입 연산자는 기본적으로 '얕은 복사'와 동일한 방법으로 진행되기 때문에,

동적 메모리 할당을 사용한다면, '깊은 복사'와 같은 처리를 직접 개발자 단에서 해주어야만 합니다.

 

연산자 오버로딩을 남용하지 말 것

기본 의미와 일관된 정의를 사용해야 한다는 것입니다.

+ 를 오버로딩했는데, 단순히 더하는 작업만 해야지 추가적인 작업을 해주면

나중에 실수가 일어날 확률이 증가합니다.

 

추가적인 작업이 필요한 경우라면, 차라리 함수를 사용하는 것이 옳습니다.

 

마치며

연산자 오버로딩은 굉장히 자주 쓰이는 기법이기 때문에

자유자재로 사용할 수 있어야만 합니다.