["EffectiveC++"] == 가상 함수 == http://netdb.chungbuk.ac.kr/~jrshin/C++/chap10.html === 가상 함수는 어떻게 움직이는가? === C++는 가상 함수가 움직이는 방법을 지정하지만 구현은 컴파일러 작성자에게 남겨 두었따. 가상 함수를 사용하기 위해 구현을 알 필요는 없지만, 그것이 이루어지는 방법을 알면 개념을 좀더 잘 이해할 수 있으므로 잠시 살펴보자. 컴파일러가 가상 함수를 처리하는 일반적인 방법은 각 객체에 은닉된 멤버를 추가하는 것이다. 은닉된 멤버는 함수 주소의 배열에 대한 포인터를 보관한다.그러한 배열을 대개 표(table)라고 하는데, 이것은 그 클래스의 객체에 대해 선언된 가상 함수의 주소를 저장한다. 예를 들어, 기초 클래스의 객체는 그 클래스에 대한 모든 가상 함수의 주소로 이루어진 표 포인터를 갖게 되고, 유도 클래스 객체는 별도의 주소표 포인터를 가지게 된다. 유도 클래스가 가상 함수의 새로운 정의를 제공하면 표는 새 함수의 주소를 저장한다. 유도 클래스가 가상 함수를 재정의하지 않으면 표는 원본 함수의 주소를 저장한다.그리고 유도 클래스가 새 함수를 정의하여 그것을 가상으로 만들면 그 주소가 표에 추가된다. 한 클래스에 대해 가상함수를 1개만 추가하든 또는 10개를 추가하든 주소 멤버는 한 객체에 하나만 추가하면 된다. 다만 추가하는 가상 함수의 개수에 따라 표 크기는 달라질 것이다.[[BR]] ..[[BR]] 가상 함수를 호출하면, 프로그램은 객체에 저장된 표 주소를 조사하고 해당 함수 주소표로 간다. 클래스 선언에 정의된 첫째 가상 함수를 사용하면 프로그램은 배열의 첫째 함수 주소를 사용하고 그 주소를 가진 함수를 실행시키며, 클래스 선언에 셋째 가상함수를 사용하면 프로그램은 주소가 배열의 셋째 원소인 함수를사용한다.[[BR]] [[BR]] 쉽게 말해서 가상 함수를 사용하면 메모리와 실행 속도에 다음과 같은 약간의 부담이 따른다.[[BR]] * 각 객체의 크기가 주소를 저장하는 데 필요한 양만큼 커진다 * 컴파일러는 각 클래스에 대해 가상 함수의 주소표(주소 배열)를 만든다. * 각 함수를 호출할 때, 표로 가서 주소를 조사하는 몇 단계가 더 필요하다. 비가상 함수는 가상 함수보다 능률은 조금 낫지만 동적 결합을 제공하지는 않는다. === 동적 결합 === 기초 클래스와 유도 클래스의 가상 함수를 선택적으로 쓰기위한 방법?? [[BR]] 메서드를 호출하는 객체의 데이터형에 따라 사용된 메서드가 결정된다. {{{~cpp BankAccount bretta; // 기초 클래스 객체 Overdraft ophelia; // 유도 클래스 객체 bretta.ViewAcct (); // BacnkAccount::ViewAcct ()를 사용 ophelia.ViewAcct (); // Oberdraft::ViewAcct ()를 사용 }}} 그런데, 포인터를 사용하여 메서드를 호출한다고 가정해보자 {{{~cpp BankAccount *bp = &bretta; // BackAccount객체를 지시 bp->ViewAcct (); // BackAccount::ViewAcct ()를 사용 bp = &ophelia; // Overdraft 객체를 지시하는 BackAccount 포인터 bp->ViewAcct (); // 헉! 어느 버전을 사용하지? }}} 여기서 컴파일러가 포인터형을 사용한다면 마지막 명령문은 BackAccount::ViewAcct ()를 호출하겠지만 포인터가 지시하는 객체의 데이터 형을 사용한다면Overdraft::ViewAcct ()를 호출할 것이다. 그렇다면 컴파일러는 어떤 선택을 할것인가? [[BR]] [[BR]] 기본적으로 C++는 포인터나 참조의 데이터형을 사용하여 사용할 함수를 결정하고 지시되거나 참조된 대상 객체의 데이터형을 무시한다. 그러므로 앞의 예에서 프로그램은 BackAccount::ViewAcct ()를 사용할 것이다. 이렇게 하는 이유는 분명하다. 컴파일러가 데이터형을 모르는 경우가 많기 때문이다. 예를 들어.. {{{~cpp int nSelect; cin >> nSelect; BackAccount *bp; if (nSelect == 1) bp = new BackAccount; else if (nSelect == 2) bp = new Overdraft; bp->ViewAcct (); }}} 컴파일러는 컴파일할 때에는 실행 시간에 어떤 항목이 선택될지를 알 수 없으므로 bp가 지시하는 객체의 데이터형을 알 수 없다. 따라서 컴파일러가 컴파일할 때 할 수 있는 일은 클래스 메서드를 참조나 포인터의 데이터 형에 일치시키는 것뿐이다. 이 경우를 early binding 또는 static binding이라 한다.[[BR]] {{{~cpp // 정적 결합 사용 bp = &ophelia; // Overdraft객체를 지시하는 BackAccount포인터 bp->ViewAcct (); // BackAccount::ViewAcct ()를 사용 }}} Overdraft 객체에 BackAccount::ViewAcct ()를 사용해도 문제될 것은 없고 다만 일부 테이터가 출력되지 않을 뿐이다. 그렇다면 bp->ViewAcct ()를 포인터형 객체형에 결합하여 Overdraft::ViewAcct ()를 호출할 수 있으면 좋을 것이다. C++는 이 목표를 달성하기 위해 late binding 또는 dynamic binding이라는 것을 제공한다. 이 것에서는 컴파일러가 사용할 클래스 메서드를 결정하지 않고 프로그램 실행 시간에 메서드 함수 호출을 실제로 수행할 때마다 사용할 클래스 메서드를 결정하게 한다. 이 것을 사용하면 참조나 포인터가 지시하는 객체의 형에 따라 메서드를 선택할 수 있다.[[BR]] {{{~cpp // 동적 결합 BackAccount *bp = &bretta; bp->ViewAcct (); // BackAccount::ViewAcct ()를 사용 bp = &ophelia; bp->ViewAcct (); // Overdraft::ViewAcct ()를 사용 }}}