C言語のきほん|ポインタで文字列を操作する

文字を1つずつたどれるようになると、文字列操作がぐっと身近になる。

C言語では、文字列は特別な巨大なデータ型として扱われるのではなく、char 型の文字が並んだ配列として扱われます。
たとえば Hello のような文字列も、実際には H、e、l、l、o、そして終わりを表す \0 が順番に並んでいるデータです。

ここで大切になってくるのがポインタです。

これまで、ポインタを使って変数を指したり、配列を順番にたどったりしてきました。
文字列も中身は char 型の配列なので、同じ考え方でポインタを使って1文字ずつ操作することができます。

この考え方がわかると、文字列の処理が一気に見やすくなります。
文字列全体をぼんやり眺めるのではなく、ポインタが先頭文字を指し、そこから次の文字、その次の文字へと進みながら処理していく流れが見えてくるからです。

たとえば、

  • 大文字を小文字に変える
  • 小文字を大文字に変える
  • 特定の文字だけを置き換える
  • 文字数を数える
  • 文字列の終わりまで順番に読む

といった処理は、ポインタで文字列をたどる考え方ととても相性がよいです。

しかも文字列では、配列と同じように添字で扱うこともできますが、C言語ではポインタを使った書き方もとてもよく登場します。
標準ライブラリの文字列関数の考え方にもつながっていくので、ここでしっかり慣れておくことが大切です。

ここでは、ポインタで文字列を操作するというテーマについて、

  • 文字列が char 型配列であること
  • ポインタが文字列の先頭を指すこと
  • \0 まで順番に進みながら処理すること
  • ポインタを使って文字を書き換えること

を、図や表も交えながら、やさしく丁寧に整理していきます。

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

まず、いちばん大事な土台から確認しておきましょう。
C言語の文字列は、char 型の配列です。

たとえば次のような宣言があるとします。

char text[] = "CAT";

これは見た目には 3文字の文字列に見えますが、実際のメモリ上では次の4つの要素が並んでいます。

要素中身
text[0]'C'
text[1]'A'
text[2]'T'
text[3]'\0'

最後の \0 は、文字列の終わりを表す特別な文字で、ナル文字と呼ばれます。
C言語の文字列処理では、この \0 がとても重要です。
なぜなら、文字列の長さを別に保存していないことが多く、文字列の終わりは \0 を見て判断するからです。

つまり、ポインタで文字列を操作するときは、
先頭文字から順番にたどっていき、\0 に着いたら終わり
という流れになります。

ポインタで文字列を指すとは

文字列が char 型配列であるなら、配列をポインタで指す考え方がそのまま使えます。

たとえば次のように書けます。

char text[] = "CAT";
char *p = text;

このとき p は、text の先頭要素、つまり text[0] を指しています。
言い換えると、p は文字 'C' が入っている場所を指していることになります。

そして、

  • *p は text[0]
  • *(p + 1) は text[1]
  • *(p + 2) は text[2]

に対応します。

表にすると次のようになります。

ポインタ式対応する要素中身
*ptext[0]'C'
*(p + 1)text[1]'A'
*(p + 2)text[2]'T'
*(p + 3)text[3]'\0'

このように、ポインタを使うと文字列の中を1文字ずつ順番に見ていけます。

図:ポインタは文字列の先頭文字を指す

この図では、文字列 text の各文字が横一列に並び、C、A、T、\0 が順番に配置されています。
ポインタ p は先頭要素である text[0] を指しており、そこから文字列の走査が始まります。

この図のポイントは、文字列も配列なので、ポインタは先頭文字の位置を覚えているということです。
そこから p++ で次の文字へ進み、最後に \0 に到達したら処理を終える、という流れが作れます。

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

文字列をポインタで操作するときの流れは、とてもシンプルです。

  1. char 型配列として文字列を用意する
  2. その先頭要素を指す char * 型のポインタを用意する
  3. \0 に到達するまで、1文字ずつ処理していく

表で整理すると、次のようになります。

手順内容
1文字列を用意するchar word[] = "CAT";
2ポインタを先頭に合わせるchar *p = word;
3文字を順番に調べるwhile (*p != '\0')

ここで大切なのは、ループの条件です。

while (*p != '\0')

は、
今 p が指している文字が \0 ではない間だけ処理を続ける
という意味です。

この書き方は、文字列処理ではとてもよく出てきます。

サンプルプログラム

元のサンプルでは大文字を小文字に変換していましたが、ここでは別のシンプルな例に変更してみましょう。
今回は、小文字を大文字に変換するプログラムにします。
表示メッセージも別の日本語にし、コメントもすべて日本語で書き換えます。

ファイル名:11_6_1.c

#include <stdio.h>

int main(void)
{
    char message[] = "good Morning!";
    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;
}

このプログラムで起きていること

このプログラムでは、まず char 型配列 message に文字列を入れています。

char message[] = "good Morning!";

この文字列はメモリ上に、g、o、o、d、空白、M、o、r、n、i、n、g、!、\0 のように並んでいます。

次に、

char *p = message;

によって、ポインタ p は文字列の先頭文字を指します。
つまり、最初は 'g' を指している状態です。

そのあとで、

while (*p != '\0')

という条件で、文字列の終わりに着くまで1文字ずつ処理しています。

ループの中では、

if (*p >= 'a' && *p <= 'z')

で、その文字が小文字かどうかを調べています。
もし小文字なら、

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

によって、大文字へ変換しています。

ここで行っているのは、ASCII コードにおける
小文字と大文字の差
を利用した変換です。

最後に p++ を行って、次の文字へ進みます。
これを \0 に着くまで繰り返すことで、文字列全体を順番に変換できます。

実行イメージ

このプログラムの出力は、たとえば次のようになります。

変換前の文字列: good Morning!
変換後の文字列: GOOD MORNING!

ここで注目したいのは、message の内容そのものが書き換わっていることです。
つまり、ポインタ p は単に文字を見ているだけではなく、指している先の文字を直接変更しているわけです。

なぜ *p で文字を変更できるのか

p は、文字列 message の中のある1文字を指しています。
そして *p は、その場所にある文字そのものを表します。

そのため、

*p = 'X';

のように書けば、p が指している位置の文字を X に変更できます。

今回のプログラムでは、

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

としているので、p が指している位置の小文字を大文字に変換しています。

この関係を整理すると、次のようになります。

書き方意味
p今見ている文字の場所
*pその場所にある文字
p++次の文字へ進む
*p = 別の文字今見ている文字を書き換える

この流れがわかると、ポインタによる文字列操作がかなり見やすくなります。

ASCII コードの差を使う考え方

今回の変換では、

('a' - 'A')

を使っています。
これは、小文字 a と大文字 A の文字コードの差を表しています。

ASCII コードでは、英字の大文字と小文字は一定の差で並んでいます。
そのため、小文字からその差を引けば大文字になり、大文字にその差を足せば小文字になります。

たとえば、

文字文字コードのイメージ
'A'65
'a'97

なので、

計算結果
'a' - 'A'32
'c' - 32'C'
'M' + 32'm'

となります。

ここではまだ標準ライブラリの islower や toupper を使わず、文字コードの差を使って仕組みを学ぶことが目的です。
この方法は、文字列操作の基本を理解するうえでとてもよい練習になります。

図:ポインタを進めながら文字列を1文字ずつ変換する

この図では、文字列 message の各文字が横に並び、ポインタ p が左から右へ順番に進んでいく様子を示します。
p は各位置でその文字を調べ、小文字なら大文字に書き換えます。最後に \0 に到達したところで処理を終了します。

この図のポイントは、ポインタがただ文字列を眺めているのではなく、1文字ずつ実際に中身を確認し、必要なら書き換えているところです。
文字列処理のループの流れが視覚的に理解しやすくなります。

文字列処理では \0 が特に大切

配列の学習では、要素数を決めて for 文で回すことが多かったですね。
でも文字列では、\0 に出会うまで処理する形がよく使われます。

これは、文字列の長さが毎回固定とは限らないからです。
そのため、文字列の終わりを判断する目印として \0 を使います。

たとえば、

while (*p != '\0')

は、文字列操作の基本形です。

これを少しくだけて言うと、

今見ている文字が終わりの印でないなら、まだ続きがある

ということです。

文字列をポインタで操作するときは、
先頭から始めて、\0 に着いたら終わる
という流れをまずしっかりつかむことが大切です。

配列の添字を使う方法との違い

文字列は、もちろん添字でも扱えます。
たとえば次のように書けます。

message[0]
message[1]
message[2]

一方、ポインタなら

*p
*(p + 1)
*(p + 2)

のように表せます。

対応関係は次の通りです。

添字での書き方ポインタでの書き方
message[0]*p
message[1]*(p + 1)
message[2]*(p + 2)

さらに、ポインタを進めながら処理するなら、

  • 最初は p が先頭を指す
  • p++ で次の文字へ進む
  • *p で今の文字を見る

という流れになります。

添字による方法とポインタによる方法は、見た目は違っても、同じ文字列データを違う角度から扱っているだけです。
C言語では両方とも大切ですが、ポインタの流れに慣れると、文字列関数の仕組みも理解しやすくなります。

図:添字による参照とポインタによる参照の対応

この図では、文字列 word の各要素と、それに対応する添字表記とポインタ表記を並べて示します。
たとえば word[0] と *p、word[1] と *(p + 1)、word[2] と *(p + 2) を対応づけることで、同じ文字列を2つの方法で見ていることがわかります。

この図のポイントは、添字とポインタが対立するものではなく、同じ文字列データを別の書き方で操作しているという点です。

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

文字列をポインタで操作するときには、便利さと同時に注意点もあります。

\0 を越えて進まない

p++ を続けて \0 の先まで進んでしまうと、文字列の外を読んだり書いたりする危険があります。
そのため、必ず \0 を条件にして終わるようにすることが大切です。

書き換えてよい文字列かを意識する

今回の例では

char message[] = "good Morning!";

のように、文字列を配列として用意しているので中身を書き換えられます。
学習の初期では、まずこの形で理解するのが安心です。

文字コードを使った変換は仕組み理解に向いている

実際の開発では文字変換用の関数を使うことも多いですが、今はまだポインタで1文字ずつ処理する仕組みを理解することが大切です。
文字コードの差を使う方法は、その仕組みを見える形で学べるよい練習になります。

もう1つの簡単な応用例

ポインタで文字列を操作できるようになると、変換以外の処理も書けます。
たとえば、特定の文字を見つけて別の文字に変えることもできます。

ファイル名:11_6_2.c

#include <stdio.h>

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

    printf("変更前: %s\n", text);

    while (*p != '\0') {
        if (*p == 'a') {   /* a を見つけたら */
            *p = '*';      /* * に置き換える */
        }
        p++;
    }

    printf("変更後: %s\n", text);

    return 0;
}

この例では、a を見つけるたびに * に置き換えています。
このように、ポインタで1文字ずつ見ながら条件に応じて書き換えるという考え方は、文字列処理のいろいろな場面に応用できます。

ポインタで文字列を操作する感覚をつかもう

ここまでの内容で、まずしっかり押さえたいポイントは次の通りです。

覚えたいこと内容
文字列は char 型配列文字が並び、最後に \0 がある
ポインタは先頭文字を指せるchar *p = str; の形で使える
*p は今見ている文字その文字を参照したり変更したりできる
p++ で次の文字へ進む1文字ずつ順番にたどれる
\0 で終わりを判断する文字列処理の基本条件になる

この感覚がつかめると、文字列は「ただの文字の集まり」ではなく、
メモリ上に順番に並んだ文字を、ポインタで1つずつ歩きながら処理するものとして見えるようになります。

ここが見えてくると、C言語の文字列処理が急に理解しやすくなります。
ポインタは難しい記号ではなく、文字列の中を進みながら中身を調べたり書き換えたりするための、とても実用的な道具だと感じられるようになります。