연산자 오버로딩이란, 말그대로 연산자에 파라미터를 다르게 주어 그 계산 방법을 다르게 만드는 것을 의미합니다.
연산자 오버로딩을 사용하는 이유는 무엇일까요?
우리가 객체를 사용하기 전까지는 변수를 선언할 때, 내장되어있는 자료형을 사용하였습니다. 이 때 이 자료형들간의 연산 방법 또한 내장되어 있는 라이브러리를 통해 사용하였습니다.
하지만 우리가 새로 선언해준 자료형들간의 연산을 할 때에는 컴파일러가 어떻게 그 연산을 처리해야 하는지 모르고 있겠죠? 따라서 우리가 그 방법을 정해주어야 할 것입니다.
예시
이해를 돕기 위해서 간단한 예시를 먼저 소개하고 출발하도록 하겠습니다. 예를 들어, 복소수를 나타내는 Complex
라는 클래스가 있다고 생각해 봅시다. 이 클래스에는 실수부를 표현하는 int real;
, 허수부를 표현하는 int img
의 멤버 변수가 있습니다.
이때 이 Complex
로 객체 a
와 b
를 만들었다고 가정해보면, a+b는 어떻게 연산이 되어야 할까요?
당연히 실수부는 실수부끼리 허수부는 허수부끼리 계산이 되어야겠죠?
이렇게 우리가 새로만든 객체들간의 계산을 정의해줄 때 유용하게 사용할 수 있는 방법이 연산자 오버로딩입니다.
연산자 오버로딩을 공부하기 전에 연산자의 종류에 대해서 먼저 알아봅시다.
1항 연산자는 계산을 할 때 단 하나의 항만 있으면 되는 연산자를 의미합니다.
예를들면, a++
과 같은 연산이 있겠죠
2항 연산자는 계산을 할 때 단 하나의 항만 있으면 되는 연산자를 의미합니다.
예를들면, a += 1
과 같은 연산이 있겠죠
3항 연산자는 계산을 할 때 단 하나의 항만 있으면 되는 연산자를 의미합니다.
예를들면, a++ 과 같은 연산이 있겠죠
연산자를 오버로딩하는 방법에는 한가지 규칙이 있습니다.
함수의 이름을 operator
연산자 이름
의 형태로 정해야 한다는 것입니다.
예를들면 +
연산자를 오버로딩하기 위해서는 함수 이름을 operator+
라고 정해야 합니다.
2항 연산자를 멤버함수로 오버로딩하는 방법은 간단합니다.
바로, 앞의 항에 해당하는 객체(클래스)에 그 연산자를 오버로딩 하면 되겠습니다.
class Complex
{
public:
Complex operator+(const complex& right)
{
int real = this->real + right.real;
int img = this->img + right.img;
return Complex(real, imag);
}
private:
int real;
int img;
};
즉, Complex
로 선언한 객체 a
, b
, c
가 있을 때 다음 두 코드는 동일하게 작동하게 됩니다.
c = a + b;
c = a.operator+(b);
1항 연산자의 경우 2항 연산과는 다르게 연산 대상이 오른쪽에 있냐 왼쪽에 있냐가 정해져있지 않습니다.
예를들어, 2항 연산의 경우에는 a+b
처럼 양쪽 모두에 연산 대상이 존재하게 되지만 1항 연산의 경우에는 a++
이나 --a
와 같이 연산 대상이 둘중 한 곳에만 존재하게 됩니다.
따라서 1항연산은 2가지 정의 방법이 존재합니다.
전치연산의 방법(연산후에 값을 반환)
전치연산의 경우 아래와 같이 함수의 파라미터에 아무런 값을 넣어주어서는 안됩니다.
class Complex
{
public:
Complex operator++()
{
this->real++;
this->img++;
return *this;
}
private:
int real;
int img;
};
후치연산의 방법(값을 반환한 후에 연산)
후치연산의 경우 아래와 같이 함수의 파라미터 정의를 하나 해주어야 합니다.
class Complex
{
public:
Complex operator++(int)
{
Copmplex prev(this->real, this->img);
this->real++;
this->img++;
return prev;
}
private:
int real;
int img;
};
전역 함수로 연산자를 오버로딩하기 전에 반드시 생각해야 할 점이 존재합니다.
바로 private
키워드 인데요, 멤버함수로 정의할 경우에는 클래스의 멤버변수에 접근할 때 내부에서 접근하는 것이어서 딱히 신경쓸 필요가 없었습니다.
하지만 전역함수로 연산자를 정의할 경우에는 외부에서 멤버 변수에 접근하는 것이기 때문에 별도의 처리가 없으면 접근이 불가능합니다.
이때 사용할 수 있는것이 바로 friend
키워드 인데요, 어떤 함수와 friend
를 맺을지 그 클래스 안에 friend
해당 함수프로토 타입
의 방법으로 정의해 주면 해당 함수에서는 그 클래스의 모든 멤버에 접근이 가능해집니다.
2항 연산자를 전역함수로 오버로딩하는 방법은 간단합니다.
바로, 앞의 항에 해당하는 객체(클래스)에 그 연산자를 오버로딩 하면 되겠습니다.
class Complex
{
public:
Complex operator+(const complex& right)
{
int real = this->real + right.real;
int img = this->img + right.img;
return Complex(real, imag);
}
private:
int real;
int img;
};
즉, Complex
로 선언한 객체 a
, b
, c
가 있을 때 다음 두 코드는 동일하게 작동하게 됩니다.
c = a + b;
c = a.operator+(b);
1항 연산자의 경우 2항 연산과는 다르게 연산 대상이 오른쪽에 있냐 왼쪽에 있냐가 정해져있지 않습니다.
예를들어, 2항 연산의 경우에는 a+b
처럼 양쪽 모두에 연산 대상이 존재하게 되지만 1항 연산의 경우에는 a++
이나 --a
와 같이 연산 대상이 둘중 한 곳에만 존재하게 됩니다.
따라서 1항연산은 2가지 정의 방법이 존재합니다.
전치연산의 방법(연산후에 값을 반환)
전치연산의 경우 아래와 같이 함수의 파라미터에 아무런 값을 넣어주어서는 안됩니다.
class Complex
{
public:
Complex operator++()
{
this->real++;
this->img++;
return *this;
}
private:
int real;
int img;
};
후치연산의 방법(값을 반환한 후에 연산)
후치연산의 경우 아래와 같이 함수의 파라미터 정의를 하나 해주어야 합니다.
class Complex
{
public:
Complex operator++(int)
{
Copmplex prev(this->real, this->img);
this->real++;
this->img++;
return prev;
}
private:
int real;
int img;
};
+, -, <<, =(중요)
연산자 오버로딩은 특별한 경우가 아니면 연산 과정에서 받아온 값들을 변경하지 말하야겠죠?
또 객체는 그 크기가 크기 때문에 Call-by-Value
방법으로 가져오기 보다는 Call-by-Reference
의 방법으로 가져와야 한다는 사실을 항상 잊지말도록 합시다.
따라서 parameter를 받을 때에는 웬만하면, 별도의 의도가 없을경우에는 const
를 포함한 Call-by-Reference
의 방법으로 받도록 합시다.
void Complex::operator+(const Complex& right) { ~ }
return 값이 없을경우
사실 없어도 상관 없음, 하지만
return 값이 있을경우
임시 객체를 반환하기 Complex operator+()
&
반환된 임시객체가 새로 생성하는 객체일 경우, 복사생성자를 통해 해당 객체에 전해지고, 이미 만들어진 객체일 경우 대입연산을 통해 대입되게 된다.
ostream& operator<<(ostream& os, const Media& right) {
return os;
}
// ostream& os = cout;
// const Media& right = right;
return 에 & 함 => 불필요한 복사를 막음
parameter: 앞에만 const가 붙지 않는 이유 -> ostream 객체는 const할수 없는 객체(아마 내부에서 변경하는 사항이 존재하는 듯)
뒤에 파라미터: 만약 주소를 받아오는 경우는 const Media* right임을 조심
return os: 외부에서 받아온 별명을 외부로 다시 넘겨주기 위해서는 &를 써야함
cout << c1 << c2
== (cout << c1) << c2
== cout << c2
이름처럼 구현하기
오버로딩할 때 피연산자들중 적어도 하나는 객체
., ::, ? :, sizeof 는 불가능
-> ::
를 사용 x
-> 사용 이유: 교환법칙(p+10 == 10+p), 내부 라이브러리
내부 라이브러리: cout << 3;
== cout.operator<<(3);
하지만 사용자 정의 자료형에 대한 내용은 추가할 방법이 없음
기본적으로 객체에 대한 대입연산자는 정의되어 있음
전역함수의 위치
전역함수의 위치는 항상 .cpp
파일, 즉 구현파일에 들어가야 한다는 것을 잊지맙시다.