情報処理 II − 第10回

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


1.記憶クラス


1.1.自動変数

ブロック内で定義された変数は、 その変数定義のところで生成(メモリ割り当てや初期化)され、 そのブロックの終りで消去(メモリの開放)されます。 このような変数を自動変数と言います。

自動変数は、プログラムがその変数の有効範囲(スコープ) 以外の部分を実行しているときには存在しません。 例えば同じ関数を2回呼び出したとき、 1回目の呼び出しのときの変数の値は、 2回目の呼び出しのときまで保存されません。 例えば、次のプログラムでは、 sub1() の1回目の呼び出しで変数 i に 10 が代入されるものの、 2回目の呼び出しのときには i に 10 以外のものが入っています。

#include <iostream.h>

//
// 引数 f が 0 なら i に 10 を代入、非 0 なら i を表示する関数
//
void sub1(int f) {
  int i; // sub1 の中だけで有効な変数(自動変数)

  if (f == 0) {
    i = 10;
  }
  else {
    cout << "i = " << i << endl;
  }
}

void sub2(void) {
  int x = 20;
}

int main(void) {
  sub1(0);   // 一回目の呼び出しで sub1 の i に 10 を入れる。
  sub2();
  sub1(1);   // 二回目の呼び出しで sub1 の i を表示すると 10 ではない。
             // i が保存されていれば 10 が表示されるはず。
  return 0;
}

自動変数は次のようにして定義しますが、 普通 auto は省略されます。

auto int i;

1.2.静的変数

一方、ブロック内で変数された変数が ブロックの終了後も保存されるようにするには、 static を付けて変数を定義します。

static int i;

このような変数を静的変数と呼びます。 静的変数の生成(メモリ割り当てや初期化)はプログラムの実行開始時に行われ、 プログラムが終了するまで保存されます。 上のプログラムにおいて sub1() の自動変数 i を保存するには、static int i; と定義します。こうすると2回目の呼び出しのときにも i には 10 が入っています。


課題29


次のプログラム中の関数 inc() が、 呼び出す度と最初 0 を返し、 それ以降呼び出す度に返す値が1ずつ増えるようにしたいと思います。

#include <iostream.h>

int inc(void) {
  int n = 0;

  return n++;
}

int main(void) {
  int i;

  for (i = 0; i < 10; i++) {
    cout << "inc() = " << inc() < < endl;
  }

  return 0;
}

プログラム中のどこかに static を書き加え、 上記の目的を果たすようにしてください。 修正したソースプログラムをメールに添付して、 tokoi まで送ってください。Subject:(件名)は kadai29 としてください。


2.スコープ


2.1.局所変数

ブロック内で定義した変数は、 そのブロックの中でのみ利用することができます。 このような変数を局所変数と言います。 自動変数は局所変数です。 下の例では、関数 main() 内の変数 i と関数 sub() 内の変数 i は別の変数です。

#include <iostream.h>

void sub(void) {
  int i;    // 関数 sub() の中だけで有効

  for (i = 0; i < 10; i++) {
    cout.width(3);  // 出力欄の幅(桁数)を3に設定
    cout << i;
  }
  cout <<endl;
}

int main(void)
{
  int i;    // 関数 main の中だけで有効

  for (i = 0; i < 10; i++) {
    sub();
  }

  return 0;
}

for によって繰り返されている { ... } の部分も ひとつのブロックです。

#include <iostream.h>

int main(void) {                        <---------------+
  int i;                                                |
                                                        |
  for (i = 0; i < 10; i++) {            <-----------+   |
    int j;                                          |   |
                                                    |   |
    for (j = 0; j < 10; j++) {          <-------+   |   |
      cout.width(3);                            |   |   |
      cout << j;                               (1) (2) (3)
    }                                   <-------+   |   |
    cout << endl;                                   |   |
  }                                     <-----------+   |
                                                        |
  return 0;                                             |
}                                       <---------------+

上の例において (2) のブロックで定義されている変数 j は、その内側のブロック (1) でも利用できますが、 ブロック (2) の外側では利用できません。 このような変数の有効範囲のことを、 スコープ(可視範囲)といいます。


課題30


次のプログラムは y か n が入力されるまで入力を繰り返すプログラムです。 このプログラムはコンパイルするとエラーになります。 プログラムが正常に動くように修正してください。

#include <iostream.h>

