コンテンツにスキップ

第9章 文字列の基本

前章の後半では、「文字」について学習した。もっとも、私たちの身のまわりを見わたしても、単独の文字で表せるものは、あまり見当たりません。たとえば、名前や地名など、どれをとっても、多くは、複数の文字の並びである。

本章では、文字の並びである文字列の基本を学習する。

9-1 文字列とは

一連の文字の並びを表すのが、文字列である。本節では、文字列や文字列リテラルの基本を学習する。

文字列リテラル

まず、"ABC"のような、文字の並びを"二重引用符"で囲んだ文字列リテラル(string literal)について、詳しく学習していこう。

文字列リテラルの末尾には、見た目ではわからない〈オマケ〉が付いている。そのオマケは、ヌル文字(null character)という値0の文字である(8進拡張表記だと'\0'で、整数定数表記だと0である)。

そのため、"と"のあいだに置かれた文字の並びの後ろに、ヌル文字がくっついた状態で記憶域に格納される。Fig.9-1に示すのが具体例である。

  • 図a ... 見かけ上3文字の文字列リテラル"ABC"は、4文字分の記憶域を占有する。
  • 図b ... 見かけ上0文字のリテラル""は、ヌル文字のための1文字分の記憶域を占有する。

Fig.9-1 文字列リテラルの内部表現 Fig.9-1 文字列リテラルの内部表現

Tip

第7章では、charが1個の〈箱〉に相当することを学習した。文字列リテラル"ABC"は4個の箱が並んでおり、""は箱が1個だけの状態である。

文字列リテラルの大きさ

文字列リテラルの末尾にヌル文字が付加されることを、プログラムで確認しよう。List 9-1に示すのが、そのプログラムである。

List 9-1

// 文字列リテラルの大きさを表示する
#include <stdio.h>

int main(void)
{
    printf("sizeof(\"123\")     = %zu\n", sizeof("123"));
    printf("sizeof(\"AB\\tC\")   = %zu\n", sizeof("AB\tC"));
    printf("sizeof(\"abc\\0def\") = %zu\n", sizeof("abc\0def"));

    return 0;
}

実行結果

sizeof("123")     = 4
sizeof("AB\tC")   = 5
sizeof("abc\0def") = 8

三つの文字列リテラルの大きさを、sizeof演算子で取得して表示している。各文字列リテラルについて、右ページのFig.9-2を見ながら、理解を深めていこう。

a 文字列リテラル"123" 4文字分の記憶域を占有する。

b 文字列リテラル"AB\tC" 途中の'\t'は、見かけは2文字ですが、タブ文字を表す1個の文字とみなされる。

c 文字列リテラル"abc\0def" 途中にヌル文字'\0'がありますが、これとは別に、末尾にもヌル文字が付加される。

いずれも、文字列リテラルの大きさは、末尾のヌル文字を含めた文字数と一致する。

Fig.9-2 文字列リテラルの内部表現 Fig.9-2 文字列リテラルの内部表現

重要

文字列リテラルの末尾には、(途中の文字とは無関係に)値が0のヌル文字が付加される。

Column 9-1 文字列リテラルの性質

ここでは、文字列リテラルの性質を2点補足学習する。

  • 文字列リテラルには静的記憶域期間が与えられる 文字列リテラルには、永遠の寿命である静的記憶域期間が与えられる。 そのため、右に示す関数funcの実行開始時に"ABCD"が作られて、実行終了時に破棄される、ということはありません。
void func(void)
{
    puts("ABCD");
    puts("ABCD");
}
  • 同一文字列リテラルの扱いは処理系依存 関数func中の二つの"ABCD"は、同じ綴りである。このように、同じ綴りの文字列リテラルがプログラム中に複数個存在するときの記憶域への格納法は、処理系に依存する(Fig.9C-1)。

図a:同じ綴りの文字列リテラルを個別に格納 二つの文字列を別ものとみなして、記憶域上に個別に格納する。あわせて10文字分が必要である。

図b:同じ綴りの文字列リテラルをまとめて格納 二つの文字列リテラルを同一とみなして、記憶域上に1個だけを格納する。そのため、5文字分の記憶域を節約できる。

Fig.9C-1 同一綴りの文字列リテラルの記憶域への格納 Fig.9C-1 同一綴りの文字列リテラルの記憶域への格納

文字列

文字列リテラルは、整数定数の15や、浮動小数点定数の3.14のようなものである。算術型の値は、変数(オブジェクト)に入れることで、自由な演算ができるようになる。

文字の並びを表す文字列(string)も事情は同じである。オブジェクトに格納しなければ、自由に扱えません。

文字列の格納先として最適なのがcharの配列である。

たとえば、文字列"ABC"であれば、Fig.9-3に示すように、'A'と'B'と'C'と'\0'の4個の文字を、charの配列の先頭要素から順に格納する。

末尾のヌル文字'\0'は、文字列終端を示す〈目印〉として働きます。

