抽象クラスと仮想関数を使った実例


抽象クラスと仮想関数をつかったシナリオ

これまで学んだ抽象クラスや仮想関数が実際にどのように使われるかを、以下のようなシナリオとともに、ほんのちょっとだけ大きなプログラムで考えてみましょう。AliceとBobのコンビに登場してもらいます。

Alice は、要素の追加、要素の並びの反転、要素に対して印刷の指示、といった機能を持つコンテナ(なにかを格納するもの)をコーディングします。格納されるものの実態はよく知らされていませんが、Print() という印刷を行う仮想関数を持っていることは知っています。

Bob は、Aliceのコンテナを使って整数や文字列を格納し、これらの印刷と並びを逆順にする機能を使いたいとします。

Aliceは、並び替えを行う要素の基本クラスとなる Elem抽象クラスと、これを管理するListクラスを書いてBobに提供します。ListクラスはElemクラスのポインタの管理をします。実際にどのようなオブジェクトへのポインタが格納されるかは知りませんが、Elemクラスを使って並べ替えのコードを書くことができます。

Bob は Aliceに提供された Elemクラスを派生して、自分が実際に使いたい IntegerクラスとStringクラスを実装します。インスタンス化したIntegerとStringのポインタを、Aliceが提供してくれた Listクラスで管理しますが、実際にどのようなアルゴリズムで並べ替えをしているかは知りません。継承と仮想関数を使った多態の性質により、IntegerやStringといった性質の異なるオブジェクトも区別なくひとつのコンテナに格納することができます。

 

それでは、コードは以下のようになります。

list.h

// 追加(Add) と印刷(Print) そして逆順への
// 並び替え (Reverse) の機能をもったコンテナ


#ifndef _INCLUDE_LIST_H_
#define _INCLUDE_LIST_H_

//とりあえず、Printという純粋仮想関数を持った Elem という抽象クラスを定義しておいて
class Elem {
public:
	virtual void Print() = 0;
};


//このElemを格納する Listクラスを書く
//何が格納されて、どのように印刷されるかなんて知らない
class List {
public:
	List(int _max); //コンストラクタ 引数で配列の最大値を設定
	~List();
	void Add(Elem* p);   //要素の追加
	void Print();        //要素の印刷
	void Reverse();      //要素の並びを反転
protected:
	//メンバ変数
	Elem** elemArray;    //要素を格納するポインタの配列
	int maxNum;          //配列のサイズ
	int num;             //格納済みの要素数

	// Listクラス内のみで使用するスタティック関数
	static void Swap(Elem* &l, Elem* &r); //要素のポインタのスワップ
	
};

#endif

list.cpp
#include "list.h"

// コンストラクタ
List::List(int _max) : maxNum(_max), num(0)
{
	// Elemクラスポインタの配列をアロケート
	elemArray = new Elem*[_max];   //Elemへのポインタを _max個確保
}

// デストラクタ
// 配列に格納された Elemクラスのポインタを解放した後に配列自体を解放
List::~List()
{
	for (int i = 0;i < num;i++)
		delete elemArray[i];
	delete[] elemArray;
}

// 要素の追加
void List::Add(Elem* p)
{
	if (num >= maxNum)
		return;

	elemArray[num++] = p;
}

// 要素の印刷
void List::Print()
{
	for (int i = 0;i < num;i++)
		elemArray[i]->Print();
}

// 要素の並びを反転
void List::Reverse()
{
	for (int i = 0;i < num / 2;i++) {
		Swap(elemArray[i], elemArray[num - i - 1]);
	}
}

void List::Swap(Elem* &l, Elem* &r)
{
	Elem* tmp;
	tmp = l; l = r; r = tmp;
}

main.cpp
#define _CRT_SECURE_NO_WARNINGS
#include <crtdbg.h>        // メモリリークのチェック用
#include <iostream>
using namespace std;

#include "list.h"

/////////////////////////////////////////////////////
// 整数を格納する Integerクラスを作成
// Elemから派生して、Printという仮想関数を実装
class Integer : public Elem {
private:
	int i_x;
public:
	Integer(int x) { i_x = x; }
	void Print() { cout << i_x << endl; }
};

/////////////////////////////////////////////////////
// 文字列を格納する String クラスを作成

class String : public Elem {
private:
	char* buf;
public:
	String(const char*);
	~String();
	void Print() { cout << "\"" << buf << "\"" << endl; }
};

// コンストラクタ
// 文字列の領域を割り当ててコピー
String::String(const char* str)
{
	buf = new char[strlen(str)+ 1];
	strcpy(buf, str);
}

// デストラクタ
// 文字列を格納した領域を解放
String::~String()
{
	delete buf;
}


////////////////////////////////////////////////////////

void main()
{
	/* メモリリークのチェックのための関数。              */
	/* プログラム終了時に自動的に _CrtDumpMemoryLeaks()が呼び出される */
	_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
	//Listクラスのインスタンスを作成
	List list(100);

	// Elemの派生クラスであればどんどん追加
	list.Add(new Integer(100));
	list.Add(new Integer(200));
	list.Add(new Integer(300));
	list.Add(new String("hello"));
	list.Add(new String("C++"));
	list.Add(new String("world!"));

	// 印刷
	list.Print();
	cout << "-------" << endl;

	//要素の並びの反転
	list.Reverse();

	//再び印刷
	list.Print();
}

練習問題8-1

上記のプログラムを実行して動作を確認しなさい。プロジェクト名は p3k3_8 とすること。

このプログラムでは、Listクラスのデストラクタ中においてリストに格納されている要素の delete を行っている。しかし、Stringクラスのデストラクタが正常に呼び出されていない。(そのため、文字列のために確保した領域がメモリリークしている。)

プリント文を入れたり、デバッガを使ったりしてデストラクタの呼び出し状態を確認しなさい。また、それはなぜかを考察し、Stringクラスのデストラクタが正しく呼び出されるように修正し、メモリリークがなくなることを確認しなさい。

(ヒント:デストラクタの仮想化)

練習問題 8-2

以下の課題は、list.hおよびlist.cppは練習問題 8-1で修正した個所以外は一切手を加えずに実現すること。main.cpp内は適宜修正してよい。

2次元座標を管理する Coordクラスを Integerクラスの派生クラスとして定義し、動作を確認しなさい。このとき、Integerクラスに修正が必要であれば施してもよい。Printの書式は自分で工夫して作成すること。

(補足: Coordクラスを作成するときには、すでに Integerクラスが持っている変数を利用してください。)

練習問題 8-3

コンテナに、要素の削除の機能を追加して使用したい。Listクラスを派生して 要素の削除を行う Delete メンバ関数を持った List2クラスを作成し、動作を確認しなさい。Deleteメンバ関数の仕様は、以下のようにすること。また、ファイルは適切に分割せよ。

void List2::Delete(int index);

int index: 削除する要素の添え字 (ただし、0-based (ゼロから始まる添え字)とする)

動作例

List2 list2;

list2.Add(new Integer(1));
list2.Add(new Integer(2));
list2.Add(new Integer(3));

list2.Print();
cout << "---" << endl;

list2.Delete(1); //index = 1の要素を削除    
list2.Print();
  
実行結果

1
2
3
---
1
3