next up previous
Next: 6 練習 Up: 計算天文学 II 第5回 常微分方程式の初期値問題(1) Previous: 4 線形多段階法

5 構造体とクラス

ここからはちょっと C++ 言語の話題というか、今回以降の話で便利な機能につい て。

偏微分方程式とか、あるいは連立常微分方程式の数値解法をプログラムするの に、普通は配列を使う。しかし、これは割合に面倒だし、いろいろ妙な間違い をする可能性も高い。例えば、 $\mbox{\boldmath$x$}$ $\Delta \mbox{\boldmath$x$}$ を足すのは、数式と しては $\mbox{\boldmath$x$}+ \Delta \mbox{\boldmath$x$}$ で済むのに、例えば C では

for(i=0;i<n;i++) x[i] += dx[i];
Fortran なら
      do i = 1, n
          x(i) = x(i) + dx(i)
      enddo
という具合で、数式なら4文字で済むところをその何倍も書かないといけない。 これは、 C にしても Fortran にしても、配列(あるいはベクトルとか行列) といった、数学では基本的な要素であるものに対する演算を直接に表現する記 法を持っていないからである。

まあ、Fortran 95 とかそういったものを使うとそういう記法があるという話 もないわけではないが、あまり普及していない。普及していない理由はいろい ろあるが、その一つは C++ を使えば同様なことが実現できるからである。

C++ 言語自体は、配列に対する演算というものを用意しているわけではない。 しかし、C++ では、プログラムの中で新しい「型」(実数型とか整数型という のと同じ意味での)を定義して、さらにそれに対する演算を定義することがで きる。これでベクトル型とかいったものを自分の使いやすいように定義すれば いいことになる。

例えば、非常に基本的なベクトル型の定義は以下のようなものになる。ここで は加算と入出力くらいしか定義していないが、他の必要な演算も同様に定義で きる。一応使いそうな演算を定義したものが

http://grape.astron.s.u-tokyo.ac.jp/~makino/pcphysics/programs/vector.h
にあるので、実際にプログラムを作る時にはこれを使ってもよい。牧野が書い たプログラムは信用できないという向きは自分で書くこと。

/------------------------------------------------------
/  vector  --  a class for VLEN-dimensional vectors
/  VLEN  must be defined as constant before this header
/------------------------------------------------------
#ifndef  _VECTOR_H
#  define  _VECTOR_H
class mvector
{
private:
    double element[VLEN];
public:
    mvector(double c=0 ) {for (int i=0;i<VLEN;i++)element[i]=c;}
    double & operator [] (int i)       {return element[i];}
    friend const mvector operator + (const mvector &,
                                    const mvector &);
    mvector& operator += (const mvector& b)
    {for(int i=0; i<VLEN;i++)element[i] += b.element[i];       
    return *this;}
    friend ostream & operator <<(ostream & , const mvector & );
    friend istream & operator >> (istream & , mvector & );
};

inline ostream & operator <<(ostream & s, const mvector & v)
{
    for(int i=0; i<VLEN;i++)s <<v.element[i] <<"  " ;
    return s;
}
inline istream & operator >> (istream & s, mvector & v)
{
    for(int i=0; i<VLEN;i++)s >> v.element[i];
    return s;
}
inline const mvector operator + (const mvector &v1,
                                const mvector & v2)
{
    mvector v3;
    for(int i=0; i<VLEN;i++)
        v3.element[i] = v1.element[i]+ v2.element[i];
    return v3;
}
typedef const mvector (vfunc)(const mvector &); 
#endif

実際にこれを使うには、

const int VLEN = 2;
#include  "vector.h"

double k;
mvector dxdt(mvector & x)
{
    mvector d;
    d[0] = x[1];
    d[1] = -k*x[0];
    return d;
}

    .....
    double h;
    mvector kx1;
    kx1 = dxdt(x)*h;
    ......

というような具合に、ベクトルの大きさを指定する(これはここでは固定であ る)。もちろん、可変にするとかいろんなことができるが、繁雑になるのでこ こでは固定サイズにする。