Fig.9-3 配列に格納された文字列 Fig.9-3 配列に格納された文字列

重要

文字列の格納先として最適なのがcharの配列である。文字列の末尾は、最初に出現するヌル文字である。

Tip

文字列とみなされるのは、最初に出現するヌル文字までである。その一方で、文字列リテラルは、途中にヌル文字があっても構いません(前ページのFig.9-2 c)。 そのため、"123"は「文字列とみなせる文字列リテラル」で、"abc\0def"は「文字列とはみなせない文字列リテラル(文字列とみなせるのは最初の\0まで)」ということになる。

文字列"ABC"をcharの配列に格納・表示してみよう。List 9-2が、そのプログラムである。

List 9-2

// 配列に文字列を格納して表示(その1:代入)
#include <stdio.h>

int main(void)
{
    char str[4];     // 文字列を格納する配列

    str[0] = 'A';    // 代入
    str[1] = 'B';    // 代入
    str[2] = 'C';    // 代入
    str[3] = '\0';   // 代入

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

    return 0;
}

実行結果

文字列strは"ABC"です。

char[4]型の配列strの各要素に文字を代入することで、文字列"ABC"を作っている。

なお、printf関数で文字列を表示する際の変換指定は%sであり、実引数としては、配列の名前(この場合はstr)を与える。

Tip

変換指定の%sは、文字列stringに由来する。

文字配列の初期化

文字列を作るたびに、各要素に1文字ずつ文字を代入するのは面倒である。次のように宣言すれば、配列の要素を確実に初期化できる上に、コードが簡潔になる。

char str[4] = {'A', 'B', 'C', '\0'};

これは、int型やdouble型などの配列を初期化する宣言と同じ形式である。なお、文字列の初期化に限り、次に示す形式でも宣言できるようになっている。

char str[4] = "ABC";    // char str[4] = {'A', 'B', 'C', '\0'}; と同じ

この形式を使うのが、基本である(簡潔に記述できます)。

重要

文字列を格納する文字配列の初期化は、次のいずれかの形式で行う。

a. char str[] = {'A', 'B', 'C', '\0'};  
b. char str[] = "ABC";

Tip

要素数が省略可能なのは、初期化子の個数から配列の要素数が決定されるからである(p.120)。 なお、bの初期化子を{}で囲んで、{"ABC"}とすることもできる。 配列に対する初期化子の代入は不可能でした(p.121)。文字列でも同様である。

char s[4];
s = {'A', 'B', 'C', '\0'};    // エラー:初期化子の代入は不可
s = "ABC";                    // エラー:初期化子の代入は不可

前ページのプログラムを、配列の各要素に文字を代入するのではなく、初期化するように宣言を書きかえよう。List 9-3に示すのが、そのプログラムである。

List 9-3

// 配列に文字列を格納して表示(その2:初期化)
#include <stdio.h>

int main(void)
{
    char str[] = "ABC";    // {'A', 'B', 'C', '\0'}による初期化

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

    return 0;
}

実行結果

文字列strは"ABC"です。

プログラムが読みやすく、簡潔になりました。

演習9-1

List 9-3の配列strの宣言を次のように書きかえたプログラムを作成し、実行結果を考察せよ。

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

空文字列

文字列の文字数は0個でもよいことになっていて、そのような文字列は、空文字列(null string)と呼ばれる。空文字列を格納する配列は、次のように宣言できる。

char ns[] = "";    // 空文字列の宣言(要素数は1)

Fig.9-4に示すように、配列nsの要素数は0ではなく1となり、終端を示すヌル文字だけが格納される。

Tip

すなわち、次の宣言と同じである。

char ns[1] = {'\0'};

Fig.9-4 空文字列 Fig.9-4 空文字列

文字列の読込み

次は、文字列をキーボードから読み込む方法を学習しよう。List 9-4に示すのは、名前を読み込んで、挨拶するプログラムである。

List 9-4

// 名前を尋ねて挨拶(文字列の読込み)
#include <stdio.h>

int main(void)
{
    char name[48];

    printf("お名前は:");
    scanf("%s", name);    // 要注意:&を置いてはならない!!

    printf("こんにちは、%sさん!!\n", name);

    return 0;
}

実行例

お名前は:Mike
こんにちは、Mikeさん!!

何文字の名前が入力されるのかを事前に知ることはできませんので、配列の要素数は多めに準備する必要がある(本プログラムでは48としています)。

文字列の読込みの際にscanf関数に与える変換指定は%sである。なお、読み込んだ文字列の格納先として与える実引数nameは配列であるため、&演算子を置いてはいけません。

呼び出されたscanf関数は、Fig.9-5に示すように、キーボードから読み込んだ文字列を格納する際に、末尾にヌル文字を格納する。

Fig.9-5 scanf関数による読込み Fig.9-5 scanf関数による読込み

Tip

そのため、キーボードから入力する文字数は、47文字までに収める必要がある。

演習9-2

次のように宣言された文字列sを空文字列にするのには、どのような操作を行えばよいかを示せ。

