Lesson 4 : 初心者のためのClass [#ha6b660d]
さてC++の代名詞と言っても過言ではない,クラス(Class)についていよいよ入りたいと思います.
クラスによって,C言語に比べ柔軟性,汎用性,拡張性,再利用性のすべてが飛躍的に高まりました.
もちろん,そのぶん理解するまでに時間がかかると思いますが,一度クラスを使ってプログラムを書くようになると,もう2度とC言語でプログラムを書きたくなくなります.
Contents
クラスの構造 [#fd36bdee]
さて,さっそくクラスを使ったプログラムについて説明したいと思います.
まず最初にクラスの構造を簡単に説明します.
もちろん,ここではおおまかな話だけなので,基礎的なものはC++の教科書を読んでください.
一言で言うならば,関数の定義できる構造体です.
定義 [#w7844ce7]
では以下に簡単なクラスの定義を書いてみます.
class SampleClass{ // SampleClass クラス
private:
int a;
void A(){...}
protected:
float b;
void B(){...}
public:
SampleClass(){ // コンストラクタ
cout<<"Constructor"<<endl;
}
SampleClass(){ // デストラクタ
cout<<"Destructor"<<endl;
}
int c;
void SayHello(){ // Say Hello World !
cout<<"Hello World !"<<endl;
}
};
では,上から順番に見ていきましょう.
最初の行の
class SampleClass{...} // SampleClass クラス
がクラスの定義です.
使うときは例えば
SampleClass smp;
の用に宣言します.
中で定義されている変数
int a
...
void B();
などはメンバ変数,メンバ関数と呼ばれるもので,このクラスの中のメンバーです.
クラスは構造体のように使えるので,中で定義されている変数は
smp.c=10;
のように使えます
アクセス [#t0cd8977]
クラス内で定義されている変数,関数は,おなじクラス内の関数から呼び出す事ができます.
たとえば
void B(){
A();
a=2;
SayHello();
}
などです.
ところで,クラス定義の中に3つの区切りがあるのに気づきましたか?
{...
private:
... // private領域
protected:
... // protected領域
public:
... // public領域
};
これはそれぞれの領域で定義された変数や関数のアクセス制限をかけているのです.
このアクセス制限によって,ユーザーが間違ったクラスの使い方をしないように防ぐ事ができたり,不用意なグローバル変数を定義する必要がなくなります.
さて,まず「public」領域はほかの二つとは異なり,そこの領域に定義した変数や関数にアクセスする事ができます.
ここで言うアクセスとは,上のように
smp.c=10;
smp.SayHello();
のように呼び出す事を言い,「public」領域で宣言したメンバはアスセス可能です.
一方で,「private」「protected」の2つは,「public」のようにアクセスする事ができません.つまり
smp.A(); // privateな関数なのでコンパイルエラー
smp.b; // protectedな変数なのでコンパイルエラー
はコンパイルエラーになります.
では「private」と「protected」の2つの違いはなんなのでしょうか?
これは,クラスを継承したときに違いが出てきます.
ここでは詳しく説明はしませんが,privateは継承したクラス先でアクセスできず,protectedは継承した先で呼び出す事ができると言った違いです.
詳しくは,また別のページで
使い方 [#n78bcc99]
ということで実際に定義したクラスを使って見ましょう.
今回定義したクラスは「Hello World !」が出力されるだけです.
以下のようなmain関数を書いて,使ってみると
int main(){
SampleClass smp;
smp.SayHello();
return 0;
}
実行すると
Constructor
Hello World !
Destructor
が出力されます.
しかし「Hello World !」は分かりますが,「Constructor」や「Destructor」はなぜ表示されるのでしょうか?
コンストラクタ & デストラクタ [#zcc5874b]
さて,コンストラクタとデストラクタとはなんのことでしょうか?
先ほどのプログラムのpublicにクラス名と同じ関数名な
SampleClass(){ // コンストラクタ
...}
SampleClass(){ // デストラクタ
...}
という部分があります.
この部分がコンストラクタとデストラクタで,クラスを生成したときに自動的にコンストラクタが呼ばれ,破棄したときにデストラクタが呼ばれます.
何のために存在しているのかというと,クラスメンバ変数などの初期化や初期処理を記述します.
これは一体何のためにあるのかをご説明します.
次ののような配列を生成して値を格納しておくクラスを考えます.
class Array{
private:
public:
int nData; // 配列のサイズ
int *array; // 確保する配列のポインタ
Array(){ // コンストラクタ
nData=10; // 10個の配列を確保
array = new int[nData]; // C++での配列の動的確保
}
Array(){
delete [] array; // new で確保した配列を削除
}
void AllDisplay(){...} // 配列の中身を出力するだけの関数
};
このクラスはint型の配列を10個分用意するだけものもで,関数も中身をすべて出力するだけの意味のないクラスです.
このクラスでのコンストラクタとデストラクタは一体何をしているのでしょうか?
コンストラクタないでは,まず配列のサイズを10と設定して配列を確保しています.
つまり変数などを初期化しているのです.
また,デストラクタでは,newで確保した配列をdeleteで消す事しています.
このように,クラスとして使用するために必要な変数を初期化or消去するためにコンストラクタとデストラクタが用意されています.
つまり,わざわざ初期化関数を用意する必要がないという事です.
アクセス指定子 [#hd545c9d]
アクセス指定子とはさきほど上で紹介した「public」「private」「protected」の事を言います.
ここでは,これらがなぜ必要なのかを説明します.
先程の配列を確保するクラスを例として使用します.
先程のクラスでは,すべてのメンバが「public」に指定されていました.これだとクラス設計としては大問題です.
たとえば,配列の中身をすべて表示するための関数を以下のように実装したとします.
void Array::AllDisplay(){ // 関数の中身
for(int i=0; i<nData; i++){ // メンバ変数のnDataだけループ
cout<<"Array["<<i<<"] = "<<array[i]<<endl;
}
}
実に簡単にメンバ変数である配列サイズを示す「nData」の文だけループをまわせばすみます.
さて一方で,この「nData」は「public」に指定されているので呼び出し関数側で
Array array;
array.nData=20; // 配列はnData=10で初期化されているはず...
というコードを書く事ができます.この状態で「AllDisplay()」を呼び出すと
array.AllDisplay(); // 実行時エラー
// ループが配列の要素数を超えている!
というエラーが発生する事になります.
さて,このようなエラーが発生するのは,一体誰が悪いのでしょうか?
答えはnDataに変な値を入れた使う側(クライアント)ではなく,そんなことができるようにクラスを設計した人がいけないのです.
このへんは,後々クラス設計のところでしっかりと説明したいと思います.
では,このような悲劇を引き起こさないためにはどうしたらよいのでしょうか?
答えは簡単で,このように変えてほしくない変数は「private」にしてしまえばよいのです.
private変数にしてしまえば,使う側からは値をいじる事ができなくなります.
「protected」ではない理由は後々説明します.
よって,結論からするとクラスのメンバ変数は全て問答無用に「private」変数にしてしまいましょう.
それで全て解決です.
では,どうしても変数を読み書きしたい場合はどうすればよいのでしょうか?
それは,全て関数にしてしまえばよいのです.
例えば,先程の例の場合
void NData(int _nData){ nData=_nData;} // 代入するだけ
読みたい場合は,当然
int NData(){ return nData; }
// 本当はconstをつけて
// int NData() const { return nData; }
// としたい.その理由はまたあとで
にすればよいです.
ところで,同じ関数名をもつ関数を定義してよいのか,という疑問もあるかと思いますが
C++には関数のオーバーロード機能というのがあり,引数と返り値が違うので問題なく動作します.
それはともかく,このようにクラスのメンバ変数にはこのように関数経由でアクセスするようにしましょう.
ただし,アクセスする必要がない変数に対しては,関数を定義してはいけません.
間違った使い方をしてしまうような関数は定義してはいけません.
どうしても必要な関数のみを定義しましょう.
これは,メンバ関数も同じです.
例えば,
class Sample{
private:
public:
Sample(){
initA();
initB();
}
initA(){...} // 初期化関数A
initB(){...} // 初期化関数B
...
};
としたとき,initAとinitBはpublicである必要があるでしょうか?
ユーザーが初期化関数を呼ぶ必要がない,もしくは呼ぶと問題が発生するのであれば,迷わず
private:
initA(){...} // これは呼び出せない
initB(){...} // これも呼び出せない
public:
Sample(){ // ここ以外で初期か関数を呼ぶ必要がない
initA();
initB();
}
...
};
としましょう.
何度も言いますが,クラスを間違った使い方をして,変な動作をするのであれば,それはクラスを設計したあなたの所為です.
このように,クラスの中身を見せないようにして,全てを関数で操作するようにクラスを設計する事をカプセル化(Capsulation)といい,とても重要な概念を表しています.
継承 [#fb988daf]
さて,オブジェクト指向を実現化するための重要な要素である継承について説明したいと思います.
概念としては重要で,これによってクラスおよびプログラムの再利用とを容易にします.
A is a B (継承) [#y495466e]
まずは一般的な継承について説明します.
ここのサブタイトルである「A is a B」というのは後で説明したいと思います.
さて今,以下のような移動ロボットを表すクラスがあるとします.
class Robot{ // 移動ロボット
...
public:
...
void Move(); // ロボットが移動
};
ロボットはMove関数で移動する事ができます.
さて今,新しいロボットを作る事とします.そのロボットも同じく移動ロボットであり,それに加えセンサをつけたとします.
さて,この新しいロボットのクラスは以下のように作れます.
class NewRobot{ // 新しい移動ロボット
...
public:
...
void Move(); // ロボットが移動
int Sensing(); // センサの情報を取得し,その値を返す
};
さて,このクラスにはRobotクラスと同じ機能であるMove関数があります.
当然,両方ともに移動ロボットであるので移動ができる事になります.
さて,このように新しいロボットを大量に作りたい場合,おなじ部分(今回はMove関数)をコピペしないといけないのでしょうか?
それは非常に大変です.
さてこのように同じようなもので,同じ機能を持つクラスを新たに作る場合に便利なのが「継承」(Inheritance)と呼ばれる機能です.
さて,継承した新しいロボットクラスは次のようにかけます
class NewRobot : public Robot{ // Robotクラスを継承した新しい移動ロボット
...
public:
...
int Sensing(); // センサの情報を取得し,その値を返す
};
さて,これで
NewRobot robo;
robo.Move(); // ロボットクラスと共通の移動機能
ロボットが移動ができるようになります.
A has a B(実装) [#w33e67ae]
ところで,継承と似た概念で「実装」というのがあります.
詳しい説明をする前に例を示します.
センサを管理するクラスがあり,ロボットにセンサをつけることを考えます.
センサのクラスは
class Sensor{ // センサを管理するクラス
private:
...
public:
... // コンストラクタ等は省略
int data; // センサで取得したデータを保存する変数
int GetSensor(){ return data; } // センサ情報を受け取る関数
// 保存されている変数を読むだけ
};
とします.
さて,このセンサを先程のロボットクラスに取り付けるとすると継承を使って以下のようなロボットクラスが作れます.
class NewRobot : public Robot, public Sensor{ // センサを搭載した新しい移動ロボット
...
public:
...
};
これで,ロボットは
robo.GetSensor();
でセンサー値を取得する事ができるようになりました.
しかしこれで本当によいのでしょうか?
良いクラス設計の観点から言うとだめです.
例えばこのクラスにこんな関数を定義します
void NewRobot::doSomething(){ // 何かしらの処理をする
...
data=1; // Sensorクラスの変数にアクセスしてしまう
}
こんな関数を作ってしまった場合,
robot.GetSensor(); // 正しいセンサの値が読める
robot.doSomething(); // センサデータに値を書き込んでしまっている!!
robot.GetSensor(); // 本来のセンンさの値が読めていない!!
こんな問題を引き起こします.
前にも言いましたが,この場合,doSomething()の使い方に気をつけなかったユーザが悪いのではなく,そんな問題を引き起こすようなプログラム設計者が悪いのです.
また問題なのは,doSomething()の設計に問題があるのではなく,NewRobotクラスにSensoerクラスを継承させるようなクラス設計をした人が悪いのです.
つまり,クラスに機能を追加する場合なんでもかんでも継承すれば良いという訳ではないのです.
上の例では,問題点は2つあります.
1つは先程説明したように,data変数がユーザからアクセスできる「public」に定義されている事です.
これは「private」にすることで解決できますし,通常はそうするべきです.
これによって,継承した先でdata変数にアクセスできないようになり,先程の問題は解決できます.
さて,もう1つの問題がここでは重要になります.
その答えが,NewRobotクラスがRobotクラスを継承するのは良くて,Sensorクラスを継承してはいけないという考え方なのです.
つまりそれが,「A is a B」と「A has a B」の違いなのです.
「Bはある一種のAである」ということと,「AはBという機能を持っている」との違いです.
なので,BはAと基本的には同じものではありません.先程の例だと,新しいロボットはあくまでもロボットであり,センサではないという事なのです.
「Robot is a Sensor」はまちがっていて,「Robot has a Sensor」であるべきということです.
なので,NewRobotクラスはRobotクラスを継承して作るべきですが,NewRobotクラスがSensorクラスを継承するのは間違っているということなのです.
それでもSensorクラスを継承しないと使えないというのであれば,それはSensorクラスの設計が間違っているという事なのです.
ここで,継承ではうまくいかなくなる例を示します.
例えばロボットが2つ以上のセンサを搭載していたらどうしたらよいのでしょうか?
継承ではうまくいきそうにありません.
そうなれば当然,次の様にクラスを設計しなければなりません.
class NewRobot : public Robot{ // センサを"2つ"搭載した新しい移動ロボット
private:
Sensor GPS; // GPSセンサ
Sensor Laser; // レーザーセンサ
// 共にSensorクラスで記述可能
public:
...
};
こうすれば,ロボットはセンサをいくつでも実装することできます.
そもそも似たような機能を複数個作るために,クラスは作られているので,このように設計するのは,当然とも言えます.
これで継承すべき時とそうでないときのやりかたが分かったと思います.
A is implemented in terms of B (実装) [#p68a992b]
基本的には「A has a B」の話と同じで,継承ではなくクラスを実装するというお話です.
仮想関数 [#m2161c5e]
仮想関数は,クラスを継承したときに威力を発揮します.
説明はともかく,例を見ていきたいと思います.
先程のロボットクラスには「Move()」関数が定義されていました.
これは,名前の通りロボットが移動するための関数です.
しかしどのように移動するのかは定義していませんでした.車輪かもしれませんし,2足歩行かもしれません.
今,車輪ロボットと,2足歩行ロボットを考えます.
移動手段は車輪と足と異なりますが,センサをと搭載していて,そのセンサの値によって自律移動する事ができるロボットします.
つまり移動手段が違うだけでそれ以外の処理は全く同じという事になりますし,移動も前進したり曲がったりするという点では同じです.
さて,このような車輪ロボットクラスと2足歩行ロボットクラスを作る事を考えます.
いままでのようにクラス設計しようとすると次のようになると考えられます.
class Robot{ // ロボットを構成する共通の部分
private:
...
public:
Sensor sensors; // 搭載しているセンサ
...
};
class WheeledRobot : public Robot{ // 車輪ロボット
private: // 共通の部分であるRobotクラスを継承
...
public:
...
void WheelDrive(); // 車輪で移動する
void AutoNavigation(){ // 自律移動する関数
...
WheelDrive(); // センサの値からなにかしらの処理をして
} // その情報に基づいて移動する
};
class HumanoidRobot : public Robot{ // 2足歩行ロボット
private: // これも共通の部分であるRobotクラスを継承
...
public:
...
void Walking(); // 歩いてで移動する
void AutoNavigation(){ // 自律移動する関数
...
Walking(); // センサの値からなにかしらの処理をして
} // その情報に基づいて移動する
};
車輪,歩行で移動するそれぞれのロボットは,その移動手段に基づいた移動関数を持っています.
自律移動する関数「AutoNavigation()」は関数名は同じの方が使いやすいはずです.
これでユーザは
WheeledRobot wrobo;
HumanoidRobot hrobo;
wrobo.AutoNavigation(); // 車輪ロボットが自律移動
hrobo.AutoNavigation(); // 2足ロボットが自律移動
としてロボットが自律移動できるようになります.
さてさて,このクラス設計は果たして最適なのでしょうか?
ある程度共通の部分とした基底クラス(Robotクラス)をつくり,継承してそれぞれのロボットを派生させているので,オブジェクト指向っぽく見えます.
しかしよく考えると,まだまだ共通の部分がある事に気づきます.
それは自律移動関数である「AutoNavigation()」で,実はこの機能は,ロボットの持つ共通の機能でなのです.
それぞれで定義されているWheeledDrive,Walking関数は"移動"するという意味では共通であると考える事ができます.
しかし今回の場合,移動手段が違うために「AutoNavigation()」の中で必要な移動手段関数の中身を変える必要があったので別々に定義する必要がありました.
これをうまく解決し,Robotクラスで「AutoNavigation()」を定義し,派生クラスではその異なる移動手段のみを定義するだけでよい方法はないでしょうか?
この問題を解決する手段として「仮想関数」があるのです.
基底クラスで定義した仮想関数を派生クラスでその中身を上書き(Over ride)することで,共通部分はそのままにしてより多様な機能を備えさせる事が可能となります.
とりあえず,先程のクラスを仮想関数を使って見たいと思います.
class Robot{ // 新しく定義したロボットを構成する共通の部分
... // センサなどは同じ
virtual void Move(); // ロボットが移動する"仮想関数"
// どのように移動するかは未定義
void AutoNavigation(){ // 自律移動する関数
...
Move(); // 移動手段は分からないが,とりあえず移動
}
...
};
class WheeledRobot : public Robot{ // 車輪ロボット
... // 共通の部分であるRobotクラスを継承
void Move(){ // 基底クラスのMove関数を再定義
... // "車輪"で移動するように記述
}
};
class HumanoidRobot : public Robot{ // 2足歩行ロボット
... // これも共通の部分であるRobotクラスを継承
void Move(){ // 基底クラスのMove関数を再定義
... // "2足"で移動するように記述
}
};
このようにすることで,派生クラスである車輪ロボットと2足歩行ロボットクラスはシンプルになり,定義するのはMove関数だけになりました.
基底クラスで何かしらの手段でロボットが移動するMove関数を定義しました.
これは前の例ではWheeledDriveやWalking関数にあたる部分です.
手段はことなりますが,"移動"するという意味では同じであるためMoveという関数で定義されています.
これによって,さきほどはバラバラに定義したAutoNavigation関数内で呼んでいる移動するための関数はMove関数だけとなり,1つにまとめる事ができます.
これでまた一歩,オブジェクト指向プログラミングに近づきました.
ただ,別々に定義する事が間違いという訳ではありません.
要は,どちらがより親切で,シンプルで,効率的かという事です.
最終更新:2010年12月03日 13:27