Part2::Ch 03. 상속 - 03. 정적 결합, 동적 결합

2025. 5. 4. 18:28Programming Language/C++

반응형

1. 정적 결합(Static Binding)

정적이라는 부분은 대부분 컴파일 타입이라는 개념이랑 연결되는 경우가 많다.

정적 결합은 컴파일 타임(compile-time)에 함수 호출이나 멤버 접근이 어떤 코드와 연결될지 확정되는 방식으로 컴파일러가 함수 호출 대상을 미리 안다라는 개념하에 발생한 결합니다.

이는 실행 도중(런타임)이 아니라, 컴파일 단계에서 어떤 함수나 멤버가 호출될지 결정되며 대표적으로는 오버로딩이 정적 결합이며 추가로 대부분의 일반 함수 호출, 연산자 오버로딩, 템플릿 등은 정적 결합이다.

 

2. 정적 결합을 코드로

상속관계에 있는 클래스를 두개 선언해주고

부모 클래스에 멤버 변수를 하나 선언해주고

간단한 연산자 오버로딩 함수, 전역함수로 반환 타입이 Parent이고 매개변수를 const Parent&참조 타입 두개를 받는 + 연산자에 대한 오버로딩을 한번 구현해보도록 하자.

이제 사용해보면

이렇게 정상적으로 실행되는 것을 볼 수 있다.

 

그런데 만약 여기서 Parent를 Children 타입으로 변경하면 

이것도 결국엔

이 함수에 들어갈때 업 케스팅 되면서 정상적으로 실행되게 된다.

 

그럼 추가로 Children에 대한 연산자 오버로딩을 하나 추가해주자.

이때는 차별점을 주기 위해서 내부 구현은 + 연산이 아니라 * 곱연산을 하도록 구현해보고

이 형태 그대로 실행시켜보면

새로 구현한 연산자로 들어가 결과를 반환하는 것을 알 수 있다.

 

그리고 

이 부분이 가능한 이유도 클래스에는 기본적으로 아래와 같이 생성되어 있는 복사 생성자가 있기 때문에 가능한 상황이다.

Parent p = c를 했을 때 const Parent& p = c 와 같은 대입이 일어남

여기까지는 문제 없이 진행이 된다.

 

그러면 만약에 Children 타입의 객체 두개를 Parent& 참조 타입으로 업케스팅 한 후에 연산을 진행하면 

어떤 매개변수를 받는 연산자를 부르게 될까

업케스팅한 Parent 타입을 보고 연산자를 호출하게 된다.

이 선택은 컴파일 시점에 이루어지며 변수 p0, p1이 실제로는 Children 객체를 가리키고 있더라도 변수의 타입이 Parent&이기 때문에 Parent 버전에 대한 연산자가 호출된다.

 

왜냐하면 객체의 생성은 런타임에 이루어지고 컴파일 시점에는 메모리 상에 객체가 존재하지 않기 때문에 컴파일러는 참조가 가리키는 실체가 무엇인지 알 수 없고 오직 정적으로 보이는 타입만으로 판단할 수밖에 없기 때문이다.

이로 인해 오버로딩은 정적 결합(static binding)의 대표적인 예라고 할 수 있다

 

3. 동적 결합(Dynamic Binding)

동적 결합은 실행 시점(Run-time)에 어떤 함수가 호출될지를 결정하는 방식으로 반드시 상속 관계에서만 발생한다.

이는 가상 함수(virtual function)를 사용할 때 발생하며 다형성(polymorphism)을 실현할 수 있게 해주는 핵심 메커니즘이다.

 

1) 동적 바인딩의 조건

동적 바인딩이 일어나려면 세 가지 조건이 충족되어야 한다.

1. 가상 함수(virtual)여야 하고 2. 부모 클래스 포인터나 부모 클래스 참조를 통해 자식 객체에 접근해야하고 3. 자식 클래스에서 오버라이딩한 함수가 있어야 한다.

 

4. 동적 결합을 코드로

먼저 상속 관계에 있는 클래스를 두개 선언해주자.

그리고 부모 클래스에는 가상 함수를 하나 만들어주자.

 

