火曜日, 2月 19, 2008

C++解説12(クラスについて3)

1. 継承

クラスの継承について説明します。
継承を行う要因として主に3種類存在すると言われています。

1.1 拡張継承
拡張継承とはどの本でも一番最初に説明がされている継承です。
「人間は哺乳類で...」などど書かれている本があればまさしく拡張継承の説明でしょう。
まあ、今更「人間は哺乳類で...」何て説明どの本もしてないかもしれませんが
ようするに、あるクラスでは機能が不足しているため
追加機能を実装する場合に取る、継承の方法です。

1.2 データ継承
具体的には基底クラスにprotectedで定義した変数を
派生クラスがprivate変数として使用する事を指します。
まさに、データのみを継承するという事になります。
これはそのままですが、いろいろと有効に活用できます。

1.3 インターフェース継承
最後にインターフェース継承です。
オブジェクト指向で多態性(ポリモフィズム)という言葉があります。
C++にはオーバーライドという言葉とオーバーロードという言葉があります。
オーバーライドとはポリモフィズムそのものを指し、
オーバーロードとは同じ名前で宣言されている関数を
引数や返却値などの違いで使い分ける方法です。
ここではオーバーライドの説明を行います。
オーバーロードについては後ほど...


それでは継承の文法です。

class 派生クラス名 : 基底クラス名 {
// 後はクラスの書き方とまったく同じ
}

それでは細かい説明をします。

1.1.1 拡張継承(詳細説明)
「警察官」というクラスを作成してみます。
警察官は人間ですが、人間は警察官ではありません。
なので警察官とは人間の機能に警察の機能を追加する事で
実現できる事になります。

では警察官とはどういった特徴があるかを考えます。

・役職がある
・捜査する
・逮捕する
・拳銃を撃つ

もの凄く簡単ですが、拡張継承の説明としてはこれで十分です。

class Human {
private:
char *Name;
int Age;
char Birthday[9];
public:
Human(char*, int, char*);
~Human();

char* getName(void) { return Name; }
};

class Police : public Human {
private:
int OfficialPosition; // 1:巡査部長 2:警部補 以外:警視
public:
Police(char *InName, int InAge, char *InBirthday, int Position)
: Human(InName, InAge, InBirthday) { OfficialPosition = Position; }
void Search() { std::cout << "捜査してます\n"; }
void Arrest() { std::cout << "逮捕します\n"; }
void ShootGun() { std::cout << "発砲しました\n"; }
char* getOfficialPosition(void);
};

char* Police::getOfficialPosition(void) {
switch(OfficialPosition) {
case 1: return "巡査部長";
case 2: return "警部補";
default: return "警視";
}
}

実際に使ってみましょう。

int main(void) {
Police obj1("semona", 0, "20040101", 1);

std::cout << obj1.getName() << "," << obj1.getOfficialPosition() << std::endl;
std::cout << obj1.getName() << ","; obj1.Search();
std::cout << obj1.getName() << ","; obj1.ShootGun();
std::cout << obj1.getName() << ","; obj1.Arrest();
std::cout << obj1.getName() << "," << obj1.getOfficialPosition() << std::endl;

return 0;
}

ここで注目して欲しいのはPoliceオブジェクトを作成してるにも関わらず、
getName()、というHumanのpublicメソッドを使用しているところです。
これが継承という機能によって実現される事になります。

Police(char *InName, int InAge, char *InBirthday, int Position)
: Human(InName, InAge, InBirthday) { OfficialPosition = Position; }
一応この部分におやっ?っと思った人のために少しだけ解説します。

Police(...) : /* 何か書いてある */ { ... }
この部分ですが、初期化といいます。
初期化と代入の違いは説明しましたが、その初期化に当たります。
何で、こんなところに書くのかを説明したいと思います。

class Base {
public:
Base() { std::cout << "Base::Base()" << std::endl;}
~Base() { std::cout << "Base::~Base()" << std::endl;}
};

