Run if you want it

C_C++

[C++] 가상함수(Virtual function)와 다형성

rewyear 2020. 5. 31. 14:58
300x250
이 페이지는 상속(Inheritance)에 대하여 어느 정도 사전지식이 있다고 생각하고 서술하였습니다.

 

C++의 객채지향프로그래밍의 가장 중 큰 특징 중 하나인 가상함수와 다형성에 대하여 알아볼 것이다.

 

가상함수(Virtual Function)이란 자식클래스에서 이미 정의되어 있는 부모클래스의 멤버함수를 오버라이딩(재정의)멤버함수를 의미한다.

 

먼저 코드를 통해 가상함수의 기본적인 개념인 '오버라이딩'에 대해 알아보자

#include <iostream>
#include <string>
using namespace std;

class Parent {   
public:    
    void WhoIam()
    {
    	cout << "나는 부모 클래스입니다." << endl;
    }
}

class Child : public Parent
{
    void WhoIam()
    {
    	cout << "나는 자식 클래스입니다." << endl;
    }
}

int main()
{
    Parent *p1 = new Parent();
    p1->WhoIam(); //
    delete p1;
    
    Child *c1 = new Child();
    c1->WhoIam();
    delete c1;
      
    return 0;
}   
    
실행결과
나는 부모 클래스입니다.
나는 자식 클래스입니다.

실행결과 "Child 클래스"는 WhoIam() 함수를 재정의 하였기 때문에 Child 클래스에서 오버라이딩 한 WhoIam()이 호출되어 "나는 자식 클래스입니다." 가 호출된다.

 

 

Up/Down casting

위의 예제에 이어서 '업 or 다운 캐스팅'에 대하여 알아보자.

#include <iostream>
#include <string>
using namespace std;

class Parent {   
public:    
    void WhoIam()
    {
    	cout << "나는 부모 클래스입니다." << endl;
    }
}

class Child : public Parent
{
    void WhoIam()
    {
    	cout << "나는 자식 클래스입니다." << endl;
    }
}

int main()
{
    Parent *p1 = new Child();
    p1->WhoIam();
    delete p1;
      
    return 0;
}   
    
실행결과
나는 부모 클래스입니다.

 

위의 코드를 보고 Parent 포인터에 어떻게 Child 인스턴스를 할당할 수 있냐고 생각할 수 있는데,

상속의 개념을 떠올려보면 부모 클래스를 상속받은 자식 클래스는 부모 클래스의 객체도 포함하고 있기 때문에

부모클래스 포인터로 자식클래스의 인스턴스를 가리킬 수 있는 것이다.

 

자식클래스는 부모클래스의 객체도 포함하고 있음

 

하지만 Parent 클래스 포인터 p1은 Child 클래스의 Parent 부분만 가리키고 있기 때문에 호출하게 되면 Child의 WhoIam() 함수가 호출되는 것이 아니라, 부모 클래스의 WhoIam()가 호출되어 "나는 부모 클래스입니다." 가 출력된다. 이를 업-캐스팅(Up-Casting)이라고 한다.

 

반대로 다음과 '다운 캐스팅'을 할 수도 있지만 컴파일 오류가 발생한다.

int main()
{
    Parent p;
    Child c;
    
    Parent *pP = &c; // 부모클래스 포인터로 업 캐스팅
    
    Child *cC = pP; // 업 캐스팅된 부모클래스 포인터를 다시 다운 캐스팅
    cC->WhoIam();
    
    return 0;
}

 

이를 해결하기 위해서 static_cast<>를 이용한다. 이를 이용하게 되면 성공적으로 컴파일이 진행된다.

Child *cC = static_cast<Child*>(pP); 

 

하지만 이와 같은 코드는 굉장히 위험하다. 왜냐하면 비록 유저는 pP에 Child의 인스턴스를 가지고 있다는 것을 알지만 pP변수에 Child의 인스턴스가 할당되어 있다는 보장이 없기 때문에 이를 통, 이러한 오류를 방지하기 위해서 C++에서는 dynamic_cast<>를 지원하는데 상속 관계에 있는 두 포인터들 간에 캐스팅을 해주는 기능이다.

Child *cC = dynamic_cast<Child*>(pP); 

dynamic_cast를 이용해서 컴파일 하게 되면 다형성문제로 컴파일을 할수 없다는 오류를 발생시킨다.

 

 

가상함수

이제 위에서 배운 것을 바탕으로 본격적으로 '가상함수'에 대하여 설명하도록 하겠다.

#include <iostream>
#include <string>
using namespace std;

class Parent {   
public:    
    virtual void WhoIam(); // virtual 키워드를 이용하여 부모클래스에 가상함수 정의
};

void Parent::WhoIam()
{
    cout << "나는 부모 클래스입니다." << endl;
}

class Child : public Parent
{
    void WhoIam() override; // 자식클래스에서 오버라이딩
}

void Child::WhoIam()
{
    cout << "나는 자식 클래스입니다." << endl;
}

int main()
{
    Parent *p1 = new Child();
    Parent *p2 = new Parent();  
    
    p1->WhoIam();
    p2->WhoIam();
    
    delete p1;
    delete p2;
      
    return 0;
}   
    
