Eat Study Love

먹고 공부하고 사랑하라

SW 만학도/C++

[Main course!] Review 7 - Inheritance in C++

eatplaylove 2024. 7. 21. 14:55

https://eglife.tistory.com/89

 

Review 5 - Special Members in C++

https://eglife.tistory.com/88 Review 4 - Class,Overloading,Special Members in C++https://eglife.tistory.com/87 Review 2 - Functions and Memory Management in C++https://eglife.tistory.com/85 Review 2 - Container, Iteration in C++https://eglife.tistory.co

eglife.tistory.com

오죽하면 제목에도 Main Course라고 달아놓았으랴..

 

C++의 꽃

 

C++ 그 잡채! 인 C++ Class 상속에 대해서 공부를 하려한다.

 

C++을 이것에 활용하기 위해 배운다고 해도 과언이 아닐정도로 중요한 Part라서 심도 있는 집중력이 요구된다.

 

VAMOS!!!

 

 

1.Inheritance Basic

 

 

모두가 좋아하는 포켓몬을 예로 들어서 C++ 공부를 이어나가겠다.

 

포켓몬이라는 Class를 만들면, 그걸 통해서 피카츄, 파이리라는 Object를 만들 수 있지 않겠는가.

 

class Pokemon{
protected:
    string name; //Pikachu, Charmander
    int hp;
    enum Type {Electric,Fire} type; // enum : 둘 중에 하나

public:
    Pokemon(string n,int h,Type t):name(n),hp(h),type(t){}

    int getHp(){
        return hp;
    }
    string getName(){
        return name;
    }
    Type getType(){
        return type;
    }

    void decreaseHp(int amount){
        hp -= amount;
    }

    void attack(Pokemon& opp){
        cout<<"BASE: "<<name <<" attacks"<<opp.getName() << endl;
    }

};

 

단순하지만 요렇게 코드를 짜 볼 수 있다.

 

Constructor로 Pokemon은 이름, 체력, Type을 초기화 하고 각종 Attribute / Method를 Class 내부에 설정해 놓는다.

 

여기서 attack() 함수의 경우 상대 포켓몬 object를 call by reference로 받아서 상대 포켓몬에 대한 memory copy를 막고 Pointer도 쓰지 않는다. 요것이 C++만의 장점, Call By Reference!!!

 

 

2. Derived Class

 

자 이제, 피카츄랑 파이리를 만들어볼건데

 

Class의 상속 기능을 사용하기 위해서 피카츄 / 파이리만의 특성을 다룬 Class를 하나 더 정의해보자.

 

class Pika : public Pokemon {
    int electricLevel;
public:
    Pika():Pokemon("Pikachu",100,Electric),electricLevel(10){}
    ~Pika(){}
    void attack(Pokemon& opp){
        if(opp.getType()==Fire){
            opp.decreaseHp(electricLevel-2);
        }
        else opp.decreaseHp(electricLevel);
    }

    void cry(){
        cout << "Pika!!!!" << endl;
    }
};

 

이렇게 임의의 피카츄 derived class를 만들어보았다.

 

왜 derived(파생된) 이라고 불리우는 감?

저렇게 클래스 옆에 부모 클래스 이름을 적어주는 행위자체가 파생클래스를 만드는 것이다.

 

이를 피카츄 클래스가 포켓몬 클래스로부터 파생됐다, 상속됐다 등으로 표현한다.

 

저 "public"이 의미하는 건, 부모 class로 받을 수 있는 member들을 public으로 관리하겠다는 것이다.

 

즉, 부모 클래스의 "public"영역을 가져다 쓰고 public하게 관리하겠다는 것이고, 이게 만약 protected로 되어 있으면 부모 class의 member들을 자식 class에서 protected 하게 관리하겠다는 것이다.

 

또, 위에 코드를 보면 Pika 영역의 int electricLevel의 경우 지금 access specifier가 따로 선언되어있지 않은데,

 

이럴 땐 default로 "private" 영역이 설정된다.

 

이렇게, 자식 class가 부모 class를 업고 있는 경우엔 부모 class의 member들을 자식 class에서 따로 다시 선언해줄 필요는 없으며, 이런 행위는 권고되지 않는다.(이상동작이 발생할 수 있다.)

 

Constructor는 상속되지 않아서 자식 class에서 constructor를 다시 따로 써줘야한다.

 

자식 class에서 부모 class의 Constructor를 call 할 때는, body부분이 아니고 initializer list 부분에 명시적으로 써야하며 이게 없으면 부모 class constructor의 default case를 가져다가 초기화에 쓴다.

 

이와 유사하게 Destrcutor도 상속되지 않아서 자식 class에서 이것도 따로 선언해줘야 한다.

만약 자식 class에 이게 없다면, 이 역시 부모 class의 destructor를 가져와서 쓴다.

 