char s[] = "ABC";

文字列を書式化して表示

整数や浮動小数点数を表示する際にprintf関数に与える変換指定については、何回かにわたって学習してきました(p.38)。文字列表示のための変換指定も、ほぼ同様である。

List 9-5のプログラムと、実行結果を対比して理解していきましょう。

List 9-5

// 文字列"12345"を書式化して表示
#include <stdio.h>

int main(void)
{
    char str[] = "12345";

    printf("%s\n",    str);    // そのまま
    printf("%3s\n",   str);    // 最低3桁
    printf("%.3s\n",  str);    // 3桁まで
    printf("%8s\n",   str);    // 最低8桁で右よせ
    printf("%-8s\n",  str);    // 最低8桁で左よせ

    return 0;
}

実行結果

12345
12345
123
   12345
12345   

Fig.9-6に示すのが、変換指定の構造である。

Fig.9-6 変換指定の構造 Fig.9-6 変換指定の構造

[A] フラグ フラグが指定されると左側によせて表示され、指定されない場合は右側によせて表示されます。

[B] 最小フィールド幅 少なくとも、この桁数だけの表示が行われます。 指定が省略された場合や、実際に表示する文字列の桁数が指定された値を超えるときは、表示に必要な桁数で表示されます。

[C] 精度 表示する桁数の上限を指定します。そのため、ここに指定された以上の文字が出力されることはありません。

[D] 変換指定子 sは文字列を表示することの指定である。配列内の文字は、終端ヌル文字の直前まで出力されます。精度が指定されない場合や、精度が配列の大きさよりも大きい場合は、配列は必ずヌル文字を含んでいなければなりません。

Tip

変換指定を含め、printf関数に関する詳細は、p.376で学習する。

9-2 文字列の配列

文字列を配列で表せるのですから、その文字列を集めたものは「配列の配列」で表せるということである。

文字列の配列

同じ型のデータの集合は、配列で実現できることを、第6章で学習した。本節では、文字列の集合である、文字列の配列について考えていきます。

文字列そのものが配列で表せるのですから、文字列の配列は「配列の配列」で表せるということである。List 9-6に示すプログラムで学習していきます。

List 9-6

// 文字列の配列
#include <stdio.h>

int main(void)
{
    char s[][6] = {"Turbo", "NA", "DOHC"};    // 3個の初期化子が与えられているため、要素数は3となる

    for (int i = 0; i < 3; i++)
        printf("s[%d] = \"%s\"\n", i, s[i]);

    return 0;
}

実行結果

s[0] = "Turbo"
s[1] = "NA"
s[2] = "DOHC"

これは、3個の文字列"Turbo"、"NA"、"DOHC"を、3行6列の2次元配列sに格納して表示するプログラムである。Fig.9-7を見ながら理解していきましょう。

  • 配列sの要素 配列sは、要素型がchar[6]型で、要素数が3の配列である。要素はs[0]、s[1]、s[2]の3個であり、それぞれが初期化子"Turbo"、"NA"、"DOHC"で初期化されます。

Tip

{}の中に与えられている初期化子の個数から、要素数が自動的に3とみなされます。なお、列数が6ですから、各要素は(終端のヌル文字を除いて)最大5文字の長さの文字列を表せます。

  • 配列sの構成要素 2次元配列の各構成要素は二つの添字を用いた式でアクセスできます(p.133)。 たとえば、s[0][0]は'T'で、s[2][3]は'C'である。

Tip

{}内に初期化子が与えられていない要素が0で初期化される規則(p.121)が適用されるため、各文字列の末尾は、ヌル文字で初期化されます。

Fig.9-7 文字列の配列 Fig.9-7 文字列の配列

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

次は、文字列の配列の各要素を初期化するのではなく、キーボードから読み込むように変更しよう。List 9-7に示すのが、そのプログラムである。

List 9-7

// 文字列の配列を読み込んで表示
#include <stdio.h>

int main(void)
{
    char s[3][128];    // 初期化子が与えられていないため、要素数は省略できない

    for (int i = 0; i < 3; i++) {
        printf("s[%d] : ", i);
        scanf("%s", s[i]);    // 要注意:&を置いてはならない!!
    }

    for (int i = 0; i < 3; i++)
        printf("s[%d] = \"%s\"\n", i, s[i]);

    return 0;
}

実行例

s[0] : Paul
s[1] : John
s[2] : George
s[0] = "Paul"
s[1] = "John"
s[2] = "George"

このプログラムは、3個の文字列を読み込んで表示します。

main関数の冒頭で宣言されている配列sは、3行128列の2次元配列、すなわち、要素型がchar[128]型で、要素数が3の配列である。

Tip

列数が128ですから、(終端のヌル文字を除いて)最大127文字の長さの文字列が格納できます。キーボードからの入力時は、127文字を超えないようにする必要があります。

for文のループ本体内の、キーボードからの読込みに着目しましょう。

scanf("%s", s[i]);

