객체지향프로그래밍(26) - 오버로딩의 모든 것

explicit, mutable 키워드 이해

Featured image

🔚 짧게 하는 복습

✅ 1. explicit 키워드의 정의와 활용을 안다.

✅ 2. mutable 키워드의 정의와 활용을 안다.

혹시 기억이 안 난다면, 다시 돌아가자


우리는 22강부터 25강까지 오버로딩 및 형변환에 다루었다.

그러나 = 연산자의 오버로딩에 대해서만 아직 다룬 적이 없다.

그 이유는 = 연산자의 오버로딩은 묵시적 형 변환을 확실히 배운 후 다루기가 더 쉽다고 생각했기 때문이다.

그리고 마지막으로 오버로딩을 할 때, 주의할 점을 일반적으로 다루어보겠다.


같은 연산자, 다른 의미

우선 첫 번째 이유= 연산자는 C++에서 두 개의 의미가 있기 때문이다.

C에서는 대입 연산자의 의미만 있었지만, C++에서는 생성자의 역할을 같이 하기 때문이다.

우선 아래의 코드를 보자.

#include <iostream>
using namespace std;

class alpha
{
    private:
    int data;

    public:
    alpha() {} //디폴트 생성자
    alpha(int _data) : data(_data) {} // 1변수 생성자

    void display() {} // 변수 출력함수
};

void alpha::display(){
    cout << data;
};

int main()
{
  alpha a1(37);

  alpha a2;
  a2 = a1;
  // C++의 클래스에서는 이러한 연산식도 된다.

  alpha a3 = a1;
  // 그렇다면 위 코드와 이 코드의 =은 같을까? 다를까?

  return 0;
}

첫 번째 코드는 a1의 값을 a2에 = 연산자를 이용해 대입한다는 뜻이고, 두 번째 코드는 = 연산자를 이용해 복사 생성자를 호출한다는 뜻이다. (묵시적 형 변환)

그렇다면 위의 = 연산자아래 = 연산자는 같을 연산자로 취급될까? 아닐까?

이를 확인하기 위해, = 연산자를 오버로딩해보겠다.

위의 코드는 오버로딩 = 연산자가 호출되었지만, 아래는 그렇지 않은 모습이다.

이게 어떻게 된 일일까?


복사 생성자는 대입 연산자가 아니다.

  alpha a3 = a1;

이 코드가 오버로딩 = 연산자가 호출되지 않는 이유는, 간단하게 말해서 묵시적 형변환에서 호출되는 복사 생성자대입 연산자와 다르기 때문이다.

= 연산자를 오버로딩하면, 오롯이 대입의 역할에서만 사용되는 대입 연산자만 오버로딩이 된다.

반면에, 위 코드에서 = 연산자복사 생성자의 다른 형태일 뿐이다. 즉, 아래와 같다.

  alpha a3 = a1;
  //alpha a3(a1); 혹은
  //alpha a3 = alpha(a1);

그렇기에, = 연산자를 오버로딩했을 때 의도대로 사용하고 싶다면 복사 생성자 호출을 주의해야 한다.


복사 생성자의 쓰임 정리

대입 연산자값을 복사해서 대입하는 역할이라고 이미 C언어부터 쭉 배워왔다.

그렇다면 복사 생성자는 어떨 때 쓰이는지 자세히 알아보자.

우선 함수의 값에 의한 호출(call by value)에서 사용된다.

어떤 함수의 매개 변수가 어떠한 객체의 참조자가 아니고 객체라면, 즉 아래의 코드와 같다면

//void foo(alpha& a); 얘가 아니라
void foo(alpha a); // 얘라면

자연스럽게 복사 생성자를 통해서 alpha a에 값이 복사된다.

두 번째는, 값을 반환할 때도 반환형이 객체의 참조자가 아니라 객체라면, 마지막에 함수 내에서 계산된 값복사 생성자를 통해서 변수에 복사가 된다.

//alpha& foo(int a); 얘가 아니라
alpha foo(int a); // 얘라면

그렇기에 매개 변수 혹은 반환형으로 참조자를 사용하지 않는다면 소멸자를 주의해야한다.

원하지 않는 소멸자의 호출로 버그가 발생할 수 있다.


