第八回レポート総評




演習後の全ファイル

演習を終えた後のファイルの例を以下に示します。


/*  myComplex.h */

class myComplex{
 private:
  double x;   /* real part */
  double y;   /* imaginary part */

 public:
  myComplex(double xx=0, double yy=0){ x = xx; y=yy; }

  void setReal(double xx){ x = xx; }
  void setImag(double yy){ y = yy; }

  double getReal(void){ return x; }
  double getImag(void){ return y; }

  double norm(void);

  myComplex operator+(const myComplex& c){
    return( myComplex(x+c.x, y+c.y) );
  }

  myComplex operator-(const myComplex& c){
    return( myComplex(x-c.x, y-c.y) );
  }

  myComplex operator*(const myComplex& c){
    return( myComplex(x*c.x-y*c.y, x*c.y+y*c.x) );
  }
};

/* myComplex.cpp */

#include <math.h>
#include "myComplex.h"

double myComplex::norm(void){
	return(sqrt(x*x + y*y));
}

/* complex_test.cpp */

#include <iostream>
#include "myComplex.h"
using namespace std;

int main(void){

	myComplex c1(3,4);    /* myComplex 型の変数 */
	myComplex c2(1,1);
	myComplex c3;

	myComplex *cp1, *cp2, *cp3;    /* myComplex 型のポインタ */

	cp1 = new myComplex;    /* ヒープ領域への確保 */
	cp2 = new myComplex(5);
	cp3 = new myComplex(3,4);

	c3 = c1 + c2;  /* 加算 */

	cout << "c1+c2=" << c3.getReal() << "+" << c3.getImag() << "i" << endl;

	c3 = c1 - c2;  /* 減算 */

	cout << "c1-c2=" << c3.getReal() << "+" << c3.getImag() << "i" << endl;

	c3 = c1 * c2;  /* 乗算 */

	cout << "c1*c2=" << c3.getReal() << "+" << c3.getImag() << "i" << endl;

	delete cp1; /* 領域の開放 */
	delete cp2;
	delete cp3;

	return 0;
}


上のプログラムの実行結果は以下のようになります。

c1+c2=4+5i
c1-c2=2+3i
c1*c2=-1+7i


多かった間違いを列挙して行きます。

まず、複素数の乗算の定義が間違っている人が何人かいました。

( x1 + y1 i) * ( x2 + y2 i) = ( x1 x2 - y1 y2 ) + ( x1 y2 + y1 x2 ) i

ですので、注意しましょう。

また、ポインタが指す領域を開放し忘れている人が多かったようです。
メモリを自分で確保した場合は、その状態を把握しておかねばなりません。

また、領域を開放する場合、以下のように記述してしまった人が多かったようです。

delete cp1, cp2, cp3;


これは cp1、cp2、cp3 の指す領域を全て開放できているでしょうか?
結論から先に言うとこの記述は間違いです。

間違いであることを確認するため、以下のことを考えてみましょう。

まず、「delete cp1;」という命令が呼ばれると、cp1 が指すオブジェクトに対して デストラクタが呼ばれます (第九回参照)。
ですから、正しい回数デストラクタが呼ばれているかどうか確かめれば良さそうです。
そこで、以下のようなデストラクタを記述してみましょう。

/*  myComplex.h */
#include <iostream>
...
 public:
  ~myComplex(){
	std::cout << "destructed" << std::endl;
  }
...


このデストラクタは、実行された時に画面に「destructed」と表示するだけのものです。
# 簡単にデバッグを行いたいとき、このようにクラスの内部で画面表示を実行してみると、実行のタイミングなどがわかって勉強になります。

これで再構築し、"destruct" が何回表示されたか調べ、全てのメモリが 開放されているか調べれば良い、というわけです。

# なお、「myComplex c;」のように自動変数として定義した c に対しては、main 関数が終了するときにデストラクタが自動的に実行されること、
# および、「+」、「-」、「*」の演算を実行したときは一時オブジェクトの削除のためにデストラクタが呼ばれることに注意して下さい。

結論としては cp1、cp2、cp3 の指す領域を開放するには、解答例のように delete 文を三回記述しなければなりません。

また、足し算などを、以下のように「operator+」を用いて書いている人が何人かいました。

 c3 = c1.operator+(c2);


「operator+」は内部的に呼び出されるもので、実際に main 関数などに 記述する時は「c3 = c1 + c2;」として良いのです。


質問:operator+ が参照引数なのは何故か?

「+」演算子は、

  myComplex operator+(const myComplex& c){   /* (1) */
    return( myComplex(x+c.x, y+c.y) );
  }


のように、参照引数をとっています。つまり、「c1+c2 (すなわち c1.operator+(c2))」の c2 は参照として渡される、というわけです。

ここで以下のような質問がありました。
「operator+ の中では c2 は書き換えられていないのに、なぜ参照引数なのか?」
というものです。
もっともな質問だと思うので、やや細かくなりますが以下で説明してみます。

実は、参照引数でない operator+ 関数

  myComplex operator+(const myComplex c){   /* (2) */
    return( myComplex(x+c.x, y+c.y) );
  }


も期待どうりに動作します。

では、(1) と (2) の動作はどう違うのでしょうか?

まず (1) を考えます。
参照引数は内部的にはポインタ引数と同じと考えて差し支えありませんでした。 つまり、値渡しとは違って、c のコピーが渡されることはありません。
そのため、「c1 + c2」が実行されると、以下のような流れで処理が進みます。 一方、(2) の定義で「c1 + c2」が実行されると、以下のような流れで処理が進みます。 このように、参照引数版 (1) を用いた方が、加算の実行にかかるステップ数が 短く、実行効率が良いのです。
ですから質問の答えとしては「参照引数を用いたほうが、効率が良いから」となります。

さらに、引数となるクラスが巨大な場合、引数のコピーを行うのは メモリ資源の点でも、実行速度の点でも無駄であると言えます。

ですから、 「クラス (または構造体) を関数の引数にとるときは参照引数 (またはポインタ引数) を用いる」ことが多いのです。



参考にしたサイト:バケツリレーの限界




C から入る C++ に戻る