情報処理 II − 第12回

講義に入る前に shori2 にカレントディレクトリを移しておいてください。


1.変数の生成と消去


自動変数は、 その変数定義のところが実行されたときに生成され、 そのスコープの終りで消去されます。 これに対して静的変数は、 プログラムの起動時に生成され、プログラムの修了時に消去されます。

#include <iostream.h>

double position = 0.0;            // position は静的変数(大域変数)

void accelerate(double a, double t) {
  static double velocity = 0.0;   // velocity は静的変数
  double increment = a * t;       // increment は自動変数

  position += (velocity + increment / 2.0) * t;
  velocity += increment;
}

int main(void) {
  accelerate(3.0, 4.2);
  cout << "現在位置は " << position << endl;
  return 0;
}

課題36


次のプログラムは入力した金額の合計を求めるものですが、 実際には合計を計算してくれません。 プログラム中のどこか1個所に static を書き加えて、 このプログラムがちゃんと合計を表示してくれるようにしてください。

#include <iostream.h>

int main(void) {
  for (;;) {
    int sum = 0;
    int pay;

    cout << "使った金額を入力してください(0で終了):";
    cin >> pay;
    if (pay == 0) return 0;
    cout << "あなたが今までドブに捨てたお金の合計は "
         << (sum += pay)  << " 円です" << endl;
  }
}

修正したソースプログラムをメールに添付して、 tokoi まで送ってください。Subject:(件名)は kadai36 としてください。


2.コンストラクタ関数とデストラクタ関数


クラス変数の生成時と消去時に実行する関数を定義できます。 生成時に実行する関数をコンストラクタ関数、 消去時に実行する関数をデストラクタ関数と言います。

object.h
class object {
  char name[80];
public:
  object(void);            // コンストラクタ関数(デフォルトコンストラクタ)
  object(char *);          // コンストラクタ関数
  object(const object &);  // コンストラクタ関数(コピーコンストラクタ)
  ~object(void);           // デストラクタ関数
  const object &operator=(const object &);  // 代入演算子の定義
private:
  void printConstruct(void);
  void printDestruct(void);
};

コンストラクタ関数はクラス名と同じ名前を持つメンバ関数です。 デストラクタはクラス名の最初に ~(チルダ)を付けた名前を持つメンバ関数です。 これらの関数は戻り値を持ちません(戻り値のデータ型を void と宣言することもありません)。 また、コンストラクタ関数は引数の数や型で 多重定義できます。 デストラクタ関数は引数を使用しません(こっちは引数を void とします)。

object.cc
#include <stdio.h>
#include <strings.h>
#include "object.h"

//
// 変数に初期値を与えないときに実行するコンストラクタ関数(デフォルトコンストラクタ)
//
object::object(void) {
  strcpy(name, "デフォルトコンストラクタを使った変数");
  printConstruct();
}

//
// 引数で初期化を行うときに実行するコンストラクタ関数
//
object::object(char *string) {
  strcpy(name, string);
  printConstruct();
}

//
// 他のクラス変数をそのままコピーするときに実行するコンストラクタ関数(コピーコンストラクタ)
//
object::object(const object &data) {
  strcpy(name, data.name);
  strcat(name, "のコピー");
  printConstruct();
}

//
// デストラクタ関数
//
object::~object(void) {
  printDestruct();
}

//
// 代入演算子の定義
//
const object &object::operator=(const object &data) {
  if (&data != this) {
    // 自分自身への代入でないとき
    char line[200];
    strcpy(line, name);
    strcat(line, "に");
    strcat(line, data.name);
    strcat(line, "を代入");
    puts(line);

    // 代入処理の実体
    strcpy(name, data.name);
  }
  return *this;  // 続けて代入できるように
}

//
// “…を生成”を表示する
//
void object::printConstruct(void) {
  char line[40];
  strcpy(line, name);
  strcat(line, "を生成");
  puts(line);
}

//
// “…を消去”を表示する
//
void object::printDestruct(void) {
  char line[40];
  strcpy(line, name);
  strcat(line, "を消去");
  puts(line);
}

引数を用いないコンストラクタ関数は デフォルトコンストラクタ関数と呼ばれ、 初期値を指定せずに変数を定義する場合に実行されます。 そのほかのコンストラクタ関数は初期値のデータ型で多重定義できます。 特に引数がそのクラス変数の参照になっているコンストラクタ関数は コピーコンストラクタ関数と呼ばれ、 すでに生成されている他の変数を = を使ってそのままコピーして初期化する際に実行されます。

なお、コピーコンストラクタを定義したときは、 通常の代入に用いる = 演算子も定義し直す必要があります。 operator<演算子>()というメンバ関数を定義することにより、 そのクラスの<演算子>の動作を記述できます。 すなわち、

x <演算子> y
という式は、
x.operator<演算子>(y)
というメンバ関数の呼び出しに他なりません。

ところで、この例ではメッセージを表示するのに cout を使用せず、 代わりに puts() という関数を使用しています (これを使用するために stdio.h を include しています)。 実は、大域変数(下の objectmain.cc の global1、global2)のコンストラクタ中では、 cin や cout は使用できないのです。

大域変数のコンストラクタ関数は、main() を実行するに実行されます (ブロック内の静的変数は定義のある場所で一度だけ実行されます)。 ところが、このような大域変数が複数定義されているときに、 それらのコンストラクタが実行される順序は決まっていません。 このため、 大域変数のコンストラクタ内でコンストラクタを持つ他の大域変数を使用すると、 後者のコンストラクタがまだ実行されておらず、 プログラムが期待通りに動作しない場合があります。 cin、cout も大域変数のクラス変数なので、 global1、global2 のコンストラクタが実行される前に cout のコンストラクタが実行されているとは限らないのです。

