본문으로 바로가기

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

category 개발자과정준비/C++ 2021. 6. 7. 09:03
반응형

연산자 오버로딩

 

기본형의 연산자

클래스는 일종의 타입이다.

기본형에서 가능한 모든 동작은 객체에 대해서도 가능해야하며 객체끼리 연산도 할 수 있어야한다.

#include <stdio.h>

class Time
{
private:
	int hour, min, sec;

public:
	Time() {}
	Time(int h, int m, int s) { hour = h; min = m; sec = s; }
	void OutTime()
	{
		printf("%d:%d:%d\n", hour, min, sec);
	}

	const Time AddTime(const Time &other) const
	{
		Time t;
		t.sec = sec + other.sec;
		t.min = min + other.min;
		t.hour = hour + other.hour;

		t.min += t.sec / 60;
		t.sec %= 60;
		t.hour += t.min / 60;
		t.min %= 60;
		return t;
	}
};

int main()
{
	Time t1(1, 10, 30);
	Time t2(2, 20, 40);
	Time t3;

	t3 = t1.AddTime(t2);
	t3.OutTime();
}

AddTime 멤버 함수는 인수로 Time 객체를 받아 현재 객체와 더해 새로운 시간을 리턴하는 것을 볼 수 있다.

연산결과를 저장하기위해 임시 객체 t를 선언하고 현재 객체(this)와 인수로 전달받은 other의 대응되는 시간 요소끼리 더한다. 초는 초끼리, 분은 분끼리 더해 t의 멤버에 저장하고 자리 넘침을 처리한다.

 

상식에따르면 40초와 30초를 더하면 1분 10초가 되어야한다. 

main에서 1시 10분 30초의 시간을 가지는 t1 객체와

             2시 20분 40초의 시간을 가지는 t2 객체를 더해서 t3에 합산하여 출력했다.

 

AddTime 함수는 대응되는 시간 요소를 더하고 자리넘침까지 처리하여 시간 객체끼리 더해주는 함수이다.

 

클래스도 기본형과 완전히 같아지려면 일반 함수가 아닌 연산자 함수를 정의해야한다.

단, 연산자는 보통 +, -, *, / 같은 기호로 정의한다.

int a = 10, b = 20;

int c = a + b;   처럼
Time t1(1, 10, 30);

Time t2(2, 20, 40);

Time t3;

t3 = t1 + t2;  가 성립되어야 한다.

이런 명칭을 명칭 규칙상 함수명으로 쓸 수 없기 때문에 앞에 operator 키워드를 붙이고 연산자 기호를 함수명으로 사용하여 함수를 정의할 수 있다.

 

#include <stdio.h>

class Time
{
private:
	int hour, min, sec;

public:
	Time() {}
	Time(int h, int m, int s) { hour = h; min = m; sec = s; }
	void OutTime()
	{
		printf("%d:%d:%d\n", hour, min, sec);
	}

	// operator 키워드를 붙이고 '+'를 함수 이름으로 쓰는것임
	// 앞의 const는 출력값이 변경되면 안된다는 의미.
	// 뒤의 const는 멤버 변수들을 상수화 시켜줌.
	const Time operator + (const Time& other) const  
		// 다른 객체를 불러오는데 객체가 매개변수로 사용되려면 레퍼런스로 갖고와야함
	{
		// 뒤의 const를 붙였는데도 값들이 변경되고, 에러도 안뜸 => t1의 hour, min, sec를 상수화시키는 것임

		// Time t는 지금 여기있는 함수안에서 선언한 것이라 상수화가 적용안됨.
		// other로 가져오는 값은 t2의 값들임

		Time t;
		t.sec = sec + other.sec;
		t.min = min + other.min;
		t.hour = hour + other.hour;

		t.min += t.sec / 60;
		t.sec %= 60;

		t.hour += t.min / 60;
		t.min %= 60;
		return t;     // t의 타입은 Time
	}
};

int main()
{
	Time t1(1, 10, 30);
	Time t2(2, 20, 40);
	Time t3;

	t3 = t1 + t2;  // 객체 + 객체로 표기가 가능해짐.
	// t3 = t1.operator+(t2);   // 위에랑 똑같은 코드임.
	// t3 = t1.AddTime(t2);     // 일반 함수 호출할때 코드
	t3.OutTime();
}

