minkylee

[CPP02] 연산자 오버로딩 본문

대외활동/42SEOUL

[CPP02] 연산자 오버로딩

minkylee 2024. 5. 14. 14:02
모두의 코드 <연산자 오버로딩> 을 보고 정리한 내용
https://modoocode.com/202
 

씹어먹는 C++ - <5 - 1. 내가 만든 연산자 - 연산자 오버로딩>

모두의 코드 씹어먹는 C++ - <5 - 1. 내가 만든 연산자 - 연산자 오버로딩> 작성일 : 2013-08-25 이 글은 93283 번 읽혔습니다. 에 대해서 다룹니다. 안녕하세요 여러분! 지난 강좌에서 만들었던 MyString 을

modoocode.com

 

사용자 정의 연산자

C++ 에서는 사용자 정의 연산자를 사용할 수 있다. 

  • +, -, * 와 같은 산술 연산자
  • +=, -= 와 같은 축약형 연산자
  • >=, == 와 같은 비교 연산자
  • &&, || 와 같은 논리 연산자
  • -> 나 * 와 같은 멤버 선택 연산자 (*는 역참조 연산자)
  • ++, -- 증감 연산자
  • [] (배열 연산자), () (함수 호출 연산자)

이러한 기본 연산자들을 직접 사용자가 정의하는 것연산자를 오버로딩 한다고 부른다.

 

연산자 오버로딩을 사용하기 위해서는, 오버로딩을 원하는 연산자 함수를 제작하면 된다.

 

(리턴 타입) operator (연산자) (연산자가 받는 인자)

 

위 방법 외에는 함수 이름으로 연산자를 절대 넣을 수 없다. 예를 들어서 우리가 == 를 오버로딩 하고 싶다면, == 연산자는 그 결과값이 언제나 bool 이고, 인자로는 == 로 비교하는 것 하나만 받게 된다.

 

bool operator==(MyString &str);

 

우리가 str1 == str2 명령을 한다면 이는 str1.operator==(str2)로 내부적으로 변환되어 처리된다.

 

bool MyString::operator==(MyString &str) {
	return !compare(str); // str 과 같으면 compare에서 0을 리턴한다.
}

 

 

복소수 클래스 예제

 

복소수는 허수와 실수의 합으로 표현할 수 있다. 예를 들어 임의의 복소수 z는 다음과 같다.

 

$ z = a + bi $

 

class Complex {
  private:
    double real, img;

  public :
    Complex(double real, double img) : real(real), img(img) {}
};

 

위와 같은 복소수 클래스를 이용해 복소수의 사칙연산을 구현해보자! (복소수의 사칙 연산은 실수부와 허수부 따로 진행된다)

 

만일 다음과 같이 연산자의 오버로딩을 모른다고 가정하고 Complex 클래스를 구성해보자

 

class Complex {
  private:
    double real, img;

  public :
    Complex(double real, double img) : real(real), img(img) {}
    
    Complex plus(const Complex& c);
    Complex minus(const Complex& c);
    Complex times(const Complex& c);
    Complex divide(const Complex &c);
};

 

 

만약 int형 변수였다면

 

a + b / c + d

 

로 간단하게 쓸 수 있었던 명령을

 

a.plus(b.divide(c)).plus(d);

 

와 같이 복잡한 함수식을 이용해서 표현해야 한다. 정말 복잡하다.

 

하지만 연산자 오버로딩을 이용해서 plusoperator+divideoperator/ 로, 등등 바꿔준다면 단순히 프로그래머가

 

a + b / c + d 로 쓴다고 해도 컴파일러가 알아서 a.operator+(b.operator/(c)).operator(d);로 변환시켜서 처리하기 때문에 속도나 다른 면의 어떠한 차이 없이 뛰어난 가독성과 편리함을 얻을 수 있게 된다.

 

 

위를 바탕으로 아까 정의했던 Complex class를 다시 만들어보자

 

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;
}

 

 

여기서 가장 중요하게 봐야 할 부분은 바로, 사칙연산 연산자 함수들의 리턴타입이다. 

 

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

 

 

위 4개의 연산자 함수 모두 Complex가 아닌 Complex를 리턴하고 있다.

 

Complex& operator+(const Complex& c) {
  real += c.real;
  img += c.img;
  return *this;
}

// 이렇게 설계하였을 경우 Complex를 리턴하는 연산자 함수는 값의 복사가 일어나기 때문에 속도 저하가 발생하지만
// 위처럼 레퍼런스를 리턴하게 되면 값의 복사 대신 레퍼런스만 복사하는 것이므로 큰 속도의 저하는 나타나지 않는다.

 

하지만 문제가 발생한다.

 

Complex a = b + c + b

 

 

이런 수식이 있을 경우 실제로 처리될 때 (b.plus(c)).plus(b) 가 되는데 b.plus(c)를 하면서 b에는 (b + c) 가 들어가고 거기에 다시 plus(b) 를 하게 된다면 현재 b에는 (b + c) 가 들어가게 되기 때문에 결과적으로 b + c + b + c 가 된다. 

 

이러한 문제를 막기 위해서는 반드시 사칙 연산의 경우 값을 리턴해야 한다.

 

 

또한 함수 내부에서 읽기만 수행되고 값이 바뀌지 않는 인자들에 대해서는 const 키워드를 붙여주는 것이 바람직하다.

 

operator+ 의 경우 c의 값을 읽기만 하고 c의 값에 어떠한 변화도 주지 않기 때문에 const Complex& 타입으로 인자를 받았다. 

 

또한 이 버전의 operator 들은 객체 내부의 값을 변경하지 않기 때문에 상수 함수로 선언한다.

 

 

대입 연산자 함수

 

Complex& operator=(const Complex& c);

 

기본적으로 대입 연산자 함수는, 기존의 사칙연산 연산자 함수와 다르게  자기 자신을 가리키는 레퍼런스를 리턴한다.

 

a = b = c;

 

위와 같은 코드에서 b = c; 가 b를 리턴해야지 a = b; 가 성공적으로 수행될 수 있기 때문에

 

이 때 Complex 타입을 리턴하지 않고 굳이 Complex& 타입을 리턴하냐면, 대입 연산 이후에 불필요한 복사를 방지하기 위해서이다.

 

이와 같은 사실을 바탕으로 operator= 함수를 완성시켜보면 아래와 같다.

 

 

Complex& Complex::operator=(const Complex& c)

{
  real = c.real;
  img = c.img;
  return *this;
}

// main 예제

int main() {
  Complex a(1.0, 2.0);
  Complex b(3.0, -2.0);
  Complex c(0.0, 0.0);
  c = a * b + a / b + a + b;
  c.println();
}

// 실행 결과 : (10.9231, 4.61538)

 

굳이 operator= 를 만들지 않더라도, 위 소스를 컴파일 하면 잘 작동한다.

 

컴파일러 차원에서 디폴트 대입 연산자 (default assignment operator)를 지원하고 있기 때문이다. 

 

디폴트 대입 연산자는 얕은 복사를 수행한다. 따라서 깊은 복사가 필요한 클래스의 경우 (예를 들어, 클래스 내부적으로 동적으로 할당되는 메모리를 관리하는 포인터가 있다던지) 대입 연산자 함수를 꼭 만들어주어야 할 필요가 있다.