C ++에서 가상 함수가 필요한 이유는 무엇입니까?


1312

C ++을 배우고 있으며 가상 기능을 사용하고 있습니다.

내가 읽은 것 (책과 온라인에서)에서 가상 함수는 파생 클래스에서 재정의 할 수있는 기본 클래스의 함수입니다.

그러나이 책의 앞부분에서 기본 상속에 대해 배울 때을 사용하지 않고 파생 클래스의 기본 함수를 재정의 할 수있었습니다 virtual.

그래서 여기서 무엇을 놓치고 있습니까? 나는 가상 기능에 더 많은 것이 있다는 것을 알고 있으며 그것이 중요하다는 것을 분명히하고 싶습니다. 온라인에서 정답을 찾을 수 없습니다.



4
이것은 아마도 가상 함수의 가장 큰 장점입니다. 새로 파생 된 클래스가 수정없이 기존 코드와 자동으로 작동하는 방식으로 코드를 구성하는 기능입니다!
user3530616

가상 기능은 유형 삭제를 위해 OOP의 주요 기능입니다. 필자는 비가 상 메소드가 Object Pascal 및 C ++를 특별하게 만드는 이유이며 불필요한 큰 vtable을 최적화하고 POD 호환 클래스를 허용한다고 생각합니다. 많은 OOP 언어는 모든 방법을 재정의 할 수있을 것으로 기대합니다 .
스위프트-금요일 파이

좋은 질문입니다. 실제로 C ++에서이 가상의 것은 Java 나 PHP와 같은 다른 언어로 추상화됩니다. C ++에서는 드문 경우에 대해 조금 더 많은 제어권을 얻습니다 (여러 상속 또는 DDOD의 특수한 경우에 유의하십시오 ). 그러나 왜이 질문이 stackoverflow.com에 게시됩니까?
Edgar Alloro

초기 바인딩 후기 바인딩 및 VTABLE을 살펴보면 더 합리적이고 의미가 있다고 생각합니다. 따라서 여기에 좋은 설명이 있습니다 ( learncpp.com/cpp-tutorial/125-the-virtual-table ).
ceyun

답변:


2728

다음은 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*? 만약 당신이 동물로부터 더 많은 동물을 끌어 내야한다면 그들 모두는 그들 자신의 것이 필요합니다 func().

해결책은 클래스에서 가상 함수 를 만드는 것 eat()입니다 Animal.

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."

끝난.


165
그래서 이것을 올바르게 이해하고 있다면 virtual은 객체가 수퍼 클래스로 취급 되더라도 서브 클래스 메소드를 호출 할 수 있습니까?
Kenny Worden

147
중간 함수 "func"의 예를 통해 후기 바인딩을 설명하는 대신보다 간단한 데모가 있습니다. Animal * animal = new Animal; // 고양이 * 고양이 = 새 고양이; 동물 * 고양이 = 새 고양이; 동물-> 먹기 (); // 출력 : "일반 음식을 먹고 있습니다." 고양이-> 먹기 (); // 출력 : "일반 음식을 먹고 있습니다." 서브 클래 싱 된 객체 (Cat)를 할당하더라도 호출되는 메서드는 가리키는 객체 유형이 아닌 포인터 유형 (동물)을 기반으로합니다. 이것이 "가상"이 필요한 이유입니다.
rexbelia

37
C ++ 에서이 기본 동작을 찾는 유일한 사람입니까? "가상"이없는 코드가 작동 할 것으로 예상했을 것입니다.
David 天宇 Wong

20
@David 天宇 Wong virtual은 정적 바인딩 대 정적 바인딩을 소개 한다고 생각합니다. 예를 들어 Java와 같은 언어에서 온다면 이상합니다.
peterchaula

32
우선, 가상 호출은 일반 함수 호출보다 훨씬 비쌉니다. C ++ 철학은 기본적으로 빠르기 때문에 기본적으로 가상 호출은 큰 문제가되지 않습니다. 두 번째 이유는 라이브러리에서 클래스를 상속하고 기본 클래스 동작을 변경하지 않고 공개 또는 개인 메소드 (내부 가상 메소드를 호출 함)의 내부 구현을 변경하면 가상 호출로 인해 코드가 중단 될 수 있기 때문입니다.
saolof

672

