1522

私は C++ を学んでいて、仮想関数を始めたところです。

(本とオンラインで) 私が読んだことから、仮想関数は、派生クラスでオーバーライドできる基本クラスの関数です。

しかし、この本の前半で、基本的な継承について学習したときに、派生クラスの基底関数を .xml を使用せずにオーバーライドすることができましたvirtual

それで、私はここで何が欠けていますか?仮想関数には他にもあることを知っており、それが重要であるように思われるので、それが正確に何であるかを明確にしたいと思います。私はオンラインで簡単な答えを見つけることができません。

4

27 に答える 27

2997

virtual関数が何であるかだけでなく、なぜそれらが必要なのかを私がどのように理解したかを次に示します。

次の 2 つのクラスがあるとします。

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()ます。

解決策は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."

終わり。

于 2010-03-06T13:54:03.620 に答える
758

「仮想」がないと、「早期バインディング」が得られます。使用するメソッドの実装は、呼び出すポインターのタイプに基づいてコンパイル時に決定されます。

「仮想」を使用すると、「遅延バインディング」が得られます。使用されるメソッドの実装は、ポイントされたオブジェクトのタイプ(元々構築されたもの)に基づいて実行時に決定されます。これは、必ずしもそのオブジェクトを指すポインタのタイプに基づいて考えるものではありません。

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++でのアーリーバインディングとレイトバインディングについて説明します。

于 2010-03-06T07:56:18.250 に答える
90

それを実証するには、少なくとも 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
}
于 2010-03-06T07:26:25.223 に答える
52

仮想関数は、ランタイム ポリモーフィズムをサポートするために使用されます。

つまり、virtualキーワードは、コンパイル時に (関数バインディングの) 決定を行うのではなく、実行時まで延期するようにコンパイラに指示します。

  • virtual基本クラスの宣言でキーワードの前に置くことで、関数を仮想化できます。例えば、

    class Base
    {
       virtual void func();
    }
    
  • 基本クラスに仮想メンバー関数がある場合、基本クラスから継承するクラスは、まったく同じプロトタイプで関数を再定義できます。つまり、関数のインターフェイスではなく、機能のみを再定義できます。

    class Derive : public Base
    {
       void func();
    }
    
  • 基本クラス ポインターを使用して、基本クラス オブジェクトと派生クラス オブジェクトを指すことができます。

  • 基本クラス ポインターを使用して仮想関数が呼び出されると、コンパイラは実行時に関数のどのバージョン (基本クラス バージョンまたはオーバーライドされた派生クラス バージョン) を呼び出すかを決定します。これはランタイム ポリモーフィズムと呼ばれます。

于 2016-10-12T09:41:38.377 に答える
48

安全なダウンキャスト単純さ、および簡潔さのための仮想メソッドが必要です。

それが仮想メソッドの役割です。明らかにシンプルで簡潔なコードを使用して安全にダウンキャストし、より複雑で冗長なコードでの危険な手動キャストを回避します。


非仮想メソッド ⇒ 静的バインド =======================================

次のコードは、意図的に「正しくない」ものです。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メソッドが仮想ではないためです。


仮想メソッド ⇒ 動的バインディング。======================================

静的に既知の型でvalueasを宣言すると、各呼び出しでオブジェクトの実際の型が確認され、その動的型に関連する実装が呼び出されることが保証されます。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;
}

ここでは、仮想メソッドが virtual と呼ば6.86れるため、出力は本来あるべきものになります。これは、呼び出しの動的バインディングとも呼ばれます。オブジェクトの実際の動的タイプを見つけて、少しチェックが実行され、その動的タイプに関連するメソッド実装が呼び出されます。

関連する実装は、最も具体的な (最も派生した) クラスの実装です。

ここでの派生クラスのメソッド実装はマークされていませんがvirtual、代わりにマークされていることに注意してくださいoverride。それらはマークできますvirtualが、自動的に仮想になります。このoverrideキーワードにより、一部の基本クラスにそのような仮想メソッドがない場合にエラーが発生することが保証されます (これは望ましいことです)。


仮想メソッドなしでこれを行うことの醜さ ========================================= ========

それがなければ、動的バインディングのDo It Yourselfvirtualバージョンを実装する必要があります。一般に、安全でない手動のダウンキャスト、複雑さ、および冗長性を伴うのはこれです。

ここに示すように、単一の関数の場合、オブジェクトに関数ポインターを格納し、その関数ポインターを介して呼び出すだけで十分ですが、それでも安全でないダウンキャスト、複雑さ、および冗長性が含まれます。

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

これを肯定的に捉える方法の 1 つは、上記のような安全でないダウンキャスト、複雑さ、および冗長性に遭遇した場合、多くの場合、1 つまたは複数の仮想メソッドが実際に役立つということです。

于 2014-11-24T07:24:45.033 に答える
39

基本クラスがBaseで、派生クラスがの場合、実際に のインスタンスを指すポインタをDer持つことができます。を呼び出すと、 が仮想でない場合、が実際を指しているという事実を無視して、 のバージョンが実行されます。foovirtual の場合、ポイントされた項目の実際のクラスを完全に考慮して、の「最下位」のオーバーライドを実行します。したがって、仮想と非仮想の違いは実際には非常に重要です。前者はランタイムポリモーフィズム(オブジェクト指向プログラミングのコア コンセプト) を許可しますが、後者は許可しません。Base *pDerp->foo();fooBasepDerp->foo()foo

于 2010-03-06T07:27:16.683 に答える
27

仮想機能の必要性を説明【分かりやすい】

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

したがって、仮想関数を使用すると、ランタイム ポリモーフィズムを実現できます。

