オブジェクト指向プログラミング

オブジェクト指向プログラミング

オブジェクト指向プログラミング(OOP)は、データと動作が密接に繋がっていながら、主に、データに焦点を当てたプログラミングです。オブジェクトがクラスインスタンスでありながら、クラスはデータと操作演算の両方によって構成されます。

オブジェクト指向のアプローチのコンポーネントは次の通りです。

  • 型の封裝と拡張
  • 継承
  • ポリモーフィズム
  • 多重定義(オーバーロード)
  • 仮想関数

OOP は演算を操作演算のモデル化とみなします。モデル化されているのは、計算抽象化によって表されるオブジェクトです。よく知られているゲーム「テトリス」を書きたいとしましょう。これを行うためには、4 つの正方形で構成されて辺で接合されたランダムな図形をモデル化する方法を学ぶ必要があります。また図形の落下速度を調整し、図形の回転と移動の動作を定義する必要があります。画面上の図形の移動は井戸の境界によって限られており、この要件もモデル化されなければなりません。それに加えて、正方形のそろった行を破棄して得点を数える必要があります。

このように、この理解しやすいゲームは、図形モデル、井戸モデル、図形運動モデルといったいくつかのモデル作成を要します。これらの全てのモデルは抽象で、コンピュータでの計算で示されます。これらのモデルを説明するためには抽象データ型、ADT(またはコンプレックスデータ型)の概念が使用されます。厳密に言うと、DOM 内の「図形」運動のモデルはデータ型ではなく、「井戸」データ型の制限を使用した「図形」のデータ型に対する操作のセットです。

オブジェクトは クラス 変数です。オブジェクト指向プログラミングは、ADTの作成と使用を簡単にします。オブジェクト指向プログラミングは、継承メカニズムを使用します。継承の利点は、すでにユーザによって定義されたデータ型から派生型を得ることが可能になることです。

例えば、テトリス図形を作成するには、まず Shape 基本クラスを作成すると便利です。全ての 7 つの可能な図形の型を表現する他のクラスは、その基礎の上に作られます。図形の動作は基本クラスで定義され、各別個の図形の動作は派生クラスで実装されます。

OOPでは、オブジェクト自身がそれぞれの動作に責任を持ちます。ADTの開発者は、対応するオブジェクトから通常予測される動作を説明するためのコードを含むべきです。オブジェクト自体がその動作に責任を持つという事実は、このオブジェクトのプログラミング作業を非常に簡素化します。

画面上に図形を描画したい場合、どこが中心になってそれをどのように描画するかを知る必要があります。別々の図形がそれ自身を描画する方法を知っていれば、プログラマはこのような図形を使用した場合、「draw」メッセージを送信出来ます。

MQL5 言語は C++ に似ており ADT 実施のためにカプセル化メカニズムを備えています。カプセル化は特定の型の実装の内部の詳細を結合する一方、この型のオブジェクトに影響を与えることが出来る外部からアクセス可能の関数を組み合わせます。実装の詳細はこの型を使用するプログラムのからはアクセス出来ません。

 

OOP の概念は次のような関連する概念のセットで成り立ちます。

  • 現実世界アクションのシミュレーション
  • ユーザ定義のデータ型
  • 実装の詳細の隠蔽
  • 継承によるコード再利用の可能性
  • 実行時の関数呼び出しの解釈

これらの概念のいくつかはかなり曖昧、いくつかは抽象的でいくつかは一般的です。

型の封裝と拡張

OOP はソフトウェアを書くのにバランスのとれたアプローチです。データと操作演算は一体化されています。このカプセル化は、ユーザ定義のデータ型を作成し、言語データ型を拡張しそれらと相呼応します。型の拡張は言語に使いやすいユーザ定義のデータ型、ならびに基本型を追加する機会です。

例えば抽象データ型の文字列は、理想的なよく知られた操作演算の種類を描画します。

文字列のユーザは、連結やプリントなどの文字列操作が特定の動作を持っていることを知っています。このような操作はメソッドと呼ばれます。

ADT の特定の実装は、例えば、文字列の長さの制限などを有することがあります。これらの制限は全ての操作演算に影響を与えます。同時に、内部またはプライベート実装の詳細はユーザがオブジェクトを見る方法には直接影響しません。例えば、文字列は多くの場合配列として実装されていて、配列の内部ベースアドレスや名称はユーザには必要でありません。

カプセル化はユーザ定義型へのオープンインタフェースが提供された時に実装の詳細を隠す機能です。MQL5 言語ではクラスと構造体の定義(classと struct )は privateprotected 及び public のアクセスキーワードとともにカプセル化の規定に使用されています。

public キーワードは、キーワードの使用されたメンバへのアクセスが制限なくオープンなことを示します。このキーワードが指定されなかった場合、クラスメンバは、デフォルトでロックされています。private メンバは、そのクラスのメンバ関数のみからアクセス出来ます。

Protected クラス関数はこのクラスでだけでなくその継承クラスからも利用可能です。Public クラス関数は、クラス宣言の有効範囲内の全ての関数から利用可能です。この様な保護は、クラスの実装の一部を隠してデータの構造の予期しない変更を防止します。アクセス制限やデータ隠蔽はオブジェクト指向プログラミングの特徴です。

通常、クラス関数は保護されており protected 修飾子を使用して宣言されます。値の読み書きは public 修飾子を用いて定義された特殊ないわゆるセットメソッドとゲットメソッドを用いて行われます。

 

例:

class CPerson
{
protected:
string            m_name;                     // 名称
public:
void              SetName(string n){m_name=n;}// 名称を付ける
string            GetName(){return (m_name);} // 名称を返す
};

 

このアプローチにはいくつかの利点があります。まず、関数名で関数の機能がわかります。つまりクラスメンバの値をセットまたはゲットするものです。次に、将来的に CPerson クラスまたはその派生クラスのいずれかで m_name 変数の型を変更する必要があるかもしれません。

この場合必要なのは SetName() と GetName() 関数の実装の変更だけです。 m_name のデータ型の変更はユーザに知られる余地もなく、CPerson クラスのオブジェクトはコード変更なしでプログラムで使用できます。

例:

struct Name
{
string            first_name;                 // 名
string            last_name;                 // 姓
};

class CPerson
{
protected:
Name              m_name;                     // 名前
public:
void              SetName(string n);
string            GetName(){return(m_name.first_name+” “+m_name.last_name);}
private:
string            GetFirstName(string full_name);
string            GetLastName(string full_name);
};

void CPerson::SetName(string n)
{
m_name.first_name=GetFirstName(n);
m_name.last_name=GetLastName(n);
}

string CPerson::GetFirstName(string full_name)
{
int pos=StringFind(full_name,” “);
if(pos>0) StringSetCharacter(full_name,pos,0);
return(full_name);
}

