01. Inheritance Intro
# 기본적인 개념
1) Class relationship
2) Code reuse
3) Class Interface Consistency
: abstract / interface, pure virtual function
4) Dynamic function binding
: virtual function, virtual table
# 접근 권한 키워드 (private, public, protected)
접근 권한 키워드는
멤버 변수 및 함수에 작성하는 경우와
상속 권한에 작성하는 경우가 있다
멤버 변수 및 함수에 작성하는 경우에 주의점은
클래스 외부에서
private 멤버 변수에 접근하려면 public 멤버 함수를 통해서 접근 가능하다는 것이다
상속 권한에 작성하는 경우에는 아래와 같은 관계를 띤다
1) 상속 public
public > public
protected > protected
private > 접근 불가
2) 상속 private
public > private
protected > private
private > 접근 불가
3) 상속 protected
public > protected
protected > protected
private > 접근 불가
위의 개념을 쉽게 코드로 보여주는 코드는 아래와 같다
google에 아래와 같이 검색 시 나오는
"c++ private public protected inheritance"에 대한
stackoverflow.com의 답변 코드이다
class A
{
public:
int x;
protected:
int y;
private:
int z;
};
class B : public A
{
// x is public
// y is protected
// z is not accessible from B
};
class C : protected A
{
// x is protected
// y is protected
// z is not accessible from C
};
class D : private A // 'private' is default for classes
{
// x is private
// y is private
// z is not accessible from D
};
02. Virtual Function
# virtual 소멸자
소멸자에 virtual을 붙여주지 않는다면
부모형태로 자식을 만들 때,
자식 소멸자 호출이 안 되는 문제가 발생한다
#include <array>
#include <iostream>
class Animal
{
public:
virtual void speak()
{
std::cout << "Animal" << std::endl;
}
virtual ~Animal() = default;
};
class Cat : public Animal
{
public:
void speak() override
{
std::cout << "meow~" << std::endl;
}
};
class Dog : public Animal
{
public:
void speak() override
{
std::cout << "bark!" << std::endl;
}
};
int main()
{
std::array<Animal*, 5> animals;
for (auto& animalPtr : animals)
{
int i = 0;
std::cin >> i;
if (i == 1)
{
animalPtr = new Cat();
}
else
{
animalPtr = new Dog();
}
}
for (auto& animalPtr : animals)
{
animalPtr->speak();
delete animalPtr;
}
}
# virtual과 override
Base 클래스에서 virtual 키워드를 붙이고
상속을 받은 클래스에서 virtual 키워드는 생략이 가능하다
단, override 혹은 final 키워드를 붙여서 가독성을 높여주는 것이 일반적이다
#Dynamic Polymorphism
오브젝트를 컴파일 시간에 결정하는 것이 아닌
상속과 virtual 키워드를 이용해서 런타임 과정 중에
오브젝트를 결정하는 것을 Dynamic Polymorphism 혹은 Runtime Polymorphism이라고 한다
03. Virtual Table
# virtual 함수가 포함된 클래스의 사이즈
일반적인 클래스의 사이즈는
멤버 변수의 바이트 정렬방식에 따라 크기가 결정된다
반면,
클래스 내부에 virtual 함수가 있는 경우는
8바이트의 virtual table 주소가 추가되어 클래스의 크기가 결정된다
(참고로 실제 virtual table은 데이터 영역에 추가된다)
대부분 클래스는 정상적인 상속을 위해
vitual 소멸자를 가지기에
virtual table을 가진다고 볼 수 있다
04. Pure Virtual Function
# Pure Virtual Funtion과 Abstract 클래스
virtual 함수에 " = 0" 키워드를 작성한다면
Pure Virtual Function이 되고
Pure Virtual Function을 하나라도 가지고 있는 클래스는
Abstract 클래스라고 불린다
Abstract 클래스는 오브젝트 생성이 불가능하고
반드시 Interface 클래스에서 오버라이드를 해주어야 한다
# 유지 보수를 위한 Pure Virtual Function
Abstract 클래스에서는
모든 function들을 pure virtual function으로 만들어주고
멤버 변수를 전부 없애주는 것이 유지 보수가 쉽다
물론, 이러한 방식은
Interface 클래스에서 코드의 재사용이 발생되기에
이를 해결하려면, Implementation 클래스를 따로 구현해서
Abstract 클래스와 Implementation 클래스
둘 다 상속받는 Interface 클래스를 만들면
유지 보수가 훨씬 쉽다
// Abstract 클래스: 순수 가상 함수로만 구성되어 인터페이스 역할
class Abstract {
public:
virtual void DoSomething() = 0;
virtual ~Abstract() = default;
};
// Implementation 클래스: 기능을 구체적으로 구현하는 클래스
class Implementation : public Abstract {
protected:
int data; // 구현 세부사항을 위한 멤버 변수
public:
Implementation(int value) : data(value) {}
void DoSomething() override {
std::cout << "Doing something with data: " << data << std::endl;
}
};
// Interface 클래스: 외부에서 사용될 클래스로 Abstract와 Implementation을 다중 상속받음
class Interface : public Abstract, public Implementation {
public:
Interface(int value) : Implementation(value) {}
// Abstract의 순수 가상 함수를 `Implementation`의 구현을 통해 제공
void DoSomething() override {
Implementation::DoSomething();
}
};
int main() {
Interface obj(42); // Interface를 통해 기능을 사용
obj.DoSomething(); // "Doing something with data: 42" 출력
return 0;
}
05. Multiple Inheritance
# Linear Inheritance
하나만 상속을 받아 선형적인 형태를 띠는 경우를 의미한다
# Multiple Inheritance
다중 상속을 받는 경우를 의미한다
다중 상속의 경우는 상속받은 클래스 개수만큼
Vitual Table이 증가하며 그에 맞는 클래스의 크기를 가진다
# Diamond Inheritance의 문제
Diamond Inheritance의 경우
Animal 클래스 생성자가 두 번 호출되는 문제가 발생한다
이를 해결하려면, Lion과 Tiger은 virtual 상속을 받아야 한다
06. Virtual Inheritance
# Diamond Inheritanced의 문제점 1
Diamond Inheritance의 경우 Base Constructor가 2번 호출되기 때문에
Virtual 상속을 통해 해결할 수 있다
# Diamond Inheritanced의 문제점 2
단순히 생성자 호출 문제뿐 아니라
중복 데이터가 발생하는 것을
Virtual 상속을 통해 해결할 수 있다
# Virtual Inheritance의 원리
vitual 상속을 받지 않으면,
위에서 아래로 쌓이고
virtual 상속을 받으면,
아래에서 위로 쌓이다 보니
1번 경우(Derived Class 포인터형으로 생성)는 문제가 없지만
2번 경우(Base Class 포인터형으로 생성)는
해당 virtual table로는 Derived 클래스의 데이터를 알 수 없기 때문에
thunk 함수를 통해 오프셋을 참고하여
Derived 클래스 데이터에 접근하고 관련 함수를 호출한다
#include <iostream>
class Animal
{
public:
virtual void speak();
virtual ~Animal() = default;
private:
double animalData;
};
class Lion : virtual public Animal
{
public:
virtual void speak();
private:
double LionData;
};
int main()
{
// 1번 경우
// Lion * lionPtr = new Lion();
// lionPtr->speak();
// delete lionPtr;
// 2번 경우
Animal* polyAnimal = new Lion();
polyAnimal->speak();
delete polyAnimal;
return 0;
}
07. Object Slicing
# 상속의 문제점 1 - Object Slicing
Object Slicing이 발생하는 상황은
1) copy constructor/copy assignment 를 통해 오브젝트를 생성하는 경우
copy constructor/copy assignment에서는 virtual table을 copy 해주지 않기 때문에
copy constructor/copy assignment를 통해 오브젝트를 생성하는 경우 Object Slicing이 발생한다
#include <iostream>
class Animal
{
public:
Animal() = default;
virtual void speak()
{
std::cout << "Animal" << std::endl;
}
virtual ~Animal() = default;
public:
double animalData = 0.0f;
};
class Cat : public Animal
{
public:
Cat(double d) : catData{ d } {};
void speak() override
{
std::cout << "meow~" << std::endl;
}
public:
double catData;
};
int main()
{
Cat kitty{ 1.0 };
// kitty.speak();
Animal & animalRef = kitty;
animalRef.speak();
Animal animalObj = kitty; //copy constructor
animalObj.speak();
return 0;
}
2) 멤버 함수 인자를 값으로 받는 경우
class Base {
public:
virtual void Print() const { std::cout << "Base class" << std::endl; }
};
class Derived : public Base {
public:
int extraData = 42;
void Print() const override { std::cout << "Derived class with extraData: " << extraData << std::endl; }
};
void ProcessByValue(Base obj) { // 값으로 받는 경우
obj.Print(); // Base 클래스의 Print 호출 (Object Slicing 발생)
}
int main() {
Derived derivedObj;
// 값으로 받는 경우
ProcessByValue(derivedObj); // Slicing 발생
return 0;
}
Object Slicing 해결법
1) copy constructor/assignment를 delete로 막기
이처럼 delete로 막는 경우에는
derived 클래스끼리 copy constructor도 막아주기 때문에 좋은 결과가 아니다
따라서,
copy assignment만 delete를 하고
copy constructor는 protected에 두어서 derived 클래스끼리는 가능하게 만들어주면 된다
또 다른 해결방법으로는 clone 함수를 작성하는 방법이 있다
// 예시 1
class Base {
public:
virtual ~Base() = default;
// 복사 생성자는 protected로 선언하여 외부에서 접근하지 못하게 방지
protected:
Base(const Base&) = default;
// 복사 할당 연산자를 삭제하여 복사할 수 없도록 방지
Base& operator=(const Base&) = delete;
public:
// 다형성을 활용하여 객체를 복사할 때 사용할 clone 함수
virtual std::unique_ptr<Base> clone() const = 0;
virtual void Print() const = 0;
};
// Derived 클래스
class Derived : public Base {
private:
int value;
public:
Derived(int val) : value(val) {}
// Derived에서 clone 함수를 재정의하여 Derived 타입의 객체를 복사
std::unique_ptr<Base> clone() const override {
return std::make_unique<Derived>(*this);
}
void Print() const override {
std::cout << "Derived with value: " << value << std::endl;
}
};
int main() {
std::unique_ptr<Base> original = std::make_unique<Derived>(42);
original->Print();
// clone 함수를 사용하여 복사
std::unique_ptr<Base> copy = original->clone();
copy->Print();
return 0;
}
// 예시 2
class Animal
{
public:
Animal() = default;
//Animal(const Animal& other) = delete;
Animal& operator=(Animal other) = delete;// copy assignment
//virtual std::unique_ptr<Animal> clone()
virtual void speak()
{
std::cout << "Animal" << std::endl;
}
virtual ~Animal() = default;
protected:
Animal(const Animal& other) = default;
public:
double animalData = 0.0f;
};
class Cat : public Animal
{
public:
Cat(double d) : catData{ d } {};
void speak() override
{
std::cout << "meow~" << std::endl;
}
public:
double catData;
};
int main()
{
Cat kitty{ 1.0 };
// kitty.speak();
Animal & animalRef = kitty;
animalRef.speak();
Animal animalObj = kitty; //copy constructor
animalObj.speak();
return 0;
}
2) 멤버 함수 인자를 포인터나 참조로 받기
class Animal
{
public:
Animal() = default;
virtual void speak()
{
std::cout << "Animal" << std::endl;
}
virtual ~Animal() = default;
public:
double animalData = 0.0f;
};
class Cat : public Animal
{
public:
Cat(double d) : catData{ d } {};
void speak() override
{
std::cout << "meow~" << std::endl;
}
public:
double catData;
};
int main()
{
Cat kitty{ 1.0 };
// kitty.speak();
Animal & animalRef = kitty;
animalRef.speak();
Animal animalObj = kitty; //copy constructor
animalObj.speak();
return 0;
}
# 상속의 문제점 2 - 연산자 오버로딩
연산자 오버로딩을 Base 클래스 기준으로 만든다면,
상속받은 클래스의 데이터는 비교를 하지 않기 때문에 문제가 발생한다
따라서,
Base 클래스의 데이터와 상속받은 클래스의 데이터
모두를 비교하는 연산자가 필요하기 때문에
클래스 연산자 오버로딩을 여러 종류로 만들어 재정의를 해줘야 한다
#include <iostream>
class Animal
{
public:
Animal() = default;
virtual void speak()
{
std::cout << "Animal" << std::endl;
}
virtual ~Animal() = default;
public:
double animalData = 0.0f;
};
bool operator==(const Animal& lhs, const Animal& rhs)
{
std::cout << "animal comp" << std::endl;
return lhs.animalData == rhs.animalData;
};
class Cat : public Animal
{
public:
Cat(double d) : catData{ d } {};
void speak() override
{
std::cout << "meow~" << std::endl;
}
public:
double catData;
};
bool operator==(const Cat& lhs, const Cat& rhs)
{
std::cout << "Cat comp" << std::endl;
//add base class comparison
return lhs.catData == rhs.catData;
};
int main()
{
Cat kitty{ 1.0 };
Cat nabi{ 2.0 };
if (kitty == nabi)
{
std::cout << "same" << std::endl;
}
return 0;
}
# 상속의 문제점을 해결하는 근본적인 방법
위와 같은 상속의 2가지 문제점을 포함한
다양한 문제를 해결하는 가장 쉬운 방법은
Base Class를 pure abstract class로 만들어주면
위와 같은 문제를 바로 해결 가능하다
08. Dynamic Cast
dynamic_cast는 RTTI(Run-Time Type Information)를 사용하는데
대부분 C++ 프로젝트에서는 사용을 금지하고 있다
일단,
업캐스트(Base에 Derived를 넣는)는 문제가 없지만
다운캐스트(Derived에 Base를 넣는)의 경우에는
static_cast를 통한 다운캐스트는 매우 위험하기에
dynamic_cast를 지원해 주고
잘못된 다운캐스트를 하면 nullptr을 반환한다
dynami_cast를 통해
클래스들 각각의 virtual table에 저장되어 있는
type info의 포인터를 통해
실제 오브젝트와 캐스팅하고자 하는 오브젝트 타입이 일치하는지
비교해서 런타임 시간에 알아낸다
위처럼
dynamic_cast는 RTTI(Run-Time Type Information)를 통해 런타임에 타입을 확인한다
이 과정은 성능에 부하를 줄 수 있으며,
특히 게임 엔진이나 실시간 응용 프로그램과 같이 성능이 중요한 프로젝트에서는
이로 인해 병목 현상이 발생할 수 있다
따라서, 최대한 dynamic_cast는 사용하지 않는 것이 좋고
Base 클래스의 순수 가상함수를 오버라이드한다면
사용하는 경우가 거의 없다
#include <iostream>
class Animal
{
public:
virtual void speak()
{
std::cout << "animal" << std::endl;
}
virtual ~Animal() = default;
private:
double animalData;
};
class Cat : public Animal
{
public:
void speak() override
{
std::cout << "meow~" << std::endl;
}
void knead()
{
std::cout << "kkuk kkuk" << std::endl;
}
private:
double catData;
};
class Dog : public Animal
{
public:
void speak() override
{
std::cout << "bark!" << std::endl;
}
void wagTail()
{
std::cout << "wagging" << std::endl;
}
private:
double dogData;
};
int main()
{
// type info에서 type id를 확인하는 방법
// std::cout << typeid(Animal).hash_code() << std::endl;
// std::cout << typeid(Cat).hash_code() << std::endl;
// std::cout << typeid(Dog).hash_code() << std::endl;
Animal * animalPtr = new Animal();
Cat * catPtr = dynamic_cast<Cat*>(animalPtr);
if(catPtr == nullptr)
{
std::cout << "This is not a Cat object" << std::endl;
return 0;
}
catPtr -> speak();
catPtr -> knead();
delete catPtr;
return 0;
}
09. I/O Inheritance
c++ 라이브러리 내부에서 상속의 구조를 활용하면 좋은 코드를 작성할 수 있다
예를 들어, ostream을 통해 코드를 구현하면
ostream의 자식들인 iostream, stringstream, fstream 등에서도 활용할 수 있다
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
class Cat
{
public:
Cat(std::string name, int age) : mName{std::move(name)}, mAge{age} {};
void print(std::ostream & os)
{
os << mName << "," << mAge << "\n";
}
private:
std::string mName;
int mAge;
};
int main()
{
Cat kitty{"kitty",3};
Cat nabi{"nabi",2};
std::stringstream ss;
kitty.print(ss);
nabi.print(ss);
std::cout << ss.str() << std::flush;
// {
// std::ofstream ofs{"test.txt"};
// if(!ofs)
// {
// std::cout << "cannot open the file" << std::endl;
// return 0;
// }
// }
return 0;
}
'C++ > [노코프] C++' 카테고리의 다른 글
[C++ NOTE] Template (0) | 2024.03.30 |
---|---|
[C++ NOTE] Smart Pointer (0) | 2024.03.29 |
[C++ NOTE] OOP (0) | 2024.03.27 |
[C++ NOTE] Resource Move (0) | 2024.03.26 |
[C++ NOTE] Compile Process (0) | 2024.03.25 |