Chapter 13. Class Inheritance

지환·2022년 7월 24일
0
post-custom-banner

앞서 배운 class를 확장하고 수정할 수 있는 기능이 바로 class inheritance이다.
이를 통해 OOP 주요 목표 중 하나인 reusable code를 간단하게 실현할 수 있음.

솔직히 code를 복사해서써도 상속의 기능들이 가능하지만, class inheritance를 이용하는게 더 간단하다.
(심지어 class inheritance에선 class를 derive하기위해 source code에 접근할 필요도 없다.)

상속 되는 original class를 base class, 상속 받는 class를 derived class 라고 한다.

public derivation

class RatedPlayer : public TableTennisPlayer {
...
}

:를 이용한다. 앞에붙은 public이 base class가 public base class임을 나타낸다.
이를 public derivation 이라고 한다(private/protected derivation도 있음).

Public Derivation

  1. base class의 public member모두 derived class의 public member가 된다.
    : derived object를 통해 base class의 public data에도 접근할 수 있고, 함수도 사용 가능
  2. base class의 private에 있는 애들도 마찬가지로 derived class에 속해진다. 하지만 얘네는 base class의 public or protected의 함수들을 통해서만 접근할 수 있다.

쉽게말해서 public derivation에선 base class의 모든 member들이 derived class의 member가 된다. , private section에 있는 애들은 base class의 public or protected 함수들을 통해서만 접근할 수 있는 것이다.

> Derived Class에 추가하는 것

  1. derived class에서 추가된 data member와 member functions을 정의
    : 이건 뭐 그냥 추가로 필요하면 하던대로 정의하고 하면 됨.
  2. derived class만의 constructor 정의
뭐 둘 다 필수는 아니긴하나 대부분! 저 두개가 추가됨.

> 추가내용
class를 "선언"하는건, 어떤 메모리에 할당되는게 아니라 object를 만드는 방법을 정의하는 것이다.
여기서 inheritance는 base class의 object를 derived class에 추가하는 것 과 비슷하다고 한다.
뭐 그렇게 보는것도 나쁘진 않지만, ★여기★ 내용 이해하고 보니까 구현 메커니즘 이해도 잘되고 좋다.
결국 cfront에선 저렇게 해석했다는 것이고 다른 컴파일러들에도 cfront가 하는대로 구현된다는 보장은 없지만, 지금도 거의 저걸 따른다고는하니 봐두면 inheritance 특징들 구현원리 이해하기 좋다.

derived class의 Constructor

derived class의 object를 만들려면 사실 base class의 object가 무조건 먼저 만들어져야한다.
왜냐하면 derived class(object)에 base class의 member들이 포함되기 때문이다.

문제는, (1)base class의 private에 직접 접근할 수 없다는 것과 (2)base object가 derived object 이전에 만들어져야 된다는 것이다.
그래서 아래와 같이 member initializer list + base class constructor 이용한다.(해결!)

RatedPlayer::RatedPlayer(unsigned int r, const string & fn, const string & ln,
						 bool ht) : TableTennisPlayer(fn, ln, ht)
{
	rating = r;
}

아래처럼 base object를 인자로 받는다면 base class의 copy constructor를 사용한다.

RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer & tp)
						 : TableTennisPlayer(tp), rating(r)  //rating도 그냥 한번에 해봤음
{ }
이렇게 만들어진 constructor를 이용해 derived class의 object를 생성하면,
embedded base class object를 "먼저" 만들고,
"이후" derived class에 추가된 member(rating)를 초기화하며 construction을 마무리한다.

base class의 object가 무조건 먼저 만들어져야하므로,
만약 member initializer list에 base class의 constructor를 제공하지 않는다면, base class의 default constructor를 자동으로 사용한다.
즉, derived::derived(int a) { }derived::derived(int a) : base() { }와 같다.
(ctor자체가 아예 없을때 만들어지는 automatic default ctor도 base의 default ctor 사용)

그럼 만약 derived ctor에서 base class의 ctor을 호출하지 않았는데,
default가 아닌 ctor이 정의돼있어서 자동으로 호출할 default ctor이 없다면?
: 오류 발생 (그래서 default ctor은 항상 선언해두는 것이 좋다..고 어디서 본 듯)

★ Destructor 호출되는 순서
derived class의 object가 만료될때
먼저 derived class의 destructor가 호출되고, 이후 base의 desturctor가 호출된다.(당근 자동)
(derived class의 dtor이 base class의 dtor을 자동으로 호출한다.)


Is-a Relationship

public derivationis-a relationship을 구현한다.
is-a 관계라 함은, Derived is a BaseDerived\ is\ a\ Base란 뜻으로,
derived class의 object는 base class의 object도 돼야한다는 뜻이다.
그러므로 base class object에서 가능한 일은 당연히 derived class object에서도 가능하다.
즉, Banana is a Fruit 이므로 Banana class는 Fruit class를 상속받을 수 있다.

