第21回
Cの簡略記法~評価の順序を前提にしたシンプルな書き方

ポインタとインクリメント演算子

Cの簡略表記では、文字列処理などでよく用いられるポインタ変数のインクリメント/デクリメントがおそらく最も有名でしょう。演算子の優先順位と結合規則を利用して、非常にシンプルな式を記述できますが、慣れていないと一見してその動作が分かりにくいことも事実です。

文字列を順に調べる処理

文字列中でスペース(0x20)が何文字目に現れるかを調べる場合、文字列から1文字ずつ取り出してはその内容を調べる──という動作を行うことになります。そのためには、リスト1のような流れが考えられます。

リスト1:文字列からスペースの現れる位置を調べる~冗長なソース
	char *p; --------------- 取り出した1文字を受け取る
	int n = 0; ------------- カウンタ
	p = "Hello world!"; ---- 文字列の先頭アドレスを代入

	while (*p != '\0') { --- 終端まで繰り返す
	  if (*p != 0x20) { ---- スペースでないとき
	    n++; --------------- カウンタを増加
	    p++; --------------- 次の1文字へ
	  }
	  else { --------------- *pがスペースのとき
	    printf("%d 文字目にスペースを発見しました。\n", n+1);
	  }

比較とインクリメントをまとめる

リスト1では、whileの条件式(*p != '\0')で「pの示す1文字が'\0'(NULL)でないか」を調べています。

さらに、続くif文の条件式で(*p != 0x20)として「pの示す1文字がスペース(0x20)でないかどうか」を調べ、その結果が真(*pがスペースではない)なら文字の位置を示すnと共に文字列を操作するポインタ変数pもインクリメント(1増加)しています。

このような処理はリスト2のように簡略化できます。

whileの条件式で(*p++)とすれば、演算子の優先順位と真偽値の判定に従って、以下のような動きをします。

1.*p(pの示す1文字)を判定
2.*pが0(NULL)かどうかを判定
3.p(アドレス)を1増加

後述するように、変数名の後ろに++や--の付く後置演算子は『まずインクリメントまたはデクリメントされる前の変数の値を評価し、その後で変数の値をインクリメントまたはデクリメントする』という動きをします。

リスト2:文字列からスペースの現れる位置を調べる~簡略版
	char *p;
	int n = 0;
	p = "Hello world!";

	while (*p++) {
	  if (*p != 0x20) {  /* スペースでないとき */
	    n++;
	  }
	  else {
	    printf("%d 文字目にスペースを発見しました。\n", n+1);
	  }

ifとelseの{ }を省く

if~elseで実行する文が1行なので、リスト2のwhile以降は以下のようにさらに簡略化できます。

但し、{ }を略すとソースが分かりにくくなるので、あまりお奨めはできません。ifやfor、whileなどで実行される処理単位は、たとえ1行でも{ }で囲んでおく方がソースの可読性を向上させます。
while (*p++) {
  if (*p != 0x20)
     n++;
  else
    printf("%d 文字目にスペースを発見しました。\n", n+1);
}

後置インクリメントの動き

後置インクリメント/デクリメント演算子はポインタ参照演算子*と同じ優先順位ですが、先述したように『まず変数の値を評価してから、その後に変数の値に1を加算または減算する』という動きをします。従って"*p++"のような記述では、
まずpの値に基づいて*でその示す先を取り出す
続いてpの値を1増加する
ということになります。

少し分かりにくいですが、この法則を応用した以下のような記述を見れば直感的に理解できるのではないかと思います。
*d++ = *s++;
これは、文字列をコピーするときによく用いられるソースです。

文字列のコピー

文字列をコピーするにはstrcpy関数を使えばよいのですが、これを自前で作成するとすれば、リスト3のようなソースになります。

この場合の"*d++ = *s++"の動作は、以下のようになります。

1.dはそのまま
2.*dを評価
3.dを増加
4.sもそのまま
5.*sを評価
6.sを増加
7.増加前の*dに増加前の*sを代入(=)
(このときdとsはインクリメントされている)

リスト3:文字列をコピーする関数のソース
	void strcopy(const char *s, const char *d)
	{
	  while (*s) {    /* *sが末尾に達するまで */
	    *d++ = *s++;  /* *sを*dに代入して両者をインクリメント */
	  }
	}

++/--演算子の後置と前置

インクリメント演算子++とデクリメント演算子--は、変数の前に記述した場合(前置)と後ろに記述した場合(後置)とで、動作が異なります。

前置:先に変数の値をインクリメントまたはデクリメントしてから評価する
後置:先に変数の値を評価してからインクリメントまたはデクリメントする
こうした演算子の優先順位や性格(動作の法則)を前提にして、簡略的な表記が可能となっているのです。頭の中で処理の流れを整理し、パズルを解くような感じで理解すると楽しめます。そして、最終的にはパターンとして覚えるのがいいでしょう。

あとがき

hiropの『ちょっと気になる専門用語』~《戻り値》

現在では、ほとんどの書籍や雑誌で、関数の返す値を「戻り値」と呼んでいますが、かつては色々な呼び方がありました。代表的なものに「返値」「返り値」があります。

これは、英語の"return value"の訳なので、どれも「(関数から)返ってくる値」、「(関数が}返す値」というニュアンスです。

「返値」はCのバイブルとも呼ばれている「プログラミング言語C」(共立出版)の第1版で、「返り値」は昔のVisualBasicのオンラインヘルプで使われていた表記です。

「返値」は「へんち」と読みますが、音読みのため「辺地」と誤解されて伝わりにくい場合もあります。「返り値」は「かえりち」→「返り血」と連想されるので、なんだか物騒なイメージです。

そのような理由でこれらが却下されて「戻り値」に落ち着いたのかどうかは定かではありませんが、いずれにせよ文字ではなく言葉(音声)を介して伝えにくいであろうことは容易に推測できます。

「返」という文字が付くと、関数が返す、関数から返ってくる──といったように『関数が主体』となります。が、「戻」という文字を使うと「関数から戻ってくる」というイメージになり、『それを受け取る側=プログラマー』が主体となります。

関数という機能ではなく、それを扱う人間が主体である──「戻り値」という表現にはそんなニュアンスがあるため、受け入れやすかったのではないでしょうか。