class Delived : public Base {
public:
Delived() { std::cout << "Delived::Delived()" << std::endl;}
~Delived() { std::cout << "Delived::~Delived()" << std::endl;}
};

int main(void) {
Delived dObj;

return 0;
}

実行結果
Base::Base()
Delived::Delived()
Delived::~Delived()
Base::~Base()

派生クラスのインスタンスを作成したところ、
基底クラスのコンストラクタとデストラクタがしっかり動いていますね。
しかも、コンストラクタは先に、デストラクタは後に...
これはC++の仕様になります。
つまり、派生クラスは自分のコンストラクタ実行時には
基底クラスをきちっとした形で使用できるという事になります。
でも、基底クラスがこんな感じになっていた場合どっちが動くの?
プログラムは分からない場合は何でも実験です。やってみましょう。

class Base {
public:
Base() { std::cout << "1:Base::Base()" << std::endl;}
Base(int i) { std::cout << "2:Base::Base()_" << i << std::endl;}
~Base() { std::cout << "Base::~Base()" << std::endl;}
};

実行結果
1:Base::Base()
Delived::Delived()
Delived::~Delived()
Base::~Base()


予想通りの結果ですかね。
待って下さいよ。わたくしは2:Baseを使用したいのですが...
でもDelivedのコンストラクタが動くときには終わっちゃってるし
何か、見えてきたって感じですね。
何のためにこんな説明をしているのか!そうですよ初期化ですよ
という事で派生クラスをちょこっと変更

class Delived : public Base {
public:
Delived() : Base(3){ std::cout << "Delived::Delived()" << std::endl;}
~Delived() { std::cout << "Delived::~Delived()" << std::endl;}
};

実行結果
2:Base::Base()_3
Delived::Delived()
Delived::~Delived()
Base::~Base()


他にもこんな使い方が
class Base {
private:
const int CONST_INT;

...
};

このようにconstで宣言された変数には代入は行えません。
しかし代入はできないのですが初期化は当然できます。
だから「Base() : CONST_INT(1) {}」こんな感じで初期化します。
もちろん、こんな初期化が許されるのは、コンストラクタだけです。
理由は簡単ですね。コンストラクタがクラスの初期化だからです。

少し脱線しましたが、以上が拡張継承です。
ここでは人間機能しかなかったHumanというクラスに
Policeというクラスを拡張した警察官というクラス作成しました。
Humanクラスを拡張したため、人間の機能も持っているわけですね。
もし、Policeクラスは基底クラスに犬クラスを選択すれば警察犬ってところですね。


1.2.1 データ継承(詳細説明)

人間ってクラスを作ってきましたが、そもそも人間って
こんな簡単でしょうか?(身も蓋もねえ事、言ってんじゃねえよ!)
すいません。例が無くって、でも進めましょう。

例えば、人間とは男と女に分けられます。
でもって男と女とはまったく違うものです。
それを人間なんて一つのクラスにしてしまったらこの違いをどう表現すればいいのでしょうか?
一つに拡張継承を使って、人間クラスを継承する男クラスと女クラスを作成するという表現方法があります。
しかし、ここではデータ継承を使って表現したいと思います。
今まで、人間クラスで管理していた名前、年齢、誕生日といった項目は人間であれば誰もが持っている項目です。
これらを人間クラスのデータをして保持する事には何も変化はありません。
今までは
class Human {
private:
char *Name;
int Age;
char Birthday[9];
};

として保持し、ここにアクセスするメソッドを提供するという方針でした。
データ継承とはアクセスするメソッドを提供する代わりに、直接継承したクラスにはアクセス権を与える継承方法となります。

クラスにはprivate(非公開情報)、public(公開情報)という他に
protected(継承先には公開、非継承先には非公開)という宣言方法が存在します。
つまり、今までprivateで宣言してきた変数達をprotectedで宣言してやると
いう事ですね。

