C++入門
シープラスプラスにゅうもん

PCinfo トップページプログラミング専門分野ハードウエアその他

継承の基礎

継承に関してはオブジェクト指向での概念のところで大まかな考え方を話しましたが、 今回はそこでの話をベースにして実際プログラミングをしてみましょう。

それでは早速プログラムをみてください。「子クラスである"猫のたま"と"犬のポチ"」が「"動物クラス"(親クラス)」の 性質をちゃんと受け継いでいることを確認してください。

#include <iostream.h>

class animal{
public:
	void run()//走る
	{
		cout << "タタタタッ・・・\n";
	}
	void eat()//食べる
	{
		cout << "パクパクもぐもぐ\n";
	}
	void sleep()//寝る
	{
		cout << "くーくーすやすや・・・\n";
	}
};

class cat : public animal{
public:
	void scratch()//引っ掻く
	{
		cout << "ウニャ〜〜〜がりっっ!!\n";
	}
};

class dog : public animal{
public:
	void bark()//吠える
	{
		cout << "ガルルル・・・ワンワン!!\n";
	}
};

int main()
{
	cat tama;		//たまで〜す
	dog pochi;	//ポチで〜す

	tama.run();	//たまは動物なので走れま〜す!
	tama.eat();	//たまは動物なのでメシ食えま〜す!
	tama.sleep();	//たまは動物なので寝てしまいま〜す!

	tama.scratch();   //たまは猫独自の能力として"引っ掻く"ことができま〜す!

       //tama.bark();      ←エラーです(たまは犬ではないので吠えることはできません!)


	pochi.run();	//ポチは動物なので走れま〜す!
	pochi.eat();	//ポチは動物なのでメシ食えま〜す!
	pochi.sleep();	//ポチは動物なので寝てしまいま〜す!

	pochi.bark();	//ポチは犬独自の能力として"吠える"ことができま〜す!

       //pochi.scratch();  ←エラーです(ポチは猫ではないので引っ掻くことはできません!)

	return 0;
}

まずはメイン関数より上の部分をみていきましょう。

親クラスであるanimalクラスには子クラスであるdogクラスとcatクラスに"共通する性質"(関数)が記述されています。 次にdogクラスとcatクラスですが、これらのクラスにはそれぞれ"独自に備わっている性質"が記述されています。 加えてanimalクラスの性質を継承しています。

いつの間に継承した?って感じですけど "class cat" "class dog" の後に続けて : public animal と書かれて ありますね。これによりdogクラスとcatクラスはanimalクラスを継承するってことになるんです。 クラス名に続けて :public 継承するクラス名 でOKです。

これを見ておそらく「public以外にも private とかあるんじゃないの?」と考えたりすると思いますが、確かにあります。 それどころかprotectedとかvirtualなどという訳の分からんキーワードが付いたりもします。これらを駆使する ことによって継承にバリエーションがでるのですが、ややこしい部分でもあるのでまた別の機会にお話するということで 今は頭の隅っこの方に置いといてください(^^;)


次にメイン関数の部分です。

本来ならクラスの作成のところでしておくべき話を何故かしていないので ここで話します。それは "クラスをメイン関数内でどう使うか" ということです。
クラスというのが構造体のパワーアップバージョンであるという話は以前したと思います。つまりクラス自体は 構造体と同じく単なる "型" であり、変数でいうところの int や char, float みたいなもので、そのままでは 使えません。使えるようにするためには「"型"から"実体"を作り出してやる」必要があります。で、その作業を cat tama; dog pochi; のところでやっている訳です。ちなみに cat, dog という型から作り出した実体 tama, pochi のことをオブジェクト指向では "インスタンス" と呼んだりします。一応参考までに覚えといてください。

次にクラスに記述した関数を使う場合ですが、インスタンスの後のドット .に続けて関数を記述してください。 上記のプログラム例だけですと「戻り値とかある時どう書きゃいいの?」てことになりますが、一応…

戻り値 = インスタンス名.関数名(引数);

で大丈夫のようです。"一応"といったのはこの様な記述はあまり見かけないからです。(というか見たことない…) 通常、メンバ関数の戻り値はメンバ変数に対してのアクセスとなり、このように無関係の変数に対してアクセスする ようなことはありません。第一これではカプセル化のとこ(オブジェクト指向での概念 のとこの話です)で話した情報隠蔽というオブジェクト指向の重大なシステムが台無しです。よってこのような 記述もできないことはないですが、"やってはいけない"と心の中で呪文のように繰り返し唱えましょう(^^)

