3.14 フレンドメンバ関数とポインタの利用  (1999.12.05 初版)

3.14.1 フレンドなメンバ関数とフレンドクラス

 あるクラスのメンバ関数を、別のクラスのフレンド関数にすることができます。この
場合フレンドとするクラス定義の内部で、そのメンバ関数をスコープ解決演算子を使っ
て friend 宣言します。
 また、あるクラスのすべてのメンバ関数が別のクラスのフレンド関数であるようにす
ることもできます。これは、クラスそのものを別のクラスのフレンドとすることであり、
この様なクラスのことをフレンドクラスと呼びます。例えば、行列クラス Matrix を
ベクトル・クラス Vector のフレンドクラスにするには、次のように宣言します。

class Vector{
    friend class Matrix; // フレンドクラス
    ・・・・・・

};

この様にすることで、Matrixクラスのすべてのメンバ関数から Vectorクラスの非公開
メンバにアクセスすることができるようになります。しかし、2つのクラスの関係を良
く吟味する必要があります。フレンド関係は情報隠蔽(カプセル化)を破るものである
ため、内部表現へのアクセス権は必要最小限にすべきです。ここでのベクトル・行列ク
ラスでは、フレンドクラスの導入は行き過ぎでしょう。

 特定のメンバ関数を、別のクラスのフレンド関数にすることが意義のある場合もあり
ます。例として、行列クラスのメンバ関数(*=) Matrix::operator*=() をベク
トルクラスのフレンド関数にする場合を取り上げます。
// matrix20.h
// 行列クラスのメンバ関数(*=)をベクトルクラスのフレンド関数にする

class Vector; // 前方宣言

class Matrix{
    ・・・・・・
public:
    ・・・・・・
    Matrix &operator*=(const Matrix &);
    ・・・・・・
private:
    Vector *ptr;// 行ベクトルへのポインタ
    int Row;    // 行の数
    int Col;    // 列の数
};

class Vector{
    ・・・・・・
    friend Matrix &Matrix::operator*=(const Matrix &);// フレンドメンバ関数 
    ・・・・・・
private:
    double *ptr; //ベクトルの先頭要素へのポインタ
    int Dim;     //ベクトルの次元
};
行列の演算子関数*=は、行ベクトルの内部表現に添え字演算子を使わずに直接アクセ
スできます。この様にすることで、実行効率を高めることができます。
// 多重定義された*=演算子
Matrix &Matrix::operator*=(const Matrix &right)
{
    if((Col != right.Row) || ((Col != right.Col))){//サイズのチェック
        cout << "エラー:行列の型が一致しません\n";
        abort();
    }

    Matrix mat(Row, right.Col); //一時的なオブジェクトを作る
    for (int i = 0; i < Row; i++)
        for(int j = 0; j < right.Col; j++){
            double sum = 0.0;
            for(int k = 0; k < Col; k++)
                sum += ptr[i].ptr[k] * right.ptr[k].ptr[j];
            mat.ptr[i].ptr[j] = sum;
        }

    mat.cleanup(); // 微少要素の除去

    return *this = mat;
}
 クラスのインターフェース部で、Matrixクラス定義を先に行っていることに注目しま
しょう。Vectorクラス定義内で、Matrixクラスのメンバ関数を friend宣言するには、
そのメンバ関数が既に宣言されている必要があるからです。

3.14.2 行ベクトルの交換

 行列オブジェクトの行の交換を行うメンバ関数を実装します。行列オブジェクトの内
部データ(要素の値)は、各行要素の値が配列として納められていて、それらをポイン
タが指しています。
 このとき行の交換は、行要素のデータを指しているポインタの値(アドレス)同士を
交換することで実現できます。つまり、2つのポインタの値を交換することで、実際の
データ値そのものを直接交換することなく行えます。これが、ポインタ利用の大きな利
点です。

 次の図は、5×5行列オブジェクトで第2行と第4行を交換する様子を示しています。
