꾸역 ~ 꾸역 이해했던 저번의 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를 상속받는 것이다.
#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
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
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;)
일반 가상 함수 (virtual void 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
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++에 대한 내용 전반 정리 끝
이론은 이제 추가로 할 건 없다.
배운 내용을 토대로 어떻게든 실기자료를 찾아서 코딩짜는 연습에 매진할 때이다.
스스로 칭찬한다~
고생 많았다~ 여기까지 공부하고 내용정리하느라 ㅎㅎ
코딩실습도 화이팅 넘치게 가 즈 자아아아아ㅏ~
'SW 만학도 > C++' 카테고리의 다른 글
Review 2 - Container, Iteration in C++ (0) | 2024.07.18 |
---|---|
Review 1 - Basic Standard Library in C++(Cin/out,file I/O, String) (0) | 2024.07.16 |
8. Inheritance - Basics (0) | 2024.04.12 |
7. Copy&Move, Special Members (2) | 2024.04.12 |
6. Out_of_class Definition & Operator Overloading (0) | 2024.04.10 |