読み込んだ文字列の格納先としてscanf関数に与える実引数s[i]は配列ですから、&演算子を置いてはいけません。

演習9-3

List 9-7を次のように書きかえたプログラムを作成せよ。 - 文字列の個数を3よりも大きな値とし、その値をオブジェクト形式マクロとして定義する。 - 最初のfor文では、"$$$$$"を読み込んだ時点で読込みを中断・終了する。 - 2番目のfor文では、"$$$$$"より前に入力された全文字列を表示する。

9-3 文字列の操作

ここまでは、文字列を作ったり読み込んだりするだけでした。文字列を自在に扱う方法を学習していきましょう。

文字列の長さ

まずは、次のように宣言された配列strを考えます。

char str[6] = "ABC";

Fig.9-8に示すように、要素数6の配列に対して、ヌル文字を含めて4文字の文字列が格納されます。そのため、末尾側のstr[4]とstr[5]の領域は、未使用の状態です。

このように、「配列の要素数」と「文字列の長さ」は必ずしも一致しません。

文字列の長さが必要な場合は、配列の要素数を調べるのではなく、先頭文字から'\0'の直前までに、何個の文字があるのかを調べる必要があります。

Fig.9-8 配列内の文字列 Fig.9-8 配列内の文字列

この考えに基づいて、文字列の長さを求めましょう。それが、List 9-8のプログラムです。

List 9-8

// 文字列の長さを調べる
#include <stdio.h>

//--- 文字列strの長さを返す ---//
int str_length(const char s[])
{
    int len = 0;

    while (s[len])
        len++;
    return len;
}

int main(void)
{
    char str[128];    // ヌル文字を含めて128文字まで格納できる

    printf("文字列を入力せよ:");
    scanf("%s", str);

    printf("文字列\"%s\"の長さは%dです。\n", str, str_length(str));

    return 0;
}

実行例

文字列を入力せよ:GT6
文字列"GT6"の長さは3です。

関数str_lengthは、引数sに受け取った文字列の長さ(ヌル文字の直前までの文字数)を求めて返却する関数である。

その関数str_lengthでは、変数lenを巧く使うことで、配列の要素の走査を行うとともに、文字列の長さを求めています。

走査を行うのが、プログラム水色のwhile文です。

事前に0で初期化された変数lenは、ループカウンタとして働きます(Fig.9-9)。

繰返しの継続条件は、着目要素s[len]が、0すなわちヌル文字ではないことです。

図に示すように、0で初期化されたlenは、繰返しのたびにインクリメントされていきます。

そして、lenが3になったときに、着目要素s[len]が0すなわちヌル文字になるため、while文の繰返しが終了します。

このときのlenの値3が、文字列の長さ(ヌル文字の直前までの文字数)です。

Fig.9-9 文字列の長さを求める Fig.9-9 文字列の長さを求める

なお、この関数str_lengthは、次のようにも説明できます。

配列sの要素のうち、最も先頭側に位置するヌル文字の添字を返す関数

これは、第6章で学習した線形探索(p.164)そのものです。

なお、関数間での文字列の受渡しの要領も、第6章で学習した配列の受渡しと同じです。

重要

関数間の引数としての文字列の受渡しの要領は、配列と同じである。呼出し側は、実引数として配列の名前のみを与え、呼び出される側は配列として受け取る。

Tip

念のために、配列の受渡しについて復習しましょう(すべて第6章で学習した内容です)。 呼び出す側 ... 配列を渡す側の実引数は、[]を付けずに名前だけとします。関数str_lengthを呼び出すプログラムの関数呼出し式では、実引数としてstrを渡しています。 呼び出される側 ... 呼び出された側の仮引数の配列は、実質的に、呼出し側の実引数の配列をそのものです(受け取る配列の要素の値を更新しない場合は、仮引数の宣言にconstを置きます)。

ただし、『要素数』を別の引数で受け取る必要がない点が、配列の受渡しと違います。

重要

通常の配列とは異なり、文字列を受け取る際に要素数を別の引数として受け取る必要はない。末尾のヌル文字までを処理対象とすればよいからである。

演習9-4

文字列sを空文字列にする関数を作成せよ。

void null_string(char s[]);

演習9-5

文字列sの中に、文字cが含まれていれば、その添字(文字列中に文字cが複数ある場合は、最も先頭側の添字とする)を返し、含まれていなければ-1を返す関数を作成せよ。

int str_char(const char s[], int c);

文字列の表示

次は、printf関数やputs関数に頼らず、putchar関数で文字列を表示することを考えます。 もちろん、文字列を先頭から1文字ずつ走査することで実現します。

List 9-9に示すのが、そのプログラムです。

List 9-9

// 文字列を走査して表示する
#include <stdio.h>

//--- 文字列sを表示(改行はしない)---//
void put_string(const char s[])
{
    int i = 0;
    while (s[i])
        putchar(s[i++]);
}

int main(void)
{
    char str[128];

    printf("文字列を入力せよ:");
    scanf("%s", str);

    printf("あなたは");
    put_string(str);
    printf("と入力しました。\n");

    return 0;
}