行ベクトルの交換
行列オブジェクトの表現自体は、行ベクトル・オブジェクトの配列の先頭を指すポイン
タ ptr です。そして、各行ベクトル・オブジェクト(ptr[0] 〜 ptr[4])は、行ベク
トル要素が格納されている配列を指すポインタ ptr を非公開メンバとして持ちます。
従って、要素を指すポインタは行列オブジェクトからは隠蔽されています。
 行を交換するメンバ関数を実現するために、このメンバ関数をベクトル・クラスに対
してフレンド関数にします。
// matrix20.h
// 行を交換するメンバ関数(ベクトルクラスのフレンド関数にする)

class Vector; // 前方宣言


class Matrix{
    ・・・・・・
public:
    ・・・・・・
    void swap(int, int); // 行の交換
    ・・・・・・
private:
    Vector *ptr;// 行ベクトルへのポインタ
    int Row;    // 行の数
    int Col;    // 列の数
};

class Vector{
    ・・・・・・
    friend void Matrix::swap(int, int); // Matrixクラスのメンバ関数
    ・・・・・・
private:
    double *ptr; //ベクトルの先頭要素へのポインタ
    int Dim;     //ベクトルの次元
};
また、行を交換するメンバ関数 Matrix::swap() は次のようになります。
// 行の交換
void Matrix::swap(int i, int j)
{
    // 引数チェック
    if( ( i < 0  || i >= Row) || ( j < 0  || j >= Row) ){    
        cout << "エラー:Matrix::swap()の引数範囲の逸脱\n";
        abort();
    }
    if(i == j ) return; // 同一行

    // 行ベクトルの交換
    double *temp = ptr[i].ptr;
    ptr[i].ptr = ptr[j].ptr;
    ptr[j].ptr = temp;
}
変数の値(この場合はポインタの値)を交換するには、一方の値を記憶するための
一時変数(この場合はポインタ)を用意して

    double *temp = ptr[i].ptr;
    ptr[i].ptr = ptr[j].ptr;
    ptr[j].ptr = temp;

とします。
 
 以上をヘッダファイル( matrix20.h )、関数定義プログラム( matrix20.cpp ) 
にまとめます。 

   ソースファイルはこちら  次のプログラムは、メンバ関数 Matrix::swap() の使い方を示しています。
// lst03_33.cpp
// 行の交換
// matrix20.cpp と一緒にコンパイルすること
#include <iostream.h>
#include "matrix20.h"

int main()
{

    Matrix a(5,4); // 5行4列の行列

    for(int i = 0; i < a.getRow(); i++)
        for(int j = 0; j < a.getCol(); j++)       
            a[i][j] = 1.0*(i+1) + 0.1*(j+1);

    cout << a << endl;

    // 2行と5行を入れ替える
    cout << "2行と5行を入れ替える" << endl;
    a.swap(1,4);

    cout << a << endl;

    return 0;
}
[実行結果]      ソースプログラム ( lst03_33.cpp )
   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   
   4.100000e+00   4.200000e+00   4.300000e+00   4.400000e+00   
   5.100000e+00   5.200000e+00   5.300000e+00   5.400000e+00   

2行と5行を入れ替える
   1.100000e+00   1.200000e+00   1.300000e+00   1.400000e+00   
   5.100000e+00   5.200000e+00   5.300000e+00   5.400000e+00   
   3.100000e+00   3.200000e+00   3.300000e+00   3.400000e+00   
   4.100000e+00   4.200000e+00   4.300000e+00   4.400000e+00   
   2.100000e+00   2.200000e+00   2.300000e+00   2.400000e+00   
このメンバ関数 Matrix::swap(int, int) の引数に渡す2つの行は、0
から始まる行番号であることに注意しましょう。

3.14.3 連立方程式の解法(ピボット選択法)

 例題として、ガウス・ジョルダン法の改良版であるピボット選択法を取り上げます。
ガウス・ジョルダン法では、係数行列の対角要素をピボット(軸)にして掃き出し計算
をします。このとき対角要素での割り算を行います。そのため、対角要素に0または、
非常に小さい値がある場合は正しく計算できません。
 この様な問題を回避するために、行の交換を行ってピボットが小さな値を取らないよ
