今週のプログラムその9

ここまで、プログラムの実行順序を制御する構文として、「指定回数だけ繰り返す」ための for{;;}(C)または do-enddo(Fortran)および「条件判断」のための if{} else if{} else{}(C)または if-else if-else-endif(Fortran)を扱った。 これだけを知っていればかなり多くの場合を扱えるのだけど、場合によってはかなり煩雑なプログラムを書くはめになるかもしれない。 いくらサバイバル・プログラミングとは言ってももうひとつだけ実行制御を知っておいたほうが便利だろう。それは

「条件が満たされている間繰り返し」
これを今まで取り上げなかったのには理由がある。いわゆる
「永久ループ」が容易に作れてしまう
のだ。  for や do なら、なにがあろうと指定回数だけ繰り返せば終わってくれるので「永久ループ」にはならないのだが、今回扱うループ構造は「条件」を間違えるとたちまち永久ループになるので、要注意。

解説

  1. Cでは while という命令を使う。

    1. たとえば
      
         int i;
         i = 0;
         while(i < 10){
          ++i;
          printf("%d\n",i);
         }
      
      は、条件 i<10 が満たされている間、iを1ずつ増やし、その値を出力する。 もっとも、これだけなら
      
         int i;
          for(i=1; i <= 10; ++i){
          printf("%d\n",i);
         }
      
      と同じなのだが。ただし、while はまず最初に条件が満たされているかどうかを調べ、満たされていれば{}内を実行する、という順序で実行されるので不等号の範囲に注意する。

    2. 上の例はループの実行回数が自明だったので for でも書くことができた(というより、普通は for で書く)。while は、ループが何回実行されるかわからない場合に威力を発揮する。 たとえば

      
         int i,x;
         i = 0;
         x = 100;
         while((x % 2) == 0){
          ++i;
          x /= 2;
         }
          printf("%d\n",i);
      
      は、整数xを2で何回割ることができるかを計算する(%は整数同士の割り算をした余りを求める演算子。2で割った余りが0なら偶数)。

    3. while を使うと、ファイルからデータを次々と読み込ませるプログラムでデータ数をあらかじめ指定しないことが可能になる(もっとも、データ数を知らずに計算するのはむしろ問題なので、あえてデータ数指定のプログラムを書くほうがいいかもしれない)。

      
         int i,x;
         while(scanf("%d",&x) != EOF){
           printf("%d\n",x);
         }
      
      このプログラムは整数データを次々に読み込み、そのまま出力する。 データの読み込み元は標準入力だが、例によって 入力データをファイルにいれ、リダイレクションで読ませればよい。

      EOFはファイルの終わりを表わす特殊記号である(End of file)。 今まで秘密にしていたが、scanf は整数型の関数で、 それ自体がある整数値を返す。実はそれはちゃんと読めたデータの個数 (上の例では一度に一個しかデータを読まないので、1)なのだが、 ファイルの終わりを検知すると個数ではなくEOFを返す。

      つまり、while の中の条件 scanf("%d",&x) != EOF は 「標準入力から整数変数xに値をひとつ読み、それがファイルの終わりでなければ」 というもの。 なお、端末から直接データを入力する場合は、EOF記号はCtrl-D(Ctrlキーを 押しながらDを押す)によって発生させられるので、最後のデータを入力したら次に Ctrl-Dを押せばよい(ただし、これはUNIX系OSの場合)。

    1. Fortranでも do-enddo と if-exit という構文を組み合わせて、while に相当する構文を書くことができる。 ただし、これはFortran90と呼ばれる言語の仕様であり、 Fortran77と呼ばれる古い仕様には含まれていない。 一方、LINUXなどでよく使われるFortranコンパイラであるg77はその名のとおりFortran77 に基づいているのだが、一部Fortran90の仕様がとりいれられている。 実はこれまでに使ってきた do-enddo もそうなのだった。if-exit もg77で使えることを確認してある。

      
            integer i
            i = 0
            do
              if(i .ge. 10) exit
              i = i+1
              write(*,*)i
            enddo
      
      このプログラムはiが10より小さいあいだだけ、iを1ずつ増やしながらループを回す。 Cでの while 構文とは逆に「条件が満たされなければループを抜ける」という書き方になっていることに注意。推測できると思うが、if-exit は do-enddo ループの中のどこにでも置くことができるので、それによって条件判断をいつ行なうかを好きに決められる。たとえば、
       
            integer i
            i = 0
            do
              i = i+1
              write(*,*)i
              if(i .ge. 10) exit
            enddo
      
      とすれば、足し算と出力を実行してから条件判断が行なわれる。 2行目を i=10 としてみれば、ふたつの違いが明らかになるだろう。

    2. ループが何回実行されるかわからない場合の例として、 整数xを2で何回割ることができるかを計算するプログラムのFortran版を挙げる。

      
            integer i,x
            i = 0
            x = 100
            do
              if(mod(x,2) .eq. 0) exit
              x = x/2
              i = i+1
            enddo
            write(*,*)i
      
      if-exit の置かれている位置に注意すること。 また、mod(x,2) はxを2で割った余りを与える関数である。 なお、x/2 は整数同士の割り算なのでxが偶数でなければ余りは切り捨てられてしまうことに注意。

    3. Fortranでも読み込むデータ数をあらかじめ指定することなく、 ファイルからデータを次々と読み込ませるプログラムを書くことはできる。

      
         integer x, io
         do
           read(*,*,IOSTAT=io) x
           if(io .lt. 0)exit
           write(*,*)x
         enddo
      
      このプログラムは整数データを次々に読み込み、そのまま出力する。 データの読み込み元は標準入力だが、例によって 入力データをファイルにいれ、リダイレクションで読ませればよい。

      io(名前はなんでもいいが)を整数変数と宣言し、 read 文のかっこ内に書かれた  IOSTAT=io という部分が、データの読み込み状況を 変数ioに格納するという指定である。 IOSTATはデータが正常に読まれれば0、ファイルの終わりを検知すると負の値を 出力するので、それが変数ioに格納される。if 文でioの値が負か どうかを調べ、負ならループを抜ければよい。 なお、端末から直接データを入力する場合は、EOF(ファイルの終わり)記号はCtrl-D(Ctrlキーを 押しながらDを押す)によって発生させられるので、最後のデータを入力したら次に Ctrl-Dを押せばよい(ただし、これはUNIX系OSの場合)。



