E D R , A S I H C RSS

가상함수

가상 함수는 어떻게 움직이는가?

C++는 가상 함수가 움직이는 방법을 지정하지만 구현은 컴파일러 작성자에게 남겨 두었따. 가상 함수를 사용하기 위해 구현을 알 필요는 없지만, 그것이 이루어지는 방법을 알면 개념을 좀더 잘 이해할 수 있으므로 잠시 살펴보자. 컴파일러가 가상 함수를 처리하는 일반적인 방법은 각 객체에 은닉된 멤버를 추가하는 것이다. 은닉된 멤버는 함수 주소의 배열에 대한 포인터를 보관한다.그러한 배열을 대개 표(table)라고 하는데, 이것은 그 클래스의 객체에 대해 선언된 가상 함수의 주소를 저장한다. 예를 들어, 기초 클래스의 객체는 그 클래스에 대한 모든 가상 함수의 주소로 이루어진 표 포인터를 갖게 되고, 유도 클래스 객체는 별도의 주소표 포인터를 가지게 된다. 유도 클래스가 가상 함수의 새로운 정의를 제공하면 표는 새 함수의 주소를 저장한다. 유도 클래스가 가상 함수를 재정의하지 않으면 표는 원본 함수의 주소를 저장한다.그리고 유도 클래스가 새 함수를 정의하여 그것을 가상으로 만들면 그 주소가 표에 추가된다. 한 클래스에 대해 가상함수를 1개만 추가하든 또는 10개를 추가하든 주소 멤버는 한 객체에 하나만 추가하면 된다. 다만 추가하는 가상 함수의 개수에 따라 표 크기는 달라질 것이다.

..

가상 함수를 호출하면, 프로그램은 객체에 저장된 표 주소를 조사하고 해당 함수 주소표로 간다. 클래스 선언에 정의된 첫째 가상 함수를 사용하면 프로그램은 배열의 첫째 함수 주소를 사용하고 그 주소를 가진 함수를 실행시키며, 클래스 선언에 셋째 가상함수를 사용하면 프로그램은 주소가 배열의 셋째 원소인 함수를사용한다.



쉽게 말해서 가상 함수를 사용하면 메모리와 실행 속도에 다음과 같은 약간의 부담이 따른다.

  • 각 객체의 크기가 주소를 저장하는 데 필요한 양만큼 커진다
  • 컴파일러는 각 클래스에 대해 가상 함수의 주소표(주소 배열)를 만든다.
  • 각 함수를 호출할 때, 표로 가서 주소를 조사하는 몇 단계가 더 필요하다.
비가상 함수는 가상 함수보다 능률은 조금 낫지만 동적 결합을 제공하지는 않는다.

동적 결합

기초 클래스와 유도 클래스의 가상 함수를 선택적으로 쓰기위한 방법??

메서드를 호출하는 객체의 데이터형에 따라 사용된 메서드가 결정된다.
~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 ()를 호출할 것이다. 그렇다면 컴파일러는 어떤 선택을 할것인가?



기본적으로 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이라 한다.

~cpp 
// 정적 결합 사용
bp = &ophelia; // Overdraft객체를 지시하는 BackAccount포인터
bp->ViewAcct (); // BackAccount::ViewAcct ()를 사용
Overdraft 객체에 BackAccount::ViewAcct ()를 사용해도 문제될 것은 없고 다만 일부 테이터가 출력되지 않을 뿐이다. 그렇다면 bp->ViewAcct ()를 포인터형 객체형에 결합하여 Overdraft::ViewAcct ()를 호출할 수 있으면 좋을 것이다. C++는 이 목표를 달성하기 위해 late binding 또는 dynamic binding이라는 것을 제공한다. 이 것에서는 컴파일러가 사용할 클래스 메서드를 결정하지 않고 프로그램 실행 시간에 메서드 함수 호출을 실제로 수행할 때마다 사용할 클래스 메서드를 결정하게 한다. 이 것을 사용하면 참조나 포인터가 지시하는 객체의 형에 따라 메서드를 선택할 수 있다.

~cpp 
// 동적 결합
BackAccount *bp = &bretta; 
bp->ViewAcct (); // BackAccount::ViewAcct ()를 사용
bp = &ophelia;
bp->ViewAcct (); // Overdraft::ViewAcct ()를 사용
Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2021-02-07 05:28:38
Processing time 0.0227 sec