Programming/C, C++

[C++] 동적 바인딩과 가상 함수에 대한 짧게 읽을거리

변화의 물결1 2025. 4. 17. 23:32

 

 

 안녕하세요.

 

 이번에는 C++ 동적 바인딩과 이를 가능하게 하는 가상 함수 개념에 간단하게 확인해 보겠습니다.

 


 

1. 동적 바인딩 (Dynamic Binding) 이란?

 

 동적 바인딩 또는 늦은 바인딩 (Late Binding)은 실행 시점(Runtime)에 호출될 함수를 결정하는 방식입니다. 이는 컴파일 시점에 호출될 함수가 결정되는 정적 바인딩 (Static Binding)과 대조가 되는 개념입니다.

 C++에서는 주로 가상 함수 (Virtual Functions)를 통해 동적 바인딩을 구현합니다.

 

 

2. 가상 함수 (Virtual Functions)

 

 이전 글에서도 한번 설명하였습니다. 가상 함수는 기반 클래스에서 virtual 키워드를 사용하여 선언된 함수입니다.

 파생 클래스에서 이 가상 함수를 재정의 (Override)할 수 있습니다. 동적 바인딩의 핵심은 기반 클래스 타입의 포인터 또는 참조를 통해 파생 클래스의 객체를 다룰 때 발생합니다.

 이 경우, 실제로 호출되는 함수는 포인터 또는 참조가 가리키는 객체의 실제 타입에 따라 결정됩니다.

 

 

3. override 키워드의 중요성

 

 파생 클래스에서 기반 클래스의 가상 함수를 재정의할 때는 override 키워드를 사용하는 것이 좋습니다. override 키워드는 컴파일러에게 해당 함수가 기반 클래스의 가상 함수를 재정의하는 것임을 명시적으로 알려줍니다.

 만약 파생 클래스에서 override로 지정된 함수가 기반 클래스에 존재하지 않거나 시그니처(함수의 이름, 매개변수 타입 및 개수, 반환 타입)가 일치하지 않으면 컴파일 에러가 발생하여 실수를 방지할 수 있습니다.

 

 virtual로 선언된 함수에 override를 사용하는 것은 오버로딩(Overloading)을 시도하는 것이 아닙니다. 오버로딩은 같은 클래스 내에서 이름은 같지만 매개변수 목록이 다른 여러 함수를 정의하는 것을 의미합니다. override는 기반 클래스의 가상 함수를 파생 클래스에서 재정의하는 것입니다.

 

 

4. 위의 내용을 설명할 클래스 구조 다이어그램

 

 위에서 설명한 개념을 설명하기 위해서 간단하게 클래스를 만들어 소스 코드로 한번 확인해 보겠습니다.

 

 

 BASE_INFO 클래스는 추상 클래스이며, 순수 가상 함수 exec2()를 가지고 있습니다.

 DERIVED_INFO 클래스는 BASE_INFO 클래스를 상속받아 exec2() 함수를 재정의합니다.

 (<|--) 화살표는 상속 관계를 나타냅니다.

 

 위 다이어그램은 BASE_INFO와 DERIVED_INFO 클래스 간의 상속 구조와 멤버 함수를 표현한 것으로 이것을 기반으로 코드를 작성해 보겠습니다.

 

 

5. 실습 코드

 

#include <iostream>
using namespace std;

class BASE_INFO {
public :
         virtual ~BASE_INFO() {
                  cout << "~BASE_INFO" << endl;
         }
         void exec1()
         {
                  cout << "BASE_INFO exec1" << endl;
         }
         virtual void exec2() = 0;

         void doWork() { // interface , template method pattern
                  exec2();
         }
};

class DERIVED_INFO : public BASE_INFO {
public :
         virtual ~DERIVED_INFO() {
                  cout << "~DERIVED_INFO" << endl;
         }
         virtual void exec2() override
         {
                  cout << "DERIVED_INFO exec2" << endl;
         }
};

int main()
{
         BASE_INFO* derivedInfo = new DERIVED_INFO(); //subtype principle

         derivedInfo->exec2(); //late binding
         derivedInfo->doWork();

         delete derivedInfo;
}

 

 

 

 

6. 서브타입 원칙 (Subtype Principle)

 

 main() 함수에서 BASE_INFO* derivedInfo = new DERIVED_INFO();는 서브타입 원칙 (Subtype Principle)을 나타냅니다.

 이는 파생 클래스의 객체는 기반 클래스 타입으로 활용할 수 있다는 것입니다. 기반 클래스 타입의 포인터로 파생 클래스의 객체를 가리킬 수 있습니다.

 

 

7. 동적 바인딩 예시

 

 derivedInfo->exec2(); 코드를 실행하면 컴파일 시점에는 BASE_INFO의 exec2()가 호출될 것처럼 보이지만, 실제 실행 시점에는 derivedInfo가 가리키는 객체의 실제 타입인 DERIVED_INFO의 exec2()가 호출됩니다.

 이것이 바로 동적 바인딩의 예시이며, 늦은 바인딩 (Late Binding)이라고도 합니다. 이는 실행 시간에 결정될 수 있다는 것을 의미합니다.

 

 

