『Cプログラミング診断室』目次次(第2章 これでもプロ まとめ)

第2章 これでもプロ

その他


以上の他にも、このプログラム独特の問題点など、結構面白い「下手」な個所があります。その うちのいくつかで、読者の役に立ちそうなものからいくつかをピックアップしていきましょう。

■printfの便利な機能■

325行から

    for (i = 0; i < s; i++)
        printf(" ");
となっています。これは、スペースを、変数 s の値だけ並べます。一見これで良さそうですが、 どうしてこれだけのことがprintfだけでできないのでしょうか。実は、ちゃんとしたCコンパイラ では同じことが、
 
    printf( "%*s", s, "" );
で済みます。これは、文字列""を文字幅 s の右寄せでstdoutに出力します。ふつう幅は数値で指 定しますが、*で指定すると、幅の指定に変数が使えるようになります。知っていましたか。浮動 小数点の場合の精度にも*は使えます。ただし、これはあくまでもちゃんとしたCコンパイラの場 合です。

今回の例は極端ですが、*を使うと便利なことも多いです。でも、この機能は、どのCでも確実 にサポートしているとは限らないので、確認してから使ってください。

■いい加減な関数■

363行から関数realchkがあります。この関数はどうも実数か否かの判定をしているようです。こ のソースだけから動作を判断できますか。動作チェックのためのプログラムに仕立てあげました (リスト2−2)。

リスト2−2 実数検査

     1  #include        <stdio.h>
     2  
     3  static  realchk();
     4  
     5  main()
     6  {
     7          char    buf[100];
     8          int     status;
     9  
    10          for(;;) {
    11                  gets( buf );
    12                  status = realchk( buf );
    13                  printf( "status = %d\n", status );
    14          }
    15  }
    16  
    17  static  realchk(s)
    18    char *s;
    19  {
    20          int i,periodsw;
    21  
    22          periodsw = 0;
    23          if (s[0] == '+' || s[0] == '-')
    24                  i = 1;
    25          else
    26                  i = 0;
    27          while(s[i] != '\0') {
    28                  if (!((s[i] == '.' && periodsw == 0)
    29                      || s[i] == '/' || (s[i] >= '0' && s[i] <= '9')))
    30                          return(1);
    31                  if (s[i] == '.')
    32                          periodsw = 1;
    33                  ++i;
    34          }
    35          return(0);
    36  }

図2−1 実行結果

    12345
    status = 0
    .
    status = 0
    10*5
    status = 1
    10/5
    status = 0
    10/5/30
    status = 0
    10//30
    status = 0
    10/./4
    status = 0
    //30
    status = 0
    1.5
    status = 0
    1.5.5
    status = 1
    1.5/+7
    status = 1
    -1.5/7
    status = 0
    -1.5/7.2
    status = 1
    10.5/4.5*-25
    status = 1

符号つきの小数点数で、除算も可能というふうに読み取れます。除算が可能なのに、乗算ができ ないみたいで、不思議な設計です。

どういい加減かもテストしてみました(図2−1)。

この関数の問題点を列挙してみましょう。

  1. 仕様のいい加減さ
  2. 動作のいい加減さ(バグでしょう)
  3. ポインタを使いこなしていない
  4. periodswという奇妙な変数
  5. isdigitを知らない
元のプログラムは、結構ひねくれた書き方になっています。ここで、もっとすっきりしたものに してしまいましょう。

realchkの仕様

	static int realchk( s )
	  char     *s;
文字列 s が、拡張された仕様の実数であるか検査をする。検査結果を関数の戻り値 で返す。正常なとき0、エラーのとき1を返す。拡張された実数とは、符号つき整数、 符号つき小数点数、およびそれらが*または/でつながったもの。浮動小数点数は、 小数点があり、整数部または小数部の少なくともいずれかが存在するもの。つまり、 符号付き小数点の乗除算可能、加減算は不可という状態です。
以上のように、より自然な仕様にしてしまうと、プログラムもより自然な流れで書けてしまいま す(リスト2−3)。

リスト2−3 実数検査
     1  #include        <stdio.h>
     2  #include        <ctype.h>
     3  
     4  static  realchk();
     5  
     6  main()
     7  {
     8          char    buf[100];
     9          int     status;
    10  
    11          for(;;) {
    12                  gets( buf );
    13                  status = realchk( buf );
    14                  printf( "status = %d\n", status );
    15          }
    16  }
    17  
    18  static  realchk(s)
    19    char *s;
    20  {
    21          int     seisuu_bu, syousuu_bu;
    22  
    23          for( ; *s ; ++s ) {
    24                  seisuu_bu = syousuu_bu = 0;
    25                  if( *s=='+' || *s=='-' )
    26                          ++s;
    27                  for( ; isdigit(*s) ; ++s )
    28                          seisuu_bu = 1;
    29                  if( *s == '.' ) {
    30                          ++s;
    31                          for( ; isdigit(*s) ; ++s )
    32                                  syousuu_bu = 1;
    33                  }
    34                  if( seisuu_bu==0 && syousuu_bu==0 )
    35                          return  1;
    36  
    37                  if( ! ( *s == '/' || *s == '*' ) )
    38                          break;
    39          }
    40  
    41          return  *s ? 1 : 0;
    42  }

図2−2 実行結果

    12345
    status = 0
    .
    status = 1
    10*5
    status = 0
    10/5
    status = 0
    10/5/30
    status = 0
    10//30
    status = 1
    10/./4
    status = 1
    //30
    status = 1
    1.5
    status = 0
    1.5.5
    status = 1
    1.5/+7
    status = 0
    -1.5/7
    status = 0
    -1.5/7.2
    status = 0
    10.5/4.5*-25
    status = 0

実行結果(図2−2)も、前のような訳の分からぬものにはなりませんね。

もっと完全な判定を必要とするときには、コンパイラの中身の解説書をご覧ください。括弧つき の場合とか、演算子の優先順位も考慮した「式」の判定の方法とかが詳しく解説されているでしょ う。中級上位から上級を目指す人の必読書です。そのうち、コンパイラの中身を知って使うように しましょう。簡単なコンパイラくらいは作ってみるのもいいですね。

■論外■

355行からの関数 wait_5 は、forループで、時間待ちのためにループを50,000回しているので、 特に問題ないとお思いでしょう。でも、これはとんでもない問題を抱えた個所なのです。同じタイ プのコンピュータでも、機種により実行時間は相当異なることがあります。この関数でのウエイト 時間は、コンピュータの実行速度に完全に依存してしまいます。ある機種でちょうど良いように設 定しても、新機種が出て実行速度が早くなると、全然使いものにならないソフトになったりします。

このように、forループなど、純粋にソフト的にウエイトするものを「ソフトウェアタイマ」と 呼びます。そして、絶対してはいけないことの一つです。

普通は、次のいずれかを使います。

    int sleep( seconds )
    unsigned seconds;

    usleep( useconds )
    unsigned useconds;
sleepが秒単位で、usleepがマイクロ秒単位の指定です。ただし、これらも、 コンパイラによっては多少異なるようです。


Copyright1996 Hirofumi Fujiwara. No reproduction or republication without written permission
『Cプログラミング診断室』目次次(第2章 これでもプロ まとめ)