"가상"이 없으면 "초기 바인딩"이됩니다. 사용되는 메소드의 구현은 호출하는 포인터의 유형에 따라 컴파일 타임에 결정됩니다.

"가상"을 사용하면 "늦은 바인딩"이 나타납니다. 사용되는 메소드 구현은 런타임시 지정된 오브젝트의 유형 (원래 구성한 내용)에 따라 결정됩니다. 이것은 반드시 해당 객체를 가리키는 포인터의 유형에 따라 생각할 필요는 없습니다.

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* obj = new Derived ();
  //  Note - constructed as Derived, but pointer stored as Base*

obj->Method1 ();  //  Prints "Base::Method1"
obj->Method2 ();  //  Prints "Derived::Method2"

편집 - 이 질문을 참조하십시오 .

또한 이 튜토리얼 은 C ++에서의 초기 바인딩과 늦은 바인딩에 대해 다룹니다.


11
우수하고 더 나은 예제를 사용하여 신속하게 집으로 돌아옵니다. 그러나 이것은 간단하며 질문자는 실제로 parashift.com/c++-faq-lite/virtual-functions.html 페이지를 읽어야 합니다. 다른 사람들은 이미이 스레드와 연결된 SO 기사 에서이 리소스를 지적했지만 이것이 다시 언급 할 가치가 있다고 생각합니다.
Sonny

36
나도 몰라 초기후기 바인딩 specificly는 C ++ 커뮤니티에서 사용하는 용어가 있지만 정확한 용어는 정적 및 (컴파일시에) 동적 바인딩 (실행시).
mike

31
@mike- ""후기 바인딩 "이라는 용어는 적어도 1960 년대로 거슬러 올라갑니다. 여기서 ACM의 커뮤니케이션에서 찾을 수 있습니다." . 각 개념에 대해 올바른 단어가 하나 있으면 좋지 않습니까? 불행히도, 그렇지 않습니다. "초기 바인딩"및 "late binding"이라는 용어는 C ++ 및 심지어 객체 지향 프로그래밍보다 앞서 있으며 사용하는 용어만큼 정확합니다.
Steve314

4
@BJovke-이 답변은 C ++ 11이 게시되기 전에 작성되었습니다. 그럼에도 불구하고 방금 GCC 6.3.0 (기본적으로 C ++ 14 사용)으로 문제없이 컴파일했습니다. 변수 선언을 래핑하고 main함수 등을 호출하는 것입니다. 포인터에서 파생 된 포인터는 암시 적 으로 포인터를 기반으로 캐스트합니다. (보다 전문적으로 암시 적으로 더 일반적으로 캐스트합니다). 비자와는 달리 명시적인 캐스트가 필요합니다 dynamic_cast. 일반적으로 . 다른 모든 것-정의되지 않은 동작이 발생하기 쉬우므로 수행중인 작업을 알고 있어야합니다. 내가 아는 한, C ++ 98조차도 이전부터 바뀌지 않았습니다.
Steve314

10
오늘날의 C ++ 컴파일러는 바인딩이 어떻게 될지 확신 할 수있을 때 초기 바인딩 초기에 최적화 할 수 있습니다. 이것을 "비 가상화"라고도합니다.
einpoklum

83

이를 입증하려면 최소한 하나의 상속 레벨과 다운 캐스트가 필요합니다. 다음은 매우 간단한 예입니다.

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
}

39
귀하의 예는 반환 된 문자열이 함수가 가상인지 여부에 따라 다르지만 어떤 결과가 가상에 해당하고 어떤 것이 가상이 아닌지에 대해서는 말하지 않습니다. 또한 반환되는 문자열을 사용하지 않으므로 약간 혼란 스럽습니다.
Ross

7
: 가상 키워드로 으릉 . 가상 키워드없이 : ? .
Hesham Eraqi

가상없이 @HeshamEraqi 그것은 초기 바인딩이며 "?"를 표시합니다 기본 클래스
Ahmad

46

안전한 다운 캐스팅 , 단순성간결성을 위해 가상 방법이 필요합니다 .

가상 메소드는 간단하고 간결한 코드를 사용하여 안전하게 다운 캐스트하며, 더 복잡하고 자세한 코드에서 안전하지 않은 수동 캐스트를 피합니다.


비가 상 방법 ⇒ 정적 바인딩

