Eat Study Love

먹고 공부하고 사랑하라

SW 만학도/C++ & Algorithm

9. Inheritance - Advanced and Applications

eatplaylove 2024. 4. 13. 19:54

https://eglife.tistory.com/52

 

8. Inheritance - Basics

https://eglife.tistory.com/51 7. Copy&Move, Special Members https://eglife.tistory.com/50 6. Out_of_class Definition & Operator Overloading https://eglife.tistory.com/49 5. Classes https://eglife.tistory.com/43 4. Functions and Memory Management https://eg

eglife.tistory.com

 

 

꾸역 ~ 꾸역 이해했던 저번의 Inheritance 기초과정에 추가로 심화된 부분을 다뤄보자.

 

마찬가지로 전단계에서 공부했던 내용 + 금번에 공부할 내용 Keyword 간단히 정리

 

- Previous : Inheritance(Syntax, Class Substitution, Dynamic binding)

+++++

 

- Overview : Multiple inheritance, Multi-level inheritance, Special Classes, Type Casting, Exception handling

 

 

Multiple Inheritance

- 문자 그대로 Derived Class가 여러 개의 Base case를 상속받는 것이다.

 

피카츄, 파이리 Object를 걍 2개의 Class를 상속받아서 만들자

 

#include <iostream>
#include <string>

// Base Case 1
class BasePokemon{
    protected:
        std::string _name;
        int _hp;
    public:
        BasePokemon(std::string name, int hp) :
        _name(name),_hp(hp) {} // Body 비워~
    virtual ~BasePokemon() {}

    int getHp() const {return _hp;}
    std::string getName() const {return _name;}
};

enum Type {electric, fire};

// Base Case 2
class Elec{
    public:
        enum Type{electric} type;
        int electricLevel;
    Elec(int level) : electricLevel(level),type(electric) {}
        Type getType(){
            return(type);
        }
};
// Base Case 3
class Fire{
    public:
        enum Type{fire} type;
        int fireLevel;
    Fire(int level) : fireLevel(level),type(fire) {}
        Type getType(){
            return(type);
        }
};
// 피카츄 object 생성
class Pika: public BasePokemon, public Elec{
    public:
        std::string cry;
        Pika():BasePokemon("Pika",100), Elec(10),cry("PIKAPIKAPIKA!!!"){}
};

// 파이리 object 생성
class Pairi:public BasePokemon, public Fire {
    public:
        int height;
        Pairi(): BasePokemon("Pairi",200), Fire(5),height(188) {}
};



int main(){

    Pika elec1;
    Pairi fire1;

    std::cout << elec1.getType() <<"\t"<<elec1.getHp()<<"\t"<<elec1.electricLevel <<"\t"<<elec1.cry<<std::endl;
    std::cout << fire1.getType() <<"\t"<<fire1.getHp()<<"\t"<<fire1.fireLevel <<"\t"<<fire1.height<<std::endl;
// 0       100     10      PIKAPIKAPIKA!!!
// 1       200     5       188    

    return 0;
}

 

 

- type은 Type enum을 사용했더니 직접 출력할 땐 0 , 1로 출력이 된다.

 

- 만~~약에 Type enum에 있는 것들을 직접 문자로 출력해서 보고 싶다면 아래와 같이 Function을 하나 더 만들고 Main 함수도 좀 수정을 해야 한다. like below

 

// Type enum 정의
enum Type { electric, fire };

// Type을 문자열로 변환하는 함수
std::string typeToString(Type t) {
    switch (t) {
        case electric: return "electric";
        case fire: return "fire";
        default: return "unknown";
    }
}

int main() {
    Pika elec1;
    Pairi fire1;

    // getType()의 결과를 문자열로 변환하여 출력
    std::cout << typeToString(elec1.getType()) << "\t" << elec1.getHp() << "\t" << elec1.electricLevel << "\t" << elec1.cry << std::endl;
    std::cout << typeToString(fire1.getType()) << "\t" << fire1.getHp() << "\t" << fire1.fireLevel << "\t" << fire1.height << std::endl;

    return 0;
}

 

 

