C++ 상속

2020. 11. 5. 23:04컴퓨터 수업/C++

@먼저 기본적인 소스코드를 보자면

다른건 볼 필요없고 가장 중요한건 마지막줄의 derived.print_base(); 이다.

상속받은 클래스의 인스턴스가 부모클래스의 메서드를 사용했다.


부모클래스,super클래스,base클래스(기본클래스) 모두 동의어, 상속을 "해주는" 클래스를 이와같이 표현

자식클래스,sub클래스,derived클래스(파생클래스) 모두 동의어, 상속을 "받는" 클래스를 이와같이 표현


@상속은 어떤 문법으로 이뤄지는가?

접근지정자가 상속을 해주는 직접적인 문법은 아니니, 어떻게보면 : 라는 operator가 상속을 지시하는것이라 생각할 수도 있겠다.

 

다중 상속은 , 으로 나눈다 

ex) class Parent:public child1,public child2{};


@상속 접근 지정자란 무엇인가? 그리고 상속은 접근지정자에 의해 어떤 영향을 받는가?

먼저, class의 멤버에 붙는 접근 지정자를 멤버접근지정자라 하고, 상속을 행할때 붙는 접근지정자를 상속접근지정자라고 한다면,

 

멤버접근지정자가 먼저 처리된다, 이 말이 무슨말이냐면.

 

멤버접근지정자에 따라, 상속이 될지 안될지가 결정된다. 그 외엔 어떤 영향도 미치지 않는다.

 

즉, 멤버접근지정자가

public -> 상속시 해당 멤버 상속함

private -> 상속시 해당 멤버 상속안함

protected -> 상속시 해당 멤버 상속함

 

상속접근지정자는, 상속된 멤버를 => 상속된 멤버는 현재 어떤 접근 지정자도 갖고 있지 않다고 가정하자.

 

sub클래스에 어떤 접근지정자를 갖는 멤버로 상속해야하는지를 고려해준다.

 

상속접근지정자가 private이든, public이든, protected든 상관없이, super클래스의 private멤버는 상속되지 않을것이다.

 

super클래스의 멤버가 public이였다면, 우선 상속은 성공적으로 이루어졌을것이고, 해당 멤버는 다음과 같이 상속된다.

 

상속접근 지정자가 public 이라면, public 멤버는 상속이 허용되고, 이 멤버는 앞으로 sub클래스의 public 상속접근자를 가진 멤버로써 살아간다.

상속접근 지정자가 protected라면 super클래스의 public을 가졌던 멤버는 보안이 올라간다고 생각될 수 있다. 즉, protected인 멤버는 일단 상속은 되는데, 이 멤버가 sub클래스에서 protected보다 낮은 보안등급을 가지는것은 조금은 이상하다. 따라서, private>protected>public의 rule을 따르며 상속된다.

따라서, super클래스의 protected 멤버는 public상속에서 protected멤버로 상속된다.

 


다음 코드를 보면 조금은 다른 생각이 든다...

아니 private 멤버는 위에서 상속을 아예 안한다고 했는데....

=> 상속은 하는데, 스코프의 차이라 생각하자...

 

정리하자면, 상속시, 모든게 상속된다. 그리고 그냥 도표로 외우는게 제일 좋지만.

private은 상속은 되지만, sub클래스에서 절대 접근 할 수 없다.

(그러나 super클래스의 메서드로는 접근 할 수 있어야 한다.)

또한 멤버접근제어자와 상속접근제어자 둘 중에서 더 보안등급이 높은 제어자를 찾아, 그 제어자를상속 시, 멤버의 접근제어자로 사용한다.


@friend와의 차이

 

질문은 다음과 같다.

상속받는 sub클래스는 super클래스의 변수에 접근 할 스코프를 갖게 되는것인지,

혹은 진짜로 data template를 받는것인지.

그런데 위의 코드를 보면 진짜로 data template를 받는것이라는것을 알 수 있다.

 

그러므로, freind와는 이런 차이가 있다는 것을 알 수 있다,

 frient는 scope의 확장, 즉 접근제어자의 확장인 반면, 상속은 실제로 해당 멤버들을 받는것이다.


@상속받은 sub클래스 "안에서" super클래스의 메서드를 사용하는 문법