デストラクタについても同様で、 大域変数および静的変数のデストラクタは main() が終了したに実行されますが、 その実行順序は決まっていません。 このため、デストラクタ内で他のクラス変数を使用していると、 そのデストラクタが先に実行されているために、 やはりプログラムが期待通りに動作しない可能性があります。

なお、puts() など stdio.h で宣言されている関数と、 iostream.h で宣言されている cin、cout は、そのままでは一つのプログラムで混在して使用することはできません。 混在して使用する必要があるときは main() の最初で ios::sync_with_stdio(); を実行しておきます。

objectmain.cc
#include <stdio.h>
#include "object.h"

int main(void) {
  puts("--- main() の開始 ---");

  object naibu1("main() の中の自動変数");       // 自動変数
  object naibu2 = naibu1;                       // 自動変数〜コピーコンストラクタ

  for (int i = 0; i < 5; i++) {
    object loop1("for の中の自動変数");         // 自動変数
    static object loop2("for の中の静的変数");  // 静的変数
  }

  puts("--- main() の終了 ---");
  return 0;
}

object global1("大域変数");                     // 大域変数
object global2;                                 // 大域変数〜デフォルトコンストラクタ

このプログラム (objectmain.cc, object.cc) をコンパイルして実行すると、 以下のような出力を得ます。

% a.out[Enter]
大域変数を生成
デフォルトコンストラクタを使った変数を生成
--- main() の開始 ---
main() の中の自動変数を生成
main() の中の自動変数のコピーを生成
for の中の自動変数を生成
for の中の静的変数を生成     (← loop2 のコンストラクタの実行は1回だけ)
for の中の自動変数を消去
for の中の自動変数を生成
for の中の自動変数を消去
for の中の自動変数を生成
for の中の自動変数を消去
for の中の自動変数を生成
for の中の自動変数を消去
for の中の自動変数を生成
for の中の自動変数を消去
--- main() の終了 ---
main() の中の自動変数のコピーを消去
main() の中の自動変数を消去
for の中の静的変数を消去
デフォルトコンストラクタを使った変数を消去
大域変数を消去

大域変数 global1、global2 は main() よりも後ろで定義されているにも関わらず、 main() より先にコンストラクタが実行されています。 また for で繰り返される部分で宣言されている自動変数 loop1 は繰り返すたびに生成と消去が繰り返されていますが、 静的変数 loop2 は変数定義の部分を最初に実行したときに一度だけ実行されています。


課題37


objectmain.cc において、たとえば以下のように、 コンストラクタをもつクラス変数の配列を定義するとどうなるでしょうか。 またその一部の要素に初期値を与えるとどうなるでしょうか。

object global3[5];                              // とか
object global4[5] = { "obj1", "obj2", "obj3" };  // などと

プログラムの実行結果がなせそうなるのか簡単に考察して (プログラムの出力をそのまま書いて「こうなりました」ではだめ) tokoi まで送ってください。Subject:(件名)は kadai37 としてください。


3.static メンバ


3.1.static メンバ変数

クラスのメンバ変数に static を付けると、 そのメンバ変数は生成されたすべてのクラス変数で共有されます。 この場合はメンバ関数と同様に、 この変数の実体を別のところで大域変数として定義しておく必要があります。

#include <iostream.h>

class staticmember {
  static int shared;
public:
  void setSharedVar(int);
  void printSharedVar(void);
};

int staticmember::shared;  // 実体を大域変数として定義

void staticmember::setSharedVar(int value) {
  shared = value;
}

void staticmember::printSharedVar(void) {
  cout << "共有されている変数の値は " << shared << endl;
}

int main(void) {
  staticmember x, y;

  x.setSharedVar(10);
  y.printSharedVar();
}

また static なメンバ変数を public にすることで、 クラス変数を生成していなくてもこのメンバ変数にアクセスできるようになります。 この場合はスコープ演算子 (::) を使用します。

#include <iostream.h>

class staticmember {
public:
  static int shared;
  void setSharedVar(int);
  void printSharedVar(void);
};

int staticmember::shared;  // 実体を大域変数として定義

void staticmember::setSharedVar(int value) {
  shared = value;
}

void staticmember::printSharedVar(void) {
  cout << "共有されている変数の値は " << shared << endl;
}

int main(void) {
  staticmember::shared = 5;  // スコープ演算子で shared にアクセス

  staticmember y;            // クラス変数を生成

  y.printSharedVar();
}

3.2.static メンバ関数

public メンバ関数に static を付けて宣言すると、 その関数をクラス変数を介さずに直接呼び出すことができるようになります。

#include <iostream.h>

class staticmember {
public:
  static int shared;
  static void setSharedVar(int);
  static void printSharedVar(void);
};

int staticmember::shared;  // 実体を大域変数として定義

void staticmember::setSharedVar(int value) {
  shared = value;
}

void staticmember::printSharedVar(void) {
  cout << "共有されている変数の値は " << shared << endl;
}

int main(void) {
  staticmember::setSharedVar(5);   // static なパブリックメンバ
  staticmember::printSharedVar();  // static なパブリックメンバ
}

上の例では staticmember クラスのクラス変数を一つも生成していない状態で、 setSharedVar() や printSharedVar() を呼び出しています。