C ++で仮想関数が必要なのはなぜですか?


1312

私はC ++を学習していて、仮想関数を取得しています。

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

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

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


13
私はここで仮想関数のための実用的な説明を作成しました:nrecursions.blogspot.in/2015/06/...
Navの

4
これは、おそらく仮想関数の最大の利点です-新しく派生したクラスが変更せずに古いコードで自動的に機能するようにコードを構造化する機能!
user3530616 2017

tbh、仮想関数は型消去のためのOOPの主要な機能です。非仮想メソッドはObject PascalとC ++を特別なものにしているものだと思います。不要な大きなvtableを最適化し、POD互換のクラスを許可します。多くのOOP言語は、すべてのメソッドをオーバーライドできることを期待してます。
スウィフト-フライデーパイ

これは良い質問です。実際、C ++のこの仮想的なものは、JavaやPHPなどの他の言語では抽象化されています。C ++では、いくつかのまれなケースでもう少し多くの制御が得られます(多重継承またはDDODのその特別なケースに注意してください)。しかし、なぜこの質問がstackoverflow.comに投稿されるのですか?
エドガーアローロ

アーリーバインディング-レイトバインディングとVTABLEを見ると、もっと合理的で意味があると思います。だからここに良い説明があります(learncpp.com/cpp-tutorial/125-the-virtual-table)。
ceyun

回答:


2729

これが私がどのような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*ますか?動物からより多くの動物を派生させなければならない場合、それらはすべて彼ら自身のものを必要とするでしょう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ではサブクラスメソッドを呼び出すことができますか?
ケニーワーデン2015

147
中間関数 "func"の例を通して遅延バインディングを説明する代わりに、より簡単なデモを示します- 動物*動物=新しい動物; // Cat * cat = new Cat; 動物*猫=新しい猫; animal-> eat(); //出力:「私は一般的な食べ物を食べています。」 cat-> eat(); //出力:「私は一般的な食べ物を食べています。」サブクラス化オブジェクト(Cat)を割り当てている場合でも、呼び出されるメソッドは、それが指しているオブジェクトのタイプではなく、ポインタータイプ(Animal)に基づいています。これが、「仮想」が必要な理由です。
レックスベリア2016

37
C ++でこのデフォルトの動作を見つけたのは私だけですか?「仮想」なしのコードが機能することを期待していました。
David天宇Wong 2016

20
@David天宇Wong virtual動的バインディングと静的バインディングの両方が導入されていると思います。そうです、Javaのような言語から来ているのは奇妙です。
peterchaula

32
まず、仮想呼び出しは通常の関数呼び出しよりもはるかに高価です。C ++の哲学はデフォルトで高速であるため、デフォルトの仮想呼び出しは大きな禁止事項です。2番目の理由は、ライブラリからクラスを継承し、基本クラスの動作を変更せずにパブリックメソッドまたはプライベートメソッド(内部で仮想メソッドを呼び出す)の内部実装を変更すると、仮想呼び出しによってコードが破損する可能性があるためです。
saolof 2017

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
アーリーバインディングとレイトバインディングがc ++コミュニティで具体的に使用されている用語かどうかはわかりませんが、正しい用語は静的(コンパイル時)と動的(実行時)バインディングです。
マイク、

31
@mike- 「「遅延バインディング」という用語は、少なくとも1960年代にさかのぼります。この用語は、Communications of the ACMにあります。」。コンセプトごとに正しい単語が1つあればいいのではないでしょうか。残念ながら、そうではありません。「アーリーバインディング」および「レイトバインディング」という用語は、C ++やオブジェクト指向プログラミングよりも古いものであり、使用する用語と同じくらい正確です。
Steve314

4
@BJovke-この回答は、C ++ 11が公開される前に作成されました。それでも、問題なくGCC 6.3.0でコンパイルしました(デフォルトではC ++ 14を使用)-変数の宣言やmain関数の呼び出しなどは明らかにラップしています。Pointer -to- Derivedは暗黙的に Pointer -to-Baseにキャストします(より特殊化された暗黙的により一般的なキャスト)。Visaと逆の場合、明示的なキャストが必要dynamic_castです。通常はです。その他-未定義の動作が発生しやすいので、自分が何をしているかを確認してください。私の知る限りでは、これはC ++ 98でさえ以前から変わっていません。
Steve314

10
今日のC ++コンパイラーは、バインディングがどのようになるかがはっきりしている場合に、多くの場合、後期バインディングから早期バインディングに最適化できることに注意してください。これは「非仮想化」とも呼ばれます。
einpoklum