Con : 부모class꺼 먼저 / Des : 자식class꺼 먼저

 

이러니까 위의 예시처럼,

 

Pokemon class에는 Destructor는 없어도 된다. 왜냐면 그 밑에 피카츄 class에서 선언해주면 얘의 Destructor가 먼저 실행되니까!

 

그런데, Pokemon class에 Constructor가 없다면 이제 문제가 되는 것이다. 부모 class 먼저 cons 실행하는데 없다고 하니까

 

class Charman : public Pokemon{
public:
    int fireLevel;
    Charman():Pokemon("Pairi",120,Fire),fireLevel(12){}
    ~Charman(){}
    void attack(Pokemon& opp){
        if(opp.getType()==Electric){
            opp.decreaseHp(fireLevel+1);
        }else{
            opp.decreaseHp(fireLevel);
        }
    }
};
int main(){
    Pika P;
    P.cry();
    Charman C;
    cout<<"Pika HP:"<<P.getHp()<<",Charman HP: "<<C.getHp()<<endl;
    P.attack(C);
    cout<<"Pika HP:"<<P.getHp()<<",Charman HP: "<<C.getHp()<<endl;
    C.attack(P);
    cout<<"Pika HP:"<<P.getHp()<<",Charman HP: "<<C.getHp()<<endl;
// Pika HP:100,Charman HP: 120
// Pika HP:100,Charman HP: 112
// Pika HP:87,Charman HP: 112

    return 0;
}

 

파이리 Class까지 만들어준다음에,

 

피카츄, 파이리 Object를 만들어서 여러가지를 출력해보았다.

 

일단 attack함수의 경우 부모 class의 attack 함수 內 문구는 출력되지 않는다.

 

자식 class 기준으로 method가 실행된다.

 

만약 굳이 부모 class의 method를 실행시키고 싶으면 " :: " 이걸로 scope를 정해줘야한다.

 