다음 코드는 의도적으로 "잘못된"코드입니다. 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때문에 메소드가 호출 됩니다.Expressionvalue


가상 방법 ⇒ 동적 바인딩.

정적으로 알려진 유형 value과 같이 선언 하면 각 호출이 실제 유형의 객체를 확인하고 해당 동적 유형 의 관련 구현을 호출합니다 .virtualExpressionvalue

#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;
}

여기서는 6.86가상 메소드가 사실상 호출 되기 때문에 출력은 그대로 있어야합니다 . 이를 호출의 동적 바인딩 이라고도 합니다. 실제 동적 유형의 오브젝트를 찾아서 약간의 점검이 수행되고 해당 동적 유형에 대한 관련 메소드 구현이 호출됩니다.

관련 구현은 가장 구체적인 (가장 파생 된) 클래스의 구현입니다.

여기서 파생 클래스의 메소드 구현은 표시되지 virtual않지만 대신 표시 override됩니다. 표시 할 수는 virtual있지만 자동으로 가상입니다. override가있는 경우 해당 키워드 보장하지만 하지 몇 가지 기본 클래스에서 이러한 가상 메서드, 다음 (바람직하다) 오류가 발생합니다.


가상 방법없이이 작업을 수행 할 때의 추악함

없이 virtual하나는 일부 구현해야 할 일이 자신 동적 바인딩의 버전을. 일반적으로 안전하지 않은 수동 다운 캐스팅, 복잡성 및 세부 정보가 포함됩니다.

단일 함수의 경우 여기에서와 같이 객체에 함수 포인터를 저장하고 해당 함수 포인터를 통해 호출하면 충분하지만 안전하지 않은 다운 캐스트, 복잡성 및 세부 정보가 포함됩니다.

#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;
}

이를 보는 한 가지 긍정적 인 방법은 위와 같이 안전하지 않은 다운 캐스팅, 복잡성 및 자세한 정보가 표시되는 경우 가상 방법이 실제로 도움이 될 수 있습니다.


40

가상 함수는 런타임 다형성 을 지원하는 데 사용됩니다 .

즉, virtual 키워드는 컴파일러에게 컴파일 타임에 함수 바인딩 결정을하지 말고 런타임에 연기하도록 지시합니다 . "

  • virtual기본 클래스 선언에서 키워드 앞에있는 함수를 가상으로 만들 수 있습니다 . 예를 들어

     class Base
     {
        virtual void func();
     }
  • 기본 클래스는 가상 멤버 함수를 가지고, 모든 클래스 기본 클래스에서 상속을 할 수있는 재정 과 기능을 정확히 같은 프로토 타입 , 즉 유일한 기능, 재정 기능이 아닌 인터페이스 할 수있다.

     class Derive : public Base
     {
        void func();
     }
  • 기본 클래스 포인터는 파생 클래스 개체뿐만 아니라 기본 클래스 개체를 가리키는 데 사용할 수 있습니다.

  • Base 클래스 포인터를 사용하여 가상 함수를 호출하면 컴파일러는 런타임에 함수의 어떤 버전 (즉, Base 클래스 버전 또는 재정의 된 파생 클래스 버전)을 호출할지 결정합니다. 이것을 런타임 다형성 이라고 합니다.

34

기본 클래스가 Base이고 파생 클래스가 Der이면 Base *p실제로 인스턴스를 가리키는 포인터를 가질 수 있습니다 Der. 당신이 호출 할 때 p->foo();경우 foo입니다 하지 가상, 다음 Base그것의의 버전은 그 사실을 무시하고, 실행 p실제로 포인트를 Der. foo virtual 인 경우 p->foo(), "leafmost"재정의를 실행 foo하고 지정된 항목의 실제 클래스를 완전히 고려합니다. 따라서 가상과 비가상의 차이점은 실제로 매우 중요합니다. 전자는 OO 프로그래밍의 핵심 개념 인 런타임 다형성을 허용 하지만 후자는 그렇지 않습니다.


8
나는 당신과 모순되는 것을 싫어하지만 컴파일 타임 다형성은 여전히 ​​다형성입니다. 멤버가 아닌 함수를 오버로드하더라도 링크의 용어를 사용하는 임시 다형성의 형태입니다. 여기서의 차이점은 초기 바인딩과 늦은 바인딩의 차이입니다.
Steve314