int main(void)
{
  for (;;) {
    cout << "y(es) か n(o) を入れてください:";
    char yesno[5];                       // yes + '\n' + '\0' の5文字
    cin.getline(yesno, sizeof yesno);
    if (*yesno == 'y' || *yesno == 'n')  // 最初の1文字だけで判別する
      break;
  }

  cout << "入力したのは " << yesno << " です" << endl;

  return 0;
}

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


2.2.大域変数

複数の関数の間で同じ変数を共有したいときは、 その変数を関数の外側の、 その変数を共有しようとする全ての関数より前で定義します。

#include <iostream.h>

int sum; // 共有しようとする変数

void goukei(int n) {
  while (n > 0) {
    sum += n--;
  }
}

void hyouji(void) {
  cout << "合計は " << sum;
}

int main(void) {
  sum = 0;
  goukei(100);
  hyouji();

  return 0;
}

このような変数は大域変数(グローバル変数、外部変数)と呼ばれます。 大域変数は特定のブロックには関連づけられていないので、 プログラムの実行開始時に生成されます。 すなわち大域変数は静的変数です。

大域変数の定義が、 その変数を利用している部分より前にないときは、 その部分より前で 「どこかにそう言う変数があるよ」という宣言をします。

extern int x;

この宣言自体は x という変数の生成を行いませんが、 どこか他のところで定義されている x という大域変数を利用することを宣言しています。 この宣言は関数の内側・外側のいずれにも置くことができます。 関数の内側においた場合は、そのブロック内でのみ有効になります。

#include <iostream.h>

extern int x;

int addx(int x) {
  return ::x + x;    // ::x は大域変数の x
}
  
int main(void) {
  extern int y;      // この y は main() の中でのみ利用可能

  cout << "x + y = " << addx(y) << endl;

  return 0;
}

int x = 10, y = 20;  // 大域変数の定義(これが実体)

仮引数や局所変数が大域変数と同じ変数名のときは、 仮引数や局所変数が優先され大域変数は隠蔽されてしまいます。 この場合、大域変数を利用するには、 クラス名を伴わないスコープ演算子 ::グローバルスコープ演算子)を用います。


課題31


次のプログラムはコンパイルのときにエラーになります。 このプログラムに1行追加して(他の部分をいじっちゃいけません)、 正常にコンパイルできるようにしてください。

#include <iostream.h>

void goukei(int n) {
  while (n > 0) {
    sum += n--;
  }
}

void hyouji(void) {
  cout << "合計は " << sum << endl;
}

int sum;  // 共有しようとする変数

int main(void) {
  sum = 0;
  goukei(100);
  hyouji();

  return 0;
}

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


2.3.関数プロトタイプ

関数も、 それを利用している場所より前で定義されている必要があります。 もし、関数の定義が利用している場所より前にないときは、 大域変数と同様「どこかにそう言う関数があるよ」という宣言をします。 この宣言では関数名と戻り値の型、および引数の型を明示します。 これを関数プロトタイプと言います。

#include <iostream.h>

extern void swap(int &, int &);  // これが関数プロトタイプ

int main(void) {
  int x, y;

  x = 10;
  y = 20;

  cout << "x=" << x << ", y=" << y;
  cout << " のとき x と y を入れ替えると ";
  swap(x, y);
  cout << "x=" << x << ", y=" << y;
  cout << endl;

  return 0;
}

//
// 関数 swap() の定義(これが実体)
//
void swap(int &a, int &b) {
  int t;

  t = a;
  a = b;
  b = t;
}

この例では関数 swap の戻り値の型が void(値を返さない)で、 2つの引数がいずれも int 型の参照渡しであることを示しています。


3.コンパイル単位


クラスのところでも説明した通り、 ソースプログラムは複数のファイルに分けて作ることができます。

goukei.cc
extern int sum; // どこか他で定義されている

//
// 関数 goukei() の定義
//
void goukei(int n) {
  while (n > 0) {
    sum += n--;
  }
}

hyouji.cc
#include <iostream.h>

extern int sum; // どこか他で定義されている

//
// 関数 hyouji() の定義
//
void hyouji(void) {
  cout << "合計は " << sum;
}

main.cc
int sum;        // 共有しようとする変数の定義

extern void goukei(int), hyouji(void);  // 関数プロトタイプ

int main(void) {
  sum = 0;
  goukei(100);
  hyouji();

  return 0;
}

この一つ一つのファイルがコンパイル単位になります。 このようにファイルを分けた場合は、 次のようにしてコンパイルします。