string CPerson::GetLastName(string full_name)
{
string ret_string;
int pos=StringFind(full_name,” “);
if(pos>0) ret_string=StringSubstr(full_name,pos+1);
else      ret_string=full_name;
return(ret_string);
}

継承

継承によるコードの再利用の促進は OOP の特徴です。新しいクラスは、基本クラスと呼ばれる既存するクラスから作られます。派生クラスは基本クラスのメンバを使用するだけでなくそれらを変更したり補完したりすることが出来ます。

多くの型は既存の型のバリエーションです。多くの場合、各バリエーションのために新しいコードを開発することは面倒です。また、新しいコードは新しいエラーを意味します。派生クラスは基本クラスの記述を継承するのでコードの再開発と再テストは不要です。継承関係は階層構造を持ちます。

階層構造は、要素の全ての多様性と複雑さを複製することを可能にします。それはオブジェクトの分類を紹介します。例えば、元素表はガスを含みます。ガスは全ての元素に固有の特性を保有します。

不活性ガスは次に重要なサブクラスを構成します。ここでみられる階層は、アルゴン等の不活性ガスがガスで、ガスはひいてはシステムの一部であるということです。このような階層を使用すれば、不活性ガスの挙動が簡単に解釈出来ます。不活性ガスは他の全ての要素と同様に原子に陽子と電子があることがわかります。

不活性ガスは全てのガスのように常温で気体状態であることもわかります。不活性ガスのサブクラスに属するガスは全ての不活性ガスの特性であるように他の元素との通常の化学反応に入らないこともわかります。

幾何学的図形の継承の例を考えてみましょう。数々の単純な図形を記述するのに(円形、三角形、四角形、正方形等)一番良い方法は、全ての派生クラスの祖先である基本クラスを作成することです( ADT )。

図形を記述する最も一般的なメンバを含む基本クラス CShape を作成してみましょう。これらのメンバは図形の特徴であるプロパティ(図形の種類とメインアンカーポイントの座標)を記述します。

例:

//— 基本クラス Shape
class CShape
{
protected:
int       m_type;                   // 図形の種類
int       m_xpos;                   // 基点の X 座標
int       m_ypos;                   // 基点の Y 座標
public:
CShape(){m_type=0; m_xpos=0; m_ypos=0;} // コンストラクタ
void      SetXPos(int x){m_xpos=x;} // X を設定
void      SetYPos(int y){m_ypos=y;} // Y を設定
};

次に、基本クラスから派生した新しいクラスを作成し、それぞれのクラスを指定するのに必要なフィールドを追加します。円形の場合には、半径値が含まれているメンバを追加する必要があります。正方形は側面値によって特徴付けられます。従って、基本クラスの CShape から継承された派生クラスは次のように宣言されます。

//— 派生クラス circle
class CCircle : public CShape       // コロンの後に基本クラスを定義する
{                                   // 継承の元
private:
int             m_radius;           // 円の半径

public:
CCircle(){m_type=1;}// コンストラクタ タイプ 1
};

正方形クラスの宣言も似ています。

//— 派生クラス Square
class CSquare : public CShape       // コロンの後で基本クラスを定義する
{                                   // 継承の元
private:
int            m_square_side;       // 正方形の側面

public:
CSquare(){m_type=2;} // コンストラクタ(タイプ 2)
};

オブジェクト作成時に基本クラスのコンストラクタが最初に呼び出されその後で派生クラスのコンストラクタが呼び出されることには留意するべきです。オブジェクト破壊時には派生クラスのデストラクタが呼び出されてから基本クラスのデストラクタ が呼び出されます。

従って、最も一般的なメンバを基本クラスの中で宣言することによって、派生クラスに特定のクラスを指定するメンバを追加することが出来ます。継承は何度も再利用することが出来る力強いコードライブラリの作成を可能にします。

既存クラスから派生クラスを作成するための構文は次の通りです。

class クラス名 :
(public | protected | private) opt  基本クラス名
{
クラスメンバの宣言
};

 

派生クラスの側面の 1 つは、そのメンバの後継者の視認性(オープンネス)です。public、protected 及び private キーワードは 基本クラスのメンバが派生クラスにどの程度使用可能かを示すものです。派生クラスヘッダーのコロンの後の public キーワードは、基本クラス CShape の protected 及び public メンバが派生クラス CCircle の protected 及び public メンバとして継承される必要があることを示します。

基本クラスの private クラスメンバは、派生クラスでは使用出来ません。public 継承は、派生クラス( CCircle と CSquare )が CShapes であることを意味します。つまり、Square( CSquare )は図形( CShape )ですが、その図形は必ずしも正方形である必要はありません。

派生クラスは基本クラスのバージョン例で基本クラスの protected と public メンバを継承します。基本クラスのコンストラクタとデストラクタは継承出来ません。基本クラスのメンバに加えて、派生クラスには新しいメンバが追加されます。

派生クラスは、基本クラスとは違うメンバ関数の実装を含むことが出来ます。これは、同名の関数の意味がシグネチャの違いによって異なるものになるオーバーロードとは全く違うものです。

protected 継承では、基本クラスの public 及び protected メンバは派生クラスの protected メンバになります。private 継承では、基本クラスの public 及び protected メンバは派生クラスの private メンバになります。

protected 及び private 継承では「派生クラスのオブジェクトは基本クラスのオブジェクトである」という関係は当てはまりません。protected 及び private 継承タイプはまれであり慎重に使用される必要があります。

継承の種類( public、protected 及び private )は派生クラスが基本クラスメンバにアクセスする方法に影響を及ぼさないことは留意されるべきです。全ての継承のタイプで、public 及び protected のアクセス指定とともに宣言された基本クラスのメンバのみが派生クラスによって利用出来ます。次の例を見てみましょう。

#property copyright “Copyright 2011, MetaQuotes Software Corp.”
#property link     “https://www.mql5.com”
#property version   “1.00”
//+——————————————————————+
//| いくつかのアクセスタイプを持つクラスの例                                       |
//+——————————————————————+
class CBaseClass
{
private:             //— private メンバは派生クラスから利用出来ない
int               m_member;
protected:           //— 基本クラスの protected メソッドは派生クラスでも利用出来る
int               Member(){return(m_member);}
public:             //— クラスコンストラクタは全てのクラスメンバから利用出来る
CBaseClass(){m_member=5;return;};
private:             //— m_member に値を代入する private メソッド
void              Member(int value) { m_member=value;};

};
//+——————————————————————+
//| 間違えを含む派生クラス                                                 |
//+——————————————————————+
class CDerived: public CBaseClass // 継承は public がデフォルトであるため、public の指定は省略出来る
{
public:
void Func() // 派生クラスで、基本クラスメンバを呼び出す関数を定義する
{
//— 基本クラスの private メンバを変更する試み
m_member=0;       // エラー。基本クラスの private メンバは利用出来ない
Member(0);         // エラー。基本クラスの private メソッドは派生クラスで利用出来ない
//— 基本クラスメンバを読む
Print(m_member);   // エラー。基本クラスの private メンバは利用出来ない
Print(Member());   // 成功。基本クラスの protected メソッドは派生クラスで使用出来る
}
};