AddTime 함수명을 operator + 로 바꾸었다.

이제 함수 호출 구문으로 두 객체를 더하는게 아니라 그냥 연산문으로 두 객체를 더할 수 있다.

t1 + t2 연산문에대해 컴파일러는 Time 객체끼리 더하는 함수를 찾는데 Time 클래스에 이를 처리할 operator + 함수를 찾는다.

 

연산자 함수의 동작은 일반 함수인 AddTime과 완전히 같지만 호출문이 더 간결하다.

t1 + t2 라는 표현식이 두 객체를 더한다는 것을 잘 표현하며 누가봐도 직관적이고 가독성도 높을 것이다.

또한 연산자는 우선수위와 결합 순서의 적용도 받기 대문에 연산 순서가 자동으로 결정된다.

 

연산자 함수의 형식

시간 객체끼리 더하는 operator + 연산자를 만들어보고 사용해봤는데, 함수의 형태가 복잡해서 직접 만들거나 파악하기가 어려워보인다.

함수 하나 만드는데 const도 많이 보이고 반환값의 타입은 int같은 타입이 아니라 객체 Time으로 되어있다. 

두 객체의 연산함수를 만들기위해 완전히 같은 타입으로 만들어주고 동작해야하는데 왜 이런 형태를 띠는지 하나씩 살펴보자.

 

 

리턴 타입

연산자 함수의 인수를 피연산자로 사용된다. 덧 셈은 두 개의 피연산자를 취한다.

좌변은 이 연산자를 호출하는 자신(this)로 정해져있으니 우변의 객체만 전달 받는다.

 

논리적 연산이 가능하려면 피연산자의 타입이 제한이 없지만 완전히 일치하는 타입이어야한다.

Time 개체는 정수, 실수 등과 더할 수 있지만 문자열이나 포인터와 더하는 것은 에러가 뜰 것이다.

객체를 인수로 받는 방법은 값, 포인터, 레퍼런스 3가지가 있다.

- 값으로 남겨도 연산은 가능하지만 객체는 통째로 값을 넘겨버리면 속도가 느리다는 치명적인 단점이있다.

- 포인터는 4바이트만 전달되니 값으로 넘기는것보다는 효율적이다.

하지만 동작에는 문제없지만 호출부가 이상해진다. 포인터를 넘겨야하니 연산문이 다음 형식이어서 일반적인 형태와 맞지 않고 직관적이지 못하다. 특히, 정수를 더할때 a = B + &c;형태로 쓰지 않으니 적합하지 못하다고 볼 수 있다.

t3 = t1 + &t2;

값은 느리고, 포인터는 연산문이 비상식적인데 이 두 문제를 깔끔하게 해결할 수 있는 방법이 레퍼런스이다.

레퍼런스로 넘기면 실제로는 주소가 전달되어 빠르고 호출문에 암시적으로 &를 붙여주므로 표기법도 기본형과 같다.

 

예제의 other 인수는 const 지정자가 붙은 상수 레퍼런스로 선언하였다.

이항 연산자는 피연산자를 읽기만 할뿐 변경하지 않으며, a+b 연산 후에 b의 값은 그대로이다.

피연산자는 const 지정자를 붙여 변경할 수 없도록 제한하며 상수 객체에 대해서도 이 연산자를 사용할 수 있다.

a = b + 3 이 가능한 것처럼 const Time t2로 선언된 객체도 더할 수 있어야 한다.

 

 

호출 객체의 상수성

인수뿐만아니라 연산자를 호출하는 객체도 상수로 받는다.

a + b 연산에서 a도 값을 읽기만 할 뿐 덧셈 후에 변경되지 않는다. 그래서 함수 자체가 호출 객체를 건드리지 않는다는 것을 분명히 하기위해 함수 끝에 const 지정자를 붙였다.

const int a = 1;
int b = 2;
int c = a + b;

좌변의 a가 상수로 선언되어있어도 값을 바꾸려는게 아니라 다른 값에서 덧셈할때만 쓰이며, 덧셈 후에도 a의 값은 변하지 않을 것이다.

 