うにして掃き出し計算を行うのが、ピボット選択法です。つまり、各ピボットのある列
の中で、絶対値が最大となる行と交換しながら計算してゆきます。
// lst03_34.cpp
// 連立方程式の解法(ピボット選択法)
// Matrix20.cppと一緒にコンパイルする

#include <iostream.h>
#include "matrix20.h"

bool Gauss_Jordan(Matrix &);// ガウス・ジョルダン法(ピボット選択法)

int main()
{
    cout << "連立方程式の解法(ピボット選択法)" << endl << endl;  

    // 係数行列(1行1列目の要素が0)
    double mat[4][5]={{ 0.0, 4.0, 1.0,-1.0,-10.0},
                       {-3.0,-2.0, 2.0, 3.0, 10.0},
                       {-2.0,-2.0, 1.0, 2.0,  5.0},
                       { 3.0, 6.0,-1.0,-2.0,  2.0}};

    Matrix a(4,5);// Matrixクラスのオブジェクトを生成する

    // 行列の読み込み
    for(int i = 0; i < a.getRow(); i++)
        for(int j = 0; j < a.getCol(); j++)
            a[i][j] = mat[i][j];

    cout << "係数行列:" << endl << a << endl;

    // ガウス・ジョルダン法(ピボット選択法)
    bool result = Gauss_Jordan(a);

    if(result)
        cout << endl << "連立方程式の解:"
             << endl << a << endl;

    return 0;
}

// ガウス・ジョルダン法(ピボット選択法)
// 係数行列の掃き出し計算を行い単位行列化した結果を返す
bool Gauss_Jordan(Matrix &a)
{
    for(int k = 0; k < a.getRow(); k++){

       // ピボットの選択
        double max = 0.0;
        int i, n;
        for(i = k; i <  a.getRow(); i++)
            if(fabs(a[i][k]) > max){
                max = fabs(a[i][k]);
                n = i;
            }
        if(max < NEARLY_ZERO) return false; // 解けない

        if( n != k ) a.swap(k,n); // 行の交換

        double p=a[k][k];// ピボット係数
        for (i = k; i <  a.getCol(); i++) a[k][i] /= p;

        for (i = 0; i < a.getRow(); i++){// ピボット列の掃き出し
            if(i != k){
                double d = a[i][k];
                for(int j = k; j < a.getCol(); j++)
                    a[i][j] -= d * a[k][j];
            }
        }
    }
    return true; // 成功
}
[実行結果]      ソースプログラム ( lst03_34.cpp )
連立方程式の解法(ピボット選択法)

係数行列:
   0.000000e+00   4.000000e+00   1.000000e+00  -1.000000e+00  -1.000000e+01  
  -3.000000e+00  -2.000000e+00   2.000000e+00   3.000000e+00   1.000000e+01  
  -2.000000e+00  -2.000000e+00   1.000000e+00   2.000000e+00   5.000000e+00  
   3.000000e+00   6.000000e+00  -1.000000e+00  -2.000000e+00   2.000000e+00  

連立方程式の解:
   1.000000e+00   0.000000e+00   0.000000e+00   0.000000e+00   1.500000e+01  
   0.000000e+00   1.000000e+00   0.000000e+00   0.000000e+00  -2.000000e+00  
   0.000000e+00   0.000000e+00   1.000000e+00   0.000000e+00   9.000000e+00  
   0.000000e+00   0.000000e+00   0.000000e+00   1.000000e+00   1.100000e+01  
 プログラムでは、次の連立1次方程式を解きます。

  | 0  4  1 -1 | | x |   |-10 | 
  |-3 -2  2  3 | | y | = | 10 | 
  |-2 -2  1  2 | | z |   |  5 | 
  | 3  6 -1 -2 | | w |   |  2 |, 

第1行1列目の要素の値が0になっています。1列の掃き出しの時には、絶対値が最大
の要素を持つ行と交換して計算します。

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

Copyright(c) 1999 Yamada, K