상속(2)

은수·2022년 6월 6일

cpp study

목록 보기
7/21

가상함수와 다형성

is - ahas - a

is a (~ 이다)

class Manager : public Employee
  • ManagerEmployee를 상속한다.
  • Manager 클래스는 Employee 의 모든 기능을 포함한다.
  • Manager 클래스는 Employee의 모든 기능을 수행할 수 있기 때문에,ManagerEmployee라고 칭해도 무방하다.

따라서, 모든 상속 관계는 is a관계라고 볼 수 있음. (하지만 뒤바꾸면 성립되지 X)

  • manager는 employee 이지만
  • employee는 manager가 아님.


상속 관계 도표로 표현 시, 파생 클래스기반 클래스를 화살표로 가리킴.

상속의 특징
1. 특수화 (구체화 : specialize)

  • 클래스가 파생되면 파생될 수록 특수화 됨.
  • Employee는 일반적 사원 Manager는 일반적 사원 중 특수한 사원
  1. 일반화 (generalize)
  • 반대로 기반클래스로 거슬러 올라갈수록 일반화되는 특성

has a (~를 가진다)

  • 포함, 집합관계.
  • 한 클래스가 다른 클래스를 포함, 소유하는 관계
  • ex) 자동차 클래스를 구성하기 위한 엔진 클래스, 브레이크 클래스, 오디오 클래스 등은 has - a 관계

업 캐스팅

파생 클래스에서 기반 클래스로 캐스팅하는 형태

Base p;
Derived c;

// Derived 의 객체 c 를 Base 객체를 가리키는 포인터에 넣음
Base* p_c = &c;


Derived가 Base를 상속 받았으므로, Base 객체를 가리키는 포인터가 Derived c객체를 가리켜도 무방함. 단, p는 Base 객체를 가리키는 포인터이므로 Base class에 있는 것만 사용

다운 캐스팅

기반 클래스에서 파생 클래스로 캐스팅하는 형태

// error 발생
Base p;
Derived c;

Derived* p_p = &p;

  • Derived* 포인터가 Base 객체를 가리킬 때, p_p->what()하게 된다면 Derived의 what함수가 호출되어야 하는데, p_p가 가리키는 객체는 Base의 객체이므로 Derived에 대한 정보 없기 때문에 불가능
Base p;
Derived c;

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

위 코드에서도 마찬가지로 에러 발생

// dynamic_cast
Derived* p_c = static_cast<Derived*>(p_p);

위와 같이 강제적으로 타입 변환하면 컴파일 오류 발생하지는 않지만 권장X

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;
  // 기반 클래스의 what()
  p_p->what();

  std::cout << " == 실제 객체는 Derived == " << std::endl;
  // 파생 클래스의 what()
  p_c->what();

  return 0;
}

위의 코드에서 p_p와 p_c는 모두 Base를 가리키는 포인터지만, 실제 p_p 와 p_c 가 무엇과 결합해 있는지 아는 것 처럼 p_p 는 Base 객체를 가리키고, p_c 는 Derived 객체를 가리킴. == virtual 키워드 때문!

여기서 virtual 키워드는, 동적 바인딩 실행.

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

즉, p_c->what(); 에서 아래의 순서로 작동되는 것!
1. p_c는 Base 포인터니까 Base의 what()을 실행해야지
2. 근데 what이 virtual 이네?
3. 이거 Base의 객체가 맞나?
4. 아니 Derived의 객체네
5. 그럼 Derived의 what을 실행해야지!

virtual 소멸자

클래스 상속을 사용할 때, 소멸자를 가상함수로 만들어야 된다는 점을 중요하게 처리해야 함

#include <iostream>

class Parent {
 public:
  Parent() { std::cout << "Parent 생성자 호출" << std::endl; }
  virtual ~Parent() { std::cout << "Parent 소멸자 호출" << std::endl; }
};
class Child : public Parent {
 public:
  Child() : Parent() { std::cout << "Child 생성자 호출" << std::endl; }
  ~Child() { std::cout << "Child 소멸자 호출" << std::endl; }
};
int main() {
  std::cout << "--- 평범한 Child 만들었을 때 ---" << std::endl;
  { 
    // 이 {} 를 빠져나가면 c 가 소멸된다.
    Child c; 
  }
  std::cout << "--- Parent 포인터로 Child 가리켰을 때 ---" << std::endl;
  {
    Parent *p = new Child();
    delete p;
  }
}

Q) https://modoocode.com/211 주석부분 차이?