Constructor Calls

 

 

 

- 부모 Class의 Constructor는 맨 윗 줄 Base Class list에 선언된 순서대로 call 된다. (요기선 BasePokemon -> FirePokemon 순서, Base -> Fire -> Charmander Constructor순서로 CALL CALL CALL~)

 

 

- Derived Class 내부 constructor에서 base class call이 없으면, compiler는 base class의 default constructor를 call하려 할 것이다.. 꼬임! ==> 상속을 받았으면 고놈을 사용하라는 뜻이다.

 

 

Destructor Calls

 

- Destructor는 Constructor의 반대순서로 Call된다.

( Des of Derived class -> Des of Base Class(Base Class list 선언 역순!!)

 

 

Memory Layout

 

 

- 피카츄 , 파이리 모두 Virtual Pointer가 1빠로 등장하고, 그 다음 Basepoke Class, 각자 Electric / Fire Type을 지칭하던 2 번째 Base Class, 그리고 피카츄의 경우는 Compile Optimization을 위한 Padding영역 등장(Depends on Compiler 인듯) 그리고 마지막으로 각 Derived Class에 선언해줬던 Attribute가 등장한다.

 

Ambiguity(발생 가능한 모호함)

 

혼종 탄생;;

 

- getType() 이라는 Method가 Electric / FirePokemon Class에서 모두 찾아볼 수 있으니 Error OCCUR!!

 

- 비단 Method뿐 아니라, multiple parents class를 상속했을 때, member( Method or Attributes )의 이름이 겹치면 Error가 발생한다.

 

- 이런 경우에 derived class는 각 base class member 별로 다른 instance를 갖게 한다. --> Error

 

- 이게 바로 member의 이름으로 access 하려다보니까 발생한 문제점이다.

 

 

Ambiguity Solution

 

  1) Parent Class를 Specify 해준다. ex) pikamander.FirePokemon::type

 

  2) Method를 Override 해준다. ex)Pikamander::getType()

 

  3) 이런 Design 결함을 제거해준다.

 

- 전체적으로 위 방법들은 그리 좋은 해결책은 아니다. 괜히 코드만 더러워지고 헷갈려짐;;

 

다른 solution을 강구해보자

 

Multi - Level Inheritance

 

- 디자인을 수정한다. BasePokemon에 type을 넣어버리면 되는 것 아닌가!

 

 

 

//Base Classes

class BasePokemon{

protected:
    std::string _name;
    int _hp;
    enum Type {telec,tfire} _type;

public:
    BasePokemon(std::string name,int hp,Type type) :
    _name(name), _hp(hp), _type(type) {}

    Type getType(){ return _type; }
    int getHp(){return _hp;}
    std::string getName(){return _name;}
};

class electric : public BasePokemon {
public:
    int electriclevel;

    electric(std::string name, int hp, int level) : BasePokemon(name,hp,telec), electriclevel(level) {}

    // void cry(){
    //     std::cout << "PIKAKAKAKAKAK" << std::endl;
    // }
};

class fire : public BasePokemon{
public:
    fire(std::string name,int hp,int level) : BasePokemon(name,hp,tfire) { firelevel = level;} // 걍 body에 넣어봄
    int firelevel;
    // void cry(){
    //     std::cout << "Pairi!!!" << std::endl;
    // }
};

 

//Derived Classes

class pika : public electric{
public:
    pika() : electric("Pikachu",100,10) {}

    void cry(){
        std::cout << "pikapika!" << std::endl;
    }
};

class charmander : public fire{
public:
    charmander() : fire("Pairi",200,20) {height = 188;}
    int height;
};

int main()
{
    pika pok1;
    charmander pok2;

    std::cout << pok1.getName() << "\t" << pok1.getHp()<< "\t"  << typecall(pok1.getType())<< "\t"  << pok1.electriclevel << std::endl;
    std::cout << pok2.getName() <<  "\t"<< pok2.getHp() << "\t"  <<pok2.getType() << "\t" << pok2.firelevel << "\t" << pok2.height << std::endl;
    
    pok1.cry();
// Pikachu 100     0       10
// Pairi   200     1       20      188
// pikapika!

    return 0;
}

 

