연산자 override

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

형식만 알고 있자.

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

타입 변환 연산자 override

아래와 같은 래퍼 (Wrapper) 객체를 만들었다고 하자.

class Int
{
  int data;
 public:
  Int(int data) : data(data) {}
  Int(const Int& i) : data(i.data) {}
};
operator int() { return this.data; }

이렇게 해 두면, 아래 코드가 수행이 된다.

Int x = 3;      // Wrapper 객체
int a = x + 4;  // int 로 자동 casting 되어 a 가 계산된다.

그 외의 연산자 override

T& operator++() { .. }

전위 증감 연산자 (++x)는 이렇게 레퍼런스를 반환해야 한다. 전위 증감 연산자는 보통 증감 이후의 변수 자체를 다시 받기 때문이다. 그리고 당연히 실제 인자가 없다.

T operator++(int) { .. }

후위 증감 연산자 (x++)는 실제 인자가 없어 보이지만, 존재한다. 하지만 정말로 값이 들어오는 건 아니므로 인자 하나를 아무거나 명시해 주는 것이다. 또 하나의 차이점은 레퍼런스가 아닌 값을 반환한다. 후위 증감이므로, 변하기 전의 값을 줘야 한다.

T& operator[](const int index);

인덱스 참조 연산자 (첨자 연산자, str[10]) 의 경우에는 인자가 정수다. 반환하는 값은 특정 타입이나 특정 클래스의 레퍼런스를 반환해야 한다. char[] 라면 char& 를 반환하는 것 처럼.

ostream& operator<<(ostream& os, const T& t);

cin, cout 에 쓰는 입출력 연산자도 오버라이딩이 된다. cin 이면 당연히 istream 을 쓰면 된다. 봐서 알겠지만 이 연산자는 binary (이항) 이다. 따라서 왼쪽의 stream 객체, 오른쪽에 원하는 객체나 값을 넣는다.

상속

자식 클래스 (Child) 의 생성자엔 반드시 초기화 리스트에 부모 클래스 (Parent) 의 생성자를 호출해야 한다.

그런데 Child 가 Parent 의 함수를 호출하면, 함수가 바라보는 멤버 변수는 Child 일까 Parent 일까 고민을 맨날 했었을 것이다. 답은 Parent 의 것이다.

 class Parent {
  string s;

 public:
  Parent() : s("부모") { cout << "부모 클래스" << endl; }
  void what() { cout << s << endl; }
};

class Child : public Parent {
  string s; // Parent 의 것과 다르다.

 public:
  Child() : Parent(), s("자식") {
    cout << "자식 클래스" << endl;
    what(); // parent 의 what 을 호출한다.
  }
};

일단 이렇게 하면 Parent/Child 구조가 이렇게 된다. 두 s 가 구별되는 것을 주의하자.

./img/cpp.png

출력은 이렇다.

부모 클래스
자식 클래스
부모
  1. Parent() 를 초기화 리스트에서 호출했기 때문에, 부모 클래스가 먼저 뜬다.
  2. 이후, Child 의 생성자에서 자식 클래스를 출력한다.
  3. what() 에서 s 멤버 변수를 출력하는데, 이게 Child 의 것이 아닌 Parent 의 것이다. ‘부모’ 가 출력된다.

Child 의 s 값을 출력하는 what() 을 원한다면, 새로 정의하면 된다.


class Child : public Parent {
  string s;

 public:
  Child() : Parent(), s("자식") {
    cout << "자식 클래스" << endl;
    what(); // child 의 what 을 호출한다.
  }
  void what() { cout << s << endl; }
};

가장 가까운 what() 을 호출한다. 나중에 배울 키워드 (virtual, override) 를 안 쓴다면 두 함수는 이름은 같지만 엄연히 ‘다른’ 함수가 된다. 이걸 오버라이딩 (overriding) 이라고 한다. 오버로딩 아니다!

protected

Child 에서 Parent 멤버 변수에 접근 못 한다. 그렇다고 Parent 를 public 멤버 변수로 쓸 수도 없으면, protected 를 쓰면 된다. 이건 아니까 넘어가자.

다형성

Java 에서의 다형성이나 C++ 에서의 다형성이나 그 구현 방식은 큰 차이가 없다.

Up-casting

Child sChild = Child();
Parent *sParentPtr = &sChild;
Parent &sParentRef = sChild; // 역시 가능

이러면 sParentPtr 이 가리키는 Child 객체는 Parent 로 인식된다. sParentRef 도 가능하다. 부모 클래스로 캐스팅하면 업 캐스팅이라고 한다. 다운 캐스팅은 (당연히) 불가능하다. Fruit is not an apple!