그리고 자식 클래스에서 가상 함수를 오버라이딩 해주자.

그리고 추가로 상속 받는 자식 클래스를 하나 만들고 가상 함수를 오버라이딩 해주자.

 

이렇게 모두 오버라이딩 했기에 업케스팅을 하더라도 실제 각자가 바라보는 객체의 drive를 호출하게 된다.

 

이건 런타임 중에 c1 혹은 c2가 무엇을 가리키고 있는지를 매번 확인해서 drive를 호출해줘야 하기에 동적 바인딩이 된다.

 

만약 virtual이 없다면 무조건 c1 혹은 c2 가 가리키는 Car 클래스에 있는 drive를 호출하게 될 것 이다.

그래서 virtual을 안붙이면 무조건 정적 바인딩이다.

 

5. 동적 결합의 구현

동적 결합, 오버라이딩의 경우는 어떻게 구현해라 라고 하는 표준적인 기준이 존재하지 않는다.

그냥 런타임 도중에 결정이 되어야 한다와 같은 표준들만 존재할 뿐이다.

 

컴파일러는 보통 비슷한 구조로 구현하고 있는데 먼저 Car 클래스 내부에 int 형 포인터를 하나 만들어보자.

그리고 main함수에서 Car 클래스의 사이즈를 한번 확인해보자.

사이즈가 8이 나온다.

이것은 우리가 64비트로 실행해서 8이 나온거고 

32비트로 실행하면 4가 나온다.

이제 drive함수에 virtual을 선언해주면

사이즈가 몇으로 나올까

사이즈가 4늘어난 8이 나오는 것을 알 수 있다.

이건 virtual을 추가함으로 클래스 내부에 무엇인가가 생겼다고 알 수 있는데 그걸 알아보기 위해서 Car 타입으로 객체를 하나 생성하고 객체 c의 주소값과 c의 ptr의 주소값을 출력해보자.

보면 딱 003AFB1C 와 4바이트 차이나는 주소인 003AFB20에 ptr이 위치한다는 것을 볼 수 있는데 클래스에 가상함수를 생성하면 클래스의 가장 앞에 뭔가가 추가가 된다는 것을 알 수 있다.

 

이걸 알기 위해서 디버깅모드로 브레이크 포인트를 출력부 쪽에 찍어주고 

실행해보자.

보면 ptr의 앞에 __vfptr이라는 포인터가 하나 추가된것을 볼 수 있는데 이는 virtual function ptr이라는 명칭 보이며 이 내용물에는 

virtual로 생성했던 drive 함수가 들어 있는 것을 볼 수 있다.

 

이게 그냥 함수 때문인가 싶어서  Car 클래스에 

이렇게 함수를 하나 추가해준 후에 실행해보면

변화가 없는 것을 볼 수 있다.

 

그러면 새로 생성한 함수에 virtual을 붙여주면

이렇게 print 함수가 내부에 추가된것을 볼 수 있다.

 

이렇게 가상함수가 있는 클래스에는 이런 가상함수를 가리키는 포인터를 하나씩 가지게 된다.

그래서 가상함수가 아닌 함수의 경우는 자기 본인의 타입에 맞는 함수를 실행시키게 되고 가상함수인 함수는 실제 저 __vfptr이 가리키는 가상함수를 실행시키는 것이다.

 

그래서 만약에 업 케스팅의 경우를 한번 확인해보면

c1과 c2 변수의 타입 자체는 Car 이지만 

각각 drive함수를 실행시킬때에는 __vfptr에 등록되어 있는 클래스에 있는 drive로 가서 drive를 실행시키는 것이다.

 

디버깅 모드로 실행시켜 런타임 도중에 __vfptr의 주소 값을 바꿔보면

이렇게 ElectricCar에서 HybridCar로 변경되고 그 결과도 

이렇게 HybridCar를 바라본것과 같게 나온다.

이건 런타임 도중에 결정되기에 런타임 시점에 변경이 가능한 것이다.

 

이걸 봤을때 확실하게 가상함수가 실행될때는 __vfptr에 있는 함수로 가서 실행시킨다는점, 또한 런타임에 이게 결정된다는 점을 알 수 있다.

 

반응형