上記の例では CBaseClass の唯一の public メソッドはコンストラクタです。コンストラクタはクラスオブジェクトを作成する時に自動的に呼び出されます。従って、private メンバ m_member と protected メソッド Member() は外から呼び出すことは出来ません。しかし public 継承では、基本クラスの Member() メソッドは派生クラスから利用出来るようになります。

protected 継承では、基本クラスの public 及び protected メンバは派生クラスの protected メンバになります。これは、もし基本クラスの public データメンバとメソッドが外からアクセス出来たとすると、protected 継承ではそれらは派生クラスまた派生クラスの派生クラスなどによってのみ利用可能なことを意味します。

//+——————————————————————+
//| いくつかのアクセスタイプを持つクラスの例                                       |
//+——————————————————————+
class CBaseMathClass
{
private:             //— private メンバは派生クラスから利用出来ない
double            m_Pi;
public:             //— m_Pi 値の取得と設定
void              SetPI(double v){m_Pi=v;return;};
double            GetPI(){return m_Pi;};
public:             // クラスコンストラクタは全てのメンバから利用出来る
CBaseMathClass() {SetPI(3.14); PrintFormat(“%s”,__FUNCTION__);};
};
//+——————————————————————+
//| m_Pi が変更出来ない 派生クラス                                         |
//+——————————————————————+
class CProtectedChildClass: protected CBaseMathClass // Protected 継承
{
private:
double            m_radius;
public:             //— 派生クラスの Public メソッド
void              SetRadius(double r){m_radius=r; return;};
double            GetCircleLength(){return GetPI()*m_radius;};
};
//+——————————————————————+
//| スクリプトを開始する関数                                                 |
//+——————————————————————+
void OnStart()
{
//— 派生クラスの作成時に、基本クラスのコンストラクタは自動的に呼び出される
CProtectedChildClass pt;
//— 半径を指定する
pt.SetRadius(10);
PrintFormat(“Length=%G”,pt.GetCircleLength());
//— SetPI() は protected であるため、以下の文字列をコメントした場合コンパイルの段階でエラーが発生する
// pt.SetPI(3);

//— 基本クラスの変数を宣言し Pi 定数を 10 に設定してみる
CBaseMathClass bc;
bc.SetPI(10);
//—結果
PrintFormat(“bc.GetPI()=%G”,bc.GetPI());
}

この例は基本クラス CBaseMathClass の SetPI() 及び GetPi() メソッドがプログラムのどこからでも呼び出せることを示します。 しかし同時に、それらから派生する CProtectedChildClass では、これらのメソッドは CProtectedChildClass クラスまたはその派生クラスのメソッドのみからしか呼び出せません。

private 継承では基本クラスの public 及びprotected メンバは派生クラスの private メンバになり、それ以上の継承を通してそれらを呼び出すことは不可能です。

MQL5 言語は多重継承を持っていません。

ポリモーフィズム

ポリモーフィズムは継承によって繋がったオブジェクトクラスが同じ関数要素を呼び出す時に使用されます。これは基本クラスだけでなくその子孫クラスの振る舞いも記述する普遍的なメカニズムを作成するのに役立ちます。

図形の面積を計算するために設計された基本クラス CShape を開発しメンバ関数 GetArea() を定義していきましょう。基本クラスからの継承によって生成された子孫クラスの全てで、特定の図形の面積を計算する規則に従って、この関数を定義し直します。

正方形( CSquare クラス)では面積がその辺を介して計算され、円( CCircle クラス)では面積が半径で表現されます。基本クラスと全ての子孫クラスの CShape 型のオブジェクトを格納出来る配列を作成することが出来ます。 更に配列の各要素に同じ関数を呼び出すことが出来ます。

例:

//— 基本クラス
class CShape
{
protected:
int            m_type;               // 図形の種類
int            m_xpos;               // 基点の X 座標
int            m_ypos;               // 基点の Y 座標
public:
void           CShape(){m_type=0;};   // コンストラクタ(タイプ 0 )
int            GetType(){return(m_type);};// 図形の種類を返す
virtual
double         GetArea(){return (0); }// 図形の面積を返す
};

これで、派生クラスの全てがゼロの値を返すメンバ関数 getArea() を持つことになります。この関数の実装は各子孫関数内で異なります。

//— 派生クラス circle
class CCircle : public CShape           // コロンの後に基本クラスを定義する
{                                     // 継承の元
private:
double         m_radius;             // 円の半径

public:
void           CCircle(){m_type=1;}; // コンストラクタ(タイプ 1 )
void           SetRadius(double r){m_radius=r;};
virtual double GetArea(){return (3.14*m_radius*m_radius);}// 円の面積
};

Square クラスの宣言も同じです。

//— 派生クラス Square
class CSquare : public CShape           // コロンの後に基本クラスを定義する
{                                     // 継承の元
private:
double         m_square_side;       // 正方形の側面

public:
void            CSquare(){m_type=2;}; // コンストラクタ(タイプ 1 )
void            SetSide(double s){m_square_side=s;};
virtual double GetArea(){return (m_square_side*m_square_side);}// 正方形の面積
};

正方形と円の面積を計算するためにはそれぞれ m_radius と m_square_side の値が必要となるので、それぞれのクラス宣言に関数 SetRadius() と SetSide() を追加します。

1 つの基本型 CShape から派生した異なる種類のオブジェクト( CCircle と CSquare )が両方 1 つのプログラムで使用されていると仮定します。ポリモーフィズムによって、基本クラス CShape オブジェクトの配列が作成出来ますが、配列の宣言時にはこれらのオブジェクトはまだ未知で型も定義されていません。

配列の各要素に含まれるオブジェクト型はプログラムの実行中に直接決定されます。これは関係するクラスオブジェクトの動的作成を必要とするのでオブジェクトのかわりにオブジェクトポインタを使用することが必要となります。

new 演算子はオブジェクトの動的作成に使用されます。このようなオブジェクトは、個別に明示的に delete 演算子を使用して削除される必要があります。そこで、CShape 型のポインタの配列を宣言し、次のスクリプトの例に示すように各要素に適切な型のオブジェクトを作成します( new クラス名 )