実行例

文字列を入力せよ:G12
あなたはG12と入力しました。

関数put_stringが、受け取った文字列を1文字ずつ順に表示する関数です。

文字列内のすべての文字を先頭から末尾へと走査する手順は、前のプログラムの関数str_lengthと同じです。Fig.9-10に示すように、ヌル文字までを走査して表示します。

Fig.9-10 文字列を走査して表示 Fig.9-10 文字列を走査して表示

Tip

表示するのは、ヌル文字の直前の文字までです。ヌル文字は表示しません。

演習9-6

文字列s中に、文字cが含まれている個数(含まれていなければ0とする)を返す関数を作成せよ。

int str_chnum(const char s[], int c);

演習9-7

文字列sをn回だけ連続して表示する関数を作成せよ。

void put_stringn(const char s[], int n);

たとえば、sとnに"ABC"と3を受け取った場合、「ABCABCABC」と表示すること。

数字文字の出現回数

次は、文字列の中身を調べることにします。List 9-10は、文字列に含まれる'0'~'9'の数字文字の個数をカウントするプログラムである。

List 9-10

// 文字列内の数字文字をカウントする
#include <stdio.h>

//--- 文字列str内に含まれる数字文字の出現回数を配列cntに格納 ---//
void str_dcount(const char s[], int cnt[])
{
    int i = 0;
    while (s[i]) {
        if (s[i] >= '0' && s[i] <= '9')
            cnt[s[i] - '0']++;
        i++;
    }
}

int main(void)
{
    int  dcnt[10] = {0};    // 分布
    char str[128];          // 文字列

    printf("文字列を入力せよ:");
    scanf("%s", str);

    str_dcount(str, dcnt);

    puts("数字文字の出現回数");
    for (int i = 0; i < 10; i++)
        printf("'%d':%d\n", i, dcnt[i]);

    return 0;
}

実行例

文字列を入力せよ:3.14159265358979323846
数字文字の出現回数
'0':0
'1':2
'2':1
'3':3
'4':1
'5':3
'6':1
'7':1
'8':2
'9':3

関数str_dcountは、受け取った文字列s内に含まれる各数字文字の個数を、配列cntに格納します。数字文字のカウント法は、List 8-9(p.246)とList 8-11(p.249)で学習した方法と同じです。

演習9-8

文字列を後ろから逆に表示する関数を作成せよ。

void put_stringr(const char s[]);
たとえば、sに"SEC"を受け取ったら「CES」と表示すること。

演習9-9

文字列sの文字の並びを反転する関数を作成せよ。

void rev_string(char s[]);

たとえば、sに"SEC"を受け取ったら、その配列を"CES"に更新すること。

大文字・小文字の変換

文字列内の英字(アルファベット文字)を、大文字に変換する関数と、小文字に変換する関数を作りましょう。List 9-11に示すのが、そのプログラムです。

List 9-11

// 文字列内の英字を大文字/小文字に変換
#include <ctype.h>
#include <stdio.h>

//--- 文字列内の英字を大文字に変換 ---//
void str_toupper(char s[])
{
    int i = 0;
    while (s[i]) {
        s[i] = toupper(s[i]);
        i++;
    }
}

//--- 文字列内の英字を小文字に変換 ---//
void str_tolower(char s[])
{
    int i = 0;
    while (s[i]) {
        s[i] = tolower(s[i]);
        i++;
    }
}

int main(void)
{
    char str[128];

    printf("文字列を入力せよ:");
    scanf("%s", str);

    str_toupper(str);
    printf("大文字:%s\n", str);

    str_tolower(str);
    printf("小文字:%s\n", str);

    return 0;
}

実行例

文字列を入力せよ:FukuOka79
大文字:FUKUOKA79
小文字:fukuoka79

本プログラムで定義している二つの関数の働きは、次のとおりです。

  • 関数str_toupper ... sに受け取った文字列内の英字を大文字に変換する。
  • 関数str_tolower ... sに受け取った文字列内の英字を小文字に変換する。

これらは、基本的に同じ構造の関数です。文字列sを先頭から順に走査して、着目文字を大文字あるいは小文字に変換します。

変換のために利用しているのが、ヘッダで提供されるtoupper関数とtolower関数です。

Fig9-top

Fig.9-11に示すのが、二つの関数の働きです。

関数str_toupper関数str_tolowerで文字列を走査する過程では、走査における着目文字s[i]に対して、これらの関数の返却値を代入しています。

Fig.9-11 toupper関数とtolower関数 Fig.9-11 toupper関数とtolower関数

Tip

toupper関数とtolower関数は、仮引数cに受け取った文字が英字以外の文字であれば、その文字cをそのまま返却します。そのため、関数str_toupper関数str_tolowerが、英字以外の文字を誤って変換することはありません。

ただし、これらの関数が変換対象とするのは、いわゆる半角文字です。漢字などの全角文字などには対応していませんので注意しましょう。