has-a 관계, is-like-a 관계, is-implemented-as-a 관계, uses-a 관계 모두 안된다.
derived가 base의 모든 특성을 가져야하는데, 위 관계에선 base의 모든 특성을 가질 수 없기 때문이다.(자세한 예시는 p.721)
has-a 관계는 "포함하는 class"의 member로 "포함되는 class" type을 선언함으로써 처리하고,
is-like-a 관계는 공통 특성만 모은 class를 따로 만들어서 그걸 각 class가 상속하는식으로 처리한다.
뭐 어쨌든, 오직 is-a relationship만 public derivation으로 구현해라.
(쉽게말해 base class의 모든 특성이 derived에 있어도 된다면 public derivation ㄱㄱ)

> BASE Pointer와 Reference
1. Base-class Pointerderived-class object를 가리킬 수 있다.(형변환 필요없음)
2. Base-class Referencederived-class object를 가리킬(?) 수 있다.(형변환 필요없음)
단, 이를 이용해 base-class의 member (public) function/data에만 접근할 수 있다.
(책엔 base의 함수에만 접근할 수 있다고만 나오는데, 여기보면 data도 안된다.)
반대(derived pointer가 base object를 가리키는 것)는 성립 안한다.

왜 반대는 성립 안하나??
: "pointer는 애초에 본인이 가리키는 범위를 알고있다."(pointer의 type으로 알 수 있음.)
그래서 base-class pointer로 derived object를 가리켰을때는, base의 함수를 호출해도 문제가 없다.
왜냐하면 derived object안엔 base의 멤버가 다 다들어있기 때문이다.(상속)
하지만 반대로 derived-class pointer가 base object를 가리킬 수 있다고 해보자.
이 경우 derived method도 pointer가 가리키는 범위에 포함되므로 derived의 method도 호출할 수 있다.
하지만 얘는 base object를 가리키고 있으므로 derived 정보를 포함하지 않아 안된다.

활용
1. base-class reference/pointer argument로 작성된 함수는 derived class object도 처리할 수 있다.
2. base class object를 derived class object를 이용해 initialize 할 수 있다.
(== base class object가 parameter인 함수에 derived class object를 넘겨줄 수 있다.)
3. base class object를 derived class object를 이용해 assign 할 수 있다.

1번은 당연하고, 2번, 3번 경우만 한번 보자.(일단 2번먼저)
`Derived A;`라고 object를 만든 뒤, `Base B(A);` 식으로 초기화하는데 써먹을 수 있단 뜻이다.
Base class에 copy constructor가 있든 없든, parameter는 `const Base &` 일 것이고,
`const Base &`는 derived class를 가리킬 수 있으니 문제없다.
Base class의 복사생성자는 매개변수로 받은 derived object에서 Base 부분만 뽑아내 초기화할 것이다.
assing도 같은 원리로 가능하다.

> upcasting/downcasting
is-a relationship을 위해 C++은 base-class pointer가 derived class의 object를 가리킬 수 있다고 했다.
그걸 upcasting이라고 한다.
downcasting은 그 반대과정인데, 앞서 정리했듯이(downcasting에선 보통 is-a 관계 성립 X 이므로),
명시하지않으면 이런 downcasting은 불가능하다.(implicit downcasting은 불가능)


Polymorphic "Public Inheritance"

상속받은 함수를 그대로 사용하는 것이 아니라, 입맛에 맞게 재정의 하고싶을 수 있다.
그럼에도 is-a 관계는 유지돼야하므로 함수를 재정의하는데 어느 정도 제약은 있지만,
재정의해두면 object에 따라 같은 이름의 다른 함수를 사용할 수 있다.

Derived Class에서 Base Class의 functions을 재정의 할 수 있는데, 이를 polymorphic이라고 한다.
아래 두 매커니즘을 사용해 polymorhic public inheritance를 구현한다.

  1. Redefinition
  2. virtual methods

1. Redefinition (Override)

Derived Class에선 Base Class의 함수들을 redefine 할 수 있다.
그냥 같은 이름의 함수를 Derived Class에 만들면 된다.
그러면 어떤 object로 호출하냐에따라 호출되는 함수가 달라진다.
redefine 되지 않은 함수라면 당연히 base class version의 함수를 사용하고,
class가 계속 상속되며 redefine됐다면, 가장 최근에 define된 version을 사용한다.

(stackoverflow.com/questions/2680369/how-is-inheritance-implemented-at-the-memory-level)
derived class에서 base class의 member function를 어떻게 사용하냐면,
derived-class object를 base-class pointer가 가리킬 수 있으니,
(derived로 함수 복사해오고 그런게 아니라)그냥 base에 정의된 함수를 그대로 사용한다.
말했듯 *this에 derived object를 넘겨줄 수 있기 때문이다.
위처럼 구현된다면 redefine은 그냥 derived만의 함수를 만드는 것에 불과하다.
둘은 구분되는 함수(애초에 class scope가 다르기때문)이니 문제될게 전혀 없다.

