프로그램/C,C++

[C++ 기본3] 7. 가상함수

naudhizb 2022. 5. 15. 19:10
반응형

C++의 다형성과 같은 특징들은 컴파일 시간에 호출할 함수가 정해진다는 특성이 있다.
만약 여러 클래스에 대해서 같은 함수를 만들고, 기반 클래스의 함수 포인터를 이용하여 핸들링하게 되는 경우 기반 클래스에 있는 함수를 호출하게 된다.(컴파일 시간에 기반 클래스의 함수로 바인딩 되기 때문에)

기반 클래스의 함수를 호출하면서 동작은 실제 클래스별로 다르게 동작시키고 싶은 경우 함수 호출에 따라 실제 사용할 함수를 동적으로 결정하여야 하며, 이 때 가상함수(virtual function) 을 사용하여야 한다.

// 7_가상함수1  144 page ~

#include <iostream>

class Animal
{
public:
    virtual void Cry() { std::cout << "Animal Cry" << std::endl; }
};
class Dog : public Animal
{
public:
    // 함수 override : 기반 클래스의 함수를 파생 클래스가
    //                  재정의 하는 것
    virtual void Cry() { std::cout << "Dog Cry" << std::endl; }
};
int main()
{
    Animal a; a.Cry(); // 1
    Dog    d; d.Cry(); // 2

    Animal* p = &d;

    //int n; 
    //cin >> n;
    //if (n == 0) p = &a;

    p->Cry();  // 1 ? 2
}

// 함수 바인딩 : p->Cry() 를 어떤 함수로 연결할것인가 ?

// 1. static binding : 컴파일 시간에 컴파일러가 결정.
//            컴파일러는 컴파일 시간에 포인터 타입밖에 알수없다.
//            포인터 타입으로 호출.  Animal::Cry()
// ealry binding. 빠르다. 논리적이지 않다.
//            C++, C#

// 2. dynamic binding : 컴파일 시간에 p가 가리키는 메모리를
//            조사하는 기계어 코드 생성
//            실행시 메모리 조사후 호출.  메모리에있는객체::Cry()
// late binding. 느리다. 논리적이다.
//          java. swift, objective-c, kotlin
//            C++/C#의 virtual 함수

가상함수의 반대되는 키워드는 override이며, 가상함수에 override 키워드를 사용하게 되면 컴파일 시간에 오류가 발생하여 인적 오류를 막아준다.

// 7_가상함수2.cpp  147 page ~

class Shape
{
public:
    virtual void Draw() {};
    virtual void Clone() const {};
    virtual void Move() {};
};
class Rect : public Shape
{
public:
    // override : C++11 에서 추가된 문법
    //            가상함수 재정의시 사용하면 버그를 줄일수 있다
    virtual void draw() override {};
    virtual void Clone()override {};
    virtual void Move(int n) override {};
};
int main()
{

}

굉장히 헷갈리는 것중의 하나는 템플릿과의 연동인데, 템플릿의 경우 컴파일 시간에 적용되기 때문에 실제 템플릿 타입이 무엇인지 주의하여야 한다.

#include <iostream>
// 아래 2줄을 동일 합니다.
//const int c = 10; 
//int const c = 10;
template<typename T> 
class Base
{
public:
    // a는 상수 이다.
    //virtual void foo(const T a)
    virtual void foo(T const a)
    {
        std::cout << "Base foo" << std::endl;
    }
};
class Derived : public Base<int*>
{
public:
    // foo 재정의 해보세요. override 붙이지 마세요
    // a는 상수가 아니다.
    // a를 따라가면 상수
    //virtual void foo(const int*  a)
    virtual void foo( int *  const a)
    {
        std::cout << "Derived foo" << std::endl;
    }

};
int main()
{
    Base<int*>* p = new Derived;
    p->foo(0); // "Derived foo" 나오게 해보세요
}

C++에서는 동적으로 함수 호출을 결정하기 위하여 VTable을 운용하며 그 원리는 아래와 같다.

#include <iostream>

//가상함수 원리   164page ~

void* animal_table[] = { RTTI정보,
                        &Animal::Cry,
                        &Animal::Run };
class Animal
{
    void* vtptr = animal_table; // 컴파일러가 추가한 코드
    int age;
public: 
    
    virtual void Cry() {}
    virtual void Run() {}
};
//--------------------
void* dog_table[] = { RTTI정보,
                        &Dog::Cry,
                        &Animal::Run };

class Dog : public Animal
{
    void* vtptr = dog_table;
    int color;
public:    
    virtual void Cry() {}
};


int main()
{
    Animal a;
    Dog    d;
    Animal* p = &d;
    p->Cry();  // p가 가리키는 객체만 알면 안되고.
                // 함수의 주소도 알아야 한다.

            // p->vtptr[1]( ) 로 컴파일 됩니다.
}

때문에 동적 바인딩은 정적 바인딩보다 오버헤드를 가지고 있다.

C++ 구조 상 포인터 연산이 가능하기 때문에 트릭을 이용하는 방법도 있지만, 비추천한다. 아래 코드는 구조를 이해하기 위한 예제 정도로만 이해하자

// 7_가상함수5
#include <iostream>

// 둘다 가상이 아닐때 : goo
// 둘다 가상 일때    : foo
// foo가상, goo비가상 : goo
// foo비가상, goo가상 : runtime error

class A
{
    int a;
public:
    void foo() { std::cout << "foo" << std::endl; }
};
class B   // 주의 상속관계 아닙니다.
{
    int b;
public:
    virtual void goo() { std::cout << "goo" << std::endl; }
};
int main()
{
    A aaa;
    B* p = reinterpret_cast<B*>(&aaa);
    p->goo(); // foo ? goo 
            // p->vtptr[1]() 
}
반응형