본문 바로가기
서적 정리/C++ 기초 플러스

77.정적 결합과 동적 결합

by 민돌이2 2022. 7. 13.

프로그램이 함수를 호출할 때 실행 코드의 어느 블록을 사용할지 결정하는 것은 컴파일러에게 있다. 소스 코드의 함수 호출을 특정 블록에 있는 함수 코드를 실행하라는 것을 "함수 이름을 결합(binding)한다" 라고 한다. 컴파일 하는 동안 컴파일러는 함수의 이름과 매개변수들을 조사하는데 이때 일어나는 결합을 정적 결합(static bindind) 또는 초기 결합(early binding)이라고 한다. 그러나 가상 함수들은 이 작업을 어렵게 만든다. 프로그램을 실행할 때 사용자가 객체를 결정하기 때문에, 컴파일러는 컴파일하는 동안 그 객체가 어떤 종류인지 알 수 없다. 그러므로 컴파일러는 올바른 가상 메소드가 선택되도록 하는 코드를 만들어야 하는데, 이 작업을 동적 결합(dynamic binding) 또는 말기 결합(lately bindin)이라고 한다. 

 

 

포인터와 참조형의 호환

C++에서 동적 결합은 포인터와 참조에 의해 호출되는 메소드와 관련되어 있다. 일반적으로, C++는 한 데이터형의 주소를 다른 데이터 형의 포인터에 대입하는 것을 허용하지 않으며, 참조 역시 허용하지 않는다.

double x= 2.5;
int* pi = &x; //포인터형이 불일치하므로 대입 불가
long& rl = x; //참조형이 불일치하므로 대입 불가

그러나 기초 클래스를 지시하는 포인터나 참조는 명시적인 캐스팅이 없이도 파생 클래스 객체를 참조할 수 있다.

Dog dog(30, "Mung");
Animal* pAnim = &dog; //가능
Animal& rAnim = dog; //가능

파생 클래스 참조나 포인터를 기초 클래스 참조나 포인터로 변환하는 것을 업캐스팅(upcasting)이라 한다. 자식→부모로 외우면 쉽게 외울 수 있다. public 상속에서는 명시적인 데이터형 변환이 없어도 업캐스팅이 허용된다. 이 규칙은 is-a 관계를 나타내는 특징이다.

반대로 기초 클래스 참조나 포인터를 파생 클래스 참조나 포인터로 변환하는 것을 다운캐스팅(downcasting)이라고 한다. 역시 부모→자식로 외우면 쉽게 외울 수 있다. 다운캐스팅은 명시적인 데이터형 변환 없이는 허용되지 않는다. 생각을 해보자. 파생 클래스의 멤버나 메소드를 기초 클래스는 알 수 없다. 이런 경우 예상이 못한 에러를 야기하기 때문에 다운캐스팅의 사용은 깊이 고민해야 한다.

 

 

가상 멤버 함수와 동적 결합

참조나 포인터를 사용하여 메소드를 호출하는 과정을 다시 보자.

Dog dog; //파생 클래스 객체
Animal* animal; //기초 클래스 객체

animal = &dog; //파생 클래스 객체를 지시하는 기초 클래스 포인터
animal->Bark(); //어떤 클래스의 메소드가 호출되는가?

 

animal->Bark();의 코드는 어떤 클래스의 메소드가 호출될까? 가상 메소드가 아니라면 기초 클래스 Animal* 형의 포인터에 기초하여 Animal::Bark()가 호출될 것이다. 컴파일러는 컴파일 시에 Bark()를 Animal::Bark()에 결합할 수 있다. 따라서 컴파일러는 가상이 아닌 함수들에 대해서 정적 결합을 사용한다.

그러나 Bark가 가상 메소드라면 animal->Bark(); 코드는 Dog::Bark()를 호출한다. 일반적으로 그 객체형은 프로그램이 실행될 때 결정될 수있다. 따라서 컴파일러는 가상 함수들에 대해서 동적 결합을 사용한다.

 

대부분의 경우 동적 결합이 더 좋다. 특별한 클래스형에 맞게 설계된 메소드를 프로그램이 선택하도록 허용하기 때문인데, 그럼 아래와 같은 의문이 들 수 있다.

  • 왜 두 종류의 결합이 필요한가?
  • 동적 결합이 더 좋은데, 왜 정적 결합이 디폴트인가?
  • 동적 결합은 어떻게 동작하는가?

첫째로 효율성 문제이다.

동적 결합은 실행 시간에 결정할 수 있도록 하기위해 기초 클래스 포인터나 참조가 지시하는 객체의 종류가 무엇인지 추적해야하므로 자원 소모가 생긴다. 하지만 정적 경합의 경우 어떤 객체를 사용하는지 알고 있기 때문에 자원 소모가 생기지 않는다.

둘째로 다시 정의되면 안 된다는 의도를 나타낼 수 있다.