//+——————————————————————+
//| スクリプトプログラムを開始する関数                                          |
//+——————————————————————+
void OnStart()
{
//— 基本型のオブジェクトポインタの配列を宣言する
CShape *shapes[5];   // CShape オブジェクトポインタの配列

//— 配列に派生オブジェクトを書き込む
//— CCircle 型のオブジェクトポインタを宣言する
CCircle *circle=new CCircle();
//— circle ポインタでオブジェクトのプロパティを設定する
circle.SetRadius(2.5);
//— shapes[0] にポインタ値を入れる
shapes[0]=circle;

//— CCircle オブジェクトをあと 1 つ作成し shapes[1] にそのポインタを書き込む
circle=new CCircle();
shapes[1]=circle;
circle.SetRadius(5);

//— 意図的に shapes[2] の値を設定するのを忘れる
//circle=new CCircle();
//circle.SetRadius(10);
//shapes[2]=circle;

//— 使用されない要素は NULL に設定する
shapes[2]=NULL;

//— CSquare オブジェクトを作成し shapes[3] にそのポインタを書き込む
CSquare *square=new CSquare();
square.SetSide(5);
shapes[3]=square;

//— CSquare オブジェクトを作成し shapes[4] にそのポインタを書き込む
square=new CSquare();
square.SetSide(10);
shapes[4]=square;

//— ポインタ配列ができたのでサイズを取得する
int total=ArraySize(shapes);
//— 配列内のポインタを全てループに渡す
for(int i=0; i<5;i++)
{
//— 指定されたインデックスのポインタが有効の場合
if(CheckPointer(shapes[i])!=POINTER_INVALID)
{
//— 図形の種類とスクウェアをログに記録する
PrintFormat(“The object of type %d has the square %G”,
shapes[i].GetType(),
shapes[i].GetArea());
}
//— ポインタの種類が POINTER_INVALID の場合
else
{
//— エラーの通知
PrintFormat(“Object shapes[%d] has not been initialized! Its pointer is %s”,
i,EnumToString(CheckPointer(shapes[i])));
}
}

//— 作成した全ての動的オブジェクトは削除が必要
for(int i=0;i<total;i++)
{
//— POINTER_DYNAMIC 型のポインタを持つオブジェクトのみを削除可能
if(CheckPointer(shapes[i])==POINTER_DYNAMIC)
{
//— 削除の通知
PrintFormat(“Deleting shapes[%d]”,i);
//— オブジェクトをポインタを通して削除
delete shapes[i];
}
}
}

delete 演算子を使用してオブジェクトを削除する時にはそのポインタの種類のチェックが必要なのでご注意ください。POINTER_DYNAMIC ポインタを持つオブジェクトのみが delete を使用して削除することが出来ます。他のポインタ型の場合はエラーが返されます。

しかし、ポリモーフィズムの定義には、継承時の関数の再定義に加えて、クラス内での 1 つの関数のパラメータで異なる複数の実装が含まれます。これはクラス内に同名で型及び/またパラメータの異なる関数がいくつか存在出来ることを意味します。この場合、ポリモーフィズムは関数オーバーロードを通して実装されます。

多重定義(オーバーロード)

1 つのクラス内で同名であるがパラメータの数が異なる複数のメソッドを定義することは可能です。この場合、メソッドはオーバーロードされたといわれ、その過程は、メソッドオーバーロードと呼ばれています。

メソッドオーバーロードはポリモルフィズム実現の方法の 1 つです。メソッドのオーバーロードは関数のオーバーロードと同じルールに従って行われます。

呼び出された関数が正確に一致していない場合、コンパイラは順次 3 つのレベルで適切な関数を検索します。

  1. 自己のクラスメソッド内での検索
  2. 自己に最も近い先祖から基本クラスに順にさかのぼって基本クラスのメソッドを検索
  3. 他の関数内での検索

もし全てのレベルで正確な一致が 1 つもなく、異なるレベルでいくつかの適切な関数がある場合は、最低レベルでの関数が使用されます。各レベル内で、複数の適切な関数が存在することはありません。

仮想関数

virtual キーワードは、実行時に動的に基本クラスと派生クラスの関数から適切な関数メンバを選択するメカニズムを提供する関数指定子です。構造体は仮想関数を含むことが出来ません。virtual キーワードは、関数メンバのみの宣言を変更することが出来ます。

仮想関数は通常の関数同様に実行可能な本体を持たなければなりません。呼び出される際にはそのセマンティックは他の関数の物と同様です。

仮想関数は派生クラスでオーバーライドされる場合があります。仮想関数に対してどの関数定義が呼び出されるかは(実行時に)動的に選択されます。基本クラスに仮想関数が含まれ、派生クラスがこの関数の独自のバージョンを持っているのが典型的なケースです。

基本クラスへのポインタは、基本クラスオブジェクトまたは派生クラスのオブジェクトのいずれかを示すことが出来ます。呼び出されるメンバ関数の選択は実行時に行われ、ポインタの型ではなくオブジェクトの型に依存します。派生型のメンバが存在しない場合は、基本クラスの仮想関数がデフォルトで使用されます。

デストラクタ は virtual キーワードで宣言されたかされなかったにかかわらずは常に仮想です。

MT5_Tetris.mq5 の例で仮想関数の使用を考えてみましょう。仮想関数 Draw を持つ基本クラス CTetrisShapeis が インクルードファイル MT5_TetisShape.mqh で定義されています。

//+——————————————————————+
class CTetrisShape
{
protected:
int               m_type;
int               m_xpos;
int               m_ypos;
int               m_xsize;
int               m_ysize;
int               m_prev_turn;
int               m_turn;
int               m_right_border;
public:
void              CTetrisShape();
void              SetRightBorder(int border) { m_right_border=border; }
void              SetYPos(int ypos)          { m_ypos=ypos;           }
void              SetXPos(int xpos)          { m_xpos=xpos;           }
int               GetYPos()                  { return(m_ypos);        }
int               GetXPos()                  { return(m_xpos);        }
int               GetYSize()                 { return(m_ysize);       }
int               GetXSize()                 { return(m_xsize);       }
int               GetType()                  { return(m_type);        }
void              Left()                     { m_xpos-=SHAPE_SIZE;    }
void              Right()                    { m_xpos+=SHAPE_SIZE;    }
void              Rotate()                   { m_prev_turn=m_turn; if(++m_turn>3) m_turn=0; }
virtual void      Draw()                     { return;                }
virtual bool      CheckDown(int& pad_array[]);
virtual bool      CheckLeft(int& side_row[]);
virtual bool      CheckRight(int& side_row[]);
};

また、それぞれの派生クラスでは、この関数は子孫クラスの特性に応じて実施されます。例えば、最初の図形 CTetrisShape1 には独自の Draw() 関数が実装されています。

