본문 바로가기
lang/C,C++

[Class와 OOP] 6. OOP 다형성, 가상함수, 추상클래스, 순수가상함수

by Wordbe 2019. 10. 9.
728x90

1. 가상함수(Virtual Function)

가상 함수란 파생 클래스에서 재정의할 것으로 예상되는 멤버 함수를 의미합니다.

가상 함수는 자신을 호출하는 객체의 동적 타입에 따라 실제 호출함수가 결정됩니다.

파생 클래스의 멤버 함수 쪽에서 virtual 키워드를 사용하여 가상 함수라는 것을 명확히 하는 것이 좋습니다.

즉, 파생클래스에서 재정의 할 것이라고 예상되는 멤버함수는 기초클래스에서도 virtual 을 명시하고, 파생클래스에서도 virtual을 명시하는 것이 좋습니다.

class A{
public:
    virtual void Print() { cout << "A 클래스의 Print() 함수" << endl; }
};

class B : public A{
    virtual void Print() { cout << "B 클래스의 Print() 함수" << endl; }
};

int main(void){
    A* ptr;
    A obj_a;
    B obj_b;

    ptr = &obj_a;
    ptr->Print();
    ptr = &obj_b;
    ptr->Print();
    return 0;

}

결과는

A 클래스의 Print() 함수
B 클래스의 Print() 함수

둘다 정상적으로 호출이 잘 됩니다.


동적 바인딩(Dynamic Binding)

C++ 컴파일러는 함수를 호출할 때,

함수가 어느 블록에 있는지, 함수가 저장된 정확한 메모리 위치는 어딘지 알아야 합니다.

바인딩(binding)이란, 함수를 호출하는 코드에서 어느 블록에 있는 함수를 실행할지 결정하는 것입니다.

오버로딩된 함수가 있다면 이 과정이 복잡해질 것입니다.

대부분 함수를 호출하는 코드는 컴파일 타임에 고정된 메모리 주소로 변환됩니다.

이것을 정적바인딩(static binding) 또는 초기바인딩(early binding)이라고 합니다.

가상함수가 아닌 멤버 함수는 모두 이러한 정적 바인딩을 하게 됩니다.

하지만 가상 함수의 호출은 컴파일러가 어떤 함수를 호출해야하는지 미리 알 수 없습니다.

가상 함수는 프로그램이 실행(run time)될 때 객체를 결정하므로, 컴파일 타임(compile time)에 해당 객체를 알 수 없기 때문입니다.

따라서 가상 함수는 런타임에 올바른 함수가 실행될 수 있도록 해주어야 합니다.

이를 동적 바인딩(dynamic binding) 또는 지연 바인딩(late binding)이라고 합니다.

하지만, 가상함수도 결합하는 타입이 분명한 경우, 정적바인딩을 합니다.

즉, 가상함수는 기초 클래스 타입의 포인터나 참조를 통하여 호출될 때만 동적 바인딩을 합니다.


가상 함수 테이블(Virtual function table, vtbl)

컴파일러가 가상함수를 다루는 가장 일반적인 방식은 가상함수테이블을 이용하는 것입니다.

컴파일러는 각각의 객체마다 가상 함수 테이블을 가리키는 포인터를 저장하기 위한 숨겨진 멤버를 하나씩 추가합니다.

즉, 가상함수를 단 하나라도 가지는 클래스에 대해서 만들어진 모든 인스턴스는 각각의 vtbl이 존재합니다.

각각의 vtbl에는 해당 클래스의 객체들을 위해 선언된 가상 함수들의 주소가 저장됩니다.

그리고 가상 함수를 호출하면, C++ 프로그램은 vtbl에 접근하여 자신이 필요한 함수의 주소를 찾아 호출합니다.

가상 함수를 사용하면 이처럼 함수 호출과정이 복잡해지므로, 메모리, 실행속도 측면에서 부담이 됩니다. 따라서 기본 바인딩은 정적 바인딩이고 필요한 경우에만 동적바인딩을 사용합니다.

하지만, 파생 클래스가 재정의할 가능성이 있는 함수는 모두 가상 함수로 선언하는 것이 좋습니다.


가상 소멸자(Virtual Destructor)

Person* kim = new Student;

delete kim;

위 예제에서 Person 클래스는 Student의 부모 클래스이므로, kim에는 Student 객체가 동적으로 할당될 수 있습니다.

하지만, 마지막 구문의 delete는 ~Student() 소멸자 대신, ~Person 소멸자를 호출할 것입니다.

그러므로 Student 객체에 동적으로 할당된 메모리는 정상적으로 해제되지 않을 것입니다.

여기서, Person 클래스의 소멸자를 가상으로 선언한다면, 위 구문은 정상적으로 ~Student() 소멸자를 호출할 것입니다.

따라서 기초클래스는 명시적으로 소멸자를 선언할 필요가 없더라도, 아무일도 하지 않는 이상 가상소멸자를 선언해야 합니다.

2. 추상 클래스(Abstract Class)

순수 가상 함수(pure virtual function)

가상함수는 반드시 재정의 해야하만 하는 함수가 아닌, 재정의가 가능한 함수를 가리킵니다.

이와는 달리, 순수 가상함수(pure virtual function)는 파생 클래스에서 반드시 재정의해야 하는 멤버 함수를 의미합니다.

순수 가상함수는 일반적으로, 함수의 동작을 정의하는 본체를 가지고 있지 않습니다.

따라서, 파생 클래스에서 재정의하지 않으면 사용할 수 없습니다.

virtual pureVirtualFunction원형=0;

추상 클래스(abstract class)

하나 이상의 순수 가상 함수를 포함하는 클래스를 추상 클래스라고 합니다.

이러한 추상 클래스는 OOP에서 중요한 특징인 다형성(Polymorphism)을 가진 함수의 집합을 정의할 수 있게 해줍니다.

즉, 반드시 사용되어야 하는 멤버함수를 추상 클래스에 순수 가상함수로 선언해 놓으면, 이 클래스의 모든 파생클래스는 이 가상 함수를 반드시 재정의해야 합니다.

추상 클래스는 동작이 정의되지 않은 순수 가상 함수를 포함하므로, 인스턴스를 생성할 수 없습니다.

따라서 추상 클래스는 먼저 상속을 통해 파생 클래스를 만들고, 파생 클래스는 순수 가상함수를 모두 오버라이딩하고 나서야 비로소 파생 클래스의 인스턴스를 생성할 수 있습니다.

하지만, 추상 클래스 타입의 포인터와 참조는 바로 사용할 수 있습니다.

class Animal{
public:
    virtual ~Animal() {} // 가상소멸자(virtual destructor)
    virtual void cry()=0; // 순수가상함수(pure virtual function)
};

class Dog: public Animal{
public:
    virtual void cry() {
        cout << "멍멍!!!" << endl;
    }
};

class Cat: public Animal{
public:
    virtual void cry() {
        cout << "야옹야옹~!" << endl;
    }
};

// main
Dog dog;
dog.cry();
Cat cat;
cat.cry();

결과

멍멍!!!
야옹야옹~!

추상 클래스의 용도 제한

추상클래스는 다음과 같은 용도로 사용할 수 없습니다.

1) 변수 또는 멤버 변수

2) 함수의 전달되는 인수 타입

3) 함수의 반환 타입

4) 명시적 타입 변환의 타입

[출처 - TOPSCHOOL.com http://tcpschool.com/cpp]

728x90

댓글