6-2. 가상(virtual) 함수와 다형성
Last updated
Last updated
C++ 에서 상속을 도입한 이유는 단순히 똑같은 코드를 또 쓰는 것을 막기위한 용도가 아니다.
더 큰 이유는 상속을 통해서 개체 사이의 관계를 표현할 수 있게 되었다.
아래 코드의 의미를 해석하자면 다음과 같다.
Manager 클래스는 Employee 의 모든 기능을 포함한다.
Manager 클래스는 Employee 의 기능을 모두 수행핧 수 있기 때문에, Manager 를 Employee 라고 칭해도 무방하다.
즉, 모든 Manager 는 Employee 이다.
따라서, 모든 상속 관계는 is a 관계라고 볼 수 있다. -> 당연하게도 이 관계를 뒤바꾸면 성립하지 않는다.
위 예제는 is a 관계의 예제로 BackAccount(은행계좌) 라는 클래스가 있고 CheckingAccount(자유출금O, 이자X), SavingsAccount(자유출금X, 이자O) 가 이를 상속받고 있다. -> 즉, 계좌라는 점은 동일하지만 약간 씩 다른 계좌 클래스들이 BackAccount 계좌를 상속받았다.
여기서 상속의 중요한 특징을 알 수 있는데, 그것은 아래와 같다.
파생클래스는 기반클래스에 비해서 구체화(specialize) 된다는 의미이다.
기반클래스는 파생클래스에 비해서 일반화(generalize) 된다는 의미이다.
하지만 모든 클래스 사이의 관계를 is a 관계로만 표현할수는 없다. 당연히 그렇지 않다.
예를 들어, 자동차 클래스는 엔진 클래스, 브레이크 클래스, 오디오 클래스 등등 수 많은 클래스들을 필요로 한다. 이들 사이에 is a 관계는 도입할 수 없다.
해당 케이스들은 has a 관계로 표시할 수 있다.
아래 코드는 has a 형식으로 클래스를 표현한 것으로 상속이 아닌, 합성을 사용했다.
아래 코드는 EmployeeList 를 표현한 것으로 EmployList 는 Employee 들을 포함하고 있으므로 has a 관계이다.
아래 코드는 기본적인 오버라이딩의 예제로 실행 시 아래 결과가 호출 된다.
실행 클래스를 아래와 같이 변경하여 실행하면 다음과 같은 결과를 얻을 수 있다.
자식 클래스에서 부모 클래스로 캐스팅 하는 것을 업 캐스팅이라고 부른다.
아래 코드에서 p 는 엄연한 Base 를 가리키는 포인터로, p 의 what 을 실행한다면 p 는 당연히 Base 의 what 을 실행해 주어야 겠구나 하고 Base what 을 실행해서 Base 의 s 를 출력하게 된다. (정적바인딩)
실행 클래스를 다운 캐스팅 형태로 변경하여 실행하게 되면, 런타임 에러가 발생하게 된다.
이는 당연한 결과이다. Derived* 가 Base 개체를 가리킨다면 p_p->what() 실행 시 Derived 의 what 함수가 호출되어야 하는데, p_p 가 가리키는 개체는 Base 개체이므로 Derived 에 대한 정보가 없다.. -> 이와 같은 문제를 막기 위해서 컴파일러 단에서 다운 캐스팅을 금지하고 있다.
아래 코드에서는 실제 개체가 해당 함수를 호출할 수 있는 상황임에도 컴파일러는 에러를 발생시킨다. -> 컴파일러 단에서 다운캐스팅을 금지한다는 이유 때문이다..
이를 해결하기 위해서는 아래 코드처럼 강제적으로 캐스팅(static_cast)을 하면 문제는 해결된다. -> 잘못 캐스팅 시 런타임 오류가 날 수 있다는 단점을 가지고 있지만, 컴파일이 가능하다! -> 런타임 오류를 방지하기 위해서 주의해서 캐스팅을 해야한다.
p 를 강제적으로 Derived * 로 캐스팅을 하게 되면, 컴파일 타임에는 문제가 일어나지는 않지만 런타임에 문제가 일어난다. -> 실제로 사용하려보니 개체가 함수를 사용할 수 없는 상태이다.
이러한 문제를 컴파일 타임에 확인하기 위해 아래와 같이 dynamic_cast 를 사용해 컴파일 타임에 문제를 확인할 수 있다.
위에서 다룬 EmployeeList 클래스 코드에서, 새로운 직원 중 하나인 Manager 가 추가해달라는 요구사항이 있을 때, 코드는 아래와 같이 변경될 수 있을 것이다.
위 와 같은 구성에서 가장 큰 문제는 직원에 해당하는 클래스가 추가될 수록 관리할 코드의 수가 반복적으로 추가된다는 사실이다.
우리는 위에서 클래스간의 업캐스팅은 자유롭게 이루어질 수 있다는 것을 알았는데, 이를 활용해 각 직원 클래스에 동일한 부모 클래스를 만들고 그 부모 클래스로 업캐스팅을 하면 코드 중복을 줄일 수 있지 않을까?
아래는 업캐스팅을 사용해 manager 클래스를 지우고, employee_list 의 내용을 출력하는 코드이다.
하지만 여기서도 하나의 문제점을 발견할 수 있는데, employee_list[i]->print_info(); 가 실제 개체의 print_info() 호출하는 것이 아닌, 부모클래스(Employee) 의 print_info() 가 호출된다는 점이다. (정적바인딩)
이를 해결하기 위해 virtual 키워드를 사용할 수 있다.
아래 코드는 기존에 비해 virtual 키워드만 추가되었을 뿐인데, 다른 결과를 보여준다.
virtual void what() 함수는 실행시(런타임), 컴퓨터 입장에서 다음과 같이 판단한다.
p_p 는 Base 의 포인터이니까 Base 의 what() 을 실행해야겠다. 오 그런데 virtual 키워드가 붙어있네? 실제 Base 개체 맞아? 아 Base 의 개체가 맞구나 Base 의 what() 을 실행해야겠다.
p_c 는 Base 의 포인터이니까 Base 의 what() 을 실행해야겠다. 오 그런데 virtual 키워드가 붙어있네? 실제 Base 개체 맞아? 아 Derived 의 개체구나 Derived 의 what() 을 실행해야겠다.
다음과 같이 컴파일 타임이(정적바인딩) 아닌, 런타임에 어떤 개체를 사용를 사용할지를 가리키는 동작을 동적바인딩이라 부른다.
C++ 11 에서는 자식 클래스에서 부모 클래스의 가상 함수를 오버라이드 하는 경우, override
키워드를 통해서 명시적으로 나타낼 수 있다.
아래와 같이 의도와 다르게 오버라이딩이 이루어지지 않을수도 있다. -> Base 의 incorrect() 와 다르게 Derived 의 incorrect() 는 상수 함수이기 때문이다.
만약 Derived 의 incorrect() 를 오버라이딩 하고자 하는 의도가 있을 때 아래와 같이 override 키워드를 붙여서 컴파일 타임에 오버라이딩을 할 수 있는 상태인지 확인할 수 있다.
아래 코드는 컴파일 타임에 override 를 할 수 없기 때문에, 다음과 같은 오류를 내린다.