본문 바로가기
Programming/열혈 C++ 프로그래밍(저자 윤성우)

Ch 07-09. 상속의 이해, 상속의 다형성, 가상(Virtual)의 원리와 다중상속

by minjunkim.dev 2020. 8. 17.

    모든 내용은 [윤성우 저, "열혈 C++ 프로그래밍", 오렌지미디어] 를 기반으로 제 나름대로 이해하여 정리한 것입니다. 다소 부정확한 내용이 있을수도 있으니 이를 유념하고 봐주세요!


# 상속은 기존에 정의해 놓은 클래스의 재활용을 목적으로만 하는 것이 아니다.

# 클래스의 유형
1) 데이터적 성격(entity 클래스)
2) 기능적 성격(컨트롤클래스, 핸들러클래스)

# 컨트롤클래스(핸들러클래스)
1) 기능제공의 핵심이기에 모든 객체지향 프로그램에서 필수적임
2) 프로그램 전체의 기능을 담당
3) 컨트롤클래스만 봐도 프로그램 전체기능과 흐름 파악이 가능

# Entity 클래스
1) 데이터적 성격이 강함. 따라서 파일 및 데이터베이스에 저장된 데이터를 소유
2) 프로그램의 기능을 파악하는데에는 도움을 주지 못함
3) 프로그램상에서 관리되는 데이터의 종류를 파악하는데 도움이 됨
 
# 모든 소프트웨어의 설계에 있어서 중요한 것들 중 두가지
1) 요구사항의 변경에 대응하는 "유연성"
2) 기능에 추가에 따른 프로그램의 "확장성"

# 유도(자식) 클래스는 기초(부모) 클래스의 모든 멤버를 상속받는다.
1) 유도 클래스의 생성자에서는 기초 클래스의 멤버를 초기화할 의무를 지님
2) 이때, 기초 클래스의 생성자를 통해 유도 클래스를 초기화하는 것이 안정적임

즉, 유도 클래스의 멤버 이니셜라이저에서 기초 클래스의 생성자를 호출

# 클래스에서의 private, public 등의 접근제한의 기준은 "해당 클래스"이다.
따라서 상속되었더라도 직접접근이 불가능하고 public 함수를 통해 간접접근 가능한 경우 존재

# 유도 클래스의 객체 생성과정에서 기초 클래스의 생성자 호출은, 초기화 관점에서 매우 중요하다.
1) 기초 클래스의 생성자는 무조건 호출됨
2) 유도 클래스의 생성자에서 기초 클래스의 생성자 호출을 명시하지 않으면, 기초 클래스의 void 생성자가 호출됨

 

# 유도 클래스 객체의 생성과정
1) 메모리 공간 할당 진행
2) 유도 클래스 생성자 호출
3) 기초 클래스 생성자 호출 및 실행
3) 유도 클래스 생성자 실행
=> 기초 클래스의 객체 생성 이후, 유도 클래스의 객체 생성

# 항상 지켜져야 할 원칙
- 클래스의 멤버는 멤버의 해당 클래스의 생성자를 통해 초기화되어야 한다.

- 따라서 기초 클래스를 상속받은 유도 클래스가 생성자를 통해 초기화하는 상황에서도,

기초 클래스의 멤버는 기초 클래스의 생성자를 통해 초기화해야 한다.


# 유도 클래스 객체의 소멸과정
1) 유도 클래스 소멸자 호출 및 실행
2) 기초 클래스 소멸자 호출 및 실행
3) 할당된 메모리 공간 해제

=> 유도 클래스의 객체 소멸 이후, 기초 클래스의 객체 소멸

# 생성자에서 동적할당(new)한 메모리 공간은 소멸자에서 반드시 해제(delete)하자.

# C++ 접근제어 지시자(레이블)
1) private : 오로지 해당 클래스만 직접접근 가능

- 이 클래스를 상속한 유도 클래스도 직접접근은 불가능하고,

상속받은 클래스의 public 함수를 통한 간접접근은 가능하다.


2) protected : private과 동일(해당 클래스 외부에서는 직접접근 불가)하나, 유도 클래스에서는 직접접근 가능
- 제한적으로 사용되며, 기초 클래스와 유도 클래스 사이에서도

기본적으로는 "정보은닉"은 지켜지는 것이 좋음

3) public : 해당 클래스 내, 외부에서 모두 직접접근 가능

# C++ 상속의 형태
1) private 상속 : private보다 접근범위가 넓은 멤버(protected, public)는 private로 변경시켜 상속
2) protected 상속 : protected보다 접근범위가 넓은 멤버(public)는 protected로 변경시켜 상속
3) public 상속 : public보다 접근범위가 넓은 멤버(실제로 존재하지 않음)는 public으로 변경시켜 상속
=> private를 제외한 나머지는 그냥 그대로 상속(private는 직접접근 불가가 됨)


- private 멤버는 어떤 방식으로 상속되든, 유도 클래스에서의 직접접근은 불가하다.

(상속받은 기초 클래스의 public 멤버함수를 통해 간접접근만 가능)

# 상속으로 설계를 위한 조건
1) A is a B 가 만족시(B가 기초, A가 유도 클래스), 상속으로 설계
2) has a 관계 가 만족시, 상속으로 설계 가능하지만 복합관계로 대신하는 것이 일반적

