2.9 モジュール・プログラミング (1999/04/24 初版)

2.9.1 行列を作成・削除する関数  前のページ「2.8 動的配列」のプログラム matrix06.cpp を関数を使って表現し ましょう。行列を動的に作成し、表示、そして行列領域を解放する。この3つの動作 を関数にします。次のプログラムを見てください。
// lst02_13.cpp
// 行列を動的に作成
// 関数版
#include <iostream.h>

int **matrix_new(int, int);   // 行列の動的作成
void matrix_del(int **, int); // 行列の解放
void print(int **, int, int); // 行列の表示

int main()
{
    int row, col; // 行と列の大きさ
    cout << "行の大きさを入力してください: ";
    cin  >>  row;
    cout << "列の大きさを入力してください: ";
    cin  >> col;

    int **mat;
    mat = matrix_new(row, col); // 行列の作成

    // 行列の値を設定
    for(int i = 0; i < row; i++)
        for(int j = 0; j < col; j++)
            mat[i][j] = (i+1)*10 + (j + 1);

    print(mat, row, col);

    matrix_del(mat, row);

    return 0;
}

// 行列の動的作成
int **matrix_new(int row, int col)
{
    int **a = new int *[row];        // 行を作る  
    for(int i = 0; i < row; i++)     // 列を作る  
        a[i] = new int[col];

    return a; // アドレスを返す
}

//行列の解放
void matrix_del(int **a, int row)
{
    for(int i = 0; i < row; i++)
        delete [] a[i];  // 列を解放
    delete [] a;         // 行を解放
}

void print(int **a, int row, int col)
{
    for(int i = 0; i < row; i++){
        for(int j = 0; j < col; j++)
            cout << " " << a[i][j];

        cout << endl;  //行の終わりに改行
    }
}

ソースファイル lst02_13.cpp
[実行結果]
行の大きさを入力してください: 5 
列の大きさを入力してください: 8   
 11 12 13 14 15 16 17 18 
 21 22 23 24 25 26 27 28 
 31 32 33 34 35 36 37 38 
 41 42 43 44 45 46 47 48 
 51 52 53 54 55 56 57 58
関数の中で宣言したローカルな自動変数は、関数が実行されるときに作成されて、関数 の終了で破壊されます。しかし、関数の中で動的に作成されたメモリ領域は関数が終了 後も残ります。行列を動的に作成する関数のプロトタイプは int **matrix_new(int row, int col); の様に、整数型のポインタを指すポインタを返します。戻り値の型は(int **)型です。 つまり引数で渡した行と列の大きさの行列の領域をnew演算子で作成し、そのアドレス を返します。それが次のreturn文で、 return a; ローカル変数 aの値(アドレス値)を返して、この変数は消滅します。従って動的に作 成した配列領域を指すポインタは関数の呼び出し側でそのアドレス値を受け取ります。 int **mat = matrix_new(row, col); これで、matは行列を表す変数(2次元配列)として扱うことができます。  また、この行列領域を解放するには、次のプロトタイプの関数 void matrix_del(int **a, int row); の中で、delete演算子を使って領域を解放します。関数の呼び出しは次のように、 matrix_del(mat, row); 行列のメモリ領域を指すポインタ matと行の数 rowを渡す必要があります。