처음엔 함수도 다른 멤버들처럼 inherit될때 복사라도 되나 싶었는데(그렇게 생각하니 복잡)
저 링크 참고하니까 깔끔하네

> 주의
redefine은 함수 overload 같은 개념이 아니다.
derived class에서 redefine하면, 위 class들의 기존의 같은 이름의 함수들은 모두 hide된다.

hide되는건 unqualified name을 사용할때의 얘기다.
:: 사용하면 hide된 부모 class의 함수들을 접근할 수 있다.
만약 redefine하는 함수에서 기존 함수를 호출할 일이 있다면 무조건 ::를 사용해야한다.
아니면 본인을 가리키는 줄 알고, recursive call 발생.
redefine의 경우가 아니라면 unqualified name으로 호출해도 잘 호출된다.

member 함수들은 숨겨진 argument *this가 들어간다.
위처럼 scope operator를 사용해 base의 methods를 호출하든 아님 다른 member 함수를 호출하든,
class 내부에서 member 함수를 호출할때는 그냥 calling function이 받아온 this를 그대로 넘기지 싶다
(뭐 그냥 같은 class 내부니까 꼭 이렇게까지 해석안해도 이해는 되지만,
호출하는 순간부턴 this가 그렇게 쭉쭉 넘어오는게 맞는듯)

그래서 redefine시 RULE이 있다.(경험적으로 생겨난 관례같은거)
(아래와 같이 해야 헷갈리지도 않고, 오류 발생도 줄일듯.. 설계측면에서 보는거지,,,)

  1. redefine시 original prototype과 정확하게 일치하도록 한다.
    : signature나 return type이 같아야 is-a 관계에 걸맞게 base class method가 쓰이는 곳에서 derived class의 재정의된 method도 쓰일 수 있음.
    <예외> return type이 base class의 pointer/reference라면 derived class의 pointer/reference로 바꿔도 된다.(이를 "covariance of return type"이라 함)
    왜 이건 예외지?
    : (원문) return type을 같게 제한함으로써 base method가 올 위치에 derived method가 올 수 있도록했다(왜? is-a니까). 하지만 위 예외상황까지 허용해도 이는 참이므로 허용해준다.

  2. 부모 class에서 overload 함수로 구현했다면, overload된 같은 이름 함수들을 모두 redefine한다.
    : 한 version만 redefine한다면 나머지 버전들은 hide되므로 derived object가 (unqualified name으로) 사용할 수 없다.
    한 version만 수정이 필요하다면 나머지는 버전들은 redefine은 하되,
    void Hovel::pr() const { Dwelling::pr(); } 이렇게 base의 함수 호출하면 됨.

> 문제점
하지만 위에서 봤듯이 is-a 관계에 의해 base-class pointer는 derived object를 가리킬 수 있다.
만약 특정 함수가 redefine됐고, base-class pointer가 derived object를 가리키고 있을때,
redefine된 함수를 호출하면 어떻게 될까?

static binding에 의해 compile time에 bind가 이루어지고, compile time엔 그 pointer가 어떤 object를 가리키는지 알지 못하므로 base-class의 기존 함수를 호출하게 된다.

어떤 object를 가리키는지는 code가 실행돼야 pointer에 저장된걸 보고 알 수 있음.
compile time엔 pointer type을 보고 member (function)에 접근할 수 밖에 없다.

pointer/reference가 아니라 object로 접근했다면 전혀 문제없다.
애초에 object는 정확히 본인 type만 저장하기때문.
상속관계에서 pointer/reference만 제한이 살짝 느슨한 것이다.(is-a 관계 구현때문에)

2. Virtual Methods

dynamic binding을 이용해 redefine 중 발생하는 바로 위 문제 해결
그래서 redefinition과 virtual methods는 같이 쓰는 것이 관행이다.
redefine 할 거라면, 꼭 base에서 해당 함수를 virtual로 선언해야한다.

> Binding 이란?
source code에서의 함수 호출을 특정 block(함수)의 code를 실행하는 것으로 해석하는 것이다.
어셈블리어에서 함수호출하려면 그 code 주소 필요했었는데, 그렇게 함수호출문과 실제 code가 어딘지 연결해주는 작업을 말하는 것 같다.
C에선 함수 이름이 다 달라서 이 작업이 비교적 쉬웠지만, C++에선 function overload까지 있어서 좀 복잡하다.(함수이름+signature까지 보고 판단)

compilation 중에 하는 것을 static binding(early binding),
프로그램 run 중에 하는 것을 dynamic binding(late binding)이라고 한다.

> Virtual Methods
"C++에서 dynamic binding을 구현하는 방법""Virtual Methods"이다.
(기존 non-virtual methods는 static binding)
virtual keyword를 prototype 앞에 붙이면 된다. (definition엔 X)
base class에서만 ↑이렇게↑ 해도 derived class의 모든 같은 이름 함수들을 virtual로 인식하지만(derive가 연쇄적으로 일어나도 base에서만 virtual로 해두면 전부 virtual로 인식),
virtual이란걸 알리기위해 그냥 다 적는게 좋다.

