runtime polymorphism c
C ++의 런타임 다형성에 대한 자세한 연구.
런타임 다형성은 동적 다형성 또는 후기 바인딩이라고도합니다. 런타임 다형성에서 함수 호출은 런타임에 해결됩니다.
반대로 컴파일 타임이나 정적 다형성을 위해 컴파일러는 런타임에 개체를 추론 한 다음 개체에 바인딩 할 함수 호출을 결정합니다. C ++에서 런타임 다형성은 메서드 재정의를 사용하여 구현됩니다.
이 튜토리얼에서는 런타임 다형성에 대한 모든 것을 자세히 살펴볼 것입니다.
학습 내용 :
함수 재정의
함수 재정의는 기본 클래스에 정의 된 함수가 파생 클래스에 다시 정의되는 메커니즘입니다. 이 경우 파생 클래스에서 함수가 재정의되었다고 말합니다.
함수 재정의는 클래스 내에서 수행 될 수 없음을 기억해야합니다. 이 함수는 파생 클래스에서만 재정의됩니다. 따라서 함수 재정의를 위해 상속이 있어야합니다.
두 번째는 우리가 재정의하는 기본 클래스의 함수가 동일한 서명 또는 프로토 타입을 가져야한다는 것입니다. 즉, 동일한 이름, 동일한 반환 유형 및 동일한 인수 목록을 가져야합니다.
메서드 재정의를 보여주는 예를 살펴 보겠습니다.
#include using namespace std; class Base { public: void show_val() { cout << 'Class::Base'< 산출:
클래스 ::베이스
분류 :: 유도
위 프로그램에는 기본 클래스와 파생 클래스가 있습니다. 기본 클래스에는 파생 클래스에서 재정의 된 show_val 함수가 있습니다. main 함수에서는 Base 및 Derived 클래스 각각에 개체를 만들고 각 개체와 함께 show_val 함수를 호출합니다. 원하는 출력을 생성합니다.
위의 각 클래스의 객체를 이용한 함수 바인딩은 정적 바인딩의 예입니다.
이제 기본 클래스 포인터를 사용하고 파생 클래스 개체를 내용으로 할당 할 때 어떤 일이 발생하는지 살펴 보겠습니다.
예제 프로그램은 다음과 같습니다.
최고의 무료 레지스트리 클리너 윈도우 7
#include using namespace std; class Base { public: void show_val() { cout << 'Class::Base'; } }; class Derived:public Base { public: void show_val() //overridden function { cout <<'Class::Derived'; } }; int main() { Base* b; //Base class pointer Derived d; //Derived class object b = &d; b->show_val(); //Early Binding }
산출:
클래스 ::베이스
이제 출력이 'Class :: Base'임을 알 수 있습니다. 따라서 기본 포인터가 어떤 유형의 객체를 보유하고 있는지에 관계없이 프로그램은 기본 포인터가 유형 인 클래스의 함수 내용을 출력합니다. 이 경우 정적 링크도 수행됩니다.
기본 포인터 출력, 올바른 내용 및 적절한 링크를 만들기 위해 함수의 동적 바인딩을 수행합니다. 이는 다음 섹션에서 설명하는 가상 기능 메커니즘을 사용하여 달성됩니다.
가상 기능
재정의 된 함수는 함수 본문에 동적으로 바인딩되어야하므로 'virtual'키워드를 사용하여 기본 클래스 함수를 가상으로 만듭니다. 이 가상 함수는 파생 클래스에서 재정의 된 함수이며 컴파일러는이 함수에 대해 후기 또는 동적 바인딩을 수행합니다.
이제 다음과 같이 virtual 키워드를 포함하도록 위 프로그램을 수정 해 보겠습니다.
#include using namespace std;. class Base { public: virtual void show_val() { cout << 'Class::Base'; } }; class Derived:public Base { public: void show_val() { cout <<'Class::Derived'; } }; int main() { Base* b; //Base class pointer Derived d; //Derived class object b = &d; b->show_val(); //late Binding }
산출:
분류 :: 유도
그래서 위의 Base 클래스 정의에서 show_val 함수를“virtual”로 만들었습니다. 기본 클래스 함수가 가상으로 만들어 지므로 파생 클래스 개체를 기본 클래스 포인터에 할당하고 show_val 함수를 호출하면 런타임에 바인딩이 발생합니다.
따라서 기본 클래스 포인터에 파생 클래스 개체가 포함되어 있으므로 파생 클래스의 show_val 함수 본문이 show_val 함수에 바인딩되어 출력이됩니다.
C ++에서 파생 클래스의 재정의 된 함수는 전용 일 수도 있습니다. 컴파일러는 컴파일 타임에만 객체의 유형을 확인하고 런타임에 함수를 바인딩하므로 함수가 공개 또는 비공개 인 경우에도 차이가 없습니다.
함수가 기본 클래스에서 가상으로 선언되면 모든 파생 클래스에서 가상이됩니다.
그러나 지금까지 우리는 바인딩 할 올바른 함수를 식별하는 데 가상 함수가 정확히 어떤 역할을하는지, 즉 실제로 얼마나 늦은 바인딩이 실제로 발생하는지에 대해 논의하지 않았습니다.
가상 함수는 다음 개념을 사용하여 런타임시 정확하게 함수 본문에 바인딩됩니다. 가상 테이블 (VTABLE) 숨겨진 포인터 _vptr.
이 두 개념은 모두 내부 구현이며 프로그램에서 직접 사용할 수 없습니다.
가상 테이블 및 _vptr 작업
먼저 가상 테이블 (VTABLE)이 무엇인지 이해하겠습니다.
컴파일 타임에 컴파일러는 가상 함수가있는 클래스와 가상 함수가있는 클래스에서 파생 된 클래스에 대해 각각 하나의 VTABLE을 설정합니다.
VTABLE에는 클래스의 개체에서 호출 할 수있는 가상 함수에 대한 함수 포인터 인 항목이 포함됩니다. 각 가상 기능에 대해 하나의 기능 포인터 항목이 있습니다.
순수 가상 기능의 경우이 항목은 NULL입니다. (이것이 추상 클래스를 인스턴스화 할 수없는 이유입니다).
다음 엔티티 인 vtable 포인터라고하는 _vptr은 컴파일러가 기본 클래스에 추가하는 숨겨진 포인터입니다. 이 _vptr은 클래스의 vtable을 가리 킵니다. 이 기본 클래스에서 파생 된 모든 클래스는 _vptr을 상속합니다.
가상 함수를 포함하는 클래스의 모든 객체는이 _vptr을 내부적으로 저장하며 사용자에게 투명합니다. 객체를 사용하는 모든 가상 함수 호출은이 _vptr을 사용하여 해결됩니다.
vtable 및 _vtr의 작동을 보여주는 예제를 살펴 보겠습니다.
#include using namespace std; class Base_virtual { public: virtual void function1_virtual() {cout<<'Base :: function1_virtual()
';}; virtual void function2_virtual() {cout<<'Base :: function2_virtual()
';}; virtual ~Base_virtual(){}; }; class Derived1_virtual: public Base_virtual { public: ~Derived1_virtual(){}; virtual void function1_virtual() { coutfunction2_virtual(); delete (b); return (0); }
산출:
Derived1_virtual :: function1_virtual ()
베이스 :: function2_virtual ()
위의 프로그램에는 두 개의 가상 함수와 가상 소멸자가있는 기본 클래스가 있습니다. 우리는 또한 기본 클래스에서 클래스를 파생 시켰습니다. 하나의 가상 기능 만 재정의했습니다. 주 함수에서 파생 클래스 포인터는 기본 포인터에 할당됩니다.
그런 다음 기본 클래스 포인터를 사용하여 두 가상 함수를 모두 호출합니다. 재정의 된 함수는 기본 함수가 아닌 호출 될 때 호출됩니다. 두 번째 경우에는 함수가 재정의되지 않았으므로 기본 클래스 함수가 호출됩니다.
이제 위 프로그램이 vtable과 _vptr을 사용하여 내부적으로 어떻게 표현되는지 살펴 보겠습니다.
이전 설명에 따라 가상 함수가있는 클래스가 두 개이므로 각 클래스에 하나씩 두 개의 vtable이 있습니다. 또한 _vptr은 기본 클래스에 표시됩니다.
위의 그림은 위의 프로그램에서 vtable 레이아웃이 어떻게 될지에 대한 그림입니다. 기본 클래스의 vtable은 간단합니다. 파생 클래스의 경우 function1_virtual 만 재정의됩니다.
따라서 파생 클래스 vtable에서 function1_virtual에 대한 함수 포인터가 파생 클래스의 재정의 된 함수를 가리 킵니다. 반면 function2_virtual의 함수 포인터는 기본 클래스의 함수를 가리 킵니다.
따라서 위의 프로그램에서 기본 포인터에 파생 클래스 개체가 할당되면 기본 포인터는 파생 클래스의 _vptr을 가리 킵니다.
따라서 b-> function1_virtual () 호출이 이루어지면 파생 클래스에서 function1_virtual이 호출되고 함수 호출 b-> function2_virtual ()이 수행 될 때이 함수 포인터가 기본 클래스 함수 인 기본 클래스 함수를 가리 킵니다. 호출됩니다.
순수 가상 함수 및 추상 클래스
이전 섹션에서 C ++의 가상 함수에 대한 세부 정보를 보았습니다. C ++에서는 ' 순수 가상 기능 일반적으로 0과 동일합니다.
순수 가상 함수는 아래와 같이 선언됩니다.
virtual return_type function_name(arg list) = 0;
'라고하는 순수 가상 함수가 하나 이상있는 클래스 추상 클래스 ”. 추상 클래스를 인스턴스화 할 수 없습니다. 즉 추상 클래스의 객체를 만들 수 없습니다.
이는 VTABLE (가상 테이블)의 모든 가상 기능에 대한 항목이 만들어 졌음을 알고 있기 때문입니다. 그러나 순수 가상 기능의 경우이 항목에는 주소가 없으므로 불완전하게 렌더링됩니다. 따라서 컴파일러는 불완전한 VTABLE 항목이있는 클래스에 대한 개체를 만드는 것을 허용하지 않습니다.
이것이 추상 클래스를 인스턴스화 할 수없는 이유입니다.
아래 예제는 순수 가상 기능과 추상 클래스를 보여줍니다.
#include using namespace std; class Base_abstract { public: virtual void print() = 0; // Pure Virtual Function }; class Derived_class:public Base_abstract { public: void print() { cout <<'Overriding pure virtual function in derived class
'; } }; int main() { // Base obj; //Compile Time Error Base_abstract *b; Derived_class d; b = &d; b->print(); }
산출:
파생 클래스에서 순수 가상 함수 재정의
위의 프로그램에는 추상 클래스로 만드는 순수 가상 함수를 포함하는 Base_abstract로 정의 된 클래스가 있습니다. 그런 다음 Base_abstract에서 'Derived_class'클래스를 파생시키고 그 안에있는 순수 가상 함수 print를 재정의합니다.
주요 기능에서는 첫 번째 줄이 주석 처리되지 않습니다. 이는 주석 처리를 제거하면 추상 클래스에 대한 개체를 만들 수 없기 때문에 컴파일러에서 오류가 발생하기 때문입니다.
그러나 두 번째 줄부터 코드가 작동합니다. 기본 클래스 포인터를 성공적으로 만든 다음 파생 클래스 개체를 할당 할 수 있습니다. 다음으로 파생 클래스에서 재정의 된 인쇄 함수의 내용을 출력하는 인쇄 함수를 호출합니다.
추상 클래스의 몇 가지 특성을 간단히 나열 해 보겠습니다.
- 추상 클래스를 인스턴스화 할 수 없습니다.
- 추상 클래스에는 하나 이상의 순수 가상 함수가 포함됩니다.
- 추상 클래스를 인스턴스화 할 수는 없지만 항상이 클래스에 대한 포인터 나 참조를 만들 수 있습니다.
- 추상 클래스는 순수 가상 함수와 함께 속성 및 메서드와 같은 일부 구현을 가질 수 있습니다.
- 추상 클래스에서 클래스를 파생시킬 때 파생 클래스는 추상 클래스의 모든 순수 가상 함수를 재정의해야합니다. 그렇게하지 못하면 파생 클래스도 추상 클래스가됩니다.
가상 소멸자
클래스의 소멸자는 가상으로 선언 될 수 있습니다. 업 캐스트를 수행 할 때마다, 즉 파생 클래스 객체를 기본 클래스 포인터에 할당 할 때마다 일반 소멸자는 허용되지 않는 결과를 생성 할 수 있습니다.
예를 들어,일반 소멸자의 다음 업 캐스팅을 고려하십시오.
#include using namespace std; class Base { public: ~Base() { cout << 'Base Class:: Destructor
'; } }; class Derived:public Base { public: ~Derived() { cout<< 'Derived class:: Destructor
'; } }; int main() { Base* b = new Derived; // Upcasting delete b; }
산출:
기본 클래스 :: 소멸자
위의 프로그램에는 기본 클래스에서 상속 된 파생 클래스가 있습니다. 기본에서 파생 클래스의 개체를 기본 클래스 포인터에 할당합니다.
이상적으로는 'delete b'가 호출 될 때 호출되는 소멸자는 파생 된 클래스의 소멸자 여야하지만 출력에서 기본 클래스의 소멸자가 호출 된 것을 기본 클래스 포인터가 가리키는 것으로 볼 수 있습니다.
이로 인해 파생 클래스 소멸자가 호출되지 않고 파생 클래스 개체가 그대로 유지되어 메모리 누수가 발생합니다. 이에 대한 해결책은 개체 포인터가 올바른 소멸자를 가리키고 개체의 적절한 소멸이 수행되도록 기본 클래스 생성자를 가상으로 만드는 것입니다.
가상 소멸자의 사용은 아래 예제에 나와 있습니다.
#include using namespace std; class Base { public: virtual ~Base() { cout << 'Base Class:: Destructor
'; } }; class Derived:public Base { public: ~Derived() { cout<< 'Derived class:: Destructor
'; } }; int main() { Base* b = new Derived; // Upcasting delete b; }
산출:
파생 클래스 :: 소멸자
기본 클래스 :: 소멸자
이것은 기본 클래스 소멸자 앞에 가상 키워드를 추가 한 것을 제외하고는 이전 프로그램과 동일한 프로그램입니다. 기본 클래스 소멸자를 가상으로 만들어 원하는 출력을 얻었습니다.
파생 클래스 개체를 기본 클래스 포인터에 할당 한 다음 기본 클래스 포인터를 삭제하면 소멸자가 개체 생성의 역순으로 호출되는 것을 볼 수 있습니다. 즉, 먼저 파생 클래스 소멸자가 호출되고 개체가 소멸 된 다음 기본 클래스 개체가 소멸됩니다.
노트 : C ++에서 생성자는 객체를 생성하고 초기화하는 데 관여하므로 가상이 될 수 없습니다. 따라서 모든 생성자가 완전히 실행되어야합니다.
결론
런타임 다형성은 메서드 재정의를 사용하여 구현됩니다. 이는 각각의 객체로 메서드를 호출 할 때 잘 작동합니다. 그러나 기본 클래스 포인터가 있고 파생 클래스 개체를 가리키는 기본 클래스 포인터를 사용하여 재정의 된 메서드를 호출하면 정적 링크로 인해 예기치 않은 결과가 발생합니다.
이를 극복하기 위해 우리는 가상 기능의 개념을 사용합니다. vtables 및 _vptr의 내부 표현을 통해 가상 함수는 원하는 함수를 정확하게 호출하는 데 도움이됩니다. 이 튜토리얼에서는 C ++에서 사용되는 런타임 다형성에 대해 자세히 살펴 보았습니다.
이것으로 C ++의 객체 지향 프로그래밍에 대한 튜토리얼을 마칩니다. 이 튜토리얼이 C ++의 객체 지향 프로그래밍 개념을 더 잘 이해하는 데 도움이되기를 바랍니다.
=> 처음부터 C ++를 배우려면 여기를 방문하십시오.
추천 도서