강의/computer science

[Computer Science/C++] Multiple Inheritance, Multi-Level Inheritance, Abstract Class, Friend Class

하기싫지만어떡해해야지 2024. 10. 25. 17:09

이 게시글은

서울대학교 데이터사이언스대학원

조요한 교수님의

데이터사이언스 응용을 위한 컴퓨팅 강의를

학습을 위해 재구성하였습니다.


Multiple Inheritance

Multiple Inheritance란 하나의 derived class가

여러 개의 base class를 상속받게 해주는 것이다

 

 

저번 시간과 동일하게 포켓몬으로(..)

예시를 한 번 보자

 

전기속성을 띄는 포켓몬은 electricLevel이라는 attribute를

불속성을 띄는 포켓몬은 flameLevel이라는 attribute를

갖고 있다

 

따라서 ElectricPokemon이라는 클래스와

FirePokemon이라는 클래스를 또 만들려고 한다

 

그럼 Pikachu는 포켓몬이자 전기속성을 띄는 포켓몬이므로

BasePokemon class와 ElectricPokemon class를

상속받게 되는 것이다

마찬가지로 파이리도

BasePokemon class와 FirePokemon class

2개를 상속받게 된다.

 

 

전체 구현은 위와 같다

BasePokemon에는 name과 hp와 같은

기본적인 것들만 구현하고

enum Type은 바깥에 구현해뒀다

 

그런 다음 ElectricPokemon class와

FirePokemon class를 구현해줬다

 

그런 다음 이제

BasePokemon과 ElectricPokemon을

상속받을 Pikachu 클래스를 구현해보자

 

위와 같이

Class Pikachu : public BasePokemon, public ElectricPokemon

이렇게 써주면 Pikachu는 2가지 클래스를 상속받게 되는 것이다

 

constructor에서는

BasePokemon과 ElectricPokemon도

초기화해준다

 

그런 다음 main함수에서

Pikachu object를 생성한뒤

BasePokemon의 method나

ElectricPokemon의 method를

실행시키면 정상적으로 되는 것을 볼 수 있다

 

 

Charmander class도 피카츄와

비슷하게 구현해주면된다

 

 

이렇게 여러 개의 class를 상속받을 때

constructor의 호출 순서에 대해 알아보자

 

부모 class들의 constructor가 먼저 호출이 되는데

그 순서는 위에 있는

BaseClassList에 작성이 되어있는

순서대로 호출이 된다

 

class Charmander : public BasePokemon, public FirePokemon

과 같이 BaseClassList가 구현되어있으므로

BasePokemon이 먼저

그 다음 FirePokemon이 호출이 된다

 

constructor에 적혀있는 순서는

아무런 상관이 없다

 

그렇다면 Destructor의

호출 순서를 한 번 알아보자

 

Destructor의 call은 constructor call과는

반대 방향으로 호출된다

 

우선 derived class의 destructor가

먼저 호출되고

BaseList에 기록된 반대 방향으로 호출된다

 

 

이제 이렇게 여러가지 class를 상속받은

class의 memory layout을 살펴보자

 

Pikachu는 상속을 받은 class이므로

vptr이 가장 위에 있고

그 다음에는 BasePokemon의 attribute들

그다음은 ElectricPokemon의 attribute들이

있는 것을 볼 수 있다

 

그다음 가장 마지막에

Pikachu에 특화된 method인

cry가 있는 것을 볼 수 있다

 

중간 padding은 저번 시간에도 정리했지만

한 번 메모리에 접근할 때

cpu는 뭉텅이(?)로 가져오기때문에

그 효율성을 위한 값이라고 보면 된다

 

그런데 이렇게 한 클래스가

여러 개의 클래스를 상속받으면

발생할 수 있는 문제는 어떤 것들이 있을까?

 

위 코드 예시를 통해 한 번 살펴보자

 

피카츄와 파이리의 혼종인

Pikamander를 만들었다

불도 쓰고 전기도 쓰는

초특급 울트라 초강력 포켓몬이라고한다

 

그래서 이를 구현하기위해

Pikamander라는 class를 새로 만들고

BasePokemon, ElectricPokemon, FirePokemon

3가지의 포켓몬을 상속받았다

 

그런 다음 main함수에서

Pikamander를 정의하고

getType을 호출했다

 

그럼 이 과정에서 에러가 뜨는데 그 이유는

getType method는

ElectricPokemon class에도

FirePokemon class에도 존재하기때문에

두 클래스에서 어떤 메소드를

호출해야할지 몰라 에러가 뜨는 것이다

 

이런 문제를

Ambiguity라고 한다

 

같은 이름을 가진 method나 attribute가

muliple parents에 있을 때

발생하는 것이다

 

 

메모리 레이아웃을 보며

어떻게 Ambiguity가 발생하는지

한 번 이해해보자

 

이름이 동일한 attritbute를 갖고있는

2개의 클래스를 상속받았다는 것은

동일한 attritbute에 대한 copy가 2개가

생긴다는 의미이다

 

따라서 위 메모리 레이아웃을보면

