질문자 :Jake Wilson
저는 C++를 배우고 있으며 가상 기능을 배우고 있습니다.
내가 읽은 것(책과 온라인)에서 가상 함수는 파생 클래스에서 재정의할 수 있는 기본 클래스의 함수입니다.
virtual
을 사용하지 않고 파생 클래스의 기본 함수를 재정의할 수 있었습니다.
여기서 내가 놓치고 있는 것이 무엇입니까? 가상 기능에 더 많은 것이 있다는 것을 알고 있고 중요한 것 같아서 정확히 무엇인지 명확히 하고 싶습니다. 온라인에서 간단한 답변을 찾을 수 없습니다.
virtual
기능이 무엇인지 뿐만 아니라 왜 필요한지 이해한 방법입니다.
다음 두 클래스가 있다고 가정해 보겠습니다.
class Animal { public: void eat() { std::cout << "I'm eating generic food."; } }; class Cat : public Animal { public: void eat() { std::cout << "I'm eating a rat."; } };
주요 기능에서:
Animal *animal = new Animal; Cat *cat = new Cat; animal->eat(); // Outputs: "I'm eating generic food." cat->eat(); // Outputs: "I'm eating a rat."
지금까지는 너무 좋죠? 동물은 일반 음식을 먹고 고양이는 쥐를 먹으며 모두 virtual
.
eat()
이 중간 함수를 통해 호출되도록 약간 변경해 보겠습니다(이 예에서는 간단한 함수).
// This can go at the top of the main.cpp file void func(Animal *xyz) { xyz->eat(); }
이제 주요 기능은 다음과 같습니다.
Animal *animal = new Animal; Cat *cat = new Cat; func(animal); // Outputs: "I'm eating generic food." func(cat); // Outputs: "I'm eating generic food."
어... 우리는 고양이를 func()
전달했지만 쥐를 먹지는 않을 것입니다. func()
를 오버로드해야 Cat*
합니까? Animal 에서 더 많은 동물을 파생시켜야 하는 경우 모두 고유한 func()
가 필요합니다.
Animal
클래스의 eat()
을 가상 함수로 만드는 것입니다.
class Animal { public: virtual void eat() { std::cout << "I'm eating generic food."; } }; class Cat : public Animal { public: void eat() { std::cout << "I'm eating a rat."; } };
기본:
func(animal); // Outputs: "I'm eating generic food." func(cat); // Outputs: "I'm eating a rat."
완료.
M Perry"가상"이 없으면 "조기 바인딩"을 얻습니다. 사용되는 메서드 구현은 호출하는 포인터의 유형에 따라 컴파일 타임에 결정됩니다.
"가상"을 사용하면 "늦은 바인딩"을 얻습니다. 메소드의 어떤 구현이 사용되는지는 런타임에 가리키는 객체의 유형(원래 생성된 방식)에 따라 결정됩니다. 이것은 해당 개체를 가리키는 포인터의 유형을 기반으로 생각하는 것과 반드시 같지는 않습니다.
class Base { public: void Method1 () { std::cout << "Base::Method1" << std::endl; } virtual void Method2 () { std::cout << "Base::Method2" << std::endl; } }; class Derived : public Base { public: void Method1 () { std::cout << "Derived::Method1" << std::endl; } void Method2 () { std::cout << "Derived::Method2" << std::endl; } }; Base* basePtr = new Derived (); // Note - constructed as Derived, but pointer stored as Base* basePtr->Method1 (); // Prints "Base::Method1" basePtr->Method2 (); // Prints "Derived::Method2"
편집 - 이 질문을 참조하십시오.
또한 -이 튜토리얼 은 C++의 초기 및 후기 바인딩을 다룹니다.
Steve314이를 입증하려면 최소한 1레벨의 상속과 업캐스트가 필요합니다. 다음은 매우 간단한 예입니다.
class Animal { public: // turn the following virtual modifier on/off to see what happens //virtual std::string Says() { return "?"; } }; class Dog: public Animal { public: std::string Says() { return "Woof"; } }; void test() { Dog* d = new Dog(); Animal* a = d; // refer to Dog instance with Animal pointer std::cout << d->Says(); // always Woof std::cout << a->Says(); // Woof or ?, depends on virtual }
Henk Holterman가상 함수는 런타임 다형성 을 지원하는 데 사용됩니다.
즉, virtual 키워드는 컴파일러에게 컴파일 타임에 (함수 바인딩에 대한) 결정을 내리지 않고 런타임을 위해 연기하도록 지시합니다." .
기본 클래스 선언에서 virtual
키워드를 앞에 사용하여 함수를 가상으로 만들 수 있습니다. 예를 들어,
class Base { virtual void func(); }
기본 클래스가 가상 멤버 함수가있는 경우, 모든 클래스 기본 클래스에서 상속 정확히 같은 프로토 타입 즉, 단지 기능과 기능, 재정 기능이 아닌 인터페이스 할 수있다 다시 정의 할 수있다.
class Derive : public Base { void func(); }
기본 클래스 포인터는 기본 클래스 개체와 파생 클래스 개체를 가리키는 데 사용할 수 있습니다.
- 기본 클래스 포인터를 사용하여 가상 함수를 호출할 때 컴파일러는 런타임에 어떤 버전의 함수(예: 기본 클래스 버전 또는 재정의된 파생 클래스 버전)를 호출할지 결정합니다. 이것을 런타임 다형성 이라고 합니다.
user6359267안전한 다운캐스팅 , 단순성 및 간결성을 위해 가상 메서드가 필요합니다.
이것이 바로 가상 메서드가 하는 일입니다. 간단하고 간결한 코드로 안전하게 다운캐스팅하여 그렇지 않으면 가질 수 있는 더 복잡하고 장황한 코드에서 안전하지 않은 수동 캐스트를 방지합니다.
가상이 아닌 방법 ⇒ 정적 바인딩 =========================================
다음 코드는 의도적으로 "잘못된" 것입니다. value
메서드를 virtual
로 선언하지 않으므로 의도하지 않은 "잘못된" 결과, 즉 0이 생성됩니다.
#include <iostream> using namespace std; class Expression { public: auto value() const -> double { return 0.0; } // This should never be invoked, really. }; class Number : public Expression { private: double number_; public: auto value() const -> double { return number_; } // This is OK. Number( double const number ) : Expression() , number_( number ) {} }; class Sum : public Expression { private: Expression const* a_; Expression const* b_; public: auto value() const -> double { return a_->value() + b_->value(); } // Uhm, bad! Very bad! Sum( Expression const* const a, Expression const* const b ) : Expression() , a_( a ) , b_( b ) {} }; auto main() -> int { Number const a( 3.14 ); Number const b( 2.72 ); Number const c( 1.0 ); Sum const sum_ab( &a, &b ); Sum const sum( &sum_ab, &c ); cout << sum.value() << endl; }
"bad"로 주석 처리된 줄에서 Expression::value
메서드가 호출됩니다. 왜냐하면 정적으로 알려진 형식 (컴파일 시간에 알려진 형식)이 Expression
이고 value
메서드가 가상이 아니기 때문입니다.
가상 방법 ⇒ 동적 바인딩. =======================================
정적으로 알려진 유형 Expression
value
을 virtual
으로 선언하면 각 호출에서 이것이 실제 유형의 객체인지 확인하고 해당 동적 유형에 value
의 관련 구현을 호출합니다.
#include <iostream> using namespace std; class Expression { public: virtual auto value() const -> double = 0; }; class Number : public Expression { private: double number_; public: auto value() const -> double override { return number_; } Number( double const number ) : Expression() , number_( number ) {} }; class Sum : public Expression { private: Expression const* a_; Expression const* b_; public: auto value() const -> double override { return a_->value() + b_->value(); } // Dynamic binding, OK! Sum( Expression const* const a, Expression const* const b ) : Expression() , a_( a ) , b_( b ) {} }; auto main() -> int { Number const a( 3.14 ); Number const b( 2.72 ); Number const c( 1.0 ); Sum const sum_ab( &a, &b ); Sum const sum( &sum_ab, &c ); cout << sum.value() << endl; }
가상 메소드가 virtual 로 호출 되기 때문에 출력은 6.86
입니다. 이를 호출의 동적 바인딩 이라고도 합니다. 약간의 검사가 수행되어 실제 동적 유형의 객체를 찾고 해당 동적 유형에 대한 관련 메소드 구현이 호출됩니다.
관련 구현은 가장 구체적인(가장 파생된) 클래스에 있는 구현입니다.
여기에서 파생된 클래스의 메서드 구현은 virtual
표시되지 않고 대신 override
로 표시됩니다. virtual
으로 표시될 수 있지만 자동으로 가상입니다. override
키워드는 일부 기본 클래스에 그러한 가상 메서드가 없는 경우 오류가 발생하도록 합니다(바람직함).
가상 메서드 없이 이 작업을 수행하는 것의 추함 ======================================== ========
virtual
없으면 동적 바인딩의 Do It Yourself 버전을 구현해야 합니다. 이것은 일반적으로 안전하지 않은 수동 다운캐스팅, 복잡성 및 장황함을 포함합니다.
단일 함수의 경우 여기에서와 같이 객체에 함수 포인터를 저장하고 해당 함수 포인터를 통해 호출하는 것으로 충분합니다.
#include <iostream> using namespace std; class Expression { protected: typedef auto Value_func( Expression const* ) -> double; Value_func* value_func_; public: auto value() const -> double { return value_func_( this ); } Expression(): value_func_( nullptr ) {} // Like a pure virtual. }; class Number : public Expression { private: double number_; static auto specific_value_func( Expression const* expr ) -> double { return static_cast<Number const*>( expr )->number_; } public: Number( double const number ) : Expression() , number_( number ) { value_func_ = &Number::specific_value_func; } }; class Sum : public Expression { private: Expression const* a_; Expression const* b_; static auto specific_value_func( Expression const* expr ) -> double { auto const p_self = static_cast<Sum const*>( expr ); return p_self->a_->value() + p_self->b_->value(); } public: Sum( Expression const* const a, Expression const* const b ) : Expression() , a_( a ) , b_( b ) { value_func_ = &Sum::specific_value_func; } }; auto main() -> int { Number const a( 3.14 ); Number const b( 2.72 ); Number const c( 1.0 ); Sum const sum_ab( &a, &b ); Sum const sum( &sum_ab, &c ); cout << sum.value() << endl; }
이를 긍정적으로 보는 한 가지 긍정적인 방법은 위와 같이 안전하지 않은 다운캐스팅, 복잡성 및 자세한 내용이 발생하는 경우 가상 메서드 또는 메서드가 실제로 도움이 될 수 있다는 것입니다.
Cheers and hth. - Alf기본 클래스가 Base
이고 파생 클래스가 Der
Der
인스턴스를 가리키는 Base *p
포인터를 가질 수 있습니다. p->foo();
, foo
가 가상 이 아니면 p
실제로 Der
가리킨다는 사실을 무시하고 Base
의 버전이 실행됩니다. foo 가 가상이면 p->foo()
는 가리킨 항목의 실제 클래스를 완전히 고려하여 foo
의 "최하위" 재정의를 실행합니다. 따라서 가상과 비가상의 차이는 실제로 매우 중요합니다. 전자는 OO 프로그래밍의 핵심 개념인 런타임 다형성을 허용하지만 후자는 허용하지 않습니다.
Alex Martelli위에서 언급한 답변과 동일한 개념을 사용하지만 가상 기능의 또 다른 용도를 추가하고 싶지만 언급할 가치가 있다고 생각합니다.
가상 소멸자
기본 클래스 소멸자를 가상으로 선언하지 않고 아래의 이 프로그램을 고려하십시오. Cat에 대한 메모리가 정리되지 않을 수 있습니다.
class Animal { public: ~Animal() { cout << "Deleting an Animal" << endl; } }; class Cat:public Animal { public: ~Cat() { cout << "Deleting an Animal name Cat" << endl; } }; int main() { Animal *a = new Cat(); delete a; return 0; }
산출:
Deleting an Animal
class Animal { public: virtual ~Animal() { cout << "Deleting an Animal" << endl; } }; class Cat:public Animal { public: ~Cat(){ cout << "Deleting an Animal name Cat" << endl; } }; int main() { Animal *a = new Cat(); delete a; return 0; }
산출:
Deleting an Animal name Cat Deleting an Animal
Aryaman Gupta가상 기능의 필요성 설명 [이해하기 쉬움]
#include<iostream> using namespace std; class A{ public: void show(){ cout << " Hello from Class A"; } }; class B :public A{ public: void show(){ cout << " Hello from Class B"; } }; int main(){ A *a1 = new B; // Create a base class pointer and assign address of derived object. a1->show(); }
출력은 다음과 같습니다.
Hello from Class A.
그러나 가상 기능:
#include<iostream> using namespace std; class A{ public: virtual void show(){ cout << " Hello from Class A"; } }; class B :public A{ public: virtual void show(){ cout << " Hello from Class B"; } }; int main(){ A *a1 = new B; a1->show(); }
출력은 다음과 같습니다.
Hello from Class B.
따라서 가상 기능을 사용하면 런타임 다형성을 얻을 수 있습니다.
Ajay GU오버라이드와 오버로딩을 구별해야 합니다. virtual
키워드가 없으면 기본 클래스의 메서드만 오버로드합니다. 이것은 숨기는 것 외에는 아무 의미가 없습니다. 하자 당신은 기본 클래스가 있다고 가정 해 Base
및 파생 클래스 Specialized
구현 모두 void foo()
. Specialized
의 인스턴스를 가리키는 Base
대한 포인터가 있습니다. foo()
를 호출하면 virtual
만드는 차이점을 관찰할 수 있습니다. 메서드가 가상이면 Specialized
구현이 사용되며 누락된 경우 Base
버전이 선택됩니다. 기본 클래스에서 메서드를 오버로드하지 않는 것이 가장 좋습니다. 메소드를 비가상화하는 것은 작성자가 서브클래스에서 확장을 의도하지 않았다고 말하는 방법입니다.
h0b0더 나은 읽기를 위해 대화 형식으로 답변했습니다.
가상 기능이 필요한 이유는 무엇입니까?
다형성 때문입니다.
다형성이란 무엇입니까?
기본 포인터가 파생 형식 개체도 가리킬 수 있다는 사실입니다.
다형성에 대한 이러한 정의는 어떻게 가상 기능에 대한 필요성으로 이어집니까?
글쎄, 초기 바인딩을 통해 .
조기 바인딩이란 무엇입니까?
C++에서 초기 바인딩(컴파일 타임 바인딩)은 프로그램이 실행되기 전에 함수 호출이 고정되어 있음을 의미합니다.
그래서...?
따라서 기본 유형을 함수의 매개변수로 사용하는 경우 컴파일러는 기본 인터페이스만 인식하고 파생 클래스의 인수를 사용하여 해당 함수를 호출하면 슬라이스가 발생합니다. 이는 원하는 일이 아닙니다.
우리가 원하는 것이 아니라면 왜 이것이 허용됩니까?
다형성이 필요하기 때문입니다!
그렇다면 다형성의 이점은 무엇입니까?
기본 유형 포인터를 단일 함수의 매개변수로 사용할 수 있으며 프로그램의 런타임에서 해당 단일 함수의 역참조를 사용하여 문제 없이 각 파생 유형 인터페이스(예: 해당 멤버 함수)에 액세스할 수 있습니다. 기본 포인터.
가상 기능이 어떤게 좋은지는 아직 모르겠어...! 그리고 이것은 나의 첫 번째 질문이었습니다!
글쎄, 이것은 당신이 당신의 질문을 너무 빨리 했기 때문입니다!
가상 기능이 필요한 이유는 무엇입니까?
파생 클래스 중 하나의 개체 주소가 있는 기본 포인터를 사용하여 함수를 호출했다고 가정합니다. 위에서 이야기한 것처럼 런타임에서 이 포인터는 역참조됩니다. 그러나 지금까지는 "파생 클래스에서" 메서드(== 멤버 함수)가 실행될 것으로 예상합니다! 그러나 기본 클래스에 동일한 메소드(헤더가 동일한 메소드)가 이미 정의되어 있는데 왜 프로그램에서 다른 메소드를 선택해야 합니까? 다시 말해서, 이전에 일반적으로 발생하는 것으로 보았던 시나리오와 어떻게 이 시나리오를 구분할 수 있습니까?
간단한 대답은 "기본의 가상 멤버 함수"이고 조금 더 긴 대답은 "이 단계에서 프로그램이 기본 클래스의 가상 함수를 보면 사용자가 사용하려는 것을 알고(인식) 다형성"을 사용하여 파생 클래스(후기 바인딩의 한 형태인 v-table 사용)로 이동하여 헤더는 동일하지만 구현 이 다른 다른 메서드를 찾습니다.
왜 다른 구현인가?
너클 헤드! 좋은 책 읽으러 가자!
좋아요, 기다려요, 잠깐만요, 왜 기본 포인터를 사용하는 것을 귀찮게 할까요? 단순히 파생형 포인터를 사용할 수 있는데도 말이죠? 당신이 판사가 되십시오. 이 모든 두통이 가치가 있습니까? 다음 두 스니펫을 보십시오.
//1:
Parent* p1 = &boy; p1 -> task(); Parent* p2 = &girl; p2 -> task();
//2:
Boy* p1 = &boy; p1 -> task(); Girl* p2 = &girl; p2 -> task();
좋아, 1 이 여전히 2 보다 낫다고 생각하지만 다음과 같이 1을 작성할 수 있습니다.
//1:
Parent* p1 = &boy; p1 -> task(); p1 = &girl; p1 -> task();
게다가 이것은 내가 지금까지 설명했던 모든 것을 인위적으로 사용한 것에 불과하다는 사실을 알아야 합니다. 이 대신, 예를 들어 프로그램에 파생된 각 클래스의 메서드를 각각 사용하는 함수가 있다고 가정합니다(getMonthBenefit()):
double totalMonthBenefit = 0; std::vector<CentralShop*> mainShop = { &shop1, &shop2, &shop3, &shop4, &shop5, &shop6}; for(CentralShop* x : mainShop){ totalMonthBenefit += x -> getMonthBenefit(); }
이제 두통 없이 이것을 다시 작성해 보십시오!
double totalMonthBenefit=0; Shop1* branch1 = &shop1; Shop2* branch2 = &shop2; Shop3* branch3 = &shop3; Shop4* branch4 = &shop4; Shop5* branch5 = &shop5; Shop6* branch6 = &shop6; totalMonthBenefit += branch1 -> getMonthBenefit(); totalMonthBenefit += branch2 -> getMonthBenefit(); totalMonthBenefit += branch3 -> getMonthBenefit(); totalMonthBenefit += branch4 -> getMonthBenefit(); totalMonthBenefit += branch5 -> getMonthBenefit(); totalMonthBenefit += branch6 -> getMonthBenefit();
그리고 실제로 이것은 아직 고안된 예일 수도 있습니다!
M-JC++에서 가상 메서드가 필요한 이유는 무엇입니까?
빠른 답변:
- 객체 지향 프로그래밍에 필요한 "성분"1 중 하나를 제공합니다.
Bjarne Stroustrup C++ 프로그래밍: 원칙 및 실습, (14.3):
가상 함수는 기본 클래스에서 함수를 정의하고 사용자가 기본 클래스 함수를 호출할 때 호출되는 파생 클래스에서 동일한 이름 및 유형의 함수를 가질 수 있는 기능을 제공합니다. 호출된 함수는 사용된 개체의 유형에 따라 런타임에 결정되기 때문에 이를 종종 런타임 다형성 , 동적 디스패치 또는 런타임 디스패치라고 합니다.
- 가상 함수 호출 2 가 필요한 경우 가장 빠르고 효율적인 구현입니다.
가상 호출을 처리하려면 파생 객체 3 와 관련된 하나 이상의 데이터가 필요합니다. 일반적으로 수행되는 방법은 함수 테이블의 주소를 추가하는 것입니다. 이 테이블은 일반적으로 가상 테이블 또는 가상 기능 테이블이라고 하며 그 주소를 가상 포인터 라고 합니다. 각 가상 기능은 가상 테이블에서 슬롯을 얻습니다. 호출자의 개체(파생) 유형에 따라 가상 함수는 차례로 해당 재정의를 호출합니다.
1. 상속, 런타임 다형성 및 캡슐화의 사용은 객체 지향 프로그래밍 의 가장 일반적인 정의입니다.
2. 런타임에 대안 중에서 선택하기 위해 다른 언어 기능을 사용하여 기능을 더 빠르게 또는 더 적은 메모리를 사용하도록 코딩할 수 없습니다. Bjarne Stroustrup C++ 프로그래밍: 원칙 및 실습.(14.3.1) .
3. 가상 함수를 포함하는 기본 클래스를 호출할 때 실제로 호출되는 함수를 알려주는 것.
Ziezi기본 클래스에 함수가 있는 경우 파생 클래스에서 이를 Redefine
하거나 Override
방법 재정의
Community Wiki기본 메커니즘을 알고 있으면 도움이 됩니다. C++는 C 프로그래머가 사용하는 일부 코딩 기술을 공식화합니다. "클래스"는 "오버레이"를 사용하여 대체되었습니다. 공통 헤더 섹션이 있는 구조체는 다른 유형의 객체를 처리하는 데 사용되지만 일부 공통 데이터 또는 작업이 사용됩니다. 일반적으로 오버레이의 기본 구조(공통 부분)에는 각 개체 유형에 대한 다른 루틴 집합을 가리키는 함수 테이블에 대한 포인터가 있습니다. C++는 동일한 작업을 수행하지만 C++ ptr->func(...)
메커니즘을 숨깁니다. 여기서 func는 C처럼 가상입니다. (*ptr->func_table[func_num])(ptr,...)
파생 클래스 사이는 func_table 내용입니다. [가상이 아닌 메소드 ptr->func()는 mangled_func(ptr,..)로 변환됩니다.]
그 결과 파생 클래스의 메서드를 호출하려면 기본 클래스만 이해하면 됩니다. 즉, 루틴이 클래스 A를 이해하는 경우 파생 클래스 B 포인터를 전달할 수 있으면 호출된 가상 메서드는 다음과 같습니다. 함수 테이블 B가 가리키는 것을 통과하기 때문에 A가 아닌 B입니다.
Kevvirtual 키워드는 컴파일러에게 초기 바인딩을 수행하지 않아야 함을 알려줍니다. 대신 후기 바인딩을 수행하는 데 필요한 모든 메커니즘을 자동으로 설치해야 합니다. 이를 수행하기 위해 일반적인 컴파일러1는 가상 함수를 포함하는 각 클래스에 대해 단일 테이블(VTABLE이라고 함)을 만듭니다. 컴파일러는 해당 특정 클래스에 대한 가상 함수의 주소를 VTABLE에 배치합니다. 가상 기능이 있는 각 클래스에는 해당 개체에 대한 VTABLE을 가리키는 vpointer(약칭 VPTR)라는 포인터가 비밀리에 배치됩니다. 기본 클래스 포인터를 통해 가상 함수 호출을 수행할 때 컴파일러는 VPTR을 가져오고 VTABLE에서 함수 주소를 조회하는 코드를 조용히 삽입하여 올바른 함수를 호출하고 늦은 바인딩이 발생하도록 합니다.
이 링크에 대한 자세한 내용은 http://cplusplusinterviews.blogspot.sg/2015/04/virtual-mechanism.html
rvkreddyvirtual 키워드는 컴파일러가 포인터의 클래스가 아닌 개체의 클래스에 정의된 메서드 구현을 선택하도록 합니다.
Shape *shape = new Triangle(); cout << shape->getName();
위의 예에서 기본 클래스 Shape에서 getName()이 가상으로 정의되지 않는 한 Shape::getName이 기본적으로 호출됩니다. 이렇게 하면 컴파일러가 Shape 클래스가 아닌 Triangle 클래스에서 getName() 구현을 찾도록 합니다.
가상 테이블 은 컴파일러가 하위 클래스의 다양한 가상 메서드 구현을 추적하는 메커니즘입니다. 이것은 또한 동적 파견라고하며, 그와 연관된 오버 헤드가있다.
마지막으로 C++에서도 가상이 필요한 이유는 무엇입니까? Java에서와 같이 기본 동작으로 만들지 않겠습니까?
- C++는 "제로 오버헤드" 및 "사용한 만큼 지불"의 원칙을 기반으로 합니다. 따라서 필요한 경우가 아니면 동적 디스패치를 수행하지 않습니다.
- 인터페이스에 더 많은 제어를 제공합니다. 함수를 비가상화함으로써 인터페이스/추상 클래스는 모든 구현에서 동작을 제어할 수 있습니다.
javaProgrammerOOP 답변: 하위 유형 다형성
C++에서는 다형성 을 구현하기 위해 가상 메소드가 필요하며 , 위키피디아 의 정의를 적용하면 더 정확하게는 서브타이핑 또는 서브타입 다형성이 필요합니다.
Wikipedia, Subtyping, 2019-01-09: 프로그래밍 언어 이론에서 하위 유형(또한 하위 유형 다형성 또는 포함 다형성)은 하위 유형이 일부 개념에 의해 다른 데이터 유형(상위 유형)과 관련된 데이터 유형인 유형 다형성의 한 형태입니다. 상위 유형의 요소에 대해 작동하도록 작성된 프로그램 요소(일반적으로 서브루틴 또는 함수)가 하위 유형의 요소에 대해서도 작동할 수 있음을 의미합니다.
참고: 하위 유형은 기본 클래스를 의미하고 하위 유형은 상속된 클래스를 의미합니다.
하위 유형 다형성 에 대한 추가 읽기
기술 답변: 동적 디스패치
기본 클래스에 대한 포인터가 있는 경우 가상으로 선언된 메서드 호출이 생성된 개체의 실제 클래스 메서드로 전달됩니다. 이것이 C++에서 Subtype Polymorphism 이 실현되는 방식입니다.
C++ 및 Dynamic Dispatch의 다형성 추가 읽기
구현 답변: vtable 항목 생성
메서드의 각 수정자 "가상"에 대해 C++ 컴파일러는 일반적으로 메서드가 선언된 클래스의 vtable에 항목을 만듭니다. 이것이 일반적인 C++ 컴파일러가 Dynamic Dispatch를 실현하는 방법입니다.
추가 읽기 vtables
예제 코드
#include <iostream> using namespace std; class Animal { public: virtual void MakeTypicalNoise() = 0; // no implementation needed, for abstract classes virtual ~Animal(){}; }; class Cat : public Animal { public: virtual void MakeTypicalNoise() { cout << "Meow!" << endl; } }; class Dog : public Animal { public: virtual void MakeTypicalNoise() { // needs to be virtual, if subtype polymorphism is also needed for Dogs cout << "Woof!" << endl; } }; class Doberman : public Dog { public: virtual void MakeTypicalNoise() { cout << "Woo, woo, woow!"; cout << " ... "; Dog::MakeTypicalNoise(); } }; int main() { Animal* apObject[] = { new Cat(), new Dog(), new Doberman() }; const int cnAnimals = sizeof(apObject)/sizeof(Animal*); for ( int i = 0; i < cnAnimals; i++ ) { apObject[i]->MakeTypicalNoise(); } for ( int i = 0; i < cnAnimals; i++ ) { delete apObject[i]; } return 0; }
예제 코드의 출력
Meow! Woof! Woo, woo, woow! ... Woof!
코드 예제의 UML 클래스 다이어그램
Jörg Brüggmann가상 기능이 필요한 이유는 무엇입니까?
가상 함수는 불필요한 typecasting 문제를 피하고 우리 중 일부는 파생 클래스 포인터를 사용하여 파생 클래스의 특정 함수를 호출할 수 있는데 왜 가상 함수가 필요한지에 대해 논쟁할 수 있습니다. 대답은 - 대규모 시스템에서 상속에 대한 전체 아이디어를 무효화한다는 것입니다. 단일 포인터 기본 클래스 객체를 갖는 것이 훨씬 바람직한 개발.
가상 기능의 중요성을 이해하기 위해 아래 두 개의 간단한 프로그램을 비교해 보겠습니다.
가상 기능이 없는 프로그램:
#include <iostream> using namespace std; class father { public: void get_age() {cout << "Fathers age is 50 years" << endl;} }; class son: public father { public : void get_age() { cout << "son`s age is 26 years" << endl;} }; int main(){ father *p_father = new father; son *p_son = new son; p_father->get_age(); p_father = p_son; p_father->get_age(); p_son->get_age(); return 0; }
산출:
Fathers age is 50 years Fathers age is 50 years son`s age is 26 years
가상 기능이 있는 프로그램:
#include <iostream> using namespace std; class father { public: virtual void get_age() {cout << "Fathers age is 50 years" << endl;} }; class son: public father { public : void get_age() { cout << "son`s age is 26 years" << endl;} }; int main(){ father *p_father = new father; son *p_son = new son; p_father->get_age(); p_father = p_son; p_father->get_age(); p_son->get_age(); return 0; }
산출:
Fathers age is 50 years son`s age is 26 years son`s age is 26 years
두 출력을 면밀히 분석함으로써 가상 기능의 중요성을 이해할 수 있습니다.
akshaypmurgod가상 기능에 대한 설명의 문제점은 가상 기능이 실제로 어떻게 사용되는지, 유지 관리에 어떻게 도움이 되는지 설명하지 않는다는 것입니다. 나는 사람들이 이미 매우 유용하다고 생각하는 가상 기능 튜토리얼을 만들었습니다. 또한 전장 전제를 기반으로 하므로 좀 더 흥미진진합니다. https://nrecursions.blogspot.com/2015/06/so-why-do-we-need-virtual-functions.html .
다음과 같은 전장 애플리케이션을 고려하십시오.
#include "iostream" //This class is created by Gun1's company class Gun1 {public: void fire() {std::cout<<"gun1 firing now\n";}}; //This class is created by Gun2's company class Gun2 {public: void shoot() {std::cout<<"gun2 shooting now\n";}}; //We create an abstract class to interface with WeaponController class WeaponsInterface { public: virtual void shootTarget() = 0; }; //A wrapper class to encapsulate Gun1's shooting function class WeaponGun1 : public WeaponsInterface { private: Gun1* g; public: WeaponGun1(): g(new Gun1()) {} ~WeaponGun1() { delete g;} virtual void shootTarget() { g->fire(); } }; //A wrapper class to encapsulate Gun2's shooting function class WeaponGun2 : public WeaponsInterface { private: Gun2* g; public: WeaponGun2(): g(new Gun2()) {} ~WeaponGun2() { delete g;} virtual void shootTarget() { g->shoot(); } }; class WeaponController { private: WeaponsInterface* w; WeaponGun1* g1; WeaponGun2* g2; public: WeaponController() {g1 = new WeaponGun1(); g2 = new WeaponGun2(); w = g1;} ~WeaponController() {delete g1; delete g2;} void shootTarget() { w->shootTarget();} void changeGunTo(int gunNumber) {//Virtual functions makes it easy to change guns dynamically switch(gunNumber) { case 1: w = g1; break; case 2: w = g2; break; } } }; class BattlefieldSoftware { private: WeaponController* wc; public: BattlefieldSoftware() : wc(new WeaponController()) {} ~BattlefieldSoftware() { delete wc; } void shootTarget() { wc->shootTarget(); } void changeGunTo(int gunNumber) {wc->changeGunTo(gunNumber); } }; int main() { BattlefieldSoftware* bf = new BattlefieldSoftware(); bf->shootTarget(); for(int i = 2; i > 0; i--) { bf->changeGunTo(i); bf->shootTarget(); } delete bf; }
래퍼 클래스가 생성된 이유에 대한 요지를 알아보려면 먼저 블로그의 게시물을 읽는 것이 좋습니다.
이미지에서 볼 수 있듯이 전장 소프트웨어에 연결할 수 있는 다양한 총기/미사일이 있으며 해당 무기에 명령을 내려 발사 또는 재보정 등을 할 수 있습니다. 여기서 문제는 변경/교체할 수 있다는 것입니다. 파란색 전장 소프트웨어를 변경할 필요 없이 총기/미사일을 발사하고 코드를 변경하고 다시 컴파일할 필요 없이 런타임 중에 무기 간에 전환할 수 있습니다.
위의 코드는 문제가 해결되는 방법과 잘 설계된 래퍼 클래스가 있는 가상 함수가 함수를 캡슐화하고 런타임 중에 파생 클래스 포인터를 할당하는 데 도움이 되는 방법을 보여줍니다. WeaponGun1
클래스를 생성 Gun1
처리를 클래스로 완전히 분리할 수 있습니다. 당신이 무엇 이건 변경 Gun1
만 변경하게해야합니다 WeaponGun1
, 그리고 다른 클래스가 영향을받지 있다는 자신감을 가지고.
WeaponsInterface
클래스로 인해 이제 파생 클래스를 기본 클래스 포인터 WeaponsInterface
할당할 수 있으며 해당 함수는 가상이므로 WeaponsInterface
의 shootTarget
을 호출하면 파생 클래스인 shootTarget
이 호출됩니다.
가장 좋은 점은 런타임 중에 총을 변경할 수 있다는 것입니다( w=g1
및 w=g2
). 이것이 가상 기능의 주요 장점이며 이것이 가상 기능이 필요한 이유입니다.
따라서 총을 교체할 때 더 이상 여러 곳에서 코드를 주석 처리할 필요가 없습니다. WeaponGun3
또는 WeaponGun4
BattlefieldSoftware
의 코드나 WeaponGun1
/ WeaponGun2
의 코드를 엉망으로 만들지 않을 것이라고 확신할 수 있기 때문에 더 많은 총 클래스를 추가하는 것도 더 쉽습니다. 암호.
Nav효율성에 대해 가상 기능 은 초기 바인딩 기능보다 약간 덜 효율적입니다.
"이 가상 호출 메커니즘은 "일반 함수 호출" 메커니즘만큼 효율적으로 만들 수 있습니다(25% 이내). 공간 오버헤드는 가상 함수가 있는 클래스의 각 개체에 하나의 포인터와 각 클래스에 대해 하나의 vtbl을 더한 것입니다." [ A Bjarne Stroustrup 의 C++ 둘러보기]
Duke가상 메서드는 인터페이스 디자인에 사용됩니다. 예를 들어 Windows에는 아래와 같은 IUnknown이라는 인터페이스가 있습니다.
interface IUnknown { virtual HRESULT QueryInterface (REFIID riid, void **ppvObject) = 0; virtual ULONG AddRef () = 0; virtual ULONG Release () = 0; };
이러한 메소드는 인터페이스 사용자가 구현하도록 남겨둡니다. IUnknown을 상속해야 하는 특정 개체의 생성 및 파괴에 필수적입니다. 이 경우 런타임은 세 가지 메서드를 인식하고 호출할 때 구현될 것으로 예상합니다. 따라서 어떤 의미에서 그것들은 객체 자체와 그 객체를 사용하는 모든 것 사이의 계약 역할을 합니다.
user2074102다음은 가상 방법이 사용되는 이유를 보여주는 완전한 예입니다.
#include <iostream> using namespace std; class Basic { public: virtual void Test1() { cout << "Test1 from Basic." << endl; } virtual ~Basic(){}; }; class VariantA : public Basic { public: void Test1() { cout << "Test1 from VariantA." << endl; } }; class VariantB : public Basic { public: void Test1() { cout << "Test1 from VariantB." << endl; } }; int main() { Basic *object; VariantA *vobjectA = new VariantA(); VariantB *vobjectB = new VariantB(); object=(Basic *) vobjectA; object->Test1(); object=(Basic *) vobjectB; object->Test1(); delete vobjectA; delete vobjectB; return 0; }
user3371350메서드가 가상으로 선언되면 재정의에서 '가상' 키워드를 사용할 필요가 없다는 사실을 언급하고 있다고 생각합니다.
class Base { virtual void foo(); }; class Derived : Base { void foo(); // this is overriding Base::foo };
Base의 foo 선언에서 '가상'을 사용하지 않으면 Derived의 foo가 그림자를 드리울 것입니다.
edwinc다음은 처음 두 답변에 대한 C++ 코드의 병합된 버전입니다.
#include <iostream> #include <string> using namespace std; class Animal { public: #ifdef VIRTUAL virtual string says() { return "??"; } #else string says() { return "??"; } #endif }; class Dog: public Animal { public: string says() { return "woof"; } }; string func(Animal *a) { return a->says(); } int main() { Animal *a = new Animal(); Dog *d = new Dog(); Animal *ad = d; cout << "Animal a says\t\t" << a->says() << endl; cout << "Dog d says\t\t" << d->says() << endl; cout << "Animal dog ad says\t" << ad->says() << endl; cout << "func(a) :\t\t" << func(a) << endl; cout << "func(d) :\t\t" << func(d) << endl; cout << "func(ad):\t\t" << func(ad)<< endl; }
두 가지 다른 결과는 다음과 같습니다.
#define virtual 이 없으면 컴파일 타임에 바인딩됩니다. Animal *ad와 func(Animal *)은 모두 Animal의 say() 메소드를 가리킵니다.
$ g++ virtual.cpp -o virtual $ ./virtual Animal a says ?? Dog d says woof Animal dog ad says ?? func(a) : ?? func(d) : ?? func(ad): ??
#define virtual 을 사용하면 런타임에 바인딩됩니다. Dog *d, Animal *ad 및 func(Animal *)은 Dog의 개체 유형이므로 Dog의 say() 메서드를 가리킵니다. [Dog's say() "woof"] 메서드가 정의되지 않은 경우 클래스 트리에서 가장 먼저 검색되는 메서드가 됩니다. 즉, 파생 클래스가 기본 클래스의 메서드를 재정의할 수 있습니다[Animal's say()].
$ g++ virtual.cpp -D VIRTUAL -o virtual $ ./virtual Animal a says ?? Dog d says woof Animal dog ad says woof func(a) : ?? func(d) : woof func(ad): woof
Python의 모든 클래스 속성(데이터 및 메서드) 이 사실상 virtual 이라는 점은 흥미롭습니다. 모든 객체는 런타임에 동적으로 생성되기 때문에 유형 선언이나 키워드 virtual이 필요하지 않습니다. 다음은 Python의 코드 버전입니다.
class Animal: def says(self): return "??" class Dog(Animal): def says(self): return "woof" def func(a): return a.says() if __name__ == "__main__": a = Animal() d = Dog() ad = d # dynamic typing by assignment print("Animal a says\t\t{}".format(a.says())) print("Dog d says\t\t{}".format(d.says())) print("Animal dog ad says\t{}".format(ad.says())) print("func(a) :\t\t{}".format(func(a))) print("func(d) :\t\t{}".format(func(d))) print("func(ad):\t\t{}".format(func(ad)))
출력은 다음과 같습니다.
Animal a says ?? Dog d says woof Animal dog ad says woof func(a) : ?? func(d) : woof func(ad): woof
이는 C++의 가상 정의와 동일합니다. d 와 ad 는 동일한 Dog 인스턴스를 참조하거나 가리키는 두 개의 서로 다른 포인터 변수입니다. 표현식(ad is d)은 True를 반환하고 그 값은 < main .Dog object at 0xb79f72cc>와 같습니다.
Leon Chang함수 포인터에 대해 잘 알고 있습니까? 가상 함수는 데이터를 가상 함수(클래스 구성원으로)에 쉽게 바인딩할 수 있다는 점을 제외하면 유사한 개념입니다. 데이터를 함수 포인터에 바인딩하는 것은 쉽지 않습니다. 나에게 이것은 주요 개념적 차이입니다. 여기에 있는 다른 많은 답변은 "왜냐하면... 다형성!"이라고 말하고 있습니다.
user2445507"런타임 다형성"을 지원하기 위한 가상 메서드가 필요합니다. 포인터 또는 기본 클래스에 대한 참조를 사용하여 파생 클래스 개체를 참조하는 경우 해당 개체에 대한 가상 함수를 호출하고 파생 클래스의 함수 버전을 실행할 수 있습니다.
rashedcs결론은 가상 기능이 삶을 더 쉽게 만든다는 것입니다. M Perry의 아이디어 중 일부를 사용하여 가상 함수가 없고 대신 멤버 함수 포인터만 사용할 수 있는 경우 어떤 일이 발생하는지 설명하겠습니다. 가상 함수가 없는 일반 추정에서 다음이 있습니다.
class base { public: void helloWorld() { std::cout << "Hello World!"; } }; class derived: public base { public: void helloWorld() { std::cout << "Greetings World!"; } }; int main () { base hwOne; derived hwTwo = new derived(); base->helloWorld(); //prints "Hello World!" derived->helloWorld(); //prints "Hello World!"
좋아, 그게 우리가 아는 것입니다. 이제 멤버 함수 포인터를 사용해 보겠습니다.
#include <iostream> using namespace std; class base { public: void helloWorld() { std::cout << "Hello World!"; } }; class derived : public base { public: void displayHWDerived(void(derived::*hwbase)()) { (this->*hwbase)(); } void(derived::*hwBase)(); void helloWorld() { std::cout << "Greetings World!"; } }; int main() { base* b = new base(); //Create base object b->helloWorld(); // Hello World! void(derived::*hwBase)() = &derived::helloWorld; //create derived member function pointer to base function derived* d = new derived(); //Create derived object. d->displayHWDerived(hwBase); //Greetings World! char ch; cin >> ch; }
멤버 함수 포인터로 몇 가지 작업을 수행할 수 있지만 가상 함수만큼 유연하지는 않습니다. 클래스에서 멤버 함수 포인터를 사용하는 것은 까다롭습니다. 멤버 함수 포인터는 거의 적어도 내 실습에서는 항상 위의 예에서와 같이 주 함수 또는 멤버 함수 내에서 호출되어야 합니다.
반면에 가상 함수는 약간의 함수 포인터 오버헤드가 있을 수 있지만 작업을 극적으로 단순화합니다.
편집: eddietree와 유사한 또 다른 방법이 있습니다: C++ 가상 함수 대 멤버 함수 포인터(성능 비교) .
fishermanhat출처 : http:www.stackoverflow.com/questions/2391679/why-do-we-need-virtual-functions-in-c