演習9-10

文字列s内のすべての数字文字を除去する関数を作成せよ。

void del_digit(char s[]);

たとえば、"AB1C9"を受け取ったら、"ABC"に更新する。

文字列の配列の受渡し

次は、2次元配列で実現された〈文字列の配列〉を関数間でやりとりする方法を学習します。 まずは、List 9-12のプログラムを例に理解していきます。

List 9-12

// 文字列の配列を表示(関数版)
#include <stdio.h>

//--- 文字列の配列を表示 ---//
void put_strary(const char s[][6], int n)
{
    for (int i = 0; i < n; i++)
        printf("s[%d] = \"%s\"\n", i, s[i]);
}

int main(void)
{
    char cs[][6] = {"Turbo", "NA", "DOHC"};

    put_strary(cs, 3);

    return 0;
}

実行結果

s[0] = "Turbo"
s[1] = "NA"
s[2] = "DOHC"

このプログラムは、List 9-6(p.262)をベースにして、文字列の配列を表示する箇所を関数put_straryとして独立させたものです。

文字列の配列を受け取るのが、仮引数sであり、その要素数を受け取るのが、int型の仮引数nです。関数本体では、その2次元配列のn個の要素をfor文で表示しています。

Tip

多次元配列を受け取る仮引数の宣言で省略できる要素数は、先頭の次元のみです(Column 6-2;p.171)。そのため、関数put_straryは、要素数が6でない文字列の配列(要素型がchar[6]型でない配列)を受け取れません。もちろん、次のような宣言は不可能です。

void put_strary(const char s[][], int n);

Column 9-2 文字列ではない文字の配列

次の宣言を考えましょう。

char str[4] = "ABCD";

ヌル文字を含めると、5文字分の領域が必要ですが、配列の領域が4文字分しかありません。

このように、末尾のヌル文字だけが格納できないような初期化子を与えたときは、末尾のヌル文字が格納されないことになっています。すなわち、上記の宣言は、

char str[4] = {'A', 'B', 'C', 'D'};

とみなされるのです。

このような配列は、《文字列》ではなく、《文字が4個集まった配列》として使います。

それでは、文字列をそのまま表示するのではなく、各文字を1文字ずつ走査して表示するように書きかえましょう。List 9-13に示すのが、そのプログラムです。

List 9-13

// 文字列の配列を表示(関数版:1文字ずつ走査)
#include <stdio.h>

//--- 文字列の配列を表示(1文字ずつ表示)---//
void put_strary2(const char s[][6], int n)
{
    for (int i = 0; i < n; i++) {
        int j = 0;
        printf("s[%d] = \"", i);
        while (s[i][j])
            putchar(s[i][j++]);
        puts("\"");
    }
}

int main(void)
{
    char cs[][6] = {"Turbo", "NA", "DOHC"};

    put_strary2(cs, 3);

    return 0;
}

実行結果

s[0] = "Turbo"
s[1] = "NA"
s[2] = "DOHC"

本プログラムの走査部では、添字演算子[]が2重に適用されています。複雑に見えますが、学習ずみのことを組み合わせているだけです。

Fig.9-12に示すように、文字列を走査・表示するList 9-9(p.266)の走査部と同じ構造です。

  • 赤色の部分 ... 走査・表示の対象文字列
  • 水色の部分 ... 走査において、現在着目している要素の添字

Fig.9-12 文字列内の文字の走査 Fig.9-12 文字列内の文字の走査

演習9-11

List 9-12を、次のように書きかえたプログラムを作成せよ。 - 文字列の個数を3よりも大きな値とし、その値をオブジェクト形式マクロとして定義する。 - 文字列の文字数を6ではなく128とし、その値もオブジェクト形式マクロとして定義する。 - 文字列の配列を読み込む関数を作成する。演習9-3(p.263)と同様に、"$$$"を読み込んだ時点で読込みを中断・終了する。 - "$$$"より前に入力された全文字列を表示する。

演習9-12

受け取った文字列の配列に格納されているn個の文字列の文字の並びを反転する関数を作成せよ。

void rev_strings(char s[][128], int n);

たとえば、sに{"SEC", "ABC"}を受け取ったら、その配列を{"CES", "CBA"}に更新すること。

まとめ

  • ヌル文字は、値が0の文字である。8進拡張表記で表記すると'\0'で、整数定数で表記すると0である。

  • 文字列リテラルの末尾にはヌル文字が付加される。そのため、文字列リテラル"ABC"は記憶域上に4バイトを占有し、文字列リテラル""は1バイトを占有する。

  • 文字列リテラル"..."の大きさは、末尾のヌル文字を含めた文字数と一致する。この値は、sizeof("...")で求められる。

  • 文字列リテラルは、静的記憶域期間が与えられるため、プログラム開始から終了まで記憶域を占有する。

  • 同じ綴りの文字列リテラルが複数ある場合、1個のものとみなして記憶域を節約するのか、別個のものとみなすのかは処理系によって異なる。

  • 文字列の格納先として最適なのが、charの配列である。文字列の末尾は、最初に出現するヌル文字である。

  • 文字列を格納する文字配列の初期化は、次のいずれかの形式でも行える。

    char str[] = {'A', 'B', 'C', '\0'};
    char str[] = "ABC";
    
    後者の形式の初期化子は、{}で囲んでもよい。