話を戻しますが、catクラスとdogクラスの中には run, eat, sleep という関数は記述されてませんが、これらが記述された animalクラスを継承しているため、問題なくtama と pochi からこれらの関数を呼び出せたと思います。また、cat, dog 独自の関数である scratchとbark もそれぞれ呼び出せていることを確認してください。ただし、コメントアウト してある部分のようなことは出来ません。試しにコメントを解除してコンパイルしてみてください。結果は予想通りといった ところでしょうか。

とりあえず継承の基本は以上です。たぶん難なく理解できたと思います。ただ、このような概念は”理解できた”からといって ”使いこなせる”ということには結びつきません。使いこなすためにはプログラムを書いて書いて書きまくるしかないでしょう な・・・あ〜大変(-_-)

目次へ







関数のオーバーロード

今回は関数の多重定義に関するお話です。

関数のオーバーロードというのは簡単に言っちゃうと、 「同じ関数名でも引数が違えば別物の関数として扱いますよ」 という仕組みのことなのです。
百聞は一見にしかずです。早速プログラムを見てみましょう。

#include <iostream.h>

void square(int);		//引数が「整数」の場合
void square(float);	//引数が「実数」の場合
void square(int, int);	//引数が「2つの整数」の場合

int main(int, char**)
{
	square(3);	//①を呼び出す
	square(2.5f);	//②を呼び出す
	square(2, 3);	//③を呼び出す
	return 0;
}

void square(int a)//①
{
	cout << a*a << endl;
}

void square(float b)//②
{
	cout << b*b << endl;
}

void square(int c, int d)//③
{
	cout << (c+d)*(c+d) << endl;
}

関数のオーバーロードでは引数に応じて自動的に それに応じた関数が呼ばれるようになっています。このsquareという関数は引数の二乗 を画面上に表示するものですが、その引数が「整数」か「実数」か 「2つの整数」かによって、呼び出される関数が自動選別されます。

この仕組みはC++になって登場したものであり、当然Cにはない概念です。

Cでこのようなプログラムを書く場合、それぞれ別の関数を用意しなければなりませんでした。 機能的には全く同じ関数であるにも関わらず、引数が違うだけで別の関数名に しなければならないというのは、非常に面倒な話です。関数のオーバーロードはそんな面倒を 解決してくれるための機能としてC++に採用されたのです。

以上見てきたように非常に便利な仕組みではあるのですが、オーバーロードした関数が デフォルト値を持っていた場合、関数の呼び出しで混乱が起こる可能性があります。 この話の続きは”関数のデフォルト引数”のところでしたいと思います。

目次へ







関数のデフォルト引数

C++では関数のプロトタイプの引数部分にあらかじめ値をセットしておくことができます。
そしてこのあらかじめセットされた値(デフォルト値)を持つ引数のことをデフォルト引数と呼ぶのです。
まずは次のプログラムを見てください。

#include <iostream.h>

void add(int, int);//プロトタイプ宣言

int main(int, char**)
{
	add(2, 5);
	return 0;
}

void add(int a, int b)
{
	cout << a+b << endl;
}

これはadd()という関数を使って足し算をするプログラムです。
この場合 2 と 5 の値が引数として与えられてますので 7 という結果が画面上に表示されます。

ではこのadd(2, 5)という部分をadd()として引数なしで実行したらどうなるでしょうか?
お分かりのように、エラーが出て実行どころかコンパイルすらできませんね。

ですがC++では関数にデフォルト値を与えることによって引数なしでも実行可能になります。

#include <iostream.h>

void add(int = 2, int = 5);//ここでデフォルト値を与える

int main(int, char**)
{
	add();//すると引数無しでもOKになる
	return 0;
}

void add(int a, int b)
{
	cout << a+b << endl;
}

ちなみにデフォルト値というのは引数に何も値がセットされてなかった場合 利用される値ですので、

     add(8, 4)

というように引数の値を伴って関数が呼び出された場合、2 5 のデフォルト値よりも 8 4 の方が優先されて 12 という表示結果になります。

また関数がデフォルト引数を持つ場合ちょっと変わったことができます。例えば

     add(8)

というように引数を一つだけで関数を呼び出すことも可能になります。
この場合、関数の第1引数に 8 が入り、第2引数にはデフォルト値である 5 が入る ことになり、13 という表示結果になります。

この様にデフォルト引数を持つ関数は引数の省略ができる訳ですが、

     add( , 6)

というように第1引数を省略することはできません。

目次へ







あいまいさの問題

今回はデフォルト引数を持った関数をオーバーロードした場合に発生する混乱についてお話したいと思います。 混乱と言いますのは、コンピュータがコンパイル時にどうしたらいいのか困ってしまいエラーを出す ということですが、時として人までも(本当の意味で)混乱してしまうことがあります。

では実際にその混乱ぶりを見てみましょう。

#include <iostream.h>