= 연산자를 오버로딩할 때 주의할 점(1)

첫 번째, 대입 연산자와 복사 생성자 모두 정의할 것

위에서는 대입 연산자를 오버로딩했을 경우, 복사 생성자의 호출을 조심하라고 했다.

하지만 현실적으로 복사 생성자를 모두 금지할 것이 아니라면, 언제든 생각지도 못한 곳에서 버그가 발생할 수 있다.

또, 가장 최악인 점은 컴파일, 런타임에도 오류가 없는 오작동이 일어날 수 있다.

그렇기에 복사 생성자도 대입 연산자처럼 따로 정의를 해줘서, 어떤 경우에서 오작동이 일어난 줄 알게끔 짜는 것이 좋다.


= 연산자를 오버로딩할 때 주의할 점(2)

두 번째는, 둘 중 하나를 금지시켜버리는 것이다.

어떻게 금지하냐고 물을 수 있는데, 생각보다 간단하다.

외부에서 참조할 수 없게 만들면 된다. 이는 private 영역에서 정의해버리면 된다는 뜻을 의미한다.

class alpha
{
private:
  alpha& operator = (alpha&); //대입 연산자 금지
  alpha(alpha&); //복사 생성자 금지
};

위 코드에서는 private에서 모두 정의하며, 대입 연산자와 복사 생성자를 금지시켜버렸다.


= 연산자를 오버로딩 할 때 주의할 점(3)

세 번째는, = 연산자는 오버로딩해도 상속이 되지 않는다.

기본적으로 부모 클래스에서 연산자를 오버로딩한 경우, 해당 연산자는 하위 클래스에서도 동일한 방식으로 동작한다.

또 메소드처럼, 하위 클래스에서 별도로 연산자를 다시 오버로딩하는 경우, 하위 클래스의 오버로딩된 연산자가 호출된다.

하지만 대입 연산자는, 부모 클래스에서 오버로딩된 경우에도 하위 클래스로 상속되지 않는다.

이는 대입 연산자가 특수한 동작을 수행하며, 기본적으로 클래스의 멤버 대 멤버 복사를 수행하기 때문인데, 오버로딩된 = 연산자은 해당 클래스에 대해서만 적용되며 하위 클래스로 상속되지 않는다.

따라서 하위 클래스에서 계속 사용하고 싶다면 다시 대입 연산자를 다시 오버로딩해야 한다.


마지막, 연산자 오버로딩할 때 주의할 점

아래에서 다룰 내용은 이는 = 연산자에 국한되는 것이 아니라 다른 연산자나 함수를 사용할 때도 대부분 중요하게 이용된다.

첫 번째, 참조에 의한 호출(call by reference)을 이용할 것

값에 의한 호출(call by value)모든 변수의 값을 복사하는 무거운 작업에 비해, 참조에 의한 호출(call by reference)참조자로 가져오면 되기에 훨씬 가볍다.

두 번째, this 포인터참조자 반환형을 이용해 체이닝을 가능하게 할 것

사실 이는 앞 전 this 포인터 강의에서도 사용된 적이 있는데, this 포인터의 몇 가지 장점 중 체이닝이라는 것이 있었다.

잠깐 복기하자면, 체이닝하나의 객체에서 아래처럼 메소드를 연속적으로 호출하는 것이다.

a1.plus().minus().plus();

이를 대입 연산자에 적용하면 아래와 같은 작업이 가능해진다.

a3 = a2 = a1;

훨씬 유연하고 확장성 있는 코드를 작성할 수 있게 한다.


📖 오늘의 핵심(다 알기 전까지는 넘어가지 말자❗)

✅ 1. = 연산자와 복사 생성자의 차이를 안다.

✅ 2. 복사 생성자의 쓰임을 안다.

✅ 3. = 연산자와 복사 생성자의 오버로딩 시 주의사항을 안다.

✅ 4. 오버로딩을 할 때 주의사항을 안다.

⚠️ = 연산자와 복사 생성자의 오버로딩 시 둘 다 정의하는 것을 추천한다.

⚠️ = 연산자는 오버로딩해도 상속되지 않는다.

💣 과제, 앞에서 배운 다른 연산자의 오버로딩도 오늘 배운 내용을 적용해보자 (난이도 中)