class CTetrisShape1 : public CTetrisShape
{
public:
//— 図形を描画する
virtual void      Draw()
{
int    i;
string name;
//—
if(m_turn==0 || m_turn==2)
{
//— 水平
for(i=0; i<4; i++)
{
name=SHAPE_NAME+(string)i;
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,m_xpos+i*SHAPE_SIZE);
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,m_ypos);
}
}
else
{
//— 垂直
for(i=0; i<4; i++)
{
name=SHAPE_NAME+(string)i;
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,m_xpos);
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,m_ypos+i*SHAPE_SIZE);
}
}
}
}

Square 図形は CTetrisShape6 クラスによって記述され独自の Draw() メソッドを実装します。

class CTetrisShape6 : public CTetrisShape
{
public:
//— 図形を描画する
virtual void      Draw()
{
int    i;
string name;
//—
for(i=0; i<2; i++)
{
name=SHAPE_NAME+(string)i;
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,m_xpos+i*SHAPE_SIZE);
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,m_ypos);
}
for(i=2; i<4; i++)
{
name=SHAPE_NAME+(string)i;
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,m_xpos+(i-2)*SHAPE_SIZE);
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,m_ypos+SHAPE_SIZE);
}
}
};

作成したオブジェクトが属するクラスに応じて、このクラスの仮想関数かその派生クラスの仮想関数が呼び出されます。

void CTetrisField::NewShape()
{
//— 7 つの可能な図形から 1 つをランダムに選んで作成する
int nshape=rand()%7;
switch(nshape)
{
case 0: m_shape=new CTetrisShape1; break;
case 1: m_shape=new CTetrisShape2; break;
case 2: m_shape=new CTetrisShape3; break;
case 3: m_shape=new CTetrisShape4; break;
case 4: m_shape=new CTetrisShape5; break;
case 5: m_shape=new CTetrisShape6; break;
case 6: m_shape=new CTetrisShape7; break;
}
//— 描画する
m_shape.Draw();
//—
}

override修飾子 #

override修飾子は、宣言される関数は必ず親クラスのメソッドをオーバーライドする必要があるということを意味しています。この修飾子を使用することで、オーバーライド時のメソッドシグネチャのランダムな変更といったエラーを回避することができます。例として、基底クラスで引数としてint型変数を取るfuncメソッドを定義します。

class CFoo
{
  void virtual func(int x) const { }
};

次にメソッドを派生クラスでオーバーライドします:

class CBar : public CFoo
{
  void func(short x) { }
};

しかしエラーによって引数の型がintからshortに変わります。実際には、このような場合、オーバーライドではなくメソッドのオーバーロードが起こります。オーバーロードされた関数定義のアルゴリズムに従い、特定のケースにおいてコンパイラがオーバーライドされたメソッドの代わりに規定クラスで定義されたメソッドを選択することがあります。

このようなエラーを回避する為には、明示的にオーバーライドメソッドにoverride修飾子を追加する必要があります。

class CBar : public CFoo
{
  void func(short x) override { }
};

オーバーライド時にメソッドシグネチャが変更された場合、コンパイラは親クラスで全く同じシグネチャを見つけることができず、コンパイルエラーを出します。

‘CBar::func’ method is declared with ‘override’ specifier but does not override any base class method

final修飾子 #

final修飾子は逆に、派生クラスでのメソッドのオーバーライドを禁止します。メソッドの実装が十分であり、完全に禁止されている場合、派生クラスで変更されないようにするためにfinal修飾子を使って宣言してください。

class CFoo
{
  void virtual func(int x) final { }
};
 
class CBar : public CFoo
{
  void func(int) { }
};
 

final修飾子を使用したメソッドのオーバーライドを試行すると、上記の例で示したように、コンパイルはエラーを出します。

‘CFoo::func’ method declared as ‘final’ cannot be overridden by ‘CBar::func’
see declaration of ‘CFoo::func’

クラス及び構造体の静的メンパ

静的メンパ

クラスのメンバは、ストレージクラス修飾子 static を使用して宣言することが出来ます。これらのデータメンバはクラスの全てのインスタンスで共有されて 1 つの場所に格納されます。非静的データメンバは、各クラスのオブジェクト変数のために作成されます。

クラスの静的メンバを宣言することが出来なければこれらのデータをプログラムのグローバルレベルで宣言することが必要になります。これはデータとクラス間の関係を破り、クラスでデータとそれらを処理するためのメソッドを結合するという OOP の基本的なパラダイムと一致しないでしょう。静的メンバは特定のインスタンスに固有でないクラスのデータがクラススコープ内に存在することを可能にします。

静的クラスメンバは特定のインスタンスに依存しないため、それへの参照は次のようになされます。

class_name::variable

ここで class_name がクラス名で variable がクラスメンバ—名です。

ご覧のように、クラスの静的メンバにアクセスするにはコンテキスト解決演算子 ::が使用されます。クラスのメソッド内で静的メンバにアクセスする際にはコンテキスト演算子は任意です。

クラスの静的メンバは、明示的に所望の値で初期化する必要があります。このためには、クラスの静的メンバはグローバルスコープで初期化また宣言されなければなりません。静的メンバの初期化の順序は、そのグローバルスコープでの宣言の順序に対応します。

例えば、テキスト解析クラス CParser があって、処理された単語や文字の合計数を数える必要があるとします。必要なクラスのメンバのみを static として宣言し、それらをグローバルレベルで初期化する必要があります。クラスの全てのインスタンスは、単語や文字の共通のカウンタを使用します。

//+——————————————————————+
//| テキスト解析クラス                                                     |
//+——————————————————————+
class CParser
{
public:
static int        s_words;
static int        s_symbols;
//— コンストラクタとデストラクタ
CParser(void);
~CParser(void){};
};

//— グローバルレベルでの Parser クラスの静的メンバの初期化
int CParser::s_words=0;
int CParser::s_symbols=0;

静的クラスメンバは const キーワードを使用して宣言されます。静的定数はグローバルレベルで const キーワードを使用して初期化されます。

//+——————————————————————+
//| 処理されたデータを格納するためのスタッククラス                                  |
//+——————————————————————+
class CStack
{
public:
CStack(void);
~CStack(void){};

private:
static const int  s_max_length; // スタックの最大許容
};

//— CStack クラスの静的定数の初期化
const int CStack::s_max_length=1000;

this ポインタ #

キーワード this はそれ自体(つまり、メソッドが実行されたコンテキスト内の特定のクラスインスタンス)に暗黙的に宣言されたポインタを示します。これは、クラスの非静的メソッド内のみで使用することが出来ます。this ポインタはクラスの暗黙的な非静的メンバです。

