치춘짱베리굿나이스
[Rank 4] CPP 04 본문
CPP 04
Subtype polymorphism, abstract classes, interfaces
서브타입 다형성, 추상 클래스, 인터페이스
서브타입 다형성
포함 다형성이라고도 한다
상위 클래스의 메서드를 하위 클래스가 상속받은 뒤 다른 동작을 하게끔 재정의하여 사용하는 방법이며, 오버라이딩 (Overriding) 이라고도 한다
과제에서는 Animal 클래스를 상속받은 Dog, Cat 클래스가 각기 다른 소리를 내도록 오버라이딩하는 과정이 들어있다
가상 함수 (virtual function)
부모 클래스의 멤버 함수 중, 파생 클래스에서 재정의할 것을 기대하는 함수이다
기대 라는 단어가 뭔가 요상하기도 한데, 부모 클래스에서 이 멤버 함수가 자식 클래스에 의해 재정의될 것을 예상하고 있으니, 자식 클래스가 이 함수를 상속받아 재정의한다면 부모 클래스의 내용 대신 자식 클래스의 내용을 호출해라 라는 뜻이다
class FooClass {
public:
virtual void foo(void) {
std::cout << "foo\n";
}
};
class BarClass: public FooClass {
public:
void foo(void) {
std::cout << "bar\n";
}
};
가상 함수임을 나타내기 위해 함수의 머리에 virtual
키워드를 붙인다
언뜻 보면 굳이 안 붙여도 되지 않나? 싶은데, 가상함수를 사용하는 이유가 있다
일반적으로는 코드가 어떤 함수를 호출할 지 컴파일 단계에서 결정되며, 이를 ‘정적 바인딩’ 이라고 한다
class FooClass {
public:
void foo(void) {
std::cout << "foo\n";
}
};
class BarClass: public FooClass {
public:
void foo(void) {
std::cout << "bar\n";
}
};
// 정상적으로 작동
FooClass* foo = new FooClass();
BarClass* bar = new BarClass();
foo->foo(); // foo
bar->foo(); // bar
// 정적 바인딩으로 인한 문제 발생
FooClass* foo = new FooClass();
FooClass* bar = new BarClass(); // 타입이 부모 클래스의 포인터
foo->foo(); // foo
bar->foo(); // foo
virtual
키워드를 foo
함수에 붙이지 않았을 경우, 첫 번째 케이스에서는 bar
변수의 정적 타입이 BarClass*
이기 때문에 컴파일러도 BarClass
의 함수를 호출하고, 예상대로 “bar”가 출력된다
하지만 두 번째 케이스에서는 bar
의 생성자는 BarClass
의 생성자를 호출했음에도 불구하고 타입이 부모 클래스의 포인터인 FooClass*
이다
컴파일러는 이 정적 타입 (FooClass*
) 만을 보고 ‘아 얘는 FooClass
이구나' 라고 판단하여 FooClass
의 foo
함수를 호출하게 된다
class FooClass {
public:
virtual void foo(void) {
std::cout << "foo\n";
}
};
class BarClass: public FooClass {
public:
void foo(void) {
std::cout << "bar\n";
}
};
FooClass* foo = new FooClass();
FooClass* bar = new BarClass(); // 타입이 부모 클래스의 포인터
foo->foo(); // foo
bar->foo(); // foo
위의 잘못된 예시를 virtual
키워드를 이용하여 바로잡으면 foo
함수는 동적 바인딩이 되며, 결과적으로 부모 클래스의 foo
대신 자식 클래스에서 오버로딩된 foo
를 가져오게 된다
정적 타입, 동적 타입
- 정적 타입 언어는 컴파일 시에 타입이 결정된다
- C, C++, C#, Java 등
- 결정된 타입 (자료형) 에 맞지 않는 값이 변수에 들어올 경우 컴파일 에러가 발생한다
- 타입 관련 문제점을 초기에 발견할 수 있다
- 동적 타입 언어는 런타임 (실행 시) 에 타입이 결정된다
- Javascript, Python, Ruby 등
- 타입을 굳이 붙이지 않고 변수를 선언한다
- 변수의 내용물 (값) 에 의해 타입이 결정된다
- 예상치 못한 타입 오류가 발생할 여지가 많아진다
정적 바인딩, 동적 바인딩
정적 바인딩(Static binding) vs. 동적 바인딩(Dynamic binding)
- 정적 바인딩은 컴파일 단계에서 함수나 변수의 성질과 내용 등이 결정된다
- 함수를 호출하면 메모리상의 어느 주소로 점프해야 할 지 결정하여 바인딩한다
- 런타임 때는 해당 주소로 점프해서 내용을 바로 호출한다
- 동적 바인딩은 런타임 단계에서 함수나 변수의 성질과 내용 등이 결정된다
- 포인터 바인딩을 미뤄두고 공간만 마련해 두었다가, 런타임 시에 결정한다
- 메모리 공간의 낭비 및 타입 체킹으로 인한 속도 저하가 발생한다
순수한 가상 함수와 추상 클래스
virtual void foo() const = 0;
순수한 가상 함수 (Pure Virtual Function) 란, 가상함수를 선언하되 본체를 선언하지 않는다
이 함수는 본체가 없기 때문에 무조건 자식 클래스에서 오버라이딩을 해주어야 사용할 수 있다
순수한 가상 함수를 하나라도 포함한 클래스를 추상 클래스 (Abstract Class) 라고 하며, 추상 클래스는 동작이 정의되지 않은 멤버 함수를 포함하고 있기 때문에 인스턴스를 만들 수 없다
모든 순수한 가상 함수를 자식 클래스에서 오버라이딩해야만 내용이 존재하므로 컴파일이 된다
추상 클래스의 포인터와 참조는 사용할 수 있다 (인스턴스만 생성할 수 없을 뿐)
'42 > 42s Cursus' 카테고리의 다른 글
[Rank 4] CPP 06 (0) | 2022.09.17 |
---|---|
[Rank 4] CPP 05 (0) | 2022.09.11 |
[Rank 4] CPP 03 (0) | 2022.09.11 |
[Rank 4] CPP 02 (0) | 2022.09.11 |
[Rank 4] CPP 00 ~ 01 (0) | 2022.09.10 |