C言語のきほん|ポインタの理解を深める演習

手を動かして解いていくと、ポインタは急にわかりやすくなる。

ポインタは、C言語の中でも特に「難しそう」と感じやすいテーマです。
変数のアドレスを扱ったり、* や & の意味を区別したり、配列や文字列との関係を理解したりと、最初は頭の中で整理しにくいところがいくつもあります。

けれども、ポインタは説明を読むだけではなく、小さな問題を実際に解きながら慣れていくと、かなり理解しやすくなります。
なぜなら、ポインタは「こういう場面でこう使う」という実感が大切だからです。

たとえば、

  • 文字列リテラルをポインタで順番に読む
  • ナル文字 \0 まで進んで文字列を走査する
  • 末尾まで進めてから戻りながら表示する
  • ポインタの配列で複数の文字列を管理する
  • 添字とポインタ式の関係を確かめる

といった演習を通して、ポインタの動きが少しずつ見えてきます。

特に文字列や配列を扱う演習は、ポインタの理解を深めるのにとても向いています。
1文字ずつ読む、1つ先へ進む、複数の文字列の先頭を管理する、といった処理は、ポインタの考え方そのものだからです。

ここでは、ポインタの理解を深める演習として、

  • 文字列リテラルをポインタで調べる実践問題
  • 文字列を逆順にたどる実践問題
  • ポインタの配列を使って複数の文字列を管理する実践問題
  • 複数の文字列を判定するチャレンジ問題
  • 11章の確認問題に○×問題

をまとめて整理していきます。

解答例だけでなく、なぜその書き方になるのかどこがポインタのポイントなのかも丁寧に説明していきます。
演習を通して、ポインタを「暗記するもの」ではなく、「使いながら理解するもの」としてつかんでいきましょう。

演習で意識したいポイント

問題を解く前に、ポインタ学習で特に意識したいポイントを整理しておきます。

ポイント内容
文字列は char 型配列として扱う最後に \0 がある
文字列リテラルは const char * で指す書き換えないことを明確にする
*p は今見ている文字中身を参照する
p++ は次の文字へ進む文字列走査の基本
\0 で終わりを判断する文字列処理でとても重要
ポインタの配列は文字列への入口を並べる複数の文字列管理に便利

このあたりを意識しながら演習を見ると、コードの意味がつかみやすくなります。

図:ポインタ学習でよく出てくる操作の流れ

この図では、1本のポインタが文字列の先頭を指し、\0 まで1文字ずつ進んでいく流れと、ポインタの配列が複数の文字列の先頭をまとめて管理している流れをあわせて示します。
ポインタ学習では、まず1本のポインタで1つの文字列を追いかける感覚を身につけ、そのあとで複数の文字列をポインタの配列で扱う感覚へ広げていくのが自然です。

実践問題

元の pr11_3_1.c と似た考え方で、文字列の中から条件に合う文字だけを抜き出す問題を作ってみましょう。

以下の手順でプログラムを作成してください。

① 任意の文字列リテラルをポインタで指します。
② その文字列リテラルの中に含まれる数字文字だけを探し、見つかった文字を順に表示してください。

数字文字は、0以上かつ9以下で判定してください。

実行結果例

元の文字列: Room 305, Desk 12, Box 7.
含まれる数字: 305127

解答例

ファイル名:11_9_1.c

#include <stdio.h>

int main(void)
{
    const char *p = "Room 305, Desk 12, Box 7.";

    printf("元の文字列: %s\n", p);
    printf("含まれる数字: ");

    while (*p != '\0') {
        if (*p >= '0' && *p <= '9') {
            printf("%c", *p);
        }
        p++;
    }

    printf("\n");

    return 0;
}

解説

この問題では、文字列リテラルを const char * 型のポインタで指しています。

const char *p = "Room 305, Desk 12, Box 7.";

文字列リテラルは基本的に書き換えないので、const を付けるのが適切です。

ループでは、

while (*p != '\0')

として、ナル文字に到達するまで1文字ずつ順番に見ています。
そして、

if (*p >= '0' && *p <= '9')

で、今見ている文字が数字かどうかを判定しています。
条件に合う文字だけ printf で表示しているので、文字列中の数字だけが順に表示されます。

この問題のよいところは、ポインタで文字列を先頭から最後まで走査する流れを、わかりやすく練習できる点です。

図:ポインタで文字列を先頭から順番に調べる