静的関数では、クラスの静的メンバと静的メソッドのみにアクセスすることが出来ます。

静的メソッド

MQL5 では static 型のメンバ関数が使用出来ます。static 修飾子は、クラス内の宣言では関数の戻り値の型を先行しなければなりません。

class CStack
{
public:
//— コンストラクタとデストラクタ
CStack(void){};
~CStack(void){};
//— 最大スタック容量
static int        Capacity();
private:
int               m_length;     // スタック内の要素数
static const int  s_max_length; // スタックの最大許容
};
//+——————————————————————+
//| スタックに格納出来る要素の最大数を返す                                     |
//+——————————————————————+
int CStack::Capacity(void)
{
return(s_max_length);
}
//— CStack クラスの静的定数の初期化
const int CStack::s_max_length=1000;
//+——————————————————————+
//| スクリプトプログラムを開始する関数                                          |
//+——————————————————————+
void OnStart()
{
//— 型の変数を宣言
CStack stack;
//— オブジェクトの静的メソッドを呼び出す
Print(“CStack.s_max_length=”,stack.Capacity());
//— メソッドが静的でありオブジェクトの存在を必要としないため次のような呼び出しも可能
Print(“CStack.s_max_length=”,CStack::Capacity());
}

const 修飾子を持つメソッドは定数と呼ばれ、そのクラスの暗黙のメンバを変更することは出来ません。クラスの定数関数と定数パラメータの宣言は定数コレクトネス制御と呼ばれます。この制御により、コンパイラがオブジェクトの値の一貫性を確保し、問題がある場合にはコンパイル時にエラーを返すことを確認出来ます。

const 修飾子はクラス宣言内で引数リストの後に配置されます。クラス外での定義も const 修飾子を含む必要があります。

//+——————————————————————+
//| 四角形クラス                                                         |
//+——————————————————————+
class CRectangle
{
private:
double            m_width;     // 幅 
double            m_height;     // 高さ
public:
//— コンストラクタとデストラクタ
CRectangle(void):m_width(0),m_height(0){};
CRectangle(const double w,const double h):m_width(w),m_height(h){};
~CRectangle(void){};
//— 面積の計算
double            Square(void) const;
static double     Square(const double w,const double h);// { return(w*h); }
};
//+——————————————————————+
//| 四角形オブジェクトの面積を返す                                           |
//+——————————————————————+
double CRectangle::Square(void) const
{
return(Square(m_width,m_height));
}
//+——————————————————————+
//| 2 つの変数の積を返す                                                  |
//+——————————————————————+
static double CRectangle::Square(const double w,const double h)
{
return(w*h);
}
//+——————————————————————+
//| スクリプトプログラムを開始する関数                                          |
//+——————————————————————+
void OnStart()
{
//— 側面が 5 と 6 の四角形 rect を作成する
CRectangle rect(5,6);
//— 定数メソッドを使用して四角形の面積を求める
PrintFormat(“rect.Square()=%.2f”,rect.Square());
//— クラス CRectangle の静的メソッドを使用して数値の積を求める
PrintFormat(“CRectangle::Square(2.0,1.5)=%f”,CRectangle::Square(2.0,1.5));
}

定数制御を使用するもう 1 つの利点は、コンパイラが、定数オブジェクトを読み出し専用メモリに配置するなどの特別な最適化を適応することです。

const 修飾子は関数を呼び出す時にインスタンスメンバの不変を保証するため、静的な関数には const は使用できません。しかし、前述したように、静的関数は非静的クラスメンバにアクセスすることは出来ません。

関数テンプレート

オーバーロードされた関数は一般的に様々なデータ型に対して同様の演算を行うために使用されます。ArraySize() は MQL5 でのそのような関数の簡単な例で、配列のサイズを返します。実際には、このシステム関数はオーバーロードされており、このようなオーバーロードの実装は全体的に MQL5 アプリケーションの開発者から隠されています。

int  ArraySize(
void&  array[]      // チェックされた配列
);

つまり MQL5 言語コンパイラはこの関数の呼び出しごとに必要な実装を挿入するわけです。例えば、整数型の配列には下記が行われます。

int  ArraySize(
int&  array[]      // int 型要素の配列
);

ArraySize() は履歴データ形式の相場を操作するための MqlRates 型の配列にしては次のように表示されます。

int  ArraySize(
MqlRates&  array[] // MqlRates 型の配列
);

よって、同様の関数を異なる型の演算をするため使用するのが非常に便利です。しかし、準備作業が必要です。つまり、必要な関数は演算される全てのデータ型に対してオーバーロードされるべきです。

これには便利な解決法があります。それぞれのデータ型に同様の演算が実行される場合には、関数テンプレートの使用が可能です。この場合、プログラマは 1 つの関数テンプレートの記述をする必要があります。テンプレートを記述する際は、関数が作業する明確なデータ型の代わりに一部のみの仮パラメータが指定されます。コンパイラは関数が呼び出される時に使用される引数の型に基づいて自動的に各データ型の適切な取り扱いのための関数を生成します。

関数テンプレートの定義は template キーワードで始まり角括弧内の仮パラメータのリストが続きます。仮パラメータには typename キーワードが先行します。仮パラメータ型は組み込み型またはユーザ定義型です。用例は次の通りです。

  • 関数の引数の型を指定する
  • 関数の戻り値の型を指定する
  • 関数定義内で変数を宣言する

 

テンプレートパラメータの数は8を超えることは出来ません。テンプレート定義の各仮パラメータは、少なくとも 1 度は関数パラメータのリストに表示されるべきです。仮パラメータの名称は一意である必要があります。

数値型(整数や実数)の配列で最高値を検索する関数テンプレートの例は次の通りです。

template<typename T>
T ArrayMax(T &arr[])
{
uint size=ArraySize(arr);
if(size==0) return(0);

T max=arr[0];
for(uint n=1;n<size;n++)
if(max<arr[n]) max=arr[n];
//—
return(max);
}

このテンプレートは、渡された配列の最高値を求める関数を定義し、この値を結果として返します。MQL5 に内蔵された ArrayMaximum() 関数は最高値のインデックスを返すので、それから値そのものを見つけることが可能になります。例:

//— 配列を作成する
double array[];
int size=50;
ArrayResize(array,size);
//—  ランダムな値を書き込む
for(int i=0;i<size;i++)
{
array[i]=MathRand();
}

//—配列内の最大値のインデックスを見つける
int max_position=ArrayMaximum(array);
//— 配列内の最大値を取得する
double max=array[max_position];
//— 見つけた値を表示する
Print(“Max value = “,max);