7
@ Steve314, 당신은 Pedantically 정확합니다 (동료 pedant로서, 나는 그것을 승인합니다 ;-)-누락 형용사를 추가하기 위해 답변을 편집 ;-).
Alex Martelli

26

가상 기능의 필요성 설명 [쉽게 이해]

#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.

따라서 가상 기능을 사용하면 런타임 다형성을 달성 할 수 있습니다.


25

위에서 언급 한 답변과 동일한 개념을 사용하지만 가상 기능의 다른 사용을 추가하고 싶지만 언급 할 가치가 있다고 생각합니다.

가상 파괴자

기본 클래스 소멸자를 가상으로 선언하지 않고 아래의이 프로그램을 고려하십시오. 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

11
without declaring Base class destructor as virtual; memory for Cat may not be cleaned up.그것보다 더 나쁘다. 기본 포인터 / 참조를 통해 파생 된 객체를 삭제하는 것은 순수한 정의되지 않은 동작입니다. 따라서 일부 메모리가 누출되는 것은 아닙니다. 컴파일러는 아무것도로 변환 할 수 있도록 오히려, 프로그램은, 잘못을 형성한다 : 기계 코드 발생하는 작업 벌금, 또는 코에서 아무것도, 또는 소환 악마를하지 않습니다, 또는 등 그 프로그램이 같은 설계되어있는 경우 이유의 일부 사용자 는 기본 참조를 통해 파생 인스턴스를 삭제할 있는 방법으로 , 기본 에는 가상 소멸자가 있어야합니다.
underscore_d

21

오버라이드와 오버로드를 구별해야합니다. virtual키워드가 없으면 기본 클래스의 메서드 만 오버로드합니다. 이것은 숨기는 것 외에는 아무것도 의미하지 않습니다. 기본 클래스 Base와 파생 클래스 Specialized가 모두 구현되어 있다고 가정 해 봅시다 void foo(). 이제 Base의 인스턴스 를 가리키는 포인터 가 Specialized있습니다. 호출 할 때 foo()차이점을 관찰 할 수 있습니다 virtual. 메소드가 가상 인 경우 구현 Specialized이 사용되며 누락 된 경우의 버전 Base이 선택됩니다. 기본 클래스에서 메서드를 오버로드하지 않는 것이 가장 좋습니다. 메소드를 가상이 아닌 것으로 만드는 것은 서브 클래스에서의 확장이 의도 된 것이 아니라는 것을 저자에게 알리는 방법입니다.


3
이 없다면 virtual당신은 오버로드되지 않습니다. 당신은 그림자 입니다. 기본 클래스는 경우 B하나 개 이상의 기능을 가지고 foo, 그리고 파생 클래스가 D정의 foo이름을, 그 foo 가죽 모든 이들 foo의 -s B. B::foo범위 확인 을 사용하여 도달 합니다. 과부하 를 위해 B::foo함수 를 승격 시키려면 D을 사용해야 using B::foo합니다.
Kaz

20

C ++에서 가상 메소드가 필요한 이유는 무엇입니까?

빠른 답변 :

  1. 객체 지향 프로그래밍에 필요한 "성분" 1 을 제공 합니다 .

Bjarne Stroustrup C ++ 프로그래밍 : 원칙과 실습 (14.3) :

가상 함수는 기본 클래스에서 함수를 정의하고 사용자가 기본 클래스 함수를 호출 할 때 호출되는 파생 클래스에서 동일한 이름 및 유형의 함수를 갖는 기능을 제공합니다. 호출 된 함수는 사용 된 객체의 유형에 따라 런타임에 결정되므로 런타임 다형성 , 동적 디스패치 또는 런타임 디스패치 라고도합니다.

  1. 가상 함수 호출 2 가 필요한 경우 가장 빠르고 효율적인 구현 입니다.

가상 호출을 처리하려면 파생 된 객체 와 관련된 하나 이상의 데이터가 필요 합니다 3 . 일반적으로 수행되는 방법은 함수 테이블의 주소를 추가하는 것입니다. 이 테이블을 일반적으로 가상 테이블 또는 가상 함수 테이블이라고 하며 해당 주소를 종종 가상 포인터 라고합니다 . 각 가상 함수는 가상 테이블에서 슬롯을 가져옵니다. 호출자의 객체 (유도 된) 유형에 따라 가상 함수는 차례로 해당 재정의를 호출합니다.