この図では、文字列 Room 305, Desk 12, Box 7. の各文字が並び、ポインタ p が左から右へ進んでいく様子を表します。
数字の文字に到達したときだけ、その文字を取り出して表示する流れを示します。
この図を見ると、ポインタが1文字ずつ順番に文字列を調べていることがはっきりわかります。

実践問題

以下の手順でプログラムを作成してください。

① 任意の文字列リテラルをポインタで指します。
② その文字列リテラルを末尾から先頭へ向かって逆順に表示してください。

いったん \0 までポインタを進めてから、1文字ずつ戻りながら表示してください。

実行結果例

元の文字列: Summer
逆順の文字列: remmuS

解答例

ファイル名:11_9_2.c

#include <stdio.h>

int main(void)
{
    const char *text = "Summer";
    const char *p = text;

    printf("元の文字列: %s\n", text);
    printf("逆順の文字列: ");

    while (*p != '\0') {
        p++;
    }

    p--;

    while (p >= text) {
        printf("%c", *p);
        p--;
    }

    printf("\n");

    return 0;
}

解説

この問題では、まず p を文字列の先頭に合わせています。

const char *p = text;

そのあと、

while (*p != '\0') {
    p++;
}

として、ポインタを文字列の終端まで進めます。
ここで p はナル文字を指しているので、すぐに表示したい最後の文字ではありません。
そこで 1 つ戻して、

p--;

とすることで、最後の文字を指すようにしています。

あとは、

while (p >= text)

で、先頭アドレス以上である間、1文字ずつ表示しながら p-- で戻っていきます。

この問題は、ポインタを前に進めるだけでなく、後ろへ戻して使う感覚を身につけるのにとてもよい練習です。

実践問題

以下の手順でプログラムを作成してください。

① 以下の 1 から 7 までの曜日名をポインタの配列で管理します。

1: Sunday
2: Monday
3: Tuesday
4: Wednesday
5: Thursday
6: Friday
7: Saturday

② 数字を入力し、その番号に対応する曜日名を表示します。
③ 不正な入力があった場合はエラーメッセージを表示します。

配列の添字は 0 から始まるので、1 は 0 番目として扱います。

実行結果例1

曜日番号を入力してください(1〜7) > 5
5番目の曜日はThursdayです。

実行結果例2

曜日番号を入力してください(1〜7) > 8
入力が不正です。1〜7の範囲で入力してください。

解答例

ファイル名:11_9_3.c

#include <stdio.h>

int main(void)
{
    const char *week[7] = {
        "Sunday", "Monday", "Tuesday", "Wednesday",
        "Thursday", "Friday", "Saturday"
    };

    int n;

    printf("曜日番号を入力してください(1〜7) > ");
    scanf("%d", &n);

    if (n >= 1 && n <= 7) {
        printf("%d番目の曜日は%sです。\n", n, week[n - 1]);
    } else {
        printf("入力が不正です。1〜7の範囲で入力してください。\n");
    }

    return 0;
}

解説

この問題では、複数の曜日名をポインタの配列で管理しています。

const char *week[7]

は、文字列を指すポインタが 7 個入る配列です。
それぞれの要素が Sunday や Monday などの文字列リテラルの先頭を指しています。

入力された値 n が 1〜7 の範囲なら、配列の添字に合わせるために 1 を引いて、

week[n - 1]

を参照します。

たとえば n が 5 なら week[4] になるので、Thursday が表示されます。

この問題では、ポインタの配列を使って複数の文字列リテラルを簡潔に管理する考え方が確認できます。

図:ポインタの配列で複数の文字列を管理する

この図では、week[0] から week[6] までの各要素が並んでいて、それぞれが Sunday、Monday、Tuesday などの文字列リテラルを指している様子を示します。
入力値 5 が与えられたときに、week[4] を参照して Thursday を表示する流れがわかるようにすると理解しやすいです。

実践問題

ここでは、複数の文字列に対して先頭文字と末尾文字を比べる応用問題を作ってみましょう。

以下のプログラムを作成してください。

① 次の文字列をポインタの配列で管理します。

"apple", "radar", "banana", "level", "orange"

② 各文字列について、先頭文字と末尾文字が同じであれば、その文字列とともに
「先頭と末尾が同じです」
と表示してください。

同じでなければ、その文字列とともに
「先頭と末尾が異なります」
と表示してください。

実行結果例

apple: 先頭と末尾が異なります。
radar: 先頭と末尾が同じです。
banana: 先頭と末尾が異なります。
level: 先頭と末尾が同じです。
orange: 先頭と末尾が異なります。