Fig.9-13

  • 文字が1個もない、ヌル文字だけの文字列は、空文字列と呼ばれる。

  • 文字列中の全文字の走査は、先頭文字から始めてヌル文字に出会うまで順に着目することで実現できる。

  • 文字列を走査して、先頭文字からヌル文字の直前の文字までの文字数をカウントすれば、文字列の長さ(ヌル文字を含まない文字数)が得られる。

  • 画面に文字列を表示するためにprintf関数に与える変換指定は%sである。表示の桁数や、表示の右よせ/左よせなどは、最小フィールド幅や精度などで指定できる。

  • キーボードから文字列を読み込むためにscanf関数に与える変換指定は%sである。格納先として与える実引数の配列に&演算子を適用してはならない。

  • 関数が受け取る文字列は、呼び出した側が与えた文字列そのものである。なお、ヌル文字までを処理対象と判断できるため、要素数を別の引数としてやりとりする必要がない。

  • 文字列の配列は、配列の配列、すなわち2次元配列で表せる。たとえば、ヌル文字を含んで最大12文字まで格納できる文字列(すなわちchar[12]型の配列)を5個集めた配列は、次のように、5行12列の2次元配列として実現できる。

    char ss[5][12];    // 要素型がchar[12]で要素数が5の配列
    
    ssは2次元配列であるため、その構成要素は、添字演算子[]を2重に適用した式ss[i][j]でアクセスできる。

- アルファベット文字の小文字を大文字に変換するのがtoupper関数で、大文字を小文字に変換するのがtolower関数である(これらの関数の変換対象は、アルファベットのみである)。いずれもヘッダで提供されるライブラリ関数である。

Fig.9-14

演習問題

目次

  1. 文字列を逆順に表示
  2. 文字の出現回数
  3. 数字文字の除去
  4. 文字列の配列の反転

演習問題11-1:文字列を逆順に表示

問題の説明

文字列を後ろから逆に表示する関数put_stringrを実装してください。例えば、引数として"SEC"を受け取ったら、「CES」と表示するようにしてください。

期待される結果

文字列を入力してください:Programming
逆から表示するとgnimmargorPです。

ヒント

  • 文字列を逆に表示するには、まず文字列の長さを知る必要があります
  • 文字列の長さは、先頭から順に文字を調べ、ヌル文字('\0')に達するまでカウントして求めます
  • 文字列の表示は、最後の文字(長さ-1の位置)から先頭(添字0)まで逆順に行います
  • putchar関数を使うと、1文字ずつ表示できます
  • 文字列の添字は0から始まることに注意しましょう(最後の文字の添字は長さ-1)
  • 以下はmain関数の全体コードです:
    int main(void)
    {
        char str[128];    // 文字列を格納する配列
    
        printf("文字列を入力してください:");
        scanf("%s", str);
    
        printf("逆から表示すると");
        put_stringr(str);
        printf("です。\n");
    
        return 0;
    }
    

演習問題11-2:文字の出現回数

問題の説明

文字列s中に、文字cが含まれている個数(含まれていなければ0)を返す関数str_chnumを実装してください。

期待される結果

文字列を入力してください:Mississippi
探す文字を入力してください:s
文字列"Mississippi"の中に文字's'は4個含まれています。

ヒント

  • 文字列は、先頭から順に1文字ずつ走査することができます
  • 走査は、ヌル文字('\0')に達するまで続けます
  • 関数str_chnumでは、探す文字を引数cとして受け取り、文字列内でその文字を探します
  • 走査中に引数で指定された文字と一致する文字が見つかったら、カウンタを増やします
  • 文字の比較には、単純な等値演算子(==)を使用できます
  • 以下はmain関数の全体コードです。文字入力処理に注目してください:
    int main(void)
    {
        char str[128];  // 文字列を格納する配列
        char ch;        // 探す文字
    
        printf("文字列を入力してください:");
        scanf("%s", str);
    
        printf("探す文字を入力してください:");
        getchar();      // 前の入力の改行文字を読み飛ばす
        ch = getchar(); // 文字を読み込む
    
        int cnt = str_chnum(str, ch);
    
        printf("文字列\"%s\"の中に文字'%c'は%d個含まれています。\n", str, ch, cnt);
    
        return 0;
    }
    
  • getchar関数は標準入力から1文字読み込む関数です
  • 最初のgetchar()は入力バッファに残っている改行文字を読み飛ばすために使用しています
  • 二つ目のgetchar()で実際に探す文字を読み込み、変数chに格納しています
  • 関数str_chnumの引数cをint型で受け取っているのは、C言語の標準ライブラリの慣例に従っているためです(文字関数は通常int型で文字を扱います)