83

それを実証するには、少なくとも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
}

39
あなたの例では、返される文字列は関数が仮想かどうかに依存すると述べていますが、どの結果が仮想に対応し、どの結果が非仮想に対応するかは示していません。さらに、返される文字列を使用していないため、少し混乱します。
ロス

7
仮想キーワード:Woof。仮想キーワードなし:
Hesham Eraqi 2013

仮想のない@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;
}

「悪い」とコメントされた行ではExpression::value静的に既知の型(コンパイル時に既知の型)がExpressionであり、valueメソッドが仮想ではないため、メソッドが呼び出されます。


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

valueとして宣言virtual静的に既知の型にExpression各呼び出しはオブジェクトの実際の型は、これが何であるかをチェックし、関連の実装を呼び出すことを保証する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;
}

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

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

ここで派生クラスのメソッド実装はマークされていないが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;
}

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


40

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

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

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

     class Base
     {
        virtual void func();
     }
  • ときに基本クラスが仮想メンバ関数を持っている、任意のクラスの基本クラスから継承できることを再定義した関数と全く同じプロトタイプすなわち機能だけが、再定義機能のないインタフェースすることができます。

     class Derive : public Base
     {
        void func();
     }
  • Baseクラスポインターは、BaseクラスオブジェクトとDerivedクラスオブジェクトを指すために使用できます。

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

34

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


8
私はあなたと矛盾したくないのですが、コンパイル時の多態性は依然として多態性です。非メンバー関数のオーバーロードでさえ、ポリモーフィズムの一種です-リンク内の用語を使用したアドホックなポリモーフィズム。ここでの違いは、事前バインディングと遅延バインディングの間です。
Steve314

7
@ Steve314、あなたは完全に正しいです(仲間のペダントとして、私はそれを承認します;-)-欠落している形容詞を追加するために回答を編集します;-)。
Alex Martelli、2010年

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

上記の回答と同じ概念を使用していますが、言及する価値があると思いますが、仮想関数の別の使用法を追加したいと思います。

仮想破壊者

Baseクラスのデストラクタを仮想として宣言せずに、以下のこのプログラムを検討してください。猫のメモリがクリーンアップされない場合があります。

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、のインスタンスを指すポインタができましたSpecializedfoo()あなたがそれを呼び出すとき、あなたはvirtual作る違いを観察することができます:メソッドが仮想の場合、の実装Specializedが使用されます、それが欠落している場合、からのバージョンBaseが選択されます。基本クラスからメソッドをオーバーロードしないことがベストプラクティスです。メソッドを非仮想化することは、サブクラスでの拡張が意図されていないことを作成者が伝える方法です。


3
virtualあなたなしでは過負荷ではありません。あなたは影を落としている。基本クラスBに1つ以上の関数fooがあり、派生クラスDfoo名前を定義しfoo いる場合、それらのfoo-s すべて非表示になりBます。それらは、B::fooスコープ解決を使用することで達成されます。B::foo関数Dをオーバーロードに昇格させるには、を使用する必要がありますusing B::foo
Kaz 2017年

20

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

素早い回答:

  1. これは、必要に応じて、「食材」の1を提供してくれます1のためのオブジェクト指向プログラミング

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

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

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

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


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

2.他の言語機能を使用して機能を高速化したり、メモリ使用量を減らしたりして、実行時に代替機能を選択することはできません。Bjarne Stroustrup C ++プログラミング:原則と実践。(14.3.1)

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


15

私はよりよく読むために会話の形で私の答えを持っています:


なぜ仮想関数が必要なのですか?

ポリモーフィズムのため。

ポリモーフィズムとは何ですか?

ベースポインターが派生型オブジェクトを指すこともできるという事実。

このポリモーフィズムの定義は、仮想関数の必要性にどのようにつながるのですか?

さて、事前バインディングを通じて。

事前バインディングとは何ですか?

C ++の事前バインディング(コンパイル時バインディング)は、プログラムが実行される前に関数呼び出しが修正されることを意味します。

そう...?

そのため、関数のパラメーターとして基本型を使用する場合、コンパイラーは基本インターフェイスのみを認識し、派生クラスからの引数を使用してその関数を呼び出すと、スライスされてしまいますが、これは意図したことではありません。

それが私たちが起こしたいことではないのであれば、なぜこれが許可されるのですか?

ポリモーフィズムが必要だからです!

