第9章 文字列の基礎

テキスト『新・明解C言語 入門編』第9章の内容に関する補足説明を提供します.

9-1 文字列とは

文字列リテラル

ナル文字(null character)は 「すべてのビットが 0 である文字」と定義されます. 数字やアルファベットのように表示されませんが, これも「文字」です. C言語では8進拡張表記を使って '\0'と表記します. これは文字 '0' とは異なるので注意してください. たとえば,テキスト p.232 に示されている JIS コードで, 文字 '0' は 30(10進数での48)であって, 0 ではありません.この表ではコード 00 に対応する文字が空欄になっていますが, 実はこれはナル文字です.

ナル文字 '\0' が 0 であることを簡単に確かめるには,


printf("ナル文字の値は%dです。\n", '\0');

というように,printf 関数で '\0' を10進表示してやればよいでしょう.

文字列リテラルの大きさ

プログラム中に文字列リテラルがあると, Fig.9-3 のように,メモリ上に文字列が記憶されます. これはプログラムの開始時に行われ, プログラムの終了時まで存在します. テキストでの, 「文字列リテラル "ABCD" を puts 関数に渡す必要があるため, 文字列リテラルには静的記憶域期間が与えられる」という説明は, 少しわかりにくいかもしれません. この関数は引数に与えられた文字列リテラルを出力するという理解だと, 文字列リテラルをずっとどこかに記憶しておく必要性がよくわからないでしょう. これを理解するには,第10章と第11章で学習するポインタの知識が必要です. 簡単に説明すると,puts 関数に渡されているのは, 文字列ではなく,文字列が記憶されているメモリのアドレスなのです. 最初に文字列がメモリのどこかに記憶され, puts 関数にはその文字列のアドレスが渡されます. すると,puts 関数はそのアドレスに記憶されている文字列を出力します.

メモリのどこかに保存されている文字列リテラルは, いつでも利用できますが, 書き換えることはできません(正確には, 書き換えようとしたときの動作は不定で, 何が起きるかはわかりません). この点で,すぐ後で学習する, 自由に書き換えが可能な文字配列とは異なります.

文字列

文字列リテラルは文字列定数とも呼ばれます. 定数なので,変数に代入して使用されます. ただし,C言語には文字列を丸ごと保存する型の変数がないので, 1文字ずつを要素として char の配列に保存します.

テキストは「文字列リテラル」と「文字列」を区別しています. この区別はC言語の決まりではなく,このテキスト独自のものです. このテキストでは,文字列はナル文字で終了するので, そのあとにまた文字は続かないと考えているようです. これは文字配列とは整合性のある考え方です. 文字配列では,要素を最初から順に見ていくと, ナル文字が出てきたところで終了です. これはあとで演習 9-1 を行うとよくわかるでしょう.

List 9-2 のように,文字配列の内容を表示するときの変換指定は %s, 実引数は配列名です. 第11章では,この実引数が文字配列の先頭要素へのポインタであることを学びます.

文字配列の初期化

演習 9-1 での配列 str は4番目の要素がナル文字です(これより前にナル文字はありません). そのため,この配列の内容を printf 関数で表示すると, 3番目の要素までが表示されます. しかし,この配列の要素数は8であり, 5番目から8番目の要素にも文字は入っています. 8番目はナル文字です.これを確かめるには, 以下のように main 関数を書き換えるとよいでしょう.


int main(void)
{
    int i;

    char str[] = "ABC\0DEF";

    printf("文字列strは\"%s\"です。\n", str);

    printf("sizeof(str) = %u\n", (unsigned) sizeof(str));

    for (i = 0; i < sizeof(str); i++) {
        printf("str[%d] = %d\n", i, str[i]);
    }

    return 0;
}

このように main 関数を書き換えて実行した結果は下図です. 文字コード ASCII において,65 は文字 'A'に対応します.

ex0901

空文字列

