プリント基板設計・シミュレーション

TOP > アポロレポート > コラム > 第45回 プログラミングについて『C++のクラスの詳細(Complex の例)』
コラム
2023/09/29

第45回 プログラミングについて『C++のクラスの詳細(Complex の例)』

アポロレポート

 今回はもう少しオブジェクトについて詳しく考えてみましょう。構造体の説明でも例としてよく使われるのですが、複素数をオブジェクトとして扱う例を考えてみましょう。複素数は誰でもが知っているもので、実数部と虚数部の2つの要素しかないので説明の例にするには好都合なのです。

 複素数を構造体で表現すると、

 struct complex_strct{
  double x,y;
 };

となります。普通はこれ以外に構造の表現方法はないかと思います。この構造体を使って変数を2つ定義して加算を行なってみましょう。

 #include <stdio.h>

 struct complex_strct{
  double x,y;
 };

 void main()
 {
  struct complex_strct cmplx1,cmplx2;

  cmplx1.x=1.0; cmplx1.y=2.0;

  cmplx2.x=3.0; cmplx2.y=4.0;

  cmplx1.x+=cmplx2.x;
  cmplx1.y+=cmplx2.y;

  printf("%f %f\n",cmplx1.x,cmplx1.y);
 }

このプログラムの結果は、

 4.000000 6.000000

となります。代入する部分と加算する部分を関数にしたとすると、

 #include <stdio.h>

 struct complex_strct{
  double x,y;
 };

 struct complex_strct complex_set(double x,double y);
 struct complex_strct complex_add(
                struct complex_strct c1,
                struct complex_strct c2);
 void main()
 {
  struct complex_strct cmplx1,cmplx2;

  cmplx1=complex_set(1.0,2.0);
  cmplx2=complex_set(3.0,4.0);

  cmplx1=complex_add(cmplx1,cmplx2);

  printf("%f %f\n",cmplx1.x,cmplx1.y);
 }

 struct complex_strct complex_set(double x,double y)
 {
  struct complex_strct tmp;

  tmp.x=x; tmp.y=y;

  return tmp;
 }

 struct complex_strct complex_add(
                struct complex_strct c1,
                struct complex_strct c2)
 {
  c1.x+=c2.x; c1.y+=c2.y;

  return c1;
 }

となり、少しはスッキリしました。

 余談ですがFORTRANではこれがどうなるのかと言えば、

 COMPLEX CMPLX1,CMPLX2

 CMPLX1=(1.0,2.0)
 CMPLX2=(3.0,4.0)

 CMPLX1=CMPLX1+CMPLX2

 WRITE(6,*)CMPLX1

 END

と書くことができ、結果は、

 (4.000000,6.000000)

となります。さすがは科学計算用のプログラミング言語と言われるだけあって、あまりにも何事もなく表現できるのでちょっと拍子抜けの感があります。FORTRANは複素数を言語の仕様自体のなかで扱えるようになっているのでこれほど簡単に表現することができるのです。私が知っている限り複素数を言語として扱えるのはFORTRANだけです。複素数の定数というものも表現ができ、(1.0,2.0) とすると複素数の定数となるのです。

 それでは、C++で複素数をオブジェクトにしたときにはどのようになるのでしょうか。

 #include <iostream.h>

 class Complex{
  public:
   double x,y;

   Complex(double x=0.0,double y=0.0);
   Complex Add(Complex cmplx);
 };

 void main()
 {
  Complex cmplx1(1.0,2.0);
  Complex cmplx2(3.0,4.0);

  cmplx1.Add(cmplx2);

  cout << cmplx1.x << " " << cmplx1.y << endl;
 }

 Complex::Complex(double x,double y)
 {
  this->x=x;
  this->y=y;
 }

 Complex Complex::Add(Complex cmplx)
 {
  x+=cmplx.x;
  y+=cmplx.y;

  return *this;
 }

 この例はあまり感心できるものではないのですが詳しく見てみましょう。まずクラスの宣言部。

 class Complex{
  public:
   double x,y;

   Complex(double x=0.0,double y=0.0);
   Complex Add(Complex cmplx);
 };

 このクラスは変数 x と y がパブリックになっています。ということはこの変数は誰でも直接アクセスすることができるということになります。コンストラクタの宣言の引数の部分がC言語にはない形式です。このように宣言するとオブジェクトを実際に生成させるときに、

 Complex c;

と単純に記述すると、引数が省略されたということでコンパイラは省略された引数の代わりに 0.0 を使用するのでオブジェクトを生成したときに自動的に初期化されるのです。

 Complex c(5.5);

として生成させたときには第2引数が省略されているので、第2引数のみにディフォルトの値を使用することになります。ですからメインの関数では省略されていませんので指定された値を使用して初期化されます。また、

 Complex c=Complex(1.0,2.0);

と記述することもでき、このときには c をディフォルトの値で生成した後、Complex クラス型の値を代入することを意味します。この Complex(1.0,2.0) は定数です。

 次にメインの関数を見てみましょう。特に変ったところといえば、

 cmplx1.Add(cmplx2);

の部分です。こんな書き方はC言語ではありません。これはオブジェクト cmplx1 自身が持っている機能、すなわち Complex クラスのパブリックな関数を使用して cmplx2を加算することを意味します。 Complex クラスのデータメンバーがパブリックとしていますので、加算するときにはそのデータメンバーどうしで加算を行なうことも当然できます。

  cout << cmplx1.x << " " << cmplx1.y << endl;