객체도 마찬가지로 상수 객체를 피연산자로 쓰려면 함수가 상수성을 가져야한다.

좌우변의 피연산자가 모두 상수이므로 덧셈에 사용할 임시 객체가 필요하며 이런 목적으로 결과 저장을 위한 임시 객체 t를 선언해 사용했다.

 

전역 함수로 구현

p1 + p2

1. p1.operator + (p2)

2. operator + (p1, p2)

 

전역 연산자 함수

객체끼리 연산하는 함수는 클래스안의 멤버로 캡슐화하는 것이 무난하지만 인수만 제대로 전달한다면 클래스 외부의 전역함수로 작성할 수도 있다.

객체 자신이 피연산자가 되든 인수로 전달받든 연산에 필요한 피연산자를 구할 수만 있으면 된다.

// 파일이름 : TimeOpPlus.cpp
#include <stdio.h>

class Time
{
	friend const Time operator +(const Time& me, const Time& other);
private:
	int hour, min, sec;

public:
	Time() {}
	Time(int h, int m, int s) { hour = h; min = m; sec = s; }
	void OutTime()
	{
		printf("%d:%d:%d\n", hour, min, sec);
	}
};

const Time operator +(const Time& me, const Time& other)
{
	Time t;
	t.sec = me.sec + other.sec;
	t.min = me.min + other.min;
	t.hour = me.hour + other.hour;

	t.min += t.sec / 60;
	t.sec %= 60;
	t.hour += t.min / 60;
	t.min %= 60;
	return t;
}

int main()
{
	Time t1(1, 10, 30);
	Time t2(2, 20, 40);
	Time t3;

	t3 = t1 + t2;
	t3.OutTime();
}

Time 클래스 바깥에 operator + 일반 함수를 전역으로 작성했다.

피연산자 두 개를 me, other이라는 이름의 인수로 전달받아 두 객체를 더해 리턴한다.

더하는 논리는 같은데 연산 대상을 별도의 인수로 받는다는 점만 다르다.

 

외부에 존재하는 연산자 함수가 대상 클래스의 멤버를 자유롭게 읽으려면 friend 선언이 필요하며 이럴때 사용하는 것이 프렌드 지정이다. TIme은 operator + 를 멤버로 포함하지 않지만 friend 지정을 통해 엑세스 권한을 부여한다.

프렌드 선언을 빼면 외부 함수에서 숨겨진 멤버를 참조할 수 없어 연산이 불가하다.

 

멤버 함수나 전역 함수나 소속만 다를 뿐 연산하는 코드나 호출하는 방법은 똑같다.

대신 외부에서 클래스의 멤버변수들을 접근하기위해 프렌드 키워드를통해 친구관계를 만들어야하는 불편함도있다.

 

별도의 연산자 기호를만들어서 오버로딩을 할수는없다.

사칙연산 연산자만 오버로딩이 가능.(sizeof나 다른 연산자는 거의 불가능)

 

피연산자 중 하나는 사용자 정의형이어야하고 기본 타입끼리 연산하는 방법은 이미 컴파일러에 이미 규정되어있어서 오버로딩할 수 없다.

 

 

객체와 기본형의 연산

함수가 임의 타입의 인수를 받을 수 있듯이 연산자 함수가 받는 피연산자의 타입도 제약이 없다.

객체끼리 연산할 수도 있고 객체와 호환되는 기본형과 연산할 수도 있다.

현재 시간에서 100초후가 언제인지 알고 싶다면 시간 객체에 정수 100을 더해주면된다.

Time 객체에 정수를 더하고 싶다면 operator +(int) 연산자 함수를 정의한다.

// 파일이름 : TimePlusInt.cpp
#include <stdio.h>

class Time
{
private:
	int hour, min, sec;

public:
	Time() {}
	Time(int h, int m, int s) { hour = h; min = m; sec = s; }
	void OutTime()
	{
		printf("%d:%d:%d\n", hour, min, sec);
	}

	const Time operator +(int s) const
	{
		Time t = *this;
		t.sec += s;

		t.min += t.sec / 60;
		t.sec %= 60;
		t.hour += t.min / 60;
		t.min %= 60;
		return t;
	}
};