virtual(로 선언된) method가 호출되면, dynamic binding이므로,
"object를 보고서 해당 object의 함수를 bind"한다.

함수 특성(?) 명시 위치 정리)
1. const 멤버 함수 : declaration "and" definition
2. default argument : declaration
3. inline 함수 : declaration "or" definition
4. virtual 함수 : declaration (에 하고 definition에 또 추가로 해도 괜찮다.)
5. friend 함수 : declaration in class

근데 그럼 dynamic binding이 더 좋은거 아닌가? 그럼에도 static binding이 default인 이유?
1. "efficiency 측면", dynamic binding은 object를 보고 추적해야하니
static에 비해 overhead가 발생할 수 밖에 없다. 그렇기때문에 필요할때만 쓰는게 맞다.
Stroustrup은 "C++에선 사용하지 않는 특징에대해 대가를 지불하지 말아야한다."고 했다.(이런 철학 반영)
쉽게말해, 필요도 없는 dynamic binding때문에 메모리, 시간낭비되면 안되니 선택할 수 있도록 한다.
2. "conceptual model 측면", virtual을 사용하거나 사용하지 않음으로써
해당 함수를 redefine 할지 안할지 알려주는 역할을 할 수 있다.

> 내부적으로 어떻게 작동하나?
C++에선 virtual functions이 어떻게 행동해야하는지만 기술한다. 구현은 compiler writer가 맡는다. 그렇기때문에 구현세부사항을 알 필욘없지만, 그래도 알면 이해하는데 도움된다.

보통 virtual function table(vtbl)이라는 virtual 함수 포인터 배열을 hidden member로 추가해서 구현한다.
해당 class에 virtual methods가 있다면 그 함수포인터를 저 배열에 추가하는 것이다.
base class의 vtbl은 상속될때 같이 내려오며, derived class에서 해당 virtual 함수를 redefine한다면 그 재정의된 함수의 포인터로 덮어씌워진다.(재정의안하면 부모꺼 그대로 쓰고)
derived class에서 base class에 없는 새로운 virtual 함수를 정의한다면 그건 따로 derived class의 vtbl에만 추가된다.(p.741 그림 참고)
object에는 vtbl의 주소만 추가되므로 object 공간을 많이 차지하진 않는다.
(vtbl의 주소를 보통 vptr이라 한다.)

이렇게 vtbl을 만들고,
virtual 함수가 호출되면, 해당 object의 vtbl을 확인해 그에 맞는 함수를 호출하는 원리이다.
(호출된 virtual 함수가 class declaration에 선언된 순서를 보고 호출한다. 첫번째 virtual 함수면 vtbl에서 첫번째 함수.. 이런식으로)

pointer/reference 사용안하고 그냥 쌩으로 class type 사용한 함수 호출은 static binding밖에안된다
dynamic binding한다고 쳐도 object를 확인하는 과정이 있는데 이미 자체가 그 object인데 뭐

> 제한사항/추가사항

  1. Constructor
    : Consturctor는 virtual이 될 수 없다. 애초에 derived class는 base-class의 constructor를 inherit하지도 않는다.
  2. Friends
    : Friends도 virtual이 될 수 없다. 왜냐하면 friend 함수는 member 함수가 아니기 때문이다. (정 그래야한다면, friend 내부에서 virtual 함수 사용)
  3. Destructor
    : "모든 base class"는 destructor가 아무 일도 안해도 virtual destructor를 선언해야한다.

왜 Base class의 Destructor virtual로?
: base-class pointer가 derived object를 "아래와 같이" 가리킨다고 해보자.
base * d_obj = new derived;
만약 이 derived object에서 new를 이용해 할당받는 공간이 있다면, 이 object가 해제될때 destructor가 호출돼야한다.
그런데 만약 저상태로 놔둔다면, delete d_obj;d_obj가 만료될때 base destructor만 호출된다.(static binding이니까..)
즉, destructor 호출이 제대로 이루어지려면 base의 destructor를 virtual로 해둬야한다.
왜 base의 destructor를 virtual로 하지?? 앞서 봤듯이 일단 "특정 함수를 호출하려고했을때, 그것이 virtual라면" 그때서야 vtbl을 활용한다.
즉, 일단 d_obj가 만료될때는 base의 destructor를 호출하려 할 것이고, 그것이 virtual라는걸 알면 그제서야 d_obj가 가리키는 object의 vtbl을 활용하게 되는 것이다.

(dtor들은 같은 이름은 아니지만 같이 분류되나보다. 그러니까 derived class의 vtbl에 들어가있는거겠지)


private에 선언된 함수 redefine?
: 좀만 생각해보면 당연히 가능한거 알 수 있음. private 함수를 redefine하는 이유는 링크 참고


protected

(p.749까지 참고해서 보자면)
protected에 포함되는 member들은 바깥에선 private처럼(public method 이용해서 접근) 취급되지만, 해당 class와 그 class를 derive하는 class에선 public처럼 바로 접근할 수 있다.
오해하면 안되는게, derived class의 public으로 추가되는게 아니다.
그냥 derived class에서 접근할 수 있을 뿐이다(public과 다른 점)