개발자가 직접 구현해야하는 함수와 구현되어 있는 함수를 찾아서 들어가지 않아도 된다는 의미이다.

컴파일러가 가상함수를 다루는 일반적인 방법은 각각의 객체에 숨겨진 멤버를 하나씩 추가하는 것이다. 이 멤버는 함수의 주소들로 이루어진 배열을 지시하는 포인터를 저장하고, 이 배열을 가상 함수 테이블(virtual function table:vtbl)이라고 한다. 파생 클래스가 가상 함수를 재정의를 하지 않으면 가상 함수 테이블은 기초 클래스의 가상 함수의 주소를 저장한다. 반대로 재정의를 하면 파생 클래스의 가상 함수의 주소를 저장한다. 정리하자면 가상 함수를 사용하면 메모리와 실행 속도 면에서 다음과 같은 약간의 부담이 따른다.

  • 각 객체의 크기가 주소 하나를 저장하는 데 필요한 양만큼 커진다.
  • 각각의 클래스에 대해, 컴파일러는 가상 함수들의 주소로 이루어진 하나의 테이블(배열)을 만든다.
  • 각각의 함수 호출에 대해 실행할 함수의 주소를 얻기 위해 테이블에 접근하는 가외의 단계가 필요하다.

 

 

가상 메소드에 대해 알아야 할 사항

생성자는 가상으로 선언할 수 없다. 파생 클래스 객체의 생성은, 기초 클래스 생성자가 아니라 파생 클래스 생성자를 호출한다. 파생 클래스의 생성자가 호출된 후 기초 클래스 생성자를 호출한다. 이 시퀀스는 상속 메커니즘과 다르며 파생 클래스는 기초 클래스 생성자를 상속하지 않으므로, 생성자를 가상으로 만들 필요가 없다.

 

기초 클래스의 소멸자는 가상으로 선언해야 한다. 파생 클래스를 소멸할 때 기초 클래스의 소멸자가 가상 함수가 아니여서 디폴트 정적 결합이 적용된다면, 기초 클래스의 소멸자를 호출할 것이다. 그러면 파생 클래스의 소멸자는 호출되지 않는다. 하지만 소멸자가 가상이라면 파생 클래스의 소멸자를 호출한다. 그 소멸자는 파생 클래스 성분이 지시하는 메모리를 해제한 후, 기초 클래스의 소멸자를 호출하여 메모리를 해제한다.

 

프렌드는 가상 함수가 될 수 없다. 멤버 함수만 가상함수가 될 수 있는데, 프렌드는 클래스 멤버가 아니기 때문이다. 이 점 때문에 설계에 문제가 생긴다면, 프렌드 함수가 내부적으로 가상 멤버 함수를 사용하게 하여 문제를 해결할 수 있다.

 

가상 함수를 재정의하지 않으면 기초 클래스의 가상 함수를 호출한다. 만약 파생 클래스가 계속 파생하여 소세지로 되어있다면 가장 가까운 기초 클래스의 가상 함수를 호출한다.

 

가상 함수를 재정의하면 기초 클래스의 메소드는 은닉된다. 만약 함수의 이름은 같은데 매개변수가 다른 경우 어떻게 될까?

//Animal.h
class Animal
{
public:
	virtual void Bark(string sound) const;

	...
};

class Dog : public Animal
{
public:
	virtual void Bark() const;

	...
};
//main.cpp
#include "Animal.h"

int main()
{
	Dog dog;
	dog.Bark(); //가능
	dog.Bark("Mung"); //불가

	system("Pause");
	return 0;
}

새로운 정의는 파생 크래스의 Bark() 함수를 정의한다. 오버로딩된 두 개의 함수 버전을 생성하진 않는다. 이 경우 기초 클래스의 Bark(string sound)를 가린다. 동일한 시그니처를 가지고 있는 기초 클래스의 선언만 가리는 것이 아닌 같은 이름을 가진 모든 기초 클래스 메소드들을 가린다. 따라서 두 가지 규칙을 지켜야한다.

    •  상속된 메소드를 재정의할 경우 오리지널 원형과 정확히 일치해야 한다.
    • 기초 클래스 선언이 오버로딩되어 있다면, 파생 클래스에서 모든 기초 클래스 버전들을 재정의해야 한다.
728x90

'서적 정리 > C++ 기초 플러스' 카테고리의 다른 글

81.클래스 설계 복습  (0) 2022.07.19
80.상속과 동적 메모리 대입  (0) 2022.07.18
79.추상화 기초 클래스  (0) 2022.07.14
78.접근제어: protected  (0) 2022.07.13
76.public 다형 상속  (0) 2022.07.12
75.상속: is-a 관계  (0) 2022.07.12
74.간단한 기초 클래스부터 시작하자  (0) 2022.07.12
차례  (0) 2022.07.12

댓글