int main()
{
	Time now(11, 22, 33);
	now.OutTime();
	now = now + 1;
	now.OutTime();
}

임시 객체 t를 this의 사본으로 생성한 후 t.sec에 인수로 받은 s초를 더한다.

간단하게 1초만 증가시켜봤는데 100초나 1000초를 더해도 잘 동작하게 될 것이다.

 

Time 객체끼리 뿐만 아니라 호환되는 타입과 연산이 가능해져 Time이 기본형에 더욱 가까워진 것 같다.

그러나 아직 문제가 있는데, 교환 법칙이 성립하는 덧셈은 피연산자의 순서를 바꿔도 잘 동작해야 할 것이다.

int형은 다음 두가지 연산문이 모두 가능하다.

int a = 3, b;
b = a + 1;
b = 1 + a;

수학적 상식에 의하면 a + 1 이든 1 + a든 같아야할 것이다.

아직 위예제의 Time 클래스에는 now + 1은 잘 연산하지만 1 + now는 수행할 수 없다.

1 + now를 수행할 수 없는 이유는 이 수식을 처리할 수 있는 함수가 정의되어있지 않기 때문이다.

 

1 + now가 가능하려면 1의 소속 클래스인 int에 Time 객체를 받는 연산자 함수가 있어야 한다.

그러나 내장 타입인 int는 임의로 확장할 수 없으며 따라서 연산자 함수를 추가하는 것은 불가능하다.

첫번째 인수의 소속 클래스인 int에 멤버 함수를 추가할 수 없기 때문에 어쩔 수 없이 (int, Time)순으로 인수를 받는 전역연산자 함수가 필요하다.

// // 파일이름 : TimePlusInt2.cpp
#include <stdio.h>

class Time
{
	friend const Time operator +(int s, const Time& me);
private:
	int hour, min, sec;

public:
	Time() {}
	Time(int h, int m, int s) { hour = h; min = m; sec = s; }
	void OutTime()
	{
		printf("%d:%d:%d\n", hour, min, sec);
	}
	
};

const Time operator + (int s, const Time& me)
{
	Time t = me;

	t.sec += s;

	t.min += t.sec / 60;
	t.sec %= 60;
	t.hour += t.min / 60;
	t.min %= 60;
	return t;
}

int main()
{
	Time now(11, 22, 33);
	now.OutTime();
	now = 1 + now;
	now.OutTime();
	now = now + 1;
	now.OutTime();
}

operator + (int, Time) 전역 함수를 추가하여 정수에 시간 객체를 더한다.

Time 클래스는 외부함수에 대해 프렌드로 지정한다. 정수와 Time 객체를 받는 함수가 정의되었으니 1 + now 연산이 가능해졌다.

 

그러나 1 + now는 가능하지만 이번에는 now + 1은 처리하지 못한다.

결국 두 연산이 모두 가능해지려면 멤버함수든 전역함수든 int, Time 순의 함수와 Time, int 순의 함수가 모두 필요하다.

앞의 두 예제에서 연산하는 논리는 같으니 예제를 합쳐보자.

// 파일이름 : TimePlusInt3.cpp
#include <stdio.h>

class Time
{
private:
	int hour, min, sec;

public:
	Time() {}
	Time(int h, int m, int s) { hour = h; min = m; sec = s; }
	void OutTime()
	{
		printf("%d:%d:%d\n", hour, min, sec);
	}

	const Time operator +(int s) const
	{
		Time t = *this;

		t.sec += s;

		t.min += t.sec / 60;
		t.sec %= 60;
		t.hour += t.min / 60;
		t.min %= 60;
		return t;
	}
};

const Time operator +(int s, const Time& me)
{
	return (me + s);
}


int main()
{
	Time now(11, 22, 33);
	now.OutTime();
	now = 1 + now;
	now.OutTime();
	now = now + 1;
	now.OutTime();
}

멤버 함수가 실제 코드를 제공하고 전역 함수는 (int, Time) 순으로 인수로 받아 (Time + int)로 순서를 바꿔 넘긴다.

이렇게 하면 전역 함수가 Time 클래스의 멤버를 직접 읽지 않으므로 프렌드 지정을 생략해도 무방하다.

 