- 요렇게 하니까 깔끔했다. Class 끼리 꼬리에 꼬리를 무는 구조라 조금 복잡하긴 했지만, 그래도 코드는 정상적으로 동작하였다.

 

- 조금 더 욕심을 부려서 type을 숫자 말고 string으로 출력하려했는데, 위 다른 예시처럼 출력하려니까 SCOPE 문제로 ERROR가 발생했다.

 

enum Type {telec,tfire} _type; // 밑에 함수를 만들어주려고 일부로 enum Type을 글로벌하게 추가 선언

std::string typecall(Type x){
    switch(x){
        case telec : {return "electrical type";}
        case tfire : {return "fire type";}
        default : return "Unknown";
    }
}

int main(){
 std::cout << pok1.getName() << "\t" << pok1.getHp()<< "\t"  << typecall(pok1.getType());
}

// ERROR : pokeprac.cpp:77:89: error: cannot convert 'BasePokemon::Type' to 'Type'pokeprac.cpp:77:89: error: cannot convert 'BasePokemon::Type' to 'Type'

 

 

- 이 말은 즉슨, 지금 피카츄 / 파이리의 type은 Basepokemon class 안에 있는 enum Type으로 선언해놨는데, 직접 만든 typecall 함수는 글로벌하게 선언된 enum Type을 input으로 받고 있어서 Scope가 달라 Error가 발생한다는 것이다.

 

- 해결책으론 예전처럼 그냥 enum Type을 class 안이 아닌 글로벌한 영역에 only로 선언을 해 주던가,

 

std::string typecall(BasePokemon::Type x){
    switch(x){
        case 0 : {return "electrical type";}
        case 1 : {return "fire type";}
        default : return "Unknown";
    }
}

int main()
{
    pika pok1;
    charmander pok2;

    std::cout << pok1.getName() << "\t" << pok1.getHp()<< "\t"  << typecall(pok1.getType())<< "\t"  << pok1.electriclevel << std::endl;
    std::cout << pok2.getName() <<  "\t"<< pok2.getHp() << "\t"  <<typecall(pok2.getType()) << "\t" << pok2.firelevel << "\t" << pok2.height << std::endl;
    
    pok1.cry();
// Pikachu 100     electrical type 10
// Pairi   200     fire type       20      188
// pikapika!

    return 0;
}

 

 

- 위처럼 typecall 함수를 만들어주면 된다. 이 때, enum Type을 받기 위해서 Basepokemon class의 해당 영역을 Protected 영역에서 Public으로 바꿔줘야 한다.

 

- typecall 함수에선 case 마다 enum이 정수형으로 data를 저장하기에 순서대로 0, 1 이렇게 구분하던가 아니면

case BasePokemon::telec 이런식으로 구분해주면 된다.

 

 

Constructor Calls

Constructor Call 순서 : Elec Class -> Base Class -> Base Class의 Body 수행.. 쭉쭉쭉 Body수행

 

 

The Diamond Problem

 

- 다시 돌연변이 Pikamander로 돌아와보자~~

 

 

 

- 귀요미 파카츄는 getType 함수가 겹쳐서 Error가 뜬다.

(int main에 pikamander 관련해서 member를 소환만 하지 않으면 Compile Error는 뜨지 않음)

 

 

 

- 위 문제에 대한 Solution은 뭘까용?

 

 

Virtual Inheritance

class BasePokemon{

protected:
    std::string _name;
    int _hp;

public:
    enum Type {telec,tfire,mix} _type; // Pikamander 추가
    BasePokemon(std::string name,int hp,Type type) :
    _name(name), _hp(hp), _type(type) {}

    Type getType(){ return _type; }
    int getHp(){return _hp;}
    std::string getName(){return _name;}
};

class electric : virtual public BasePokemon {
public:
    int electriclevel;