演習問題11-3:数字文字の除去

問題の説明

文字列s内のすべての数字文字を除去する関数del_digitを実装してください。例えば、引数として"AB1C9"を受け取ったら、その文字列を"ABC"に更新するようにしてください。

期待される結果

文字列を入力してください:A1B2C3
変換前:A1B2C3
変換後:ABC

ヒント

  • 文字列操作では、「読み込み位置」と「書き込み位置」を別々に管理する手法がよく使われます
  • 数字文字かどうかは、文字コードの範囲('0'~'9')で判定できます
  • 書き込み位置は、数字以外の文字を見つけた場合にのみ進めます
  • 読み込み位置は、常に1つずつ進めます
  • 処理が終わったら、書き込み位置に必ずヌル文字を置いて、文字列を正しく終端させます
  • 数字文字を除去するので、書き込み位置は常に読み込み位置以下になります
  • 以下はmain関数の全体コードです:
    int main(void)
    {
        char str[128];  // 文字列を格納する配列
    
        printf("文字列を入力してください:");
        scanf("%s", str);
    
        printf("変換前:%s\n", str);
    
        del_digit(str);  // 数字文字を除去
    
        printf("変換後:%s\n", str);
    
        return 0;
    }
    

演習問題11-4:文字列の配列の反転

問題の説明

受け取った文字列の配列に格納されているn個の文字列の文字の並びを反転する関数rev_stringsを実装してください。例えば、引数として{"SEC", "ABC"}を受け取ったら、その配列を{"CES", "CBA"}に更新するようにしてください。

期待される結果

反転前:
cs[0] = "Turbo"
cs[1] = "NA"
cs[2] = "DOHC"

反転後:
cs[0] = "obruT"
cs[1] = "AN"
cs[2] = "CHOD"

ヒント

  • 文字列の配列とは何か:
  • 文字列の配列は2次元配列として表現されます(char cs[][6]
  • 配列csの各要素cs[0]、cs[1]などはそれぞれ一つの文字列です
  • 各文字列には添字を二つ使ってアクセスします(例:cs[0][2]は最初の文字列の3番目の文字)

  • 反転処理の手順:

  • まず、外側のループで文字列を一つずつ選択します(0からn-1まで)
  • 選択された各文字列に対して、内側のループで文字を反転させます
  • 反転は、先頭と末尾の文字を交換し、次に2番目と後ろから2番目の文字を交換...という具合に進めます

  • 文字列の反転アルゴリズム:

  • 文字列の長さを求める必要があります(ヌル文字が見つかるまで計数)
  • 二つのインデックス(i:先頭から、j:末尾から)を使います
  • iがjより小さい間、s[k][i]とs[k][j]を交換し、iを増やしjを減らします
  • 文字列の長さが奇数の場合、中央の文字は交換する必要がありません

  • 実装上の注意点:

  • 文字列の長さを求める際、ヌル文字自体は反転の対象に含めないでください
  • 交換には一時変数(temp)を使うと簡単です
  • 文字列の末尾を示すインデックスjは、文字列の長さ-1です(ヌル文字の直前)
  • このプログラムでは、初期化された文字列の配列を使用しています:

  • 以下はmain関数の全体コードです:

    int main(void)
    {
        char cs[][6] = {"Turbo", "NA", "DOHC"};  // 初期化された文字列の配列
    
        // 反転前の表示
        printf("反転前:\n");
        for (int i = 0; i < 3; i++)
            printf("cs[%d] = \"%s\"\n", i, cs[i]);
    
        // 反転処理
        rev_strings(cs, 3);
    
        // 反転後の表示
        printf("\n反転後:\n");
        for (int i = 0; i < 3; i++)
            printf("cs[%d] = \"%s\"\n", i, cs[i]);
    
        return 0;
    }
    

文字列配列の構造と反転処理の図解

文字列配列の構造

cs[0]: [T][u][r][b][o][\0]      <- 1つ目の文字列 "Turbo"
cs[1]: [N][A][\0][\0][\0][\0]   <- 2つ目の文字列 "NA"
cs[2]: [D][O][H][C][\0][\0]     <- 3つ目の文字列 "DOHC"

反転後の状態

cs[0]: [o][b][r][u][T][\0]      <- "obruT"
cs[1]: [A][N][\0][\0][\0][\0]   <- "AN"
cs[2]: [C][H][O][D][\0][\0]     <- "CHOD"

文字列の反転処理の流れ("Turbo"の例)

  1. 初期状態: [T][u][r][b][o][\0]
  2. i=0, j=4 (先頭と末尾)
  3. 交換: [o][u][r][b][T][\0]

  4. 次のステップ: i=1, j=3

  5. 交換: [o][b][r][u][T][\0]

  6. 最終ステップ: i=2, j=2

  7. i==j なので交換は不要(中央の文字)
  8. 結果: [o][b][r][u][T][\0] <- これが "obruT" となる