Inheritance
-
[C++] Multiple Inheritance (다중 상속)2021.08.21
-
[C++] Class Access Specifier (클래스 접근 제한자)2021.08.20
[C++] Virtual Inheritance (가상 상속) : Diamond Problem (다중 상속 문제) 해결
Multiple Inheritance (다중 상속)으로 인해 Dianmond Problem (다중 상속 문제)를 야기할 수 있는데 Dianmond Problem과 해당 문제를 해결하는 방법에 대해서 배워보자.
Diamond Problem (다중 상속 문제)
Diamond Problem은 다중으로 상속을 했을 때 나타나는 문제이다.
하이브리드 동물 Liger를 클래스로 나타낸다면 아래 그림과 같을 것이다.
위 그림처럼 Tiger와 Lion은 둘 다 동물이기 때문에 Animal클래스를 상속을 받고 Tiger와 Lion의 교배로 낳은 Liger는 두 동물의 상속으로 만들어진다. 위 그림을 코드화 시키면 아래와 같다.
#include <iostream>
class Animal
{
public:
Animal()
{
std::cout << "Animal Constructor\n";
}
virtual ~Animal() = default;
private:
double animalData;
};
class Lion : public Animal
{
public:
Lion()
{
std::cout << "Lion Constructor\n";
}
virtual ~Lion() = default;
private:
double lionData;
};
class Tiger : public Animal
{
public:
Tiger()
{
std::cout << "Tiger Constructor\n";
}
virtual ~Tiger() = default;
private:
double tigerData;
};
class Liger : public Lion, public Tiger
{
public:
Liger()
{
std::cout << "Liger Constructor\n";
}
virtual ~Liger() = default;
private:
double ligerData;
};
int main()
{
Liger miniLiger;
}
위 코드를 보면 딱히 문제점이 없어 보인다. 하지만 Liger객체를 생성하여 생성자가 호출이 되는 순서를 보게 되면 이상한 현상이 일어나게 된다.
출력 화면을 보게 되면 Animal의 생성자가 2번 호출이 되는 것을 확인할 수 있다. 사실 쉽게 생각하면 Tiger와 Lion은 각각 Animal을 상속을 받았기 때문에 Animal object를 가지게 되고 또 Liger는 Tiger와 Lion을 다중 상속을 받았기 때문에 Animal 객체가 두 번 생성이 되는 것이다. 또 다른 문제는 만약 Liger 객체를 위와 같이 생성을 했다고 가정을 하면 Animal에 animalData를 접근하는 멤버 함수를 만들고 animalData를 접근하려고 한다면 생성된 두 개의 Animal객체 중 어떤 Animal객체의 멤버 함수를 호출하는가라는 문제가 또 생긴다.
이러한 현상을 방지하기 위해 우리는 Virtual Inheritance라는 것을 사용하면 위 문제를 해결할 수 있다.
Virtual Inheritance (가상 상속)
가상 상속이란 클래스 상속에서 "Virtual"키워드를 통하여 가상으로 상속을 받게 하는 방법이다. Virtual Function에서 Derived클래스에서 해당 Virtual Function에 대한 재정의를 기대한다고 말했는데 Virtual Inheritance에서도 동일하게 다른 클래스에서 해당 Base클래스를 상속받을 것을 기대한다고 보면 된다. 만약 다른 클래스에서 해당 Base클래스를 상속받지 않았을 경우는 자기가 받으면 문제는 해결이 된다.
위에 코드를 가상 상속으로 수정한 코드는 아래와 같다.
#include <iostream>
class Animal
{
public:
Animal()
{
std::cout << "Animal Constructor\n";
}
virtual ~Animal() = default;
private:
double animalData;
};
class Lion : virtual public Animal
{
public:
Lion()
{
std::cout << "Lion Constructor\n";
}
virtual ~Lion() = default;
private:
double lionData;
};
class Tiger : virtual public Animal
{
public:
Tiger()
{
std::cout << "Tiger Constructor\n";
}
virtual ~Tiger() = default;
private:
double tigerData;
};
class Liger : public Lion, public Tiger
{
public:
Liger()
{
std::cout << "Liger Constructor\n";
}
virtual ~Liger() = default;
private:
double ligerData;
};
int main()
{
Liger miniLiger;
}
위와 같이 virtual 키워드를 통해서 Diamond Problem을 해결할 수 있다.
Virtual Inheritance를 통해서 상속을 받게 되면 기존 상속과 Memory Layout이 다르다. thunk함수와 offset을 이용하여 맞는 함수를 가리키게 되는데 이 부분은 복잡하고 또한 Virtual Inhertance자체가 잘 사용이 되지 않는 기능이기 때문에 해당 Memory Layout은 영상을 참고하여 한번 보는 것으로 끝낸다. 궁금하다면 하단 레퍼런스에서 링크를 통해 보길 바란다.
Ref.
https://www.youtube.com/watch?v=0izSeUXpwDw&list=PLDV-cCQnUlIar6Wx3rkXHs7wbfmlz34Fz&index=7&t=300s
'Modern C++' 카테고리의 다른 글
[C++] Smart Pointer Intro (스마트 포인터) (0) | 2021.08.22 |
---|---|
[C++] Object Slicing (오브젝트 슬라이싱) (0) | 2021.08.22 |
[C++] Multiple Inheritance (다중 상속) (0) | 2021.08.21 |
[C++] Pure Virtual Function (순수 가상 함수), Abstract Class (추상 클래스) (0) | 2021.08.20 |
[C++] Virtual Table (가상 테이블)과 Dynamic Polymorphism (동적 다형성) (0) | 2021.08.20 |
[C++] Multiple Inheritance (다중 상속)
지금까지 다루었던 상속은 단일 상속이었다. 이번에는 다중으로 클래스를 상속받는 방법에 대해서 알아본다.
Multiple Inheritance (다중 상속)
말 그대로 Base Class가 두 개 이상이 되는 것을 의미한다. 내용은 간단하니 바로 코드를 보면서 알아본다.
Base 클래스가 두 개일 경우 생성자와 소멸자는 어떤 순서로 호출이 되는지를 보여주는 코드이다.
#include <iostream>
class Lion
{
public:
Lion()
{
std::cout << "Lion constructor\n";
}
virtual ~Lion()
{
std::cout << "Lion destructor\n";
}
private:
double lionData;
};
class Tiger
{
public:
Tiger()
{
std::cout << "Tiger constructor\n";
}
virtual ~Tiger()
{
std::cout << "Tiger destructor\n";
}
private:
double tigerData;
};
class Liger : public Tiger, public Lion
{
public:
Liger()
{
std::cout << "Liger constructor\n";
}
~Liger()
{
std::cout << "Liger destructor\n";
}
private:
double ligerData;
};
int main()
{
Liger liger();
}
Tiger와 Lion을 Base클래스로 가지는 Derived클래스 Liger는 상속의 순서를 public Tiger, public Lion으로 지정을 해주었다. 그럴 경우 아래와 같이 생성자와 소멸자가 호출이 되게 된다.
사진과 같이 앞에 있는 Tiger의 생성자가 호출이 되고 그다음 Lion 그다음 Liger의 생성자가 호출이 되는 것을 확인할 수 있다. 만약 Lion을 앞에 써주었을 경우에는 Lion의 생성자가 먼저 호출이 될 것을 유추할 수 있다.
다중 상속의 경우 해당 클래스의 사이즈는 어떤지 알아보자. 위 코드에서 상속받은 두 클래스의 사이즈를 각각 알아보면 Lion클래스 사이는 1개의 double형 멤버 변수 8바이트와 virtual함수로 인한 virtual table을 가리키는 포인터 8바이트 총 16바이트이다. Tiger클래스 또한 double형 멤버 변수 1개와 virtual 멤버 함수로 인한 virtual table을 가리키는 포인터 8바이트 총 16바이트이다. Liger클래스에 double형 멤버 변수가 존재하니 통 40바이트이다.
그러하면 궁금증이 두 개의 클래스를 상속을 받아 virtual table을 가리키는 포인터 두 개 존재하게 되는데 어떻게 작용하는지 궁금할 수 있다. 그것에 대해 알아보자.
클래스 자체의 코드는 생략하고 main함수의 코드만 가지고 예를 들어본다. ㅇ
int main()
{
Tiger* polyTiger = Liger();
delete polyTiger;
return 0;
}
지난 포스트에서 Base클래스 포인터 형으로 Derived 객체를 생성할 수 있다고 배웠다. 위 예는 두 개의 Base 클래스에서 Tiger 클래스 포인터를 이용하여 Liger객체를 생성한 코드이다. 이 경우 아래 사진과 같이 Tiger 오브젝트에 관한 메모리는 보이지 않고 Liger와 Tiger만 접근이 가능하다. 그러하기 때문에 Tiger객체에 있는 포인터가 Liger Virtual Table을 가리키게 된다.
다음 예제로 Lion 클래스형 포인터로 Liger 객체를 생성하였을 때 접근 가능한 메모리와 어떠한 포인터가 Virtual Table을 가리키게 되는지를 알아본다.
int main()
{
Lion* polyLion = Liger();
delete polyLion;
return 0;
}
Lion클래스 포인터로 Liger객체를 선언했다. 아래는 해당 내용의 메모리와 Virtual Table을 어떻게 가리키게 되는지 표현하는 그림이다.
마지막으로 Liger클래스 포인터로 Liger객체를 선언할 경우이다.
int main()
{
Liger* polyLiger = Liger();
delete polyLiger;
return 0;
}
위 경우는 모든 Base클래스 오브젝트가 접근이 가능하고 두 Base클래스의 포인터가 모두 Liger Virtual Table을 가리키게 된다.
'Modern C++' 카테고리의 다른 글
[C++] Object Slicing (오브젝트 슬라이싱) (0) | 2021.08.22 |
---|---|
[C++] Virtual Inheritance (가상 상속) : Diamond Problem (다중 상속 문제) 해결 (0) | 2021.08.21 |
[C++] Pure Virtual Function (순수 가상 함수), Abstract Class (추상 클래스) (0) | 2021.08.20 |
[C++] Virtual Table (가상 테이블)과 Dynamic Polymorphism (동적 다형성) (0) | 2021.08.20 |
[C++] Virtual Function (가상 함수) (0) | 2021.08.20 |
[C++] Virtual Table (가상 테이블)과 Dynamic Polymorphism (동적 다형성)
클래스의 크기(Memory Size)를 통해서 Virtual Table 혹은 vTable에 대해서 알아보자.
먼저 아래 코드를 보면
#include <iostream>
class Animal
{
public:
void speak()
{
std::cout << "Animal\n"
}
private:
double height;
};
int main()
{
std::cout << sizeof(Animal) << '\n';
return 0;
}
*double의 크기는 8바이트로 간주
main함수에서 sizeof() 연산자를 이용하여 Animal클래스의 크기를 출력을 해보면 8바이트가 출력이 된다. 이는 Animal클래스가 private 하게 가지고 있는 double형 멤버 변수 height에 의한 것이다.
어쩌면 의문이 드는 것이 spaek() 함수에 대한 메모리는 저장이 되지 않는 것인가?이다. 멤버 함수는 일반 함수와 동일하게 "code"영역에 저장이 되게 된다. 객체가 생성될 때마다 매번 멤버 함수가 할당이 되면 비효율적이기 때문에 "code"영역에 저장하고 공유를 한다.
저번 포스팅에서 공부한 Virtual Function을 이용하여 상속이 될 경우 클래스의 크기는 어떻게 변할까?
class Animal
{
public:
virtual void speak()
{
std::cout << "Animal" << std::endl;
}
private:
double hetght;
};
class Dog : public Animal
{
public:
virtual void speak() override
{
std::cout << "Woof" << std::endl;
}
private:
double weight;
};
int main()
{
std::cout << "Animal size : " << sizeof(Animal) << std::endl;
std::cout << "Dog size : " << sizeof(Dog) << std::endl;
return 0;
}
위 코드를 실행시켜볼 경우 Animal 클래스의 사이즈는 16바이트, Dog 클래스의 사이즈는 26바이트로 출력이 된다. 이는 Virtual Function을 사용하여 Virtual Table이 생성이 되고 이를 가리키는 pointer가 객체의 메모리 영역에 생기기 때문이다.
이해하기 쉽게 아래 그림으로 클래스에서 어떻게 Virtual Function이 호출이 되는지 그림으로 나타냈다.
Dynamic Polymorphism (동적 다형성)
프로그래밍에서 말하는 다형성은 사실 생물학에서 가져온 용어이다. 동일한 종(species) 중에서도 다양한 변이를 가지는 현상을 만한다. C++에서는 Polymorphism은 동일한 객체 혹은 함수가 특정 상황에서는 다른 게 작동하는 것을 의미한다. 우리는 이미 이런 특성을 공부하지 않았는가? 결국엔 객체들이 Polymorphism을 가지게 하기 위해서는 Virtual Function을 Override 함으로써 다형성을 얻을 수 있다.
그럼 여기서 Dynamic은 무엇을 뜻하느냐? Polymorphism을 Compile-time이 아닌 Run-time에 얻는 것을 의미한다. C++ 같은 객체지향 언어는 상속이 이루어진 클래스의 Base클래스 포인터 타입에 Derived 클래스의 주소를 가리키는 것이 가능하기 때문에 동적으로 다형성을 취득할 수 있다.
아래 코드는 Virtual Function으로 Polymorphism을 가지게 하고 Animal 포인터 타입 변수로 사용자의 입력에 따라 동적으로 객체를 생성하게 하여 Dynamic Polymorphism을 얻게 한 코드이다.
#include <iostream>
class Animal
{
public:
virtual void speak() const
{
std::cout << "Animal" << std::endl;
}
private:
double hetght;
};
class Dog : public Animal
{
public:
virtual void speak() const override
{
std::cout << "Woof" << std::endl;
}
private:
double weight;
};
int main()
{
Animal* animalPtr = nullptr; //Animal *형 변수를 만들어 null pointer로 초기화
int i;
std::cout << "which one do you want to create?\n1. Animal\n2. Dog\n";
std::cin >> i;
if (i == 1)
{
animalPtr = new Animal();
}
else if (i == 2)
{
animalPtr = new Dog();
}
//사용자가 1을 넣으면 Animal객체가 생성이 되고 2를 입력하면 Dog객체가 생성이 된다
//동적으로 만들어진 객체는 Heap영역에 생성이 되고 stack영역에 있는 animalPtr이 객체를 가리키게 된다.
animalPtr->speak();
delete animalPtr; //동적할당 메모리 해제
return 0;
}
Ref.
'Modern C++' 카테고리의 다른 글
[C++] Multiple Inheritance (다중 상속) (0) | 2021.08.21 |
---|---|
[C++] Pure Virtual Function (순수 가상 함수), Abstract Class (추상 클래스) (0) | 2021.08.20 |
[C++] Virtual Function (가상 함수) (0) | 2021.08.20 |
[C++] Class Access Specifier (클래스 접근 제한자) (0) | 2021.08.20 |
[C++] Class Keywords (클래스 키워드) : const, explicit (0) | 2021.08.19 |
[C++] Class Access Specifier (클래스 접근 제한자)
Class에는 Access Specifier (접근 제한자)가 존재한다. 상속에 관련된 내용을 공부하기 전에 접근 제한자와 상속관계의 접근 제한자에 대해서 포스팅한다.
먼저 기본적인 클래스의 접근 제한자에 대해서 알아보면 접근 제한자는 총 3가지가 존재한다. public, protected, private이 있다.
Public
public 접근 제한자에 선언된 멤버 변수 혹은 멤버 함수는 어디에서든지 접근이 가능하다.
Protected
protected에 선언된 멤버 함수 혹은 멤버 변수는 클래스 내부와 상속된 클래스에서 섭근이 가능하다.
Private
private에 선언된 멤버 변수 혹은 멤버 함수는 오직 클래스 내부에서 접근이 가능하다.
*접근 권한자를 명시해주지 않을 경우 private으로 간주한다.
#include <iostream>
class Dog
{
public:
Dog() = default;
Dog(int age) : mAge{ age } {};
void public_print() const
{
protected_print();
}
protected:
void protected_print() const
{
std::cout << "The dog is " << mAge << " years old\n";
}
private:
int mAge;
};
int main()
{
Dog choco{ 9 };
choco.public_print();
}
위 코드에서 클래스를 먼저 보게 되면 public, protected와 private 접근 제한자가 모두 사용된 것을 볼 수 있다. 먼저 private을 보면 integer형 멤버 변수 mAge가 선언되었다. 해당 멤버 변수는 클래스 내부에서만 접근이 가능하다. 다음으로 protected를 보게 되면 protercted_print() 멤버 함수가 존재한다. 이 멤버 함수에서는 private에 mAge를 불러와 출력을 하는 함수이다. protected 또한 현재 상속된 클래스가 존재하지 않기 때문에 클래스 내부에서만 접근이 가능하다. 마지막으로 public을 보면 기본 생성자와 integer 변수를 파라미터로 가지는 생성자가 있다. 그다음으로 public_print() 멤버 함수가 존재한다. 해당 멤버 함수에서 protected에 존재하는 protected_print() 멤버 함수를 호출하게 되는 멤버 함수이다.
결론적으로 main함수에서 choco 객체를 생성하고 public에 있는 public_print()를 호출하고 그다음 protected에 protected_print() 함수를 호출하여 private에 있는 mAge를 불러와 출력을 하게 된다.
Inheritance (상속)
상속에 가장 기초인 상속 접근 제한자에 대해서 간략하게 말하자면 아래 코드와 같다.
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
};
위 코드는 Stack overflow의 "Difference between private, public and protected inheritance" 질문에서 발췌한 코드이다.
상속을 받기 위해서 어떻게 상속을 받을 것이냐를 정해주는 것이 접근 제한자이다. 각 접근 제한자에 대해서 상속받은 클래스 B, C, D에 대해서 A클래스의 멤버들의 접근 권한을 코드의 주석으로 나타냈다.
Ref.
'Modern C++' 카테고리의 다른 글
[C++] Virtual Table (가상 테이블)과 Dynamic Polymorphism (동적 다형성) (0) | 2021.08.20 |
---|---|
[C++] Virtual Function (가상 함수) (0) | 2021.08.20 |
[C++] Class Keywords (클래스 키워드) : const, explicit (0) | 2021.08.19 |
[C C++] Overloading (오버 로딩) (0) | 2021.08.19 |
[C++] Copy / Move Constructor & Assignment (복사, 이동 생성자와 대입 연산자) (0) | 2021.08.19 |