2.11 考察とその他の話題  (1999/05/11 初版)

2.11.1 行列操作ライブラリについての考察  これまで設計してきた行列(及びベクトル)を表現するデータ構造は、いくつかの 不満足な部分があります。それらは次の3つに要約できます。  (1)行列 Matrix やベクトル Vector はデータ型ではない  (2)要素の添字範囲の逸脱についてのチェック機構がない  (3)行列・ベクトルのデータ構造とそれらを操作する関数との関係性が弱い  第1の点は、Matrix型とは、(double **)型のことであってポインタ変数に過ぎ ないということです。次の宣言 Matrix a; では行列の領域がメモリ上に作られるのではありません。ポインタの値を記憶する領 域が作られるだけです。行列の領域を作るには、 a = mat_new(10, 10); の様に関数呼び出しを、ユーザーが行う必要があります。また領域を解放するときも mat_del(a); と明示的に行います。これは組み込みの基本データ型の場合とは違い、完全なデータ 型ではありません。  第2の点は、配列の範囲外を操作してもC/C++はチェックしません。これはユーザ の責任で行わなければなりません。このことと関連して3番目は、サイズが正しくな い行列やベクトルを関数に渡たしても、コンパイラは関数引数の型チェックをしませ ん。例えば(n×m)の行列と(m×k)の行列の積は、(n×k)の行列となりま す。つまり、 Matrix a = mat_new(n, m); Matrix b = mat_new(m, k); Matrix c = mat_new(n, k); // a, b の値を設定する mat_prod( a, b, c, n, m, k); // 行列の積abをcに代入 というコードで実現されます。ここでは行列a、b、cの間の型の整合性や各行列とそ のサイズn、m、kとの整合性はチェックされないでコンパイルされます。行列の型は すべて同じ(double **)であるため関数プロトタイプに問題は発見されないからです。 このことは引数として渡すデータとそれを操作する関数との間に関係性がないことを示 します。つまり間違ったデータを渡しても問題なくコンパイルされ、実行時にエラーを 生じます。すべての操作はユーザの責任で行う必要があります。  以上の欠点は、次のテーマ「行列操作ライブラリの設計(2)」で行列とベクトルの クラスを設計することで解決されます。クラスとは、任意のデータ構造を組み込のデー タ型と同じような「データ型」として定義するもので、オブジェクト指向プログラミン グの中核となる概念です。後のテーマで取り上げるように、行列クラス Matrix では 次の宣言 Matrix a; で適切なサイズの行列の実体(オブジェクト) aが作られ正しく初期化されます。行列 要素の参照は a[i][j] で行うことができて、このとき添字操作に対する範囲チェック が行われます。また2つの行列a、bの和や積は、「+」や「*」演算子を使った演算 が可能で、その演算結果を cout << a + b; cout << a * b; のように、演算結果を適切に書式化して標準出力することが可能になります。これは、 組み込型の intdouble などと同じ完全なデータ型となっています。しかもそのデ ータ操作はこれまでの関数呼び出しによる形式ではなく、演算子を使った直感的なスタ イルで行うことができます。つまり、クラスはデータとその操作(関数)を結びつけて 1つにパッケージ化したデータ型をつくります。そしてクラスで定義されたデータ型の 実体をオブジェクト、あるいはクラスのインスタンスと呼びます。  テーマ「行列操作ライブラリの設計(1)」の最後として、オブジェクト指向プログ ラミングの準備を含めていくつかの話題を取り上げます。