の部分は cmplx1 のデータメンバー x と y をそれぞれ cout に出力しています。このデータメンバーは double 型ですので、double 型のオブジェクトはそれぞれに << 演算子に対する動作を知っていますので問題なく出力されます。

 Complex クラスのメンバー関数の実体の部分を見てみましょう。
まずはコンストラクタ。

 Complex::Complex(double x,double y)
 {
  this->x=x;
  this->y=y;
 }

この関数には見慣れない上に定義もされていないポインタが使用されています。この this というポインタはオブジェクト内で暗黙のうちに存在するもので、オブジェクト自身を指し示すポインタです。このポインタを説明するために、コンストラクタの引数名をデータメンバー名と同じにしたのですが引数名を違ったものにすれば、次の様に見慣れた表現をすることができます。

 Complex::Complex(double xx,double yy)
 {
  x=xx;
  y=yy;
 }

加算をするメンバー関数では、加算をした結果を *this で返しています。このオブジェクトでの this は (Complex *) ですので *this は *(Complex *) となります。

 上の例はそれはそれでいいのですが、FORTRANのものと比べるとまだ不満が残ります。

 cmplx1=cmplx1+cmplx2;

とか、

 cout << cmplx1 << endl;

などとできればプログラムがもっと分かりやすくなるように思いませんか。そこで次ののように改造してみましょう。だたしデータメンバーはパブリックのままとします。

 #include <iostream.h>

 class Complex{
  public:
   double x,y;

   Complex(double x=0.0,double y=0.0);
   Complex operator + (Complex cmplx);
   friend ostream& operator << (ostream& os,Complex& cmplx);
 };

 void main()
 {
  Complex cmplx1(1.0,2.0);
  Complex cmplx2(3.0,4.0);

  cmplx1=cmplx1+cmplx2;

  cout << cmplx1 << endl;
 }

 Complex::Complex(double x,double y)
 {
  this->x=x;
  this->y=y;
 }

 Complex Complex::operator + (Complex cmplx)
 {
  return Complex(x+cmplx.x,y+cmplx.y);
 }

 ostream& operator << (ostream& os,Complex& cmplx)
 {
  return cout << '(' << cmplx.x << ',' << cmplx.y << ')' ;
 }

 また見慣れないものが増えてしまいました。cmplx1+cmplx2 の + も、cout << cmplx1の << も演算子ですので、この演算子に対する宣言と定義が必要になるのでこれも仕方ありません。
 メインの関数は目的の記述をしていますので説明は省略しましょう。最初に Complexクラスの宣言部を見てみましょう。

 class Complex{
  public:
   double x,y;

   Complex(double x=0.0,double y=0.0);
   Complex operator + (Complex cmplx);
   friend ostream& operator << (ostream& os,Complex& cmplx);
 };

 Complex operator + (Complex cmplx); の行は、演算子 + の左辺が自分自身で右辺がComplex 型のときには Complex 型を返すという宣言です。その次の行が演算子 << の宣言を行なっているのですが理解するのはちょっと厄介です。

 ostream 型は cout のクラスで、ostream& とするとこの型の参照を意味します。参照の詳細はまた別の機会に説明しますが基本的にはポインタと似ています。この場合は、cout と同じ型のものとでも考えておいて下さい。
 また難しいのがこの行の最初の friend です。friend を指定するとこの演算子 << はComplex クラス以外のクラスからでもクラス以外のものからでもアクセスすることができるようになります。またこの場合はクラス内のメンバー関数(演算子)ではなく通常の関数(演算子)と同じ扱いになるので、クラスの宣言の外側で、

 class Complex{
  public:
   double x,y;

   Complex(double x=0.0,double y=0.0);
   Complex operator + (Complex cmplx);
 };

 ostream& operator << (ostream& os,Complex& cmplx);

と宣言しても同じことになります。演算子 + の宣言と違い演算子 << の宣言では、

 (ostream& os,Complex& cmplx)

と2つの型を指定しています。これは、

 cout << Complex(1.0,2.0) << endl ;

としたときに、cout は ostream クラスですのでこのクラスそのものには Complex クラスとの演算子は定義されていませんからエラーになってしまうのですが、

 ostream& operator << (ostream& os,Complex& cmplx);

と宣言することで、演算子 << の左辺が ostream クラスで右辺が Complex クラスの演算を行なえるようにするためなのです。

 各関数の実体の部分は次のようになり特に難しいことはありませんが friend で宣言した演算子 << は Complex クラスのメンバー関数(演算子)ではなく通常の関数(演算子)として定義します。

 Complex Complex::operator + (Complex cmplx)
 {
  return Complex(x+cmplx.x,y+cmplx.y);
 }

 ostream& operator << (ostream& os,Complex& cmplx)
 {
  return cout << '(' << cmplx.x << ',' << cmplx.y << ')' ;
 }

 最後にメインの関数をFORTRANの例と見比べてみましょう。

 FORTRAN版

                   void main()
                   {
 COMPLEX CMPLX1,CMPLX2         Complex cmplx1(1.0,2.0);
                    Complex cmplx2(3.0,4.0);

 CMPLX1=(1.0,2.0)
 CMPLX2=(3.0,4.0)

 CMPLX1=CMPLX1+CMPLX2         cmplx1=cmplx1+cmplx2;

 WRITE(6,*)CMPLX1           cout << cmplx1 << endl;

 END                 }

 ほぼ同じ記述の仕方になっています。かえって簡単になっているかも知れません。

 それではまた次回。


そのお悩み、
アポロ技研に話してみませんか?

アポロ技研でワンランク上の物創りへ!
そのお悩み、アポロ技研に話してみませんか?