1. 상속, 런타임 다형성 및 캡슐화의 사용은 객체 지향 프로그래밍 의 가장 일반적인 정의입니다 .

2. 런타임시 대안 중에서 선택하기 위해 다른 언어 기능을 사용하여 기능을 더 빠르게 코딩하거나 더 적은 메모리를 사용하도록 코딩 할 수 없습니다. Bjarne Stroustrup C ++ 프로그래밍 : 원리와 실습. (14.3.1) .

3. 가상 함수가 포함 된 기본 클래스를 호출 할 때 실제로 어떤 함수가 호출되는지 알려줍니다.


15

나는 더 잘 읽을 수 있도록 대화 형식으로 답변을했습니다.


가상 기능이 필요한 이유는 무엇입니까?

다형성 때문에.

다형성이란 무엇입니까?

기본 포인터가 파생 된 형식 개체를 가리킬 수도 있습니다.

다형성에 대한 이러한 정의는 가상 함수의 필요성을 어떻게 이끌어 냅니까?

초기 바인딩을 통해 .

초기 바인딩이란 무엇입니까?

C ++에서 초기 바인딩 (컴파일 타임 바인딩)은 프로그램이 실행되기 전에 함수 호출이 수정되었음을 의미합니다.

그래서...?

따라서 기본 유형을 함수의 매개 변수로 사용하는 경우 컴파일러는 기본 인터페이스 만 인식하고 파생 클래스의 인수를 사용하여 해당 함수를 호출하면 잘려 나갑니다.

우리가 원하는 것이 아니라면 왜 이것이 허용됩니까?

다형성이 필요하기 때문에!

그렇다면 다형성의 이점은 무엇입니까?

기본 유형 포인터를 단일 함수의 매개 변수로 사용할 수 있으며, 프로그램 런타임에서 단일 유형의 역 참조를 사용하여 문제없이 각 파생 유형 인터페이스 (예 : 멤버 함수)에 액세스 할 수 있습니다. 기본 포인터.

나는 아직도 어떤 가상 기능이 좋은지 모른다! 그리고 이것은 나의 첫 번째 질문이었습니다!

글쎄, 당신이 너무 빨리 질문을했기 때문입니다!

가상 기능이 필요한 이유는 무엇입니까?

파생 클래스 중 하나에서 객체의 주소를 가진 기본 포인터로 함수를 호출했다고 가정하십시오. 위에서 언급했듯이 런타임 에서이 포인터는 역 참조되었지만 지금까지는 훌륭했지만 파생 클래스의 메서드 (= = 멤버 함수)가 실행될 것으로 기대합니다! 그러나 동일한 메소드 (같은 헤더를 가진 메소드)가 이미 기본 클래스에 정의되어 있으므로 프로그램에서 다른 메소드를 선택 해야하는 이유는 무엇입니까? 다시 말해, 이전에 일반적으로 발생했던 방식에서이 시나리오를 어떻게 알 수 있습니까?

간단한 대답은 "기본의 가상 멤버 함수"이고, 조금 더 긴 대답은 "이 단계에서 프로그램이 기본 클래스에서 가상 함수를 볼 경우 사용하려고한다는 것을 알고 있습니다 (실현) 다형성 "과 같은 방법으로 파생 클래스 ( v-table , 후기 바인딩 형식)를 사용하여 헤더가 동일하지만 구현 이 다른 다른 메소드 를 찾습니다 .

왜 다른 구현입니까?

너 너클 머리! 좋은 책을 읽으십시오 !

OK, wait wait wait, 왜 파생 형 포인터를 사용할 수있을 때 왜 기본 포인터를 사용하는 것이 좋을까요? 당신은 판사입니다.이 두통이 그만한 가치가 있습니까? 이 두 스 니펫을보십시오.

//1:

Parent* p1 = &boy;
p1 -> task();
Parent* p2 = &girl;
p2 -> task();

// 2 :

Boy* p1 = &boy;
p1 -> task();
Girl* p2 = &girl;
p2 -> task();

나는 생각하지만, OK, 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();

그리고 실제로, 이것은 아직 고안된 예일 수도 있습니다!