ここで、注意してほしいのは、 + という演算子や [] という、 これも「演算子」が、新しく定義されていることである。これらは、もちろん + なら実数や整数に対してすでに定義されているが、ここではベクトル 同士の演算に対して新しい意味を持つように拡張されていることになる。 [] も同様で、配列の他にここで定義したベクトル型について新しい意味 を持つようになったわけである(といっても、こちらは普通の配列に対してと まったく同じように働くが)。

これは、偉そうにいうと C++ の演算子多重定義 (overload)という強力な機能 である。まあ、落ち着いて考えてみると、この機能の実現はそんなに難しいわ けではなくて、例えば + という演算子が出てきたところで、それが適 用されているデータ型を見て、そのデータ型に対して定義されている演算を呼 ぶようにするというだけのことである。これは、コンパイラにそういう機能を 付け加えるだけで実現できる。

C++ の場合には、このような、ある意味での機能拡張は、「クラス」というも のを使って実現されている。これはもともとは「オブジェクト指向」とかそう いった難しい機能を実現するためのものであるが、ここではとりあえずあんま りそういうことは考えないでベクトル型を使うことにする。

なお、名前が素直に vector ではなく mvector (mathematical vector のつも り)になっているのは、 C++ の新しい標準では vector と いう 語に別の意味を与えているからである。

上のコードの意味を簡単に説明しておく。 class mvector は mvector という名前のクラスの定義を始めますという 宣言である。その後に {} で囲って中身を書く。

中身には private: と書いてから書くものと public: と書い てから書くものがあって、 private: のほうは、「普通には外から見 えない」ものを定義するのに使う。典型的なのはこの例のように、あるクラス の「オブジェクト」が持つデータを宣言することである。データの宣言は普通 の変数宣言と同じ。

public: のほうでは、主に「メンバー関数」というものを宣言するこ とになる。ここで、自分の名前と同じ名前の関数は特別な意味を持っていて、 (constructor)、このクラスの変数がプログラムの中で使われる時に呼ばれる ものである。 上の例では、デフォルトでは vector a; みたいに書くと element の各要素に 0 が代入される。0以外にしたければ vector a = vector(1); とか書く。

その次の double & operator [] (int i){ ...} というのははっき りいってなんだかわからないが、 C++ ではいくつかの演算記号や上の配列の 記号等に、クラス毎に別の意味を与えることができる。これが「演算子の多重 定義」や「オーバーロード」と呼ばれる機能である。

というとなんだか恐ろしげに聞こえるが、考えてみると ``+'' だって実数と 整数では全く違うことをするわけで、別のデータ型を定義したらそれの ``+'' はまた違うことをして欲しいというのは当然であろう、というより、ここで我々 がしたいのはそもそもそういうことであった。 上の例では、 結局プログラム の中で mvector a; ... a[i] ... という感じに書くと、 a[i]a の要素である elementi 番目の要素、つまり element[i] と全く同じことになる。

なお、 C 言語では構造体 struct というものがあり、例えば

struct mvector{
    double element[NDIM];
};

といったふうに使う。ここで struct mvector a; のように変数を宣言す ると、 a.element[i] といった形で ``.'' を使って要素にアクセスす ることになる。これはちょっと見苦しい。また、C には演算子のオーバーロー ドの機能はないので、折角構造体を作っても、 ``+'' で足すというわけに はいかない。これはある意味みかけだけの問題ではあるが、見てわかりやすい ということはプログラムを書いたりデバッグしたりする時には割合大事なこと である。

その後の friend const mvector operator + ... が、実際に mvector 同士の加算を宣言する。但し、ここでは形だけを宣言して、実体は下 のほうに書く。 但し、その次の += のように全部書いてもかまわない。

mvector a, c; ... a += c; と書くと、``+='' の中で いきなり element と書かれているものは a の要素、 b.element になっ ているのは c の要素になって、結局 a の各要素に c の 各要素が足されるという、普通に期待したい動作が実現されることになる。なお、こ のような、宣言してないクラス変数を使う関数を「メンバー関数」という。

``+'' も事情は同様だが、ここでは 2 つ引数を取る普通の関数とし て定義されている。 friend を付けると、 private: と宣言 したものもこの関数の中では使えるようになるので、ここでは friend を付けている。

`` <<'', `` >>'' はメンバ関数として実現されている。



Jun Makino
平成17年11月7日