Code Reading/2章/2.10~2.19

Last-modified: 2010-05-04 (火) 07:44:33

2.10

問題

expandのソースコード中の余計な中括弧を発見しろ。
また、中括弧を使用していない制御構造でどのステートメントが実行されるかをマークしろ。

解答

余計な、という意味がよくわからないがとりあえず
中括弧内のステートメントが1行しかないものという意味でとらえた。
唯一以下の箇所が該当する。
netbsdsrc/usr.bin/expand/expand.c

	while ((c = getopt (argc, argv, "t:")) != -1) {
		switch (c) {
		case 't':
			getstops(optarg);
			break;
		case '?':
		default:
			usage();
			/* NOTREACHED */
		}
	}

このwhile文の中身はswitch文のみなので、中括弧を外すことはできる。
でも多分普通は外さない。
次に、中括弧を使用していない箇所について列挙する。
l120~122

				for (n = 0; n < nstops; n++)
				  if (tabstops[n] > column)
				    break;

l134~135

			  if (column)
			    column--;

l163~164

		while (*cp >= '0' && *cp <= '9')
		  i = i * 10 + *cp++ - '0';

l170~171

		if (nstops > 0 && i <= tabstops[nstops-1]/
		  goto bad;

l173~176

		if (*cp == 0)
		  break;
		if (*cp != ',' && *cp != ' ')
		  goto bad;

個人的な感覚では、やはり1行の制御構造でも中括弧があった方がいいなあと思う。

2.11

問題
expandのインデントが制御の流れと一致していることを確認せよ。
適当なプログラムを選択して同様の確認を行え。
解答
やりました、としか書けない。省略。
選んだプログラムは以下の通り。
coreutils-6.9-9.fc8/coreutils-6.9/src/echo.c

2.12

問題:Perlで全ての制御構造に中括弧を使うことが可読性に与える影響について。
解答:Perlの話なので今はパス。

2.13

問題:ソースコードコレクション内のswitchステートメントは
他のステートメントと書式が異なるが、どこが異なるのかを理由とともに説明せよ。
解答:まず違いがくわからない…。
とりあえず以下のコードを調査してみた。
netbsdsrc/bin/ls/ls.c

2.14

問題:
(1)これまで読んだプログラムでswitchステートメントにおける
予想外の値の処理がどうなっているかを検討せよ。
(2)エラーを検出するためにプログラムに加える変更を提案せよ。
(3)この変更がプログラム強度にどう影響するかを議論せよ。
解答:
(1)
netbsdsrc/bin/ls/ls.c
より、main()関数。

		default:
		case '?':
			usage();
		}

このケースでは、-?オプションを指定する以外に「予想外の値」が入力されたときも
usageすなわち使い方を表示するようにしている。
もう一つ見てみよう。
coreutils-6.9-9.fc8/coreutils-6.9/src/echo.c
より、hextobin()関数。

static int
hextobin (unsigned char c)
{
 switch (c)
   {
   default: return c - '0';
   case 'a': case 'A': return 10;
   case 'b': case 'B': return 11;
   case 'c': case 'C': return 12;
   case 'd': case 'D': return 13;
   case 'e': case 'E': return 14;
   case 'f': case 'F': return 15;
   }
}

defaultが前に来ている、break文がない、など色々と珍しい。
まあ性質的には問題ないんだろうけど、なんでdefaultを前に持ってくるのだろう?
(2)
lsの方はともかく、echoの方はたやすいな。
関数の性質から入力は0~9, a~f, A~Fしか入らないだろう。
だから0~9もきちんとcase文で振り分けて、残りをdefaultで抱えてエラー処理すればいいだろう。
私なりに修正するとこんな感じ。

static int
hextobin (unsigned char c)
{
 switch (c)
   {
   case '0': case '1';
   case '2'; case '3':
   case '4': case '5';
   case '6'; case '7':
   case '8': case '9':
           return c - '0';
   case 'a': case 'A': return 10;
   case 'b': case 'B': return 11;
   case 'c': case 'C': return 12;
   case 'd': case 'D': return 13;
   case 'e': case 'E': return 14;
   case 'f': case 'F': return 15;
   default:
           error();
   }
}

(3)しかしこの変更がプログラム強度に影響を与えるのかどうかは不明…。
というかエラー処理をしたところで論理的強度から上がりも下がりもしないと思うのだが。

2.15