従って、配列の最大値を取得するのに必要な 2 つのステップが実行されました。ArrayMax() 関数テンプレートに適切な型の配列を渡すだけで必要な型の結果を得ることが出来ます。最後の 2 行

//—配列内の最大値のインデックスを見つける
int max_position=ArrayMaximum(array);
//— 配列内の最大値を取得する
double max=array[max_position];

は、返される結果が関数に渡された配列と同じ型を持っている1行に出来ます。

//— 最大値を見つける
double max=ArrayMax(array);

この場合、ArrayMax() 関数によって返される結果の型は自動的に配列の型と一致されます。

 

さまざまなデータ型を演算出来る汎用メソッドを作成するために引数の型を文字列として取得するには typename キーワードを使用します。データ型を文字列として返す関数の具体例を考えてみましょう。

#include <Trade\Trade.mqh>
//+——————————————————————+
//|                                                                  |
//+——————————————————————+
void OnStart()
{
//—
CTrade trade;
  double d_value=M_PI;
int i_value=INT_MAX;
Print(“d_value: type=”,GetTypeName(d_value), “,   value=”, d_value);
Print(“i_value: type=”,GetTypeName(i_value), “,   value=”, i_value);
Print(“trade: type=”,GetTypeName(trade));
//—
}
//+——————————————————————+
//| 型が文字列として返される                                               |
//+——————————————————————+
template<typename T>
string GetTypeName(const T &t)
{
//— 型を文字列として返す
return(typename(T));
//—
}

 

関数テンプレートはクラスメソッドにも使用することが出来ます。例えば、

class CFile
{

public:

template<typename T>
uint WriteStruct(T &data);
};

template<typename T>
uint CFile::WriteStruct(T &data)
{

return(FileWriteStruct(m_handle,data));
}

関数テンプレートは export、virtual 及び #import キーワードと宣言されるべきではありません。

テンプレート関数の多重定義(オーバーロード)

テンプレート関数の多重定義はしばしば必要です。例えば型キャスティングを使用して1番目のパラメータに2番目のパラメータの値を書き込むテンプレート関数があるとします。MQL5ではstringからboolへの型キャスティングは不可能です。しかし、テンプレート関数の多重定義を作成すれば、それは可能になります。例は下記です。

//+——————————————————————+
//| テンプレート関数                                                      |
//+——————————————————————+
template<typename T1,typename T2>
string Assign(T1 &var1,T2 var2)
{
var1=(T1)var2;
return(__FUNCSIG__);
}
//+——————————————————————+
//| bool+stringのための特別な多重定義                                     |
//+——————————————————————+
string Assign(bool &var1,string var2)
{
var1=(StringCompare(var2,“true”,false) || StringToInteger(var2)!=0);
return(__FUNCSIG__);
}
//+——————————————————————+
//| スクリプトプログラムを開始する関数                                          |
//+——————————————————————+
void OnStart()
{
int i;
bool b;
Print(Assign(i,“test”));
Print(Assign(b,“test”));
}

コード実行の結果、Assign()テンプレート関数はint+stringのペアで使用されていますが、多重定義されたバージョンは2回目の呼び出しでbool+stringのペアですでに使用されています。

string Assign<int,string>(int&,string)
string Assign(bool&,string)

テンプレートの利点

関数テンプレートは、さまざまなデータ型で配列内の最大要素を検索するなどの同様の操作を実行する必要がある場合に使用されます。テンプレートを適用する主な利点は、型ごとに別々の多重定義(オーバーロード)をコーディングする必要がないことです。複数の多重定義を型ごとに宣言する代わりに、</ t3>

double ArrayMax(double array[])
{

}
int ArrayMax(int array[])
{

}
uint ArrayMax(uint array[])
{

}
long ArrayMax(long array[])
{

}
datetime ArrayMax(datetime array[])
{

}

テンプレート関数を1つ書くだけです

template<typename T>
T ArrayMax(T array[])
{
if(ArraySize()==0)
return(0);
uint max_index=ArrayMaximum(array);
return(array[max_index]);
}

コ—ド内で使うには:

double high[];
datetime time[];
….
double max_high=ArrayMax(high);
datetime lasttime=ArrayMax(time);

ここで、使用されるデータの型を指定する T仮パラメータは、コンパイル時に実際に適用される型に置き換えられます。つまり、コンパイラはdouble、datetimeなどの型ごとに別々の関数を生成します。MQL5では、このアプローチのすべての利点を使ってクラステンプレートを開発することもできます。

クラステンプレート

クラステンプレートはtemplate キーワードの後に typenameキーワードで仮パラメータのリストを列挙する山括弧<> を使用して宣言されます 。このエントリは、クラスを実装するときに実際の変数型を定義するT仮パラメータを持つジェネリッククラスを扱うようにコンパイラに通知します。例としてT 型の要素を持つ配列を格納するためのベクトルクラスを作成しましょう。</ t10>

#define TOSTR(x) #x+” “   // オブジェクト名を表示するマクロ
//+——————————————————————+
//| T型要素を格納するためのベクトルクラス                                       |
//+——————————————————————+
template <typename T>
class TArray
{
protected:
T                 m_array[];
public:
//— コンストラクタはデフォルトで10要素の配列を作成する
void TArray(void){ArrayResize(m_array,10);}
//— 指定された配列サイズのベクトルを作成するコンストラクタ
void TArray(int size){ArrayResize(m_array,size);}
//— TArray型オブジェクトに格納されているデータの型と量を返す
string Type(void){return(typename(m_array[0])+“:”+(string)ArraySize(m_array));};
};

次に、さまざまな種類の作業を行うプログラムの3つのTArray オブジェクトを作成するさまざまな方法を適用しましょう。

void OnStart()
{
TArray<double> double_array;   // ベクトルのデフォルトサイズは10
TArray<int> int_array(15);     // ベクトルのサイズは15
TArray<string> *string_array; // TArray<string>ベクトルへのポインタ
//— 動的オブジェクトを作成する
string_array=new TArray<string>(20);
//— 操作ログにオブジェクト名、データ型、ベクトルサイズを表示する
PrintFormat(“%s (%s)”,TOSTR(double_array),double_array.Type());
PrintFormat(“%s (%s)”,TOSTR(int_array),int_array.Type());
PrintFormat(“%s (%s)”,TOSTR(string_array),string_array.Type());
//— プログラムを完了する前に動的オブジェクトを削除する
delete(string_array);
}

スクリプト実行の結果:

 double_array  (double:10)
int_array  (int:15)
string_array  (string:20)

ここでは、double、int、およびstringの異なるデータ型を持つ3つのベクトルがあります。

クラステンプレートは、任意の型の他のオブジェクトをカプセル化するために設計されるオブジェクトであるコンテナの開発にも適しています。コンテナオブジェクトは既に特定の種類のオブジェクトを含むコレクションです。通常、保存されたデータの処理は、コンテナに即座に組み込まれます。

