c++ - メソッドテーブル - 仮想関数テーブル 破壊
C++で仮想関数が必要なのはなぜですか? (15)
私はC ++を学んでいます。仮想関数に取り掛かりつつあります。
私が読んだこと(本とオンラインで)から、仮想関数は、派生クラスでオーバーライドできる基本クラスの関数です。
しかし、基本的な継承について学ぶとき、本の前半では、 virtual
使わずに派生クラスの基底関数をオーバーライドすることができました。
では、私はここで何が欠けていますか? 私はバーチャルファンクションがもっとあることを知っています。それは重要であるようですので、正確に何かを明確にしたいと思います。 私はまっすぐ答えをオンラインで見つけることができません。
なぜ仮想メソッドがC ++で必要なのでしょうか?
素早い回答:
- これは、 オブジェクト指向プログラミングのために必要な「成分」 1の1つを提供します 。
Bjarne Stroustrup C ++プログラミング:原則と実践、(14.3):
仮想関数は、基本クラスに関数を定義し、ユーザーが基本クラス関数を呼び出すときに呼び出される派生クラスに同じ名前と型の関数を持つことができます。 呼び出される関数は、実行時に、使用されるオブジェクトのタイプに基づいて決定されるため、 ランタイム ポリモーフィズム 、 動的ディスパッチ 、またはランタイムディスパッチと呼ばれることがあります。
- 仮想関数呼び出しが必要な場合は、最も効率的な実装が最も高速です2 。
仮想呼を処理するためには、 派生オブジェクト 3に関する1つまたは複数のデータが必要である。 通常行われる方法は、関数のテーブルのアドレスを追加することです。 このテーブルは通常仮想テーブルまたは仮想関数テーブルと呼ばれ、そのアドレスはしばしば仮想ポインタと呼ばれます 。 各仮想関数は、仮想テーブル内のスロットを取得します。 呼び出し側のオブジェクト(派生)型に応じて、仮想関数はそれぞれのオーバーライドを呼び出します。
1. オブジェクト指向プログラミングの最も一般的な定義は、継承、実行時多型、およびカプセル化の使用です。
2.ランタイムに選択肢の中から選択するために、他の言語機能を使用して、より速くなるように機能をコード化したり、メモリを少なくしたりすることはできません。 Bjarne Stroustrup C ++プログラミング:原則と実践(14.3.1) 。
3.仮想関数を含む基本クラスを呼び出すときに実際にどの関数が呼び出されるかを示すためのもの。
「バーチャル」がなければ、「早期拘束」が得られます。 使用されるメソッドの実装は、呼び出したポインタの型に基づいてコンパイル時に決定されます。
「バーチャル」では、「レイトバインディング」を取得します。 このメソッドのどの実装が使用されるかは、実行時にpointed-toオブジェクトのタイプに基づいて決定されます。 これは必ずしもそのオブジェクトを指すポインタのタイプに基づいて考えるとは限りません。
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 ++での早期バインディングと遅延バインディングについて説明します。
virtualキーワードは、コンパイラにポインタのクラスではなくオブジェクトのクラスで定義されたメソッド実装を選択させる。
Shape *shape = new Triangle();
cout << shape->getName();
上の例では、getName()がBaseクラスのShapeでvirtualとして定義されていない限り、Shape :: getNameがデフォルトで呼び出されます。 これにより、コンパイラはShapeクラスではなくTriangleクラスのgetName()実装を検索します。
仮想テーブルは、コンパイラがサブクラスのさまざまな仮想メソッドの実装を追跡するメカニズムです。 これは動的ディスパッチとも呼ばれ、それに関連するオーバーヘッドがあります。
最後に、C ++でバーチャルが必要なのはなぜですか?なぜJavaのようなデフォルトの動作にしませんか?
- C ++は、 "ゼロオーバーヘッド"と "あなたが使うものを支払う"の原則に基づいています。 したがって、必要な場合を除いて、動的ディスパッチを実行しようとはしません。
- インタフェースをより詳細に制御する。 関数を非仮想にすることで、インタフェース/抽象クラスはすべての実装における動作を制御できます。
安全なダウンキャスティング 、 シンプルさと簡潔 さのための仮想メソッドが必要です。
これはバーチャルメソッドが行うことです。明らかにシンプルで簡潔なコードを使用して安全にダウンキャストし、安全でない手動キャストを避けるためには、複雑で冗長なコードを使用します。
非仮想メソッド⇒静的バインディング
次のコードは意図的に「間違っています」。 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;
}
仮想メソッドは仮想的に呼び出されるので、出力は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;
}
これを見ている一つの肯定的な方法は、安全でないダウンキャスティング、複雑さと冗長さが上記のように発生した場合、しばしばバーチャルな方法または方法が本当に助けになることです。
ここでは最初の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では、実行時にバインドされます。犬* d、動物* adとfunc(動物*)のポイント/犬の言う()メソッドを参照してください。[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
興味深いのは、Pythonのすべてのクラス属性(データとメソッド)が事実上仮想的であることです。すべてのオブジェクトは実行時に動的に作成されるため、タイプ宣言やキーワード仮想の必要はありません。以下は、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インスタンスを参照/指している2つの異なるポインタ変数であることに注意してください。式(adはd)はTrueを返し、その値は同じ< main .Dogオブジェクト0xb79f72cc>です。
それを実証するには、少なくとも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
cout << d->Says(); // always Woof
cout << a->Says(); // Woof or ?, depends on virtual
}
インタフェース設計では仮想メソッドが使用されます。 たとえば、Windowsでは、以下のようなIUnknownというインターフェースがあります。
interface IUnknown {
virtual HRESULT QueryInterface (REFIID riid, void **ppvObject) = 0;
virtual ULONG AddRef () = 0;
virtual ULONG Release () = 0;
};
これらのメソッドは、実装するインタフェースユーザに委ねられています。 IUnknownを継承しなければならない特定のオブジェクトの作成と破壊には不可欠です。 この場合、ランタイムは3つのメソッドを認識しており、それらを呼び出すときにそれらが実装されることを期待しています。 つまり、ある意味では、オブジェクト自体とそのオブジェクトを使用するものとの間の契約として機能します。
オーバーライドとオーバーロードを区別する必要があります。 virtual
キーワードがなければ、基本クラスのメソッドだけがオーバーロードされます。 これは、隠れること以外何も意味しません。 void foo()
実装する基本クラスBase
と派生クラスSpecialized
あるとしましょう。 これで、 Base
へのポインターがSpecialized
インスタンスを指しています。 foo()
を呼び出すと、 virtual
の違いを見ることができます。メソッドがバーチャルの場合、 Specialized
の実装が使用されます。もし見つからない場合、 Base
のバージョンが選択されます。 基本クラスからメソッドをオーバーロードしないことがベストプラクティスです。 メソッドを非仮想にすることは、作成者がサブクラスでのその拡張が意図されていないことを伝える方法です。
上記の答えと同じコンセプトを使用していますが、私はVirtual関数の別の使い方を追加したいと思いますが、私はその価値について言及しています。
仮想荒野
基本クラスのデストラクタを仮想として宣言することなく、以下のプログラムを検討してください。 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
仮想関数の必要性が説明されている[分かりやすい]
#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%以内)とほぼ同じくらい効率的に行うことができます。その空間オーバーヘッドは、仮想関数を持つクラスの各オブジェクトに1つのポインタとそのクラスごとに1つのvtblです。 Bjarne StroustrupによるC ++のツアー ]
基本クラスがBase
で、派生クラスがDer
場合、実際にDer
インスタンスを指すBase *p
ポインタを持つことができます。 p->foo();
を呼び出すとp->foo();
もしfoo
が仮想でないなら 、 p
実際のバージョンをDer
指しているという事実を無視して、 Base
のバージョンが実行されます。 foo が仮想であれば、 p->foo()
はp->foo()
の "leafmost"オーバーライドを実行します。これは、ポイント先の項目の実際のクラスを完全に考慮したものです。 したがって、仮想と非仮想の違いは実際には非常に重要です。前者はOOプログラミングのコアコンセプトである実行時polymorphism許しますが、後者はそうではありません。
根本的な仕組みが分かっていれば役立ちます。 C ++はCプログラマが使用するいくつかのコーディング技法を形式化し、 "クラス"は "オーバーレイ"を使用して置き換えられます - 共通のヘッダーセクションを持つ構造体は、異なるタイプのオブジェクトを処理するために使用されます。 通常、オーバーレイの基本構造体(共通部分)には、各オブジェクト型ごとに異なるルーチンセットを指す関数テーブルへのポインタがあります。 C ++は同じことをしますが、C ++ (*ptr->func_table[func_num])(ptr,...)
ようにfuncが仮想であるC ++ ptr->func(...)
メカニズムを隠します。派生クラス間はfunc_tableの内容です。 [非仮想メソッドptr-> func()はmangled_func(ptr、..)に変換されます。]
それは、派生クラスのメソッドを呼び出すために基本クラスを理解する必要があるだけです。すなわち、ルーチンがクラスAを理解している場合は、派生クラスBポインタを渡すことができます。次に呼び出される仮想メソッドは、あなたは関数テーブルBを指すので、AではなくBを指します。
私はあなたがオーバーライドで '仮想'キーワードを使用する必要はありません仮想メソッドが宣言された事実を参照していると思います。
class Base { virtual void foo(); };
class Derived : Base
{
void foo(); // this is overriding Base::foo
};
Baseのfoo宣言で 'virtual'を使用しないと、Derivedのfooは単にそれをシャドーイングします。
私たちは "実行時の多態性"をサポートするための仮想メソッドが必要です。ポインタまたは基本クラスへの参照を使用して派生クラスオブジェクトを参照する場合、そのオブジェクトに対して仮想関数を呼び出して、派生クラスの関数バージョンを実行できます。