업 캐스팅 된 변수를 다시 다운 캐스팅하면, 그러니까 Child 객체를 가지고 있는 Parent 포인터 변수를 다시 Child 포인터 변수로 받으면 에러가 당연히 난다. 하지만 암묵적으로 우리가 그렇게 해달라고 요청하기 때문에 강제 캐스팅을 할 수 있는 방법이 있긴 하다. (Child *sReChildPtr = static_cat<Child*>(sParentPtr))) 알아만 두고, 쓰진 않는게 건강에 이롭다.

Virtual

업 캐스팅된 포인터 변수/레퍼런스에서, Parent/Child 가 따로 구현한 같은 시그니처의 함수를 호출하면? 항상 Parent 의 함수가 호출된다. 이를 피하기 위해서 Parent 함수 앞에 virtual 을 붙이면, Child 객체의 경우에 한해서는 Child 객체의 함수가 수행된다.

virtual void what() { cout << "부모 클래스의 what()" << endl; }

virtual 은, 상속받을 객체가 멤버 함수를 재정의’할 수 있다’는 여지를 열어주는 키워드다. 따라서 실제 수행 전에 해당 객체가 원래 객체인지, 상속받은 객체인지 체크한다. 재정의된 함수를 가진 상속받은 객체라면 그 함수를 호출해 준다. (재정의 안 되어 있으면 그냥 부모 클래스 것이 호출된다. 당연하다.)

이 키워드가 없으면, 업 캐스팅된 레퍼런스에서 상속받은 객체 함수를 호출할 수 없다.

Override

‘똑같은’ 함수를 부모 클래스와 자식 클래스가 선언한 경우라면, 부모 클래스의 virtual 선언만으로도 업 캐스팅 오버라이드가 된다. (그냥 오버라이드는 부모 클래스 함수가 아닌 내 함수를 호출하는 거고)

그런데 override 키워드는 실수로 오버라이딩을 못 하고 구별된 함수로 남게 되는 부분을 컴파일 타임에 감지할 수 있는 키워드다. 똑같다면 괜찮지만, 사람은 실수할 수 있으므로 양쪽에 virtualoverride 를 줘서 실제로 오버라이딩 될 함수가 있는지, 가능한지 여부를 다 따져야 한다.

void incorrect() const override { cout << "자식 클래스 " << endl; }

소멸자의 가상함수

상속해 줄 부모 클래스는, 반드시 소멸자를 가상함수로 만들어야 한다.

왜냐하면 업 캐스팅으로 관리할 때 자원 해제를 위해 소멸자가 수행되면, 자식 클래스의 객체들은 독자적인 메모리 영역 해제를 할 수 없는 상황에 빠지기 때문이다.

여기서 안 해도 되는 일을 적자면,

Virtual 심화편

Java 는 모두 virtual 이 붙어있다. 하지만 virtual 을 붙이면 vtable 이 생겨 한번 더 실제 함수를 찾는 과정이 생겨나므로, C++ 처럼 필요할 때만 만드는게 좋다.

순수한 가상 함수

class Animal {
  virtual void speak() = 0;
}

speak() 함수는 구현부가 존재하지 않는다. 따라서 다음 특징이 있다.

순수 가상 함수를 포함한 이런 Animal 같은 클래스를 추상 클래스 (abstract class) 라고 한다.

class Dog : public Animal {
  void speak() override { cout << "멍멍" << endl; }
}

다중 상속

class C : public A, public B {
  ...
}

초기화 리스트의 순서와는 상관없이, 오로지 상속받는 순서 (위에서는 A -> B) 대로 부모 클래스 생성자가 호출되는 점을 상기해두자. (별로 중요한 건 아님)

그런데 이렇게 잘 안 쓴다.

상속 키워드 의미

class Child : public Parent

한 가지 사실. 접근 지시자가 생략되면 무조건 private 이다. 따라서 이 구문은 실행이 안 된다. 오로지 public 이어야지 액세스 가능하다고 판단한다. (당연한 것이 private / protected 로 상속받은 자식 클래스는, 물려받은 public 멤버가 전부 가려지기 때문이다. 그래서 캐스팅 자체가 실패할 수 밖에..)

class Child : Parent {
  ...
}
int main() {
  Child child;
  Parent &sParentRef = child; // 액세스할 수 없는 기본 클래스 "Parent"(으)로의 변환은 허용되지 않습니다.
}