3.5.1 ベクトル・クラス
次のようなベクトルを表現するクラスVectorを考えましょう。
// matrix14.hの一部分
// Vectorクラス インターフェイス部
class Vector{
friend ostream &operator<<(ostream &, const Vector &);
friend istream &operator>>(istream &, Vector &);
public:
Vector(int = 0); //デフォルトコンストラクタ
Vector(const Vector &); //コピーコンストラクタ
Vector(const double *, int); // 配列で初期化
~Vector(); //デストラクタ
void setSize(int); //ベクトルのサイズを設定する
int getSize() const { return Dim;} //ベクトルのサイズを返す
Vector &operator=(const Vector &); //ベクトルを代入する
private:
double *ptr; //ベクトルの先頭要素へのポインタ
int Dim; //ベクトルの次元
// ユーティリティ関数
void new_vector();// 領域を確保する
void del_vector();// 領域を開放する
};
|
Vectorクラスは2つのデータメンバを持ちます。ベクトルの成分(1次元配列)を指
すポインタ ptrと、ベクトルの次元数 Dimです。
ベクトル成分のメモリ領域を動的に作成・解放するための非公開メンバ関数(ユー
ティリティ関数)Vector::new_vector(), Vector::del_vector()の定義は次のよ
うになります。
// matrix14.cpp の一部分
// ベクトルの領域確保
void Vector::new_vector()
{
if(Dim == 0){ // 次元数が0の場合ポインタをヌルにセット
ptr = 0;
return;
}
ptr = new double[Dim]; //ベクトルのメモリー領域を確保する
if(ptr == 0){
cout << "エラー:メモリー領域が確保できませんでした\n";
abort();
}
}
// ベクトルの領域解放
void Vector::del_vector()
{
delete [] ptr; //ベクトルのメモリー領域を開放する
} |
このベクトル・クラスでは、必要に応じてベクトルのサイズが変更できるようにします。
そのときに、これらのユーティリティ関数をメンバ関数から呼び出します。また、サイ
ズ(次元数)が0の場合は ptrはヌルポインタを指すようにしています(ベクトルの領
域は存在しない)。
ベクトル・クラスのオブジェクトのインスタンス化には次のような4つのタイプが考
えられます。
Vector x; // 既定値の次元数のベクトル(デフォルト・コンストラクタ)
Vector y(10); // 次元数を指定したベクトル(コンストラクタ)
Vector z = y; // 既存のオブジェクトで初期化(コピーコンストラクタ)
及び、配列で初期化する場合
double vec[5] = { 1.0, 2.0, 3.0, 4.0, 5.0}
Vector x(vec, 5);
です。これらに対応したコンストラクタ及びデストラクタは次のように定義できます。
// matrix14.cpp の一部分
//クラスVectorのデフォルトコンストラクタ
Vector::Vector(int dim) : Dim(dim)
{
cout << "Vectorクラスのコンストラクタが呼ばれました。" << endl;
new_vector();// ベクトルの領域確保
for(int i = 0; i < Dim; i++) // 要素を0に設定
ptr[i] = 0.0;
}
//クラスVectorのコピーコンストラクタ
Vector::Vector(const Vector &init) : Dim(init.Dim)
{
cout << "Vectorクラスのコンストラクタが呼ばれました。" << endl;
new_vector();// ベクトルの領域確保
for(int i = 0; i < Dim; i++)
ptr[i] = init.ptr[i]; //ベクトル要素をコピーする
}
//クラスVectorのコンストラクタ(配列で初期化)
Vector::Vector(const double *vec, int dim) : Dim(dim)
{
cout << "Vectorクラスのコンストラクタが呼ばれました。" << endl;
new_vector();// ベクトルの領域確保
for(int i = 0; i < Dim; i++)
ptr[i] = vec[i]; //配列要素をコピーする
}
//クラスVectorのデストラクタ
Vector::~Vector()
{
cout << "Vectorクラスのデストラクタが呼ばれました。" << endl;
del_vector();
} |
配列で初期化する場合、配列の次元数をもコンストラクタに渡す必要があることに注意
しましょう。つまり次のようになります。
Vector::Vector(const double *, int); // 配列で初期化
また、コンストラクタやデストラクタの中の標準出力
cout << "Vectorクラスのコンストラクタが呼ばれました。" << endl;
などは機能確認のためのもので、後でこれらは削除します。
残りのメンバ関数の定義は次のようになります。
// matrix14.cpp の一部分
//ベクトルのサイズを設定する
void Vector::setSize(int dim)
{
del_vector();// ベクトルの領域解放
Dim = dim;
new_vector();// ベクトルの領域確保
for(int i = 0; i < Dim; i++) ptr[i] = 0.0; //要素を0.0にする
}
//多重定義された代入演算子
Vector &Vector::operator=(const Vector &right)
{
if(this != &right){ //自己代入をチェックする
if(Dim != right.Dim) setSize(right.Dim); // サイズ設定
for(int i = 0; i < Dim; i++)
ptr[i] = right.ptr[i]; //受け取ったベクトルをコピーする
}
return *this; // x = y = zと書けるようにする
}
//クラスVectorの多重定義されたストリーム挿入演算子
ostream &operator<<(ostream &output, const Vector &a)
{
output.setf(ios::scientific); // 科学表記法
for(int i = 0; i < a.Dim; i++){
output << setw(15) << a.ptr[i];
if( !( (i+1) % 5 ) ) output << endl;
}
if( a.Dim % 5 ) output << endl;
return output; // cout << x << yと書けるようにする
}
//クラスVectorの多重定義されたストリーム抽出演算子
//ベクトルの全要素の値を入力する
istream &operator>>(istream &input, Vector &a)
{
cout << "ベクトル(次元数 " << a.Dim
<< " )の要素をスペースで区切って入力してください:" << endl;
for(int i = 0; i < a.Dim; i++)
input >> a.ptr[i];
return input; // cin >> x >> yと書けるようにする
} |
次に、このベクトル・クラスを行列クラスに組み込みます。
3.5.2 行列を行ベクトルの配列で構成する
これまでに構成した行列クラスの内部レイアウトは、行列要素のデータ領域を指すメ
ンバ ptrは(double **)型、つまりポインタへのポインタとして表しました。
class Matrix{
public:
...........
private:
double **ptr;// 要素へのポインタ
int Row; // 行の数
int Col; // 列の数
};
このレイアウトは、「行列ライブラリの設計(1)8.動的配列」にあるメモリ配置図に
あるように、ptr[i]は (i+1)行目の行ベクトル(1次元配列)を指すポインタです。
わたしたちは既に、ベクトルを表現するクラス型を持っていますので、 ptrをベクト
ル型のオブジェクトを指すポインタとして表すことができます。つまり、次のような内
部レイアウトに変更してみましょう。
class Matrix{
public:
...........
private:
Vector *ptr;// 行ベクトルへのポインタ
int Row; // 行の数
int Col; // 列の数
};
従って、この場合のメモリ配置は次の図のようになります。
データメンバ ptrは第1行目の行ベクトルを指すポインタですから、ptr[0]は1行目の
Vectorクラスのオブジェクトを意味します。以下同様に ptr[1]、ptr[2]、...は2行
目、3行目の列ベクトル・オブジェクトとなります。
このモデルでは、行列クラスがデータを直接操作するのは各行の行ベクトルまでです。
行ベクトルの要素にアクセスするには、ベクトル・オブジェクトのインタフェースを介
して行います。
行列の演算(和、差、積)では、行列を構成するベクトル(この場合行ベクトルと列
ベクトル)間の演算に還元することができます。従って、ここで採用するモデルでは、
行列の演算はすべてベクトル同士の演算に任せることができます。この様にすることで、
行列間の演算のためのコードを簡略化することができます。
3.5.3 行列クラスの合成による表現
ベクトル・クラスと合成した行列クラスの内部実装を見て行きます。クラス定義は次
のように表現されます。
// matrix14.hの一部分
// Matrixクラス インターフェイス部
class Matrix{
friend ostream &operator<<(ostream &, const Matrix &);
friend istream &operator>>(istream &, Matrix &);
public:
Matrix(int = 0, int = 0); // デフォルトコンストラクタ
Matrix(const Matrix &); // コピーコンストラクタ
~Matrix(); // デストラクタ
void setSize(int, int); // 行列のサイズを設定する
int getRow() const { return Row;}// 行数を取得
int getCol() const { return Col;}// 列数を取得
Matrix &operator=(const Matrix &);// 行列を代入する
private:
Vector *ptr;// 行ベクトルへのポインタ
int Row; // 行の数
int Col; // 列の数
// ユーティリティ関数
void new_matrix();// 行列の領域を確保する
void del_matrix();// 領域を開放する
}; |
このインタフェースは、以前の行列クラスのものと全く同じであることに注意しましょ
う。クラスを利用するアプリケーション・プログラムには何ら変更を必要としません。
これはカプセル化(情報隠蔽)の利点です。
メモリを動的に作成・解放するユーティリティ関数は次のようになります。
// matrix14.cppの一部分
// 行列の領域を確保する
void Matrix::new_matrix()
{
if(Row == 0 || Col == 0){
Row = 0;
Col = 0;
ptr = 0;
return;
}
ptr = new Vector[Row]; //行ベクトルを作る
if(ptr == 0){
cout << "エラー:領域確保に失敗しました。\n";
abort();
}
for(int i = 0; i < Row; i++) // 行ベクトルのサイズ設定
ptr[i].setSize(Col);
}
// 領域を開放する
void Matrix::del_matrix()
{
delete [] ptr; //行ベクトルを解放
} |
行列のデータ領域を作成する非公開メンバ関数 Matrix::new_matrix()では、まず
ptr = new Vector[Row]; //行ベクトルを作る
で、行数分の行ベクトルの配列を作成します。このときベクトル・クラスのデフォルト
コンストラクタが呼び出されます。
次に、各行ベクトルのサイズを設定します。
for(int i = 0; i < Row; i++) // 行ベクトルのサイズ設定
ptr[i].setSize(Col);
ここで、 ptr[i]は各行のベクトル・オブジェクトです。従って、それらのメンバ関数
を呼び出すには、
ptr[i].setSize(Col);
で、(i+1)行目のオブジェクトのメンバ関数が呼び出されて、列数のサイズが設定され
ます。
また、領域を解放するMatrix::del_matrix()では、
delete [] ptr; //行ベクトルを解放
により、行ベクトル・オブジェクトのメモリが解放される前に各行ベクトル・オブジェ
クトのデストラクタが呼び出され後始末が行われます。
コンストラクタとデストラクタ及びサイズを変更するメンバ関数は、次のようになり
ます。
// matrix14.cppの一部分
// デフォルトコンストラクタ
Matrix::Matrix(int row, int col) : Row(row), Col(col)
{
cout << "Matrixクラスのコンストラクタが呼ばれました。" << endl;
new_matrix(); // 行列の領域確保
}
// コピーコンストラクタ
Matrix::Matrix(const Matrix &init) : Row(init.Row), Col(init.Col)
{
cout << "Matrixクラスのコンストラクタが呼ばれました。" << endl;
new_matrix(); // 行列の領域確保
for(int i = 0; i < Row; i++)//要素を代入
ptr[i] = init.ptr[i];
}
// デストラクタ
Matrix::~Matrix()
{
cout << "Matrixクラスのデストラクタが呼ばれました。" << endl;
del_matrix();
}
// 行列のサイズを設定する
void Matrix::setSize(int row, int col)
{
del_matrix(); // 行列の領域解放
Row = row;
Col = col;
new_matrix(); // 行列の領域確保
} |
これらのコードは、すべて以前のものと同じです。変わったのはメモリ割り当てとその
開放の部分だけです。
最後は代入演算子と入出力に関するメンバ関数です。
// matrix14.cppの一部分
//多重定義された代入演算子
Matrix &Matrix::operator=(const Matrix &right)
{
if(this != &right){ //自己代入をチェックする
if( (Row != right.Row) || (Col != right.Col) )
setSize(right.Row, right.Col);
for(int i = 0; i < Row; i++)//要素を代入
ptr[i] = right.ptr[i];
}
return *this; // x = y = zと書けるようにする
}
// 入力演算子
istream& operator>>(istream& input, Matrix& a)
{
cout << a.Row << " 行" << a.Col << " 列の行列要素の入力:" << endl;
for(int i = 0; i < a.Row; i++){
cout << (i+1) << "行";
input >> a.ptr[i];
}
return input;
}
// 出力演算子
ostream& operator<<(ostream& output, const Matrix& a)
{
output.setf(ios::scientific); // 科学表記法
for(int i = 0; i < a.Row; i++)
output << a.ptr[i];
return output;
} |
これら3つのメンバ関数は、以前のものと比べて簡略化されています。ベクトル・ク
ラスが残りの部分を処理してくれるからです。代入演算子では、行列要素の代入は
for(int i = 0; i < Row; i++)//要素を代入
ptr[i] = right.ptr[i];
で済みます。このコードは各行ベクトルの代入です。ptr[i]は現在のベクトル・オブジ
ェクトでright.ptr[i]はコピー元のベクトル・オブジェクトです。この場合、ベクトル
・クラスの代入演算子が実行されます。入出力演算子の場合も同様です。
3.5.4 テストプログラム
簡単なテストプログラムを示します。
// lst03_11.cpp
//Matrixクラス/Vecorクラスのドライバープログラム
//Matrix14.cppと一緒にコンパイルする
#include <iostream.h>
#include "Matrix14.h"
int main()
{
Matrix a(5,4);
// すべての要素は0に初期化されている
cout << "\n行列要素\n" << a << endl;
return 0;
} |
[実行結果]
ソースファイル ( lst03_11.cpp )
Matrixクラスのコンストラクタが呼ばれました。
Vectorクラスのコンストラクタが呼ばれました。
Vectorクラスのコンストラクタが呼ばれました。
Vectorクラスのコンストラクタが呼ばれました。
Vectorクラスのコンストラクタが呼ばれました。
Vectorクラスのコンストラクタが呼ばれました。
行列要素
0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00
0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00
0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00
0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00
0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00
Matrixクラスのデストラクタが呼ばれました。
Vectorクラスのデストラクタが呼ばれました。
Vectorクラスのデストラクタが呼ばれました。
Vectorクラスのデストラクタが呼ばれました。
Vectorクラスのデストラクタが呼ばれました。
Vectorクラスのデストラクタが呼ばれました。 |
このテストプログラムでは、5行4列の行列を初期化します。まず、行列オブジェクト
のコンストラクタが呼ばれ、次にベクトル・オブジェクトのコンストラクタが行数5つ
呼ばれます。
ベクトル・クラスのコンストラクタはすべての成分をゼロに初期設定します。従って
行列要素はすべてゼロに設定されています。
main()関数が終了するときに、行列オブジェクトのデストラクタが呼ばれ、これに続
き5つのベクトル・オブジェクトのデストラクタが呼ばれすべての後始末が行われます。
もう1つの例は、ストリーム挿入・抽出演算子と代入演算子のテストプログラムです。
// lst03_12.cpp
//Matrixクラス/Vecorクラスのドライバープログラム
//Matrix14.cppと一緒にコンパイルする
#include <iostream.h>
#include "Matrix14.h"
int main()
{
Matrix a(3,4);
cin >> a;
cout << a << endl;
Matrix b;
// オブジェクトの代入
cout << "オブジェクトの代入: " << endl;
b = a;
cout << b << endl;
return 0;
} |
[実行結果]
ソースファイル ( lst03_12.cpp )
Matrixクラスのコンストラクタが呼ばれました。
Vectorクラスのコンストラクタが呼ばれました。
Vectorクラスのコンストラクタが呼ばれました。
Vectorクラスのコンストラクタが呼ばれました。
3 行4 列の行列要素の入力:
1行ベクトル(次元数 4 )の要素をスペースで区切って入力してください:
1.1 1.2 1.3 1.4
2行ベクトル(次元数 4 )の要素をスペースで区切って入力してください:
2.1 2.2 2.3 2.4
3行ベクトル(次元数 4 )の要素をスペースで区切って入力してください:
3.1 3.2 3.3 3.4
1.100000e+00 1.200000e+00 1.300000e+00 1.400000e+00
2.100000e+00 2.200000e+00 2.300000e+00 2.400000e+00
3.100000e+00 3.200000e+00 3.300000e+00 3.400000e+00
Matrixクラスのコンストラクタが呼ばれました。
オブジェクトの代入:
Vectorクラスのコンストラクタが呼ばれました。
Vectorクラスのコンストラクタが呼ばれました。
Vectorクラスのコンストラクタが呼ばれました。
1.100000e+00 1.200000e+00 1.300000e+00 1.400000e+00
2.100000e+00 2.200000e+00 2.300000e+00 2.400000e+00
3.100000e+00 3.200000e+00 3.300000e+00 3.400000e+00
Matrixクラスのデストラクタが呼ばれました。
Vectorクラスのデストラクタが呼ばれました。
Vectorクラスのデストラクタが呼ばれました。
Vectorクラスのデストラクタが呼ばれました。
Matrixクラスのデストラクタが呼ばれました。
Vectorクラスのデストラクタが呼ばれました。
Vectorクラスのデストラクタが呼ばれました。
Vectorクラスのデストラクタが呼ばれました。 |
Matrixクラスのオブジェクトaに対するコンストラクタが呼ばれると、続いて3つ
の行ベクトルのコンストラクタが呼ばれます。ストリーム挿入演算子を使って要素を入
力します。
次に、オブジェクトbのインスタンス化で Matrixクラスのコンストラクタが呼ばれ
ますが、このときはデフォルトの行列数(0行0列)で初期化されるので、 Vectorク
ラスのコンストラクタは呼ばれません。
オブジェクトの代入「b=a;」でbが3行4列に再設定されて、このとき行ベクト
ルのコンストラクタが呼ばれます。それから各行列要素が代入されます。
3.5.5 explicitコンストラクタ(明示的コンストラクタ)
ベクトル・クラスには注意すべきことがあります。引数を1つ持つコンストラクタは、
暗黙の型変換を行います。これはコンパイラが、1個の引数を持つコンストラクタを使
って暗黙にクラスのオブジェクトに変換します。暗黙の変換は、クラス型によっては理
想的な機能となりますが、場合によっては望ましくないエラーの原因となります。ここ
で設計した Vectorクラスの(デフォルト)コンストラクタがその典型です。
Vector::Vector(int = 0); //デフォルトコンストラクタ
このコンストラクタは次元数を引数に渡すことで、与えた次元数のベクトル・オブジェ
クトを生成するのが本来の役割です。しかし、コンパイラが間違ってこのコンストラク
タを暗黙の型変換に使ってしまいます。次のプログラム lst03_13.cpp はその誤った
暗黙の変換の例です。
// lst03_13.cpp
// コンストラクタが”誤った”暗黙の型変換を起こす例
//Matrix14.cppと一緒にコンパイルする
#include <iostream.h>
#include "matrix14.h"
void print(const Vector&);
int main()
{
Vector v(3); // ベクトル・オブジェクトを生成
print( v );
print( 5 ); // ”誤った”暗黙の型変換を起こす
return 0;
}
void print(const Vector &v)
{
cout << v << endl;
} |
[実行結果]
ソースファイル ( lst03_13.cpp )
Vectorクラスのコンストラクタが呼ばれました。
0.000000e+00 0.000000e+00 0.000000e+00
Vectorクラスのコンストラクタが呼ばれました。
0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00
Vectorクラスのデストラクタが呼ばれました。
Vectorクラスのデストラクタが呼ばれました。 |
実行結果をよく見てください。このプログラムは問題なくコンパイルされますが、予期
しない実行結果が起きてます。関数 print()はベクトル・オブジェクトの const参照
を引数に取ります。この関数の最初の呼び出しでは、
print( v );
と正しくオブジェクトvが渡されているので、この関数はベクトルの要素を出力します。
ところが、2回目の呼び出しではint型の整数値5を引数にして関数 print()を呼び出
しています。
print( 5 ); // ”誤った”暗黙の型変換を起こす
この様な関数は定義されていないのですが、コンパイラは暗黙の型変換を行って関数
print()を呼び出します。このときコンパイラは、Vectorクラスの中でint型をVector
型に変換できるコンストラクタを探します。それが、コンストラクタ
Vector::Vector(int = 0); //変換コンストラクタ
です。実行結果が示すように、このときこのコンストラクタが暗黙に呼ばれ次元数5の
一時オブジェクトが生成され、この一時オブジェクトが関数 print() に渡されます。
従って、「print(5)」は「print( Vector(5) )」と解釈されます。
この様に、引数を1つ持つコンストラクタは同時に暗黙の型変換を定義します。その
ため、これを変換コンストラクタと呼びます。
標準C++では、この様な暗黙の型変換を防ぐために、キーワード explicitが導入
されています。暗黙の型変換を無効にするには、コンストラクタの先頭に explicit
を付加して宣言します。
class Vector{
...........
public:
explicit Vector(int = 0); // exciplit(明示的)コンストラクタ
...........
private:
.........
};
デフォルトコンストラクタをexplicitコンストラクタに変更して、コンパイルした
結果は、次のようになります。
lst03_13.cpp:
Error E2064 lst03_13.cpp 15: Cannot initialize 'const Vector &' with
'int' in function main()
Error E2341 lst03_13.cpp 15: Type mismatch in parameter 1 in call to
'print(const Vector &)' in function main()
*** 2 errors in Compile ***
matrix14.cpp:
|
暗黙の型変換が禁止されるために、「print( 5 );」のところでコンパイルエラーと
なります。
同じ理由で、Matrixクラスの(デフォルト)コンストラクタ
Matrix::Matrix(int = 0, int = 0);
でも、explicitコンストラクタで宣言すべきです。
class Matrix{
........
public:
explicit Matrix(int = 0, int = 0); // explicitコンストラクタ
..........
private:
.........
};
このコンストラクタは2つの引数を持ちますが、デフォルトの引数を取ることができ
るために構文的には次のような1つの引数を与えて宣言できます。
Matrix a(10);
この場合は第2引数が省略されたことになり、10行0列としてコンストラクタが呼
び出されて、コンストラクタはこれを0行0列の行列として設定仕直します。従って、
事情はベクトル・クラスと同じですから、暗黙の型変換を明示的(explicit)に無効
にする方がよいでしょう。
[演習問題 3.6] Matrixクラスのコンストラクタをexplicitで宣言しない場合に、
コンパイラが間違ってこのコンストラクタを暗黙の型変換に使ってしまうことを例示
するプログラムを書き、実行して確かめなさい。
(回答)ex03_06.cpp
[注] キーワードexplicitは比較的新しく導入された機能です。
古いコンパイラによっては使えない場合があります。
| 目次 | 前のページ
| 次のページ | ページの先頭
|
Copyright(c) 1999 Yamada, K