C++

가상함수

춤추는수달 2024. 4. 12. 22:27

다형성

다형성이란 하나의 객체가 여러 타입을 가질 수 있는 것을 말합니다. 반대로 하나의 타입이 여러 타입의 객체를 참조할 수 있는 것도 다형성입니다.

다형성은 상속과 오버라이딩을 통해 구현될 수 있습니다.

 

오버라이딩

클래스의 멤버 함수는 파생 클래스에서 오버라이딩 될 수 있습니다. 즉, 같은 모습의 함수가 여러 개 존재할 수 있습니다.

같은 모습의 함수이지만, 여러 개의 구현이 존재할 수 있습니다.

그런데, 똑같이 생긴 함수를 똑같은 형식으로 호출한다면, 자식과 부모 중 어떤 함수를 호출해야 할까요?

class Parent
{
public:
    void Func() { m_i = 1; }
    int m_i;
};

class Child : public Parent
{
public:
    void Func() { m_i = 2; }
};

int main()
{

    Child c;
    Parent* p = &c;
    p->Func(); // Parent::Func 와 Child::Func 중 무엇이 호출될까?

    cout << p->m_i << endl; //출력 결과는 1
}

위의 코드는 Parent::Func가 호출되며, 출력 결과는 1 입니다. p는 Parent* 형이기 때문에 당연한 결과입니다.

그러나 누군가는 Child::Func가 호출되기를 원할 수도 있습니다. 사실, 그게 더 합당해 보입니다. p의 실제 객체는 Child 형이니까요.

그렇다면 p 를 사용해 Child::Func를 호출하고 싶을 경우에는 어떻게 해야할까요? 

이를 해결하기 위해 virtual 키워드가 존재합니다. virtual 키워드는 가상 함수를 선언하는 키워드입니다.

 

가상 함수

가상함수는 일반적인 함수 선언 앞에 virtual 키워드를 붙여 선언합니다. 위에서 봤던 예제 코드의 Parent::Func함수를 가상 함수로 만들어 보겠습니다.

class Parent
{
public:
    virtual void Func() { m_i = 1; }
    int m_i;
};

class Child : public Parent
{
public:
    void Func() { m_i = 2; }
};

int main()
{

    Child c;
    Parent* p = &c;
    p->Func(); // Parent::Func 와 Child::Func 중 무엇이 호출될까?

    cout << p->m_i << endl; //출력 결과는 2
}

Parent::Func를 가상함수로 만들었더니, 이번엔 Child::Func가 호출됐습니다.

virtual 키워드가 어떤 작용을 했길래 이렇게 됐을까요? 단순히 자식 클래스의 함수를 호출하도록하는 키워드일까요? 그럼 만약 자식의 자식 클래스가 존재하면 어떻게 될까요? 

virtual 키워드가 하는 일은 '실제 객체의 타입에 맞는 함수가 호출되도록 하는 것'입니다.

'변수의 타입에 맞는 함수가 호출'되는 일반 함수와는 확연히 다릅니다.

 

가상함수 선언 규칙

가상함수에는 선언 규칙이 존재합니다. 아래를 참고해주세요.
1. 클래스의 공개(public) 섹션에 선언합니다.
2. 가상 함수는 정적(static)일 수 없으며 다른 클래스의 친구(friend) 함수가 될 수도 없습니다.
3. 가상 함수는 실행시간 다형성을 얻기위해 기본 클래스의 포인터 또는 참조를 통해 접근(access)해야 합니다.
4. 가상 함수의 프로토타입(반환형과 매개변수)은 기본 클래스와 파생 클래스에서 동일합니다.
5. 클래스는 가상 소멸자를 가질 수 있지만 가상 생성자를 가질 수 없습니다.
출처: https://yeolco.tistory.com/125 [열코의 프로그래밍 일기:티스토리]

 

바인딩

위에서 설명한 함수 오버라이딩, 다형성, 가상함수의 동작을 정확히 이해하기 위해서는 바인딩 이라는 개념을 알아야 합니다.

바인딩이란, 어떤 식별자에 대해 이나 속성을 확정짓는 것입니다.

즉, 위의 코드의 예에서는

Parent::Func 함수를 가상함수로 만들지 않았을 때에는 p->Func에 Parent::Func 함수가 바인딩 되었고,

Parent::Func 함수를 가상함수로 만들었을 때에는 p->Func에 Child::Func 함수가 바인딩 된 것입니다.