int main(){
    Pika P;
    P.cry();
    Charman C;
    cout<<"Pika HP:"<<P.getHp()<<",Charman HP: "<<C.getHp()<<endl;
    P.attack(C);
    cout<<"Pika HP:"<<P.getHp()<<",Charman HP: "<<C.getHp()<<endl;
    C.Pokemon::attack(P);
    cout<<"Pika HP:"<<P.getHp()<<",Charman HP: "<<C.getHp()<<endl;
// Pika HP:100,Charman HP: 120
// Pika HP:100,Charman HP: 112
// BASE: Pairi attacksPikachu
// Pika HP:100,Charman HP: 112

 

Pokemon class의 scope를 해주면 부모 class의 method가 실행되긴 하는데,

 

대신에 자식 class method는 씹힌다.(체력 변동이 없음)

 

 

3. Memory Layout

 

Class Object들이 생성되면 이것들이 연속된 Memory bloack에 할당된다.

 

할당 순서는, member변수 선언 순서를 따라간다.

 

Derived Class의 경우 Memory 초반구성은 부모 Class와 똑같이 가져간다.

 

그리고 Derived Class의 경우 Padding 이라는 4byte 크기의 여유공간이 있는데, 밑에 다룰 #virtualPointer 를 위한 공간이라고 한다.

 

 

4. Class Substitution

 

C++ OOP를 가능케 하는 것이다. 자식 class를 통해 생성된 object가 마치 부모 class를 통해 생겨난 놈들처럼 처리될 수 있도록 한다.

 

부모 class를 가리키는 pointer가 자식 클래스의 object도 가리킬 수 있다.

이렇게 Pokemon class를 가르키는 Pointer를 반환하는 함수를 만들면, 얘네 가지고 부모 class public member는 물론 자식 class object에도 접근할 수 있다.

 

    Pokemon* P = new Pika();
    Pokemon* C = new Charman();

    P->attack(*C);
    cout << P->getName()<<":"<<P->getHp()<<endl;
    cout << C->getName()<<":"<<C->getHp()<<endl;
    C->attack(*P);
    cout << P->getName()<<":"<<P->getHp()<<endl;
    cout << C->getName()<<":"<<C->getHp()<<endl;
// BASE: Pikachu attacks Pairi
// Pikachu:100
// Pairi:120
// BASE: Pairi attacks Pikachu
// Pikachu:100
// Pairi:120

 

다만, 이렇게 부모 Class Pointer로 생성된 애들은 method 실행 시 부모 class 기준으로 실행이 된다.

 

 

 

5. Virtual Functions

 

이것들을 자식 Class의 method가 실행될 수 있게 해주는 것이 바로 Virtual Functions!

 

Pikachu:100
Pairi:112
Pikachu:87
Pairi:112

 

virtual + override ( 사실 부모 class에 virtual만 써줘도, 자동으로 override 기능이 활성화되어 자식 class에 안 써줘도 무방하다) 을 해줬더니 이제 위 코드에서 attack method가 자식 class 기준으로 발동되는 것을 볼 수 있다.

역시나, 만약에 그냥 virtual 을 썼음에도 기존 부모class의 method를 실행시키고 싶다면 Pokemon:: scope를 사용하면 된다.

 

이것은 일종의 polymorphic의 영역! 같은 이름의 method의 동작방식이 다를 수 있게 지원하는 것이다. for OOP!

Destructor의 경우에도 virtual <-> Override를 쓰기도 하는데, 이유는 Class Pointer로 자식 class Object를 만들어 준 뒤에 delete 하면, 이전과 달리 부모 class destructor 기준으로 삭제가 실행되기 때문이다. 이를 막기 위해선 destructor에도 virtual 작업이 필요하다.

 

 

 

6. Dynamic Binding

 

동적바인딩 Dynamic Binding이란, Runtime에서 어떤 함수를 어떤 순서로 call 할 것인가를 결정하는 녀석이다. 이것은 위에서 언급했던 Virtual Pointer(Vptr)과 Virtual table(Vtable) 개념으로 설명이 가능하다.

 

 

가령 포켓몬 class의 자식 클래스인 Pikachu 클래스를 통해 Object를 만들고,

 

여기서 override 되어있는 evolve() method를 실행시켰다고 할 때 순서도는 위와 같다.

 

애초에 override 되어있지 않은 useItem 외에는 모두 vptr을 통해 각자 자식class의 override 되어 있는 method 코드를 가리키고 있고 useItem만 부모 class의 method를 가리키고 있다.

 

vptr은 각 class에서 메모리 가장 상단에 위치해 있으며 vtable은 각각의 class에서 Pokemon 부모 class 외에 method가 어디에 있는지 그 memory주소를 담고 있는 tablel이다.

 

 

정리하면, Dynamic Binding은 Runtime에서 obect가 함수를 call하면 적절한 함수를 가리켜 값을 반환받을 수 있게 도와주고, 이를 위해 Virtual Table은 override된 method 또는 부모 Class의 method 메모리주소를 간직하고 있다.

 

그러면, 부모 클래스를 상속받은 클래스와 이를 통해 만들어진 Object 메모리 최상단에 내장된 Virtual Pointer는 Vtable을 가리키고 함수가 call 되었을 때 부모 class 내부 method 또는 Override 된 자식 class 내의 method를 찾아가 함수를 적절히 실행시킨다. (Helps Polymorphism!)

 

동적바인딩은 Compile Time에 쫙 형성된다. 그리고 Vptr역시 compile time에 각 object 내부에 심어진다.

그리고 Runtime엔 function call이 발생하면 vptr/vtable이 활용된 적절한 method dereference가 이루어진다.

 

※ Vptr은 object외에도 각 class에도 기본적으로 최상단에 심어져있는데, 얘는 heap/stack이 아닌 별도 메모리 공간에 심어지는 녀석이고, 역시나 Pointer인만큼 메모리공간은 8byte를 차지한다.

 

 

- Advacnced Part -


7. Multiple Inheritance

 

말 그대로, 자식 class를 만들 때 여러 부모 class로부터 상속받는 것이다.

 

#include <iostream>
#include <string>
using namespace std;

// Base Classes
class BasePokemon{
protected:
    string name;
    int hp;
public:
    BasePokemon(string n,int h):name(n),hp(h){}
    virtual ~BasePokemon(){}
    int getHp() const {return hp;}
    string getName() const {return name;}
};
enum Type{Electric,Fire};

class Elec{
public:
    Type type;
    int electricLevel;;
    Elec(int e):electricLevel(e),type(Electric){}
    Type getType(){return type;}
};
class Fir{
public:
    Type type;
    int fireLevel;;
    Fir(int e):fireLevel(e),type(Fire){}
    Type getType(){return type;}
};

//Derived Class
class Pikachu : public BasePokemon, public Elec{
public:
    string cry;
    Pikachu() : BasePokemon("Pikachu",100),Elec(10),cry("pikabu!"){}
};

int main(){

    Pikachu P;

    cout << P.getHp() << endl;
    cout << P.getName() << endl;
    cout << P.getType() << endl;
    cout << P.cry << endl;
// 100
// Pikachu
// 0
// pikabu!
    return 0;
}

 

BasePokemon , Elec 이라는 부모 class 2개와 이 모두를 상속받는 Pikachu라는 자식 class 하나를 만들었다.

 

여러 class를 상속한 경우 Constructor의 call 순서는 상속부분의(Base classes list) 순서를 따른다.(위에선 Base -> Fire 순)

Constructor initializer 순서와는 상관이 없다~

 

class를 상속받았는데 자식 class의 constructor 부분에 언급이 없다면? -> 컴파일러는 자동으로 부모class의 construct default 값을 가져온다. 즉, 상속을 받았으면 부모class를 사용하라는 취지!

 

반면에, Destructor의 경우 Constructor call과 반대순서로 실행된다. 위 예시에선, Charmander -> Fire -> BasePokemon 순서로!

다중상속 object의 메모리 레이아웃 : vptr -> 부모1 -> 부모2 -> padding -> 자식class

 

 

 

8. Ambiguity (모호한 Case)

만약 이렇게 두 class 의 혼용인 기괴한 짬뽕 Object가 만들어졌다면, getType()을 call 할 때 충돌이 발생한다.

 

해결법으론 scope 사용(ex: Elec::getType() ) 또는 해당 method를 자식 class에서 override 해도 된다.

 

아니면 근본적으로, class design을 아래와 같이 바꾸는 것을 고려해야 한다.

 

 

 

예를 들어, 끔찍한 혼종인 피카츄+파이리 클래스를 아래와 같이 만들었다고 하자.

이렇게 되면 type 변수가 Electirc / Fire 포켓몬 모두를 통해서 BasePokemon class에 접근하기때문에 적절한 type 반환이 되지 않는다.

 

이를 Diamond Problem 이라고 하고, 요놈을 해결하기 위해서 Virtual Inheritance를 써준다.

 

이렇게 부모 class로부터 상속받을 때 virtual을 써주면 손자 class(Pikamander() ) 에서 constructor call이 들어오면 제일 최상위 BasePokemon class를 한 번 부르게 된다. 대신에 Pikamander constructor에 BasePokemon을 추가해줘야 하는 번거로움은 있다.

 

 

이렇게되면 Pikamander() Object는 member call을 할 때, Base쪽을 부르고 중간단계 녀석들은 virtually inherit 된 것이라서 Base를 한 번 더 call 하는 Diamond Problem이 없어지는 것이다.

 

 

이렇게 左 에서 右 로 간소화된 Object를 만나볼 수 있다.

 

즉, 정리하자면 '나' class 기준으로 내가 상속한 놈들은 상속한 class의 member를 싸그리 받아온다.

다만, 내가 상속한 class가 중간다리역할이고, 이놈이 최상위을 virtual로 상속하고 있으면 '나'는 최상위에 있는 member를 싸그리 받아오는데, 이게 중간다리 놈들이 많아도 중복되지 않게 최상위에 있는 Member는 딱 한 개씩만 받아온다.

 

 

 

9. Special Classes

 

클래스들 중에는 특이한 Class가 있다.

 

첫 번째. Abstract Class

딴 거 없고 단 한개라도 Pure virtual function( " = 0 " 으로 표현)을 갖고 있으면 된다. 얘는 얘를 직접적으로 통해서 Object를 만들 수 없으며, 이 놈을 상속받은 Class 무조건 이 Pure virtual function에 대한 정의를 해야 한다.

 

Blueprint , Interface 깔끔하게 하기 위해 만들어진 class라는데, 잘 모르겠다 ㅋㅋ;;

 

그리고 Friend Class 라는 놈도 있는데. 

얘는 encapsulation을 높인다.

 

이렇게 선언하면 Pikachu class가 Pikamander class의 private/protected 영역에 접근할 수 있다. 이건 주로 Operator overloading을 구현할 때 쓴다.

 

 

10. Type Casting

 

Type Casting은 문자그대로, 변수의 data type을 바꿔주는 것이다.

C에서 하던 방식인데, compile error는 없어도 runtime error 가능성이 높다

 

특별한 syntax를 써서 Casting을 해준다.

 

RUntime에 Casting을 해주는 녀석

하지만 Dynamic Cast는 Vptr, Vtable 사용이 많아서 비싼 작업이라 굳이 사용하진 않는다.

 

그 밖에 다양한 Casting이 있다. Reinterpret Cast는 embedding system을 이용해 메모리주소 어쩌고를 할 때 많이 쓰임(ex:레지스터)

주로 bit단위를 처리한다? -> Reinterpret Cast

 

 

11. Exception Handling ( 예외 처리 )

예외 처리 구문은 위와 같다. try - catch 구문으로 어떤 error가 발생했는지 잡는다.

Error를 던지게 하는 대표적인 구문이 throw std::exception((); 이다.

 

보통 아래와 같이 사용한다.

 

권장되는 방법은

 

Exception을 call by value가 아닌 reference로 잡고, object slicing은 최대한 피해야 한다.(loss 발생될 수 있기에)

 

 

 

C++ 전반에 대해서 복습을 해보았다.

 

내용이 많은만큼 중요하다는 것!

 

관련된 실습을 잘 해보며 계속 스킬을 가다듬어야겠다.

 

- E. O. D -