type이라는 attribute는 상속이 2번 되었고

서로 다른 2번의 copy가 생겼다

 

그래서 getType을 사용하면

둘 중에 어떤 것을 호출하면 되는지 몰라

error가 발생하는 것이다

 

 

그렇다면 이런 Ambiguity를

어떻게 해결할까?

 

단순하게 생각하면

pikamader.FirePokemon::type

이런 식으로 어디서 가져온 attribute인지

적어주는게 필요할 것이다

 

아니면 조금 더 근본적으로

ambiguity를 피할 수 있도록

pikamander 클래스 안에

getType method를 정의해서

명확하게 어떤 값을 불러올지

정의를 해줄 수도 있다

 

아니면 정말 더 근본적으로

애초에 이런 문제 자체가 발생한다는 것 자체가

application의 design에 flaw가

있다는 것을 암시하기 때문에

class들의 구조를 아예 새로

refactoring하는 방법이 있다

 

 

따라서 이런 근본적인

design적 flaw를 없애기위해

나타난 것이 바로

Multi-Level Inheritance이다

 

Multi-Level Inheritance

 

그냥 모든 class를 다 상속받는게 아닌

계층 구조를 갖도록 상속받게 해주는 것이다

 

Pikachu는 ElectricPokemon만

ElectricPokemon은 BasePokemon을 상속받게끔

여러 계층으로부터 상속이 이어지는거라

multi-level inheritance라고 부른다

 

 

multi-level inheritance의

구현을 살펴보자

 

BasePokemon class를 정의해주고

아까는 밖으로 나와있던

enum Type을 BasePokemon의

attribute로 넣어준다

 

ElectricPokemon을 정의하며

BasePokemon을 상속받도록 해준다

BasePokemon을 초기화하면서

type에 ELECTRIC을 넣어준다

 

FirePokemon을 정의하며

BasePokemon을 상속받도록 해준다

 

 

가장 하위 class인 Pikachu의 구현을 보자

Pikachu는 ElectricPokemon을 상속받게 해준다

 

BaseClass -> ElelctricPokemon -> Pikachu

 

그럼 ElectricPokemon은 여기서

intermediate class가 된다

 

Pikachu의 constructor에서는

상속받는 class인

ElectricClass의 constructor만 호출해준다

그럼 ElectricClass가 호출되면서

그 Parent Class인 BasePokemon class도

같이 호출되게된다

 

Charmander class도 동일하게 구현해준다

 

그런 다음 main함수에서

Pikachu와 Charmander를 선언해서

여러 method나 attribute들을 호출해보자

 

우선 pikachu의 getType을 호출하면

getType은 BaseClass밖에 정의가

안되어있는 method이기 때문에

BaseClass의 getType이 호출된다

 

electricLevel은

ElectricPokemon에 있는

attribute이므로 거기에 있는게

호출이 된다

마지막으로 cry는 Pikachu에만 있는

method이기 때문에

Pikachu class에서 가져와서 호출이 된다

 

 

constructor call의 구조를 잘 보자

constructor의 호출은

상속받는 것의 역순이므로

BasePokemon이 가장 먼저 호출되고

그다음 ElectricPokemon -> Pikachu의 순서로

호출이 된다

 

위의 ppt의 코드 예시를 잘 보자

또 전기와 불을 모두 쓰는

초특급 혼종 포켓몬을 구현하려고했다

 

그래서 Pikamander라는 class를 새로 만들고

ElectricPokemon과 FirePokemon을

상속받게 해줬다

 

그런 다음 main함수에서

pikamander.getType()을 호출해주면

compile error가 뜨는 것을

확인할 수 있다

 

왜 이런 컴파일 에러가 생길까?

에러 메세지를 자세히 보면

getType이 multiple base-class에서

발견된다고 한다

 

한마디로 Pikamander가 상속받는

ElectricPokemon과

FirePokemon모두

type을 갖고 있어서 다시 한 번

ambiguity가 발생한 것이다

 

이런 문제를 바로

diamond problem이라고 한다

 

위 diagram을 보면 알겠지만

BaseClass가 있고

이를 상속받는 2개의 Intermediate class

그리고 그 2개의 Intermediate Class를 

모두 상속받는 Derived Class에서

같은 name을 가진 method 혹은 attribute를

호출하면 ambiguity가 발생하는 것이다

 

위 memory layout을 보면

Pikamander에는

ElectricPokemon의 attribute들

그리고 FirePokemon의 attribute들이

저장이 되어있고

name, hp, type 변수들이

2개씩 copy가 되어 저장되어있는 것을

확인할 수 있다

 

 

이를 해결해주는 방법이 바로

Virtual Inheritance이다

 

virtual inheritance의 목적은

base class를 오직 한 번만

copy해주기 위함이다

 

위의 ppt에서 구현을 자세히 보자

 

우선 BasePokemon에서의 enum Type에

ELECTRICFIRE도 추가해준다

 

그런 다음

ElectricPokemon에서 BasePokemon을 상속할 때

virtual를 붙여준다