例えば、配列外の要素にアクセスすることを許可しないクラステンプレートを作成することによって”out of range”(範囲外)重大エラーを避けることができます。

//+——————————————————————+
//| 配列要素への自由なアクセスのためのクラス                                    |
//+——————————————————————+
template<typename T>
class TSafeArray
{
protected:
T                 m_array[];
public:
//— デフォルトコンストラクタ
void              TSafeArray(void){}
//— 指定されたサイズの配列を作成するコンストラクタ
void              TSafeArray(int size){ArrayResize(m_array,size);}
//— 配列サイズ
int               Size(void){return(ArraySize(m_array));}
//— 配列サイズを変更する
int               Resize(int size,int reserve){return(ArrayResize(m_array,size,reserve));}
//— 配列を解放する
void              Erase(void){ZeroMemory(m_array);}
//— インデックスによって配列要素にアクセスする演算子
T                 operator[](int index);
//— 配列からすべての要素を一度に受け取る代入演算子
void             operator=(const T  &array[]); // T type array
};
//+——————————————————————+
//| インデックスによる要素の受け取り                                           |
//+——————————————————————+
template<typename T>
T TSafeArray::operator[](int index)
{
static T invalid_value;
//—
int max=ArraySize(m_array)-1;
if(index<0 || index>=ArraySize(m_array))
{
PrintFormat(“%s index %d is not in range (0-%d)!”,__FUNCTION__,index,max);
return(invalid_value);
}
//—
return(m_array[index]);
}
//+——————————————————————+
//| 配列の代入                                                         |
//+——————————————————————+
template<typename T>
void TSafeArray::operator=(const T  &array[])
{
int size=ArraySize(array);
ArrayResize(m_array,size);
//— T型はコピーオペレータをサポートすべき
for(int i=0;i<size;i++)
m_array[i]=array[i];
//—
}
//+——————————————————————+
//| スクリプトプログラムを開始する関数                                          |
//+——————————————————————+
void OnStart()
{
int copied,size=15;
MqlRates rates[];
//— 相場の配列をコピーする
if((copied=CopyRates(_Symbol,_Period,0,size,rates))!=size)
{
PrintFormat(“CopyRates(%s,%s,0,%d) returned %d error code”,
_Symbol,EnumToString(_Period),size,GetLastError());
return;
}
//— コンテナを作成し、それにMqlRatesの値配列を挿入する
TSafeArray<MqlRates> safe_rates;
safe_rates=rates;
//— 配列内のインデックス
int index=3;
PrintFormat(“Close[%d]=%G”,index,safe_rates[index].close);
//— 配列外のインデックス
index=size;
PrintFormat(“Close[%d]=%G”,index,safe_rates[index].close);
}

テンプレート宣言は、クラス宣言の外にメソッドを記述する際にも使用する必要がありますのでご注意ください。

template<typename T>
T TSafeArray::operator[](int index)
{

}
template<typename T>
void TSafeArray::operator=(const T  &array[])
{

}

クラステンプレートと関数テンプレートを使用すると、カンマで区切られた複数の仮パラメータの定義が可能です。下記の例は”key – value”のペアを格納するMapコレクショです。

template<typename Key, template Value>
class TMap
{

}

抽象クラスと純粋仮想関数

抽象クラスは、将来におけるより具体的な派生クラスを作成する一般的なエンティティを作成する為に作られています。抽象クラスは、いくつかの他のクラスの為に基底クラスとして使用することができるだけなので、抽象クラスのオブジェクトタイプを作成することはできません。

純粋仮想関数を一つでも含むクラスは、抽象クラスとなります。したがって、抽象クラスから派生したクラスは、その純粋仮想関数を実装する必要があり、そうしないと、これらは同様に抽象クラスになります。

仮想関数は、純粋指定子構文を使用し、『純粋』として宣言されます。例として、共通機能を提供する為だけ型に作成されるCAnimalクラスを見てみましょう。CAnimalのオブジェクト自体は、実際の使用には一般的すぎます。したがって、CAnimalクラスは、抽象クラスの為の良い例となります。

class CAnimal
{
public:
CAnimal();     //コンストラクタ
virtual void       Sound() = 0;   // 純粋仮想関数
private:
double             m_legs_count; // 動物の足の数
};

ここでのSound()関数は、純粋仮想関数PURE(=0)の指定子で宣言されている為、純粋仮想となります。

(=NULL)または(=0)と純粋指定子PUREが指定されている仮想関数のみが純粋仮想関数となります。抽象クラスの使用と宣言例:

class CAnimal
{
public:
virtual void       Sound()=NULL;   // PUREメソッドは派生クラスでオーバーライドする必要があり、CAnimalクラス自体は抽象クラスになり、作成されません
};
//— 抽象クラスからの派生
class CCat : public CAnimal
{
public:
virtual void       Sound() { Print(“Myau”); } // PUREはオーバーライドする必要があり、CCatクラスは抽象クラスではなく、作成することができます
};

//— 間違った使用例
new CAnimal;         //『CAnimal』エラーは、コンパイラは「抽象クラスをインスタンス化できません」というエラーを出します
CAnimal some_animal; // CAnimal』エラーは、コンパイラは「抽象クラスをインスタンス化できません」というエラーを出します

//— 正しい使用例
new CCat; // エラーはなく、CCatクラスは抽象ではありません
CCat cat; // エラーはなく、CCatクラスは抽象ではありません

抽象クラス使用の制限

純粋仮想関数(直接または間接)の抽象クラスのコンストラクタによる呼び出しの際、結果は未定義になります。

//+——————————————————————+
//| 抽象基底クラス                                                       |
//+——————————————————————+
class CAnimal
{
public:
//— 純粋仮想関数
virtual void      Sound(void)=NULL;
//— 関数
void              CallSound(void) { Sound(); }
//— コンストラクタ
CAnimal()
{
//— 仮想メソッドの明示的な呼び出し
Sound();
//— 明示的ではない呼び出し(第三の関数を介した)
CallSound();
//— コンストラクタまたはデストラクタは常に自分の関数を呼び出す
//— 派生クラスでの呼び出された関数によるオーバーライドや仮想性に関わらず
//— もし、呼び出される関数が純粋仮想の場合、
//— 呼び出しは重大なランタイムエラーをもたらします:『純粋仮想関数呼び出し』
}
};

しかしながら、抽象クラスのコンストラクタとデストラクタは、他のメンバー関数を呼び出すことができます。

 

Originally posted 2019-07-27 10:30:00.

Leave a Reply

Your email address will not be published. Required fields are marked *

CAPTCHA


You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">