2
단일 (슈퍼) 객체 유형을 사용하여 서로 다른 유형의 (하위) 객체를 반복하는 개념이 강조되어야합니다. 이는 여러분이 준 좋은 지적입니다
harshvchawla

14

기본 클래스에 함수가 있으면 파생 클래스에서 Redefine또는 함수를 사용할 수 있습니다 Override.

메소드 재정의 : 기본 클래스의 메소드에 대한 새로운 구현이 파생 클래스에 제공됩니다. 촉진 하지 않습니다Dynamic binding .

메소드를 재정의 : 베이스 클래스가 파생 된 클래스. 가상 메소드는 동적 바인딩을 용이하게 합니다.Redefiningvirtual method

그래서 당신이 말했을 때 :

그러나이 책의 앞부분에서 기본 상속에 대해 배울 때 '가상'을 사용하지 않고 파생 클래스의 기본 메소드를 대체 할 수있었습니다.

기본 클래스의 메소드가 가상이 아니기 때문에 재정의하지 않고 재정의했습니다.


11

기본 메커니즘을 알고 있으면 도움이됩니다. C ++은 C 프로그래머가 사용하는 일부 코딩 기술을 공식화합니다. "클래스"는 "오버레이"로 대체되었습니다. 공통 헤더 섹션이있는 구조체는 서로 다른 유형의 객체를 처리하지만 공통 데이터 나 연산은 사용합니다. 일반적으로 오버레이의 기본 구조체 (공통 부분)에는 각 객체 유형에 대해 서로 다른 루틴 세트를 가리키는 함수 테이블에 대한 포인터가 있습니다. C ++은 동일한 기능을 수행하지만 메커니즘, 즉 ptr->func(...)func가 C와 같은 가상 C ++을 숨기고 (*ptr->func_table[func_num])(ptr,...)파생 클래스 간 변경 내용은 func_table 내용입니다. [비가 상 메소드 ptr-> func ()는 단지 mangled_func (ptr, ..)로 변환됩니다.]

결론은 파생 클래스의 메소드를 호출하기 위해 기본 클래스 만 이해하면된다는 것입니다. 즉, 루틴이 클래스 A를 이해하면 파생 클래스 B 포인터를 전달하면 가상 메소드는 다음과 같습니다. 함수 테이블 B를 통해 이동하므로 A 대신 B의


8

virtual 키워드는 컴파일러에게 초기 바인딩을 수행해서는 안된다고 알려줍니다. 대신, 후기 바인딩을 수행하는 데 필요한 모든 메커니즘을 자동으로 설치해야합니다. 이를 위해 일반적인 컴파일러 1은 가상 함수를 포함하는 각 클래스에 대해 단일 테이블 (VTABLE이라고 함)을 만듭니다. 컴파일러는 해당 특정 클래스에 대한 가상 함수의 주소를 VTABLE에 배치합니다. 가상 함수가있는 각 클래스에서 vpointer (VTRTR이라고 함)라는 포인터를 비밀리에 배치합니다.이 포인터는 해당 오브젝트의 VTABLE을 가리 킵니다. 기본 클래스 포인터를 통해 가상 함수를 호출하면 컴파일러는 코드를 자동으로 삽입하여 VPTR을 가져오고 VTABLE에서 함수 주소를 조회하므로 올바른 함수를 호출하여 바인딩이 지연됩니다.

이 링크에 대한 자세한 내용은 http://cplusplusinterviews.blogspot.sg/2015/04/virtual-mechanism.html


7

가상 키워드 힘은 컴파일러에 정의 된 메소드 구현을 선택하는 개체의 클래스 대신에 포인터의 클래스를.

Shape *shape = new Triangle(); 
cout << shape->getName();

위의 예제에서 getName ()이 기본 클래스 Shape에 virtual로 정의되어 있지 않으면 Shape :: getName이 기본적으로 호출됩니다. 그러면 컴파일러가 Shape 클래스가 아닌 Triangle 클래스에서 getName () 구현을 찾도록합니다.

가상 테이블은 컴파일러가 서브 클래스의 다양한 가상 메소드 구현 추적 유지하는기구이다. 이것은 또한 동적 파견이라고하고, 거기 이다 그것과 관련된 약간의 오버 헤드가.