それでは、ポリモーフィズムの利点は何ですか?

基本型ポインターを単一の関数のパラメーターとして使用し、プログラムの実行時に、その単一型の逆参照を使用して、問題なく各派生型インターフェース(メンバー関数など)にアクセスできます。ベースポインタ。

私はまだどの仮想関数が良いのかわかりません...!そして、これが私の最初の質問でした!

まあ、これはあなたがあなたの質問を早すぎるためです!

なぜ仮想関数が必要なのですか?

派生クラスの1つからのオブジェクトのアドレスを持つベースポインターで関数を呼び出したとします。上記で説明したように、ランタイムでは、このポインターは逆参照されますが、これまでのところ良好ですが、「派生クラスから」のメソッド(==メンバー関数)が実行されることを期待しています!ただし、同じメソッド(同じヘッダーを持つメソッド)は基本クラスで既に定義されているので、プログラムで他のメソッドを選択する必要があるのはなぜですか。つまり、以前に通常発生していたものから、このシナリオをどのように区別できますか?

簡単な答えは「ベースの仮想メンバー関数」であり、少し長い答えは、「このステップで、プログラムが基本クラスで仮想関数を見つけた場合、使用しようとしていることを認識(実現)します。ポリモーフィズム」などの派生クラス(レイトバインディングの形式であるv-tableを使用)にアクセスして、同じヘッダーを持つが、予想どおり異なる実装を持つ別のメソッドを見つけます。

なぜ別の実装ですか?

ナックルヘッド!良い本を読んでください!

OK、待って、待って、待って、なぜ単純に派生型ポインターを使用できるのに、ベースポインターを使用するのが面倒なのでしょうか。あなたは裁判官になります、この頭痛の種はそれだけの価値がありますか?次の2つのスニペットを見てください。

// 1:

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

// 2:

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

OK、12よりも優れていると思いますが、次のように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(...)、Cがそうであるようにfuncが仮想であるC ++のメカニズムを隠します(*ptr->func_table[func_num])(ptr,...)ます。派生クラス間で変更されるのはfunc_tableの内容です。[非仮想メソッドptr-> func()は、mangled_func(ptr、..)に変換されます。]

その結果、派生クラスのメソッドを呼び出すには、基本クラスを理解するだけで済みます。つまり、ルーチンがクラスAを理解している場合は、派生クラスBポインターを渡すことができます。呼び出される仮想メソッドは、関数テーブルBがポイントするので、AではなくBを使用します。


8

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

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


7

仮想キーワード軍で定義されたメソッド実装選ぶようにコンパイラオブジェクトのクラスではなく、中にポインタのクラスを。

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

上記の例では、getName()が基本クラスShapeで仮想として定義されていない限り、Shape :: getNameがデフォルトで呼び出されます。これにより、コンパイラは、ShapeクラスではなくTriangleクラスでgetName()の実装を検索します。

仮想テーブルは、コンパイラは、サブクラスの様々な仮想メソッドの実装を追跡するメカニズムです。また、これは動的ディスパッチと呼ばれ、そこにされています、それに関連したいくつかのオーバーヘッドが。

最後に、なぜ仮想がC ++でも必要なのか、なぜそれをJavaのようにデフォルトの動作にしないのか?

  1. C ++は、「ゼロオーバーヘッド」と「使用した分だけ支払う」の原則に基づいています。したがって、必要がない限り、動的ディスパッチを実行しようとはしません。
  2. インターフェースをさらに制御するため。関数を非仮想化することにより、インターフェイス/抽象クラスは、そのすべての実装での動作を制御できます。

4

なぜ仮想関数が必要なのですか?

仮想関数は不必要な型キャストの問題を回避し、派生クラスポインターを使用して派生クラスに固有の関数を呼び出すことができるのに、なぜ仮想関数が必要なのかを議論する人もいます!開発、単一のポインターの基本クラスのオブジェクトを持つことが非常に望まれます。

以下の2つの単純なプログラムを比較して、仮想関数の重要性を理解しましょう。

仮想機能のないプログラム:

#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

両方の出力を綿密に分析することにより、仮想機能の重要性を理解できます。


4

OOP Answer:サブタイプのポリモーフィズム

C ++では、ウィキペディアの定義を適用する場合、ポリモーフィズム、より正確にはサブタイピングまたはサブタイプのポリモーフィズムを実現するために仮想メソッドが必要です。

