C言語の基本|C言語のポインタ演習問題

ポインタは、アドレスを眺めるだけでは身につきません。文字列をたどり、進み、戻り、比べる演習を重ねながら、動きをコードでつかんでいきましょう。

C言語を学習していると、ポインタのところで急に難しくなったように感じることがあります。

変数の値だけではなくアドレスを扱うようになり、& や * が登場し、さらに配列や文字列と組み合わさると、どこを見ているのか分からなくなりやすいからです。

ただし、ポインタは説明を読んで覚えるだけよりも、実際に小さなプログラムを書いて動きを確認したほうが理解しやすいテーマです。

たとえば、文字列をポインタで先頭から順番にたどる処理では、

  • 今、ポインタはどの文字を指しているのか
  • p++ を実行するとどこへ移動するのか
  • *p では何が取り出されるのか
  • どの条件で繰り返しを終了するのか

ということを1つずつ確認できます。

さらに、ポインタを文字列の末尾まで進めてから逆向きに戻したり、ポインタの配列を使って複数の文字列を管理したりすると、ポインタの使い方が少しずつ広がっていきます。

今回の演習では、次のような処理を扱います。

  • 文字列リテラルをポインタで順番に調べる
  • \0 までポインタを進める
  • 文字列の末尾から先頭へ戻りながら表示する
  • ポインタの配列で複数の文字列を管理する
  • 配列の添字とポインタの関係を確認する
  • 文字列の先頭文字と末尾文字を比較する

大切なのは、プログラムを丸ごと暗記することではありません。

ポインタが今どこを指しているのか

を意識しながらコードを追うことです。

演習を通して、ポインタを記号として覚えるのではなく、メモリ上を移動しながらデータを参照する仕組みとして理解していきましょう。

演習を始める前に確認しておきたいこと

問題に取り組む前に、今回の演習で何度も登場する考え方を整理しておきましょう。

ポイント内容
C言語の文字列char 型の文字が並び、最後に \0 が置かれる
文字列リテラルconst char * で指すと、書き換えないことを明確にできる
*pp が現在指している場所の文字を参照する
p++p を次の文字の位置へ進める
p--p を1つ前の文字の位置へ戻す
\0文字列の終端を判断するために使う
ポインタの配列複数の文字列への先頭アドレスをまとめて管理できる

特に大切なのが、ポインタそのものと、ポインタが指しているデータを区別することです。

たとえば、

p

はアドレスを表します。

一方、

*p

は、そのアドレスに置かれているデータを表します。

文字列をポインタでたどる場合は、

p++;

によってアドレスを次へ進め、

*p

によって、その位置の文字を読み取ります。

この2つの働きを区別できるようになると、今回の演習がかなり読みやすくなります。

図1:ポインタ学習で使う基本操作の流れ

この図から分かること

ポインタを使った文字列処理では、まず1本のポインタが文字列の先頭を指します。

そこから p++ を繰り返すことで、1文字ずつ次の位置へ移動できます。
現在の文字を読みたいときは *p を使います。

さらに、この考え方を複数の文字列へ広げたものがポインタの配列です。

1本のポインタで1つの文字列を指せるのであれば、そのポインタを複数並べることで、複数の文字列の先頭をまとめて管理できます。

まずは、指す、読む、進むという3つの動きを意識して演習を進めていきましょう。

実践問題1:文字列の中から数字だけを探す

最初の問題では、ポインタを使って文字列を先頭から最後まで順番に調べます。

次の手順でプログラムを作成します。

  • 任意の文字列リテラルをポインタで指す
  • ポインタを先頭から1文字ずつ進める
  • 現在の文字が数字かどうかを判定する
  • 数字だった場合だけ表示する
  • \0 に到達したら処理を終了する

数字は、文字コード上で 0 以上かつ 9 以下であるかを調べて判定します。

今回は、商品棚を表す文字列から数字だけを取り出してみましょう。

文字列から数字だけを取り出すプログラム

ファイル名:11_9_1.c