Parent 의 소멸자를 virtual 로 만들면, p 가 소멸자를 호출할 때, Child 의 소멸자를 성공적으로 호출할 수 있음.

레퍼런스도 된다

기반 클래스에서 파생 클래스 함수에 접근하는 2가지 방법

  • 기반 클래스의 포인터를 통해서 접근하는 것
  • 기반 클래스의 레퍼런스인 것
#include <iostream>

class A {
 public:
  virtual void show() { std::cout << "Parent !" << std::endl; }
};
class B : public A {
 public:
  void show() override { std::cout << "Child!" << std::endl; }
};

void test(A& a) { a.show(); }
int main() {
  A a;
  B b;
  test(a);
  test(b);

  return 0;
}

함수에 타입이 기반 클래스여도 그 파생 클래스는 타입 변환되어 전달할 수 있음.
따라서 test함수에서 show()를 호출했을 때, 인자로 b를 전달했다면, 비록 전달된 인자가 A의 객체이지만 show()가 virtual로 정의되어 있끼 때문에 알아서 B.show()를 찾아서 호출

가상 함수 구현 원리

C++ 에서 사용자가 직접 virtual 로 선언하도록 한 이유?

  • 가상 함수를 사용하게 되면 약간의 오버헤드 (overhead) 가 존재하기 때문 (시간이 더 오래걸림)
class Parent {
 public:
  virtual void func1();
  virtual void func2();
};
class Child : public Parent {
 public:
  virtual void func1();
  void func3();
};

C++ 컴파일러는 가상 함수가 하나라도 존재하는 클래스에 대해서, 가상 함수 테이블(virtual function table; vtable)을 만듦

가상 함수를 호출하였을 때는 가상 함수 테이블을 한 단계 더 걸쳐서, 실제로 어떤 함수를 고를지 결정

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

순수 가상 함수

class Animal {
 public:
  Animal() {}
  virtual ~Animal() {}
  virtual void speak() = 0;
};
  • 가상 함수에 = 0;을 붙여서, 반드시 오버라이딩되도록 만든 함수
  • "무엇을 하는지 정의되어 있지 않음 함수"
  • 본체가 없기 때문에, Animal 객체 생성하는 것 불가능
    즉, Animal a; a.speak(); 불가능!
  • 따라서 인스턴스화 시키기 위해서는 "클래스를 상속받는 클래스를 만들어서 모든 순수 가상 함수를 오버라이딩 해줘야 함"
  • "이 기능은 일반적인 상황에서 만들기 힘드니 너가 직접 특수화 되는 클래스에 맞추어서 만들어서 써라."

추상 클래스

class Dog : public Animal {
 public:
  Dog() : Animal() {}
  void speak() override { std::cout << "왈왈" << std::endl; }
};
  • 이렇게 순수 가상 함수를 최소 한 개 포함하고 있는 클래스
  • 반드시 상속되어야 하는 클래스
  • 위와 같이 speak()를 오버라이딩 함으로써, Dog 클래수의 객체 생성
  • 강아지는 왈왈 speak()하고, 고양이는 야옹야옹 speak()할거야!

다중 상속(multiple inheritance)

한 클래스가 다른 여러 개의 클래스들을 상속 받는 것을 허용하는 형태

class A {
 public:
  int a;
};

class B {
 public:
  int b;
};

class C : public A, public B {
 public:
  int c;
};

위의 경우, 단순히 AB의 내용이 모두 C에 들어가는 것.

생성자들의 호출 순서

#include <iostream>

class A {
 public:
  int a;

  A() { std::cout << "A 생성자 호출" << std::endl; }
};

class B {
 public:
  int b;

  B() { std::cout << "B 생성자 호출" << std::endl; }
};

class C : public A, public B {
 public:
  int c;

  C() : A(), B() { std::cout << "C 생성자 호출" << std::endl; }
};
int main() { C c; }

A -> B -> C 순서로 호출됨.
그러나 아래와 같이 코드 바꾼다면,

class C : public B, public A

B -> A -> C 순서로 호출됨.
즉, 오직 상속하는 순서에 생성자 호출 순서가 좌우되는 것.

다중 상속 시 주의사항

  • 클래스 A, B에 같은 이름의 함수 및 인자가 있으면 안됨
  • 다이아몬드 상속, 공포의 다이아몬드 상속

    위의 이미지와 같은 형태! Human의 모든 내용이 중복되는 문제가 발생하는데, virtual로 상속 받는다면 이러한 문제 해결 가능

0개의 댓글