쉽게 말해, "이 class를 상속받은 class 에서는 접근할 수 있는 private"으로 보면 될 것 같다.
protected 영역은 해당 class의 object나 derived class의 object로는 접근할 수 없다.
즉, protected는 'private인데 derived class에서까지만 접근 가능하도록 해준거' 정도로 생각하면 될듯
protected는 derived class에 protected로 상속된다.
(즉, B가 A를 상속, C가 B를 상속하면 C에서도 그냥 사용가능하단 소리)

편리할때도 있긴하지만, 보통은 접근 권한을 엄격히 하는 것이 좋다.
원래하던대로 private에 둬서 base class의 public 함수들만
해당 member들에 접근할 수 있도록 제한하는 것이 "다루기 좋다."
그러므로 "private에 멤버를 두고 + 그 class의 public 함수들로만 접근하도록" 하는 방법을 선호

Abstract Base Classes (ABC)

is-a 관계이면 상속 관계로 만들어 주는게 맞지만, 실제로 적용했을때 안되는 경우도 있다.
그럴땐 두 class 사이의 공통부분만을 포함하는 class를 만들어서 그걸 각 class가 상속하도록 하면 된다.
그렇게 공통부분을 모은 class는 ABC로 잘 표현된다.

예를들어, 원과 타원을 표현한다고 해보자.
타원 class에는 각 축을 표현하는 데이터와 타원을 회전시키는 함수 등이 있다.
원은 타원의 한종류이므로 "circles is a ellipses"이지만,
타원의 회전같은 기능은 원에선 크게 필요가 없다.
그래서 상속을 하면 낭비여서 그냥 따로 선언하는게 나을 정도지만, 그러면 공통된 부분이 있다는 점이 무시된다.
이런 경우 공통 부분만 선언하는 ABC를 이용하면 좋다.

> Pure Virtual Function
class declaration 내에서 아래와 같이 선언된 함수를 pure virtual function이라고 한다.

class Base {
public:
	virtual double Area() const = 0;  //pure virtual function
...
};

두 class의 공통 정보를 모은 Base Class엔 특정 methods를 구현하기에 충분한 정보가 없을 수 있다.
하지만 그 methods도 공통된 부분이라 Base class에 포함되긴해야한다.
이럴때 위와 같이 선언하면 함수 definition을 제공하지 않을 수 있는 것이다.

pure virtual function은 아래와 같은 효과를 가진다.

  1. 최소 하나의 pure virtual function이 있다면, Abstract Base Class가 된다.
  2. 이런 pure virtual function이 있는 Class(==ABC)는 object를 생성하지 못한다.
    : pure virtual function을 가진 class는 base class로의 역할만 한다는 idea
  3. ABC에서는 pure virtual function의 definition을 제공해도되고 안해도 된다.
    ABC를 inherit하는 classpure virtual function의 definition을 제공해야한다.
즉, pure virtual functions을 사용했다는건
1. 해당 Class는 ABC (따라서 object 생성불가)
2. 해당 함수는 그 class에서 구현해도 되고 안해도 된다.
두가지 의미를 가짐.

(당연히) ABC type의 pointer모든 derived class object를 가리킬 수 있다.
array of pointer를 통해 해당 ABC를 inherit하는 class의 object들을 섞어서 다룰 수도 있다.

ABC와 반대로 object를 만들 수 있는 class를 concrete class라고 한다.

설계적인 측면에서, 공통 부분을 모은 base class로는 concrete class보단 ABC가 더 낫다.
object 생성도 안되고, 보기도 쉽고 하니까..
그래서 concrete class는 base class로 사용하지 않으면 더 깔끔하고 어렵지않은 디자인이 된다고하네

Base Class내에서 구현될 수 없는 경우에 보통 pure virtual functions으로 만들지만,
모든 함수가 구현가능한 경우인데도 일부러 ABC로 만들기위해 pure virtual functions을 만들기도한다.
-> definition이 있어도 된다고 했으니, 그냥 definition 구현하고 virtual func(~) {~} = 0;으로만 선언하면 된다.
(굳이 왜 그러냐면 위에서 말했듯 concrete보단 ABC가 base로는 더 나으니까)

> ABC의 constructor
object를 못만드니까 ctor가 없어도 되나 생각할 수 있는데, ABC에도 ctor은 있어야한다.
다시 말하자면, derived class의 object가 만들어질때 base class의 ctor를 이용해 초기화하는게 먼저다.
(base class의 private엔 직접 접근 못하니까 base class의 ctor 이용)
이는 ABC도 마찬가지이다.
그러니 자동생성되는 default ctor 쓰던가 따로 정의하던가 하면 된다.

이렇게 ABC를 만든 뒤엔 특정 공통부분을 포함하는 derived class의 base class로 사용한다.
inherit 한 뒤, 각 class에서 필요한 것들 추가해서 각 class 완성

