새소식

인기 검색어

CS/C++

다형성, virtual, override, virtual table, 동적 바인딩

  • -

다형성과 동적 바인딩

런타임에 적절한 함수를 호출하는 데는 가상 테이블(vtable)가상 포인터(vptr)라는 메커니즘이 사용된다. 이러한 메커니즘은 다형성을 구현하고 런타임에 객체의 실제 타입을 기반으로 올바른 함수를 호출할 수 있게 해준다. 아래에서 이 메커니즘이 어떻게 작동하는지 간략하게 설명

 

  • 가상 테이블(vtable):
    각 클래스에 대해 컴파일러는 가상 테이블을 생성. 가상 테이블은 클래스의 가상 함수에 대한 포인터를 저장하는 테이블이다. 각 가상 함수에 대해 하나의 항목이 있으며, 이 항목은 해당 함수의 주소를 저장한다.
  • 가상 테이블(vtable):
    각 클래스에 대해 컴파일러는 가상 테이블을 생성. 가상 테이블은 클래스의 가상 함수에 대한 포인터를 저장하는 테이블이다. 각 가상 함수에 대해 하나의 항목이 있으며, 이 항목은 해당 함수의 주소를 저장한다.
  • 가상 포인터(vptr):
    클래스의 각 객체에 대해 컴파일러는 가상 포인터를 생성한다. 가상 포인터는 객체의 가상 테이블에 대한 포인터이다. 객체가 생성될 때 가상 포인터는 해당 클래스의 가상 테이블을 가리킨다.
  • 런타임 함수 호출:
    가상 함수가 호출될 때, 컴파일러는 객체의 가상 포인터를 사용하여 가상 테이블에 접근한다. 그런 다음 가상 테이블을 사용하여 호출되어야 하는 함수의 주소를 찾고, 이 주소를 사용하여 함수를 호출한다.

아래 예제를 통해 이를 더 잘 이해할 수 있다.

Copy code
class Animal {
public:
    virtual void speak() {
        std::cout << "Animal sound" << std::endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Bark" << std::endl;
    }
};

int main() {
    Animal* a = new Dog();
    a->speak();  // 출력: Bark
    delete a;
    return 0;
}

위 예제에서 Animal 포인터 a는 Dog 객체를 가리키며, a->speak()를 호출할 때 런타임에 Dog 클래스의 speak 메서드가 호출된다. 이는 가상 테이블과 가상 포인터를 사용한 런타임 바인딩 덕분이다.

 

가상 소멸자

위 작동원리 때문에, base 클래스의 소멸자는 반드시 virtual로 선언해 줘야 한다.

만약 virtual로 선언하지 않는다면 다형성을 이용한 객체 사용과 메모리 할당 해제 시, 올바른 소멸자를 호출하지 못해 메모리 누수와 같은 문제가 발생할 수 있다.

 

가상 소멸자(virtual destructor)는 기반 클래스에서 선언되며, 이는 파생 클래스의 객체가 기반 클래스의 포인터를 통해 삭제될 때 올바르게 소멸되도록 보장한다. 가상 소멸자를 사용하면, 파생 클래스의 소멸자가 먼저 호출되고, 그런 다음 기반 클래스의 소멸자가 호출된다. 이는 리소스 누수를 방지하고 올바른 정리를 보장하는 데 중요하다.

 

class Animal {
public:
    Animal() {
        std::cout << "Animal constructor" << std::endl;
    }
    
    virtual ~Animal() {  // 가상 소멸자
        std::cout << "Animal destructor" << std::endl;
    }
};

class Dog : public Animal {
public:
    Dog() {
        std::cout << "Dog constructor" << std::endl;
    }

    ~Dog() override {  // 파생 클래스의 소멸자
        std::cout << "Dog destructor" << std::endl;
    }
};

int main() {
    Animal* a = new Dog();
    delete a;  // 출력:
               // Animal constructor
               // Dog constructor
               // Dog destructor
               // Animal destructor
    
    return 0;
}

위 예제에서 Animal 클래스의 소멸자는 virtual로 선언되어 있다. 이로 인해 Animal 포인터를 통해 Dog 객체를 삭제할 때 Dog 클래스의 소멸자가 먼저 호출되고, 그런 다음 Animal 클래스의 소멸자가 호출된다. 이는 객체의 리소스가 올바르게 정리되도록 보장한다.

만약 Animal 클래스의 소멸자가 virtual이 아니었다면, delete a; 문장에서 Dog 클래스의 소멸자는 호출되지 않았을 것이며, 이는 잠재적으로 리소스 누수를 초래할 수 있다. 따라서, 객체의 정리를 올바르게 수행하기 위해 기반 클래스의 소멸자를 virtual로 선언하는 것이 중요하다.

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

std::string 은 동적 할당 배열이다!  (0) 2023.11.01
explicit 키워드  (0) 2023.11.01
중괄호 초기화와 다양한 초기화 방법  (0) 2023.11.01
lvalue, rvalue, move 그리고 RVO  (0) 2023.10.31
"#define" vs "static const" vs "enum"  (0) 2023.10.26
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.