テキストでは 空文字列(null string)を 「文字が1個もない文字列」 と定義しています. Fig.9-5 に示されているように, ナル文字1個だけの配列は空文字列となります. 要素数がいくつであっても, 先頭要素がナル文字であれば空文字列であると考えられます. 次のセクションにある演習9-2では, 先頭要素をナル文字にすることで, 空文字列ではない文字列を空文字列にします.

文字列の読込み

List 9-4 で,scanf 関数の実引数に & 演算子をつけないことに注意してください. 今の段階ではこれは例外のように見えますが, 第10章と第11章でポインタを学習すると, 変数名に & 演算子をつけていたこれまでの書き方と整合性があることがわかります.

テキスト p.244 での 「scanf 関数は,キーボードから読み込んだ文字列を配列に格納する際に, 末尾にナル文字を格納します」という記述は, 変換指定子が s のときの動作です.テキスト p.359 に掲載されている, scanf 関数の変換指定子の仕様を参照してください.

文字列を書式化して表示

変換指定での精度が「表示する桁数の上限」となるのは, 変換指定子が s の場合です. テキスト p.354-357 に掲載されている printf 関数の仕様では 「文字列から出力に書き出すことができる最大の文字数」と書かれています. テキスト第2章で学習したように, d 変換での精度は「出力すべき最小の桁数」, f 変換での精度は「小数点の後ろに出力するべき桁数」となります.

よく使う変換指定は自然に覚えてしまいますが,意識的に覚える必要はありません. 変換指定の意味をよく理解し, 必要に応じて printf 関数の仕様を参照すれば, 自分の望む出力ができるようにしてください.

9-2 文字列の配列

文字列の配列

ここまでは「文字の配列」を扱ってきたのに対して, ここで扱うのは「文字列の配列」です. 「文字配列の配列」あるいは「文字配列を要素とする配列」と表現した方が, わかりやすいかもしれません.配列名が cs だとすると, cs[0] は最初の文字配列,cs[1] は次の文字配列を指します. ちょうど,int 型の配列名に [ ] をつけて参照すれば int 型の値が取得できるように, 文字配列の配列名に [ ] をつけて参照すれば文字配列が取得できます.

文字列の配列への文字列の読み込み

演習 9-3 では,「最初の for 文で "$$$$$" を読み込んだ時点で読み込みを中断・終了する」ために, 読み込んだ文字列が "$$$$$"であるかどうか if 文で判定することはすぐに思いつくでしょう. 問題なのは if 文の制御式です.意味的には if (s[i] == "$$$$$") としたいところですが, これはうまくいきません. その理由は第10章と11章でポインタを学習するとわかります. この式はポインタの比較になっています. このように制御式を書いて clang でコンパイルすると, 下図のように警告が出ます.

ex0903 warning

第11章で学習する strcmp 関数を使って,


if (strcmp(s[i], "$$$$$") == 0)

とすればいいのですが,今の段階ではまだこの関数を学習していません. そのため,少し面倒な制御式を書く必要があります.

9-3 文字列の操作

文字列の長さ

演習 9-5 および演習 9-6 では, 検索する文字 c(任意の1文字を入力します)は配列に保存するとよいでしょう. 1文字だけなので配列でなく変数を使うこともできますが, 配列を使った方が簡単です. 変数を使うと,プログラムの内容によっては, ちょとした工夫が必要になることがあります. たとえば,演習 9-5 で下図のような main 関数を書いたとしましょう. 検索する文字は,getchar 関数で読み込み,int 型変数 ch に保存しようと考えています.

the getchar function doesn't work properly

このプログラムにはエラーはありませんので,コンパイルは通ります. しかし,実行すると,意図したように動作しません. 下図のように,検索したい文字の入力がスキップされてしまいます.

the getchar function doesn't work properly