Derived Class에서의 동적할당

1. Derived Class에서 new를 사용하지 않는 경우

base class에선 new를 사용하는데, derived class에선 new를 사용하지 않는 경우,
base class에서 dtor/copy ctor/assignment operator 잘 설정해뒀다면, 아무것도 할 필요가 없다.

왜 그런가? 아무것도 하지 않았다고 가정해보자.

(1) dtor이 없으면 default dtor가 만들어진다.
언급했듯이 (일반 dtor와는 다르게) derived class의 dtor은 실행된 후 base의 dtor을 항상 자동으로 호출한다.
따라서 derived class의 자동생성된 default dtor이 알아서 base class의 dtor을 호출할 것이므로 신경쓸 것 없다.

(2) copy ctor이 없는 경우도 마찬가지로 implicit copy ctor가 만들어진다.
얘는 memberwise copy를 하는데, derived class만의 member들은 자기들끼리 copy(assign)된다.(특정 class type이면 그 class copy ctor 사용하가며)
그럼 상속받은 부분의 member들은 어떻게 copy될까?
앞서 봤듯 derived의 ctor은 먼저 base의 ctor을 이용해 해당 부분을 초기화한다.
마찬가지로 ★이 경우엔 "base class의 copy ctor"을 이용하여 상속받은 부분의 멤버를 초기화★한다.
(자동으로 만들어진 copy ctor의 list initialization list에 base class의 copy ctor가 사용되겠지)

(3) assignment operator도 마찬가지로 implicit assignment operator가 만들어진다.
얘도 memberwise copying을 하는데, memberwise copying은 해당 data type에 정의된 방식대로 copying을 한다.
이때 상속받은 부분의 member는 base의 assignment operator를 자동으로 호출하여 초기화한다.

2. Derived Class에서 new를 사용한 경우

이 경우 지금껏 해왔듯이 해당 Class(여기선 Derived Class)에 대한 "dtor/copy ctor/assgiment operator"을 정의한다.

(1) dtor은 별거없고 그냥 delete만 잘 해주면 된다.
참고로 base class의 dtor은 derived class의 dtor이 끝날때 항상 자동호출되는 것이기 때문에 derived의 dtor에서 명시적으로 따로 호출할 필요는 없다.

(2) Copy Constructor (for derived class)

Derived::Derived(const Derived & dd) : Base(dd)
{
	...  //derived만의 data들 copy 진행
}

주목할 부분 -> : Base(dd)
마찬가지로 얘도 ctor이니 member initializer list를 이용하여 base-class의 ctor을 호출해 초기화해야한다.
여기서 사용한 것은 base-class의 copy ctor이다.
(다른 ctor도 당연히 사용가능하긴한데, 대부분은 그냥 copy ctor 사용할 거임)
base-class reference는 derived-class object를 가리킬 수 있으니,
위처럼 넘겨주어 base part만 일단 따로 copy한다.

(3) Assignment Operator (for derived class)