2.11.2 構造体  配列は、同一型のデータの集合を1つの型にしたデータ型でした。構造体を使うと異 なるデータ型の集合からなる複合的なデータ型を定義することができます。配列では、 すべての要素はそれぞれが番号付けられていました。構造体では、個々の要素に名前が 付きます。  構造体は元々はC言語で導入されていたユーザ定義のデータ型です。C++では機能が 拡張されました。そして、この"拡張されたC++構造体"こそ、本質的にはC++のクラス にほかなりません。ここでは、C++構造体の機能のうちで、Cの構造体の部分について だけ触れます。そうすることでクラスが、Cの構造体を自然な形で拡張したものである ことが分かります。  構造体を定義するにはキーワード struct を使います。その一般形式は次のように なります。 struct 構造体型名 { 型 メンバ名; 型 メンバ名; .......... }; 構造体型名は構造体タグと呼ばれ、定義したその構造体変数の型の名前になります。大 括弧{ }で囲んだ内部で宣言された変数名はその構造体のメンバです。構造体の定義 の最後にはセミコロン(;)を付けなければなりません。  同じ構造体の1つ1つのメンバは異なる名前でなければなりません。どんな型でもメ ンバとして使えます。しかし、構造体はそれ自身をメンバとして持つことはできません。 ただし、構造体自身へのポインタをメンバとして持つことはできます。  構造体は多くの関連したデータを個別の変数としてではなく、1つの変数(オブジェ クト)として扱う目的で使われます。次の例は、3次元空間の座標点 (x,y,z) を表現 するのに構造体 Point を定義しています。 struct Point{ int x; // x座標 int y; // y座標 int z; // z座標 }; この構造体メンバはすべて int型なのでこれを配列で定義することも可能です。しか し、構造体を使うことで各座標の成分を番号ではなく意味を持った名前(メンバ名) で扱うことができます。  構造体の定義はデータの型を定義するのであって、メモリ上に領域が確保されるわけ ではありません。メモリ上に構造体変数を確保するには宣言を行います。次の宣言 Point p, pArray[5], *ptrP; は、Point型の変数 p、要素数5のPoint型の配列 pArray、Point型へのポインタ変数 ptrPをメモリ上に確保します。 ------------------------------------------------------------------- [注]C言語では、構造体型を宣言するときにはキーワードstructが必要です。 次のように宣言します。 struct Point p; // C言語での宣言 Cでは宣言の度にstructを付けるのが煩わしいために、構造体をtypedefで 定義することが慣用となっています。 typedef struct point{ int x; // x座標 int y; // y座標 int z; // z座標 } Point; この様にすることで、構造体変数の型PointをC++と同様に宣言することができ ます。 Point p; この場合構造体タグpointをPointで型定義することを意味します。これは次のよ うに定義するのと同じです。 struct point{ int x; // x座標 int y; // y座標 int z; // z座標 }; typedef struct point Point; ----------------------------------------------------------  構造体は、配列と同様に初期値リストで初期化することができます。 Point p = { 10, 20, 30 }; は、構造体変数aを生成して、メンバxを10、yを20に、zを30に初期化しま す。指定した初期値が構造体メンバよりも少ない場合、残りのメンバは0(ポインタ の場合はヌルポインタ)に初期化されます。 (注)配列と違って、構造体メンバはメモリ内に連続して格納されるとは限りません。  構造体メンバにアクセスするには、ドット演算子(.)を使います。例えば、構造体 変数 pのメンバ x、y、zに値を代入するには、 p.x = 10; p.y = 20; p.z = 30; 構造体へのポインタの使い方は、普通の変数型の場合と同じです。 Point *ptrP = &p; // ptrP は構造体 pを指す ポインタを介してメンバを参照するには逆参照演算子(*)を使って、 cout << (*ptrP).x; // p.xつまり値10を出力する でポインタptrPが指す pのメンバにアクセスできます。 (注)「(*ptrP).x」の様に括弧が必要です。ドット演算子(.)は逆参照演算子(*) よりも演算子の優先順位が高いからです。  また逆参照演算子「(*ptrP).x」を使う代わりに、より直感的なアロー演算子 -> を 使って逆参照できます。 cout << ptrP -> x; // p.xの値10を出力する ptrP -> y = 25; // 代入する(p.yの値は25になる)  構造体は配列と違って、構造体変数を同じ型の構造体変数に代入することができます。 Point q; q = p; // pの値をqに代入 この場合、pの各メンバの値がqの各メンバに代入されます。

2.11.3 構造体を使った行列の表現  構造体を使うと、前に設計した"行列"に行と列のサイズをメンバにすることができま す。次の構造体 matrix を定義しましょう。 struct matrix{ double **cell; // 行列要素(以前の Matrix) int row; // 行のサイズ int col; // 列のサイズ }; 行列のデータ領域を指すポインタcell及び、行と列の値 rowと colをメンバにもつ構造 体です。これを使って、行列を入力・表示する、簡単なサンプルプログラムを作ります。
// matrix11.h
// 行列操作ライブラリ
// 構造体を使って定義 
#ifndef MATRIX11_H
#define MATRIX11_H

#include <iostream.h>
#include <iomanip.h>

struct matrix{
        double **cell; // 行列要素を指す
        int row;       // 行数
        int col;       // 列数
};

void mat_new(matrix &, int, int);
void mat_del(matrix &);
void mat_read(matrix &);
void mat_print(const matrix &, char * = 0);


// 行列の動的作成
void mat_new(matrix &a, int m, int n)
{
    a.cell = new double *[m];         // 行を作る
    for(int i = 0; i < m; i++)        // 列を作る
        a.cell[i] = new double[n];

    a.row = m; // 行の数
    a.col = n; // 列の数
}

// 行列の解放
void mat_del(matrix &a)
{
    for(int i =0; i < a.row; i++)
        delete [] a.cell[i];  // 列を解放
    delete [] a.cell;         // 行を解放

    a.row = 0;
    a.col = 0;
}


// 行列要素の読み込み
void mat_read(matrix &a)
{
    cout << a.row << " 行" << a.col << " 列の行列要素を入力してください"
    << endl;
    for(int i = 0; i < a.row; i++)
        for(int j = 0; j < a.col; j++){
            cout << (i+1) << " 行" << (j+1) << " 列: ";   
            cin  >> a.cell[i][j];
        }
}

// 行列要素の表示
void mat_print(const matrix &a, char *s)
{
    if(s) cout << s << endl;
    cout.setf(ios::scientific); // 科学表記法
    for(int i = 0; i < a.row; i++){
        for(int j = 0; j < a.col; j++){
            cout << setw(15) << a.cell[i][j];
            if( !( (j+1) % 5 ) ) cout << endl;
        }
        if( a.col % 5 ) cout << endl;
    }
}

#endif

ソースファイル matrix11.h
ここでは関数プロトタイプ宣言と関数定義を1つのヘッダファイルにまとめました。ま た、ドライバ・プログラムは次のようになります。
// lst02_19.cpp
// 行列操作ライブラリーのテストプログラム
#include "matrix11.h"


int main()
{
    matrix a;
    mat_new(a, 2, 3);

    mat_read(a);
    mat_print(a, "行列aの表示");

    mat_del(a);

    return 0;
}
ソースファイル  lst02_19.cpp
[実行結果]
2 行3 列の行列要素を入力してください
1 行1 列: 1.1 
1 行2 列: 1.2 
1 行3 列: 1.3 
2 行1 列: 2.1 
2 行2 列: 2.2 
2 行3 列: 2.3 
行列aの表示 
   1.100000e+00   1.200000e+00   1.300000e+00        
   2.100000e+00   2.200000e+00   2.300000e+00
 もはや、行列を関数に渡すときに行と列のサイズを渡す必要がありません。また行列 を参照型「matrix &」で渡していることに注目しましょう。構造体は通常、「値渡し」 で関数に渡されます。もしこれが大きな構造体であるときは、構造体メンバをコピーす るオーバーヘッドは無視できなくなります。従って、構造体を関数に渡すときはその参 照を渡すことで、オーバーヘッドを避けることができます。また呼び出し側の値を変更 しない場合は「const参照」で渡します。 【演習問題 2.14】上の matrix11.h を使って書かれた次のプログラムには重大な バグがあります。どこが誤りか指摘しなさい。
#include "matrix11.h"

int main() 
{ 
    matrix a, b; 
    mat_new(a, 2, 3); 
    mat_new(b, 2, 3); 

    mat_read(a); 

    b = a; // コピーする 

    mat_del(a); 
    mat_del(b); 

    return 0; 
}
 Cスタイルの構造体を使ったデータ型にも依然欠点があります。第1に、適切に初期 化されないことです。従って、初期化されてないデータを使ったり、たとえ初期化され ていてもそれが正しく初期化されてない可能性があります。  第2に、プログラムの中で構造体のメンバに直接アクセスすることができるので、不 正なデータ値に書き換えられる可能性があります。例えば、matrix型のメンバ rowや colに不正な値が代入される場合があり得ます。


2.11.4 ストリーム  オブジェクト指向の身近な例の1つは、C++の入出力機能です。C++の入出力はスト リームという形式で行われます。ストリームとはバイトの流れのことで、デバイス(キ ーボード、ディスプレイ装置、ディスク装置、プリンタ装置、ネットワーク装置など) とメイン・メモリとの間でバイト単位のデータをやり取りする仕組みをいいます。入力 操作では、デバイスからメイン・メモリ内の実行中のプログラムへバイトデータが流れ ます。出力操作では、その反対にメイン・メモリからデバイスへバイトデータが流れま す。  標準入出力では、キーボードが入力デバイスでディスプレイが出力デバイスとなりま す。ストリーム入出力操作は iostream ライブラリのヘッダファイル<iostream.h>を インクルードすることで利用できます。次の標準入出力を行うコードを見てください。 cin >> data; // 入力 cout << data; // 出力 ここでdataが正しく宣言されているとすると、dataは整数型、浮動小数点型、文字列の いずれであろうとも、それぞれの型に合った入出力が正しく行われます。C言語の入出 力のように、型ごとに応じた変換指定子(%d, %f, %s など)を必要としません。この 様にC++の入出力では、データ型を自動的に識別します。これを型保証入出力といいま す。  この cinや coutこそオブジェクトです。cinはストリーム入力を行うistreamクラス のオブジェクトで、標準入力デバイスに結合(接続)されています。また、coutはスト リーム出力を行うostreamクラスのオブジェクトで、標準出力デバイスに結合されてい ます。  これらオブジェクトは、それらを操作する演算子(>>)と(<<)と関係付けられてい ます。ストリーム入力を表す右シフト演算子 >> はストリーム抽出演算子と呼ばれ、ス トリーム出力を表す左シフト演算子 << をストリーム挿入演算子と呼びます。  >>演算子と<<演算子は、標準のデータ型に対してデータ値を受け入れることができる ように型ごとに多重定義されています。これが型保証入出力の仕組みです。さらに、予 期しないデータが渡されると、エラーフラグ(エラーの識別)がセットされます。ユー ザーはこれを使って入出力操作が成功したか失敗したかを判別できます。このためプロ グラムが制御不能に陥ることはありません。


2.11.5 演算子の多重定義  オブジェクト志向型の入出力の優れた機能の1つは、ユーザが入出力の振る舞いを拡 張できることです。詳しくは後のテーマで扱いますが、ここではその一端を示します。  ストリーム演算子を多重定義して、行列を入出力できるようにします。つまり、次の ようなプログラムが書けるように演算子を拡張してみましょう。 matrix a; // 行列aの領域確保を行っておく cin >> a; // 行列要素の入力 cout << a; // 行列要素を表示する  次のプログラムを見てください。
// lst02_20.cpp
// lat02_20.cpp
// ユーザ定義型のストリーム演算子
#include "matrix11.h"


istream& operator>>(istream& input, matrix& a)
{
    cout << a.row << " 行" << a.col << " 列の行列要素を入力してください"
    << endl;
    for(int i = 0; i < a.row; i++)
        for(int j = 0; j < a.col; j++){
            cout << (i+1) << " 行" << (j+1) << " 列: ";
            input  >> a.cell[i][j];
        }
    return input;
}

ostream& operator<<(ostream& output, const matrix& a)
{
    output.setf(ios::scientific); // 科学表記法
    for(int i = 0; i < a.row; i++){
        for(int j = 0; j < a.col; j++){
            output << setw(15) << a.cell[i][j];
            if( !( (j+1) % 5 ) ) output << endl;
        }
        if( a.col % 5 ) output << endl;
    }
    return output;
}

int main()
{
    matrix a;
    mat_new(a, 2, 3);

    cin >> a;

    cout << a;

    mat_del(a);

    return 0;
}

ソースファイル  lst02_20.cpp
[実行結果]
2 行3 列の行列要素を入力してください
1 行1 列: 1.1 
1 行2 列: 1.2 
1 行3 列: 1.3 
2 行1 列: 2.1 
2 行2 列: 2.2 
2 行3 列: 2.3 
   1.100000e+00   1.200000e+00   1.300000e+00         
   2.100000e+00   2.200000e+00   2.300000e+00
入出力ストリーム演算子 >> と << を多重定義するには、次の関数プロトタイプ宣言で与 えられる2つの関数を定義します。 // >>演算子の多重定義 istream& operator>>(istream& , matrix& ); // <<演算子の多重定義 ostream& operator<<(ostream& , const matrix& ); ストリーム抽出演算子 >> は2つのオブジェクト(または変数)に作用します。 cin >> a; ではistreamクラスのオブジェクトcinと行列aです。この様に2つに作用する演算子を 二項演算子といいます。二項演算子は2つの引数をとる関数 operator>>() で定義しま す。引数に渡されるのは、2つのオブジェクト cin とaの参照です。 関数定義 istream& operator>>(istream& input, matrix& a) { cout << a.row << " 行" << a.col << " 列の行列要素を入力してください" << endl; for(int i = 0; i < a.row; i++) for(int j = 0; j < a.col; j++){ cout << (i+1) << " 行" << (j+1) << " 列: "; input >> a.cell[i][j]; } return input; } の中身と、上の matrix11.h で定義した読み込み関数 void mat_read(matrix &a) { cout << a.row << " 行" << a.col << " 列の行列要素を入力してください" << endl; for(int i = 0; i < a.row; i++) for(int j = 0; j < a.col; j++){ cout << (i+1) << " 行" << (j+1) << " 列: "; cin >> a.cell[i][j]; } } と良く比べてみてください。ほとんど同じコードです。入力オブジェクトcinが、関数 内で受け取った同じクラスのオブジェクトinputに代わっています。最後に、inputの 参照を返却しています。  この様に>>演算子が多重定義されると、コンパイラはプログラムの中で次のコード cin >> a; を見つけると、 operator>>(cin , a ); を呼び出します。その結果、前のプログラムで関数 mat_read()を呼び出した同じ動作 をストリーム演算子で行うことができます。<<演算子の多重定義も同様に行われます。  ここまでくれば、matrix11.h で定義した行列の読み込みと出力の関数は不要になり ます。ヘッダファイルの関数コードをストリーム演算子を多重定義したものと置き換え ることで、より自然な形式で行列操作のプログラムが書けるようになりました。

2.11.6 参照を返す関数  上の例で、行列を入出力するために多重定義したストリーム演算子では、戻り型が参 照型になっています。 iostreamライブラリで定義されている組込型についてのストリ ーム入出力演算子は、すべて参照を返却するように多重定義されています。それにより、 次のようなストリーム演算子の連鎖的な利用ができます。 int x = 10; cout << "xの値は:" << x << endl; このストリーム挿入文は次のように実行されます。 ( ( (cout << "xの値は:") << x ) << endl); <<演算子は cout への参照を返します。最初に cout << "xの値は:" は文字列を出力して、coutへの参照を返します。それによって次の外側の括弧() cout << x が評価されxの中身10を出力し、coutへの参照を返します。さらにそれによって次の 文 cout << endl; が評価されます。つまり参照を返すとは、内側の括弧()が評価されるたびに括弧内が cout に置き換えられて行き、連鎖的な出力をすることができます。

2.11.7 ポインタの学習について  ここでのテーマに於いて、ポインタの本質的な概念については説明を行いましたが、 他に触れていないことがいくつかあります。(もしあなたが学習中であれば)ポイン タの意義や機能に習熟するために、以下の事項について学ぶことをお薦めします。 (1)文字列操作 (2)ポインタと構造体 いずれも、C言語の機能ですので多くのテキストや解説があります。(1)について、 そのエッセンスはK&Rのテキスト、  B.W.カーニハン / D.M.リッチー著『プログラミング言語C』、共立出版 のポインタの章の中にあります。  また(2)は構造体を指すポインタの使い方です。リスト(List)や木(Tree)構造な どのデータ構造の典型的なアルゴリズムがあります。アルゴリズムについてのテキスト も多数あります。

| 目次 | 前のページ |

Copyright(c) 1999 Yamada,K