가상 함수는 어떻게 움직이는가? ¶
C++는 가상 함수가 움직이는 방법을 지정하지만 구현은 컴파일러 작성자에게 남겨 두었따. 가상 함수를 사용하기 위해 구현을 알 필요는 없지만, 그것이 이루어지는 방법을 알면 개념을 좀더 잘 이해할 수 있으므로 잠시 살펴보자. 컴파일러가 가상 함수를 처리하는 일반적인 방법은 각 객체에 은닉된 멤버를 추가하는 것이다. 은닉된 멤버는 함수 주소의 배열에 대한 포인터를 보관한다.그러한 배열을 대개 표(table)라고 하는데, 이것은 그 클래스의 객체에 대해 선언된 가상 함수의 주소를 저장한다. 예를 들어, 기초 클래스의 객체는 그 클래스에 대한 모든 가상 함수의 주소로 이루어진 표 포인터를 갖게 되고, 유도 클래스 객체는 별도의 주소표 포인터를 가지게 된다. 유도 클래스가 가상 함수의 새로운 정의를 제공하면 표는 새 함수의 주소를 저장한다. 유도 클래스가 가상 함수를 재정의하지 않으면 표는 원본 함수의 주소를 저장한다.그리고 유도 클래스가 새 함수를 정의하여 그것을 가상으로 만들면 그 주소가 표에 추가된다. 한 클래스에 대해 가상함수를 1개만 추가하든 또는 10개를 추가하든 주소 멤버는 한 객체에 하나만 추가하면 된다. 다만 추가하는 가상 함수의 개수에 따라 표 크기는 달라질 것이다.
..
가상 함수를 호출하면, 프로그램은 객체에 저장된 표 주소를 조사하고 해당 함수 주소표로 간다. 클래스 선언에 정의된 첫째 가상 함수를 사용하면 프로그램은 배열의 첫째 함수 주소를 사용하고 그 주소를 가진 함수를 실행시키며, 클래스 선언에 셋째 가상함수를 사용하면 프로그램은 주소가 배열의 셋째 원소인 함수를사용한다.
쉽게 말해서 가상 함수를 사용하면 메모리와 실행 속도에 다음과 같은 약간의 부담이 따른다.
..
가상 함수를 호출하면, 프로그램은 객체에 저장된 표 주소를 조사하고 해당 함수 주소표로 간다. 클래스 선언에 정의된 첫째 가상 함수를 사용하면 프로그램은 배열의 첫째 함수 주소를 사용하고 그 주소를 가진 함수를 실행시키며, 클래스 선언에 셋째 가상함수를 사용하면 프로그램은 주소가 배열의 셋째 원소인 함수를사용한다.
쉽게 말해서 가상 함수를 사용하면 메모리와 실행 속도에 다음과 같은 약간의 부담이 따른다.
- 각 객체의 크기가 주소를 저장하는 데 필요한 양만큼 커진다
- 컴파일러는 각 클래스에 대해 가상 함수의 주소표(주소 배열)를 만든다.
- 각 함수를 호출할 때, 표로 가서 주소를 조사하는 몇 단계가 더 필요하다.
동적 결합 ¶
기초 클래스와 유도 클래스의 가상 함수를 선택적으로 쓰기위한 방법??
메서드를 호출하는 객체의 데이터형에 따라 사용된 메서드가 결정된다.
기본적으로 C++는 포인터나 참조의 데이터형을 사용하여 사용할 함수를 결정하고 지시되거나 참조된 대상 객체의 데이터형을 무시한다. 그러므로 앞의 예에서 프로그램은 BackAccount::ViewAcct ()를 사용할 것이다. 이렇게 하는 이유는 분명하다. 컴파일러가 데이터형을 모르는 경우가 많기 때문이다. 예를 들어..
메서드를 호출하는 객체의 데이터형에 따라 사용된 메서드가 결정된다.
~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 ()를 사용