仮想関数は動的バインディングの基礎です。仮想関数は非静的メンバー関数である必要があります。仮想関数が導出された後、実行中のプロセスの多態性をクラス ファミリ内で実現できます。
派生クラスのオブジェクトは、型互換性ルールに従って、基本クラスのオブジェクトの代わりに使用できます。基底クラス型のポインタが派生クラスのオブジェクトを指している場合、このポインタを介してオブジェクトにアクセスできますが、アクセスされるのは基底クラスから継承した同名の関数メンバのみです。基本クラスのポインターを介して派生クラスのオブジェクトを指し、基本クラスと同じ名前のメンバーにアクセスする必要がある場合は、最初に同じ名前の関数を基本クラスの仮想関数として宣言します。このようにして、基本クラス型のポインタを通じて、異なる派生クラスに属する異なるオブジェクトが異なる動作を持つことができるため、実行中のプロセスの多態性が実現されます。
1. 一般的な仮想関数メンバー
(1) 一般的な仮想関数メンバーの宣言構文は次のとおりです。
virtual 函数类型 函数名(参数表);
これは、実際には、クラス定義内のvirtual
メンバー関数を制限するキーワードを使用しています。仮想関数宣言は、メンバー関数の実装時ではなく、クラス定義内の関数プロトタイプ宣言にのみ出現できます。
動作中のポリモーフィズムは、次の 3 つの条件を満たす必要があります。
(1) クラス間の型互換性ルールを満たす必要がある
(2) 仮想関数が宣言されている必要がある
(3) 仮想関数はメンバー関数によって呼び出されるか、ポインタと参照を介してアクセスされる必要がある
[注意] 仮想関数の呼び出しには動的バインディングが必要であり、インライン関数の処理は静的であるため、仮想関数は通常インライン関数として宣言されません。そのため、仮想関数は通常インライン関数として処理できません。ただし、仮想関数をインライン関数として宣言しても、コンパイラは自動的に無視するため、エラーは発生しません。
(2) 通常関数メンバと仮想関数メンバの比較
① 通常の関数メンバー
#include<iostream>
using namespace std;
class A//基类A定义
{
public:
void display()const//声明基类A中的成员函数为普通函数
{
cout << "显示类A" << endl;
}
};
class B :public A//公有派生类B定义
{
public:
void display()const
{
cout << "显示类B" << endl;
}
};
class C :public B//公有派生类C定义
{
public:
void display()const
{
cout << "显示类C" << endl;
}
};
void fun(A* p)//参数为指向基类A的对象的指针
{
p->display();//"对象指针->成员名"
}
int main()
{
A a;//定义基类对象A
B b;//定义直接基类为A类的派生类B的对象
C c;//定义直接基类为B类的派生类C的对象
fun(&a);//用基类A的对象的指针调用fun函数
fun(&b);//用直接基类为A类的派生类B对象的指针调用fun函数
fun(&c);//用直接基类为B类的派生类C对象的指针调用fun函数
return 0;
}
実行結果:
分析:
上記プログラムでは、基底クラス A のポインタは派生クラス B と C のオブジェクトを指していますが、 fun 関数を実行すると、関数から派生クラス B と C へのポインタのみが継承されます。このポインターを介して基本クラス A にアクセスできます。派生クラス B および C 自体の同じ名前の関数表示の代わりに、 のメンバー関数が表示されます。
②仮想機能メンバー
class A//基类A定义
{
public:
virtual void display()const//声明基类A中的成员函数为虚函数
{
cout << "显示类A" << endl;
}
};
class B :public A//公有派生类B定义
{
public:
void display()const//覆盖基类的虚函数
{
cout << "显示类B" << endl;
}
};
class C :public B//公有派生类C定义
{
public:
void display()const//覆盖基类的虚函数
{
cout << "显示类C" << endl;
}
};
void fun(A* p)//参数为指向基类A的对象的指针
{
p->display();//"对象指针->成员名"
}
int main()
{
A a;//定义基类对象A
B b;//定义直接基类为A类的派生类B的对象
C c;//定义直接基类为B类的派生类C的对象
fun(&a);//用基类A的对象的指针调用fun函数
fun(&b);//用直接基类为A类的派生类B对象的指针调用fun函数
fun(&c);//用直接基类为B类的派生类C对象的指针调用fun函数
return 0;
}
実行結果:
分析:
プログラム内の A、B、C は同じクラス ファミリに属し、パブリックから派生しているため、型互換性規則を満たします。同時に、基底クラス A の関数メンバが仮想関数として宣言され、プログラム内で関数メンバにアクセスするためにオブジェクト ポインタが使用されます。このようにして、動作中にバインディング処理が完了し、動作中の多態性が実現される。ポイントされているオブジェクトのメンバは、基本クラス型のポインタを介してアクセスできるため、同じクラス ファミリ内のオブジェクトを均一に処理でき、抽象プログラムがより高度になり、プログラムがより簡潔で効率的になります。
このプログラムでは、派生クラスでは明示的に仮想関数を宣言していませんが、このときシステムは関数メンバが仮想関数であるかどうかを以下の規則に従って判定します: (1) 関数の名前が同じかどうか (
2
)基底クラスの仮想関数と同じ引数の数、対応する引数の型が同じか
(3) 基底クラスの仮想関数と同じ戻り値、または代入互換性を満たすポインタを持つ関数かルール、参照戻り値
派生クラスの関数は、名前、パラメータ、戻り値の3つの観点からチェックされ、上記の条件を満たしていれば仮想関数として自動的に判定されます。このとき、派生クラスの仮想関数は、クラスの仮想関数を網羅することになる。それだけでなく、派生クラスの仮想関数は、基本クラス内の同じ名前の関数の他のオーバーロードされた形式をすべて隠します。
【注意事項】
①基底クラス内の派生クラスがカバーするメンバ関数は、派生クラスのオブジェクトへのポインタで呼び出すことができ、メソッドは「::」で制限されます。たとえば、上記の例の fun 関数が次の形式に変更された場合、他の部分は変更されません。
void fun(A* p)//参数为指向基类A的对象的指针
{
p->A::display();//"对象指针->成员名"
}
実行結果:
"::" を使って定義した後は、p が指すオブジェクトの多態性型に関係なく、最後には必ずクラス A の表示関数が呼び出されることになります。派生クラスの関数では、最初に基本クラスのオーバーライドされた関数を呼び出してから、派生クラスの特定の操作を実行する必要がある場合があります。このとき、「基本クラス名::関数名()」を使用できます。 ...)" を使用して、基本クラスのオーバーライドされた関数を呼び出します。
②派生クラスが基底クラスのメンバ関数をオーバーライドする場合、virtualキーワードの有無に違いはありません。基底クラスでメンバ関数が仮想関数として宣言されていれば、派生クラスで同じ名前のメンバ関数を仮想関数として宣言する必要はありません。場合によっては、これが仮想関数であることを明確に示すことができるため、派生クラス関数で virtual キーワードを使用するのが慣例となります。
(3) 基本クラスのコンストラクタとデストラクタが仮想関数を呼び出す
① 基底クラスのコンストラクタが仮想関数を呼び出す場合、派生クラスの仮想関数は呼び出されません。
基底クラス A と派生クラス B があり、その 2 つのクラスに仮想メンバー関数 fun() があるとします。派生クラス B のコンストラクターを実行する場合、最初に基底クラス A のコンストラクターを呼び出す必要があります。 。A::A() が仮想関数 fun() を呼び出す場合、B::fun() の代わりに A::fun() が呼び出されます。これは、基本クラスが構築された時点ではオブジェクトがまだ派生クラス オブジェクトではないためです。
また、基本クラスが破棄されると、オブジェクトは派生クラス オブジェクトではなくなるため、A::~A() が fun() を呼び出すと、B::fun() の代わりに A::fun() が呼び出されます。
class A//基类A定义
{
public:
A()
{
fun();
cout << "调用基类A的默认构造函数" << endl;
}
A(int a):x(a)
{
fun();
cout << "调用基类A的构造函数" << endl;
}
virtual void fun()const//声明基类A中的成员函数为虚函数
{
cout << "显示类A" << endl;
}
~A()
{
cout << endl;
fun();
cout << "调用基类A的析构函数" << endl;
}
private:
int x;
};
class B :public A//公有派生类B定义
{
public:
B(){
}
B(int b) :y(b)
{
cout << "调用派生类B的构造函数" << endl;
}
virtual void fun()const//覆盖基类的虚函数
{
cout << "显示类B" << endl;
}
~B()
{
cout << endl;
fun();
cout << "调用派生类B的析构函数" << endl;
}
private:
int y;
};
int main()
{
A a(5);
cout << endl;
B b(3);
return 0;
}
実行結果:
分析:
main 関数では、基底クラス A のオブジェクト a が定義されて初期化され、初期化中に基底クラス A のコンストラクターが呼び出され、基底クラス A のコンストラクター内で仮想関数 fun() が呼び出されますが、基本クラス A と派生クラス B には仮想関数 fun() がありますが、基本クラス A のコンストラクターで呼び出される fun() 関数は、派生クラスの fun() 関数ではなく、基本クラス A の fun 関数です。クラスB。派生クラス オブジェクト b が定義され、初期化されます。派生クラス オブジェクト b を初期化するとき、最初に基底クラス A のデフォルト コンストラクターが呼び出され、次に B クラスのコンストラクターが呼び出されて b オブジェクトが初期化されます。 A クラスの仮想関数 fun が呼び出されます。A 仮想関数 fun は、クラスの既定のコンストラクターで呼び出されます。ここで呼び出される仮想関数 fun は、やはりクラス B の仮想関数 fun ではなく、クラス A の仮想関数 fun です。
これは、基底クラス A が構築された時点では、オブジェクトはまだ派生クラス オブジェクトではないためです。
② 多態性バインドされるのは仮想関数のみであり、派生クラスが基底クラスの動作を変更する必要がある場合 (つまり、基底クラスの関数と同じ名前の関数を書き換える場合)、派生クラスは対応する関数を仮想関数として宣言する必要があります。基本クラス。基本クラスで宣言された非仮想関数は、通常、派生クラスによって変更されたくない関数を表します。つまり、ポリモーフィズムを実現できません。一般に、継承された非仮想関数を書き換えないでください。基底クラスのポインターと派生クラスのポインターを介して同じ名前の関数を呼び出すと、異なる結果が生じ、混乱が生じるためです。
[注意] 継承した仮想関数を書き換える場合、関数のパラメータ値がデフォルトの場合は、別の値を再定義しないでください。仮想関数は多態的にバインドされていますが、デフォルトのパラメータは静的にバインドされているためです。つまり、派生クラス オブジェクトを指す基本クラス ポインターを通じて、派生クラスの仮想関数にアクセスできますが、デフォルトのパラメーター値は基本クラス定義からのみ取得できます。例えば:
class A//基类A定义
{
public:
virtual void display()const//声明基类A中的成员函数为虚函数
{
cout << "显示类A" << endl;
}
};
class B :public A//公有派生类B定义
{
public:
virtual void display()const//覆盖基类的虚函数
{
cout << "显示类B" << endl;
}
};
class C :public B//公有派生类C定义
{
public:
virtual void display()const//覆盖基类的虚函数
{
cout << "显示类C" << endl;
}
};
void fun(A* p)//参数为指向基类A的对象的指针
{
p->A::display();//"对象指针->成员名"
}
int main()
{
C c;//定义派生类对象
A* p = &c;//基类指针p可以指向派生类对象
A& r = c;//基类引用r可以作为派生类对象的别名
A a = c;//调用基类A的拷贝构造函数用c构造a,a的类型是A而非C
return 0;
}
ここでは、A a = c;
型 C のオブジェクト c を使用して型 A のオブジェクト a を初期化し、型 A のコピー コンストラクターを初期化に使用します。コピー コンストラクターは型 A の定数参照を受け取るため、型 C の c は型互換性規則に準拠し、パラメーターとして渡すことができます。クラス A のコピー コンストラクターが実行されるため、型 A のメンバーのみがコピーされ、クラス C の新しく追加されたデータ メンバーはコピーされず、それらを格納するスペースがないため、生成されるオブジェクトはオブジェクトになります。基本クラスAの。派生クラス オブジェクトをコピーして基本クラス オブジェクトを構築するこの動作は、オブジェクト スライスと呼ばれます。このとき、aを使って基底クラスAの仮想関数を呼び出した場合、呼び出しの対象となるオブジェクトはオブジェクトスライス後のA型オブジェクトとなり、C型のcオブジェクトとは関係がありません。オブジェクトの型は非常に明確なので、ポリモーフィック バインディングの必要はありません。