마지막으로 C ++에서 가상이 필요한 이유는 무엇입니까?

  1. C ++은 "제로 오버 헤드"및 "사용하는 비용 지불"의 원칙을 기반으로합니다. 따라서 필요하지 않은 경우 동적 디스패치를 ​​수행하려고 시도하지 않습니다.
  2. 인터페이스에 더 많은 제어 기능을 제공합니다. 함수를 가상이 아닌 것으로 만들면 인터페이스 / 추상 클래스는 모든 구현에서 동작을 제어 할 수 있습니다.

4

가상 기능이 필요한 이유는 무엇입니까?

가상 함수는 불필요한 타입 캐스팅 문제를 피하고, 우리 중 일부는 파생 클래스 포인터를 사용하여 파생 클래스에 특정한 함수를 호출 할 수 있는데 왜 가상 함수가 필요한지 토론 할 수 있습니다! 단일 포인터 기본 클래스 객체를 갖는 것이 많이 요구되는 개발.

가상 함수의 중요성을 이해하기 위해 아래 두 간단한 프로그램을 비교해 보겠습니다.

가상 기능이없는 프로그램 :

#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

두 출력을 모두 면밀히 분석함으로써 가상 기능의 중요성을 이해할 수 있습니다.


3

OOP 답변 : 하위 유형 다형성

C ++에서 다형성 ,보다 정확하게 서브 타이핑 또는 서브 타입 다형성 을 실현하려면 가상 메소드가 필요합니다. 의 정의를 적용 할 경우 .

Wikipedia, Subtyping, 2019-01-09 : 프로그래밍 언어 이론에서 하위 유형 지정 (또는 하위 유형 다형성 또는 포함 다형성)은 하위 유형이 일부 개념에 의해 다른 데이터 유형 (슈퍼 유형)과 관련된 데이터 유형 인 유형 다형성의 한 형태입니다. 대체 가능성 (substitutability)의 의미는 프로그램 요소, 일반적으로 서브 루틴 또는 함수 가 수퍼 타입의 요소에서 작동하도록 작성되었음을 의미합니다. 요소들상에서 동작 할 수 .

참고 : 하위 유형은 기본 클래스를 의미하고 하위 유형은 상속 된 클래스를 의미합니다.

아형 다형성 에 관한 추가 자료

기술 답변 : 동적 발송

기본 클래스에 대한 포인터가있는 경우 메소드 호출 (가상으로 선언 됨)은 작성된 오브젝트의 실제 클래스 메소드에 전달됩니다. 이것이 서브 타입 다형성 이 실현되는 방법입니다 C ++.

C ++ 및 동적 디스패치 에서 다형성 읽기

구현 답변 : vtable 항목을 만듭니다.

메소드의 각 수정 자 "가상"에 대해 C ++ 컴파일러는 일반적으로 메소드가 선언 된 클래스의 vtable에 항목을 작성합니다. 이것이 일반적인 C ++ 컴파일러가 동적 디스패치를 실현하는 방법입니다 입니다.

추가 읽기 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 클래스 다이어그램

코드 예제의 UML 클래스 다이어그램


1
다형성의 가장 중요한 사용법을 보여주기 때문에 저의 의견을 들으십시오. 가상 멤버 함수가있는 기본 클래스는 인터페이스 또는 다른 말로 API를 지정합니다. 이러한 클래스 프레임 작업 (여기 : 주 함수)을 사용하는 코드는 컬렉션의 모든 항목 (여기 : 배열)을 균일하게 처리 할 수 있으며 호출 할 구체적인 구현이 필요하지 않고 원하지 않으며 실제로 수 없습니다. 아직 존재하지 않기 때문에 런타임에. 이것은 객체와 핸들러 사이의 추상적 관계를 조각하는 기초 중 하나입니다.
피터-모니카 복원

2

다음은 가상 방법이 사용되는 이유를 보여주는 완전한 예입니다.

#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;
}

1

효율성과 관련하여 가상 기능 은 초기 바인딩 기능보다 약간 덜 효율적입니다.

"이 가상 호출 메커니즘은"정상 함수 호출 "메커니즘 (25 % 이내)만큼 효율적으로 수행 될 수 있습니다. 공간 오버 헤드는 가상 함수가있는 클래스의 각 오브젝트에서 하나의 포인터와 각 클래스에 대해 하나의 vtbl입니다"[ A Bjarne Stroustrup 의 C ++ 둘러보기 ]


