C++入門
オブジェクト指向を理解しよう


目次

 1ページ目
 第1話 オブジェクト指向入門【UPDATE】(2010/03/14)
 第2話 オブジェクト指向での概念
 第3話 クラスの作成
 第4話 コンストラクタとデストラクタ
 第5話 ポインタ型と参照型

 2ページ目
 第6話 継承の基礎
 第7話 関数のオーバーロード
 第8話 関数のデフォルト引数
 第9話 あいまいさの問題
 第10話 仮想関数のオーバーライド

 3ページ目
 第11話 テンプレート関数
 第12話 テンプレートクラス
 第13話 フレンド関数

 関連ページ
 C言語の話(目次)
 C言語入門
 Windowsプログラミング入門



ちょっとリニューアルしました。記事を書いたのが6年も7年も前・・・どころか10年前じゃね?これ書いたの? ということで、随時加筆・修正していきたいと思います。

C++のページはいろいろと酷いな・・・アクセス数少なくて良かった。 ここは大幅に書き直すかもしんない。(2010/03/14)


オブジェクト指向入門


オブジェクト指向という考え方が生まれる以前は、構造化プログラミングという考え方が主流でした。

構造化プログラミングとは、ある処理のまとまりを関数(サブルーチン)という単位で実装し、それを組み合わせることでプログラム全体を構築する方法です。

C言語は構造化プログラミング言語の代表格です。C言語以外にも、FortranやPascalなど旧来の主だった プログラミング言語は大抵この構造化プログラミング言語に属します。


基本的に、1つの関数は1つの機能のまとまりとして実装されます。 そして、その関数が集まって1つの大きなプログラムとなるのです。


この考え方はプログラマーに多くの恩恵をもたらしました。


例えば、複数人でのシステム開発が容易になる点です。

関数という明確な単位にシステムを分割することで、Aさんは関数1、Bさんは関数2と関数3、 Cさんは関数4を担当してもらうというように開発分担が容易に行えます。

また、関数とは機能のまとまりとして実装されたものなので、ソフトAとソフトBの両方である共通する機能が 必用であった場合、その機能をもつ関数を1つ実装してしまえば、 ソフトAとソフトBの両方でその関数を使い回せばよいので開発の手間を軽減することができます。

これは生産性の向上を意味してますので、プログラマーは楽が出来て万々歳なわけです。