derived 클래스의 print_base()는 소위 말하는 "오버라이딩 된 메서드"이다.

오버라이딩은 밑에서 설명하도록 하고, 해당 Derived함수가 상속의 장점 중 하나인 다형성을 지원하기 위해, super클래스의 메서드를 호출하고 싶다면("안에서") 위와같이

Super class의 클래스명 :: 사용하고자 하는 메서드

Base::print_base();

를 사용하도록 하자.


@오버라이딩 하는 방법은 알고 있으니, 오버라이딩과 오버로딩의 차이점

오버로딩은 입출력타입이 다르지만, 오버라이딩은 입출력타입이 같다. 상속의 다형성을 지원해준다.


@virtual과 override 키워드

사실 덜 배워서 그렇긴한데, 일단 virtual이라는 키워드가 그렇게 의미가 있나...? 싶다.

먼저 사용법을 보자면 자바의 interface 구현과 유사한 방식으로 메서드를 작성한다.

virtual void print(){}; 처럼,

그리고 override는 implement와 유사한데, virtual 키워드로 선언된 메서드를 오버라이딩 해주는것이다.

마치 virtual이 컴파일타임에 저 "구현" 해주셔야되요~ 라고 말하는것처럼 보이기도 한다.

override는 virtual과 달리 함수명 뒤에 적는다.

void print() override


@상속받은 sub클래스의 생성자에서 super클래스의 생성자를 사용하는 경우.

상속받은 Derived 클래스에서는 Base_x의 멤버인 base_x를 초기화 해줘야 한다. 왜 why?

 

추측)

설령 절대 접근할 수 없더라도, 게터나 세터를 통해서는 접근 해야하므로.

 

원래는 super클래스의 contructor를 sub클래스의 constructor에서 invoke하려 했지만, 이는 c++ 문법에 의해 안되었다.

//Constructor
Derived(){
Base::Base();
}

원래는 위와 같은 문법을 쓰려 했으나, 막혔다.

 

이는 아무래도 그 생성자를 메서드와 같이 사용하는것이 불가하기 때문이다.

 

지금 내 현상황으로는 new 키워드를 이용하여, heap에 Derived 클래스만큼의 크기를 할당하고, 이런 생성자를 호출하는 방법만 생성자를 사용하는 방법이다.

 

그런데 아래와 같은 코드는 작동한다.

 

왤까? 이는 Derived의 모든 생성자가 작동될 때, 컴파일러가 자동으로 Base 클래스의 default constructor를 invoke하기 때문이다. 따라서, 만약 Base 클래스의 default constructor가 아닌, 다른 사용자 지정 생성자를 사용하고 싶다면, 다음과 같은 statement를 사용해야 한다.

마치 array initialization list와 유사하게 생겼당. super클래스의 생성자를 이렇게 호출하는 이유는 sub클래스는 super클래스의 private member에 대해 접근 권한이 없기 때문이다.


@업캐스팅과 다운캐스팅

 

위의 코드의 내용은 "업캐스팅"이다. 따라서 데이터손실이 발생했다. (그런데 이건 포인터를 이용해서 캐스팅을 했기 때문에, 실제로 데이터손실이 발생하지는 않는다.) 따라서 Base는 Derived의 메서드를 사용하지 못한다.

물론 다음과 같은 stack 객체 캐스팅도 가능하다. 집에 가고 싶다.

 

캐스팅은 각 클래스의 필드를 복사해서 붙여넣는 느낌으로 생각하자.

 

예를들어 서로 다른 초기화를 수행한 클래스끼리 = 대입연산자를 사용하고 싶을때,

 

위와같은 캐스팅을 수행한 후에 수행하면 될 것이다.


@정적 바인딩과 동적 바인딩

그냥 사실 별거없다. 동적할당처럼 런타임에 무언가 하는것이 아니라, 동적으로 어떤 특정 객체를 가르키는 포인터변수가 돌아다니면서 여러 객체를 가르키는것을 바인딩이 동적으로 설정 되어있다고 한다.

 

그러나 다음과 같은 코드는 주의 할 필요가 있다.

다음과 같은 코드와 그 출력은 dynamic binding의 좋은 예시다.

 

2/3은 업캐스팅의 좋은 예시이다.

