다형성이란, 하나의 형태로 다양한 기능을 구현하는것을 의미한다. C++에서는 다양한 방법으로 다형성을 구현할 수 있는데, 대표적인 방법이 오버라이딩이다.
우선, 같은 함수를 여러 가지 인자 구성으로 오버라이딩하여서 같은 이름의 함수를 목적에 따라 여러개 구현하는 경우가 이러한 경우이다.
int Add(){
return 5;
}
int Add(int num){
return num + 5;
}
위의 예시가 이상하긴 하지만, 위와 같은 형태로 함수의 다형성을 구현 할 수 있다.
클래스에서 다형성을 구현하는 방법은 상속 받은 함수의 재구현, 즉, 오버라이딩으로 구현 할 수 있다.
부모 클래스로 부터 A 라는 함수를 상속 받았다고 가정해 보자. 만약 자식 클래스가 A를 오버라이딩 하지 않고 사용한다면, 부모 클래스의 A 함수가 그대로 호출 될것이다. 하지만 자식 클래스가 A 함수를 내부에서 재정의 하였다면, A함수는 부모 함수와 다른 동작을 하게 된다.
또한 부모 클래스를 인자로 받는 함수에선 해당 부모 클래스 대신, 부모 클래스를 상속한 자식 클래스도 인사로 사용 할 수 있다. 하지만, 자식 클래스를 인자로 받는 함수에선 부모 클래스를 사용 할 수 없다. 아래 예시를 통해 확인 해 보자.
class Parent{
public:
void A() { std::cout << "Do nothing" << std::end; };
}
class Child : public Parent{
public:
void A() { std::cout << "Do something" << std::end; };
Child();
}
void CheckParent(Parent* parent){
parent->A();
}
void CheckChild(Child* child){
child->A();
}
int main(){
Parent p;
Child c;
CheckParent(&p);
CheckParent(&c);
CheckChild(&p); // 오류 발생
CheckChild(&c);
}
위 함수에서 CheckParent 함수와 CheckChild 함수를 통해 각각 부모 클래스와 자식 클래스를 인자로 사용해 멤버 함수를 호출 하고 있다. 하지만 3번째 경우인, 자식 클래스를 인자로 받는 함수에서의 부모 클래스 대입은 오류가 발생한다. 이것은 자식이 부모 클래스를 상속 하였더라도, 자식과 부모 클래스가 같은 멤버를 가진다는 것을 보장 할 수 없기 때문이다.
두번째 경우인, 부모 클래스를 인자로 받는 함수에 자식 클래스를 대입하는 것은 가능하다. 부모 클래스를 상속 받는 자식 클래스는, 부모 클래스의 멤버를 동일하게 지니기 때문이다.
하지만, 이때, 위 경우엔 자식 클래스에서 오버라이딩 된 A 함수가 호출 되는것이 아니라, 부모 클래스의 원본 함수 A가 호출되게 된다.
바인딩 이란, 함수, 변수, 클래스 등을 실제 메모리 블럭, 변수의 주소 등에 연결 하는 과정을 의미한다.
바인딩의 종류엔 정적 바인딩과 동적 바인딩이 있다. 정적 바인딩이란, 컴파일 타임, 즉 프로그램이 실제로 실행 되기 전인 컴파일 타임에 바인딩을 하여서, 미리 어떤 함수가 어떤 함수 주소를 사용할지 등을 결정하는 것을 의미한다.
동적 바인딩의 경우엔, 프로그램이 실행 중인 런타임에 동적으로 바인딩을 하는 것을 의미한다.
위 예시의 두번째 경우인 부모 클래스를 인자로 받는 함수에 자식 클래스를 대입하는 것은 컴파일 타임에 함수 내부에서 미리 해당 인자를 Child가 아닌 Parent로 인지하여서 Parent의 A 함수가 호출 되는 것이다.
이러한 상황을 방지하기 위해서 virtual 함수라는 것을 사용할수 있다.
가상 함수란, 클래스 상속에서, 함수가 동적 바인딩으로 실행 될수 있도록 도와주는 키워드이다. 가상 함수로 지정 할 부모 클래스의 원본 함수 제일 앞에 virtual 키워드를 붙여서 지정할 수 있다.
부모의 가상 함수를 상속받은 자식 클래스 내부에서 해당 가상 함수를 오버라이딩 하더라도, 여전히 해당 함수는 virtual 키워드를 붙이지 않아도 가상 함수로 취급된다.
가상 함수가 정의 되면, 클래스가 생성 되는 컴파일 타임에 해당 클래스의 vftable, 즉, 가상 함수 테이블이 생성된다. 이 테이블은 해당 클래스 메모리 공간에서 제일 앞에 위치하는데, 가상 함수를 실행 할 때, 이 테이블을 참고하여 실행할 함수의 주소 값을 얻어서 실행한다.
이전 포스트에서 언급 하였던 것 처럼 부모 클래스를 상속한 자식 클래스는 부모 클래스로 부터 상속 받은 자원을 위한 메모리 공간 + 자식 클래스의 개별적인 메모리 공간의 조합과 순서로 메모리가 할당 된다. 이때, 자식 클래스가 생성 될때 부모 클래스의 가상 함수 테이블을 복사하고, 자식 클래스가 가상 함수를 오버라이딩 하였다면, 해당 함수의 주소 값을 오버라이딩 된 자식 클래스의 함수 주소 값으로 대체한다.
위와 같은 방식을 통해서 상속 받은 함수를 오버라이딩 한 후, 부모 클래스 포인터를 인자로 받는 함수에 자식 클래스를 넣은 후, 오버라이딩한 함수를 호출 하면, 자식 클래스의 가상 함수 테이블을 참고하여 부모 클래스의 함수가 아닌 자식 클래스의 함수가 실행되게 된다.