Derived & Derived::operator=(const Derived & dd) {
	if (this == &dd)
    	return *this;
    Base::operator=(dd);
    ...  //뭐 기존 데이터 삭제하고, new로 새로 할당받아서 copy하고, *this 반환하고

주목할 부분 -> Base::operator=(dd);
Base portion만을 assign하기위한 함수 호출이다.
그래서 일부러 scope-resolution operator까지 써가며 Base의 assignment operator를 호출한 것이다.
class 내부에서 호출한거라 저렇게 띡 써논게 어색해보일 수 있는데 기능 잘 생각해보면 이해됨.

위에서 implicit이면 자동으로 base의 assignment opeator 사용한댔는데,
아마 위의 `Base::operator=(dd);`를 자동으로 추가하는거지 않을까 싶네

> derived class에서 << operator overload하기
derived class에서 <<를 (하던대로) friend로 overload하다고 해보자. (cout)
base에 포함되는 private member에는 접근하지 못하므로,
그 member들을 print하기위해 base의 << operator function을 사용한다.

굳이 base꺼를 따로 정의 안하고 derived에서 만든 operator 함수를 base class와도 friend로 만들면?
: 되긴 하겠지만,,,
1. base에도 애초에 `<<`가 필요할 수도 있고
2. 그렇지 않은 경우라고 해도, 저렇게 만들어버리면 derived class가 다시 누군가의 base class가 됐을때
설계상 일관성이 없어지거나 이미 만들어진 class를 수정해야한다는 문제가 생긴다.

참고로 << overload할땐 friend로 하는게 보통이라 member 함수가 아니라서 ::는 사용하면안된다.
사실상 그냥 overload된 꼴이다.
그러므로 derived class에서 base class의 << operator function을 호출하려면,
os << (const Base &) dd; 식으로 type casting
을 해줘야 제대로 호출된다.

derived class에선 base의 private에 접근할 수 없으니,
derived class의 함수들은 내부적으로 base 함수 이용해서 구현하는게 거의 기본이네

> Derived에서의 Copy Ctor
링크
앞서 배웠듯이 Derived Class에서 Copy ctor을 정의하지 않는다면, implicit copy ctor이 만들어지고,
이 implicit copy ctor은 base class의 copy ctor을 사용한다.
그런데 Derived Class에서 copy ctor을 정의는 하되 base의 ctor을 member initialization list에 작성하지 않으면, base class의 default ctor을 사용한다.
(사실 이것도 배운 내용이다. 여기 초반정리된거보면 derived ctor의 멤버초기화리스트에 아무것도 안적으면 default 사용한다고 돼있음)

왜 그런가는 링크 참고,, 일단 멤버초기화 리스트 없으면 default ctor이 기본이고,
copy ctor이 아예 정의가 안돼있으면 당연히 memberwise copying해야되니까 copy ctor 쓰는 것이고..

> Special member functions의 Inheritance
inherit이 뭔데?
쉽게말해 interface에 추가되면 inherit이라고 한다.
Ctor과 Dtor은 그 class에만 해당되는 함수이므로 inherit이 되지 않는다.
없으면 자동으로 만들어지는 나머지 special member functions들도 마찬가지일까?

링크를 보면 알겠지만, assignment operator는 inherit 된다.
(p.772는 또 안된단다.. 그래 뭐 용어문제니까 어떻게 되는지나 알고 집착할 필욘 없는듯)
일단 inherit은 일어나고, derived class에서 implicit assignment operator가 가리는 것이다.
(나머지 special member functions들도 마찬가지일거라고 본다. Ctor/Dtor만 inherit 안됨.)

Q. implicit을 자동으로 만드는건 그 함수가 없어야 그런건데, inherit이 일어났는데 왜 만드는걸까?
A. 이걸보고 어느정도 의문이 해결됐다.
일단 derived class자체에는 assignment operator가 없는 것이 맞다. inherit으로 받아오더라도 결국 Base의 함수를 복사해오는게 아니라 그걸 사용할 수 있게 되는 것일 뿐이기때문이다.
assignment operator가 자동생성되지 않았다면 다른 멤버 함수들처럼 inherit돼서 쓰였을 것이다.
따라서 일단 inherit은 되는 것이고, derived class 자체에 정의가 안된건 맞으므로 derived 만의 assignment operator가 만들어진다.(redefine되는 셈)

using Base::operator=;를 derived class에 추가하면 assignment operator를 자동생성하지않고 base class의 것을 사용한다.


Class Design Review

Class 관련된 내용이 많다. 그만큼 높은 자유도로 다양한 문제에 적용할 수 있다.
그럼에도 자주 적용되는 몇몇 guidelines은 있으니 복습하며 살펴보자.

> Default Constructor
derived class의 constructor에 member initialization list를 제공하지 않으면 base class의 default constructor를 사용한다.
근데 만약 이 경우에 base class에 default가 아닌 다른 ctor이 정의돼있다면 error
(나중에 error뜨면 왜 이러나 헷갈릴만해서 적어둠)
그래서 default constructor는 항상 적당한 값으로 initialize 되도록 정의해두는게 좋다.

> Inheritance?
책에선 Inheritance"Derived Object가 Base-class method를 사용할 수 있다." 는 의미라고 한다.
멤버 method는 기존 object를 이용해 호출하지만, constructor는 그런 object를 만드는 놈이다.
그러므로 inherit되지 않는다. (특정 object로 invoke되지도 않음)

> object간의 assignment
기본 assignment operator는 같은 type일 경우에만 작용한다.
서로 다른 type끼리 assign하는 경우라면,
1. 그에 맞는 assignment operator를 따로 제공해주거나
2. default constructor의 type conversion에 의존해야한다.

> object간의 assignment (inherit 관계인 경우)
(1) Drived object를 Base object에 assign하는 것은 OK
: 일단 Base object가 invoking object이므로 Base class의 assignment operator을 사용한다.
argument로는 Base type을 받을 것이다. 근데 Base type은 Derived type을 받을 수 있으니 잘 작동하는 것이다.
(assignment 함수의 argument가 reference아니어도 됨, 여기보면 아닌 경우도 있음. reference 아니고 일반적인 type이면 copy constructor 이용해서 형변환 되니 괜찮)
(2) Base object를 Derived Object에 assign하는건 아래 조건을 만족해야 OK
Derived(const Base &);Derived & Derived::operator=(const Base &) {}가 있는 경우
(왜그런진 혼자 생각해보셈 모르겠으면 p.774)

> Destructor
new로 할당된 부분이 있다면 잘 delete해주고,
Base Class로 쓰인다면 Destructor 사용유무상관없이 무조건 virtual로 선언

> Conversion
(1) argument가 하나인 Constructor는 conversion 역할을 한다.
(2) 특정 class type을 built-in type으로 변환하고 싶다면, 특수한 conversion functions을 따로 정의해야한다.
위 둘 중 어느 경우든 conversion은 유의해서 다뤄야하며, 되도록 explicit keyword를 활용해 implicit conversion은 최대한 일어나지 않도록 해야한다.

//constructor와 conversion function이 그냥 정의돼있다면 아래 code에서
//ins를 double로 바꿔서 double addtion을 할지, 20.2를 Vector로 바꿔서 Vector addtion을 할지
//ambiguous이므로 실행되지 않는다.
Vector lux = ins + 20.2;   //ins는 Vector class object

> Reference
reference의 efficiency때문에 parameter이든 return type이든 reference를 선호
(return의 경우 지역변수라면 안되는 경우도 있으니 조심)
심지어 parameter에서 reference를 사용하면, 해당 class를 inherit하는 class object도 가리킬 수 있다.

> const
member 함수를 const로 만드는건 넘겨받는 *this를 const로 만드는 것이다.
그렇기때문에 만약 const member 함수에서 *this를 reference로 반환한다면,
*this는 const이므로 const reference로 반환해야 한다.

> protected
Stroustrup형님도 The Design and Evolution of C++ 이란 책에서 private을 쓰는게 더 낫다고 말하셨다고하네
private 주로 사용하는게 버그 범위를 줄여줌(p.778)

> virtual functions
redefine할거라면 virtual로 선언(다른 개념이긴하다만 거의 같이 따라다니며 같은 개념처럼된듯)
virtual로 했어도 code를 이상하게짜면 제대로 작동안할 수도 있다.
예를들어 Derived object를 넘겨받는 parameter가 그냥 Base type이라면(not referece/pointer),
해당 parameter에는 Base copy ctor가 Derived object를 이용해 생성한 Base object가 들어가게 되고,
그렇게되면 virtual로 설정했다고 할지라도 Derived의 redefine된 함수가 호출될 수 없다.

> friend function
member 함수가 아니므로 inherit되지 않는다.
특정 class에 소속돼서 구분되는 꼴이 아니라 그냥 사실상 함수들이 overloading되는 꼴이기때문에,
다른 class의 friend를 호출하려면 argument를 명확히해서 호출해야한다.
예를들어 argument type casting해서 호출하면 됨.

Class Function Summary

FunctionInheritedMember or FriendGenerated by DefaultCan Be VirtualCan Have a Return Type
ConstructorNoMemberYesNoNo
DestructorNoMemberYesYesNo
=NoMemberYesYesYes
&YesEitherYesYesYes
ConversionYesMemberNoYesNo
()YesMemberNoYesYes
[]YesMemberNoYesYes
->YesMemberNoYesYes
op=YesEitherNoYesYes
newYesStatic memberNoNovoid *
deleteYesStatic memberNoNovoid *
Other operatorsYesEitherNoYesYes
Other membersYesMemberNoYesYes
FriendsNoFriendNoNoYes
op=은 compound assignment operator 말하는 것. +=, *= 같은거

Chapter Review

상속하지 않는 것: Constructor, Destructor, Assignment operator, friend functions

derived class에 새롭게 추가된 data member가 없다면, derived class의 constructor는 그냥 empty body로 만들면 된다.

base-class object = derived-class object : assignment OK
void func(base-class object){~} 일때, func(derived-class object) : OK
즉, 꼭 base type 부분이 reference/pointer가 아니어도 저런 assign/argument사용 OK

전자는 assignment operator 함수의 인자가 reference/pointer가 아니어도 상관없다.
결국 둘 다 마지막에 copy ctor이 적용되면 reference가 쓰이므로,, 좀만 생각해보면 왜 되는지 앎

생각

class는 C의 structure를 확장해서 knk에서 구현했던 ADT를 아예 간단하게 만들수 있도록 했다는건 알겠다.
inheritance는 어떻게 이뤄질까?
일단 public/private/protected 뭐든 간에 derived class는 base class의 object를 포함한다. 링크1 링크2
public은 public에 object가 추가되고, private은 private에 protected는 protected에 된다고 생각하면 된다.(접근권한도 이렇게 해당 구역에 object가 만들어진다고 생각하니 쉽네, base의 private은 당연히 위 어떤 경우에서도 접근안되고,, private의 public/protected는 private이되고 나머지 경우도 좀만 생각해보면 마찬가지)
즉 쉽게말해 unnamed inherited object를 derived class내에 선언한 것처럼 포함시켜서 관리하는 것이기때문에
derived class의 ctor에서도 base ctor을 호출하는 이유가 그 object부분을 초기화하는 역할이라고 추측해본다.
즉, data member들은 그냥 base class object를 만들어버림으로써 해결하고, 함수의 경우 그냥 있는걸 그대로 이렇게 사용한다.

이게 뭐 표준에 기술된 내용인지, 정확하겐 모르겠지만 일단 대충 이런식으로 구현되는구나 알고있으면
이해하는데 도움 될듯

영단어

derive from ~에서 비롯되다, 끌어내다, 얻다
derived 파생된
furnish 비치하다, 제공하다
splendid 정말 좋은, 훌륭한

post-custom-banner

0개의 댓글