まずはC版

program 20


/* 指数関数の値をテーラー展開によって求める。
 関数の引数は端末から入力する。
 収束判定をwhileによって行なう。
 収束精度はPRECという値で指定する。
 ただし、このプログラムは小さな値を足した場合の「積み残し誤差」
  について、まったく考慮していないので、このままでどんな級数に
  も応用できる限らない
*/
#include<stdio.h>
#include<math.h>            検算のための指数関数と絶対値関数を使うので必要
#define PREC 0.0000001        収束の精度をPRECという名前で定義しておく
main()
{
	double x_old,x_new,z,f;
	int i;
	scanf("%lf",&z);              値を端末から入力
	x_old = 0;          級数の初期値 (0項目)
	x_new = 1;          級数の1項目は1 (次数は0)
	i = 0;                              次数の初期値(0)
	f = 1;                              fには階乗を計算した値がはいる(整数型にしないのは、値が巨大になっても対応できるように)
	while( fabs((x_old-x_new)/x_old) > PREC){    x_oldとx_newの差の相対値がPRECになるまでループを回す
          x_old = x_new;            現在の値をx_oldにいれる
	  ++i;                      次数をひとつ上げる
          f *= i;                   次の階乗を計算(fが階乗になることを理解しよう)
	  x_new += pow(z,i)/f;        現在の次数の項を足す(powはzのi乗を与える)
	  printf("%d  %15.12f  %15.12f\n",i,x_new,exp(z));   次数と現在の値とexpの本当の値を出力
	}                           ループはここまで
}                                   プログラム終了

program 21


/*
  個数不明のデータを読んで、データの個数とそれらの平均
 を計算するプログラム (program 4の書き換え)
 データを端末から入力する場合は最後のデータの次にCtrl-D
*/
#include<stdio.h>
main()
{
  double y,sum;
  int n;

  n = 0;
  sum = 0;
  while(scanf("%lf",&y) != EOF){  データは倍精度実数とする
    ++n
    sum += y;
  } 

  printf("number of data= %d\n",n);  データ数を出力
  printf("average= %f\n",sum/n);        平均値を出力
}

