はじめての C

C programming note*1

今回は さらに オプションスイッチを 追加し、"-n" オプションが 指定されたときに 行番号を 出力するように 機能追加して、cat4.c と した。(p111)

オプションを 追加する やり方は 同じで、関数 do_one() に 少し 変更を 加えて、新しい関数 cat() を 作成します。
main() と 同じく ここでも flag を 使っていますが、その仕組みが とても 巧妙です。

void cat(FILE *fp)
{
int line_no; /* 行番号 */
int c;
int flag;

flag = YES;
line_no = 0;

while )((c = fgetc(fp))( != EOF) {
if (n_flag == YES && flag == YES)
fprintf(stdout, "%6d ", ++line_no);

/* c が 改行記号の ときのみ flag = YES */
/* それ以外の 文字は flag = NO に 変換 */
flag = (c == '\n') ? YES : NO;

if (iskanji(c)) {
fputc(c, stdout);
fputc(fgetc(fp), stdout);
}
else if )((c_flag == YES && s_flag == NO) && islower(c))(
fputc(toupper(c), stdout);
else if )((c_flag == NO && s_flag == YES) && isupper(c))(
fputc(tolower(c), stdout);
else
fputc(c, stdout);
}
}

cat() 関数では、line_no と flag という 変数が 追加されている。

line_no のほうは 行数を 数えるために 使われている。 この変数は、関数 cat() が呼び出されるたびに 毎回 while文の 直前で 0 に 初期化されるので、出力される 行番号は ファイルごとに リセットされる (各ファイルの 最初の行で 1 に 戻る) ことになる。

fprintf() の中で line_no++ ではなくて ++line_no に なっていることにも 注目すること。 変数を インクリメントした後の 値を 使うので (入力ファイルの) 1行目の 行番号は 0 にはならず 1 になる。(p115)

コマンドラインに ファイル名を 続けて 指定すると、各ファイルごとに 1 から 行番号が ついていく、ということですね。

flag には 「直前の 文字が 改行 '\n' だった場合」 か もしくは 「ファイルの先頭」 で YES に 規定される。

つまり 行番号を 出力すべき 「行の先頭」 の タイミングを 表す変数だ。(p115)

ここの ところです、

if (n_flag == YES && flag == YES)
fprintf(stdout, "%6d ", ++line_no);
flag = (c == '\n') ? YES : NO;

1行目の if文では 「もし -n オプションが 指定されているときで、かつ 直前の文字が 改行コードだったときには」 という 条件に あてはまる場合に、その文字を 出力する前に 行番号を 出している。(p116)

次は、3行目の 条件演算子について、

これは 条件演算子 conditional operator というもので、
式1 ? 式2 : 式3
という形式で 使われる。 3つの 総演算数を とるため 3項演算子 ternary operator とも 呼ばれる。

条件演算子は、式1 が まず 評価され、それが 真 true だった場合には、式2 が 評価されて その値が この式全体の 値となり、そうでなくて 偽 false の場合は 式3 が 評価されて それが 値となる ... という 働きのものだ。

つまり この cat4.c で 使われている 例では、変数 c の値が 改行コードだった場合には 変数 flag には YES が 代入され、そうでないときには NO が 代入されるわけだ。

この if文を 見ても わかるように、この部分では ループの くり返し中での 次回に 備えて 「今回の 文字が 改行だったか どうか」 を 記録している。(p118)

では、

このアルゴリズムに 欠点は ないだろうか ?

たしかに (入力した ファイルの) 3行目の前に 行番号を つけるには、直前 (2行目の終わり) は 改行コードだろうし、その事情は 4行目でも 5行目でも 同じだ。 最終行においても その直前の行の 末尾は 改行に 違いない。

では 1行目については どうだろうか ? 実は それも 心配いらない。 そのために while ループの 直前で flag の値は YES に 設定してあるのだ。(p118-119)

あと 変更の あったところでは、オプションの flag を チェックする if文で、条件式を ちょっと いじってます。

if )((c_flag == YES && s_flag == NO) && islower(c))(
fputc(toupper(c), stdout);
upper3r.c にある コードと 比べると、行数が 短くなったことが わかります。
&& 演算子は 左から 結合していくので、式の 左側にある ( ) の中を 評価して、それから 右の islower() を 評価する、ということですね。 (この内側の ( ) は 略しても いいのかも しれませんが ...)
プログラムは 次のとおり、
/* cat4r.c */
#include
#include
#include
#include

#define iskanji(c) (0xa1 <= ((c) & 0xff) && ((c) & 0xff) <= 0xfe)
#define YES 1
#define NO 0

void cat();
void cant();
int c_flag = NO;
int s_flag = NO;
int n_flag = NO;

main(int argc, char **argv)
{
FILE *fp;
char *s;

while (--argc > 0 && **++argv == '-') {
for (s = *argv + 1; *s != '\0'; s++) {
switch (*s) {
case 'c':
c_flag = YES;
break;
case 's':
s_flag = YES;
break;
case 'n':
n_flag = YES;
break;
}
}
}

if (argc == 0)
cat(stdin);
else {
while (argc--) {
if )((fp = fopen(*argv, "r"))( == NULL)
cant(*argv);
cat(fp);
fclose(fp);
argv++;
}
}

return 0;
}

void cat(FILE *fp)
{
int line_no;
int c;
int flag;

flag = YES;
line_no = 0;

while )((c = fgetc(fp))( != EOF) {
if (n_flag == YES && flag == YES)
fprintf(stdout, "%6d ", ++line_no);
flag = (c == '\n') ? YES : NO;

if (iskanji(c)) {
fputc(c, stdout);
fputc(fgetc(fp), stdout);
}
else if )((c_flag == YES && s_flag == NO) && islower(c))(
fputc(toupper(c), stdout);
else if )((c_flag == NO && s_flag == YES) && isupper(c))(
fputc(tolower(c), stdout);
else
fputc(c, stdout);
}
}

void cant(char *name)
{
fprintf(stderr, "can't open %s\n", name);
exit(1);
}

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