6.2 메서드의 다형성
기반 클래스 형식 포인터 변수로 파생된 개체를 관리를 할 수 있다는 것은 매우 매력적입니다. 하지만 모든 행위가 모두 동일하게 동작한다면 굳이 기반 클래스와 파생 클래스로 나눌 필요가 없겠지요. 기반 클래스 형식 포인터 변수로 관리하는 개체의 멤버 메서드를 호출할 때에 파생된 각 클래스에서 새롭게 정의한 메서드를 호출할 수 있게 할 수 있습니다. 이처럼 사용하는 곳에서 호출하는 메서드는 동일하지만 실제 동작하는 모습이 다를 수 있다는 것도 중요한 다형성의 특징입니다.
만약, 오케스트라의 모든 음악가가 같은 연주를 한다면 어떤 느낌이 들까요? 아마도 각 음악가가 연주하는 모습이 다르지만 각각의 연주가 조화를 이루기 때문에 더욱 더 장엄하고 아름답게 들리는 것이로 생각됩니다. 프로그램에서도 마찬가지입니다.
예를 들어, 기반 클래스 Musician에서 Play 메서드를 정의하고 파생된 Pianist에서 Play 메서드를 재 정의를하면 Pianist 개체는 어떻게 연주를 할까요? 아무런 명시도 하지 않으면 Pianist 개체를 관리하는 변수의 형식에 따라 연주하게 됩니다. 즉, Pianist 개체를 Musicain 포인터 변수로 관리할 경우 실제 개체의 형식이 아닌 변수의 형식인 Musician에 정의된 Play 메서드가 수행되고 Pianist 포인터 변수로 관리한다면 Pianist에 정의된 Play 메서드가 수행됩니다. 다음과 같이 Musician과 Pianist의 Play 메서드가 정의되어 있다고 가정합시다.
void Musician::Play()
{
cout<<"랄라라"<<endl;
}
void Pianist::Play()
{
cout<<"딩동댕"<<endl;
}
그리고, 다음의 예처럼 사용한다고 하면 관리하는 변수에 따라 동작합니다.
void main()
{
Musician *musician = new Pianist();
Pianist *pianist = new Pianist();
musician->Play();
pianist->Play();
delete pianist;
delete musician;
}
이는 컴파일러에서는 생성된 개체가 어떠한 형식인지를 판단하지 않기 때문입니다. 컴파일러에서는 선언된 변수 형식에 따라 다음과 같이 코드를 전개합니다. 다음은 컴파일러에 따라 전개된 코드의 모습일 뿐 실제 코드가 아닙니다.
void main()
{
Musician *musician = new Pianist();
Pianist *pianist = new Pianist();
Musician::this = musician;
Musician::Play();
Pianist::this = pianist;
Pianist::Play();
delete pianist;
delete musician;
}
[그림 6.3]
[그림 6.3]을 보시면 실제 생성한 개체는 둘 다 Pianist 개체를 생성하였지만 관리하는 형식에 따라 Play 메서드가 수행되는 것을 알 수 있습니다.
이와 같이 동작하게 되면 Orchestra에 모든 음악가를 기반 클래스인 Musicain 포인트 형식으로 관리한다고 했을 때 모두 똑같이 Play를 하게 됩니다. 이렇게 동작하는 것은 우리가 원하는 모습이 아닐 수 있습니다. 만약, 관리하는 형식은 기반 클래스인 Musician 포인트 형식이지만 실제 동작하는 것은 실존하는 개체의 형식에 따라 연주를 하게 하려면 어떻게 해야 할까요?
기반 클래스에 특정 메서드를 정의할 때 파생 클래스에서 다르게 정의할 수 있다면 virtual 키워드를 명시하여 캡슐화 하십시오. 이러면 관리하는 형식에 상관없이 실제 실존하는 개체의 형식에 따라 해당 메서드가 동작하게 됩니다.
Example.cpp |
#include <iostream> using std::cout; using std::endl; class Musician { public: virtual void Play(); }; void Musician::Play() { cout<<"랄라라"<<endl; }
class Pianist:public Musician { public: void Play(); }; void Pianist::Play() { cout<<"딩동댕"<<endl; }
void main() { Musician *musician = new Pianist(); Pianist *pianist = new Pianist(); musician->Play(); pianist->Play(); delete pianist; delete musician; } |
위와 같이 기반 클래스에서 특정 메서드에 virtual 키워드를 명시한 메서드를 가상 메서드라 얘기합니다. 이와 같은 가상 메서드를 하나라도 존재하는 개체가 생성될 때에는 멤버필드 외에 가상 함수들의 코드 메모리 주소를 보관하는 테이블이 동적으로 생성되고 이 테이블의 위치 정보를 개체는 갖게 됩니다. 그리고 파생된 클래스에서 파생 개체의 생성자를 수행하면서 해당 함수가 정의된 코드 주소로 변경하는 작업을 수행하게 됩니다. 그리고, 가상 메서드를 호출하는 부분은 컴파일러 전개에서 가상 함수 테이블에 있는 코드 주소를 호출하는 구문으로 전개합니다. 이러한 이유로 인해 virtual 키워드가 명시된 메서드는 관리하는 형식이 아닌 실존하는 개체의 메서드가 호출하게 되는 것입니다. (여기에서 메서드는 호출할 때 사용되는 도구를 의미하고 함수는 수행할 코드가 정의된 부분을 의미합니다.)
[그림 6.4]
[그림 6.4]를 보시면 관리하는 변수의 형식이 Musician 포인터이든 Pianist 포인터이든 상관없이 실존하는 개체 형식인 Pianist의 Play 메서드가 동작한다는 것을 알 수 있습니다.
[그림 6.5]
[그림 6.5] 는 Pianist 개체가 생성되는 일련의 과정을 도식한 것입니다. 상속에서 설명했듯이 Pianist 개체가 생성될 때 먼저 기반 클래스인 Musician 부분이 먼저 생성되고 Pianist 부분이 생성됩니다. Musician부분이 생성될 때 내부에 가상 메서드가 있기 때문에 멤버 필드 부분 외에 가상 함수 테이블의 위치 정보를 보관하기 위한 부분이 추가되고 가상 함수 테이블이 동적으로 생성됩니다. 그리고 Musician에 정의된 가상 함수의 코드 메모리 주소가 설정됩니다. Pianist 의 생성자 메서드에서 Panist 부분이 추가 형성되면서 가상 메서드 중에 재 정의된 것이 있다면 해당 코드 메모리 주소로 변경하게 됩니다. 그리고 가상 메서드를 호출하는 부분에 대한 컴파일 전개는 해당 메서드와 연결될 코드 메모리 주소 호출로 전개됩니다.
*이 책에서는 순수 가상 함수와 추상 클래스는 7.연산자 중복 정의, 함수 개체 실습 부분에서 다루고 있습니다. *
(모든 동영상 강의는 무료입니다.)
'언어 자료구조 알고리즘 > Escort C++' 카테고리의 다른 글
[C++] 대입 연산자 중복 정의 (0) | 2016.04.15 |
---|---|
[C++] 클래스에 연산자 중복 정의 (0) | 2016.04.15 |
[C++] 전역 연산자 중복 정의 (0) | 2016.04.15 |
[C++] 연산자 중복 정의 (0) | 2016.04.15 |
[C++] 하향 캐스팅 (0) | 2016.04.15 |
[C++] 개체의 다형성 (0) | 2016.04.15 |
[C++] 파생 시에 액세스 지정 (0) | 2016.04.15 |
[C++] 무효화 된 멤버 사용하기 (0) | 2016.04.15 |
[C++] 무효화 (0) | 2016.04.15 |
[C++] 파생 개체 생성 과정 및 초기화 (0) | 2016.04.15 |