void add(int = 2);		//①
void add(int = 2, int = 5);	//②

int main(int, char**)
{
	add();		//コンパイルエラー
	add(7);		//コンパイルエラー
	add(4, 6);	//これだけならOK ( ②が呼ばれる )
	return 0;
}

void add(int a, int b)
{
	cout << a+b << endl;
}

void add(int a)
{
	cout << a+a << endl;
}

ちなみにVC++6.0でこのプログラムをコンパイルすると、コンパイラは次のように文句をたれてきます。

error C2668: 'add' : オーバーロード関数の呼び出しを解決することができません。

実はこのような文句を言われるのは、関数が曖昧な状況になっているためです。


では曖昧な状況というのは一体どういう状況なのでしょうか?


main関数部分にある3つのadd関数に注目してください。

最初に add() とありますが、この関数、一体①と②のどちらを呼び出そうとしているのでしょう。

おそらく大抵の人が「あれ?①と②どっちでも呼べちゃうんでないの?」と混乱するに違いありません。 このように、どちらでも呼べてしまう状況、言いかえればどちらを呼べばいいか分からない状況のことを 曖昧な状況というのです。人間にさえ分からないのですからコンパイラに分かるはずがありません。 よってコンパイラはエラーを出すのです。


では次の add(7) はどうでしょうか。

このプログラムの書いた人(そりゃ私ですが)の意図が理解できるでしょうか。 このプログラムを書いた人は①を呼び出そうと思ってadd(7)と書いた訳です。 確かにオーバーロードのルールからいくと引数が1つであることから①が呼ばれるような気はします。 しかし待ってください。関数がデフォルト引数を持っているときは、 引数の省略ができたはずです。

つまりこのプログラムの書いた人としては①を呼び出しているつもりなのですが、 コンパイラとしては①のadd(5)を呼んでいるのか、②のadd(7, 5)を呼んでいるのか区別できない訳です。
よってここでも曖昧さが発生しておりエラーとなります。


ですが3つ目の add(4, 6) には曖昧さがありません。
オーバーロードのルールからもデフォルト引数のルールからも②が呼ばれることになります。


では最後にこのような問題を回避するための方法を伝授しましょう。

オーバーロードとデフォルト引数は併用しない!

実に単純ですが、これが基本ですね^^;

目次へ







仮想関数のオーバーライド

まずは用語の説明から始めましょう。

仮想関数とは派生クラス(子クラス)で再定義されることを前提として 基本クラス(親クラス)で宣言されるメンバ関数のことです。そして、基本クラスで宣言された仮想関数を派生クラスで再定義することを オーバーライド(上書き)と 呼びます。

仮想関数の作り方はメンバ関数の前にvirtualというキーワードを付けるだけでOKです。

#include <iostream.h>

class animal
{
public:
	virtual void cry()
	{
		cout << "動物の鳴き声は・・・" << endl;
	}
};

class cat : public animal{
public:
	void cry()
	{
		cout << "ニャーニャー" << endl;
	}
};

class dog : public animal{
public:
	void cry()
	{
		cout << "ワンワンワン" << endl;
	}
};

int main(int, char**)
{
	animal pet;
	cat tama;
	dog pochi;

	pet.cry();
	tama.cry();
	pochi.cry();

	return 0;
}

実行してみると、animalクラスのcry( )が派生クラスであるcatクラスとdogクラスで見事にオーバーライド(上書き) されているのが分ります。

ところで、animalクラスのcry( )なのですが、この部分は実は無理に実装する(関数の中身を書く)必要はないのです。 上記のプログラムでは書くことが見つからないため cout << "動物の鳴き声は・・・" << endl;な〜んて 適当なこと書いてますけど、はっきり言ってこの部分不要です。無駄です。邪魔です。

よってそんな余計な部分はサクッと削除してやりましょう。

ただし、削除してやる際にちょっとしたルールがあります。基本クラスの仮想関数定義部分に「= 0」をつけて 実体をもたない仮想関数、つまり純粋仮想関数として定義する必要があるのです。

言葉で説明すると難しく聞こえるかもしれませんが、要は、

class animal
{
public:
	virtual void cry()
	{//←ここも削除
		cout << "動物の鳴き声は・・・" << endl;//←この部分を取っ払って
	}//←ここも削除
};
class animal
{
public:
	virtual void cry() = 0;//←とすればよい!
};

ということです。なにも難しいことはありませんね。

ただし、基本クラスが純粋仮想関数の場合、その派生クラスはオーバーライドを強制されることになります。 言い方を変えると、純粋仮想関数は派生クラスで必ず実装してやらねばならないのです。

上記のプログラムでいえば、ニャ—ニャーとかワンワンと書いてある部分を省略しちゃダメってことです。

目次へ