- 10.1 クラス
- 10.1.1 クラスの宣言
- 大きなプログラムの場合,その部分的な機能の修正のため,プログラム全体にわたって修正をしなければならないとしたら,大変なことになります.そこで,プログラムのモジュール化が非常に重要になります.各機能毎にモジュール化し,
- そのモジュール内部における詳細な処理を知らなくても,適当なインターフェースを介してデータを渡し,また,インターフェースを介して希望する結果が得られる.
- モジュールとのやりとりは,インターフェースを介してのみ可能であり,モジュールの外部から,そのモジュール内の処理を直接コントロールことはできない.
- ようにしておけば,対応する機能の修正はそのモジュールの修正だけで済みます.つまり,各モジュールをブラックボックス化するわけです.
- このモジュール化の一つの実現方法が関数です.引数を付けて関数を呼ぶというインターフェースによって,関数内部の処理の詳細を知らなくても結果を得ることができます.また,ローカル変数の存在により,関数外部から関数内部の処理を直接コントロールすることは基本的に不可能です.
- このように,関数もモジュール化の強力な手段ですが,それは,アルゴリズムに重点を置いたブラックボックス化です.しかし,場合によっては,データに重点を置いたブラックボックス化が必要になる場合があります.それが,まさに,C++ のクラスです.クラスでは,データとそれを扱う関数を一つにまとめ,特定のインターフェース(クラス内で宣言された関数)を介してのみ,その内部にアクセスできるようにしています.このような処理をデータの抽象化と呼び,抽象データ型の変数(つまり,あるクラス型として宣言された変数)をオブジェクト(データとそれを操作する手続き−関数−をひとまとめにしたもの,object )と呼びます.
- また,先に説明しましたように,クラスとは,ある「もの」に対し,その「もの」に共通する特徴等をもとにして,形式的な定義を与えたものであるといえます.もう少しプログラミング的な感覚でいえば,「もの」に共通するデータと,それらのデータを処理する方法を記述した手続きの集まりです.例えば,「車」というクラスを定義したとすれば,車の構造を記述するデータと車を動かしたりするのに必要な手続きからなっているはずです.また,クラスは「もの」に対する抽象的な定義ですが,そのインスタンス(オブジェクトとなる)はクラスを具体化したものに相当します.例えば,「車」クラスの場合であれば,そのインスタンスは,ある特定の人が所有する特定の車になります.
- 同じプログラムであっても,アルゴリズムに重点を置いたモジュール化を行うのか,または,データに重点を置いたモジュール化を行うのかによって,書き方はかなり異なってきます.問題によって,より適した方法があるはずです.以下の節で述べるクラスに関する詳細説明やプログラム例を見ながら検討してみて下さい.
- クラスは,その定義の方法から見て,ユーザーが新たに定義する変数の型と考えても良いと思います.新しい変数の型を定義すれば,その型の操作方法も必要になります.例えば,複素数に対応するような型をクラスによって定義したとします.すると,単純な加算ですら,既存の方法を使用することができません.従って,定義した変数の加算をどのようにして実行するかについても定義してやる必要が出てきます.そこで,クラスの定義には,単にデータだけでなく,そのデータを取り扱う方法を記述した関数が必要になってくるわけです.
- クラス( class )は,C++ の最も重要な概念です.基本的には構造体の拡張であり,キーワードの違いを除けば,その宣言方法も構造体と同じです.一般的な表現方法をすると,例えばクラス Example ( Example: クラス識別子)を,
class Example {
int x; // プライベートメンバー変数
・・・
protected:
int y; // 派生クラスだけが使用可能なメンバー変数
・・・
public:
int z; // パブリックメンバー変数
・・・
private:
double fun1(int); // プライベートメンバー関数
・・・
protected:
double fun2(int); // 派生クラスだけが使用可能なメンバー関数
・・・
public:
double fun3(int); // パブリックメンバー関数
・・・
friend double fun4(int); // フレンド関数
・・・
friend class Example2; // フレンドクラス
・・・
};
- のように宣言します.ただし,上の例における public,private 等に対応するすべてのメンバー( member )を所有する必要はありません.public 等を記述しないと,クラス宣言のメンバーがプライベートとみなされる点以外,記述する順番も一般的には決まっていません.なお,クラス識別子のあとに記述される項目も存在しますが,それらについては後ほど説明します.
- private の後に記述されたメンバーは,定義されたクラスのメンバー関数,フレンド関数,および,フレンドクラスだけから参照可能です.protected の後に記述されたメンバーは,定義されたクラスの派生クラス(後述)だけから参照可能です.また,public の後に記述されたメンバーは,どこからでも参照可能です.C/C++ の通常の関数や別のクラスに対して,メンバー関数と同様のアクセス権を与えようとしたものが,フレンド関数やフレンドクラスの宣言です.なお,メンバー関数の本体は,クラス定義の中に記述することも,クラス定義の本体では関数の宣言だけを行いクラス宣言の外側(別のファイルでも構わない)に記述することも可能です.
- 上の定義からも明らかなように,構造体との大きな違いは,メンバーとして関数を持てる点です.さらに,構造体ではそのメンバーをどの関数からでもアクセスできましたが,クラスでは,メンバーを private,public,及び,protected で分類し,各メンバーをアクセスできる範囲を限定しています.C++ は,構造体を,パブリックメンバー変数だけを持つ特殊なクラスと見なしていますので,後に述べるコンストラクタ,派生クラス等,クラスに特有な機能を構造体に対してもそのまま適用できます.
- 上のようにして宣言されたクラスのオブジェクトを生成(クラス Example のインスタンスを生成,Example 型の変数の定義)し,そのメンバー変数やメンバー関数を参照するには,構造体と同様,基本的に以下のようにして行います.
Example ex; // Example 型オブジェクトの生成
EXample *p_ex = new Example; // p_ex は Example 型オブジェクトへのポインタ
ex.x = 20; // メンバー変数 x の参照
y = p_ex->func(10); // ポインターによるメンバー関数 func の参照
- (プログラム例 10.1 ) クラス宣言
- クラスの宣言とメンバー変数やメンバー関数の参照方法に関する例です.
/****************************/
/* クラスの宣言 */
/* coded by Y.Suganuma */
/****************************/
#include <iostream>
/***********************/
/* クラスExampleの宣言 */
/***********************/
class Example {
int x; // private メンバー変数
public:
int y; // public メンバー変数
void v_set1() // public メンバー関数
{
x = 10;
y = 20;
}
void v_set2() // public メンバー関数
{
x = 30;
y = 40;
}
void output() // public メンバー関数
{
std::cout << " x = " << x << ", y = " << y << std::endl;
}
}; // 「;」が必要なことに注意
/*************/
/* main 関数 */
/*************/
int main()
{
Example t1; // Example 型オブジェクト
Example *t2 = new Example; // Example 型オブジェクトへのポインタ
t1.v_set1(); // メンバー関数の呼び出し
t2->v_set2(); // メンバー関数の呼び出し
std::cout << "オブジェクト t1\n";
std::cout << " y = " << t1.y << std::endl; // 変数xへのアクセスはできない
t1.output();
std::cout << "オブジェクト t2\n";
std::cout << " y = " << t2->y << std::endl; // 変数xへのアクセスはできない
t2->output();
return 0;
}
- 10.1.2 前送りのクラス宣言(Javaを除く)
- あるクラス C1 で別のクラス C2 を参照し,かつ,クラス C2 側でもクラス C1 を参照しているように,クラスどうしが相互参照するような場合は,どちらを先に宣言してもエラーになります.この場合,次のように前送りのクラス宣言( forward class declaration ),または,不完全なクラス宣言( imcomplete class declaration )をする事によって解決できます.
class C2;
class C1 {C2 *p; ・・・};
class C2 {C1 *q; ・・・};
- ただし,各クラスで他のクラスを参照できるのは,ポインタと参照の定義に限られます.
- 10.2 メンバー関数とフレンド関数
- クラスのメンバーとして宣言された関数をメンバー関数( member function )と言います.メンバー関数は,その関数が定義されたクラス型の変数(オブジェクト)のプライベートメンバーにアクセスでき,そのオブジェクトと外部とのインターフェースの役割を果たします.下に,メンバー関数の定義の方法を示します.
関数の型 クラス名::関数名 (引数のリスト)
{
・・・・・
}
- 上の方法は,クラス宣言の外側で定義する方法ですが,クラスの宣言の中に,関数の宣言だけでなくその本体も記述することができます.その場合,その関数はインライン関数とみなされます.
- クラス宣言の中で,
friend int func(Example &);
- のように friend を付加して宣言された関数をフレンド関数( friend function )と言います.フレンド関数は,メンバー関数のように,オブジェクトのプライベートメンバーにアクセス可能な点を除けば,普通の関数と同じです.また,他のクラス(例えば,Example1 )のメンバー関数を,
friend int Example1::func(Example &);
- のように宣言し,フレンド関数とすることも可能です.さらに,
friend class List;
- のように宣言し,クラス List のすべてをフレンドとし,そのクラスのすべてのメンバー関数とメンバー変数を参照できるようにすることも可能です.
- メンバー関数とフレンド関数の違いは,次のプログラム例に見るように,関数の呼び出しやメンバーに対するアクセス方法の違いにあります.メンバー関数の場合はメンバー名だけでアクセスできますが,フレンド関数の場合は,クラス名(アドレス)が必要になります.
- (プログラム例 10.2 ) メンバー関数とフレンド関数
- 次のプログラムは,メンバー関数とフレンド関数の違いを示したものです.関数の呼び出し方,及び,メンバーの参照方法に注意して下さい.また,fun2 は普通の関数ですので,クラス Example のパブリック変数 y にアクセスすることは可能ですが,プライベート変数 x を参照しようとするとコンパイルエラーになります.
- なお,メンバー関数の中で使用されている this というキーワードは,メンバー関数の中だけで使用でき,起動されたオブジェクト(この場合は,data )へのポインタを表しています.従って,「 this->y 」と「 y 」は同じ意味になります.このように,メンバー関数の中ではメンバー名でメンバーを直接アクセスできますので必要ありませんが,例えば,
- return *this;
- のように,自分自身への参照を返したいとき等に使用されます.
- 関数 fun1 や fun2 に対して,Example 型オブジェクトを参照で渡している点に注意してください.この例の場合,関数内で,メンバー変数の値を変えているわけではないので,
- void fun1(Example data)
- のような一般的な引数の渡し方で正常に動作します.しかし,その場合は,関数の説明の時に述べましたように,オブジェクトのコピーが渡されます.単純な変数の場合は特に問題ありませんが,大きなオブジェクトの場合は,そのコピーを生成するために余分な時間とメモリを必要とします.このコピー生成の無駄を省くために参照渡しを行っています.もちろん,アドレス(ポインタ)で渡しても構いませんが,オブジェクト内の変数を参照するときに,ピリオドではなく,「->」を使用する必要があり,多少プログラムが読みにくくなります.
/******************************/
/* メンバー関数とフレンド関数 */
/* coded by Y.Suganuma */
/******************************/
#include <iostream>
/***********************/
/* クラスExampleの定義 */
/***********************/
class Example {
int x;
public:
int y;
void set(int, int);
friend void fun1(Example &);
};
void fun2(Example &);
/************/
/* main関数 */
/************/
int main()
{
Example data; // dataがExample型のオブジェクトであることを宣言
data.set(10, 20); // メンバー関数の呼び出し.ピリオドに注意
fun1(data); // フレンド関数の呼び出し
fun2(data); // 普通の関数の呼び出し
return 0;
}
/*********************************************/
/* メンバー関数 */
/* プログラム例10.1のようにクラス定義の */
/* 内部に記述することもできる */
/*********************************************/
void Example::set(int a, int b)
{
x = a;
this->y = b; // y = b と同じ
std::cout << "x " << x << " y " << y << std::endl;
}
/****************/
/* フレンド関数 */
/****************/
void fun1(Example &data)
{
std::cout << "x " << data.x << " y " << data.y << std::endl;
}
/**************/
/* 普通の関数 */
/**************/
void fun2(Example &data)
{
std::cout << "x ??" << " y " << data.y << std::endl; // xにはアクセスできない
}
- 10.3 コンストラクタとデストラクタ
- コンストラクタ(構築子,constructor )は,オブジェクトを初期化する目的で作成するクラスと同じ名前を持った特別なメンバー関数です.コンストラクタを持つクラスのオブジェクトが存在すると,コンストラクタが自動的に呼び出され,オブジェクトを初期化します.初期化は,代入文やメンバー初期設定リストを使用して行われます(例参照).
- コンストラクタは関数ですから,通常,引数を持っています.従って,オブジェクトの宣言の際に,必ず,引数も与えてやる必要があります.ただし,コンストラクタが定義されていない場合や引数のないコンストラクタが定義されている場合は別です(今まで述べてきた例は,これに相当します).
- プログラム例 10.1 においては,Example 型オブジェクト t1,および,*t2 を定義した後,
t1.v_set1();
t2->v_set2();
- とうい方法で,メンバー関数 v_set1,および,v_set2 を呼び出し,その値を設定していました.しかし,関数 v_set1,および,v_set2 の代わりに,
Example(int x1, int y1)
{
x = x1;
y = y1;
}
- のようなコンストラクタを用意しておけば,
Example t1(10, 20); // Example 型オブジェクト
Example *t2 = new Example (30, 40); // Example 型オブジェクトへのポインタ
- という記述だけで,オブジェクトの定義と初期設定が済んでしまいます.
- デストラクタ(消滅子,destructor )は,オブジェクトを消滅させる目的で作成する特別なメンバー関数であり,「~クラス名」という関数名に決まっています.デストラクタに引数を渡したり,デストラクタから値を返したり,また,デストラクタの多重定義をするなどのことはできません.
- 大きさの決まっていないデータを扱うためには,オブジェクト内で new 演算子によりメモリを確保する方法が一般的です.このメモリの確保をコンストラクタの中で行うと,プログラムが非常に書きやすくなります.さらに,確保したメモリは,必要が無くなれば解放してやらなければなりませんが,この処理もデストラクタを使用すれば自動的に行ってくれます.
- 通常,コンストラクタやデストラクタは,public より後ろで宣言します.
- (プログラム例 10.3 ) 時間データ
/****************************/
/* 時間データの処理 */
/* coded by Y.Suganuma */
/****************************/
#include <stdio.h>
/********************/
/* クラスTimeの宣言 */
/********************/
class Time {
int hour, min, sec;
public:
// コンストラクタ,2つの引数はデフォルト
Time(int h, int m = 0, int s = 0)
{
hour = h;
min = m;
sec = s;
}
// 以下のように,メンバー初期設定リストを使用しても良い
// Time(int h, int m = 0, int s = 0) : hour(h), min(m), sec(s) {}
// コンストラクタ.引数無しも許可.このように引数の異なる
// 宣言を許す場合は,デフォルト引数又は関数名のオーバーロードが必要
Time() {}
// 出力
void print()
{
printf("%2d:%2d:%2d\n", hour, min, sec);
}
};
/************/
/* main関数 */
/************/
int main()
{
Time time1(10, 20, 23); // 10:20:23
Time time2 = Time(12, 30); // 12:30:00
Time time3; // 初期設定されない(内容は不定)
time1.print();
time2.print();
time3.print();
return 0;
}
- (プログラム例 10.4 ) プラントモデル
- 例えば,化学プラントでは,パイプを通して原料が供給され,それが反応炉等に入り,その成分構成が変化して次の反応炉等へ進むということが繰り返されます.反応炉内の処理やパイプ内の成分構成は変化しても,各反応炉等はパイプを通して入力が入り,パイプに出力されるという点ではほとんど同じです.そこで,様々な反応炉等に対応するクラスを用意しておき,その内部処理をコンストラクタや関数で行う(この例の場合は,繰り返しがないため,すべてコンストラクタで行っている)ようにしておけば,それらを適当に組み合わせることによってプラントを実現できるはずです.
- ここでは,
feed : 原料の供給
add : 2 つの原料を混ぜる
product: 反応を行う
- の 3 つのクラスを用意し,次のようなプラント(?)をシミュレーションするプログラムを書いてみました.
/****************************/
/* プラントモデル */
/* coded by Y.Suganuma */
/****************************/
#include <stdio.h>
/********************/
/* クラスFeedの定義 */
/********************/
class Feed { /* 原料の供給 */
public:
double x, y, z;
// コンストラクタ
Feed(double a, double b, double c)
{
x = a;
y = b;
z = c;
}
// 出力
void print()
{
printf("x %f y %f z %f (feed)\n", x, y, z);
}
};
/*******************/
/* クラスAddの定義 */
/*******************/
class Add { /* 混合 */
public:
double x, y, z;
// コンストラクタ
Add(Feed &a, Feed &b)
{
x = a.x + b.x;
y = a.y + b.y;
z = a.z + b.z;
}
// 出力
void print()
{
printf("x %f y %f z %f (add)\n", x, y, z);
}
};
/***********************/
/* クラスProductの定義 */
/***********************/
class Product { /* 製品 */
public:
double x, y, z;
// コンストラクタ
Product(Add &a)
{
x = 0.1 * a.x;
y = 0.2 * a.y;
z = 0.9 * a.x + 0.8 * a.y;
}
// 出力
void print()
{
printf("x %f y %f z %f (product)\n", x, y, z);
}
};
/************/
/* main関数 */
/************/
int main()
{
Feed feed1(10.0, 20.0, 0.0);
feed1.print();
Feed feed2(5.0, 10.0, 0.0);
feed2.print();
Add add1(feed1, feed2);
add1.print();
Product pro1(add1);
pro1.print();
return 0;
}
- このプログラムの実行により,以下のような結果が得られます.
x 10.000000 y 20.000000 z 0.000000 (feed)
x 5.000000 y 10.000000 z 0.000000 (feed)
x 15.000000 y 30.000000 z 0.000000 (add)
x 1.500000 y 6.000000 z 37.500000 (product)
- (プログラム例 10.5 ) ベクトルの内積と絶対値
- 次は,n 次元ベクトルの内積と絶対値を計算するプログラムです.任意の次元に対応するため,new 演算子を使用しています.このような場合,new 演算子で確保した領域を開放するためにデストラクタが必要になります.
/****************************/
/* ベクトルの内積と絶対値 */
/* coded by Y.Suganuma */
/****************************/
#include <iostream>
#include <math.h>
/**********************/
/* Vectorクラスの定義 */
/**********************/
class Vector {
public:
int n;
double *v;
Vector (int n1) // コンストラクタ
{
n = n1;
v = new double [n];
}
~Vector() // デストラクタ
{
if (n > 0)
delete [] v;
}
double zettai(); // 絶対値
double naiseki(Vector &); // 内積
void input(); // 要素の入力
};
/**********/
/* 絶対値 */
/**********/
double Vector::zettai()
{
double x = 0.0;
int i1;
for (i1 = 0; i1 < n; i1++)
x += v[i1] * v[i1];
return sqrt(x);
}
/*********************/
/* 内積 */
/* b : ベクトル */
/*********************/
double Vector::naiseki(Vector &b)
{
double x = 0.0;
int i1;
for (i1 = 0; i1 < n; i1++)
x += v[i1] * b.v[i1];
return x;
}
/********/
/* 入力 */
/********/
void Vector::input()
{
int i1;
for (i1 = 0; i1 < n; i1++) {
std::cout << " " << i1+1 << "番目の要素は? ";
std::cin >> v[i1];
}
}
/******************/
/* mainプログラム */
/******************/
int main()
{
Vector a(2), b(2);
std::cout << "a\n";
a.input();
std::cout << "b\n";
b.input();
std::cout << "内積 " << a.naiseki(b) << std::endl;
std::cout << "絶対値 " << a.zettai() << " " << b.zettai() << std::endl;
return 0;
}
- (プログラム例 10.6 ) リスト構造
- プログラム例 8.5 をクラスを用いて書いた例です.
/****************************/
/* リスト構造 */
/* coded by Y.Suganuma */
/****************************/
#include <stdio.h>
#include <string.h>
/********************/
/* クラスListの定義 */
/********************/
class List
{
char *st;
List *next;
public :
// コンストラクタ
List () { next = NULL; }
List (char *s)
{
next = NULL;
st = new char [strlen(s)+1];
strcpy(st, s);
}
void add(List *); // データの追加
void del(char *); // データの削除
void output(); // 出力
};
/**************************************/
/* データの追加 */
/* dt : Listクラスのオブジェクト */
/**************************************/
void List::add(List *dt)
{
List *lt1, *lt2 = this;
int k, sw = 1;
while (sw > 0) {
// 最後に追加
if (lt2->next == NULL) {
lt2->next = dt;
sw = 0;
}
// 比較し,途中に追加
else {
lt1 = lt2;
lt2 = lt2->next;
k = strcmp(dt->st, lt2->st); // 比較
if (k < 0) { // 追加
dt->next = lt2;
lt1->next = dt;
sw = 0;
}
}
}
}
/*********************/
/* データの削除 */
/* st1 : 文字列 */
/*********************/
void List::del(char *st1)
{
List *lt1, *lt2 = this;
int k, sw = 1;
while (sw > 0) {
// データが存在しない場合
if (lt2->next == NULL) {
printf(" 指定されたデータがありません!\n");
sw = 0;
}
// 比較し,削除
else {
lt1 = lt2;
lt2 = lt2->next;
k = strcmp(st1, lt2->st); // 比較
if (k == 0) { // 削除
lt1->next = lt2->next;
sw = 0;
}
}
}
}
/**********************/
/* リストデータの出力 */
/**********************/
void List::output()
{
List *nt = this->next;
while (nt != NULL) {
printf(" data = %s\n", nt->st);
nt = nt->next;
}
}
/****************/
/* main program */
/****************/
int main ()
{
int sw = 1;
char st[100];
List base, *lt;
while (sw > 0) {
printf("1:追加,2:削除,3:出力,0:終了? ");
scanf("%d", &sw);
switch (sw) {
case 1: // 追加
printf(" データを入力してください ");
scanf("%s", st);
lt = new List(st);
base.add(lt);
break;
case 2: // 削除
printf(" データを入力してください ");
scanf("%s", st);
base.del(st);
break;
case 3: // 出力
base.output();
break;
default :
sw = 0;
break;
}
}
return 0;
}