예를 들었던 함수 뿐만 아니라, 변수에 대해서도 바인딩이 일어납니다.

이렇게 동작이 차이가 나는 이유는, 일반 함수는 정적 바인딩, virtual 함수는 동적 바인딩이 일어나기 때문입니다.

정적 바인딩

정적 바인딩이란 컴파일 타임에 바인딩이 일어나는 것을 말합니다.

C++의 기본 바인딩은 정적 바인딩입니다.

virtual 키워드를 사용하지 않고, 소스 코드 상에 명시적으로 타입과 식별자를 선언하면 정적 바인딩이 일어납니다. 

장점 :

  • 컴파일 타임에 결정되므로 런타임에 동작이 빠릅니다.
  • 컴파일 타임에 에러를 발견하기 좋습니다.

단점 :

  • 컴파일 이후에는 결과를 바꿀 수 없습니다.

 

동적 바인딩

동적 바인딩이란 런 타임에 바인딩이 일어나는 것을 말합니다.

virtual 키워드를 사용해 함수를 선언하면 동적 바인딩이 일어납니다.

런 타임에 가상 함수 테이블(vtable)에서 알맞은 함수를 찾아 바인딩하기 때문에 동작이 더 느립니다.

 

가상 함수 테이블(vtable)

어떤 클래스에 virtual 함수가 있다면, 컴파일 과정에서 해당 클래스에 대한 가상 함수 테이블을 만듭니다.

(반대로 말하면, virtual 함수가 없으면 vtable은 만들어지지 않습니다. )

(virtual 함수가 있더라도, 자식 클래스에서 override되지 않으면 vtable은 만들어지지 않습니다.)

가상 함수 테이블이란 '어떤 클래스가 어떤 함수를 호출해야할지 적어놓은 표' 입니다.

즉, 클래스마다 각자의 vtable을 갖습니다.

그리고 해당 클래스의 객체를 생성할 때, 객체의 첫 4byte(포인터 크기)에는 vtable에 대한 포인터가 자동으로 삽입됩니다.

이것이 위에서 말했던, 실제 객체의 타입에 맞는 함수를 찾을 수 있는 이유입니다.

class Parent
{
public:
    virtual void Func() { m_i = 1; }
    int m_i;
};

class Child : public Parent
{
public:
    void Func() { m_i = 2; }
};

int main()
{

    Child c;
    Parent* p = &c;
    p->Func(); // Parent::Func 와 Child::Func 중 무엇이 호출될까?
    			// 객체 c에는 Child의 vtable을 참조하는 포인터가 있으므로 
                // Child의 Func 함수를 찾아서 호출할 수 있다.

    cout << p->m_i << endl; //출력 결과는 2.
}

Child 타입의 객체 c를 Parent* 형 변수 p가 참조하게 한 후, p->Func()를 했을 때,

p가 가리키는 객체의 첫 주소에는 Child의 vtable을 가리키는 포인터가 포함되어 있기 때문에 Child의 Func 함수를 호출할 수 있었습니다.

 

그렇다면 가상 함수 테이블은 어디에 생성되고, 어떻게 생겼을까?

가상 함수 테이블은 CODE영역에 생성되며, virtual 함수를 가리키는 포인터의 나열이다.

간단하게 예시와 그림으로 이해해보자.

class Parent
{
public:
    virtual void Func() { m_i = 1; }
    virtual void Func2() {}
    void Func3()
    int m_i;
};

class Child : public Parent
{
public:
    void Func() { m_i = 2; }
    void Func2() {}
    int m_i2;
};

위와 같은 클래스가 선언되면, CODE 영역에 아래와 같은 형태의 vtable이 생길 것이다.

vtable

그리고 아래와 같이 Child형 객체 c를 생성해보자.

int main()
{
	Child c;
    Parent* p = &c;
}

그러면 c 객체가 자리잡은 메모리에는 아래 그림과 같은 내용이 채워질 것이다.

그리고 여기의 vtable 포인터는 다음 그림과 같이 Child의 vtable을 가리킨다.

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

RTTI  (0) 2024.04.27
이름 숨김  (0) 2024.04.15
헷갈리는 C++ 질문들  (0) 2024.04.12
Casting (Static, Dynamic, Reinterpret )  (0) 2023.03.28
스마트 포인터  (0) 2022.03.29