基底クラス コンストラクタの仮想メソッドの呼び出し
C++ モデルと Object Pascal モデル への移動
Delphi 形式の基底クラスのコンストラクタ本体から呼び出された仮想メソッド、つまり Object Pascal に実装されたクラスは、C++ の場合と同様に、オブジェクトの実行時型に従って、ディスパッチされます。 C++Builder ではオブジェクトの実行時型を即座に設定する Object Pascal モデルが、派生クラスが構築される前に、基底クラスを構築する C++ モデルと結合されるので、Delphi スタイルのクラスに対する基底クラスのコンストラクタから仮想メソッドを呼び出すと、軽微な副作用が発生することがあります。 この副作用の影響を次に説明し、少なくとも 1 つの基底クラスから派生するインスタンス化クラスの例を示します。 この説明では、インスタンス化クラスは派生クラスとも呼びます。
目次
Object Pascal モデル
Object Pascal では、プログラマは inherited キーワードを使用できます。これにより、派生クラスのコンストラクタ本体(内の任意の場所)から基底クラスのコンストラクタを柔軟に呼び出すことができます。結果として、オブジェクトのセットアップ(データ メンバの初期化)により決まる任意の仮想メソッドを派生クラスがオーバーライドする場合は、基底クラスのコンストラクタが呼び出され、仮想メソッドが呼び出される前にこれが発生することがあります。
C++ モデル
C++ 構文には、派生クラスの構築中(の任意の時点)に基底クラスのコンストラクタを呼び出すための inherited キーワードがありません。 C++ モデルでは、オブジェクトの実行時型は派生クラスではなく、構築中の現在のクラスの型であるから inherited の使用は必要ありません。 したがって、呼び出される仮想メソッドは、派生クラスではなく、現在のクラスのメソッドです。 結果として、これらのメソッドを呼び出す前に、データ メンバの初期化(派生クラス オブジェクトのセットアップ)が必要ありません。
C++Builder モデル
C++Builder の Delphi 形式のオブジェクトには、基底クラスのコンストラクタに対するすべての呼び出し中に、派生クラスの実行時型があります。 したがって、基底クラスのコンストラクタが仮想メソッドを呼び出す場合は、派生クラスがオーバーライドすると、派生クラスのメソッドが呼び出されます。 この仮想メソッドが、初期化リスト内の対象または派生クラスのコンストラクタ本体に依存している場合は、これが発生する前にメソッドが呼び出されます。 たとえば、CreateParams が、TWinControl のコンストラクタで間接的に呼び出される仮想メンバ関数です。 TWinControl からクラスを派生し、コンストラクタにある対象に依存するように CreateParams をオーバーライドする場合は、このコードは CreateParams が呼び出された後に処理されます。 この状況が該当するのは基底クラスの任意の派生クラスです。 クラス C が B から派生し、B が A から派生している場合を考えてみましょう。 B がメソッドをオーバーライドするが、C がしない場合に、C、A のインスタンスを作成すると、B のオーバーライド メソッドも呼び出されます。
コンストラクタで明示的には呼び出されず、間接的に呼び出される CreateParams のような仮想メソッドに注意してください。
例:仮想メソッドの呼び出し
次の例では、仮想メソッドをオーバーライドする C++ クラスと Delphi 形式のクラスを比較します。この例で示すのは、両方の場合に基底クラスのコンストラクタからの仮想メソッド呼び出しを解決する方法です。MyBase と MyDerived は標準 C++ クラスです。MyRTLBase と MyRTLDerived は TObject から派生する Delphi 形式のクラスです。仮想メソッド what_am_I() は両方の派生クラスでオーバーライドされるが、派生クラスのコンストラクタ内ではなく、基底クラスのコンストラクタ内のみで呼び出されます。
# include <sysutils.hpp >
# include <iostream.h >
# include <stdio.h>
// 非 Delphi 形式のクラス
class MyBase {
public:
MyBase() {
what_am_I();
}
virtual void what_am_I() {
cout << "I am a base" << endl;
}
};
class MyDerived : public MyBase {
public:
virtual void what_am_I() {
cout << "I am a derived" << endl;
}
};
// Delphi 形式のクラス
class MyRTLBase : public TObject {
public:
__fastcall MyRTLBase() {
what_am_I();
}
virtual void __fastcall what_am_I() {
cout << "I am a base" << endl;
}
};
class MyRTLDerived : public MyRTLBase {
public:
virtual void __fastcall what_am_I() {
cout << "I am a derived" << endl;
}
};
int main(void) {
MyDerived d; // instantiation of the C++ class
MyRTLDerived *pvd = new MyRTLDerived; // instantiation of the Delphi style class
getchar();
return 0;
}
この例の出力は次のとおりです。
I am a base
I am a derived
各基底クラスのコンストラクタへの呼び出し中に MyDerived と MyRTLDerived の実行時型に差分があるためです。
仮想関数に対するデータ メンバのコンストラクタの初期化
データ メンバは仮想関数で使用できるので、初期化時期と方法を理解することは重要です。 Object Pascal で、すべての初期化されていないデータはゼロで初期化されます。 たとえば、これが該当するのはコンストラクタが派生から呼び出されない基底クラスです。 標準 C++ では、初期化されていないデータ メンバの値は保証されません。 クラス データ メンバの次の型は、クラスのコンストラクタの初期化リストで初期化される必要があります。
- 参照
- デフォルト コンストラクタがないデータ メンバ
ただし、これらのデータ メンバの値(コンストラクタ本体で初期化された値)は、基底クラスのコンストラクタが呼びされたときに未定義です。 C++Builder では、Delphi 形式のクラスのメモリはゼロで初期化されます。
コンストラクタ本体または初期化リストで初期化されたメンバ変数の値に依存する仮想関数は、変数がゼロで初期化されたように動作します。 これは、基底クラスのコンストラクタが呼び出されてから、初期化リストが処理されるか、コンストラクタ本体に入るからです。 次の例はこれを示しています。
# include <sysutils.hpp>
class Base : public TObject {
public:
__fastcall Base() {
init();
}
virtual void __fastcall init() {
}
};
class Derived : public Base {
public:
Derived(int nz) : not_zero(nz) {
}
virtual void __fastcall init() {
if (not_zero == 0)
throw Exception("not_zero is zero!");
}
private:
int not_zero;
};
int main(void)
{
Derived *d42 = new Derived(42);
return 0;
}
この例では、Base のコンストラクタで例外が送出されます。 Base が Derived の前に構築されるので、not_zero は、コンストラクタに渡される値 42 で初期化されていません。 基底クラスのコンストラクタが呼び出される前に、ユーザーの Delphi 形式のクラスのデータ メンバを初期化できないことに注意してください。