問題:switchステートメント内にbreak文がないことを検出する方法が
現在の環境にあるかどうかをサンプルプログラムを作成して検討しろ。
解答:作成したコードは以下の通り。
コード/2.15-1
gcc -Wallでは残念ながら検出できなかった。
でも多分何らかのオプションがあると思うのだが…。
現在のところはわからない。

break文がないcase:も保証されているのでエラーじゃないし、
検出できるのか?

2.16

問題:本書に掲載されているソースコードを調べ、forステートメントの10種類の用法を列挙せよ。
解答:いきなり難しくないか?
とりあえずp.36~p.39の部分から列挙してみると、

  • ある回数繰り返して処理を実行する
  • ライブラリ関数から返される結果の繰り返し処理
  • 無限ループ
    ……3つしかないな。残り7つはおいおい見つけるとしよう。
    4つ目
    netbsdsrc/usr.bin/compress/zopen.c
    より、zwrite()関数
    middle:	for (i = 0; count--;) {
    初め条件式ないじゃんとか思ってしまった。
    こんな式の書き方もあるのか……。
    まあ多分自分では書かないけど。

トリッキーな使い方はあるけど、本質的には下の4パターンしか無い気がする。
左の初期化のスペースと右側のループ枚に呼び出される箇所はコンマで区切ると複数の式をかける。

  • for(i=0; i<max; i++)
  • for(;;)
  • for(; i<max; i++)
  • for(i=0; ; i++)
  • for(i=0; i<max;)

おもしろいねこれ。ソース読んでないから何を意図してるのかわからんけど

  • for(;;); /* roop */

2.17

問題:(1)本書2.5節の例を、forの代わりにwhileを使って表せ。
(2)forとwhile、どちらの表現が読みやすいか検討せよ。
解答:
(1)
(a)

for (i = 0; i < len; i++) {
...
}

i = 0;
while (i < len) {
...
i++;
}

(b)

for (i = 0; i <=  extrknt; i++) {
...
}

i = 0;
while (i <= extrknt) {
...
i++;
}

(c)

for (i = 1; i < month; i++) {
...
}

i = 1;
while (i < month) {
...
i++;
}

(d)

for (i = 1; i <= nargs; i++) {
...
}

i = 1;
while (i <= nargs) {
...
i++;
}

(e)

for (code = 255; code  >= 0; code--) {
...
}

code = 255;
while (code >= 0) {
...
code--;
}

(f)

for (dp = readdir(dd); dp != NULL; dp = readdir(dd)) {
...
}

while ((dp = readdir(dd)) != NULL) {
...
}

(g)

for (cnt = 1, t = p; cnt <= cnt-orig; ++t, ++cnt) {
...
}

cnt =  1;
t = p;
while (cnt <= cnt-orig) {
...
++t;
++cnt;
}

(h)

for(;;){}

while(1){}

(2)ほとんどどれもfor文の方が読みやすい気がする。
1行で全て書かれてるから、ループ条件、終了条件がわかりやすい。
次の問題とも関連するけど、繰り返しの条件式に関わる変数がループ内で
あれこれと変化する場合はwhileの方がいい気がする。
こういった場合は逆にforだとわかりにくいと思うのだがどうだろうか?
(追記)(f)だけはwhileの方が読みやすいな。
開始式、条件式、繰り返し式が一まとめにできるのだったらwhileの方が見やすいということか。

2.18

問題:
(1)forループよりwhileループを選択するためのガイドラインを考案せよ。
(2)ガイドラインを適当なソースに適用して検証せよ。
解答:
(1)前問でも少し言及しているが、以下のように考案してみた。
(a)条件式で比較する変数に関数の返り値を次々と代入していく場合
(b)forループだと横に長すぎる場合
(c)forループだと縦に短すぎる場合
(d)条件式で使用する変数がループ内で次々と変わる場合
(2)以下のソースコードファイルをとりあげた。
/media/cdrom/netbsdsrc/usr.bin/compress/zopen.c
l286~287

	for (fcode = (long)hsize; fcode < 65536L; fcode *= 2L)
		hshift++;

l293
middle: for (i = 0; count--;) {
l510~513

	for (code = 255; code >= 0; code--) {
		tab_prefixof(code) = 0;
		tab_suffixof(code) = (char_type) code;
	}

l528~529

			for (code = 255; code >= 0; code--)
				tab_prefixof(code) = 0;

l696~697

	for (i += 16; i > 0; i--)
		*--htab_p = m1;

forステートメント中の全ての繰り返しの式が単なる四則演算に止まっている。
それに対しwhileループは、
l525

	while ((code = getcode(zs)) > -1) {

これは条件(a)に該当する。
l544~547

		while (code >= 256) {
			*stackp++ = tab_suffixof(code);
			code = tab_prefixof(code);
		}

これは条件(b)に該当しそうだが…。
for文に直すと以下のような感じになる。

for (; code >= 256; code = tab_prefixof(code)) {
*stackp++= tab_suffixof(code);
}

繰り返し文のところに関数入っているのは変な感じもするが…。

2.19

問題:(1)本書掲載のソースコードからbreakとcontinueを10個探せ。
(2)それぞれ対応するステートメントの実行後に制御が移る位置を示せ。
(3)そのステートメントが使われる理由を説明せよ。
※コードのロジックについて深く説明する必要はなく、ステートメントの
使用パターンに基づいて説明するだけでよい。
解答:いい加減NetBSDのソースは飽きたので以下のソースを調べる。
tar-1.17-3.fc8/tar-1.17/src/buffer.c
$ grep break buffer.c | wc -l
で検索したところ、全部で37個。
うちswitch以外で用いられているbreakは以下の4つ。
l691,l948,l1492,l1567
これ以外に2つほどswitch-breakをとりあげよう。
print_total_stats () l316~l354

void
print_total_stats ()
{
  switch (subcommand_option)
    {
    case CREATE_SUBCOMMAND:
    case CAT_SUBCOMMAND:
    case UPDATE_SUBCOMMAND:
    case APPEND_SUBCOMMAND:
      /* Amanda 2.4.1p1 looks for "Total bytes written: [0-9][0-9]*".  */
      print_stats (stderr, _("Total bytes written"),
		   prev_written + bytes_written);
      break;
    case DELETE_SUBCOMMAND:
      {
	char buf[UINTMAX_STRSIZE_BOUND];
	print_stats (stderr, _("Total bytes read"),
		     records_read * record_size);
	print_stats (stderr, _("Total bytes written"),
		     prev_written + bytes_written);
	fprintf (stderr, _("Total bytes deleted: %s\n"),
		 STRINGIFY_BIGINT ((records_read - records_skipped)
				    * record_size
				   - (prev_written + bytes_written), buf));
      }
      break;
    case EXTRACT_SUBCOMMAND:
    case LIST_SUBCOMMAND:
    case DIFF_SUBCOMMAND:
      print_stats (stderr, _("Total bytes read"),
		   records_read * record_size);
      break;
    default:
      abort ();
    }
}

この関数ではbreak実行後に関数が終了する。
使っている理由は、subcommand_optionがいくつになるかによって処理を分岐させたかったから、
といったところだろうか。
もう一ついこう。_open_archive()よりl470~485

     switch (wanted_access)
	{
	case ACCESS_READ:
	  child_pid = sys_child_open_for_uncompress ();
	  read_full_records = true;
	  record_end = record_start; /* set up for 1st record = # 0 */
	  break;
	case ACCESS_WRITE:
	  child_pid = sys_child_open_for_compress ();
	  break;
	case ACCESS_UPDATE:
	  abort (); /* Should not happen */
	  break;
	}

ここではなぜかdefaultが用いられていない。
次はwhile-breakの例。
short_read()よりl683~691

 while (left % BLOCKSIZE != 0
	 || (left && status && read_full_records))
   {
     if (status)
	while ((status = rmtread (archive, more, left)) == SAFE_READ_ERROR)
	  archive_read_error ();
     if (status == 0)
	break;

breakの手前のwhileで、rmtread()とarchive_read_error()をひたすら実行している。
そしてrmtread()から受け取った値がSAFE_READ_ERRORでなくなったときループから抜ける。
ここでさらにstatusが0だと、breakする。
次はfor-breakの例。continueとセットで説明。
simple_flush_read()よりl1479~1493

 for (;;)
   {
     status = rmtread (archive, record_start->buffer, record_size);
     if (status == record_size)
	{
	  records_read++;
	  return;
	}
     if (status == SAFE_READ_ERROR)
	{
	  archive_read_error ();
	  continue;		/* try again */
	}
     break;
   }

無限ループになっているが、ループの一番浅い階層にbreakがあるので
何事もなければ1度も繰り返すことなくループを抜け出すだろう。
statusの値によって、関数そのものが終了するか(return)、
ループを繰り返すか(continue)が決まる。
continueについては以下のソースも調べよう。
tar-1.17-3.fc8/tar-1.17/src/extract.c
ざっと読んだところ、一つの共通点が出てきた。
いずれもfor内でのcontinueだが、ループの末尾に必ずbreakを伴っている。
なぜだろう?