C++ 문법(기법) 3. 연산자 오버로딩

Ui Jin·2021년 10월 22일
0

C++ Grammar

목록 보기
12/13

연산자 오버로딩

연산자 오버로딩이란, 말그대로 연산자에 파라미터를 다르게 주어 그 계산 방법을 다르게 만드는 것을 의미합니다.

연산자 오버로딩을 사용하는 이유는 무엇일까요?

우리가 객체를 사용하기 전까지는 변수를 선언할 때, 내장되어있는 자료형을 사용하였습니다. 이 때 이 자료형들간의 연산 방법 또한 내장되어 있는 라이브러리를 통해 사용하였습니다.

하지만 우리가 새로 선언해준 자료형들간의 연산을 할 때에는 컴파일러가 어떻게 그 연산을 처리해야 하는지 모르고 있겠죠? 따라서 우리가 그 방법을 정해주어야 할 것입니다.

예시
이해를 돕기 위해서 간단한 예시를 먼저 소개하고 출발하도록 하겠습니다. 예를 들어, 복소수를 나타내는 Complex라는 클래스가 있다고 생각해 봅시다. 이 클래스에는 실수부를 표현하는 int real;, 허수부를 표현하는 int img의 멤버 변수가 있습니다.

이때 이 Complex로 객체 ab를 만들었다고 가정해보면, a+b는 어떻게 연산이 되어야 할까요?

당연히 실수부는 실수부끼리 허수부는 허수부끼리 계산이 되어야겠죠?

이렇게 우리가 새로만든 객체들간의 계산을 정의해줄 때 유용하게 사용할 수 있는 방법이 연산자 오버로딩입니다.


연산자의 종류

연산자 오버로딩을 공부하기 전에 연산자의 종류에 대해서 먼저 알아봅시다.

  • 1항연산자

    1항 연산자는 계산을 할 때 단 하나의 항만 있으면 되는 연산자를 의미합니다.

    예를들면, a++ 과 같은 연산이 있겠죠

  • 2항연산자

    2항 연산자는 계산을 할 때 단 하나의 항만 있으면 되는 연산자를 의미합니다.

    예를들면, a += 1 과 같은 연산이 있겠죠

    • ``
  • 3항연산자

    3항 연산자는 계산을 할 때 단 하나의 항만 있으면 되는 연산자를 의미합니다.

    예를들면, a++ 과 같은 연산이 있겠죠

연산자 오버로딩 방법

함수의 이름

연산자를 오버로딩하는 방법에는 한가지 규칙이 있습니다.

함수의 이름을 operator 연산자 이름의 형태로 정해야 한다는 것입니다.

예를들면 +연산자를 오버로딩하기 위해서는 함수 이름을 operator+라고 정해야 합니다.

1. 멤버함수 사용

  • 2항 연산자

    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항 연산자

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

2. 전역함수 사용

전역 함수로 연산자를 오버로딩하기 전에 반드시 생각해야 할 점이 존재합니다.

바로 private키워드 인데요, 멤버함수로 정의할 경우에는 클래스의 멤버변수에 접근할 때 내부에서 접근하는 것이어서 딱히 신경쓸 필요가 없었습니다.

하지만 전역함수로 연산자를 정의할 경우에는 외부에서 멤버 변수에 접근하는 것이기 때문에 별도의 처리가 없으면 접근이 불가능합니다.

이때 사용할 수 있는것이 바로 friend키워드 인데요, 어떤 함수와 friend를 맺을지 그 클래스 안에 friend 해당 함수프로토 타입의 방법으로 정의해 주면 해당 함수에서는 그 클래스의 모든 멤버에 접근이 가능해집니다.

  • 2항 연산자

    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항 연산자

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

+, -, <<, =(중요)

주의점

함수의 parameter, return

parameter

연산자 오버로딩은 특별한 경우가 아니면 연산 과정에서 받아온 값들을 변경하지 말하야겠죠?
또 객체는 그 크기가 크기 때문에 Call-by-Value방법으로 가져오기 보다는 Call-by-Reference의 방법으로 가져와야 한다는 사실을 항상 잊지말도록 합시다.

따라서 parameter를 받을 때에는 웬만하면, 별도의 의도가 없을경우에는 const를 포함한 Call-by-Reference의 방법으로 받도록 합시다.

void Complex::operator+(const Complex& right)
{ ~ }

return

  1. return 값이 없을경우
    사실 없어도 상관 없음, 하지만

  2. 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);
하지만 사용자 정의 자료형에 대한 내용은 추가할 방법이 없음

<<의 경우 inline 설정을 해줘야 함?

디폴트 대입연산자

기본적으로 객체에 대한 대입연산자는 정의되어 있음

위치

전역함수의 위치
전역함수의 위치는 항상 .cpp파일, 즉 구현파일에 들어가야 한다는 것을 잊지맙시다.

profile
github로 이전 중... (https://uijinee.github.io/)

0개의 댓글