2
늦은 바인딩은 함수 호출을 느리게 할뿐 아니라 런타임까지 호출 된 함수를 알 수 없게하므로 함수 호출에 대한 최적화를 적용 할 수 없습니다. 이것은 모든 f.ex를 변경할 수 있습니다. 값 전파로 인해 많은 코드가 제거되는 경우 ( if(param1>param2) return cst;컴파일러가 전체 함수 호출을 상수로 줄일 수있는 경우를 생각해보십시오 ).
curiousguy

1

가상 메소드는 인터페이스 디자인에 사용됩니다. 예를 들어 Windows에는 아래와 같이 IUnknown이라는 인터페이스가 있습니다.

interface IUnknown {
  virtual HRESULT QueryInterface (REFIID riid, void **ppvObject) = 0;
  virtual ULONG   AddRef () = 0;
  virtual ULONG   Release () = 0;
};

이러한 메소드는 인터페이스 사용자가 구현해야합니다. IUnknown을 상속해야하는 특정 개체의 생성 및 파괴에 필수적입니다. 이 경우 런타임은 세 가지 방법을 인식하고 호출 할 때 구현 될 것으로 예상합니다. 어떤 의미에서 그들은 객체 자체와 그 객체를 사용하는 모든 것 사이의 계약 역할을합니다.


the run-time is aware of the three methods and expects them to be implemented순수한 가상이기 때문에의 인스턴스를 생성 할 수있는 방법이 IUnknown없으므로 모든 서브 클래스 컴파일하기 위해 이러한 모든 메소드를 구현 해야합니다 . 그것들을 구현하지 않고 런타임에 알아내는 것의 위험은 없습니다 (물론 분명히 그것을 잘못 구현할 수 있습니다 !). 그리고 와우, 오늘 나는 사용자가 (A) 이름 의 접두사 를 보거나 (B) 클래스를보고 인터페이스인지 알 수 없기 때문에 Windows #definesa 매크로를 배웠습니다 . UghinterfaceI
underscore_d

1

메소드가 가상으로 선언되면 'virtual'키워드를 재정의 할 필요가 없다는 사실을 언급하고 있다고 생각합니다.

class Base { virtual void foo(); };

class Derived : Base 
{ 
  void foo(); // this is overriding Base::foo
};

Base의 foo 선언에서 'virtual'을 사용하지 않으면 Derived의 foo는 그림자를 만듭니다.


1

다음은 처음 두 답변에 대한 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의 says () 메소드를 가리 킵니다.

$ 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의 says () 메소드를 가리 킵니다. [Dog 's says () "woof"] 메소드가 정의되어 있지 않으면 클래스 트리에서 먼저 검색되는 메소드가됩니다. 즉, 파생 클래스는 기본 클래스의 메소드를 대체 할 수 있습니다 [Animal 's says ()].

$ 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

파이썬의 모든 클래스 속성 (데이터 및 메소드) 이 사실상 가상 이라는 점에 주목하는 것이 흥미 롭습니다 . 모든 객체는 런타임에 동적으로 생성되므로 유형 선언이나 키워드 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 is d)은 True를 반환하고 해당 값은 < main .Dog object at 0xb79f72cc>와 같습니다.


0

"런타임 다형성"을 지원하기위한 가상 방법이 필요합니다. 포인터 나 기본 클래스에 대한 참조를 사용하여 파생 클래스 객체를 참조 할 때 해당 객체에 대한 가상 함수를 호출하고 파생 클래스의 함수 버전을 실행할 수 있습니다.


0

함수 포인터에 익숙하십니까? 가상 함수는 데이터를 가상 함수에 쉽게 클래스 멤버로 바인딩 할 수 있다는 점을 제외하면 비슷한 아이디어입니다. 데이터를 함수 포인터에 바인딩하는 것은 쉽지 않습니다. 나에게 이것은 주요한 개념의 구별이다. 여기에 다른 많은 답변들이 "다형성 때문에!"


-1

결론은 가상 기능이 삶을 더 쉽게 만든다는 것입니다. 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 ++ 가상 함수 대 멤버 함수 포인터 (성능 비교) .

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.