于 2014-12-12T11:56:42.453 に答える
23

オーバーライドとオーバーロードを区別する必要があります。キーワードがなければvirtual、基本クラスのメソッドをオーバーロードするだけです。これは、隠れることに他なりません。両方を実装する基本クラスBaseと派生クラスがあるとします。これで、 のインスタンスを指すポインタができました。それを呼び出すと、その違いを観察できます。メソッドが仮想の場合は、の実装が使用されます。メソッドが欠落している場合は、からのバージョンが選択されます。基本クラスからメソッドをオーバーロードしないことがベスト プラクティスです。メソッドを非仮想にすることは、サブクラスでの拡張が意図されていないことを作成者が伝える方法です。Specializedvoid foo()BaseSpecializedfoo()virtualSpecializedBase

于 2010-03-06T07:27:04.343 に答える
21

なぜ C++ で仮想メソッドが必要なのですか?

素早い回答:

  1. これは、オブジェクト指向プログラミングに必要な「要素」1の 1 つを提供します。

Bjarne Stroustrup C++ プログラミング: 原則と実践 (14.3):

仮想関数は、基本クラスで関数を定義し、ユーザーが基本クラス関数を呼び出したときに呼び出される派生クラスで同じ名前と型の関数を持つ機能を提供します。これは、使用されるオブジェクトのタイプに基づいて実行時に呼び出される関数が決定されるため、実行時ポリモーフィズム動的ディスパッチ、または実行時ディスパッチと呼ばれることがよくあります。

  1. 仮想関数呼び出し が必要な場合、これは最速でより効率的な実装です2

仮想呼び出しを処理するには、派生オブジェクト に関連する 1 つ以上のデータが必要です3。通常行われる方法は、関数のテーブルのアドレスを追加することです。このテーブルは通常、仮想テーブルまたは仮想関数テーブルと呼ばれ、そのアドレスは仮想ポインターと呼ばれることがよくあります。各仮想関数は、仮想テーブルのスロットを取得します。呼び出し元のオブジェクト (派生) 型に応じて、仮想関数はそれぞれのオーバーライドを呼び出します。


1. 継承、ランタイム ポリモーフィズム、およびカプセル化の使用は、オブジェクト指向プログラミングの最も一般的な定義です。

2. 他の言語機能を使用して実行時に代替手段を選択することにより、機能をより速くしたり、より少ないメモリを使用したりするようにコーディングすることはできません。Bjarne Stroustrup C++ プログラミング: 原則と実践 (14.3.1) .

3. 仮想関数を含む基本クラスを呼び出すときに、どの関数が実際に呼び出されるかを示すもの。

于 2015-09-27T09:37:47.243 に答える
14

基本クラスに関数がある場合、派生クラスでそれを実行できRedefineます。Override

メソッドの再定義: 基底クラスのメソッドの新しい実装が派生クラスで提供されます。促進しませんDynamic binding

派生クラスの基本クラスのメソッド: Redefiningavirtual method仮想メソッドは Dynamic Binding を容易にします。

だからあなたが言ったとき:

しかし、この本の前半で、基本的な継承について学習したときに、「仮想」を使用せずに派生クラスの基本メソッドをオーバーライドすることができました。

基本クラスのメソッドは仮想ではなく、再定義していたため、オーバーライドしていませんでした

于 2012-02-06T08:29:56.113 に答える
11

根本的なメカニズムを知っていると役に立ちます。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の。

于 2012-11-30T08:22:55.910 に答える
10

キーワード virtual は、事前バインディングを実行しないことをコンパイラに伝えます。代わりに、遅延バインディングを実行するために必要なすべてのメカニズムを自動的にインストールする必要があります。これを実現するために、一般的なコンパイラー1 は、仮想関数を含むクラスごとに 1 つのテーブル (VTABLE と呼ばれる) を作成します。コンパイラーは、その特定のクラスの仮想関数のアドレスを VTABLE に配置します。仮想関数を持つ各クラスには、そのオブジェクトの VTABLE を指す vpointer (略して VPTR) と呼ばれるポインターが秘密裏に配置されます。基本クラスのポインターを介して仮想関数呼び出しを行うと、コンパイラーはコードを静かに挿入して VPTR をフェッチし、VTABLE で関数アドレスを検索するため、正しい関数が呼び出され、遅延バインディングが発生します。

このリンクの詳細 http://cplusplusinterviews.blogspot.sg/2015/04/virtual-mechanism.html

于 2015-04-23T12:49:31.190 に答える
2

効率については、仮想関数は早期バインディング関数よりも効率がわずかに劣ります。

「この仮想呼び出しメカニズムは、「通常の関数呼び出し」メカニズムとほぼ同じくらい効率的にすることができます (25% 以内)。そのスペースのオーバーヘッドは、仮想関数を持つクラスの各オブジェクトの 1 つのポインターに加えて、そのようなクラスごとに 1 つの vtbl です」 [ A Bjarne Stroustrupによる C++ のツアー]

于 2014-12-28T14:19:30.333 に答える
2

仮想メソッドは、インターフェイスの設計に使用されます。たとえば、Windows には、以下のような IUnknown というインターフェイスがあります。

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

これらのメソッドは、インターフェースのユーザーが実装する必要があります。これらは、IUnknown を継承する必要がある特定のオブジェクトの作成と破棄に不可欠です。この場合、ランタイムは 3 つのメソッドを認識しており、呼び出し時にそれらが実装されていることを期待しています。したがって、ある意味では、それらはオブジェクト自体とそのオブジェクトを使用するものとの間の契約として機能します。

于 2015-02-20T03:01:32.417 に答える