C++ 정적 결합(Static Binding)과 동적 결합(Dynamic Binding)

프로그래밍/C++ 2019.10.22 댓글 Plorence

프로그램이 함수를 호출할 때 실행 코드의 어느 블록이 사용되는지에 대한 대답은 컴파일러에게 있습니다.

먼저 소스 코드에 있는 함수 호출을 특정 블록에 있는 함수 코드를 실행하라는 뜻으로 해석하는 것을 "함수 이름을 결합(binding)이라고 합니다.

C언어에서는 각각의 함수가 별개의 함수에 해당하기 때문에 이 작업이 간단하지만(함수 오버 로딩이 없음.) C++에서는 함수 오버 로딩 때문에 이 작업이 조금 복잡합니다.

이 결합은 컴파일 타임에 이루어지고 이 결합을 정적 결합(static binding)또는 초기 결합(early binding)이라고 합니다.

 

하지만 가상 멤버 함수(virtual 키워드가 달린)들은 프로그램이 실행할 때 사용자가 객체를 결정하기 때문에 컴파일 타임에는 진행할 수 없습니다.

그래서 컴파일러는 프로그램을 실행할 때(런타임) 올바른 가상 멤버 함수가 선택하도록 해야 합니다.

이 작업을 동적 결합(dynamic binding) 또는 말기 결합(lately binding)이라고 합니다.

 

포인터와 참조형의 호환

C++가 포인터와 참조형의 호환을 어떻게 처리하는지 알아봅시다.

동적 결합은 포인터와 참조에 의해 호출되는 멤버 함수와 관련되어 있습니다.

일반적으로, C++는 한 데이터형의 주소를 다른 데이터형의 포인터에 대입하는 것을 허용하지 않습니다.

참조도 마찬가지입니다.

#include <iostream>
using namespace std;
int main(void) {
        int a = 0;
        long b = &a; //에러
        double & c = a; //에러
}

하지만 기초 클래스를 지시하는 포인터(또는 참조)는 명시적 형 변환이 없어도 파생 클래스 객체를 참조하거나 지시할 수 있습니다.

#include <iostream>
using namespace std;
class Person {
public:
        virtual ~Person() {
               cout << "기초 파괴자 \n";
        }
        Person() {
        }
};
class Man : public Person {
public:
         ~Man() {
               cout << "파생 파괴자 \n";
        }
};
int main(void) {
        Person per1 = Person();
        Person per2 = Man(); //문제 없다.
}

이렇게 파생 클래스 포인터(또는 참조)기초 클래스 포인터(또는 참조)변환하는 것을 업캐스팅(upcasting)이라고 합니다.

public 상속에서는 명시적 형 변환이 없어도 가능합니다.

(is-a관계를 나타내는 한 부분입니다.)

만약 Man으로부터 상속받은 새로운 클래스를 정의한다면 Person클래스는 모든 파생 클래스를 참조할 수 있습니다.

 

다운캐스팅(downcasting)

업캐스팅의 반대인 다운캐스팅은 기초 클래스파생 클래스 포인터참조변환하는 것을 말합니다.

다운캐스팅은 명시적 형 변환 없이는 허용이 안됩니다. 그 이유는 is-a관계에 대칭적이지 않기 때문입니다.

기초 클래스로부터 새로운 파생 클래스를 만든다고 하고 파생 클래스는 새롭게 Show멤버 함수를 정의한다고 해봅시다.

다운캐스팅이 되면 파생 클래스를 지시하는 포인터를 기초 클래스 객체의 주소로 설정하고 그 포인터로 멤버 함수를 호출하는 사고가 생깁니다.

 

정리하자면, 암시적 업캐스팅은 기초 클래스 포인터나 참조가 기초 클래스의 객체나 파생 클래스 객체를 참조하는 것을 가능하게 만듭니다.

이러한 것 때문에 동적 결합이 필요한 이유이며, 가상 멤버 함수는 이러한 필요성에 대한 C++의 해답입니다.

 

함수와 결합(binding)

가상 멤버 함수는 동적 결합, 그 외에는 정적 결합이라고 했습니다.

대부분의 경우는 동적 결합이 더 좋습니다.

왜냐하면 특별한 클래스형에 맞게 설계된 멤버 함수를 프로그램이 선택하도록 허용하기 때문입니다.

그럼 다음과 같은 의문점이 생깁니다.

  • 두 종류의 결합이 필요한 이유

  • 동적 결합이 좋다면서 왜 디폴트가 아닌 이유

  • 동적 결합은 어떻게 동작하는가?

두 종류의 결합이 필요한 이유, 정적 결합이 디폴트인 이유

효율성과 개념 모델이라는 두 가지 이유 때문입니다.

 

효율

프로그램이 무언가를 실행 시간에 결정할 수 있도록 하면 또 다른 처리 부담이 생깁니다.

멤버 함수를 새롭게 정의하지 않거나, 파생 클래스가 없다던가 하면 동적 결합은 부담만 커지게 됩니다.

왜냐하면 동적 결합은 어떤 멤버 함수가 호출되어야 하는지 컴파일 타임에 모르기 때문에 생겨난 거고

반대로 정적 결합은 어떤 멤버 함수가 호출되어야 하는지 알기 때문에 컴파일 타임에 이것에 대해 처리를 하게 됩니다.

이러한 이유로 정적 결합이 효율적이기 때문에 디폴트로 되어 있습니다.

(이것은 사용하지 않는 기능들과 관련하여 부담을 떠안지 않는 것이 C++의 기본 철학이다.)

 

개념 모델

기초 클래스로부터 새로운 파생 클래스를 만들 때 재정의를 원치 않는 멤버 함수가 있을 겁니다.

가상이 아닌 멤버 함수로 만듦으로써 효율이 좋아지고 이 멤버 함수가 다시 정의되면 안 된다는 의도를 가지고 있습니다. 

 

동적 결합은 어떻게 동작하는가?

내용이 길어지기 때문에 이에 대한 것은 따로 작성하겠습니다.

가상 함수 동작 원리 설명 게시글

 

댓글