    electric(std::string name, int hp, int level) : BasePokemon(name,hp,telec), electriclevel(level) {}
};

class fire : virtual public BasePokemon{
public:
    fire(std::string name,int hp,int level) : BasePokemon(name,hp,tfire) { firelevel = level;} // 걍 body에 넣어봄
    int firelevel;
};

class Pikamander : public electric, public fire {
public:
Pikamander() : BasePokemon("Pikamander",300,mix), electric("Pikamander", 300, 10),
fire("Pikamander", 300, 5) {}
};

int main()
{
Pikamander pikamander;
std::cout << pikamander.getHp()<< "  "<<typecall(pikamander.getType()) << std::endl;

// 300  mix type

 

- elec / fire에 BasePokemon 쪽을 virtual inheritance를 시켜주면, elec / fire을 상속받는 피카츄,파이리,피카맨더 모두 Constructor에 BasePokemon에 대한 default값을 initialize 해줘야 한다.

(이게 좀 구찮;;)

 

 

Constructor Calls

Elec / Fire는 Base를 Virtually inherit 해서 그 안에선 Base Constructor 실행 XXX
혼종 Memory 정리

 

 

Special Classes - Abstract Classes

 

- 대충 Base 에다가 =0 을 해서 함수를 만들어주면 이를 상속받는 class에서 동일 함수를 override한 뒤에 각 각 특성에 맞게 내용을 채워나간다.

 

- 근데 여기서 생긴 의문점, 예를 들어 지금 Base에서 cry()라는 함수에 pure virtual을 이용하고 싶다고 하면,

 

 1) virtual void cry() = 0

 2) virtual void cry(); 

 

이 둘의 차이는 뭘까? 어차피 Subclass에선 동일 함수이름을 override해서 쓸 거 아닌가??

 

순수 가상 함수 (virtual void cry() = 0;)

  • 정의: 순수 가상 함수는 함수 선언 끝에 = 0;을 붙여 정의합니다. 이는 해당 함수가 추상 함수임을 의미하며, 함수의 본체(implementation)가 없다는 것을 나타냅니다.
  • 목적: 클래스를 추상 클래스로 만들기 위해 사용됩니다. 즉, 이 클래스는 인스턴스화될 수 없으며, 오직 파생 클래스를 통해서만 사용될 수 있습니다.
  • 효과: 파생 클래스는 반드시 이 순수 가상 함수를 오버라이드하여 구현해야 합니다. 만약 구현하지 않으면, 파생 클래스 역시 추상 클래스가 됩니다.

