가상 함수와 오버라이딩
오버라이딩이란 파생 클래스에서 기본 클래스에 작성된 가상함수를 재작성하여, 기본 클래스의 가상 함수를 무력화 시키는 것이다.
기본 클래스 포인터를 사용하든, 파생 클래스 포인터를 사용하든 파생 클래스에 오버라이딩된 함수가 항상 실행된다.
오버라이딩시에 virtual 키워드를 사용하는데, 이는 자신의 호출바인딩을 실행시간까지 미루도록 지시한다.
함수 재정의와 다르다!
함수 재정의는 컴파일 시간 다형성을 실현하고, 오버라이딩은 실행 시간 다형성을 실현한다.
오버라이딩 사례
#include <iostream>
using namespace std;
class Base {
public:
virtual void f() {
cout << "Base f()" << endl;
}
};
class Derived : public Base{
public:
virtual void f() {
cout << "Derived f()" << endl;
}
};
int main(void) {
Derived d; //파생 객체 생성
Derived *pder; //파생 포인터
pder = &d; //파생 포인터로 파생 객체 가리킴
Base *pbase; //기본 포인터
pbase = pder; //(업캐스팅)기본 포인터로 파생 객체 가리킴
pbase->f();
}
이 코드를 보자.
파생 클래스의 포인터 pder, 그리고 기본 클래스의 포인터 pbase는 둘다 d라는 파생 클래스 객체를 가리키고 있다.
pbase->f()을 통해서 f()함수를 실행하면, 기본 클래스에 있는 f()함수가 실행될 것 같지만, 파생 클래스의 f()가 실행된다.
Why?
기본 클래스의 f()함수를 virtual 키워드로 선언했기 때문에, 이 함수는 오버라이딩 될 것이다.
즉, 기본 클래스의 f()함수는 무시당하고 파생클래스의 f()가 실행된다. Base의 f()함수에 대한 호출은 실행시간중에 Derived의 f()함수를 실행하도록 동적바인딩 된다.
오버라이딩의 목적
기본 클래스에 가상 함수를 만드는 목적은, 기본 클래스에서 파생 클래스에서 구현해야할 함수의 인터페이스를 제공한다고 보면 된다.
하나의 틀에 대해서 서로 다른 모양의 구현이라고 생각하면 된다.
C++ 오버라이딩의 특징
- 오버라이딩 성공 조건
- 오버라이딩시 virtual 지시어 생략 가능
- 가상함수 접근 지정
가상 함수의 이름과 매개 변수 타입, 개수, 리턴타입이 일치해야 오버라이딩이 성공한다.
파생클래스에서는 virtual키워드를 생략해도 된다.
가상 함수도 3가지 접근지정 private public protected 로 지정 가능하다.
오버라이딩과 범위 지정 연산자 (::)
오버라이딩에 의해서 기능을 상실한, 무력화된 기본 클래스의 가상함수는 더이상 실행할 수 없는가?
아니다. 범위 지정 연산자를 통해서 실행할 수 있다. 정적 바인딩을 하게 된다.
이 경우는 기본 클래스의 가상 함수를 그대로 활용하고, 기능을 추가하고 싶을 때 사용한다.
가상 함수에 중요한 기능이 들어 있어 그대로 기능을 유지한채 기능을 추가하고싶을 때 한다.
#include <iostream>
using namespace std;
class Shape {
public:
virtual void draw() {
cout << "Shape is ";
}
};
class Circle : public Shape {
public:
virtual void draw() {
Shape::draw(); //범위 지정 연산자
cout << "Circle" << endl; //추가적인 기능
}
};
int main(void) {
Circle circle;
Shape *pshape;
pshape = &circle;
pshape->draw(); //동적 바인딩 발생
pshape->Shape::draw(); //정적 바인딩 발생
}
이 코드를 보면,
Circle 클래스의 draw()함수를 만들 때, 기본 클래스의 draw()함수를 재활용하고 중요한 기능을 사용하기 위해서 범위 지정 연산자를 사용했다.
pshape->draw(); 에서는 파생 클래스의 오버라이딩된 draw()가 실행된다(동적 바인딩)
pshape->Shape::draw(); 에서는 범위 지정 연산자를 이용해서 기본 클래스의 draw()함수를 실행시킨다(정적바인딩)
가상 소멸자
기본 클래스의 소멸자를 만들 때, 가상 함수로 만드는 편이 낫다.
만약, 소멸자를 가상 함수로 지정하지 않았다면 기본클래스의 소멸자만 실행되어 문제가 생길 수 있지만,
소멸자를 가상 함수를 지정한다면, 파생 클래스의 소멸자가 실행후(소멸자의 동적 바인딩) 이어서 기본 클래스의 소멸자가 실행되기 때문에 정상적인 delete가 가능해진다.
#include <iostream>
using namespace std;
class Base {
public:
virtual ~Base() {
cout << "소멸자 Base" << endl;
}
};
class Derived : public Base {
public:
virtual ~Derived() {
cout << "소멸자 Derived" << endl;
}
};
int main(void) {
Base *b = new Derived();
Derived *d = new Derived();
delete b; //기본 클래스의 포인터로 delete
delete d; //파생 클래스의 포인터로 delete
}
이 코드를 보자.
기본 클래스의 포인터로 삭제를 할 경우에, 소멸자가 오버라이딩된 상태기 때문에 동적바인딩이 일어나게 된다.
하지만, 파생 클래스의 포인터로 삭제를 할 경우는, 동적바인딩이 일어나지 않는다.
파생 클래스의 소멸자가 먼저 실행되고, 기본 클래스의 소멸자가 이어서 실행되는 모습을 볼 수 있다.
※생성자는 가상 함수가 될 수 없다.