#include <stdio.h>

int main(void)
{
    const char *p = "Shelf 24, Rack 8, Box 315.";

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

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

        p++;
    }

    printf("\n");

    return 0;
}
実行結果の例
元の文字列: Shelf 24, Rack 8, Box 315.
含まれる数字: 248315

文字列リテラルをポインタで指す

最初に、次のようにポインタを初期化しています。

const char *p = "Shelf 24, Rack 8, Box 315.";

p は、文字列リテラルの先頭を指します。

最初の状態では、p が指している文字は S です。

概念的には次のようになります。

p
↓
S h e l f   2 4 ,   R a c k   8 , ...

文字列リテラルは書き換えないものとして扱うため、ここでは const char * を使っています。

\0 まで文字列を調べる

文字列を順番に調べているのが、次の while 文です。

while (*p != '\0') {

C言語の文字列の最後には \0 が格納されています。

したがって、この条件は、

p が現在指している文字が \0 ではない間、繰り返す

という意味になります。

文字列の最後まで進むと *p が \0 になるため、while 文が終了します。

数字かどうかを判定する

現在の文字を調べている部分が次の if 文です。

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

*p は、現在 p が指している文字です。

この文字が 0 以上、9 以下であれば数字文字だと判断できます。

たとえば、p が 2 を指しているときは条件が成立するため、

printf("%c", *p);

によって 2 が表示されます。

一方、p が S や h、空白、カンマなどを指している場合は条件が成立しないので表示されません。

p++ で次の文字へ進む

ループの最後では、

p++;

を実行しています。

これによって、p が次の char 型要素を指すようになります。

文字列が次のように並んでいる場合、

S h e l f ...
↑
p

p++ を実行すると、

S h e l f ...
  ↑
  p

となります。

さらに繰り返すことで、文字列の最後にある \0 まで1文字ずつ進みます。

この問題では、

  • *p で現在の文字を読む
  • p++ で次へ進む
  • \0 で終了を判断する

という、文字列をポインタで走査するときの基本的な流れを確認できます。

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

この図から分かること

この図では、p が文字列の先頭から始まり、1文字ずつ右へ進んでいくことが確認できます。

ポイントは、p 自体が文字ではないということです。

p は文字のある場所を指し、*p によってその場所の文字を読み取ります。

処理は、

  • *p で現在の文字を確認する
  • 数字なら表示する
  • p++ で次へ進む
  • \0 なら終了する

という順番で繰り返されます。

この流れは、ポインタを使った文字列処理の基本になります。

実践問題2:文字列を末尾から逆順に表示する

次は、ポインタを前へ進めるだけでなく、後ろへ戻して使う演習です。

次の手順で処理します。

  • 文字列リテラルをポインタで指す
  • もう1本のポインタを文字列の先頭に合わせる
  • \0 が見つかるまでポインタを前へ進める
  • \0 の位置から1つ戻る
  • 最後の文字から先頭へ向かって1文字ずつ表示する

今回は Autumn という文字列を逆順に表示してみます。

文字列を末尾から先頭へ逆順に表示するプログラム

ファイル名:11_9_2.c

#include <stdio.h>

int main(void)
{
    const char *text = "Autumn";
    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;
}
実行結果の例
元の文字列: Autumn
逆順の文字列: nmutuA

text と p は最初は同じ場所を指す

最初に次のようにしています。

const char *text = "Autumn";
const char *p = text;

text は Autumn の先頭を指します。

そして、

const char *p = text;

によって、p に text が持っている先頭アドレスを代入しています。

したがって、最初は text と p の両方が A を指しています。

text
 ↓
 A u t u m n \0
 ↑
 p

ここで text は文字列の先頭位置を覚えておくために使い、p は実際に前後へ動かすために使っています。

まず \0 まで進める

次の処理で、p を文字列の最後まで進めます。

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

Autumn はメモリ上では概念的に次のようになっています。

A u t u m n \0

ループ終了時点では、p は最後の n ではなく、\0 を指しています。

A u t u m n \0
            ↑
            p

ここを間違えないことが重要です。

p-- で最後の文字へ戻る

\0 は文字列の終端を表すための文字なので、逆順表示には含めません。

そこで、

p--;

を実行します。

すると p は、\0 の1つ前にある n を指します。

A u t u m n \0
          ↑
          p

これで逆順表示を始める準備ができました。

p-- で先頭方向へ戻っていく

次の while 文で、文字列を逆方向にたどります。

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

最初は n を表示します。

そのあと p-- によって、

n → m → u → t → u → A

の順にポインタが戻っていきます。

それぞれの位置で *p を表示するので、

nmutuA

という結果になります。

この問題では、ポインタは必ず前方向だけに進むものではなく、位置関係を理解していれば後ろ方向へ移動して利用できることが確認できます。

なお、この例の考え方では text が文字列の先頭位置を保持しているため、p を戻すときの基準として利用できます。

実践問題3:ポインタの配列で番号に対応する文字列を表示する

次は、1本の文字列ではなく、複数の文字列を扱います。

ポインタの配列を使って、複数の項目名を管理してみましょう。

今回は、7つの音階名を番号で選択するプログラムにします。

次のように対応させます。

入力番号表示する文字列
1Do
2Re
3Mi
4Fa
5Sol
6La
7Si

処理の流れは次の通りです。

  • 7つの文字列をポインタの配列で管理する
  • 1~7の番号を入力する
  • 正しい範囲なら対応する文字列を表示する
  • 範囲外ならエラーメッセージを表示する
  • 入力された番号から1を引いて配列の添字に合わせる
ポインタの配列から番号に対応する音階名を表示するプログラム

ファイル名:11_9_3.c

#include <stdio.h>

int main(void)
{
    const char *notes[7] = {
        "Do", "Re", "Mi", "Fa",
        "Sol", "La", "Si"
    };

    int n;

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

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

    return 0;
}
実行結果の例1
音階番号を入力してください(1〜7) > 5
5番目の音階はSolです。
実行結果の例2
音階番号を入力してください(1〜7) > 9
入力が不正です。1〜7の範囲で入力してください。

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

最初に注目したいのが、次の宣言です。

const char *notes[7] = {
    "Do", "Re", "Mi", "Fa",
    "Sol", "La", "Si"
};

notes は、7個のポインタを要素として持つ配列です。

それぞれの要素は、次の文字列リテラルを指しています。

配列要素指している文字列
notes[0]Do
notes[1]Re
notes[2]Mi
notes[3]Fa
notes[4]Sol
notes[5]La
notes[6]Si

大切なのは、notes の各要素に文字列そのものが直接格納されているのではなく、各文字列の先頭アドレスが入っていることです。

入力値を配列の添字へ変換する

利用者には1~7を入力してもらいます。

しかし、C言語の配列の添字は0から始まります。

そのため、

notes[n - 1]

としています。

たとえば n に5が入力された場合は、

n = 5
n - 1 = 4

なので、

notes[4]

を参照します。

notes[4] が指している文字列は Sol なので、

5番目の音階はSolです。

と表示されます。

入力値の範囲を確認する

配列を参照する前に、次の条件で入力値を確認しています。

if (n >= 1 && n <= 7) {

1~7の範囲に入っている場合だけ、

notes[n - 1]

を参照します。

もし9などが入力された場合に、そのまま notes[n - 1] を使うと、配列の範囲外を参照することになります。

そのため、ポインタの配列を扱う場合でも、添字が正しい範囲にあることを確認するのが大切です。

図3:ポインタの配列から番号に対応する文字列を選ぶ

この図から分かること

ポインタの配列を使うと、それぞれの要素から別々の文字列を参照できます。

入力値が5の場合、そのまま notes[5] を使うのではありません。

配列の添字は0から始まるため、

notes[n - 1]

として notes[4] を参照します。

このように、ポインタの配列では、

  • 配列の要素を選ぶ
  • その要素が持っているアドレスを使う
  • そのアドレスから文字列を表示する

という流れで複数の文字列を扱えます。

実践問題4:複数の文字列の先頭文字と末尾文字を比較する

最後は、ポインタの配列と文字列走査を組み合わせた問題です。

複数の文字列について、先頭文字と末尾文字が同じかどうかを調べます。

今回は次の5つの文字列を使います。

  • table
  • civic
  • melon
  • rotor
  • grape

それぞれの文字列について、

  • 先頭文字と末尾文字が同じなら、先頭と末尾が同じです
  • 異なっていれば、先頭と末尾が異なります

と表示します。

複数の文字列の先頭文字と末尾文字を比較するプログラム

ファイル名:11_9_4.c

#include <stdio.h>

int main(void)
{
    const char *words[5] = {
        "table", "civic", "melon", "rotor", "grape"
    };

    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;
}
実行結果の例
table: 先頭と末尾が異なります。
civic: 先頭と末尾が同じです。
melon: 先頭と末尾が異なります。
rotor: 先頭と末尾が同じです。
grape: 先頭と末尾が異なります。

words[i] で処理する文字列を選ぶ

words はポインタの配列です。

const char *words[5] = {
    "table", "civic", "melon", "rotor", "grape"
};

外側の for 文によって、words[0] から words[4] まで順番に処理します。

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

たとえば i が1なら、

words[1]

が指している civic を処理します。

先頭用と末尾探索用の2本のポインタを用意する

各文字列について、次の2本のポインタを用意しています。

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

最初は、p と end の両方が同じ文字列の先頭を指しています。

たとえば civic の場合は次のイメージです。

c i v i c \0
↑
p
end

ただし、このあと動かすのは end だけです。

p は先頭文字を参照するため、そのまま先頭位置に残しておきます。

end を文字列の終端まで進める

次の処理で end を前へ進めます。

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

ループが終了した時点では、end は \0 を指しています。

c i v i c \0
          ↑
          end

この位置では末尾文字を参照できないため、

end--;

によって1つ戻します。

すると、

c i v i c \0
↑       ↑
p       end

となります。

p は先頭の c、end は最後の c を指しています。

*p と *end を比較する

最後に、次の条件で文字を比較します。

if (*p == *end) {

ここで比較しているのはアドレスではありません。

*p と *end なので、それぞれのポインタが指している場所にある文字を比較しています。

civic の場合は、

*p   → c
*end → c

となるため、条件が成立します。

一方、table の場合は、

*p   → t
*end → e

となるため、条件は成立しません。

この違いはとても重要です。

比較比較しているもの
p == end2つのポインタが同じアドレスを指しているか
*p == *end2つの場所にある文字が同じか

今回調べたいのは文字の一致なので、

*p == *end

を使います。

この問題で確認できるポインタの動き

この問題には、これまで学習してきた複数の考え方が含まれています。

  • ポインタの配列から words[i] で文字列を選ぶ
  • p で文字列の先頭位置を保持する
  • end を \0 まで進める
  • end-- で末尾文字へ戻る
  • *p と *end で文字そのものを比較する

つまり、複数の文字列を選ぶ処理と、1本の文字列の中をポインタで移動する処理を組み合わせています。

なお、このプログラムは文字列全体が回文かどうかを調べているわけではありません。

比較しているのは、先頭の1文字と末尾の1文字だけです。

たとえば、先頭と末尾が同じ文字列でも、その内側の文字まで対称になっているとは限りません。

今回の演習では、あくまで文字列の先頭と末尾をポインタで取得して比較することがポイントです。

演習を解くときに意識したい見方

ポインタを使ったプログラムを読むときは、記号だけを追いかけるのではなく、ポインタの状態を頭の中で図にすると理解しやすくなります。

特に、次の5つを確認する習慣を付けると効果的です。

見方確認すること
どこを指しているかp は現在どの文字や要素を指しているか
何を参照しているかp というアドレスか、*p というデータか
どこまで進むか\0 までなのか、配列の要素数までなのか
どちらへ移動するかp++ で前進するのか、p-- で後退するのか
配列の中身は何か文字そのものか、文字列へのアドレスか

たとえば、

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

を見たときに、

単に p++ が繰り返されている、

と考えるだけでは、ポインタの動きはなかなかつかめません。

それよりも、

現在 p は先頭文字を指している
↓
*p が \0 ではない
↓
p++ で次の文字へ進む
↓
もう一度 *p を確認する
↓
最後に \0 を指したところで終了する

という流れで考えるほうが分かりやすくなります。

* とポインタそのものを区別しよう

今回の演習では、* が何度も登場しました。

たとえば、

*p

は、p が持っているアドレスそのものではなく、そのアドレスにあるデータを意味します。

文字列を指している場合は、その位置にある1文字です。

そのため、

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

では、現在の文字を数字と比較しています。

一方、

p++;

では * が付いていません。

これは文字を変更しているのではなく、p が保持しているアドレスを次の要素の位置へ進めている処理です。

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

書き方意味
pp が保持しているアドレス
*pp が指している場所のデータ
p++次の要素へポインタを進める
p--前の要素へポインタを戻す

ポインタのコードを読むときは、* が付いているかどうかを確認するだけでも、かなり意味を整理しやすくなります。

\0 を見つけることが文字列処理の基本

今回の4つの演習のうち、複数の問題で \0 を利用しました。

C言語では、文字列がどこまで続いているかを別途保存していない場合、\0 を使って終端を判断できます。

たとえば、

const char *p = "Code";

が指している文字列は、概念的には次のように並んでいます。

'C' 'o' 'd' 'e' '\0'

表示上は Code だけですが、メモリ上では最後に \0 があります。

そのため、

while (*p != '\0')

という条件によって、文字列の有効な文字だけを順番に処理できます。

ポインタを使った文字列走査では、

現在の文字を見る → \0 でないことを確認する → 次へ進む

という流れが何度も登場します。

この流れを自然に読めるようになることが、文字列とポインタを理解するうえで大切です。

ポインタの配列では二段階で考える

ポインタの配列を使った処理では、1本のポインタとは少し違い、二段階で考えると理解しやすくなります。

たとえば、

words[i]

では、まず i 番目のポインタを選んでいます。

そのポインタが、1つの文字列の先頭を指します。

イメージすると次のようになります。

words
 ├─ words[0] → "table"
 ├─ words[1] → "civic"
 ├─ words[2] → "melon"
 ├─ words[3] → "rotor"
 └─ words[4] → "grape"

したがって、

const char *p = words[i];

は、

ポインタの配列から1つの文字列を選び、その文字列の先頭アドレスを p に渡す

という処理です。

そのあとは、通常の文字列ポインタと同じように、

*p

で文字を読み、

p++;

で次へ進めます。

1本のポインタで文字列をたどる方法が分かっていれば、ポインタの配列もその延長として考えられます。

演習で身につけたいポインタの感覚

今回の4つの演習は、それぞれ別の処理をしていますが、使っている考え方はつながっています。

演習主に確認できること
数字だけを取り出す*p で文字を読み、p++ で文字列を走査する
逆順に表示する\0 まで進んだあと、p-- で戻る
番号から文字列を選ぶポインタの配列と添字の関係
先頭と末尾を比較する複数のポインタを使って文字を比較する

ポインタは、コードだけを見ていると複雑に感じやすいですが、

  • 今どこを指しているのか
  • 次にどこへ移動するのか
  • * を付けたとき何が取り出されるのか

を1行ずつ考えると、動きが見えやすくなります。

特に文字列では、先頭から \0 までメモリ上に文字が並んでいます。

ポインタは、その並びの中を移動しながら必要な文字を取り出しているだけです。

この感覚を持ちながら、自分で文字列や条件を変更して何度か実行してみると、ポインタのコードを読む力が少しずつ身についていきます。