그리고 3/3은 업캐스팅의 포인터의 구현이 실제로는 데이터를 잃지는 않았다는것을 보여주는 좋은 예시이다. 다음 코드도 한번 보자. 다음 코드는 업캐스팅을 포인터가 아닌 객체 그 자체로 수행한 것이다.

 

왜 안돼냐 진짜


정적바인딩(static binding)과 동적바인딩(dynamic binding)은 일반메소드와 virtual메소드로 구분된다.

 

일반메소드는 정적바인딩된다.

 

즉 컴파일타임에 어떤 메서드를 호출할지 결정한다.

 

예를들어, 다음과 같은 코드를 보면

base->print();는 정적바인딩된 print() 메소드를 호출한다.

base는 현재 업캐스팅된 der의 포인터주소를 가르키므로, base에 있는 메서드가 호출되는것이

컴파일타임에 결정되어있다.

 

그러나, base->printv();는 동적바인딩된 printv() 메소드를 호출한다,.

base는 현재 업캐스팅된 der의 포인터주소를 가르키는데, 이때 virtual method는 정적메모리 영역에 바인딩 되어있으므로, 컴파일 타임에는 실체를 확인 할 수 없다.

그런데, 런타임에 base는 der의 메모리주소를 가르키고 있으므로, 캐스팅과 관계없이 동적메모리 바인딩 된 der의 메모리 주소에 있는 override된 printv 메소드를 호출한다.

 

이는 다음과같이 생각할 수도 있다. 컴파일타임에 der은 Base를 상속받은 Derived클래스의 인스턴스화 된 객체인데, 이때 der의 스코프는 <expand>형식으로 생각해보면 super클래스의 print, sub클래스의 print 말고는 없다. "컴파일타임" 이니까 virtual-override의 메서드는 보이지 않는다. 컴파일 타임에 업캐스팅이 수행되면서 der메모리의 스코프를 (Base*)로 줄인다 (업캐스팅) 따라서, base에서는 Base원래 super클래스에서 상속해준 method의 스코프만을 유지한다. 따라서 base->print() 메소드는 super클래스의 메소드를 호출하는 것이다. 이는 "컴파일 타임"에 모두 결정된 사항인것이다.

 

그런데 virtual은 좀 얘기가 다르다. 일단 컴파일타임에 볼 수 있는 속성이 아닌데,

der을 업캐스팅 하더라도 base는 virtual이 보이지 않는다,

그렇다면 이 virtual은 언제보이느냐? 이는 base->printv(); 라는 printv() 메소드를 호출할때 보인다.

호출타임에 base에게 어느 메모리를 참조하고있냐고 묻게되는데, base는 der 메모리를 참조하고 있다.

(설령 업캐스팅을 하고 있긴 하지만서도) 따라서 der의 implement된 메서드를 호출하는것이다.

 

다운캐스팅을 예시로 코드를 짜보면 다음과 같다.

 

내 생각을 다시 증명하자면, 포인터변수가 아닌 방식으로 virtual를 호출해도 된다.이런 예시를 아래 적자면

우선 다운캐스팅은 불가하다. 

 

왜냐?why?

 

포인터변수의 캐스팅은 서로 부족한 부분을 충족한다. 예를들어,

base를 다운캐스팅해서 der에 넣었다고 할 때, der을 호출하면 컴파일타임에 상속받았던 메서드들은 당연히 base의것을 사용한다. 그리고 "그 외의" 것들은 der것을 사용한다.

 

그러나 일반변수의 캐스팅은 서로 부족한 부분을 충족하지 않는다. der이 본인이 따로 갖고있던 메서드를 모두 버리고 base를 받아들여야되는데 이런일은 일어나지 않는다.

 

따라서 정리하자면

1. virtual 메소드는 캐스팅을 무시한다.

 

2. 비정적포인터변수의 다운캐스팅은 불가하다.

 

3. 캐스팅은 포인터변수의 경우, 멤버의 치환과정이고

비포인터변수의 경우, 멤버의 복사대입 과정이라 생각할 수 있다.

 


@객체 포인터 변수의 크기(sizeof)

뭘 하든지간에 4입니다. int+double+char로 13byte가 예상되었지만, 4byte입니다.