本題に入り、行列・ベクトルを扱うクラス・ライブラリを設計します。以下で扱う話 題は行列という個別の例を取り上げますが、メモリ領域を動的に作成するようなクラス 設計において共通して現れる特徴を持っています。 また、私たちは行列の数学的な性質を知っています。行列は適度に複雑なデータ構造 を成し、その振る舞うべき性質は明確に定義されています。つまり、行列クラスのオブ ジェクトがどのように振る舞えばわたしたちが満足するユーザー定義のデータ型である といえるのか、その明確な基準が分かっています。従ってこれはクラス設計の格好の例 題といえます。 まず手始めに、クラスの基本概念の骨格を簡単な「おもちゃ」のクラスを使って示し ます。それから徐々に行列クラスを組み立てて行きます。
3.2.1 構造体からクラスへ
クラスは、Cの構造体を自然な形で拡張したものです。前回のテーマ「2.11 考察と その他の話題」で触れたように構造体は次のような形式で定義されます。 struct X{ int data; // 他のメンバ(データ) // ...... }; 構造体Xの変数(オブジェクト)aは次のように型宣言し、 X a; aのメンバ data にアクセスするにはドット演算子(.)を使います。 a.data = 10; // 代入 cout << a.data << endl; // 値を参照する またポインタに対しては、 X *ptr; ptr = &a; によって構造体型Xを指すポインタ ptrは変数aを指します。このポインタを通じてa のメンバにアクセスするには、間接参照(*ptr)に対してドット演算子(.)を使うか、あ るいはアロー演算子(->)を使います。 (*ptr).data = 20; ptr->data = 30; Cの構造体は複数のデータ(異なる型のデータを含めて)を1つにまとめる機能を持 ちますが、完全な「型」ではありません。クラスはこれを拡張して組み込み型と同じよ うに振る舞う「型」としての機能を持たせたものです。前のページの議論からクラスは 次のような機能を持ちます。 (1)データメンバとメンバ関数を持つ (2)情報隠蔽(カプセル化)の機能を持つ (3)宣言でオブジェクトは適切に初期化される ここで(1)については、関数をメンバに含めることができるように拡張されることは 容易に想像できます。(2)については、上で見たようにC構造体のメンバは外から丸 見え(アクセス可能)ですが、これに何らかの制限が付加されることが想像できます。 次に、クラスの骨格を見て行きます。
3.2.2 アクセス制御
上の構造体と同様なものをクラスで表現するには、キーワード classを使ってクラ スを定義します。 class X{ public: void setData(int i) { data = i; } // dataに値を代入 int getData() { return data; } // dataの値を返す // 他のメンバ関数 // .......... private: int data; // 他のデータメンバ // ........ }; この例では、クラス名Xを定義しています。構造体と同じように、クラス定義の本体は 大括弧 { と }で囲み、定義の最後にはセミコロン(;)を付けます。例ではデータ メンバdataの宣言に加えて、2つの関数 setData()、getData()が定義されています。 これらはクラスXのメンバ関数です。 更にこのクラス定義には、メンバ関数群の前にラベル public: が、データメンバ群の前には private: が付いています。これらはメンバアクセス指定子と呼ばれます。キーワードpublic の後ろにコロン(:)を付けたラベルの後メンバをpublicメンバ(公開メンバ)と 呼び、これらには宣言したオブジェクトからドット演算子(.)を使ってアクセスでき ます。 また、キーワードprivateの後ろにコロンを付けたラベルの後のメンバをprivate メンバ(非公開メンバ)と呼び、これらにはオブジェクトからアクセスできません。 つまり非公開メンバはクラスを利用するプログラムからは見えなくなります。この様に メンバにはアクセス権があり、これによってデータの内部表現を隠蔽します。 クラスを定義した後は、クラス名を使ってそのクラスのオブジェクトを宣言すること ができます。このクラスXを使った例を示します。
// lst03_01.cpp #include <iostream.h> class X{ public: void setData(int i) { data = i; } int getData() { return data; } private: int data; }; int main() { X a; // クラスXのオブジェクト a.setData(10); // a.data = 10; //エラー:非公開メンバ cout << a.getData() << endl; X *ptr; // クラスXのオブジェクトを指すポインタ ptr = &a; // 間接参照でメンバ関数にアクセス (*ptr).setData(20); cout << a.getData() << endl; // アロー演算子でメンバ関数にアクセス ptr->setData(30); cout << ptr->getData() << endl; X &ref = a; // クラスXのオブジェクトaの参照 ref.setData(40); cout << ref.getData() << endl; return 0; } |
10 20 30 40 |
クラスXのオブジェクトaのデータメンバdataは非公開メンバなのでアクセスできませ ん。 a.data = 10; //エラー:非公開メンバ の様にアクセスしようとするとコンパイル・エラーになります。代わりに、メンバ関数 を公開メンバに用意して、これらのメンバ関数を使ってデータにアクセスします。 a.setData(10); // dataに10を代入 cout << a.getData() << endl; // dataの値を表示 つまり、データそのものはクラス内部に隠蔽されていて、それらを操作するために公開メ ンバ関数を使います。この様な関数をアクセス関数と呼びます。 内部データに直接アクセスするのではなく関数を通じてアクセスすることで、データの 値の正しさが保証されます。なぜなら、データの値はアクセス関数を通じてのみ変更され ます。このとき変更する前にその値が正当かをチェックするコードを関数内に含めること ができます。 更にクラス内部のデータ値に起因するプログラミング・エラーの場所は、そのデータを 操作するメンバ関数内のコードに限られてしまいます。従ってエラーを引き起こす場所が 局所化されるためにプログラムの保守やデバッグが容易になります。
3.2.3 C++の構造体とクラス
C++の構造体はクラスと本質的には同じものです。デフォルトでのメンバのアクセス権 が違うだけです。アクセス指定子を省略した時のメンバはC++構造体では公開メンバにな ります。一方クラスでは非公開メンバとなります。上のクラスXは次のように定義するこ ともできます。 class X{ int data; // 他のデータメンバ // ........ public: void setData(int i) { data = i; } // dataに値を代入 int getData() { return data; } // dataの値を返す // 他のメンバ関数 // .......... }; C++構造体でも、メンバ関数を持たせアクセス指定子を付けることができます。しかし、 構造体を使うのはすべてのデータを公開する場合、すなわち型としてではなく「単なる データ構造」として使うのが一般的です。
3.2.4 クラススコープ
クラスのデータメンバとメンバ関数はそのクラスのスコープ(有効範囲)内にありま す。クラススコープ内では、クラスのメンバはすべてメンバ関数からアクセスできます。 つまりメンバの名前だけでアクセスできます。上のクラスXのメンバ関数の定義内では データメンバdataに直接アクセスしています。一方、クラススコープの外部では、クラ スメンバはオブジェクト名、オブジェクトへのポインタあるいはオブジェクトの参照を 通じてアクセスします。
3.2.5 オブジェクトの初期化
クラス名で型宣言することでオブジェクトは生成します。このときデータメンバを適 切に初期化することができます。次の例は、クラスXのオブジェクトが生成されるとき にデータメンバdataを適切な値で初期化します。
// lst03_02.cpp // コンストラクタ #include <iostream.h> class X{ public: X() { data = 0; } // デフォルトコンストラクタ X(int i ) { data = i; } // コンストラクタ void setData(int i) { data = i; } int getData() { return data; } private: int data; }; int main() { X a; // クラスXのオブジェクトのメンバーを0で初期化 cout << a.getData() << endl; X b(5); // クラスXのオブジェクトのメンバーを5で初期化 cout << b.getData() << endl; X c[10]; // クラスXのオブジェクトの配列を生成 cout << c[4].getData() << endl; return 0; } |
0 5 0 |
クラス定義の中に、クラスと同一名のメンバ関数X()とX(int i)があります。これらは、 そのクラスのコンストラクタと呼ばれるものです。コンストラクタはクラスオブジェク トのデータメンバを初期化する特殊なメンバ関数です。コンストラクタはオブジェクトが 生成されるとき自動的に呼び出されます。このメカニズムによって、オブジェクトを正し く初期化することができます。 例にあるように、コンストラクタは必要に応じて多重定義(オーバーロード)すること ができます。多重定義するには互いに異なる引数の数と型を取る必要があります。 これら多重定義されたコンストラクタは宣言の仕方に応じて適切なコンストラクタが呼 ばれます。 X a; では引数のないコンストラクタが呼ばれます。また、2つ目の例 X b(5); では引数を取るコンストラクタが呼ばれて整数値5が引数として渡されます。この形式は 組み込みの int型の変数を宣言時に初期化する際に、 int i = 5; の代わりに int i(5); とすることができるのと同じです。 特に、引数を取らないコンストラクタをデフォルトコンストラクタと呼びます。クラ スのオブジェクトの配列を生成するにはデフォルトコンストラクタが定義されていなけれ ばなりません。 X c[10]; のようなオブジェクトの配列宣言ではデフォルトコンストラクタが呼ばれます。 また、コンストラクタには返却型(戻り値の型)がないことに注意しましょう。コンス トラクタは値を返しません。
3.2.6 メンバ関数をクラスの外で定義する
メンバ関数はクラス定義の外で定義することもできます。その方が望ましいとされてい ます。この場合、クラス定義内では該当するメンバ関数はプロトタイプ宣言のみとなりま す。 クラス定義の外でメンバ関数を定義するには、同一名のグローバル関数や、他のクラス に属する同一名のメンバ関数と区別する必要があります。またクラススコープの外部で定 義することになります。それには、コロンを2つ重ねたスコープ解決演算子(::)を使 って、 メンバ関数の戻り値型 クラス名::メンバ関数名(引数リスト) { // メンバ関数定義の本体 } で定義します。この様に定義したメンバ関数は、クラススコープの範囲内にあります。つ まりスコープ解決演算子(::)はクラス名を特定してそのクラスのスコープを解決します。 例を示します。
// lst03_03.cpp // メンバ関数をクラスの外で定義する #include <iostream.h> class X{ public: X() { data = 0; } // デフォルトコンストラクタ X(int); // コンストラクタ(プロトタイプ宣言) void setData(int);// プロトタイプ宣言 int getData() { return data; } private: int data; // 1〜9までの値 }; // コンストラクタ X::X(int i) { if(i > 0 && i < 10) data = i; else{ cout << "エラー:データの値が不正なので終了します。" << endl; exit (1); } } // アクセス関数 void X::setData(int i) { if(i > 0 && i < 10) data = i; else cout << "警告:データが不正なので、値は更新されません。" << endl; } int main() { X a(5); // クラスXのオブジェクトのメンバーを5で初期化 cout << a.getData() << endl; a.setData(10); cout << a.getData() << endl; X b(15); // クラスXのオブジェクトのメンバーを15で初期化 cout << b.getData() << endl; return 0; } |
5 警告:データが不正なので、値は更新されません。 5 エラー:データの値が不正なので終了します。 |
またこのサンプルプログラムでは、データメンバdataが1〜9の整数値を取るものとし ています。そしてメンバ関数はデータメンバを保護します。アクセス関数X::setData() は不正な値を代入しようとすると警告を出し、データメンバは変更されません。また不 正な値でオブジェクトを初期化しようとするとコンストラクタX::X(int)はプログラム を終了させます。 メンバ関数の定義をクラス定義の中で行う場合と外で行う場合の違いは1つあります。 メンバ関数がクラス定義の中で定義された場合、その関数は自動的にインライン関数と して扱われます。インライン展開することで効率が上がりますが、インライン化はコン パイラが決めることです。複雑な関数ではコンパイラはこれを無視しますので、単に値 を返すだけの単純な関数などに限る方がよいでしょう。
クラス定義そのものはユーザー定義型の変数つまりオブジェクトを作成するための設 計図(型紙)に過ぎません。クラスの名前を使って型宣言することでメモリ上に実体 (インスタンスといいます)が生成されます。これをクラスのオブジェクトあるいはク ラスのインスタンス化といいます。このとき、各オブジェクトはそれぞれがデータメン バをメモリ上に持ちます。一方、メンバ関数は1つのクラスで1つずつしか作成されま せん。これはメモリを節約するための仕組みです。 それでは、各オブジェクトはメンバ関数をどのようにして呼び出すのでしょう。例え ば次の2つのオブジェクト X a(1); // データを1で初期化 X b(2); // データを2で初期化 について、 cout << a.getData() << endl; ではメンバ関数X::getData()はオブジェクトaによって呼び出されて、オブジェクトa のデータメンバの値1を返します。同じく、 cout << b.getData() << endl; ではメンバ関数X::getData()はオブジェクトbによって呼び出されて、オブジェクトb のデータメンバの値2を返します。 これは各オブジェクトが自分自身へのポインタを保持していて、メンバ関数呼び出しの 時に、暗黙にそのポインタが渡されるからです。この暗黙に渡されるオブジェクト自身を 指すポインタをthisポインタと呼びます。 thisポインタはメンバ関数の定義内で明示的に使うことができます。キーワードthis を使うことで、メンバ関数を呼び出したオブジェクト自身のアドレスを知ることができま す。 次の例は、thisポインタの値がオブジェクトのアドレスであることを示しています。
// lst03_04.cpp // thisポインタはオブジェクト自身を指す #include <iostream.h> class X{ public: X() { data = 0; } // デフォルトコンストラクタ X(int i ) { data = i; } // コンストラクタ void setData(int i) { data = i; } int getData() { return data; } void *getAddress() { return this;}// thisポインタのアドレス private: int data; }; int main() { X a(10), b(20); cout << "オブジェクトaのアドレス:" << &a << endl; cout << " thisポインタのアドレス:" << a.getAddress() << endl << endl; cout << "オブジェクトbのアドレス:" << &b << endl; cout << " thisポインタのアドレス:" << b.getAddress() << endl; return 0; } |
オブジェクトaのアドレス:0x0063FDF4 thisポインタのアドレス:0x0063FDF4 オブジェクトbのアドレス:0x0063FDF0 thisポインタのアドレス:0x0063FDF0 |
追加されたメンバ関数X::getAddress()は thisつまり呼び出したオブジェクトのアドレ スを返します。実行結果が示すようにその値は、呼び出したオブジェクト自身のアドレ スです。 次の例では、thisポインタをメンバ関数の定義でわざと明示的に使っています。
// lst03_05.cpp // thisポインタをメンバー関数内で明示的に使う #include <iostream.h> class X{ public: X() { this->data = 0; } // デフォルトコンストラクタ X(int i ) { this->data = i; } // コンストラクタ void setData(int i) { this->data = i; } int getData() { return this->data; } private: int data; }; int main() { X a(10); cout << a.getData() << endl; a.setData(20); cout << a.getData() << endl; return 0; } |
10 20 |
つまりメンバ関数X::setData()は次のように書かれています。 void X::setData(int i) { this->data = i; } オブジェクトaがこのメンバ関数を呼び出すと、 a.setData(20); メンバ関数にはaのアドレスが暗黙に渡されて次の意味のコードが実行されます。 a.data = 20; thisポインタの重要な使い方は2つあります。1つは、オブジェクトが自分自身に代 入されることを防ぐことです。オブジェクトが動的に確保したメモリ領域を指すポインタ をデータメンバに持つとき、自己代入は重大なエラーを起こします。2つ目は、メンバ関 数が、オブジェクトへの自己参照を返すことで、連鎖的なメンバ関数呼び出しができるよ うになることです。これらは後のページで扱います。
| 目次 | 前のページ | 次のページ | ページの先頭 |
Copyright(c) 1999 Yamada,K