ウィキペディア、サブタイピング、2019-01-09:プログラミング言語理論では、サブタイピング(サブタイプの多型または包含多型)は、ある型によってサブタイプが別のデータ型(スーパータイプ)に関連付けられているデータ型である型多型の形式ですつまり、スーパータイプの要素を操作するように記述されたプログラム要素(通常はサブルーチンまたは関数)も、サブタイプの要素を操作できます。

注:サブタイプは基本クラスを意味し、サブタイプは継承クラスを意味します。

サブタイプポリモーフィズムに関する参考資料

技術的な回答:動的ディスパッチ

基本クラスへのポインターがある場合、(仮想として宣言されている)メソッドの呼び出しは、作成されたオブジェクトの実際のクラスのメソッドにディスパッチされます。これがC ++であるサブタイプポリモーフィズムの実現方法です。

C ++およびDynamic Dispatchでのポリモーフィズムの詳細

実装の回答:vtableエントリを作成します

メソッドの「仮想」修飾子ごとに、C ++コンパイラは通常、メソッドが宣言されているクラスのvtableにエントリを作成します。これは、一般的なC ++コンパイラが動的ディスパッチを実現する方法ですです。

さらにvtableを読む


コード例

#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を指定しますこのようなクラスフレームワーク(ここでは、メイン関数)を使用するコードは、コレクション(ここでは、配列)内のすべてのアイテムを均一に扱うことができ、必要としない、したくない、実際に呼び出される具体的な実装がわからないことよくあります。実行時。たとえば、まだ存在しないため。これは、オブジェクトとハンドラーの間の抽象的な関係を切り分けるための基礎の1つです。
ピーター-モニカを復活させる

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%以内)で作成できます。そのスペースオーバーヘッドは、仮想関数を持つクラスの各オブジェクトに1つのポインターと、そのようなクラスごとに1つのvtblです Bjarne StroustrupによるC ++のツアー ]


2
レイトバインディングでは、関数呼び出しが遅くなるだけでなく、呼び出される関数が実行時まで不明になるため、関数呼び出し全体の最適化を適用できません。これは、すべてのf.exを変更できます。値の伝達によって多くのコードが削除される場合(if(param1>param2) return cst;コンパイラーが関数呼び出し全体を定数に削減できる場合を考えてください)。
curiousguy 2015年

1

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

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

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


the run-time is aware of the three methods and expects them to be implementedこれらは純粋な仮想なIUnknownので、のインスタンスを作成する方法はありません。したがって、すべてのサブクラス、単にコンパイルするために、そのようなすべてのメソッドを実装する必要があります。それらを実装せず、実行時にそれを見つけるだけの危険はありません(もちろん、当然、間違って実装する可能性があります!)。そして、すごい、今日、私はWindowsの#definesaマクロを単語interfaceで学習しました。これは、おそらくユーザーが(A)I名前のプレフィックスを見ることができない、または(B)クラスを見てインターフェースであることを確認できないためと考えられます。Ugh
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

以下は、最初の2つの回答に対する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;
}

2つの異なる結果は次のとおりです。

#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 say() "woof"]メソッドが定義されていない限り、それはクラスツリーで最初に検索されるメソッドになります。

$ 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 ++の仮想定義と同じです。dadは、同じDogインスタンスを参照/指す2つの異なるポインター変数であることに注意してください。式(ad is d)はTrueを返し、それらの値は同じ< 0xb79f72ccのmain .Dogオブジェクト>です。


1

関数ポインタに精通していますか?仮想関数は、データを仮想関数に(クラスメンバーとして)簡単にバインドできることを除いて、同様のアイデアです。データを関数ポインタにバインドするのは簡単ではありません。私にとって、これは主要な概念上の違いです。ここでの他の多くの回答は、単に「多態性のため」と言っているだけです!


0

「ランタイムポリモーフィズム」をサポートする仮想メソッドが必要です。ポインターまたは基本クラスへの参照を使用して派生クラスオブジェクトを参照すると、そのオブジェクトの仮想関数を呼び出して、派生クラスのバージョンの関数を実行できます。


-1

肝心なことは、仮想関数は生活を楽にするということです。Mペリーのアイデアの一部を使用して、仮想関数がなく、メンバー関数ポインターしか使用できなかった場合にどうなるかを説明しましょう。仮想関数を使用しない通常の推定では、次のようになります。

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

OK、それが私たちが知っていることです。次に、メンバー関数ポインターでそれを実行してみましょう:

 #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 ++仮想関数vsメンバー関数ポインター(パフォーマンス比較)

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.