解答例

ファイル名:11_9_4.c

#include <stdio.h>

int main(void)
{
    const char *words[5] = {"apple", "radar", "banana", "level", "orange"};

    for (int i = 0; i < 5; i++) {
        const char *p = words[i];
        const char *end = words[i];

        while (*end != '\0') {
            end++;
        }

        end--;

        if (*p == *end) {
            printf("%s: 先頭と末尾が同じです。\n", words[i]);
        } else {
            printf("%s: 先頭と末尾が異なります。\n", words[i]);
        }
    }

    return 0;
}

解説

この問題では、各文字列について

  • p を先頭に合わせる
  • end を末尾まで進める
  • 先頭文字と末尾文字を比較する

という流れで判定しています。

const char *p = words[i];
const char *end = words[i];

で、どちらも最初は先頭を指します。
そのあと、

while (*end != '\0') {
    end++;
}
end--;

によって、end を最後の文字まで進めています。

あとは、

if (*p == *end)

で、先頭文字と末尾文字を比べるだけです。

回文判定ほど長い比較はしていませんが、
ポインタを使って文字列の先頭と末尾をつかむ
という、とてもよい練習になります。

11章の確認問題

次の項目について、正しいものには○、間違っているものには×をつけてください。

① 単項 * 演算子は、ポインタが指す先の値を参照するために使う。

② 配列名は、どのような場面でも単なる配列全体そのものであり、先頭要素のアドレスとしては扱われない。

③ const char *p = "Hello"; とした場合、p が指す文字列リテラルは書き換えないものとして扱う。

④ int 型配列 data に対して int *p = data; としたとき、p + 1 は常に 1 バイト先を指す。

⑤ printf の %p は、ポインタ型の値を表示するために使う。

⑥ *(p + 2) は、p が配列の先頭を指しているとき、3 番目の要素を表す。

⑦ char *names[3]; は、3 本の char 型配列を宣言している。

⑧ 文字列リテラルは多くの処理系で読み取り専用として扱われるため、書き換えは安全ではない。

⑨ ポインタの配列は、複数の文字列リテラルをまとめて管理するのに使える。

⑩ *p + 1 と *(p + 1) は同じ意味である。

解答と解説

① ○
単項 * 演算子は間接参照に使います。ポインタそのものではなく、ポインタが指している先の値を扱うための記号です。

② ✕
配列名は多くの式の中で先頭要素のアドレスとして扱われます。たとえば data なら &data[0] と同じ場所を表します。

③ ○
const char *p という宣言は、p が指す先の文字を変更しないという意味です。文字列リテラルを扱うときによく使います。

④ ✕
p + 1 は 1 バイト先ではなく、p が指す型 1 個分だけ先へ進みます。int * なら int 型のサイズ分だけ進みます。

⑤ ○
ポインタ型の値、つまりアドレスを表示するときには %p を使います。表示形式は処理系によって異なることがあります。

⑥ ○
p が先頭要素を指しているなら、p + 2 は 2 つ先の要素の位置です。したがって *(p + 2) は 3 番目の要素を表します。

⑦ ✕
char *names[3] は、char 型の文字列を指すポインタが 3 個入る配列です。3 本の char 配列そのものではありません。

⑧ ○
文字列リテラルは多くの処理系で読み取り専用領域に置かれます。そのため、書き換えは未定義動作になる可能性があります。

⑨ ○
ポインタの配列を使うと、複数の文字列リテラルの先頭アドレスをまとめて管理できます。文字数にばらつきがある場合にも便利です。

⑩ ✕
*(p + 1) は次の要素の値を表します。一方 *p + 1 は、今指している要素の値に 1 を足した結果です。括弧の位置で意味が変わります。

演習を通して身につけたい見方

演習を解くときは、ただ答えを書くのではなく、次の見方を意識するとポインタの理解が深まりやすいです。

見方意識すること
どこを指しているかp は今どの文字、どの要素を見ているか
何で終わるか\0 で終わるのか、要素数で終わるのか
何を比較しているか文字そのものか、アドレスか
進み方はどうかp++ で前進するのか、p-- で戻るのか
配列かポインタの配列か中身が文字なのか、アドレスなのか

このあたりが見えるようになると、ポインタのコードが急に読みやすくなります。
同じ * や + でも、何を指していて、どこへ進み、何を取り出しているのかが整理できるようになるからです。