FirePokemon도 마찬가지

 

그런 다음 Pikamander를 초기화 할 때

BasePokemon, ElectricPokemon, FirePokemon

세 개를 모두 초기화해준다

 

이전과 다른 점은

virtual이라는 키워드를 썼다는 점과

Pikamader class에서

base class까지 constructor에

구현을 해줬다는 점이다

 

이럴 경우에는 어떻게

constructor call이 되는지 살펴보자

 

우선 Pikamander의 constructor에 진입해서

BasePokemon으로 가장 먼저 가게 된다

그 다음 ElectricPokemon의 constructor를 호출해주는데

이때 ElectricPokemon의 constructor 안에

BasePokemon이 있지만

virtual하게 상속을 받았기 때문에

BasePokemon의 constructor를 또 부를필요가

없다고 판단하여 skip하게 된다

 

그 다음 FirePokemon에 가게 돼서

똑같이 FirePokemon의 constructor를 호출하고

그 안에 있는 virtual로 선언된 BasePokemon의

constructor 호출은 skip하게 되는 것이다

 

따라서 결론적으로

BasePokemon의 constructor는

1번만 불려지게 되는 것이다

 

virtual inheritance를 할 때의

메모리 레이아웃을 살펴보자

 

virtual inheritance를 하게 되면

virtual BaseClass에 있는 것들은

딱 한 개만 생성이 된다

 

Pikamander의 Layout을 살펴보면

BasePokemon class의 attribute인

name, hp, type은 1개만 있고

그 다음 electricLevel

그 다음 flameLevel이 있는 것을

확인할 수 있다

 

Abstract Class

이제 특수한 class들을 살펴보자

 

Abstract Class의 목적은

이 클래스 자체를 상속받아서

다른 class들이 쓸 수 있게 해주는 것이

주요 목적이다

 

이게 무슨 소리냐하면

결국 상속받을 class를 만들어주는

일종의 청사진같은 존재라는 뜻이다

 

그래서 abstract class는

instance화를 시키지 못한다

 

위의 ppt에서 BasePokemon의

class 구현을 잘 보자

 

가장 마지막 attack method에서

virtual void attack(BasePokemon& opponent) = 0;

와 같이 구현된 것을 확인 할 수 있다

 

저렇게 구현된 함수를

pure virtual function이라고 하는데

pure virtual function이 1개만 있어도

abstract class로 정의된다

 

그렇다면 이 BasePokemon이라는 class는

abstract class이니 object화를 시킬 수 없다

따라서 무조건 상속을 받아서만 사용할 수 있는데

상속을 받을 때, pure virtual function인

attack은 반드시 override를 통해서 구현해줘야만한다

 

만약 override로 attack을 구현해주지 않았다면

그 상속받은 class도 abstract class로 정의되어

instance화를 시키지 못한다

 

 

abstract class의 모든 method들이

다 abstract일 필요는 없다

 

최소 1개 이상의

pure virtual function만 있으면 된다

 

abstract class를 상속받아서

pure virtual function이외의 

다른 method들을 상속받지 않았다면

그건 abstract class에 있는대로 구현된다

 

 

abstract class를 사용하는 이유는

앞에서도 말했듯이

다른 class들의 blueprint역할을 하기 위해서다

 

어떤 함수의 definiton을

알려주기위한 용도로 사용된다

 

또한 각각의 class가 각각의 class에

특화되게 구현해주는걸 가능하게하는데

이 또한 polymorphism과 관련이 있다

 

 

Friend Class

 

Friend Class란

다른 class의 Private 혹은 protected member에

special한 접근 권한을 주는 것이다

 

위의 예시를 잘보자

 

Pikamander Class를 정의하고

그 안에서 Pikachu를 friend class로 정의했다

 

그럼 Pikachu는

Pikamander의 private과 protected member에도

접근을 할 수 있도록 허용해주는 것이다

 

하지만 이런 기능이

객체지향에서 추구하는 encapsulation을

일부 깨뜨리는 것인데

Pikamander가 아닌 Pikachu에서

protected나 private을

접근할 수 있도록 권한을 줬으니

encapsulation principle을

약간 희생하게 되는 것이다

 

결과적으로 module화가 덜 되는 것이도

서로서로 coupling이 되는 결과를

초래할 수도 있다

 

마지막으로 Friend Function을 보고

이번 수업내용정리는 마무리해보려고한다

 

friend function은 어떤 특정한

function에게 접근 권한을 주는 것이다

 

위의 ppt예시를 잘 보면

osstream& operator<<는

Pikamander의 member function이 아니다

하지만 Pikamander 안에 함수 정의가 포함되어 있다

앞에 friend라는 키워드를 붙여주었다

 

그럼 저 operator overloading function에게

Pikamander의 private, protected members에

접근할 권한을 주겠다는 뜻이다

 

 

그러고 저 operator overloading 함수는

global scope의 어느 곳에

자세한 구현부가 정의되어있을 텐데

이 때 pikamander에 정의되어있는

private이나 protected한 member들에

접근이 가능하다