2.9.2 使いやすい関数の工夫  行列操作関数を使いやすくするために、いくつかの工夫を行いましょう。ここでは行列 をポインタへのポインタとして表現しています。浮動小数点型の行列の場合は、 double **mat; となります。これを独自のデータ型で定義しましょう。C/C++ではtypedef文を使って自 作のデータ型を定義できます。次のようにすると typedef double **Matrix; 新しい自作の型 Matrix が定義できます。これは(double **)型と同じです。この型定 義はキーワードtypedefを除けば、変数の宣言と同じです。typedefを付けると変数名 ではなく型名を定義します。次の宣言 Matrix a; は、次の宣言と同じです。 double **a; C言語では新しい構造体を定義するのにtypedefをよく使います。  次に、行列領域を解放する関数 matrix_del() を検討しましょう。この関数は行の数 を渡す必要があり、バランスの悪い格好をしています。できれば次のように void matrix_del(Matrix a); としたいものです。これだと関数の利用者は「行数を渡す(列数は不要)」といった奇妙 な関数の使い方(仕様)を覚える必要はありません。  「行数を渡す」必要があるのは、ポインタを関数に渡すと配列の大きさについての情報 が失われるためでした。ところが、配列の大きさについての情報を与える巧い方法があり ます。このアイデアは『奥村晴彦著:C言語による最新アルゴリズム辞典、技術評論社』 にあります。  まず行列領域を作成するときに行のポインタ配列を1つ余分に作り、行の最後を示す印 としてヌルポインタを入れておきます。 Matrix matrix_new(int row, int col) { Matrix a = new double *[row + 1]; // 行を作る(1つ余分に作る) for(int i = 0; i < row; i++) // 列を作る a[i] = new double[col]; a[row] = 0; // ヌルポインタを指す return a; // アドレスを返す } ヌルポインタは何も指していないことを意味しますから、これを目印にして領域を解放す ることができます。 void matrix_del(Matrix a) { Matrix b = a; while(*b != 0) delete [] *b++; // 列を解放 delete [] a; // 行を解放 } ここで列を解放する(つまり行ベクトルを解放する)のに、一時的なポインタbを使って いることに注意しましょう。bは行ベクトルを指す配列ポインタの先頭を指していますか ら、*bは1行目の行ベクトル(横ベクトル)のアドレスです。*bの値がヌルポインタ になるまで、ポインタbをインクリメントして列を解放して行きます。このwhile文は次 の様に書いたものと同じです。 while(*b != 0){ delete [] *b; // 列を解放 ++b; // 行を1つ進める } ポインタbはインクリメントされる度に値が変わっていますので、もはや行列の先頭を指 してはいません。そして最後に元のポインタaで行を解放します。 [注]このトリックは、文字列操作では普通に使われています。文字列は最後の要素にヌル 文字('\0')を挿入した(1個余分にとった)配列ですから、文字列の長さを返すC言語 の関数 strlen() はおよそ次のような方法でヌル文字を目印にして文字数を求めます。 // 文字列 sの長さを返す int strlen(char *s) { char *p = s; while( *p != '\0') p++; return ( p - s ); } 以上より、 Matrix a = matrix_new(row, col); で行列を作成して、 matrix_del(a); で行列を解放できるようになりました。 【演習問題 2.12】プログラム lst02_13.cpp を上の方法を使って書き換えなさい。          (回答プログラム lst02_14.cpp

2.9.3 分割コンパイル  行列を操作する関数ライブラリを作ります。プログラムを複数のファイルに分けて、関 数のプロトタイプ宣言だけを書いたファイルをヘッダファイル matrix09.h として作り、 関数実装を書いたものを別のファイル matrix09.cpp として作ります。
//  ヘッダファイル matrix09.h
// 行列操作ライブラリ インターフェイス部 
#ifndef MATRIX09_H 
#define MATRIX09_H 

typedef double **Matrix; 

Matrix matrix_new(int, int); 
void matrix_del(Matrix); 
void read(Matrix, int, int); 
void print(Matrix, int, int); 

#endif

ソースファイル matrix09.h
 また関数定義ファイル matrix0.cppには、ヘッダファイル matrix09.h をインクルー ドします。
// matrix09.cpp
// 行列操作ライブラリ 関数定義部
#include <iostream.h>
#include "matrix09.h" // 行列操作ライブラリのヘッダファイル

// 行列の動的作成
Matrix matrix_new(int row, int col)
{
    Matrix a = new double *[row + 1]; // 行を作る
    for(int i = 0; i < row; i++)      // 列を作る
        a[i] = new double[col];
    a[row] = 0;

    return a;
}

//行列の解放
void matrix_del(Matrix a)
{
    Matrix b = a;
    while(*b != 0)
        delete [] *b++;  // 列を解放
    delete [] a;         // 行を解放
}

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

// 行列要素をプリント
void print(Matrix a, int row, int col)
{
    cout.setf(ios::scientific); // 科学表記法
    for(int i = 0; i < row; i++){
        for(int j = 0; j < col; j++)
            cout << a[i][j] << '\t';

        cout << endl;  //行の終わりに改行
    }
}
ソースファイル matrix09.cpp
以上が関数ライブラリです。  次にこのライブラリを利用するドライバ・プログラムを書きます。ライブラリ関数を 利用するにはヘッダファイル matrix09.h をインクルードします。
// lst02_15.cpp
// lst02_15.cpp
// ドライバプログラム
#include <iostream.h>
#include "matrix09.h"

int main()
{
    int row, col; // 行と列の大きさ
    cout << "行の大きさを入力してください: ";
    cin  >>  row;
    cout << "列の大きさを入力してください: ";
    cin  >> col;

    Matrix mat = matrix_new(row, col);

    read(mat, row, col);

    cout << "行列要素の表示:" << endl;
    print(mat, row, col);

    matrix_del(mat);

    return 0;
}
ソースファイル lst02_15.cpp
2つのプログラムファイル lst02_15.cpp と matrix09.cpp は共に matrix09.h をイ ンクルードしていることに注目しましょう。ヘッダーファイル matrix09.h には2つの モジュール間で共有される情報が格納されています。 matrix09.cpp にはヘッダーファ イルでプロトタイプ宣言された関数の本体定義があり、関数の利用者であるユーザープ ログラム lst02_15.cpp では関数を呼び出して利用します。  2つのプログラムは別々にコンパイルされ、その後で結合(リンク)されて1つの実 行ファイルが作られます。  以下に示すのは、インプライズ社の Borland C++ Builder 3 のコマンドライン・コ ンパイラ bcc32.exe を使った分割コンパイルの例です。
C:\Source>bcc32 lst02_15.cpp matrix09.cpp 
Borland C++ 5.3 for Win32 Copyright (c) 1993, 1998 Borland International 
lst02_15.cpp: 
matrix09.cpp: 
Turbo Incremental Link 3.01 Copyright (c) 1997, 1998 Borland International 

C:\Source>
 コンパイラを実行すると、2つのプログラムが実行されます。1つはコンパイラ自体で、 2つのソースファイルをマシン語のコード(オブジェクトコード)に変換します。これら は拡張子.objの目的ファイル(オブジェクトファイル)として作成されます。  コンパイラはその後、リンカ・プログラム ilink32.exe を呼び出します。リンカはオ ブジェクトコードが参照している外部関数の呼び出しコードをリンクして、実行可能イメ ージを生成します。これが拡張子.exeの実行ファイルです。この例では、lst02_15.exe が作られます。
C:\Source>lst02_15 
行の大きさを入力してください: 3     
列の大きさを入力してください: 3      
3 行3 列の行列要素を入力してください 
1 行1 列: 123.456 
1 行2 列: 234.567 
1 行3 列: 30 
2 行1 列: .456789 
2 行2 列: 543.2 
2 行3 列: 90.9 
3 行1 列: 32.12 
3 行2 列: 3.456789 
3 行3 列: 123.5 
行列要素の表示: 
1.234560e+02    2.345670e+02    3.000000e+01         
4.567890e-01    5.432000e+02    9.090000e+01         
3.212000e+01    3.456789e+00    1.235000e+02         

C:\Source>
 コンパイラにオプション ' -c ' を付けて実行すると、コンパイラはリンカを呼び出し ません。コンパイルするだけです。  関数ライブラリは予めコンパイル済みのオブジェクト・コードとして利用されるのが普 通です。次のように、関数ライブラリプログラムをコンパイルだけを行っておいて、
C:\Source>bcc32 -c matrix09.cpp 
Borland C++ 5.3 for Win32 Copyright (c) 1993, 1998 Borland International  
matrix09.cpp: 

C:\Source>
ユーザープログラムから利用するには、ヘッダーファイル matrix09.h と目的ファイル matrix09.obj を使って、次のようにコンパイル/リンクします。matrix09.obj はリン ク時に使われます。
C:\Source>bcc32 lst02_15.cpp matrix09.obj 
Borland C++ 5.3 for Win32 Copyright (c) 1993, 1998 Borland International    
lst02_15.cpp: 
Turbo Incremental Link 3.01 Copyright (c) 1997, 1998 Borland International  

C:\Source>
 行列操作ライブラリを利用する lst02_15.cpp がインクルードして参照するのはヘッダ ファイル matrix09.h であることに注意しましょう。ヘッダファイルには外部から利用で きる関数やその他のデータ構造の宣言が記述されていて利用者に公開されます。ヘッダフ ァイルには関数の利用法についてのコメントが書かれます。利用者にとって重要なのは関 数の使い方であって、それがどのように実装されるかではありません。関数の実装コード は matrix09.objの中にあり利用者から隠されます。この様にC言語スタイルのプログラ ミングではヘッダファイルは素朴な意味での情報隠蔽の機構を形成します。C++のクラス はさらに優れた情報隠蔽の機能を持ちます。 ★コマンドライン・コンパイラの環境設定についてはこちら


2.9.4 プリプロセッサ(前処理プログラム)  プログラム・ファイルの先頭でヘッダファイルをインクルードする命令はプリプロセッ サ命令といいます。プリプロセッサは前処理プログラムのことで、コンパイラはプログラ ムをコンパイルする前にプリプロセッサを起動させます。プリプロセッサ命令はC/C++の プログラミング言語ではありません。従って、C/C++言語とは異なる構文規則に従います (つまりC/C++の構文を理解しません)。すべてのプリプロセッサ命令は先頭にハッシュ 記号(#)が付きます。  #includeプリプロセッサ命令は、指定したファイルのコピーをプログラムの中に取り込 み(インクルード)ます。これには lst02_15.cpp にあるように2つの形式があります。 #include <iostream.h> #include "matrix09.h" ヘッダファイルを括弧(<と>)で囲んだ場合、これは標準ライブラリに対して使われ、プ リプロセッサは既定のディレクトリからファイルを検索します。またファイル名が二重引 用符("")で囲まれている場合は、コンパイルされるファイルのあるディレクトリを検索 し、そこにないときは既定のディレクトリを検索します。インクルードするのはプログラ ムファイルでも構いません。従って、関数プロトタイプ宣言と関数定義を1つのファイル matrix09.h にしてこれをインクルードすることもできます。この場合、このページの最 初のプログラム lst02_13.cpp と同じ、すべてを1つのファイルとしてコンパイルされま す。  プリプロセッサが行う処理をファイルに書き出すと、これが何を行っているのかよく分 かります。 cpp32 lst02_15.cpp と実行すると、拡張子.Iのファイルが作成されます。テキストエディタでこのファイルを 見ると、末尾の部分が次のようになっています(これは大きなファイルとなっているはず です。ファイルのほとんどは iostream.h のインクルード部分で占められています)。
typedef double **Matrix; 
Matrix matrix_new(int, int);  
void matrix_del( Matrix );  
void read(Matrix, int, int);  
void print(Matrix, int, int);  

int main() 
{ 
int row, col;  
cout << "行の大きさを入力してください: ";        
cin  >>  row; 
cout << "列の大きさを入力してください: ";         
cin  >> col; 

Matrix mat = matrix_new(row, col); 

read(mat, row, col); 

cout << "行列要素の表示:" << endl; 
print(mat, row, col); 
matrix_del(mat); 

return 0; 
}
インクルードしたヘッダファイルがコピーされています。つまりプリプロセッサとは、 特殊なテキストエディタのことです。  このことから分かるように同じファイルを2度インクルードすると、宣言を重複し て行うことになるため構文エラーとなります。そのため、matrix09.h の最初と最後 にプリプロセッサ命令 #ifndef MATRIX09_H #define MATRIX09_H ...... #endif でコードを囲み、重複して読み込むことを回避しています。#ifndef命令は、MATRIX09_H という名前が既に定義されているとき、#ifndefから#endifの間のコードを無視します。 #define命令は名前 MATRIX09_H を定義します。  プリプロセッサはこの他に、記号定数やマクロをプログラムコードの中に展開したり、 条件付きでコンパイル処理を行います。

| 目次 | 前のページ | 次のページ |

Copyright(c) 1999 Yamada,K