반대로 전역 함수가 코드를 제공하고 멤버 함수는 순서만 바꿔 전역 함수를 호출해도 상관없다.

하지만 외부에서 클래스의 멤버를 읽으려면 프렌드 선언이 필요해 번거롭고 클래스 관련 코드는 가급적 내부에 캡슐화하는 것이 합당하므로 교환법칙 성립을위한 연산자 코드 작성은 TimePlusInt3 예제가 효율적이라고 볼 수 있다.

 

오버로딩의 예시

앞에서는 가장 대표적인 연산자 + 에 대한 오버로딩을 집중적으로 팠다.

이제 다양한 연산자를 오버로딩해보려고 한다.

 

관계연산자

관계연산자를 객체끼리 비교하게 만들어보자.

두 개의 Time 객체가 같은지, 어떤 객체가 더 큰지 비교 결과를 리턴하여 객체 비교식을 조건문이나 반복문에 바로 사용할 수 있다. 통상 같은 타입의 객체끼리 비교하므로 멤버 연산자 함수로 정의하는 것이 간편하다.

// 파일이름 : TimeRelation.cpp
#include <stdio.h>

class Time
{
private:
	int hour, min, sec;

public:
	Time() {}
	Time(int h, int m, int s) { hour = h; min = m; sec = s; }
	void OutTime()
	{
		printf("%d:%d:%d\n", hour, min, sec);
	}

	bool operator == (const Time& other) const
	{
		return (hour == other.hour && min == other.min && sec == other.sec);
	}

	bool operator != (const Time& other) const
	{
		return !(*this == other);
	}

	bool operator > (const Time& other) const
	{
		if (hour > other.hour) return true;
		if (hour < other.hour) return false;
		if (min > other.min) return true;
		if (min < other.min) return false;
		if (sec > other.sec) return true;
		return false;
	}

	bool operator >= (const Time& other) const
	{
		return (*this == other || *this > other);
	}

	bool operator < (const Time& other) const
	{
		return !(*this >= other);
	}

	bool operator <= (const Time& other) const
	{
		return !(*this > other);
	}
};

int main()
{
	Time t1(12, 34, 56);
	Time t2(12, 34, 56);

	if (t1 == t2)
	{
		puts("두 시간은 길다");
	}
	else
	{
		puts("두 시간은 다르다");
	}

	if (t1 > t2)
	{
		puts("t1이 더 크다.");
	}
	else
	{
		puts("t1이 더 작다.");
	}
}

비교연산자는 논리가 비슷해서 전체를 묶음으로 오버로딩했다.

== 비교가 가능하다면 != 비교도 당연히 가능해야할 것이다.

 

관계연산자는 진위여부를 판단하므로 출력타입을 bool로 반환하게 구현했다.

 

두 시간 객체가 같은지 비교하는 == 연산자는 시, 분, 초 요소가 모두 일치하는지 각각 비교하여 && 논리 연산자로 연결한다. 

!= 연산자는 세 요소중 하나라도 다른지 점검하면 되는데 새로 만들필요없이 == 연산의 결과를 반대로 뒤집어 리턴한다.

좌변이 더 큰지 검사하는 > 연산자는 코드는 길지만 논리는 굉장히 단순하다. 가장 큰 단위인 시간을 먼저 비교해보고 좌변 객체의 시간이 더 크면 참이고 더 작으면 거짓을 리턴한다. 이때 크지도않고 작지도 않다면 시간이 같다는 뜻이므로 다음 단위인 분을 비교하고 분까지 같으면 초까지 비교한다.

 

>연산자의 일차원의 과정이 번거롭다면 절대초로 바꾼 후 비교하는 간편한 방법도 있다.

int operator > (const Time &T) const
{
	return(hour * 3600 + min * 60 + sec > other.hour * 3600 + other.min * 60 + other.sec);
}

==과 >가 정의되면 나머지 연산자는 두 연산의 조합으로 정의한다.

크거나 같다는 ==와 > 연산을 ||로 묶으면 된다는 것이 상식이다. 그러나 다음 두 연산자는 주의가 필요하다.

< 의 반대 조건 : 크거나 같다.
<= 반대 조건 : 크다
반응형