# C++에서는 전역함수, 전역변수의 선언을 허용하고는 있지만,
이는 객체지향 프로그래밍을 위한 것은 아니니 가급적 사용하지 말자.

# 객체지향에서 가장 중요하다고 할 수 있는 다형성 : "문장은 같은데 결과는 다르다?"
- C++에서 AAA형 포인터변수는 AAA객체 또는 AAA를 직접 혹은 간접적으로 상속하는 모든 객체를 가리킬 수 있다.(참조자도 이와 동일함)

# 함수를 오버라이딩하면 오버라이딩 된 기초 클래스의 함수는 가려지며,
해당 함수를 호출하면 유도 클래스의 함수가 호출된다.
- 기초클래스명::함수이름 => 오버라이딩 된 기초 클래스의 함수를 호출할 수 있다.
- 상속에서 함수 오버라이딩만 있는 것은 아니다. 함수 오버로딩도 가능하다.

# C++ 컴파일러는 포인터 연산의 가능성 여부를 판단할 때,
"포인터의 자료형"을 기준으로 판단하지, "객체의 자료형"을 기준으로 판단하지 않는다.

따라서,

1) 포인터 자료형에 해당하는 클래스에 정의된 멤버에만 접근이 가능하다.
2) 함수가 오버라이딩 되었을 때, 포인터의 자료형에 따라 호출되는 함수의 종류가 달라진다.

# 가상함수
- virtual 키워드로 선언된 멤버 함수
- 가상함수를 오버라이딩 하는 함수도 모두 가상함수가 된다.(그러나 명시적으로 virtual 선언 해주자.)
- 함수가 가상함수로 선언되면,
해당 함수를 호출시 "포인터의 자료형"을 기반으로 컴파일시 호출대상을 결정하지 않고(정적 바인딩),
포인터 변수가 "실제로 가리키는 객체"를 참조하여 실행시 호출대상을 결정한다.(동적 바인딩)

# 클래스 중에는 객체 생성을 목적으로 정의되지 않는 클래스도 존재한다.
이런 클래스를 대상으로 "객체 생성이 되는 것을 방지"하기 위해 순수 가상함수 사용한다.

- 순수 가상함수란, 함수의 몸체가 정의되지 않은 함수를 의미하며, 0의 대입으로 이것을 표현

- 순수 가상함수는 virtual (함수선언부) = 0; 형식으로 선언

=> 하나 이상의 멤버함수를 순수 가상함수로 선언한 클래스를 "추상 클래스"라 한다.

# virtual 소멸자

- 상속관계에 있어서 소멸자 역시 모든 기초, 유도 클래스에 대해서 호출되어야 하나,
포인터의 자료형을 기반으로만 소멸자가 호출되면 메모리 누수의 가능성이 있다.
- 따라서, 가상 소멸자(virtual 소멸자)를 선언하면

유도 클래스 소멸자 호출 및 실행 => 기초 클래스 소멸자 호출 및 실행

의 순서로 모든 기초, 유도 클래스에 대해 소멸자가 호출 및 실행된다.


# 참조자의 참조 가능성 역시 위에서 설명한 포인터의 성질과 동일하며, 가상함수의 개념도 동일하게 적용된다.

# 프로젝트 전에 대략적인 파일의 분할원칙과 구조를 결정하고, 이를 기준으로 파일을 분할해 나가.!

# 객체가 생성되면 멤버변수는 객체 내에 존재하지만,
멤버함수는 실제로 메모리의 한 공간에 별도로 위치하고선,
이 함수가 정의된 클래스의 모든 객체가 이를 공유하는 형태를 취한다.

# 한개 이상의 가상함수를 포함하는 클래스에 대해서는 컴파일러가 "가상함수 테이블" 생성(클래스 별로 생성됨)
- "main 함수가 호출되기 이전"에 가상함수 테이블이 메모리 공간에 할당되며,
- (멤버함수의 호출에 사용되는 일종의 데이터이기 때문에) "객체 생성과 상관없이" 메모리 공간에 할당됨

# 가상함수 테이블에는 이미 오버라이딩된 가상함수에 대한 정보는 존재하지 않기 때문에,
무조건 가장 마지막에 오버라이딩을 한 유도 클래스의 멤버함수가 호출된다.

# 가상함수를 하나이상 멤버로 지니는 클래스의 객체는 가상함수 테이블의 주소값이 저장되며,
이 값을 참조하여 객체들은 가상함수 테이블을 공유한다.

# C++이 C에 비해 느린 이유는 클래스에 가상함수가 포함되면
1) 가상함수 테이블 생성되고,
2) 이 가상함수 테이블을 참조하여 호출될 함수가 결정되기 때문(동적 바인딩)

# 다중상속

- C++은 다중상속을 지원하는 객체지향 언어이지만, 가급적 사용하지 말자.
- 한 클래스를 간접적으로 두번 상속하게 되었을 때, 한번만 상속하고자 한다면 가상상속을 하자.

e.g. class (유도 클래스명):virtual public (간접적으로 두번 상속되는 기초 클래스명)


[출처] : 윤성우 저, "열혈 C++ 프로그래밍", 오렌지미디어