8. 템플릿 메서드 패턴 (Template Method Pattern)

 

 BASE_INFO 클래스의 doWork() 함수는 템플릿 메서드 패턴의 간단한 형태를 보여줍니다. 템플릿 메서드 패턴은 알고리즘의 기본적인 구조를 기반 클래스에 정의하고, 구체적인 각 단계의 구현은 파생 클래스에 위임하는 디자인 패턴입니다.

 

 여기서 doWork() 함수는 알고리즘의 틀을 제공하며, exec2() 함수는 이 틀 안에서 실행될 특정한 단계를 나타냅니다. exec2()는 기반 클래스에서 순수 가상 함수로 선언되어 있으므로, BASE_INFO를 상속받는 파생 클래스는 반드시 exec2()를 재정의(Override)하여 자신만의 구체적인 동작을 구현해야 합니다.

 

이러한 방식을 통해 doWork() 함수는 알고리즘의 전체적인 흐름을 유지하면서도, 각 단계의 세부 동작은 파생 클래스의 특성에 맞게 유연하게 변경할 수 있도록 합니다.

 

 

 설명만 길어지는 것 같아서 쉼표 겸... 이모티콘으로 분위기 잠시 전환해 보겠습니다. 

 

 

 

9. 생성자와 소멸자에서의 가상 함수

 

 생성자와 소멸자를 가상 함수로 선언하는 것은 몇 가지 중요한 고려 사항이 있습니다.

 

1) 생성자 :

 생성자는 가상 함수가 될 수 없습니다. 객체가 생성될 때 객체의 정확한 타입이 이미 결정되어야 하기 때문입니다.

 

2) 소멸자 :

 기반 클래스의 소멸자는 일반적으로 virtual로 선언하는 것이 좋습니다. 이는 파생 클래스의 객체를 기반 클래스 타입의 포인터로 삭제할 때 파생 클래스의 소멸자가 먼저 호출되도록 보장하여 메모리 누수를 방지하기 위함입니다.

  위 코드에서도 `BASE_INFO`의 소멸자가 virtual로 선언된 것을 확인할 수 있습니다.

 

3) 생성자와 소멸자에서의 의미 있는 동작 처리:

 생성자와 소멸자에서는 객체의 기본적인 초기화 및 해제 작업만 수행하고, 더 복잡하거나 의미 있는 동작은 별도의 initialize() 또는 finalize() 함수를 만들어 처리하는 것을 권장합니다. 특히 소멸자에서 가상 함수를 호출하는 것은 주의해야 합니다.

 

 예를 들어, 기반 클래스의 소멸자에서 doWork() 함수를 호출한다고 가정해 봅시다. 만약 doWork() 함수가 내부적으로 가상 함수인 exec2()를 호출하고, 파생 클래스에서 exec2()를 재정의했지만, 소멸 과정에서 파생 클래스의 객체가 이미 부분적으로 소멸된 상태라면 의도치 않은 동작이 발생하거나 프로그램이 오류를 낼 수 있습니다. 특히 exec2()가 순수 가상 함수인 경우에는 기반 클래스의 소멸자에서 호출 시 런타임 에러가 발생할 가능성이 큽니다.

 

 중요한 점은 이러한 잠재적인 문제를 문법적으로 완전히 막을 수 있는 방법이 C++ 에는 없다는 것입니다. 따라서 개발자는 설계 단계에서 이러한 상황을 염두에 두고 소멸자에서 가상 함수 호출을 최소화하거나 안전하게 처리하도록 주의해야 합니다.

 

 

10. 가상 함수 상속 규칙

 

 기반 클래스의 멤버 함수가 virtual로 선언되면, 해당 함수는 파생 클래스에서도 자동으로 가상 함수가 됩니다. 따라서 파생 클래스에서 virtual 키워드를 명시적으로 사용하지 않아도 재정의가 가능합니다. 하지만 코드의 가독성과 명확성을 위해 파생 클래스에서도 virtual 키워드를 명시적으로 표기해 주는 것이 일반적인 관례입니다.

 

 마찬가지로, 파생 클래스에서 기반 클래스의 가상 함수를 재정의할 때 사용하는 override 키워드에도 유사한 규칙이 적용됩니다. 파생 클래스에서 override 키워드를 사용하여 함수를 정의하면, 컴파일러는 해당 함수가 기반 클래스의 가상 함수를 정확하게 재정의하는지 검사합니다. 만약 재정의가 불가능한 경우(예: 함수 시그니처가 다른 경우)에는 컴파일 에러를 발생시켜 실수를 방지할 수 있습니다.

 

 override 키워드는 상속 계층을 따라 자동으로 상속되지는 않습니다. 즉, 한 파생 클래스에서 override로 선언된 함수를 그다음 단계의 파생 클래스에서 다시 재정의하려면 해당 파생 클래스에서도 명시적으로 override 키워드를 사용해야 합니다. 이렇게 명시적으로 override를 사용하는 것은 코드의 가독성을 높이고, 컴파일러의 타입 검사를 통해 안정성을 확보하는 데 도움이 됩니다.

 

 

감사합니다.

 

 

 

 

반응형