% CC main.cc goukei.cc hyouji.cc -o prog[Enter]

これによって main.cc goukei.cc hyouji.cc をそれぞれコンパイルし、 作成されたオブジェクトファイルを結合して prog という実行ファイルを 作成します(-o prog を省略すれば実行ファイルのファイル名は a.out になります)。

この例では、大域変数 sum (の実体)は main.cc の中で定義されており、 goukei.cc、hyouji.cc では、 下の宣言によってそれぞれのソースプログラムの中以外に sum が用意されていることを示しています。

extern int sum;

大域変数の定義に static を加えると、この変数は定義されているコンパイル単位内の、 定義している場所より後のみで有効になり、 他のコンパイル単位からは利用できなくなります。

static int sum;

関数も変数同様 static を付けて定義することで、 他のコンパイル単位からは利用できなくなります。

static void goukei(int n) {
  while (n > 0) {
    sum += n--;
  }
}

したがって、特定のコンパイル単位内でしか使用しない大域変数や関数は、 static を付けて定義すれば他のコンパイル単位からは隠すことができます。 こうすると仮に他のコンパイル単位で同じ名前の変数や関数を定義していたとしても、 それと衝突する(コンピュータがどちらを使えばいいか分からなくなる)ことがなくなるので、 プログラムの保守性が向上します。


課題32


X 方向と Y 方向のそれぞれについて加速度と時間を設定して、 その後の位置と速度を求めるプログラムを作ります。

加速度 at 秒間加速したとき、 速度は v = at + v0、位置は s = at2 / 2 + s0 です。 これを X 方向と Y 方向のそれぞれについて計算します。 速度と位置の初期値は 0 とします。

X 方向と Y 方向について、加速度と時間を設定する関数をそれぞれ accelerateX(), accelerateY()、現在の速度を取り出す関数をそれぞれ velocityX(), velocityY()、現在の位置を取り出す関数をそれぞれ positionX(), positionY() とします。

次の3つのソースファイルを、 それぞれ movex.cc movey.cc locate.cc として作成してください。

movex.cc
double velocity = 0.0, position = 0.0;

//
// t 秒後の移動量を求める
//     v: t 秒後の速度の増加分
//
double shift(double v, double t) {
  return (v / 2.0 + velocity) * t;
}

//
// 加速度と時間の設定
//
void accelerateX(double a, double t) {
  double v = a * t;         // 速度の増加分
  position += shift(v, t);  // 移動量を現在位置に加える
  velocity += v;            // 速度の増加分を現在速度に加える
}

//
// 速度を取り出す
//
double velocityX(void) {
  return velocity;
}

//
// 位置を取り出す
//
double positionX(void) {
  return position;
}

movey.cc
double velocity = 0.0, position = 0.0;

//
// t 秒後の移動量を求める
//     v: t 秒後の速度の増加分
//
double shift(double v, double t) {
  return (v / 2.0 + velocity) * t;
}

//
// 加速度と時間の設定
//
void accelerateY(double a, double t) {
  double v = a * t;         // 速度の増加分
  position += shift(v, t);  // 移動量を現在位置に加える
  velocity += v;            // 速度の増加分を現在速度に加える
}

//
// 速度を取り出す
//
double velocityY(void) {
  return velocity;
}

//
// 位置を取り出す
//
double positionY(void) {
  return position;
}

locate.cc
#include <iostream.h>

extern void accelerateX(double, double), accelerateY(double, double);
extern double velocityX(void), velocityY(void);
extern double positionX(void), positionY(void);

int main(void) {
  double x, y, t;

  cout << "X 方向の加速度を入力してください: ";
  cin >> x;
  cout << "Y 方向の加速度を入力してください: ";
  cin >> y;
  cout << "加速時間を入力してください: ";
  cin >> t;

  accelerateX(x, t);
  accelerateY(y, t);

  cout << "現在の位置ベクトルは "
       << "(" << positionX() << ", " << positionY() << ")" << endl;
  cout << "現在の速度ベクトルは "
       << "(" << velocityX() << ", " << velocityY() << ")" << endl;

  return 0;
}

このプログラムを次のようにしてコンパイル/リンクしてください。

% CC locate.cc movex.cc movey.cc[Enter]

  • このプログラムはコンパイル時(リンク時)にエラー(警告)を表示します。 この3つのプログラムに static を書き加えて、 プログラムが正常にコンパイルできるように修正してください。 動作が正しいことも確かめてください。

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