이 게시글은
서울대학교 데이터사이언스대학원
조요한 교수님의
데이터사이언스 응용을 위한 컴퓨팅 강의를
학습을 위해 재구성하였습니다.
이번 시간에 정리해 볼 내용은
c++에서의 class와 상속
그리고 virtual function에 관한 내용이다
교수님께서 포켓몬을 이용한 ..
예제를 들고오셨다 ㅋㅋ
c++로 이런 귀여운 애들에 대한
class를 만들어보자
각각의 attribute들과
method들은 위와 같다
class design은 다음과 같다
우선 Pokemon이라는 base class를 만든다
base class에는
name, hp, typed이라는 attributes가 있고
attack, decreaseHp, getHp, getName이라는
methods가 있다
그리고 Pokemon을 상속받은 class로
Pikachu와 Charmander를 만들어준다
Pikachu 클래스에는
pikachu만 갖고있는 electricLevel이라는
attribute와 cry라는 method가 있고
Charmander class에는
flameLevel이라는 attribute가 있다
Base Class의 기본적인 구현이다
constructor를 public으로 정의했고
기본적인 getter 메소드가 정의되어있다
함수 구현을 자세히 보자
여기서 attack method를 잘보면
input으로 opponent라는 Pokemon class의
reference를 받는 것을 알 수 있고
본인의 name과 상대방의 name을
가져와서 출력한다
이 Input에는 Pokemon을
상속받은 class도 들어갈 수 있다
reference를 넣어주기때문에
copy를 하지않아 메모리 효율을 높일 수 있다
Pokemon을 상속받은 Pikachu 클래스의
구현을 자세히보자
class Pikachu : public Pokemon
와 같이 정의해서
Pokemon이란 class를
Pikachu가 상속받을것임을 정의해준다
이렇게되면 Pikachu는
Pokemon의 모든 attribute를 상속받는다
하지만 electricLevel은
Pikachu만 갖는 attribute이니
따로 정의해준다
constructor에서 Pokemon을 먼저
초기화해준다
이 때 반드시 Pokemon을
initailizer안에 초기화해야한다
constructor내부에 하면
이미 초기화가 된 후에
초기화를 하는거라 안된다
상속받을 때의 접근제어자에 대해 알아보자
public으로 상속을 받으면
base class의 level과 똑같이
해주겠다는 뜻이다
따라서 Pokemon class에서
public -> public
protected -> protected
private -> private
이렇게 상속받게된다
protected로 받으면
base class의 Level보다
한 단계 높인 level로 상속받겠다는 뜻이다
따라서
public -> protected
protected -> private
private -> private
이 된다
마지막으로 private으로 상속받으면
모든 member를 private으로 가져오게된다
접근 제어자를 설정해주지않으면
default로 private를 설정해준다
또, base class에 이미 있는
attribute를 pikachu에서 재정의해주면
그건 서로 다른 attribute가 된다
아까 위에서 말했지만
derived class의 constructor에 대해서
다시 설명해보려고한다
parent class를 initialize 내부에서
초기화를 해주어야하고
constructor 내부에서 부를 수 없다
왜냐하면 constructor의 body에서는
이미 parent class가
초기화가 된 상태이기때문에
다시 부를 수가 없기 때문이다
만약 parent class의
constructor를 부르지 않는다면
compiler는 base class의
default constructor를 부르게된다
destructor의 호출은
derived class에 정의된
destructor가 먼저 호출된 뒤
parent class의 destructor가 호출된다
constructor와 destructor가
호출되는 순서이다
상속받은 class가 constructed될 때
parent class의 constructor가
먼저 불린 다음
상속받은 class의 constructor가 호출된다
이게 당연한게
상속받은 class는
부모 클래스로부터 상속을 받기때문에
부모 클래스가 생성되지않는다면
자식 클래스는 생성될 수가 없다
(부모없이는 자식이 있을 수가 없..)
destructor의 경우는
자식 class의 destructor가 호출되면
자식 class의 destructor가 먼저 호출된 뒤
parent class의 destructor가 호출된다
Pikachu에서 attack 함수를
정의해보자
만약 input으로 들어온
다른 Pokemon의 type이 FIRE라면
상대의 hp를 eletricLevel-2만큼 감소시킨다
옆에 짤이 아주 귀엽다
base class에 없는 메소드도
새로 정의가 가능하다
base class에는 없는
cry라는 함수를
Pikachu에 정의해보자
교수님 말씀에 따르면
피카츄는 Pika Pika하고 운단다
apparently..
Charmander class도
pikachu와 비슷하게 정의해준다
짤이 아주 구ㅣ염뽀짝하다
이제 main함수에서
어떻게 활용하며
어떻게 동작하는지 살펴보자
위 코드를 output을 찍어본 결과이다
pikachu와 charmander에서
정의해준 method들이 호출되는 것을
확인해줄 수 있다
base class의 method는
호출되지 않은 것을 확인할 수 있다
(출력이 안되어서)
dervied class에서
specific한 method들만
호출이 된 것을 확인할 수 있다
base class의 method를 쓰고싶다면
scope operation을 이용해서
base class의 method를
위 코드와 같은 방법으로 가져오면된다
상속을 받을 때도
예외 케이스가 있다
아래의 4개
constructor, destructor,
copy assignment operator,
move assignment operator
는 상속이 되지 않는다
그렇다면 이제
base class와 구현해준
child class들의 memory layout을
한 번 확인해보자
우선 base class에 있는 attribute들이
derived class에도 먼저 쌓인 다음
electricLevel, flameLevel과 같은
개별 attribute가 쌓인 걸 확인할 수 있다
그리고 pikachu와 charmander같은 경우는
원래라면 36바이트가 되어야하지만
40바이트를 차지하고 있는 것을
확인할 수 있었다
4바이트 정도 padding값이 있단걸
추측해볼 수 있다
기본적으로 메모리는
연속적으로 할당된다
memory block에는
먼저 선언이 된 것부터 순서대로
할당이 되기 때문에
derived class에서도
base class에 있는
attribute들이 먼저 할당된 뒤
다른 추가적인 attribute들이
할당된다
cpu가 메모리에 접근할 때
1바이트씩 가져오지 않는다
한 번 메모리에 접근할 때
8바이트 정도로 여러개를 가져온다
따라서 cpu가 접근할 때
메모리 접근 효율을 높여주기 위해서
compiler가 그 단위에 따라 padding과 같은
공간을 할당한다
이를 memory alignment라고
한다고 한다
이런 memory alignment는
compiler에 따라
다르게 구현될 수도 있다
Class Substitution
base class를 가지고
derived class의 시작 주소를
갖게 할 수가 있다
그래서 Pokemon을 정의한 다음
Pikachu의 method에 접근하게 할 수가 있다
하지만 Pikachu에만 있는
base class에는 없는
method들에는 접근을 못하고
base class에 정의된
method들에만
접근할 수 있다
그 이유를 한 번 잘 살펴보자
pointer변수는
메모리주소의 시작부분을 갖고있는데
그럼 시작부분에서
어디까지 가져와야하는지를
어떻게 알까
그건 바로 변수 타입을 보고 안다
pointer변수의 타입이
int이면 int의 크기만큼
접근해서 주솟값을 가져오고
string이면 string 크기만큼
접근해서 가져온다
따라서 Pokemon이라는
class의 타입으로 지정된
pointer 변수는
Pokemon타입의 사이즈만큼
주솟값을 가져오게된다
따라서 Pikachu class에
Pokemon이 갖고있지 않은
method나 attribute는
Pokemon class의
attribute와 method들보다
더 뒤의(?) 공간에 할당이 되기 때문에
Pokemon class의 size만큼 가져오면
그 이후의 Pokemon에 있는
attribute와 method들밖에 접근할 수가 없는 것이다
이런걸 class substitution이라고 한다
위의 Class Substitution을
구현한 예제 코드를 잘보자
createPokemon에서
실제로 생성되는 object는
pikachu나 charmander지만
반환값은 Pokemon의
포인터임을 확인할 수 있다
그럼 위와 같은 방식으로
Pikachu와 Charmander를
생성한 다음
함수들이 어떻게 작동하는지 ㅣ확인해보자
Pokemon 포인터를 생성한 다음
Pikachu와 Charmander의
시작 주소를 가리키도록 했다
그런 다음
attack을 시전하고
hp의 양을 출력하게 해줬다
그랬더니 출력값으로
base class에 있는 attack이 호출되고
hp도 줄어들지 않은 것을
확인할 수 있었다
그럼 우리는 base class인
Pokemon의 포인터를 사용하면서
우리가 사용하고 싶은 method는
Pikachu와 Charmander에서
재정의해준 method라는 것을
어떻게 알릴 수 있을까?
Virtual Function
그걸 가능하기 위해 나온게
바로 virtual function이라는
매우 중요한 개념이다
virtual function이란
base class의
member function을
정의해줄 때 사용하는 것인데
base class에서 정의된
virtual function들은
derived class에서
override해서 사용할 수 있는
함수들을 말한다
기본적으로 함수 반환값앞에
virtual라고 정의해주며
derived class에서
저 함수를 override 할 수 있도록
만들어주기위해서
base class의 함수들에
virtual를 붙이는 것이다
그런 다음 derived class로 가서
재정의하는 함수 뒤에
override라고 명시해주면
compiler는 이를 확인하고
부모 class에 가서 virtual함수를 확인한다
이는 객체지향(OOP)에서
아주 중요한 내용인
Polymorphism(다형성)의 개념인데
상속관계에 있는 class들이 있을 때
같은 이름을 가진 method들이
따로따로 정의가 되어있을 때
method를 호출당한 오브젝트가
뭔지 파악해서
그 오브젝트에 특화된 implementation을
호출하도록 해주는 것이다
이렇게 base class의 attack function을
virtual로 정의해주고
다시 main에서 호출해주면
이제 Pikachu는 Pikachu에서 정의된
attack 함수가
Charmander는 Charmander에서 정의된
attack함수가 호출되며
hp가 깎이는 것을 확인할 수 있다
destructor는 virtual로
설정해주는 것이 안전하다
destructor를 virtual로 설정해주지않으면
부모의 destructor가 호출이 된다
그럼 우리가 의도한대로
free가 안될 수도 있다
이전 내용에서는
operation overloading에 대해서
배웠었는데
override와 overload가
어떤 점에서 다른건지 알아보자
우선 overriding은
derived class가 base class에 있는
virtual function에 대해서
derived class specific한
implementation method를
제공해주는 것이다
이는 부모 클래스에 있는
메소드와 자식 클래스에 있는 메소드가
정확히 동일한 function signature를
갖고있어야한다
(이름, 인자, 반환값이 모두 동일해야함)
이는 주로 상속관계에서 적용이 되고
base class의 행위를
derived class에서 다르게 해주는 것이 목적이다
이제 overloading을 한 번 보자
overloading은 같은 scope 안에 있으면서
함수들의 이름이 동일하지만 인자가 다를때
발생한다
원래대로라면 인자가 다르다면
함수의 이름을 다 다르게 써줘야하는데
그 과정이 귀찮으므로
인자가 달라도 함수 이름을 같게 써주는 것을
허용해주는 것이다
override가 상속관계라면
overload는 수평적인 관계에서
동일한 scope이 있을 때 작용하고
서로 다른 인자를 갖는
동일한 이름의 함수가 있을 때
그 함수를 인자에 따라서 여러 가지 버전을 만들어
사용할 수 있게 해주는 것이 주요 목적이다
Dynamic Binding
Dynamic Binding이란
base class의 포인터로
derived class의 메소드에 접근할 때
runtime과정에서
접근하는 과정을 말한다
다시 클래스를 잘 보자
Pokemon 안에는
세 개의 virtual function이 있다
pikachu와 charmander 모두
attack과 evolve를 상속받았다
이제 저 위와 같은 구조의
상속관계에서
각 메소드들이 어떻게 호출되는지를 보자
우선 virtual function을
썼기 때문에
각 class들에는 vptr이라는
포인터가 추가된다
그리고 그 vptr이 갖고있는
메모리 주소에는
vtable이 저장되어있는데
vtable은 각각의 object에
정의되어있는 method들의
주소를 갖고있는 table이다
pikachu와 charmander에서
useItem 메소드는
override를 해주지 않았으므로
base class의 method를
가리키고 있는 것을 확인할 수 있다
그럼 Pokemon 포인터를 정의해서
Pikachu를 생성한 뒤
evolve 함수를 호출해보자
runtime에서는
무슨 일이 일어날까
그럼 우선 Pikachu의 vptr에 접근해서
vtable로 향한다
그런 다음 evolve 함수가
정의되어있는 주소를 찾아
Pikachu에 정의되어있는
evolve 메소드를 수행하게된다
useItem을 실행하게된다면
똑같이 vptr -> vtable로가서
useItem의 주솟값을 찾는다
이는 Pokemon의 useItem과 매핑되어있으므로
타고 들어가서 이를 수행하게 되는 것이다
다시 dynamic binding을 정리해보자면
object의 실제 type에 기반해서
runtime 때 의도한 class의 function을
implementation대로 호출할 수 있도록 해주는 것이다
virtual table은
function들의 코드의 위치를
갖고있는 array이고
virtual pointer는
vtable의 주솟값을 갖고있는
pointer이다
그럼 compile time에선
어떤 일이 발생할ㄲㅏ?
각각 virtual function이
들어있는 class에 대해서 vtable을 생성하고
맨 앞부분에 vptr이 들어갈
메모리 공간을 생성한다
그런 다음 vptr에는 vtable의
시작 주솟값을 넣어준다
runtime에선
vptr과 vtable을
dereferencing하는 과정을 통해
주소를 찾아가서 코드를 수행하게 된다
아까 말했던 polymorphism에 대해서
다시 짚고 넘어가보자
이러한 과정을 통해
object에 특화된 implementation을
호출할 수가 있는 것이다
runtime에 정확히 어떤 함수를
실행해야할지 정해지기때문에
이를 dynamic polymorphism이라 부르고
compile 단계에 정확히
어떤 함수를 실행할지 정해지게한다면
이를 static polymorphism이라고 부른다
그럼 vtable은
메모리 상 어디에 저장될까?
메모리 주소를 한 번 출력해봤다
vptr에 저장된 주솟값(vtable의 시작 주솟값)은
다른 밑의 attribute들이나 메소드들과 달리
0x102... 가 찍혀있는 것을
확인할 수 있다
그럼 다른 밑의 attribute, method들과는 다른
한참 앞의 어떤 메모리 공간에
저장이 되어있다는 뜻이다
보통 vtable은
code section에 저장된다
그래서 stack에 저장되는
다른 attribute, method들과는
전혀 다른 주솟값이 찍히는 것이다
시작은 포켓몬으로 귀여웠지만
뒤는 전혀 귀엽지않았던
class의 상속, virtual function, dynamic binding까지
정리 마무리.. -!
'강의 > computer science' 카테고리의 다른 글
[Computer Science/c++] Type Casting과 Exception Handling (0) | 2024.10.26 |
---|---|
[Computer Science/C++] Multiple Inheritance, Multi-Level Inheritance, Abstract Class, Friend Class (1) | 2024.10.25 |
[computer science] c++의 Copy/Move semantics, Static (0) | 2024.10.15 |
[computer science] c++의 operator overloading/연산자 구현하기 2편 (0) | 2024.10.14 |
[computer science] c++의 operator overloading/연산자 구현하기 1편 (0) | 2024.10.14 |