일반 가상 함수 (virtual void cry();)

  • 정의: 일반 가상 함수는 virtual 키워드를 사용하여 선언하며, 함수 본체가 있거나 없을 수 있습니다. 본체가 없는 경우, 파생 클래스에서 반드시 구현을 제공해야 합니다.
  • 목적: 다형성을 지원하기 위해 사용됩니다. 파생 클래스에서 이 함수를 오버라이드할 수 있으며, 오버라이드하지 않는 경우 베이스 클래스의 구현을 사용합니다.
  • 효과: 파생 클래스는 이 함수를 오버라이드할 수 있지만, 오버라이드하지 않는 경우에도 에러가 발생하지 않습니다. 베이스 클래스의 구현을 그대로 사용할 수 있습니다

    class BasePokemon { public: // 순수 가상 함수
    virtual void cry() = 0; // 이 클래스를 추상 클래스로 만듦. // 일반 가상 함수
    virtual void cry() { std::cout << "BasePokemon cry"; } // 구현이 제공됨. };

    class ElectricPokemon : public BasePokemon { public: // 순수 가상 함수를 구현해야 함
    void cry() override { std::cout << "Zzzzap!\n"; } };

    class FirePokemon : public BasePokemon
    {
    // cry()를 구현하지 않음: BasePokemon의 cry()를 사용함. };

    • 순수 가상 함수: ElectricPokemon은 cry를 구현해야 하며, 이를 구현하지 않으면 컴파일 오류가 발생합니다.
    • 일반 가상 함수: FirePokemon은 cry를 오버라이드하지 않았지만, 베이스 클래스의 구현을 사용할 수 있으므로 컴파일 오류가 발생하지 않습니다.
    결론적으로, 순수 가상 함수는 파생 클래스가 해당 메소드를 반드시 구현하도록 강제함으로써, 더 명확한 계약(인터페이스)을 제공하고 클래스 설계의 의도를 명확히 합니다. 반면, 일반 가상 함수는 선택적 오버라이딩을 허용하여 유연성을 제공합니다.

 

- 이렇다고 한다. pure virtual ( = 0 )을 쓰는 순간 해당 Class를 통해 direct 하게 element를 만들지 못한다.

 

- 그리고 cry(); 로 하면 Derived Class가 cry 함수를 쓰고 정의하지 않아도 Base에 있는 걸 그대로 가져다 쓰면 되는데, cry() = 0;을 하면 Derived Class에서 cry함수를 쓰고 내용을 적어주지 않으면 Error가 뜬다.

( --> 정정, 그냥 Base가 pure virtual을 써서 abstract class가 되어버리면, 그놈들 inherit한 class들은 그 해당 method를 내부에 정의해주지 않는다면 그 class로 element 생성X 컴파일 Error O)

 

/*pokeprac.cpp:81:14: error: cannot declare variable 'poke1' to be of abstract type 'electric'
     electric poke1("AAA",100,10);
              ^~~~~
pokeprac.cpp:38:7: note:   because the following virtual functions are pure within 'electric':
 class electric : virtual public BasePokemon {*/

 

 

Frined Classes

 

 

- Friend Class, 친구에게 내 private / protected member 접근권한을 주는 것이다.

 

- OOP의 encapsulation에 어긋날 수도 있다.

(encapsulation : 서로 관련있는 것들만 따로 묶는 것! 근데 friend class를 쓰면 서로 섞일 수도 있다.)

 

- 위의 예시에서 Pikamander가 Pikachu를 friend로 선언했으니, 피카츄가 피카맨더 member에 아무렇게나 접근이 가능하다. 반대 way도 허용하려면 피카츄 class에서 Pikamander class를 friend class로 선언을 해야한다.

 

- 주로 Operator를 overloading 할 때 유용하다!

 

 

Type Casting

 

Variable의 data type을 바꿔주는 것 --> Type Casting

 

 

C - Style Casting

 

 

- 사실 이게 ElectricPokemon*을 억지로 Charmander*로 type casting 시키는 거라 좀 불안정하다.

- Compile Error는 뜨지 않는데, Runtime Error가 뜰 가능성이 농후하다.

 

 

Static Cast

 

-  Parent : ElectricPokemon // Child : Pikachu 인 경우에서 부모<->자식간 Casting은 가능하다.

 

- Upcasting : 자식놈이 부모Class쪽으로 변환  / Downcasting : 그 반대

 

- Upcasting은 안전하다! Parent * ptr = & children ; (이렇게 좌 부모, 우 자식인 업케스팅은 안전 그 잡채)

 

 - 그치만 Pikamander와 같이 부모가 둘 인 경우 서로 Casting은 Compile은 되지만 Runtime Error 의심이다.

 

 - Elect <-> Charmander와 같이 연관이 없는 경우엔 아예 Compile Error를 때려버린다.

(이것이 Static Cast의 장점, C-Style Casting이었으면 이 경우도 Compile은 진행이 된다.

 

 

Dynamic Cast

 

- 요건 runtime에서 변환을 시켜준다.

 

- Downcast일 때(부모가 자식 쪽으로 변환) 그게 가능한지 체크 해준다.

 

- 체크를 어떻게? cast fail --> null ptr ( poiner ) , throws an exception ( reference / 참조값 ) --> 요걸로 위 if 문처럼 제대로 casting 되었나 체크가 가능하다.

 

 

- 그치만 dynamic_cast 는 비싼작업이다. virtual table 가고 type check 하고 ... 필요한 상황아니면 굳이 ㄴㄴ

 

 

Const Cast

 

 

- 최소한으로 사용해야 하는 Casting이다. Compiler가 reference를 이상한 variable로 바꿀 수 있기 때문!

- const로 고정된 값도 const_cast를 이용하면 적절히 바꿀 수 있음

 

 

Reinterpret Cast

 

- 매우 매우 강력한 C++ 캐스팅 연산자!!

 

- 포인터를 다른 포인터로 변환해준다. 또는 포인터를 다른 Type으로 변환하거나 다른 Type을 포인터로 변환해준다.

(주로 메모리 주소를 특정 타입의 포인터로 캐스팅하는 데 사용됨)

 

- float 변수를 int로 casting 할 때, float f의 비트를 그대로 int 타입으로 해석하려고 시도하다보니 Compile Error가 발생한다. ( it merey(only) reinterprets the bits ) --> 그래서 위 예시에서도 unit32_t 이런 요상한 type 사용;;

 

 

Exception Handling

뭐.. 설명만 보면 좋은 기능이다. 예외처리를 해준다는 거니까

 

#include <iostream>
#include <new>

int main(){
    try{
        int* myArray = new int[100000]; // Throws an exception if allocation fails
			.....
        delete[] myArray;
    } catch (std::bad_alloc& e){
        std::cerr << "Exception : " << e.what() << std::endl;
    }
    return 0;
}
int main(){
    int a = 10;
    int b = 5;

    try{
        int x = a/b; //Trows an exception if int is divided by 0
        ///,,,,,,,,

    }catch(std::runtime_error& e){
        std::cout <<" EXCEPTION : " << e.what() << std::endl;
    }
    return 0;
}

 

 

- 계속 exception이 안 뜨길래 뭐지.. 하다가 GPT 도움 받아서 코드 재작성

 

- throw std::runtime_error("~~~")를 해줘야 이게 된다!

 

#include <iostream>

int main() {
    int a = 10;
    int b = 0;

    try {
        if (b == 0) {
            throw std::runtime_error("Division by zero error~");
        }
        int x = a / b;
        std::cout << x << std::endl;

    } catch (std::runtime_error& e) {
        std::cout << "ERROR" << std::endl;
        std::cout << "EXCEPTION: " << e.what() << std::endl;
    }
//ERROR
//EXCEPTION: Division by zero error~
    return 0;
}

 

 

 

Syntax

- 아 밑에 관련내용 나오네.. 이걸 먼저 알려주지 아으;;

 

 

- try에서 exception이 던져지는 순간 코드는 중단된다.

 

- catch는 복수로 선언해주어도 무방하다.

 

 

 

- throw를 직접 던져버리면 바로 exception handling을 할 수 있다.

 

 

#include <exception> //std::exception
#include <string>
#include <iostream>

class myexcept : public std::exception{
    int index, size;
    std::string message;
public:
    myexcept(int index, int size) : index(index),size(size){
            message = "exception is "+std::to_string(index)+" is out of range " + std::to_string(size);
    }
    const char* what() const noexcept override{ // 뭐고 이거
        return message.c_str();
    }
};

template <typename T>
T& SimpleVector<T>::operator[](int index){
    if(index<0 || index >= size){
        throw myexcept(index,size);
    }
    return array[index];
}

 

 

- 개인적으로 error message를 어떻게 던질지(?) 커스터마이징 하는 것이다.

 

 

- 명확한 exception 설정을 하라 --> 아니면 특정 data part 가 slicing 되거나, 예기치 못한 상황 발생

 

- 이왕 이면 value 말고 reference(&)로 exception check를 진행하라! --> memory leak + value check->SLOW!

 

 


 

이상 C++에 대한 내용 전반 정리 끝

 

이론은 이제 추가로 할 건 없다.

 

배운 내용을 토대로 어떻게든 실기자료를 찾아서 코딩짜는 연습에 매진할 때이다.

 

스스로 칭찬한다~

 

고생 많았다~ 여기까지 공부하고 내용정리하느라 ㅎㅎ

 

코딩실습도 화이팅 넘치게 가 즈 자아아아아ㅏ~