私はC++が初めてです。
メソッドのオーバーライドと C++ の仮想関数の概念の違いを教えてください。
仮想関数の機能は、その派生クラスでオーバーライドできます。派生クラスで関数を再定義することは、関数のオーバーライドと呼ばれます。
実際に仮想関数があるのはなぜですか?
私はC++が初めてです。
メソッドのオーバーライドと C++ の仮想関数の概念の違いを教えてください。
仮想関数の機能は、その派生クラスでオーバーライドできます。派生クラスで関数を再定義することは、関数のオーバーライドと呼ばれます。
実際に仮想関数があるのはなぜですか?
仮想関数/メソッドは、関数の動作方法を (同じシグネチャを使用して) 再定義することにより、サブクラス (または C++ 用語では派生クラス) 内で動作をオーバーライドできる単純な関数です。
話す機能を持つ基本クラスの哺乳類を考えてみてください。この関数は無効で、哺乳類の話し方を単純に数えます。このクラスから継承すると、speak メソッドをオーバーライドして、犬が「Arf Arf!」となるようにすることができます。猫は「ニャーニャー」と鳴きます。
仮想関数を使用すると、これらの関数の動作をオーバーライドできるため、違いはありません。関数のオーバーライドとオーバーロードの違いを知りたがっているかもしれません。
関数のオーバーロードとは、名前が同じで引数が異なる、つまり引数の数と型が異なる関数を作成することを意味します。以下は、 IBM のサイトからの C++ でのオーバーロードに関する説明です。
オーバーロード (C++ のみ) 同じスコープ内の関数名または演算子に対して複数の定義を指定すると、その関数名または演算子がオーバーロードされます。オーバーロードされた関数と演算子については、関数のオーバーロード (C++ のみ) と演算子のオーバーロード (C++ のみ) でそれぞれ説明されています。
オーバーロードされた宣言は、同じスコープで以前に宣言された宣言と同じ名前で宣言された宣言ですが、両方の宣言の型が異なります。
オーバーロードされた関数名または演算子を呼び出す場合、コンパイラは、関数または演算子の呼び出しに使用した引数の型と、定義で指定されたパラメーターの型を比較して、使用するのに最適な定義を決定します。最も適切なオーバーロードされた関数または演算子を選択するプロセスは、「オーバーロードの解決 (C++ のみ)」で説明されているように、オーバーロードの解決と呼ばれます。
仮想関数が必要な状況の完全な合理的な理由については、このブログ投稿 ( http://nrecursions.blogspot.in/2015/06/so-why-do-we-need-virtual-functions) を参照してください。 html
関数のオーバーライドと関数の違いは、ポリモーフィズムvirtual
で重要になります。特に、基本クラスへの参照またはポインタを使用する場合。
C ++では、任意の派生クラスを基本クラスオブジェクトを必要とする関数に渡すことができます。(スライスとLSPも参照してください)。与えられた:
struct Base_Virtual
{
virtual void some_virtual_function();
};
struct Base_Nonvirtual
{
void some_function();
};
void Function_A(Base_Virtual * p_virtual_base);
void Function_B(Base_Nonvirtual * p_non_virtual_base);
上記のコードには、2つの基本クラスがあります。1つは仮想メソッドを宣言し、もう1つは非仮想関数を宣言します。
それぞれのベースクラスへのポインタを必要とする2つの関数が宣言されています。
virtual
ここで、ポリモーフィズム、特に非仮想(オーバーライドメソッド)との比較をテストしてみましょう。構造:
struct Derived_From_Virtual
: public Base_Virtual
{
void some_virtual_function(); // overrides Base_Virtual::some_virtual_function()
};
struct Derived_From_Nonvirtual:public Base_Nonvirtual {void some_function(); }
C ++言語によると、から継承するため、 Derived_From_Virtual
toへのポインタを渡すことができます。へのポインタを渡すこともできます。Function_A
Derived_From_Virtual
Base_Virtual
Derived_From_Nonvirtual
Function_B
virtual
とオーバーライドの違いのvirtual
修飾子は、のメソッドの代わりに使用するBase_Virtual
コンパイラに指示します。これは、メソッドが仮想であるため、最終的な定義が将来のクラスまたは派生クラスに存在する可能性があるためです。実際の定義では、定義を含む最も派生したクラスのメソッドを使用するように指示されています。Function_A
Derived_From_Virtual::some_virtual_function()
Base_Virtual
にポインタを渡すDerived_From_Nonvirtual
とFunction_B
、コンパイラは関数に基本クラスのメソッドを使用するように指示しますBase_Nonvirtual::some_function()
。派生クラスのsome_function()
メソッドは、基本クラスとは別の無関係なメソッドです。
とオーバーライドの主な違いはvirtual
、ポリモーフィズムで発生します。
C++ FAQ lite ( http://www.parashift.com/c++-faq-lite/ ) を確認してください。おそらく、初心者向けの最高の C++ リソースの 1 つです。仮想関数とオーバーライドに関する詳細な記事があります。
個人的には、C++ を学ぶ上で C++ FAQ が優れた情報源であることがわかりました。他の人は異なる意見を持っています、あなたのマイレージは異なるかもしれません
これは、回答自体よりも、この回答からのコメントのフォローアップです。
virtual
宣言されているメソッドのランタイム ディスパッチを要求し、同時にそのメソッドをオーバーライドの 1 つとして宣言するキーワードです(実装された純粋仮想メソッドは別として)。宣言されているメソッド、およびこのクラスから派生する階層で正確なシグネチャと名前を共有するメソッドはすべてoverridesです。親ポインターまたは参照を介して仮想メソッドを呼び出すと、ランタイムは、呼び出されたオブジェクトの階層内で最も派生したオーバーライドを呼び出します。
メソッドが仮想ではなく、同じメソッドが階層内で後で定義されている場合は、親メソッドを非表示にしています。ここでの違いは、メソッドが基本ポインターまたは参照を介して呼び出されている場合は基本実装を呼び出し、派生オブジェクトで呼び出されている場合は派生実装を呼び出すことです。これは、特に、基本関数と派生関数が無関係であるため、非表示と呼ばれます。派生クラスで定義すると、呼び出しから基本バージョンが隠されます。
struct base {
virtual void override() { std::cout << "base::override" << std::endl; }
void not_override() { std::cout << "base::not_override" << std::endl; }
};
struct derived : base {
void override() { std::cout << "derived::override" << std::endl; }
void not_override() { std::cout << "derived::not_override" << std::endl; }
};
int main() {
derived d;
base & b = d;
b.override(); // derived::override
b.not_override(); // base::not_override
d.not_override(); // derived::not_override
}
@erik2red による回答の違いと間違っている点は、オーバーライドは仮想関数と密接に関連しており、呼び出す最も派生したオーバーライドを決定するランタイム ディスパッチ メカニズムが存在することを意味することです。回答に示され、オーバーライドに関連付けられている動作は、実際にはオーバーライドがなく、メソッドが隠されている場合の動作です。
その他の問題
この言語では、実装を伴う純粋仮想メソッドが可能です。どの用語を使用する必要があるかについては何も述べていませんが、純粋仮想メソッドはランタイム ディスパッチの対象とは見なされません。その理由は、純粋仮想メソッドを持つクラス (実装されている場合でも) が抽象クラスと見なされ、クラスのオブジェクトをインスタンス化できないためです。そのメソッドの実装を提供する派生クラスを取得すると、その実装が階層内の最終的なオーバーライドになります。クラスをインスタンス化できるようになりましたが、純粋仮想メソッドはランタイム ディスパッチ メカニズムを通じて呼び出されません。
完全修飾名を使用すると、最終的な overrideではない仮想メソッドや非表示のメソッドを呼び出すことができます。仮想メソッドの場合、完全修飾名を使用すると、呼び出しのポリモーフィック ディスパッチ メカニズムが無効になります。派生クラスに他のオーバーライドd.base::override()
がある場合でも、基本実装が呼び出されます。
署名が一致しない場合でも、メソッドは基本クラスの他のメソッドを非表示にすることができます。
struct base {
void f() {}
};
struct derived : base {
void f(int) {}
};
int main() {
derived d;
// d.f() // error, derived::f requires an argument, base::f is hidden in this context
}
overridesと同様にd.base::f()
、 はベース バージョンを呼び出します。これは、ポリモーフィズムを無効にするためではありません。メソッドが virtual と宣言されていないため、ポリモーフィックな動作をすることは決してありません。派生クラスの別のメソッドによって隠されている場合。
概要
この記事では、C++ の仮想関数について説明します。パート 0 では、仮想関数がどのように宣言され、オーバーライドされるかについて説明します。第 1 部では、仮想機能がどのように実装されるかを説明しようとします (おそらく失敗します)。パート 2 は、パート 0 とパート 1 で定義されたサンプル クラスを使用するサンプル プログラムです。パート 3 は、すべての仮想関数 - ポリモーフィズム チュートリアルで提供される古典的な動物の例です。
パート・ゼロ
クラスのメソッドは、仮想であると宣言されている場合にのみ、仮想であると言われます。
class my_base
{
public:
void non_virtual_test() { cout << 4 << endl; } // non-virtual
virtual void virtual_test() { cout << 5 << endl; } // virtual
};
(もちろん、プログラマーが以前に のようなことをしたことがないと仮定しています#define virtual
。)
そのベースの 1 つの非仮想メソッドを再宣言して再実装するクラスは、そのメソッドをオーバーロードすると言われます。そのベースの 1 つの仮想メソッドを再宣言して再実装するクラスは、そのメソッドをオーバーライドすると言われます。
class my_derived : public my_base
{
public:
void non_virtual_test() { cout << 6 << endl; } // overloaded
void virtual_test() { cout << 7 << endl; } // overriden
};
パート1
コンパイラは、クラスに仮想メソッドがあることを検出すると、仮想メソッド テーブル( vtableとも呼ばれます) をクラスのメモリ レイアウトに自動的に追加します。結果は、このコードをコンパイルして生成されたものと似ています。
class my_base
{
//<vtable>
// The vtable is actually a bunch of member function pointers
protected:
void (my_base::*virtual_test_ptr)();
//</vtable>
// The actual implementation of the virtual function
// is hidden from the rest of the program.
private:
void virtual_test_impl() { cout << 5 << endl; }
// Initializing the real_virtual_test pointer in the vtable.
public:
my_base() : virtual_test_ptr(&my_base::virtual_test_impl) {}
public:
void non_virtual_test() { cout << 4 << endl; }
// The interface of the virtual function is a wrapper
// around the member function pointer.
inline void virtual_test() { *virtual_test_ptr(); }
};
コンパイラは、クラスが仮想メソッドをオーバーライドしたことを検出すると、vtable 内の関連するエントリを置き換えます。結果は、このコードをコンパイルして生成されたものと似ています。
class my_derived : public my_base
{
// The actual implementation of the virtual function
// is hidden from the rest of the program.
private:
void virtual_test_impl() { cout << 7 << endl; }
// Initializing the real_virtual_test pointer in the vtable.
public:
my_derived() : virtual_test_ptr(&my_derived::virtual_test_impl) {}
public:
void non_virtual_test() { cout << 6 << endl; }
};
パート2
仮想関数が vtables を使用して実装されていることが明らかになったので、vtables は関数ポインターの集まりにすぎません。このコードが何をするかは明らかです。
#include <iostream>
using namespace std;
class my_base
{
public:
void non_virtual_test() { cout << 4 << endl; }
virtual void virtual_test() { cout << 5 << endl; }
};
class my_derived : public my_base
{
public:
void non_virtual_test() { cout << 6 << endl; }
void virtual_test() { cout << 7 << endl; }
}
int main()
{
my_base* base_obj = new my_derived();
// This outputs 4, since my_base::non_virtual_test() gets called,
// not my_derived::non_virtual_test().
base_obj->non_virtual_test();
// This outputs 7, since the vtable pointer points to
// my_derived::virtual_test(), not to my_base::virtual_test().
base_obj->virtual_test();
// We shall not forget
// there was an object that was pointed by base_obj
// who happily lived in the heap
// until we killed it.
delete base_obj;
return 0;
}
パート3
動物の例がなければ仮想関数の例は完成しないので...
#include <iostream>
using namespace std;
class animal
{
public:
virtual void say_something()
{ cout << "I don't know what to say." << endl
<< "Let's assume I can growl." << endl; }
/* A more sophisticated version would use pure virtual functions:
*
* virtual void say_something() = 0;
*/
};
class dog : public animal
{
public:
void say_something() { cout << "Barf, barf..." << endl; }
};
class cat : public animal
{
public:
void say_something() { cout << "Meow, meow..." << endl; }
};
int main()
{
animal *a1 = new dog();
animal *a2 = new cat();
a1->say_something();
a2->say_something();
}
Javaを使用する場合、仮想メンバー関数と非仮想メンバー関数の概念がわかりにくい場合があります。覚えておくべきことは、JavaメソッドはC++の仮想メンバー関数に対応しているということです。
問題は、なぜ実際に仮想関数があるのかということではなく、なぜ非仮想関数があるのかということです。私がそれらを自分自身に正当化する方法(私が間違っている場合は私を訂正してください)は、それらへの呼び出しがコンパイル時に解決できるので、それらを実装する方が安価であるということです。
仮想関数は、基底クラスの動作を設計するのに役立ちます。純粋仮想関数の基本クラスはインスタンス化できず、抽象クラスと呼ばれます。
基本クラスの仮想関数によって記述されたメソッドを実装するのは、派生クラス次第です。その後、派生クラスをインスタンス化できます (それらは存在し、メモリを占有します)。
派生クラスから派生すると、親オブジェクトで既に定義されている関数を再定義できます。オーバーライドとして既に知られているこの手法により、この子オブジェクトの動作をカスタマイズできます。
C++ をさらに学習すると、継承だけがすべてではないことがわかります。構成であり、多くの場合、より良い代替手段です。楽しむ。
古典的な例は、仮想 draw() 関数を使用してベース Shape クラスが作成されるペイント プログラムの例です。次に、それぞれの形状 (円、長方形、三角形など) をサブクラスとして作成し、それぞれが適切な方法で draw() 関数を実装し、コア ペイント プログラムは、それぞれが適切な draw( を実行する形状のリストを保持できます。 ) 関数は、基本 Shape クラスへのポインターのみが格納されている場合でも機能します。
違いは、基本クラス オブジェクトへのポインターを介して、派生クラスのメソッドを呼び出すときに使用されます。その瞬間、呼び出しているメソッドが派生クラスでオーバーライドされた場合は、基本クラスの実行が得られます。仮想の場合は、派生クラスのメソッドが実行されます。
#include <iostream>
class A{
public:
virtual void getA() { std::cout << "A in base" << std::endl;};
};
class B : public A {
public:
void getA() { std::cout << "A in derived class" << std::endl;}
};
int main(int argc, char** argv)
{
A a;
B b;
a.getA();
b.getA();
A* t = new B;
t->getA();
}
例: このプログラムではt->getA()
print"A in derived class"
が表示されますが、基本クラス A に仮想修飾子がない場合は、 が表示されます"A in base"
。
それが役に立てば幸い。
ヘリコプターと飛行機はどちらも飛行しますが、その方法は異なります。どちらも架空のオブジェクト Flyer のインスタンスです。Flyer オブジェクトに「飛ぶ」ように指示することはできますが、Flyer は単なるインターフェースにすぎません。飛ぶことができる必要があるということ以外は、飛行について何も知りません。
ただし、ヘリコプターと飛行機の両方がフライヤーのインターフェースに従っている場合は、飛行場オブジェクトがあり、それにフライヤーを指定した場合よりも、飛行場が行う必要があるのは、フライヤーに飛行を要求することだけです。
例えば:
Airplace X=Airplane X("boeing 747");
Airfield::takeoff(&X);
Helicopter Y= Helicopter("Comache");
Airfield::takeof(&Y);
void Airfield::takeOff(Flyer * f)
{
f->fly();
}
C++ は厳密なタイプ セーフ言語であり、この種の機能 (基底クラスを介して間接的に派生クラスへの関数呼び出しを行うこと) は、オブジェクト階層に対して RTTI が有効であり、メンバー関数 virtual を修飾することでこれが有効になっている場合にのみ可能です。