class Human {
protected:
char *Name;
int Age;
char Birthday[9];
};

これだけですね。すると拡張継承ではPoliceクラスがHumanクラスを継承したので
同様にPoliceクラスにHumanクラスを継承させましょう。

class Police : public Human {
private:
int OfficialPosition; // 1:巡査部長 2:警部補 以外:警視
public:
// Humanの変数には直接アクセスできるため、このような初期化は不要
// Police(char *InName, int InAge, char *InBirthday, int Position)
// : Human(InName, InAge, InBirthday) { OfficialPosition = Position; }
// 新しいコンストラクタ
Police(char *InName, int InAge, char *InBirthday, int Position) {
Name = (char*)malloc(strlen(InName));
strcpy(Name, InName);
Age = InAge;
strcpy(Birthday, InBirthday);
OfficialPosition = Position;
}
};

こんな感じになります。直接アクセスする分すっきりした感じがしますね。
あんまり、難しい話ではないと思います。
ただ、私が考える注意点だけ書いておきます。このデータ継承にはいろいろな使い方があります。
しかし、データ継承を行う場合はデータのみを継承する事をお勧め致します。
データ継承を行い、クラスの機能の継承する何て事をやり出してしまったら
おそらく、自分の頭の中でパニックを起こしてしまう事でしょう。


最後はインターフェース継承です。
ここの冒頭で書いたように、こいつはクラス最大の恩恵を授かる事ができるでしょう。
少しだけ、C言語でこいつの実現を考えて見る事にしてみます。
そもそも、インターフェースプログラミングとは何でしょうか?
実は、使用する側にはインターフェースのみ意識させクラスの中身は考えさせない。
と言ったカプセル化の概念の究極の行き先ではないでしょうか?

たとえば、車を作る事を考えてみましょう。
セダン、RV、ワンボックスいろいろとありますが、要は車を作るという事です。
いろいろと部品の違いはありますが、車を作るという肯定は何も変わりません。
そうです、車を作るというインターフェースに変更はないのです。
だから、「車を作る」というメソッドのインターフェースを使ってセダン、RV、ワンボックスといろいろ作れればいいという事になります。
では実際にどのようなものか、プログラムを書いてみます。

はじめにインターフェースとなるクラスを作成します。

class CarFactory {
public:
virtual void CarCreateExec() = 0;
};

これで作成終了です。
実際に新しい概念が2つ程あります。
1つ目は「virtual」という修飾子がメソッドの前にある事
2つ目はメソッドの後ろに「= 0;」という意味不明な箇所がある所です。

まず、1つ目のvirtualとは仮想関数(バーチャルメソッド)である事を宣言しています。
仮想関数とはこいつはオーバーライドできますよ!って事になります。
オーバーライドとは、直訳すれば「取って代わる。無視する」などの意味となりますがここでは受身で取って下さい。
つまり「取って代えられる、無視される」って感じでしょうか
そして2つ目の「=0;」ですが、純粋な仮想関数である事を指しています。
純粋な仮想関数?
当然、こうなっても仕方ないですわね。
はじめに、仮想関数を説明してから純粋仮想関数について考えてみますね。

とりあえず、さっきのCarFactoryを拡張しましょう。

#include "iostream"

class CarFactory {
protected:
char *CarName;

public:
void PreCarCreate() {
std::cout << "車を作り始めます\n";
}
virtual void CarCreateExec() {
char *CreateCarName = "自動車1";
CarName = CreateCarName;
}
void SufCarCreate() {
std::cout << CarName << "の完成です\n";
}
};

int main(void) {
CarFactory *obj = new CarFactory();

obj->PreCarCreate();
obj->CarCreateExec();
obj->SufCarCreate();

delete obj;

return 0;
}

実行結果
車を作り始めます
自動車1の完成です


まあ、何て事はないですよね。
では次に「virtual」で宣言されている関数をオーバーライドします。

#include "iostream"