실행결과
나는 자식 클래스입니다.
나는 부모 클래스입니다.

 

위의 코드를 실행하면 위와 같은 결과가 나오게 되는데, 이는 virtual 키워드를 사용하여 부모클래스에 가상함수를 정의하고, 자식클래스에서 오버라이딩 했기 때문이다.

 

이런 결과가 나온 과정은 다음과 같다.

Case1: p1
1. p1은 Parent의 포인터이므로 Parent의 WhoIam()을 호출할 것이다.
2. 그러나 Parent의 WhoIam()은 virtual 키워드를 사용하고 있기 때문에, 먼저 Child클래스를 확인하게 되고, WhoIam()이 오버라이딩이 되어 있는 것을 확인하였으므로 Child의 WhoIam()을 호출하게 된다.

Case2: p2
1. p2의 경우에도 마찬가지로 Parent포인터이므로 먼저 Parent의 WhoIam()을 호출한다. 역시나 virtual키워드를 사용하고 있기 때문에 자식클래스인 Child클래스를 확인한다. 
2. 하지만 WhoIam()은 오버라이딩되어 있지 않기 때문에 그냥 그대로 Parent클래스의 WhoIam()을 호출하게된다.

 

이와같이 컴파일 타임에 어떤 함수가 실행될 지 정해지지 않고 런타임에 정해지는 것을 동적 바인딩(Dynamic Binding)이라고 한다.

 

자식 클래스에서 override 키워드를 이용하여 오버라이딩한다는 것을 명시적으로 나타낼 수 있다.(C++11)

이는 실수로 오버라이드를 하지 않는 오류를 방지해 준다.

 

 

virtual 사용시 주의해야 할 점(virtual 소멸자)

virtual키워드를 사용할 때 주의해야 할 점 중 하나는 virtual 소멸자 이다.

 

먼저 다음 예시를 살펴보자

#include <iostream>
#include <string>
using namespace std;

class Parent {   
public:
    Parent(){ cout << "Parent 클래스 생성자" << endl; }
    ~Parent(){ cout << "Parent 클래스 소멸자" << endl; }

class Child : public Parent
{
public:
    Child(){ cout << "Child 클래스 생성자" << endl; }
    ~Child(){ cout << "Child 클래스 소멸자" << endl; }
}

int main()
{
    {
        Child c1;
    }
    cout << "----------------------------------" << endl;
    {
        Parent *p1 = new Child();
        delete p1;
    } 
    return 0;
}   
    
실행결과
Parent 클래스 생성자
Child 클래스 생성자
Child 클래스 소멸자
Parent 클래스 소멸자
--------------------------------
Parent 클래스 생성자
Child 클래스 생성자
Parent 클래스 소멸자

 

위의 예제에서 주목해야 할 부분은 delete p1이다. Parent클래스의 포인터인 p1을 delete하게 되면 Child 소멸자를 호출하지 않게 되는데, 만약 Child 클래스의 생성자에서 동적할당을 진행하게 되고 소멸자에서 해제하게 되는 구조이면 이는 memory leak을 유발시킨다.

 

때문에 virtual 키워드를 이용하게 된다면 부모클래스의 소멸자를 virtual로 정의해 주어야 한다.

다음은 부모 클래스의 소멸자에 virtual 키워드를 사용한 코드와 결과이다.

#include <iostream>
#include <string>
using namespace std;

class Parent {   
public:
    Parent(){ cout << "Parent 클래스 생성자" << endl; }
    virtual ~Parent(){ cout << "Parent 클래스 소멸자" << endl; }

class Child : public Parent
{
public:
    Child(){ cout << "Child 클래스 생성자" << endl; }
    ~Child(){ cout << "Child 클래스 소멸자" << endl; }
}

int main()
{
    {
        Child c1;
    }
    cout << "----------------------------------" << endl;
    {
        Parent *p1 = new Child();
        delete p1;
    } 
    return 0;
}   
    
실행결과
Parent 클래스 생성자
Child 클래스 생성자
Child 클래스 소멸자
Parent 클래스 소멸자
--------------------------------
Parent 클래스 생성자
Child 클래스 생성자
Child 클래스 소멸자
Parent 클래스 소멸자

 

 

※ 가상함수의 기본적인 개념만 알고있었는데 이번 포팅 작업에서 이미 윗 단의 부모 클래스에서 순수 가상 함수를 정의해서 제공해주기 때문에, 1. 포팅 단에서 가상 함수를 플랫폼에 맞게 오버라이딩하여 / 2. 부모 클래스 포인터로 업 캐스팅하여 제공하고 / 3. 윗 단에서 호출하면 포팅 단에 구현해놓은 함수가 호출되는 방식을 경험하였다.

 

이는 가상함수을 실질적으로 활용하는 경험이라 좋은 기회였다. 

 

 

300x250

'C_C++' 카테고리의 다른 글

[C/C++] Volatile  (0) 2022.08.17
[C++] CallBack Function(콜백함수)  (2) 2020.01.29