C言語の基本|制御文で文字列を処理する

文字列は、1文字ずつ調べれば動きが見えてくる。制御文を組み合わせて、数える・比べる・探す・置き換える処理を身につけましょう。

制御文で文字列を処理する

C言語の文字列は、char型の配列として扱われます。そのため、配列の添字を使えば、文字列の中にある文字を1つずつ取り出して調べられます。

文字列を扱うプログラムでは、次のような処理がよく登場します。

  • 文字列を先頭から順番に調べる
  • 特定の文字が何個あるか数える
  • 文字列の長さを求める
  • 先頭文字と最後の文字を比較する
  • 複数の文字列から条件に合うものを探す
  • 特定の文字を別の文字へ置き換える

こうした処理では、for文やwhile文で文字列をたどり、if文で文字の内容を判定するのが基本です。

C言語の文字列には、終わりを示すナル文字 \0 が入っています。ループでは、現在の文字が \0 ではない間だけ処理を続ける形がよく使われます。

for (int i = 0; text[i] != '\0'; i++) {
    /* text[i]を処理する */
}

この基本形を理解すると、文字列をただの文章ではなく、順番に並んだ文字の配列として考えられるようになります。

文字列は1文字ずつ取り出せる

次の文字列を考えてみましょう。

char word[] = "Hello";

wordの内部には、次の値が並んでいます。

添字役割
0H1文字目
1e2文字目
2l3文字目
3l4文字目
4o5文字目
5\0文字列の終端

文字列全体はwordで表せますが、個々の文字はword[0]やword[1]のように添字を付けて取り出します。

たとえば、次のコードでは先頭文字Hを表示できます。

printf("%c\n", word[0]);

3文字目を表示したい場合は、添字2を指定します。

printf("%c\n", word[2]);

C言語の配列では添字が0から始まるため、3文字目の添字は2になります。

\0まで処理する理由

C言語の文字列は、\0によって終わりが示されます。

Helloを先頭から調べる場合、処理は次のように進みます。

  • word[0]のHを調べる
  • word[1]のeを調べる
  • word[2]のlを調べる
  • word[3]のlを調べる
  • word[4]のoを調べる
  • word[5]の \0 を見つける
  • 文字列の処理を終了する

この流れをfor文で表すと、次の形になります。

for (int i = 0; word[i] != '\0'; i++) {
    printf("%c\n", word[i]);
}

継続条件にあるword[i] != '\0'は、現在の文字が終端文字ではない間だけ繰り返すという意味です。

\0そのものは、通常の文字として処理しません。ループ条件を判定するための終了の印として使います。

図1:文字列を先頭から \0 まで調べる流れ

この図から分かること

文字列を処理するときは、添字を0から始めて1ずつ増やします。

現在の文字が \0 ではない間は、その文字に対する処理を行います。\0が見つかると、そこが文字列の終わりなのでループを終了します。

この基本の流れにif文を加えると、特定の文字を数えたり、条件に合う文字だけを変更したりできます。

文字列処理で使う主な制御文

文字列処理では、複数の制御文を役割に応じて組み合わせます。

制御文主な役割文字列処理の例
for文添字を変化させながら繰り返す先頭から1文字ずつ調べる
while文条件が成り立つ間繰り返す\0まで処理する
if文条件に合うか判定する現在の文字がaか調べる
if~else文2つの処理に分ける先頭と末尾が同じか判定する
if~else if~else文複数条件で分ける英字、数字、その他を分類する

文字列全体を走査するときはfor文またはwhile文を使い、その中でif文による文字の判定を行う形がよく使われます。

特定の文字を数える基本形

文字列の中に特定の文字が何個あるか数える場合は、次の流れで考えます。

  • カウンタを0で初期化する
  • 文字列の先頭から \0 まで調べる
  • 現在の文字が目的の文字と同じか判定する
  • 同じならカウンタを1増やす
  • 最後にカウンタを表示する

基本形は次のとおりです。

int count = 0;

for (int i = 0; text[i] != '\0'; i++) {
    if (text[i] == 調べる文字) {
        count++;
    }
}

ループが文字列全体を担当し、if文が1文字ごとの判定を担当します。

文字列内にiが何個あるか数える

入力文字列に含まれるiの個数を数えるプログラム

ファイル名:10_14_1.c

#include <stdio.h>