class RvCarFactory : public CarFactory {
public:
virtual void CarCreateExec() {
char *CreateCarName = "RV車1";
CarName = CreateCarName;
}
};

int main(void) {
CarFactory *obj = new RvCarFactory(); // ここがポイント

obj -> PreCarCreate();
obj -> CarCreateExec();
obj -> SufCarCreate();

delete obj;

return 0;
}

実行結果
車を作り始めます
RV車1の完成です

こんな結果になります。理解できましたかね? すべてのポイントは継承なんですが...
派生クラス(継承したクラス)は基底クラスの公開関数/変数、
継承先公開関数/変数を自分のものとか、言ってきましたが正しくは派生クラスの中に基底クラスの内容が含まれるという言い方が正しいかもしれません。

class Base { ... };
class Deli1 : public Base { ... };
class Deli2 : public Deli1 { ... };

となっている場合、

class Base
Baseクラスの内容

class Deli1
Baseクラスの内容 + Deli1クラスの内容

class Deli2
Baseクラスの内容 + Deli1クラスの内容 + Deli2クラスの内容

こんなイメージです。でっかく×2なっていっているという事です。
で今回ポイントになっている以下の文ですが、
CarFactory *obj = new RvCarFactory();

上の例でいくと
Base *obj = new Deli1();
という事になりますね。でどのような意味になるかというと*objには間違いなくDeli1の内容が格納されています。
しかし、objとはBaseポインタ型です。という事は、Baseクラスが提供している方法でしか
Deli1にアクセスできないという事になってしまいます。
でも間違いなくDeli1クラスのオブジェクトを作成しているため「RV車1の完成です」という結果になったのです。
Deli1のCarCreateExecは「RV車1」のアドレスを代入しているでしょ!

何か、いろいろ言ってるけど使えなくね~~~!。なんて思ってしまったら「ばかもん!」って言われるのですよ。

ここが最大のポイントですから。ここって何の説明でしたっけ?そうですよ。インターフェースプログラミングですよ。
インターフェースプログラミングとはインターフェースを基底クラスに宣言して、そのインターフェースを守って派生クラスを実装するって事ですよ。

今回はオーバーライドを説明するために、基底クラスに関数の実装を用意しましたが、
RV車を作成する特化したクラスがあったのですから自動車、ここではセダンだとしますね。
自動車を作成する特化したクラスがあれば良いという事になります。
またCarFactoryクラスはインターフェースに特化させてしまえば関数の実装は不要という事になります。

この「インターフェースに特化させるため、関数の実装は行いません」
という宣言を純粋仮想関数で行います。

つまり、以下のクラスのインスタンスは作成を行う事はできません。
なぜなら、CarCreateExecには関数の実装が行われていないため
実際にどれを動かせば良いのか分からないという事になるからです。
ここまでの内容を踏まえて、RV車とセダン車を作成するクラスを作成します

// インターフェース専用です
class CarFactory {
public:
virtual void CarCreateExec() = 0;
};

class RvCarFactory : public CarFactory {
public:
void CarCreateExec() {
std::cout << "RV車を作成しました\n";
}
};

class SedanCarFactory : public CarFactory {
public:
void CarCreateExec() {
std::cout << "セダン車を作成しました\n";
}
};

int main(void) {
// ここではセダン車を作成しますが、ここ1行を書き換えてしまえば
// RV車を作成する事ができます
// CarFactory *obj = new RvCarFactory();
CarFactory *obj = new SedanCarFactory();

obj -> CarCreateExec();

delete obj;

return 0;
}

実行結果
セダン車を作成しました


結構いろいろな事が思いついてきたでしょ?
ここでは、簡単なインターフェースプログラミングについて触れました。
こいつをものにする事ができればC言語とは違うまったく新しいC++の世界が見えてくると思います。

余談ですが、デザインパターンと言われているパターンを実装する場合
このインターフェースプログラミングは必須な知識/テクニックです。

0 件のコメント: