C言語の基本|文字列をポインタで操作する

先頭から終端までポインタで歩けば、文字列の一文字一文字が見えてくる。

C言語の文字列は、文字列専用の大きなデータ型として保存されているわけではありません。char 型の文字がメモリ上に順番に並び、その最後に文字列の終わりを表すナル文字 \0 が置かれています。

たとえば、文字列 DOG は、D、O、G、\0 という4つのchar型要素で構成されます。画面に表示されるのはDOGの3文字ですが、メモリ上では終端を示す\0も含めた4要素が必要です。

文字列の実体がchar型配列であるなら、配列で学んだポインタの考え方をそのまま利用できます。ポインタを文字列の先頭へ合わせ、現在の文字を*pで調べ、p++で次の文字へ進めば、文字列を一文字ずつ処理できます。

この方法を使うと、次のような処理を組み立てられます。

  • 英字の大文字と小文字を変換する
  • 特定の文字だけを別の文字へ置き換える
  • 文字数を数える
  • 文字列を先頭から順番に読む
  • \0を見つけたところで処理を終了する

文字列を一つのまとまりとして眺めるだけでなく、ポインタがどの文字を指しているのかを追えるようになると、処理の流れがぐっと見やすくなります。C言語の文字列関数が内部でどのように文字をたどっているのかを理解するためにも、大切な考え方です。

文字列はchar型配列でできている

C言語では、文字列はchar型配列として表します。次の宣言を見てみましょう。

char animal[] = "DOG";

文字列として見えるのはDOGの3文字ですが、配列animalの中には次の4要素が並びます。

配列要素格納される文字役割
animal[0]'D'1文字目
animal[1]'O'2文字目
animal[2]'G'3文字目
animal[3]'\0'文字列の終端

\0はナル文字と呼ばれ、文字列がどこで終わるのかを示します。文字列処理では、文字数を別の変数へ保存せず、\0を見つけるまで文字を読み進める方法がよく使われます。

ここで、文字の'0'とナル文字の\0を混同しないようにしましょう。'0'は画面に表示できる数字の文字ですが、\0は数値0の文字コードを持つ終端用の文字です。

表記意味
'0'数字の0を表す文字
'\0'文字列の終端を表すナル文字

文字列をポインタで処理するときは、先頭文字から始めて\0へ到達したら終了する、という流れが基本になります。

ポインタで文字列の先頭を指す

char型配列として用意した文字列は、char *型のポインタで指せます。

char animal[] = "DOG";
char *p = animal;

配列名animalは、多くの式の中で先頭要素を指すポインタへ変換されます。そのため、char p = animal;とすると、pはanimal[0]の場所を指します。最初にpを参照した結果は'D'です。

ポインタ式と配列要素の対応は、次のようになります。

ポインタ式対応する配列要素中身
*panimal[0]'D'
*(p + 1)animal[1]'O'
*(p + 2)animal[2]'G'
*(p + 3)animal[3]'\0'

pは文字列全体を内部に保存しているのではありません。pに保存されているのは、先頭文字が置かれている場所です。その場所を基準にして、p + 1やp++によって次の文字へ進みます。

図1:ポインタが文字列の先頭文字を指す仕組み

この図から分かること

文字列DOGは、メモリ上ではD、O、G、\0という4つのchar型要素として並びます。pには先頭要素animal[0]のアドレスが保存されるため、*pで'D'を参照できます。文字列の処理はこの先頭位置から始まり、最後の\0まで順番に進みます。

ポインタで文字列を操作する基本の流れ

ポインタを使った文字列操作は、次の3段階に整理できます。

手順内容
1char型配列として文字列を用意するchar word[] = "DOG";
2char *型のポインタを先頭へ合わせるchar *p = word;
3\0まで一文字ずつ処理するwhile (*p != '\0')

文字列をたどるループの基本形は、次のとおりです。

while (*p != '\0') {
    /* pが指す文字を処理する */
    p++;
}

条件式の*pは、pが現在指している文字です。*p != '\0'は、現在の文字が終端ではないことを表します。ループ内の処理を終えたらp++を実行し、pを次のchar型要素へ進めます。

処理の順番は次のようになります。

  1. *pで現在の文字を確認する
  2. 必要な処理や書き換えを行う
  3. p++で次の文字へ進む
  4. *pが\0になるまで繰り返す

\0を指した時点で条件が偽になるため、ナル文字そのものは通常の文字として処理されません。

*pで現在の文字を参照・変更する

pは現在見ている文字の場所を表し、*pはその場所に格納されている文字を表します。この違いを整理しておきましょう。

書き方意味
p現在の文字が置かれている場所
*p現在の場所にある文字
p++ポインタを次の文字へ進める
*p = 'X';現在の文字を'X'へ変更する

たとえば、pがword[2]を指しているときに*p = 'X';を実行すると、word[2]の文字が'X'へ書き換わります。ポインタpの保存内容を文字'X'へ変更しているのではなく、pが指している場所の中身を変更しています。

大文字を小文字へ変換する

最初のプログラムでは、ポインタで文字列を一文字ずつたどり、英大文字を英小文字へ変換します。小文字や空白、記号はそのまま残します。

英大文字だけを英小文字へ変換するプログラム

ファイル名:11_6_1.c

#include <stdio.h>

int main(void)
{
    char message[] = "C Pointer LESSON!";
    char *p = message;   /* 文字列の先頭を指すポインタ */

    printf("変換前: %s\n", message);

    while (*p != '\0') {                     /* ナル文字まで繰り返す */
        if (*p >= 'A' && *p <= 'Z') {        /* 英大文字かを調べる */
            *p += ('a' - 'A');               /* 英小文字へ変換する */
        }

        p++;                                 /* 次の文字へ進む */
    }

    printf("変換後: %s\n", message);

    return 0;
}
実行結果の例
変換前: C Pointer LESSON!
変換後: c pointer lesson!

このプログラムでは、messageを次のchar型配列として用意しています。

char message[] = "C Pointer LESSON!";

配列の末尾には、ソースコードに直接書いていなくても自動的に\0が追加されます。続くchar *p = message;によって、pは先頭文字'C'を指します。

ループ条件は次の部分です。

while (*p != '\0')

現在の文字が\0でなければ、まだ処理する文字が残っています。ループ内では、次の条件で*pが英大文字の範囲にあるかを調べます。

if (*p >= 'A' && *p <= 'Z')

この条件は、*pが'A'以上であり、同時に'Z'以下である場合に真になります。空白、英小文字、感嘆符などは条件に当てはまらないため、変更されません。

英大文字だった場合は、次の式で英小文字へ変換します。

*p += ('a' - 'A');

ASCIIコードでは、対応する英大文字と英小文字のコード値には一定の差があります。'a' - 'A'はその差を表し、英大文字へ加えることで対応する英小文字に変換できます。

計算の例結果
'a' - 'A'大文字と小文字のコード値の差
'C' + ('a' - 'A')'c'
'L' + ('a' - 'A')'l'

この方法はASCII互換の文字コードを前提とした学習例です。ここでは標準ライブラリの文字変換関数を使わず、ポインタで文字を調べて書き換える仕組みに注目します。

文字を処理したあとはp++を実行します。char型は1バイトなので、char *型のpは次のchar型要素へ進みます。この処理を繰り返すと、pは先頭の'C'から始まり、空白や記号も含めて一文字ずつ確認し、最後に\0へ到達します。

ポインタが文字列を変換する流れ

プログラム内でpがどのように進むのか、一部の文字を表で確認してみましょう。

pが指す文字大文字か処理後の文字次の動き
'C'はい'c'p++で次へ進む
空白いいえ空白のままp++で次へ進む
'P'はい'p'p++で次へ進む
'o'いいえ'o'のままp++で次へ進む
'L'はい'l'p++で次へ進む
'!'いいえ'!'のままp++で次へ進む
'\0'処理しない変更しないループ終了

pは文字列を眺めるだけではありません。*pへ新しい文字を代入することで、message配列の要素そのものを書き換えています。そのため、ループ後にmessageを%sで表示すると、変更後の文字列が出力されます。

図2:ポインタを進めながら文字を変換する流れ

この図から分かること

pは先頭文字から右方向へ進み、*pで現在の文字を確認します。英大文字に当てはまる'C'や'P'は、*pを通して英小文字へ書き換えられます。すでに小文字である文字や空白は変更されません。pが\0を指すとループが終了するため、文字列の範囲内だけを順番に処理できます。

文字列処理で\0が重要な理由

通常の配列では、要素数を使ってfor文を繰り返すことがよくあります。一方、文字列は長さが毎回同じとは限らないため、終端に置かれた\0を使って処理の終了位置を判断できます。

while (*p != '\0') {
    /* 文字の処理 */
    p++;
}

このループは、現在の文字が終端ではない間だけ処理を続けます。処理の対象は先頭文字から\0の直前までです。

たとえば、char word[] = "SUN";であれば、pが指す位置は次のように変わります。

繰り返しpが指す要素*p判定
1word[0]'S'処理する
2word[1]'U'処理する
3word[2]'N'処理する
ループ終了時word[3]'\0'処理を終了する

p++を実行する場所も重要です。p++を忘れると、pは同じ文字を指し続けるため、条件が変化せず無限ループになる可能性があります。反対に、\0を確認せずにp++を続けると、文字列の外側へ進んでしまいます。

添字とポインタによる文字列操作の違い

文字列はchar型配列なので、添字を使ってもポインタを使っても一文字ずつ参照できます。

たとえば、次の文字列を考えてみましょう。

char word[] = "CODE";
char *p = word;

対応関係は次のとおりです。

文字添字での参照ポインタでの参照
'C'word[0]*p
'O'word[1]*(p + 1)
'D'word[2]*(p + 2)
'E'word[3]*(p + 3)
'\0'word[4]*(p + 4)

添字を使う場合は、word[i]のiを増やして位置を指定します。ポインタを使う場合は、*(p + i)で先頭からi個先を参照するか、p++でp自身を次の文字へ進めます。

方法現在位置の表し方特徴
添字word[i]配列の先頭と位置番号を使う
ポインタ演算*(p + i)pを移動せず、式の中で位置をずらす
ポインタの更新*pとp++p自身を一文字ずつ進める

どの方法も、同じchar型配列の要素を見ています。添字とポインタは対立する仕組みではなく、同じ文字列を異なる書き方で操作する方法です。

特定の文字を別の文字へ置き換える

次は、文字列をポインタでたどり、指定した文字だけを書き換えます。文字列tomorrowに含まれる'o'を'@'へ置き換えてみましょう。

特定の文字を記号へ置き換えるプログラム

ファイル名:11_6_2.c

#include <stdio.h>

int main(void)
{
    char text[] = "tomorrow";
    char target = 'o';
    char replacement = '@';
    char *p = text;   /* 文字列の先頭を指すポインタ */

    printf("置換前: %s\n", text);

    while (*p != '\0') {             /* ナル文字まで繰り返す */
        if (*p == target) {           /* 置換対象の文字か調べる */
            *p = replacement;         /* 指している文字を書き換える */
        }

        p++;                          /* 次の文字へ進む */
    }

    printf("置換後: %s\n", text);

    return 0;
}
実行結果の例
置換前: tomorrow
置換後: t@m@rr@w

targetには探したい文字'o'、replacementには置換後の文字'@'を保存しています。pはtext[0]の't'を指した状態から始まります。

ループ内の条件は次の部分です。

if (*p == target)

*pとtargetが同じなら、現在位置に'o'があることを表します。その場合は、次の代入によって配列要素を書き換えます。

*p = replacement;

文字が一致しない場合は何も変更せず、そのままp++で次へ進みます。tomorrowには'o'が3つ含まれるため、それぞれが'@'へ置き換わり、結果はt@m@rr@wになります。

処理の一部を表にすると、次のようになります。

元の文字targetと一致するか処理後の文字
't'いいえ't'
'o'はい'@'
'm'いいえ'm'
'o'はい'@'
'r'いいえ'r'
'o'はい'@'
'w'いいえ'w'

このプログラムでも、pは文字を読むだけでなく、*pへの代入によってtext配列の内容を直接変更しています。文字の判定条件やreplacementの値を変えれば、ほかの文字列や置換処理にも応用できます。

図3:添字とポインタで同じ文字列を参照する仕組み

この図から分かること

word[0]とp、word[1]と(p + 1)のように、添字表記とポインタ表記は同じ配列要素を参照します。添字は位置番号を使い、ポインタは先頭からの移動量を使うという見方の違いです。どちらの場合も\0が文字列の終端であり、その先を文字列として処理してはいけません。

文字列を書き換えられる形を使う

今回の2つのプログラムでは、文字列を次のようなchar型配列として用意しています。

char message[] = "C Pointer LESSON!";
char text[] = "tomorrow";

この形では、各文字が配列要素として確保されるため、*pを通して内容を書き換えられます。

学習するときは、次の関係を意識すると理解しやすくなります。

  • 配列が文字を保存する領域を持つ
  • pは配列内の現在位置を指す
  • *pは現在位置の文字を表す
  • *pへ代入すると配列の文字が変更される

文字列を変更する処理では、書き換えてよい領域をポインタが指していることが必要です。この記事の例のように、まずはchar型配列として文字列を用意する形で練習すると、ポインタによる変更を確認しやすくなります。

ポインタで文字列を操作するときの注意点

ポインタによる文字列操作は便利ですが、現在位置と終端を正しく管理する必要があります。

\0を越えて処理しない

p++を繰り返して\0の先まで進み、そこを*pで参照したり書き換えたりすると、文字列の範囲外へアクセスする危険があります。ループでは必ず終端条件を用意します。

while (*p != '\0')

この条件によって、先頭文字から\0の直前までを処理できます。

p++を忘れない

while文の中でp++を忘れると、pは同じ文字を指し続けます。*pが\0以外のまま変わらなければ、ループが終わらなくなります。一文字の処理を終えたら、適切な位置でpを次へ進めましょう。

条件に合う文字だけを書き換える

文字変換や置換では、すべての文字を無条件に変更するのではなく、if文で対象を確認します。

if (*p >= 'A' && *p <= 'Z')
if (*p == target)

空白や記号など、条件に合わない文字は変更せずに次へ進みます。

ポインタを進めると先頭位置が変わる

p++を使うと、p自身が指す位置は変化します。処理後にも文字列の先頭を指すポインタが必要なら、先頭を保存するポインタと走査用のポインタを分ける方法があります。

char *first = text;
char *current = first;

currentだけを進めれば、firstは先頭位置を指したまま残せます。今回のプログラムでは配列名messageやtextをprintfへ渡しているため、pが移動したあとでも配列名を使って文字列全体を表示できます。

ポインタで文字列をたどるために覚えておきたいこと

覚えておきたいこと内容
文字列の実体char型の文字が並んだ配列
文字列の終端最後に\0が置かれる
char *p = text;pがtextの先頭文字を指す
*ppが現在指している文字を参照する
*p = 別の文字;現在の文字を書き換える
p++pを次のchar型要素へ進める
while (*p != '\0')文字列の終端まで処理する

文字列をポインタで扱うときは、pが今どの文字を指しているのか、*pで何を読み書きしているのか、いつ\0へ到達するのかを順番に追うことが大切です。

char型配列の箱を横一列に描き、最後に\0を置いてから、pの矢印を一つずつ右へ動かしてみると理解しやすくなります。ポインタを文字列の中を歩く案内役として考えれば、文字の参照、変換、置換という一連の処理を自然に読み取れるようになります。