int main(void)
{
    char text[100];
    int count_i = 0;

    printf("文字列を入力してください(最大99文字)> ");
    scanf("%99s", text);

    /* 文字列を先頭から順番に調べる */
    for (int i = 0; text[i] != '\0'; i++) {
        if (text[i] == 'i') {
            count_i++;
        }
    }

    printf("文字iは%d個含まれています。\n", count_i);

    return 0;
}
実行結果の例
文字列を入力してください(最大99文字)> programming
文字iは1個含まれています。

入力部分の解説

文字列を保存するために、100要素のchar配列を用意しています。

char text[100];

入力にはscanfを使っています。

scanf("%99s", text);

%99sの99は、最大で99文字を読み込む指定です。textには100要素ありますが、最後の1要素は \0 のために必要です。

項目
配列の要素数100
入力できる最大文字数99
\0に必要な要素数1

文字列をscanfで入力するとき、textの前に&は付けません。配列名textが入力先の先頭位置を表すためです。

なお、%sによる入力は空白の手前で終了します。このプログラムでは、空白を含まない1つの単語を入力するものとします。

ループ部分の解説

for文では、text[0]から順番に文字を調べています。

for (int i = 0; text[i] != '\0'; i++) {

変数iは、現在調べている文字の添字です。

iの値調べる要素
0text[0]
1text[1]
2text[2]
3text[3]

iが増えるたびに、次の文字へ進みます。text[i]が \0 になると条件が偽になり、ループが終了します。

if文による判定

現在の文字がiかどうかは、if文で調べています。

if (text[i] == 'i') {
    count_i++;
}

条件が真になったときだけ、count_iを1増やします。

文字の比較では、比較演算子 == を使います。代入演算子 = と書き間違えないように注意しましょう。

演算子意味
==左右が等しいか比較する
=右側の値を左側へ代入する

文字列の長さを数える考え方

文字列の長さも、\0まで1文字ずつ数えることで求められます。

int length = 0;

for (int i = 0; text[i] != '\0'; i++) {
    length++;
}

たとえばCodeの場合は、次のように進みます。

調べる文字lengthの値
C1
o2
d3
e4
\0ループ終了

\0は文字列の長さに含めません。そのため、Codeの長さは4になります。

標準ライブラリにはstrlenがありますが、自分でループを書いて長さを数えると、C言語の文字列がどのように終端を判断しているか理解しやすくなります。

文字列内にoが何回あるか数える

入力文字列に含まれるoの回数を数えるプログラム

ファイル名:10_14_2.c

#include <stdio.h>

int main(void)
{
    char text[100];
    int count_o = 0;

    printf("文字列を入力してください(最大99文字)> ");
    scanf("%99s", text);

    for (int i = 0; text[i] != '\0'; i++) {
        if (text[i] == 'o') {
            count_o++;
        }
    }

    printf("文字oは文字列中に%d回含まれています。\n", count_o);

    return 0;
}
実行結果の例
文字列を入力してください(最大99文字)> cookbook
文字oは文字列中に3回含まれています。

プログラムの詳しい解説

このプログラムでは、文字列を先頭から順番に調べ、現在の文字がoならcount_oを増やします。

処理の流れは次のとおりです。

手順処理
1文字列を入力する
2添字iを0にする
3text[i]が \0 か調べる
4現在の文字がoならcount_oを増やす
5iを1増やして次の文字へ進む
6最後にcount_oを表示する

cookbookの各文字を調べると、oは添字1、2、5にあります。そのため、最終的なcount_oは3になります。

大文字Oと小文字oは別の文字です。このプログラムが数えるのは小文字oだけです。

先頭文字と最後の文字を比べる

文字列の先頭文字は、添字0で取り出せます。

text[0]

最後の文字を取り出すには、最初に文字列の長さを求めます。

文字列の長さをlengthとすると、最後の文字の添字はlength - 1です。

text[length - 1]

たとえばrobotは5文字なので、最後の文字の添字は4になります。

文字添字
r0
o1
b2
o3
t4
\05

最後の文字はtext[4]であり、text[length - 1]と一致します。

先頭文字と最後の文字が同じか判定する

文字列の先頭文字と最後の文字を比較するプログラム

ファイル名:10_14_3.c

#include <stdio.h>

int main(void)
{
    char text[100];
    int length = 0;

    printf("文字列を入力してください(最大99文字)> ");
    scanf("%99s", text);

    /* 文字列の長さを数える */
    for (int i = 0; text[i] != '\0'; i++) {
        length++;
    }

    /* 先頭文字と最後の文字を比較する */
    if (text[0] == text[length - 1]) {
        printf("先頭の文字と最後の文字は同じです。\n");
    } else {
        printf("先頭の文字と最後の文字は異なります。\n");
    }

    return 0;
}
実行結果の例1
文字列を入力してください(最大99文字)> radar
先頭の文字と最後の文字は同じです。
実行結果の例2
文字列を入力してください(最大99文字)> robot
先頭の文字と最後の文字は異なります。

長さを求める処理

最初のfor文では、文字列の長さを数えています。

for (int i = 0; text[i] != '\0'; i++) {
    length++;
}

radarの場合は5文字なので、ループ終了後のlengthは5です。

最後の文字は、次の要素になります。

text[length - 1]

lengthが5なら、最後の文字はtext[4]です。

if~else文による比較

先頭と最後の文字は、次の条件式で比較しています。

if (text[0] == text[length - 1]) {

同じならif側、異なるならelse側の処理を実行します。

if (条件式) {
    /* 条件が真の場合 */
} else {
    /* 条件が偽の場合 */
}

radarでは先頭と最後がどちらもrなので、同じと判定されます。robotでは先頭がr、最後がtなので、異なると判定されます。

図2:文字列の長さから最後の文字を求める

この図から分かること

文字列の先頭文字はtext[0]です。最後の文字は、文字列の長さから1を引いた位置にあります。

radarの長さは5なので、最後の文字はtext[4]です。text[5]には \0 が入っているため、最後の表示文字ではありません。

先頭と最後を比較するときは、text[0]とtext[length - 1]をif文で判定します。

複数の文字列を調べる

複数の文字列をまとめて扱う場合は、2次元char配列を使えます。

char languages[][11] = {
    "C",
    "Python",
    "JavaScript",
    "Go",
    "Ruby"
};

2次元char配列では、1行が1つの文字列です。

文字列
languages[0]C
languages[1]Python
languages[2]JavaScript
languages[3]Go
languages[4]Ruby

複数の文字列から最も短いものを探すには、外側と内側の2つのループを使います。

ループ役割
外側のループ調べる文字列を選ぶ
内側のループ選んだ文字列の長さを数える

最も短い文字列を探す考え方

最短の文字列を探すときは、最初の文字列を暫定的な最短として扱います。

int min_index = 0;

その後、2つ目以降の文字列を順番に調べます。現在の文字列が、これまでの最短文字列より短ければ、最短の長さと行番号を更新します。

処理の流れは次のとおりです。

  • 1つ目の文字列の長さをmin_lengthへ入れる
  • 2つ目以降の文字列を順番に選ぶ
  • 選んだ文字列の長さを数える
  • min_lengthより短いか判定する
  • 短ければmin_lengthとmin_indexを更新する
  • 全文字列を調べたら結果を表示する

最も短いプログラミング言語名を探す

複数の文字列から最も短い名前を探すプログラム

ファイル名:10_14_4.c

#include <stdio.h>

int main(void)
{
    char languages[][11] = {
        "C",
        "Python",
        "JavaScript",
        "Go",
        "Ruby"
    };

    int min_index = 0;
    int min_length = 0;

    /* 最初の文字列の長さを初期値にする */
    for (int j = 0; languages[0][j] != '\0'; j++) {
        min_length++;
    }

    /* 2つ目以降の文字列と比較する */
    for (int i = 1; i < 5; i++) {
        int length = 0;

        for (int j = 0; languages[i][j] != '\0'; j++) {
            length++;
        }

        if (length < min_length) {
            min_length = length;
            min_index = i;
        }
    }

    printf("最も短い文字列は%sです。長さは%d文字です。\n",
           languages[min_index], min_length);

    return 0;
}
実行結果の例
最も短い文字列はCです。長さは1文字です。

2次元char配列の解説

languagesは、5個の文字列を保存する2次元char配列です。

char languages[][11]

行数は初期値の個数から5になります。列数は11です。

最も長いJavaScriptは10文字なので、末尾の \0 を含めると11要素必要になります。

文字列文字数\0を含む必要サイズ
C12
Python67
JavaScript1011
Go23
Ruby45

最初の文字列を初期値にする理由

最短の長さには、最初の文字列Cの長さを設定しています。

for (int j = 0; languages[0][j] != '\0'; j++) {
    min_length++;
}

最初からmin_lengthへ0や適当な数値を入れると、比較が正しくできない可能性があります。

たとえばmin_lengthを0にすると、文字列の長さは通常0より小さくならないため、更新されません。

実際に配列へ入っている最初の文字列を基準にすると、データに合わせて安全に比較を始められます。

二重ループの動き

外側のfor文では、調べる文字列を選んでいます。

for (int i = 1; i < 5; i++) {

内側のfor文では、選んだ文字列の長さを数えています。

for (int j = 0; languages[i][j] != '\0'; j++) {
    length++;
}

添字iは行番号、添字jは行内の文字位置です。

添字役割
iどの文字列を調べるか
j文字列内のどの文字を調べるか
languages[i]i行目の文字列
languages[i][j]i行目のj文字目

最短値の更新

現在の文字列がこれまでの最短より短い場合は、最短の情報を更新します。

if (length < min_length) {
    min_length = length;
    min_index = i;
}

min_lengthには最短の文字数、min_indexには最短文字列の行番号を保存します。

最後は、min_indexを使って該当する文字列を表示します。

languages[min_index]

文字列内の文字を置き換える

char配列に格納された文字は、添字を使って変更できます。

text[i] = new_ch;

文字列内の特定文字をすべて置き換える場合は、次のように考えます。

  • 置換前の文字を入力する
  • 置換後の文字を入力する
  • 文字列を先頭から \0 まで調べる
  • 現在の文字が置換前の文字と同じか判定する
  • 同じなら置換後の文字を代入する

基本形は次のとおりです。

for (int i = 0; text[i] != '\0'; i++) {
    if (text[i] == old_ch) {
        text[i] = new_ch;
    }
}

if文が真になった位置だけが書き換えられます。それ以外の文字は変化しません。

文字入力で先頭に空白を置く理由

文字の入力には、次の形を使います。

scanf(" %c", &old_ch);

%cの前にある空白は、入力バッファに残っている改行や空白を読み飛ばすためのものです。

文字列を入力したあとに文字を入力すると、直前のEnterによる改行が残っている場合があります。先頭に空白を置くことで、その改行を読み飛ばして次の文字を受け取れます。

入力形式動き
%c次の1文字をそのまま読む
空白付きの %c空白や改行を読み飛ばしてから1文字読む

指定した文字を別の文字へ置き換える

文字列内の指定文字をすべて置き換えるプログラム

ファイル名:10_14_5.c

#include <stdio.h>

int main(void)
{
    char text[200];
    char old_ch;
    char new_ch;

    printf("文字列を入力してください(最大199文字)> ");
    scanf("%199s", text);

    printf("置き換える文字を入力してください> ");
    scanf(" %c", &old_ch);

    printf("新しい文字を入力してください> ");
    scanf(" %c", &new_ch);

    /* 文字列を先頭から調べて一致した文字を置き換える */
    for (int i = 0; text[i] != '\0'; i++) {
        if (text[i] == old_ch) {
            text[i] = new_ch;
        }
    }

    printf("置き換え後の文字列: %s\n", text);

    return 0;
}
実行結果の例1
文字列を入力してください(最大199文字)> coconut
置き換える文字を入力してください> o
新しい文字を入力してください> a
置き換え後の文字列: cacanut
実行結果の例2
文字列を入力してください(最大199文字)> system
置き換える文字を入力してください> z
新しい文字を入力してください> x
置き換え後の文字列: system

プログラムの詳しい解説

textには最大199文字の文字列を入力できます。

char text[200];
scanf("%199s", text);

old_chには置換前の文字、new_chには置換後の文字を入力します。

char old_ch;
char new_ch;

for文では、textの先頭から \0 までを調べています。

for (int i = 0; text[i] != '\0'; i++) {

現在の文字がold_chと同じなら、new_chを代入します。

if (text[i] == old_ch) {
    text[i] = new_ch;
}

coconutでoをaへ置き換える場合、次のように変化します。

添字変更前判定変更後
0c一致しないc
1o一致するa
2c一致しないc
3o一致するa
4n一致しないn
5u一致しないu
6t一致しないt

置換対象が1つも見つからなければ、文字列は変化しません。

置換しても \0 は変更されない

ループ条件では、text[i]が \0 ではない間だけ処理しています。

そのため、文字列の終端である \0 は置換対象になりません。文字列の長さも変わりません。

このプログラムでは、1文字を別の1文字へ置き換えているため、置換前と置換後で文字列の長さは同じです。

図3:文字列を1文字ずつ調べて置き換える

この図から分かること

文字列を先頭から順番に調べ、現在の文字がold_chと一致した場合だけnew_chを代入します。

一致しない文字は変更されません。また、\0に到達するとループが終了するため、終端文字はそのまま残ります。

この処理は、for文による繰り返し、if文による比較、代入による変更を組み合わせた文字列処理です。

sizeof 演算子で大きさを調べる

sizeofは、型、変数、配列などがメモリ上で何バイト使うかを調べる演算子です。

基本的な書き方は次のとおりです。

sizeof(対象)

たとえば、int型の大きさは次のように調べられます。

sizeof(int)

変数の大きさも調べられます。

sizeof(number)

配列にsizeofを使うと、配列全体のバイト数を求められます。

sizeof(numbers)

sizeofの結果にはsize_tを使う

sizeofの結果はsize_t型になります。

size_t size = sizeof(int);

size_tは、メモリサイズや配列要素数など、0以上の大きさを表すために使われる整数型です。

printfでsize_t型の値を表示するときは、書式指定子 %zu を使います。

printf("%zu\n", sizeof(int));

配列の要素数を求める

配列全体のバイト数を、1要素分のバイト数で割ると、配列の要素数を求められます。

sizeof(配列名) / sizeof(配列名[0])

たとえば、int型の配列があるとします。

int numbers[5];

int型が4バイトの環境では、次のようになります。

対象大きさ
numbers全体20バイト
numbers[0]4バイト
20 ÷ 45要素

要素の型や大きさが変わっても、全体を1要素分で割るという考え方は同じです。

型と配列の大きさを確認する

sizeof で型と配列の大きさを表示するプログラム

ファイル名:10_14_6.c

#include <stdio.h>

int main(void)
{
    int values[6] = {10, 20, 30, 40, 50, 60};

    size_t element_count =
        sizeof(values) / sizeof(values[0]);

    printf("char型の大きさ   : %zuバイト\n", sizeof(char));
    printf("int型の大きさ    : %zuバイト\n", sizeof(int));
    printf("double型の大きさ : %zuバイト\n", sizeof(double));
    printf("配列全体の大きさ : %zuバイト\n", sizeof(values));
    printf("配列の要素数     : %zu個\n", element_count);

    return 0;
}
実行結果の例

実行環境によって型の大きさは異なることがあります。次は、int型が4バイト、double型が8バイトの環境での一例です。

char型の大きさ   : 1バイト
int型の大きさ    : 4バイト
double型の大きさ : 8バイト
配列全体の大きさ : 24バイト
配列の要素数     : 6個

プログラムの詳しい解説

valuesは、int型の要素を6個持つ配列です。

int values[6] = {10, 20, 30, 40, 50, 60};

要素数は、次の式で求めています。

sizeof(values) / sizeof(values[0])

int型が4バイトなら、配列全体は24バイトです。

4バイト × 6要素 = 24バイト

配列全体の24バイトを、先頭要素1個分の4バイトで割ります。

24バイト ÷ 4バイト = 6要素

計算結果はsize_t型のelement_countへ代入しています。

size_t element_count =
    sizeof(values) / sizeof(values[0]);

表示にはsize_t型用の %zu を使っています。

sizeofで確認できる内容

書き方求められるもの
sizeof(char)char型の大きさ
sizeof(int)int型の大きさ
sizeof(double)double型の大きさ
sizeof(values)配列values全体の大きさ
sizeof(values[0])配列の先頭要素1個分の大きさ
sizeof(values) / sizeof(values[0])配列の要素数

char型の大きさは、C言語の規則で常に1バイトです。ただし、1バイトが何ビットかや、int型やdouble型が何バイトかは、実行環境によって異なる場合があります。

制御文で文字列を処理するときの注意点

注意点内容
ループの終了条件\0に到達したら終了する
添字の開始位置0から始める
入力サイズ配列サイズから \0 の1要素分を引く
文字列入力scanfと %sでは空白の手前で入力が終わる
文字入力scanfと %cの前に空白を置く場合がある
文字の比較比較演算子 == を使う
文字の置換配列要素へ新しい文字を代入する
最後の文字text[length - 1]で指定する
sizeofの表示%zuを使う

文字列処理を読み解く視点

制御文を使った文字列処理では、次の点を順番に確認すると理解しやすくなります。

  • どのchar配列を処理しているか
  • 添字をどこから始めているか
  • ループがどの条件で終了するか
  • 現在の文字をどのif文で判定しているか
  • 条件成立時に数えるのか、比較するのか、変更するのか
  • 文字列の終端である \0 を壊していないか
  • 配列の範囲を超えていないか

文字列はchar型の配列なので、for文やwhile文で1文字ずつたどれます。その中にif文を組み合わせることで、文字数の計算、特定文字の数え上げ、前後の比較、最短文字列の探索、文字の置換といった処理へ広げられます。