클래스의 상속 -2

·2022년 6월 2일
0

cpp_study

목록 보기
11/25

가상 함수와 다형성

is-a와 has-a

is-a 관계

모든 상속 관계는 is-a 관계이다.
이를 뒤바꾸면 성립되지 않는다.

클래스가 파생되면 파생될 수록 좀 더 특수화가 된다.
반대로, 기반 클래스로 거슬러 올라가면 올라갈 수록 좀 더 일반화된다.

has-a 관계

또 다른 클래스 간의 관계로, has-a 관계 역시 존재한다.
has-a 관계는 보통 아래와 같이 객체를 프로퍼티로 가지는 경우를 말한다.

class Car {
private:
  Engine e;
  Brake b; // 아마 break 아니냐고 생각하는 사람들이 있을 텐데 :)
};

업 캐스팅

파생 클래스의 객체를 기반 클래스의 포인터로 가리키는 것
-> 파생 클래스의 객체를 기반 클래스의 객체처럼 다룰 수 있게 함

따지고 보면 Derived is a Base 이기 때문에,
Base 객체를 가리키는 용도의 포인터(p_c라고 하자)가 Derived 객체(c라고 하자)도 가리킬 수 있다.

Base* p_c = &c;

이를 그림으로 표현하면 아래와 같음.

대신 p_c는 엄연한 Base 객체를 가리키는 포인터임.
따라서 p_c는 Base 클래스의 what함수를 호출한다(Derived의 what 함수를 호출할 것 같지만 아니다).
그렇기 때문에 Base 클래스에 what함수가 존재해야 한다.

다운 캐스팅

반대로, 아래와 같이 다운 캐스팅을 시도하고 what()을 호출한다면 에러가 뜬다.

Base* p_p = &c;
Derived* p_c = p_p;
p_c->what();

그러나 우리는 what이 오버라이딩된 메소드이기 때문에 현재 상태에서는 저렇게 호출해도 아무 문제 없다는 것을 잘 안다.
따라서 아래와 같이 강제적으로 타입 변환을 할 수 있다.
static_cast가 아닌 dynamic_cast로 해야 함!

이럴 경우, 컴파일 오류를 발생시키지 않고 성공적으로 컴파일할 수 있다.
그러나 강제적으로 다운 캐스팅을 하는 경우 컴파일 타임에서 오류를 찾아내기 매우 힘들기 때문에 권장하지 않음!

virtual 키워드

#include <iostream>
class Base {
public:
  Base() { std::cout << "기반 클래스" << std::endl; }
  virtual void what() { std::cout << "기반 클래스의 what()" << std::endl; } };
  class Derived : public Base { public:
  Derived() : Base() { std::cout << "파생 클래스" << std::endl; } void what() { std::cout << "파생 클래스의 what()" << std::endl; }
};

int main() {
   Base p;
   Derived c;
   Base* p_c = &c;
   Base* p_p = &p;
  std::cout << " == 실제 객체는 Base == " << std::endl; p_p->what();
  // 기반 클래스의 what()
  std::cout << " == 실제 객체는 Derived == " << std::endl; p_c->what();
  // 파생 클래스의 what()
  return 0; 
}

따라서 p_p -> what()과 p_c-> what()을 하면 모두 Base 객체의 what() 함수가 실행되어서 둘다 기반이라고 출력이 되어야만 함.
그런데 이때 virtual 키워드로 동적 바인딩을 행하게 한다.

동적 바인딩 = 컴파일 시에 어떤 함수가 실행될 지 정해지지 않고 런타임 시에 정해지는 일을 가리킴

즉, p_c는 Base 포인터: Base의 what 실행하려다가 -> virtual 키워드 발견
-> 실제 Base 객체가 아니라는 걸 확인(Derived 객체) -> Derived의 what을 실행

virtual 소멸자

Parent 클래스를 상속 받는 Child 클래스의 객체가 소멸할 때
1. Parent 생성자 호출
2. Child 생성자
3. Child 소멸자
4. Parent 소멸자
순으로 호출됨

그러나 Parent 포인터가 Child 객체를 가리킬 때는 다른 양상을 보임

std::cout << "--- Parent 포인터로 Child 가리켰을 때 ---" << std::endl; {
  Parent *p = new Child();
  delete p;
}

delete p를 해도 사실상 Child 객체이기 때문에(p는) Child 소멸자가 호출이 되어야 하는데, 호출되지 않음.

-> 메모리 누수(memory leak)와 같은 문제가 생김

문제 해결: Parent의 소멸자를 virtual로 만들어버리면 된다.

그러면 p가 소멸자를 호출할 때 Child의 소멸자를 성공적으로 호출할 수 있음.

virtual ~Parent() { std::cout << "Parent 소멸자 호출" << std::endl; } };

레퍼런스도 가능

레퍼런스 역시 가능하다.

가상 함수의 구현 원리

함수를 virtual로 선언했을 때(즉 가상 함수를 사용할 때) 약간의 오버헤드가 발생

이 오버헤드가 생기는 과정을 이해해보자!

class Parent { 
public:
  virtual void func1();
  virtual void func2(); 
};

class Child : public Parent {
public:
  virtual void func1();
  void func3(); 
};

c++ 컴파일러는 가상 함수 테이블을 만듦.

가상 함수 테이블 = 함수의 이름 - 실제 어떤 함수가 대응되는 지에 대한 테이블

이를 그림으로 나타내면 다음과 같다.

가상함수 호출할 경우: 가상 함수 테이블을 한 단계 더 거쳐서, 실제로 어떤 함수를 고를 지 결정함

Parent* p = Parent();
p->func1();

위 예에서 컴파일러는

  1. p -> Parent를 가리키는 포인터, func1()의 정의를 Parent 클래스에서 찾아봐야 함
  2. func1()이 가상함수, func1()을 직접 실행하지 않고 가상 함수 테이블에서 func1()에 해당하는 함수를 실행해야 함

++ p-> Child 객체를 가리킨다면(Parent 포인터), Child가 가리키는 가상 함수 테이블을 따라가서 그에 해당하는 함수를 실행함.

이와 같이 행동을 함.
-> 즉 2번에 걸쳐서 행동을 하기 때문에 미미하지만 시간이 더 오래 걸림

순수 가상 함수(pure virtual function)와 추상 클래스(abstract class)

순수 가상 함수: =0; 으로 처리되어있으며, 반드시 오버라이딩 되어야만 하는 함수

#include <iostream>

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

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

class Cat : public Animal { 
public:
  Cat() : Animal() {}
  void speak() override {
    std::cout << "야옹야옹" << std::endl; }
};
int main() {
  Animal* dog = new Dog(); Animal* cat = new Cat();
  dog->speak();
  cat->speak();
}

위 코드에서 Animal 객체를 생성하려고 하면 컴파일 오류 생김!

이렇게 순수 가상 함수를 최소 한개 포함하고 있는- 반드시 상속 되어야 하는 클래스를 가리켜 추상 클래스 (abstract class)라고 부름.

다중 상속

생성자 호출 순서는 상속 순서에 좌우됨.

virtual로 상속 받기

virtual로 상속 받으면 1번만 호출할 수 있음(생성자를)
그런데
H1 -> human 상속 받음
H2 -> human 상속 받음
H3 -> H1, H2 상속 받음

이럴 경우 H3는 human 생성자를 2번 호출한다.
이때 애초에 H1, H2 에서 human 클래스를 상속 받을 때 virtual 키워드로 명시해 주면 1번만 호출해 줄 수 있다.

profile
이것저것 개발하는 것 좋아하지만 서버 개발이 제일 좋더라구요..

0개의 댓글