このようになってしまうのは, 入力された文字列(上では "ABCDEFG")の最後にある改行文字([Return / Enter] キーの入力)を, scanf 関数が「食べ残す」ためです. これを次の getchar 関数が食べてしまうため, 検索する文字列の入力がスキップされてしまう(正確には, 検索する文字として改行文字が入力されてしまう)のです. これはよく知られた scanf 関数の問題点です.

この問題に対処する方法はいくつかあります. 簡単な対処法として, 文字列に続く改行文字を読み取って, そのまま捨ててしまうことができます. 上図の main 関数で, 文字列の読み込みに使用している scanf 関数を,


scanf("%s%*c", str);

とします.ここで %*c は1文字を読み込む変換指定です. この変換指定は改行文字も食べてくれます. アスタリスク(*)は代入の阻止を意味します. つまり,改行文字を読み込みますが,どこにも保存せずに捨ててしまいます. scanf 関数の問題点とその対処法については, 以下の文献の 16.6 節に詳しい解説があります.

林晴比古 (2004) 新訂 新 C言語入門 シニア編 SBクリエイティブ

文字列の表示

増分演算子 ++ および減分演算子 -- については, テキスト p.79 と p.86 で学習しました. List 9-9 での while ループ


while (s[i])
   putchar(s[i++]);

では,s[i] がナル文字でないときに, putchar 関数で s[i] を表示してから,i の値をインクリメントします. 後置増分演算子を用いた式 i++ を評価すると, 増分前の値となるのでした.

数字文字の出現回数

演習 9-9 で必要な,配列の全要素の並びを反転する方法は, List 5-8(p.117)および演習 5-5(p.121)で学習しました.

大文字・小文字の変換

ヘッダ <ctype.h> では文字を処理するための関数(あるいはマクロ)が定義されています. List 9-11 で使用している toupper 関数(to upper case letter という意味), および, tolower 関数(to lower case letter という意味)は,そうした関数の一例です.

文字ではなく,文字列を処理するための関数は, 第11章(p.298)で学習する <string.h> ヘッダで定義されています.

文字列の配列の受渡し

テキストで注意されているように(p.254), 2次元配列を受け取る仮引数の宣言で, 要素数を省略できるのは最も先頭の次元だけでした. その後の次元では要素数は省略できません. これは,仮引数の宣言だけでなく, 関数内での配列の宣言と初期化でも同じです. このようなルールがある理由は, 文字列の2次元配列ではよくわかります. 2次元配列とは言いますが, その正体は文字列の配列を並べた1次元の配列です. メモリ上では Fig.5-9(p.125)のように要素が格納されていきます. 要素である文字列が最大で何文字なのかわからなければ, 長さの異なる文字列をうまく並べられません.

演習 9-11 では, 『解きながら学ぶC言語』での解答例に誤りがあります(初版で確認). 配列に文字列を読み込む関数 get_strary で, 最初の仮引数に const 型修飾子がつけられています. キーボードから入力された文字を配列に書き込むのですから, この関数に渡される配列は読み込み専用ではありません. const 型修飾子は不要です.

演習 9-3 と同様に,第11章で学習する strcmp 関数を使えば, 演習 9-11 で文字列 "$$$$$" が入力されたかどうかの判定は簡単に書けます. しかし,今の段階ではまだこの関数を学習していません. そのため,if 文において,少し面倒な制御式を書く必要があります.

まとめ

まとめのプログラム summary.c で, put_string_rep 関数に誤植があります(初版で確認). 2番目の while ループの中で, 文字定数を囲む単一引用符(')が一方だけ二重引用符になってしまっています. 以下に,この while ループおよびその前後の正しい記述を,注釈と共に示します.


printf("   { ");  /* { の前を半角3文字,後を半角1文字空ける */
i = 0; /* 上の while ループで,i の値は 0 でなくなっている */
while (s[i]) {
    putchar('\'');  /* ここに誤植があった */
    putchar(s[i++]);
    printf("\' ");  /* 単一引用符の後,半角1文字空ける */
}
printf("'\\0' }\n");  /* '\0' を出力した後を,半角1文字空ける */