program 22


/*
 ふたつの整数の最小公倍数を求めるユークリッド互除法
  整数をn>mとすれば、nをmで割る。余りをqとする。
  q = 0 なら、mが答
  そうでなければ、n=m, m=qとして余りが出なくなるまで繰り返す
  余りが0になったとき、最後に割った数が答
 なぜ、それでいいのか考えてみよう
*/
#include<stdio.h>
#include
main()
{
  int x1,x2;
  int n,m,q;

  scanf("%d",&x1);                      整数データをふたつ読む
  scanf("%d",&x2);

  if(x1 >= x2) {n = x1; m = x2;}    大きいほうをn、小さいほうをmとする
  else {n = x2; m = x1;}

  while( (q = n % m) > 0) {     nをmで割った余りが正の間繰り返し(条件判定の前に計算をしている。こういう書き方も可能)
    n = m;      古いmを新しいnに
    m = q;      余りqを新しいmに
  }

  printf("%d\n",m);     q=0となったときのmが答
}

FORTRAN版

program 20


c 指数関数の値をテーラー展開によって求める。
c 関数の引数は端末から入力する。
c 収束判定をwhileによって行なう。
c 収束精度はPRECという値で指定する。
c ただし、このプログラムは小さな値を足した場合の「積み残し誤差」
c  について、まったく考慮していないので、このままでどんな級数に
c  も応用できる限らない
      program main
      parameter ( prec = 0.0000001)   収束条件をprecという名前の定数にする
      real*8 x_old, x_new, z, f
      integer i,j,k
      read(*,*)z                         値を端末から入力
	x_old = 0                        級数の初期値 (0項目)
	x_new = 1                        級数の1項目は1 (次数は0)
	i = 0                            次数の初期値(0)
	f = 1             fには階乗を計算した値がはいる(整数型にしないのは、値が巨大になっても対応できるように)
	do               ループはここから
          if( abs((x_old-x_new)/x_old) .le. prec)exit    x_oldとx_newの差の相対値がprec以下ならループを抜ける
          x_old = x_new        現在の値をx_oldにいれる
	  i = i + 1          次数をひとつ上げる
          f = f * i      次の階乗を計算(fが階乗になることを理解しよう)
	  x_new = x_new + z**i/f   現在の次数の項を足す
	  write(*,*)i,x_new,exp(z)  次数と現在の値とexpの本当の値を出力
        enddo             ループはここまで
       end               プログラム終了

program 21


C  個数不明のデータを読んで、データの個数とそれらの平均
C を計算するプログラム (program 4の書き換え)
C データを端末から入力する場合は最後のデータの次にCtrl-D

      program main

      real*8 y,sum               読むデータは倍精度とする
      integer i,io

      n = 0
      sum = 0

      do
        read(*,*,IOSTAT=io)y
        if (io .lt. 0) exit     終了判定
          n = n+1
          sum = sum+y
      enddo

      write(*,*)"number of data= ",n  データ数を出力
      write(*,*)"average= ",sum/n    平均を出力
      end

program 22


C ふたつの整数の最小公倍数を求めるユークリッド互除法
C  整数をn>mとすれば、nをmで割る。余りをqとする。
C  q = 0 なら、mが答
C  そうでなければ、n=m, m=qとして余りが出なくなるまで繰り返す
C  余りが0になったとき、最後に割った数が答
C なぜ、それでいいのか考えてみよう

      program main
      integer x1,x2
      integer n,m,q

      read(*,*)x1      整数データをふたつ読む
      read(*,*)x2

      if (x1 .ge. x2) then    大きいほうをn、小さいほうをmとする
         n = x1
         m = x2
      else
         n = x2
         m = x1
      endif

      do            ここからループ
        q = mod(n,m)      qはnをmで割った余り
        if (q .eq. 0) exit      qが0ならループを抜ける
         n = m         古いmを新しいnに
         m = q         余りqを新しいmに
      enddo

      write(*,*)m       q=0となったときのmが答
      end