最高!C言語さいこーヽ(´∇`)ノ



しかし、そんな構造化プログラミングも、プログラムが大規模になるにつれて「使えん奴だ」ということが明らかになってきました。

その最たるものが、データの管理の問題です。 構造化プログラミングにおけるグローバル変数・構造体の存在が、 大規模プログラムにおいては大量のバグをもたらす原因になってしまうのです。


以下のプログラムを見てください。

#include <stdio.h>

void func1(void);
void func2(void);

//このグローバルな存在が問題なのだ!
struct data{
	int a;
	int b;
};

int main(int, char**)
{
	struct data s_data;

	//main関数からdata構造体にアクセス可能
	s_data.a = 1;
	s_data.b = 5;
	printf("s_data.a = %d, s_data.b = %d\n", s_data.a, s_data.b);

	func1();
	func2();//もしこれが意図しない処理になってたとしたら・・・

	printf("s_data.a = %d, s_data.b = %d\n", s_data.a, s_data.b);

	return 0;
}

//func1からdata構造体にアクセス可能
void func1(void)
{
	s_data.a = 10;
	s_data.b = 15;

	printf("s_data.a = %d, s_data.b = %d\n", s_data.a, s_data.b);
}

//func2からもdata構造体にアクセス可能
void func2(void)
{
	s_data.a = 444;
	s_data.b = 777;
}

どうでしょうか?data構造体に対して何処からでもアクセスできちゃいますね。
自由にアクセスできるというのは便利なようにも思えますが、データ保護の観点から言うと最悪です。


まさに、家に鍵を掛けずに外出するようなものです。自由に開くドア、誰もいない屋内、泥棒さん狂喜乱舞です。 上記のソースコードで例えると、func1をあなたの家族、func2を泥棒だと思ってください。

func1はあなたの家族ですから、その家族が冷蔵庫の中をあさって何か食べても問題ないですね。

しかし、func2はあなたと何の関係も無いただの犯罪者です。その犯罪者が「あ〜腹減った」とばかりに 勝手に家に侵入し、冷蔵庫をあさり、楽しみにとっておいた食後のデザートを食い散らかすのです。

食い物が無くなる程度ならまだいいでしょう。財布や預金通帳、実印が盗まれたりしたらどうです? 「お札の束」が「ただの紙切れの束」に変わってたら? 鍵を掛け忘れたことを大いに後悔することでしょう。


趣味と異なり、ビジネスの場でのプログラム開発とは、1人ではなく複数人での開発体制になることが一般的です。 つまり、func1とfunc2を別々のプログラマーが担当することになるのです。

func1を担当したプログラマーがdata構造体への値を適切に設定しても、 func2を担当したプログラマーのうっかりミスで構造体の値がめちゃくちゃになる、なんてこともあり得るのです。


かくして、泥棒というバグを食らったプログラマーは、構造体という「玄関開けっ放し状況」を打開すべく オブジェクト指向プログラミングという考えを生み出した訳です。


これがオブジェクト指向が出てきた一つの理由です。


オブジェクト指向での概念


オブジェクト指向において<カプセル化>と<ポリモーフィズム>と<継承>は知っておくべき最低限の概念であると言えます。 名前だけを眺めると何だか小難しいもののように感じられますが、言ってることはどうということはありません。

それでは一つずつ内容をみていきましょう。

<カプセル化>

カプセル化とは、”手続き”と”データ”を一体化してしまう技術のことです。 これまでの構造化プログラミングでは手続きとデータがバラバラであったため、どの手続きがどのデータにアクセスしているのか 不明確になり易くバグを生む原因になっていました。そこで、バラバラのままじゃマズイので”手続き”と”データ”を カプセルみたく包んでしまえという発想が出てきたのです。カプセルに包まれた結果、内部の情報が隠蔽されますので外部からの 干渉や誤用からカプセル内部を保護することが出来ます。また、”手続き”と”データ”が一体化されてるということは それ自体で何らかの処理が可能であることを示しています。 つまりカプセル化は自己完結型のブラックボックスを作ることとも言える訳です。そしてこのブラックボックスのことを ”オブジェクト”と呼びます。 具体的にオブジェクトの作成はクラスの定義から始まりますが、詳しくは後に述べます。

<ポリモーフィズム>

ポリモーフィズムとは、同一の名前で複数の目的に使用するための性質のことです。 ”多態性”と呼ばれたりもします。要は複雑さを減らすための技術なのですが、実例をもって話した方が説明が手っ取り早いので 後に述べる”関数のオーバーロード”と”演算子のオーバーロード”のところで具体的にみていきましょう。

<継承>

オブジェクト指向の中でも恐らくメインとなる話でしょう。 継承とは、新しくオブジェクトを作るとき、既にあるオブジェクトの性質を受け継ぐプロセスのことです。 性質を受け継げるというメリットは非常に大きいです。

よくある例え話を用いて説明しましょう。
今、犬というオブジェクトと猫というオブジェクトを作るとします。 その際、犬とはどういうものか猫とはどういうものかをクラスとして定義する必要があります。では定義してみましょう。


犬とは…耳があり鼻があり目があり口があり胴体があり足がありワンワンと鳴く動物です。
猫とは…耳があり鼻があり目があり口があり胴体があり足がありニャーオと鳴く動物です。


この2つの定義を見ると分かるように犬と猫とで違っている所は鳴き方がワンワンかニャーオかの違いだけです。 にもかかわらず、耳があり鼻があり・・・と同じことを繰り返している訳です。今は犬と猫だけですので良いですけど ここにあとサルと羊とヤギとカバと象と・・・・・という風に定義せよといわれたらうざったくてやってられませんね。 非効率もいいところです。そこでこの状況を打開すべく継承を導入するのです。 犬と猫に共通しているのは両者ともに動物であるということです。そこでまずいきなり犬と猫を定義するのではなく 動物クラスを定義するのです。


動物とは…耳があり鼻があり目があり口があり胴体があり足がある生物である。


次に犬・猫クラスを定義する際に動物クラスを継承します。


犬とは…動物であり、ワンワンと鳴きます。
猫とは…動物であり、ニャーオと鳴きます。


どうでしょう。最初の定義に比べてシンプルになり分かりやすくなりましたね。
一度動物クラスを定義してしまえば、それを継承することによってあとは犬・猫クラスに独自の機能や性質を追加するだけで済むので 随分楽になります。他の動物を新たに定義する際にもこの動物クラスを継承して、その動物クラスに対する差異のみを 追加するだけで新たな動物が定義できます。

クラスの作成


オブジェクト指向プログラミングでは、クラスを作りそれを組み合わせることによってアプリケーションを構築していきます。 今回はそのクラスをどのように作成していくのかについて見ていきましょう。
クラスは次のように宣言します。
class クラス名{
	//ここにprivateな変数と関数を記述
public:
	//ここにpublicな変数と関数を記述
};
これでクラスの宣言は終わりです。形としては構造体のパワーアップバージョンだと思って良いです。 構造体が変数しか記述できなかったのに対して、クラスでは関数も記述することが出来ます。 そう言われると「な〜んだ、クラスってただそれだけのことなのか」と思われるかもしれませんが、その”構造体に関数が加わった” という”ただそれだけのこと”がアプリケーションの作成に多大な恩恵をもたらしてくれるのです。詳しくは ”オブジェクト指向入門”と”オブジェクト指向での概念”の部分を読んでください。気分がのったらオブジェクト指向に関して もっと突っ込んだ話をしていきたいと思います。

クラス宣言の内容をもう少し詳しく見てみましょう。
宣言の形は構造体の宣言と似ています。構造体宣言では”struct 構造体名”ですがクラス宣言では”class クラス名”となります。 クラス内で宣言される変数と関数は”メンバ”と呼ばれます。そしてこのメンバに対しクラス内の他ののメンバからしかアクセス 出来ないメンバを private 、クラス外からでもアクセスできるメンバを public として記述します。 情報隠蔽の観点から、メンバはできる限りprivateに記述するようにして、外部とのアクセスがどうしても必要なメンバだけを publicに記述するように心掛けましょう。
それでは実際にクラスを作ってみましょう。

class fighter{
	int x, y; //座標
	void move_object(void);
public:
	void init_object(int, int); //初期状態の設定
	void key_control(int);      //位置の変更
};

void fighter::move_object(void){

	cout <<"戦闘機は座標(" << x <<","<< y <<")に移動しました" <<"\n";
}

void fighter::init_object(int set_x, int set_y){
	
	x = set_x;
	y = set_y;
}

void fighter::key_control(int key_num){

	switch (key_num){          //テンキーによる入力を想定
		case 4: x -= 1; break;
		case 6: x += 1; break;
		case 2: y -= 1; break;
		case 8: y += 1; break;
		default:
	}
}
何らかのシューティングゲームを作ることを想定して”戦闘機クラス”を作ってみました。
説明の続きはまた今度・・・・・(気が向いたら追加します)


コンストラクタとデストラクタ


コンストラクタは構築子とも呼ばれ、またデストラクタは消滅子とも呼ばれています。 これらは一体何物なのかというと、オブジェクトを生成したり破棄したりする際に、それに伴って必要になる処理を実行するための関数なのです。 これらの関数の記述には規則があります。

class クラス名
{
public:
	クラス名( 引数 ){
		//コンストラクタの中身
	}
	
	~クラス名(){
		//デストラクタの中身
	}
};
コンストラクタの関数名はクラス名と同名でなければなりません。これは規則ですのでこれ以外の書き方は許されません。 また、コンストラクタは引数を取得することができます。
デストラクタはクラス名の前に ~ (チルダ)を付けたものが関数名になります。デストラクタはコンストラクタと違い引数を取得できません。
コンストラクタとデストラクタに共通して言えることは、”戻り値がない”ということです。間違ってもvoidなどと書いてはいけません。 戻り値は"void"なのではなく"無い"のですから・・・・

次に実際にコンストラクタとデストラクタの使用例を見てみましょう。

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <ctime>
#include <conio.h>

using namespace std;

#define FULL_FUEL 100         //燃料のMAX値
#define INIT_FUEL 100         //燃料の初期設定
#define DISTANCE 100          //移動距離
#define METER_ELEMENT_NUM 10  //メーターの目盛の数
#define WAIT 10000            //時間稼ぎのためのウエイト

class car
{
	char *meter;       //残りの燃料を可視化
	int fuel;          //残りの燃料
	int rest_distance; //残りの距離
public:
	car();  //コンストラクタ
	~car(); //デストラクタ
	void set_meter(void); //meterに現在の燃料の状況を表示
	void move_car(void);
};

car::car()
{
	rest_distance = DISTANCE;
	fuel = INIT_FUEL;
	meter = (char*) malloc(FULL_FUEL * 2 + 1);

	//メモリの確保に失敗したときの処理(省略)
}

car::~car()
{
	free(meter);
}

void car::set_meter(void)
{
	int count_fuel = 0;
	int fuel_per_square;
	char full_fuel[] = "■■■■■■■■■■";   //燃料有り
	char empty_fuel[] = "□□□□□□□□□□";  //燃料無し

	fuel_per_square = (int)(FULL_FUEL / METER_ELEMENT_NUM); //燃料メーター■1つあたりの燃料量

	for(int i = 0; i < (METER_ELEMENT_NUM * 2); i++){   //■は2バイトなので処理を倍に

		if ((count_fuel - 4) < fuel) //何故か"-4"を付けないとうまくいってくんない
			meter[i] = full_fuel[i];
		else
			meter[i] = empty_fuel[i];

		//このままではfuel_per_squareが奇数の場合マズイ!
		count_fuel += (int)(fuel_per_square / 2);
	}

	for (int j = 0; j < (METER_ELEMENT_NUM * 2); j++)
		cout << meter[j];
}

void car::move_car(void)
{
		rest_distance--;  //距離1に対し
		fuel--;           //燃料1の消費

		cout << "残りの距離:" << rest_distance <<" 残りの燃料:"<< fuel <<"  ";

		if (rest_distance == 0){
			cout <<"到着しました"<<"\n";
			exit(0);
		}else if(fuel == 0){
			cout<<"燃料切れです"<<"\n";
			exit(0);
		}
}

int main(int, char**)
{
	int counter = 0;
	car impreza;
	while (!_kbhit()){ //キー入力によりループ脱出

		if (!(counter % WAIT)){    //時間稼ぎ

			cout << "\x1b[5;5H" <<"\x1b[>5h"; //画面表示の処理

			impreza.set_meter();
			impreza.move_car();
		}

		counter++;
	}

	return 0;
}
この例でいうと、コンストラクタは private の部分に記述されているメンバ変数を初期化して、ヒープ領域にメモリを確保しています。 そして、デストラクタはコンストラクタで確保されたメモリを開放しています。
・・・・・もう少しシンプルな例を近いうちに追加します。(あと解説も)


ポインタ型と参照型


プログラムを組んでいると関数から複数個の戻り値が欲しくなることがしょっちゅうあります。 しかし関数の約束事として、引数は複数個OKですが戻り値は1つだけです。つまり、

int int func(int a, int b, int c)
{
	return a+b;
	return b+c;
}
などといった離れ業はできません。しかし現実問題として複数の戻り値が必要なことはしばしばあるのです。 そのような場合どうすれば問題が解決するのかを次に見ていきます。 下のプログラムはswapという関数を用いて変数aとbの保持する値を入れ替えてしまおうというものです。

#include <stdio.h>

void swap(int a, int b);

int main(int, char**)
{
	int a = 5;
	int b = 10;

	void swap(a, b);
	printf("a=%d, b=%d",a ,b);

	return 0;
}

void swap(int a, int b)
{
	int temp;

	temp = a;
	a = b;
	b = temp;
}
このプログラムを実行してみてください。期待した結果にはならないと思います。 本来なら(a=5,b=10)はswap関数で値が入れかえられ(a=10,b=5)となるはずでした。 しかし期待を裏切り画面上にはa=5,b=10と表示されてしまいます。ではswap関数自体に問題が あるのでしょうか?そんなことはありません。試しにswap関数に次のようにprintf関数を追加して 実行してみてください。

void swap(int a, int b)
{
	int temp;

	temp = a;
	a = b;
	b = temp;
	printf("swap内部:a=%d, b=%d",a ,b); //これを追加
}
swap内部のaとbの値が見事に入れ替わってますね。一体これはどういうことでしょうか? 実はこれはmain関数の変数a,bとswap関数の変数a,bが別物であることに起因しています。 つまりmain関数で宣言された変数a,bに格納された値は、swap関数で新たに宣言された 単に名前だけが同一の別物変数a,bにコピーされたにすぎないのです。従って、 関係の無い変数の値がどう変わったところで、main関数の変数a,bに影響があるわけないのです。

ではどうすれば望み通りに結果が出るのでしょう? 答えは簡単です。先のプログラムの問題は無関係の変数に値をコピーしてしまったことにあります。 つまり、変数の名前が同じだということだけで同一の変数なんだと勘違いしていることに問題があるのです。 よって解決策はmain関数で宣言した変数a,bそのものをswap関数に渡してやれば済むことです。 言い方を変えるならswap関数にmain関数の変数a,bのポインタを渡せばよいのです。次にその例を示します。

#include <stdio.h>

void swap(int *a, int *b);

int main(int, char**)
{
	int a = 5;
	int b = 10;

	void swap(&a, &b);
	printf("a=%d, b=%d",a ,b);
	return 0;
}

void swap(int *a, int *b)
{
	int temp;

	temp = *a;
	*a = *b;
	*b = temp;
}
これでやっと期待通りの結果が返ってきたと思います。(ポインタそのものの説明は気が向いたときにでも書いときます)
本当はもうこれで終わりってことにしてもいいんですが、ついでに参照型の説明もしちゃいましょう。 C++ではポインタ型以外に参照型と呼ばれるものがあります。実はコンパイルしてしまったらポインタ型 も参照型も同じことになってしまいます。マシン語レベルでは両者に違いはありません。 にもかかわらずC++で参照型が新たに加わったのはプログラマーにとって扱いやすいからです。 上記のプログラムを参照型を用いるとどうなるか以下に示します。

#include <stdio.h>

void swap(int &a, int &b);

int main(int, char**)
{
	int a = 5;
	int b = 10;

	void swap(a, b);

	return 0;
}

void swap(int &a, int &b)
{
	int temp;

	temp = a;
	a = b;
	b = temp;
}
以上です。ポインタ型を使った例よりもかなりシンプルに見えると思います。

最後に今回の問題をポインタ型も参照型も使わずに解決する方法を言っとくと、 main関数の変数a,bをグローバル変数にしてしまえばOKです。 ある意味誰もが最初に気が付く解決方法かもしれませんが、最悪です。グローバル変数は 極力使わないよう心掛けてください。バグの原因になります。(それだけでなく山のように問題が出てきます)





C++入門Topへ