[C++] 연산자 오버로딩(1)

muchan
muchan
∙∙조회 3

https://modoocode.com/202

위 사이트에서 공부한 내용 정리입니다


연산자 오버로딩이란

연산자 오버로딩이란 기존 연산자가 사용자 정의 자료형에서도 동작하도록 기능을 정의하는 것이다.

연산자 오버로딩은 직접 만든 클래스도 기본 자료형처럼 연산자를 사용할 수 있게 하기 위해서 사용한다.

예를 들어 두 객체를 더할 때 a.add(b) 대신 a + b로 표현할 수 있어 코드가 더 직관적이고 읽기 쉬워진다.

예제 코드

arduino
#include <iostream>

class Complex {
 private:
  double real, img;

 public:
  Complex(double real, double img) : real(real), img(img) {}
  Complex(const Complex& c) { real = c.real, img = c.img; }

  Complex operator+(const Complex& c) const;
  Complex operator-(const Complex& c) const;
  Complex operator*(const Complex& c) const;
  Complex operator/(const Complex& c) const;

  void println() { std::cout << "( " << real << " , " << img << " ) " << std::endl; }
};

Complex Complex::operator+(const Complex& c) const {
  Complex temp(real + c.real, img + c.img);
  return temp;
}
Complex Complex::operator-(const Complex& c) const {
  Complex temp(real - c.real, img - c.img);
  return temp;
}
Complex Complex::operator*(const Complex& c) const {
  Complex temp(real * c.real - img * c.img, real * c.img + img * c.real);
  return temp;
}
Complex Complex::operator/(const Complex& c) const {
  Complex temp(
    (real * c.real + img * c.img) / (c.real * c.real + c.img * c.img),
    (img * c.real - real * c.img) / (c.real * c.real + c.img * c.img));
  return temp;
}

int main() {
  Complex a(1.0, 2.0);
  Complex b(3.0, -2.0);

  Complex c = a * b;

  c.println();
}

선언할 때는 operator(연산자) (매개변수) 형식으로 정의한다

이때 매개변수가 변하지 않고, operator 내부의 값이 변하지 않는다면 const로 정의하는게 좋다

연산자 함수의 반환값을 Complex&로 하지 않고 Complex로 한 이유는

만약 a = b + c + b 라는 식이 있을 때 Complex로 반환하면 값 복사라서 정상적으로 계산되는데

Complex&일 땐 a = b + c + b + c가 되기 때문이다(b+c하면 b=b+c가 되기 때문)

대입 연산자 함수

ini
Complex& operator=(const Complex& c);

대입 연산자는 위처럼 정의한다

대입 연산자는 대입 이후 불필요한 값 복사를 막기 위해 Complex&로 선언한다

하지만 대입 연산자는 정의하지 않아도 정상적으로 동작한다고 한다

왜냐면 컴파일러 차원에서 디폴트 대입 연산자를 지원하기 때문이다

하지만 디폴트 대입 연산자는 얕은 복사를 수행하므로 깊은 복사가 필요한 경우에 대입 연산자를 정의해야 한다

ini
Some_Class a = b; // 1

Some_Class a; // 2
a = b;

둘의 차이는 1은 복사 생성자가 호출 되는 것이고 2는 생성자가 호출된 이후 대입 연산이 실행되는 것이다

대입 사칙연산 함수

kotlin
Complex& operator+=(const Complex& c);
Complex& operator-=(const Complex& c);
Complex& operator*=(const Complex& c);
Complex& operator/=(const Complex& c);

Complex& Complex::operator+=(const Complex& c) {
  (*this) = (*this) + c;
  return *this;
}
Complex& Complex::operator-=(const Complex& c) {
  (*this) = (*this) - c;
  return *this;
}
Complex& Complex::operator*=(const Complex& c) {
  (*this) = (*this) * c;
  return *this;
}
Complex& Complex::operator/=(const Complex& c) {
  (*this) = (*this) / c;
  return *this;
}

대입 연산자 사칙연산 함수는 다음처럼 정의할 수 있다

대입 연산자 사칙연산 함수는 내부에서 값을 변화시키므로 const 함수로 선언할 수 없다

주의

연산자 오버로딩을 사용하게 되면 a+=b와 a=a+b가 같다고 보장되지 않는다.

컴파일러는 operator+와 operator=를 정의했다고 해서 a+=b를 자동으로 a = a+b로 바꿔주지 않는다

반드시 operator+=를 따로 만들어야 +=를 사용할 수 있게 되는 것이다

비슷한 예로 ++, --를 +=1, -=1로 바꿔주지 않는 것과 같다

추가

위의 사이트에서 문자열을 받아서 복소수로 변환하는 생성자를 선언했다

rust
Complex::Complex(const char* str)

그러면 문자열로 받아서 위에서 만든 연산자 오버로딩을 이용해서 아래처럼 쉽게 작성할 수 있다

arduino
Complex Complex::operator+(const char* str) const {
  Complex temp(str);
  return (*this) + temp;
}
Complex Complex::operator-(const char* str) const {
  Complex temp(str);
  return (*this) - temp;
}
Complex Complex::operator*(const char* str) const {
  Complex temp(str);
  return (*this) * temp;
}
Complex Complex::operator/(const char* str) const {
  Complex temp(str);
  return (*this) / temp;
}

하지만 여기서 바로 위에서 정의한 4개 함수들을 지워도 정상적으로 실행된다

ini
a = a + "-1.1 + i3.923"; // 1

a = a.operator+("-1.1 + i3.923"); // 2

컴파일러가 1번 식을 2번 식으로 자동으로 변환해주기 때문이다

그래서 문자열을 Complex형으로 바꿔주는 생성자만 만들어주면 정상적으로 동작한다

하지만 여기서

arduino
Complex& Complex::operator+(const Complex& c)

operator+의 인자가 const Complex& c가 아닌 Complex& c면 동작하지 않는다고 한다

왜냐하면 문자열 자체가 리터럴이라서 변하지 않는다는 const 특징이 보장되어 있어야 변형해준다는 것이다

그래서 뭐든지 변하지 않는 특징을 갖는다면 const를 붙여주는 게 좋다고 한다