はじめての C

C programming note*1
次は、文字列の 後ろ側にある 空白を とり除く関数、

void remove_pos_blank(char *buf)
{
char *q;

if (*buf == '\0')
return;

q = buf + strlen(buf) - 2; /* minus 2 ('\n' + '\0') */
while (ISBLANK(*q))
--q;
*(q + 1) = '\n';
*(q + 2) = '\0';
}

remove_pos_blank() は、その行のうちの 最後の 「透明ではない文字」 より 後ろにある 空白や タブを とり除いている。

これらは、文字列の 「後ろから」 順に 見ている。 文字列の 末尾は ナル文字であり、その直前は 改行コードの はずなので、文字列の長さから 2 だけ 引いた 位置から 開始している。 そこから 前方向に、空の while文で 順に 「透明な 文字の間」 くり返して 見ている。

そこで、「透明でない文字」 が 最初に 見つかった 位置の 後ろに 改行を、そのもう1つ 後ろに ナル文字を 新たに 書き込むことによって、後ろに あった 透明な文字 (タブや空白) を とり除いているわけだ。(p139)

remove_pre_blank() と同様 この関数も、文字列を 配列としてでなく、ポインタとして 引数に とっています。 つまり、この buf は 文字列の 先頭の アドレスな わけです。
まず、if文で ポインタの 指す値が ナル文字でないことを チェックしています。
そして、ポインタ q を 文字列の 末尾に 移動させ、そこから '\n' と '\0' を 除いた位置に さらに q を 動かしています。

q = buf + strlen(buf) - 2;
この文の 書き方は はじめてですね。
q = buf
で、q に 文字列の 1つ目の アドレスを コピーします。
つぎに、strlen(buf) により 文字列内の char の 個数を 求め、それを 加えていきます*2
strlen() は、文字列の 末尾の ナル文字分は 除いて 計算するので、
buf + strlen(buf)
としても、文字列での ナル文字分が 重なることは ありません。
つまり、右辺で 文字列の データ数が 加算されることで、結果として ポインタ q が 文字列の 末尾に 移動したことに なるわけです。
最後に 2 を引くことで、p は 改行コードや ナル文字でないことが 確認されます。
次の 空の while文では、デクリメントを 対象の 前に つけることで、q は 空白や ナル文字を 除いた その 「1つ前の」 位置へと 移動します。
ここまできて ようやく、この文字列の 後ろの 空白分を 除くことで 短くすることが できるように なります。
それが こちら、
 *(q + 1) = '\n';
*(q + 2) = '\0';
1行目は、改行コードを 加えて 行のスタイルを 整える、というだけです。
ポイントは、2行目の ナル文字を 加えるところです。
ナル文字は 「文字列の最後」 を 表わす コードですから、ここに '\0' を 挿入することで、この文字列の そこから後は 見えなくなります。
つまり、この remove_pos_blank() を 呼び出す側 - ここでは carte() ですが - からは、新しく 挿入した '\0' までが 文字列として 参照されるので、結果的には 空白や タブが 削除されたのと 同じに なるわけですね。

*(q + 1) というのは、q の 1つ 次の場所の 内容だ。 1つとは、その対象の 型の大きさの 1つ分のことだ。 この場合は char なので 1Byte だが、型によって サイズは 異なる。(p154)

将来 使うかもしれないので、別の書き方も、

(ここで) カッコが 必要なのは '*' のほうが '+' よりも 結合の 優先順序が 高いためだ。 (例えば) *q + 1 と 書いてしまっては q の 指す場所に 1 を 足したものなので、まるっきり別の意味に なってしまう。

カッコを 書くのは メンドウだが、実は カッコが 必要ない 書き方として、上記の 2行は、

q[1] = '\n';
q[2] = '\0';

のようにも 書ける。 これは 配列の要素の 参照の書き方と まったく 同じである。(p154)

carte() も 少し 手直ししておきます。

void carte(FILE *fp)
{
char buf[MAX_SIZE];

while (fgets(buf, MAX_SIZE, fp) != NULL) {
remove_pre_blank(buf);
remove_pos_blank(buf);
center(buf, WIDTH);
fputs(buf, stdout);
}
}

ここで、remove_pre_blank() や remove_pos_blank() の 仮引数が ポインタなのに、なぜ carte() での 実引数が 配列の名前 - buf - に なっているのか、というと、

呼び出された 関数側で、呼び出した 関数側の 変数の内容を 変更したい場合 - ここでは 文字列 - は、引数には 変数のアドレス (変数へのポインタ) を 渡す 必要がある。

もちろん、呼ばれる 関数側でも その引数が ポインタ型であると 宣言しておかなければならないし、そのポインタを 使って 間接的に 値を 変更するわけだ。

ただし、配列の場合は 事情が 異なる。 配列の名前を 関数の 引数として 使うと、その場合は その配列の 先頭アドレスが 渡される。 つまり 呼ばれた 関数側では ポインタとして 受け取るわけだ。(p110)

なぜなら、

・ 配列の名前だけを 書いた場合は その1つ目の要素の アドレスの 意味になる。

・ C の 関数の引数は 値渡し pass by value なので、関数内の 仮引数は、呼び出し側の 実引数のを コピーした ローカルな変数に すぎない。 すなわち、定数を 渡しても、(渡された側の) 関数定義内では (その定数の値で ほどよく 初期化された) 変数となる。

という 2つの事実により、「関数の引数に 配列の名前を 渡すと、関数内では ポインタとして 使える」 ということが 導きだされるのである。(p153)


考えてみると、文字列の後ろに タブや スペースがある 状況というのは よく わからない。 タイピングミス とか ?
でも [ENTER] と [SPACE], [TAB] の ボタンの位置が あんなに 離れてるのに、ですか ?
しかし、プログラマというのは、この「見えない文字」が 気になって その削除のためだけに 時間を さくことも けっこう あるらしい (これ、「ハッカーと画家」(isbn:4274065979) に 確か 書いてあったような ...)

*1:「作ってわかる Cプログラミング」

*2:ポインタ演算