6日でできる 新C言語入門|ポインタの総合演習

ポインタは、配列・文字列・関数をつなぐ大切な道具。
総合演習で手を動かしながら、C言語らしいデータ操作をしっかり身につけよう。

C言語のポインタは、変数のアドレスを扱うだけでなく、配列、文字列、関数の引数、データの加工など、さまざまな場面で使われます。

最初は、アドレスや参照という言葉が難しく感じるかもしれません。しかし、実際にプログラムを書きながら、配列の先頭をポインタで受け取る、文字列を1文字ずつたどる、関数の中で値を処理する、といった練習を重ねると、ポインタの使い方が少しずつ見えてきます。

ポインタの学習で大切なのは、単に書き方を覚えることではありません。今このポインタはどこを指しているのか、ポインタを進めると何が起きるのか、関数に渡したポインタから何を操作しているのかを意識することです。

この記事では、ポインタを使った総合演習として、配列から条件に合う値を取り出す処理、文字列の中を探索する処理、文字列を別の配列へ変換してコピーする処理、先頭文字列の比較、共通文字の抽出、数字文字のカウントを取り上げます。

それぞれの演習では、配列名が先頭アドレスとして使われること、文字列が終端文字まで続く文字の並びであること、ポインタを進めながら要素へアクセスできることを確認していきます。

ポインタ演習で確認したい基本

ポインタの総合演習では、いくつかの重要な考え方を繰り返し使います。

考え方内容
配列名は先頭アドレスとして使える関数に配列を渡すと、先頭要素のアドレスを渡せる
ポインタ演算で次の要素へ進めるarr + i のようにしてi番目の要素を指せる
*を使うと指している先の値を扱える*pでポインタが指す先の値を読み書きできる
文字列は終端文字までたどる文字列処理では終端文字に出会うまで処理する
出力先配列には終端文字を入れる文字列を作るときは最後に終端文字を入れる
配列サイズを意識する入力やコピーで範囲外アクセスを避ける

ポインタは便利ですが、自由にメモリ上の場所をたどれる分、範囲外アクセスやNULLポインタへのアクセスには注意が必要です。

今回の演習では、配列サイズを引数で渡したり、入力文字数を制限したりしながら、安全に近い書き方も意識していきます。

図:ポインタ演習で使う基本の流れ

この図から分かること

この図から分かるのは、ポインタが配列や文字列を処理するための入口になるということです。

配列や文字列の先頭アドレスを関数に渡すと、関数側ではポインタを使って要素を順番にたどれます。条件に合う値を抽出したり、文字列を探索したり、別の配列へ変換しながらコピーしたりする処理は、ポインタの基本を組み合わせて作れます。

配列から4の倍数だけを表示する

最初の演習では、整数配列の中から4の倍数だけを表示します。

配列を関数に渡すとき、関数側には配列の先頭アドレスが渡されます。そのため、関数の引数をint*型にして受け取り、ポインタ演算で各要素にアクセスできます。

関数仕様

関数名引数戻り値処理内容
printMultipleOfFourint* arr, int lenなし配列内の4の倍数を順に表示する

この関数では、arrが配列の先頭を指します。lenは配列の要素数です。

arr + iでi番目の要素の場所を表し、*(arr + i)でその場所に入っている値を取り出します。

プロジェクト/ファイル名: Lesson57_1/main.c

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define LENGTH 10

void printMultipleOfFour(int* arr, int len);

int main(void) {
    int values[LENGTH];
    int i;

    srand((unsigned)time(NULL));

    printf("生成された値: ");

    // 1から20までの乱数を配列に保存する
    for (i = 0; i < LENGTH; i++) {
        values[i] = rand() % 20 + 1;
        printf("%d ", values[i]);
    }

    printf("\n");

    // 配列の先頭アドレスと要素数を関数に渡す
    printMultipleOfFour(values, LENGTH);

    return 0;
}

// 配列の中から4の倍数だけを表示する関数
void printMultipleOfFour(int* arr, int len) {
    int i;

    printf("4の倍数: ");

    for (i = 0; i < len; i++) {
        if (*(arr + i) % 4 == 0) {
            printf("%d ", *(arr + i));
        }
    }

    printf("\n");
}

実行結果例

生成された値: 8 17 4 13 12 5 20 6 11 16
4の倍数: 8 4 12 20 16

このプログラムでは、main関数でvalues配列を用意し、乱数を10個保存しています。

そのあと、printMultipleOfFour関数にvaluesを渡しています。

printMultipleOfFour(values, LENGTH);

valuesは配列名なので、関数に渡すときには先頭要素のアドレスとして扱われます。関数側ではint* arrとして受け取ります。

void printMultipleOfFour(int* arr, int len)

関数内では、*(arr + i)で配列のi番目の値を取り出しています。

if (*(arr + i) % 4 == 0) {
    printf("%d ", *(arr + i));
}

このように、配列の要素を添え字ではなくポインタ演算でたどる練習になります。

arr[i]と*(arr + i)の関係

配列を関数に渡したあと、関数側ではarr[i]のように書くこともできます。

arr[i]

これは、次の書き方と同じ意味として扱えます。

*(arr + i)
書き方意味
arr配列の先頭要素のアドレス
arr + ii番目の要素のアドレス
*(arr + i)i番目の要素の値
arr[i]i番目の要素の値

今回のような演習では、あえて*(arr + i)を使うことで、ポインタが配列を順番にたどっている様子を理解しやすくなります。

文字列から最初の数字文字を探す

次は、文字列の中から最初に現れる数字文字の位置を探します。

文字列はchar型配列として扱えます。関数に文字列を渡すと、先頭文字へのポインタとして受け取れます。

この演習では、char*型のポインタを1文字ずつ進めながら、数字文字が出てくる位置を調べます。

関数仕様

関数名引数戻り値処理内容
findFirstDigitchar* strint最初に現れる数字文字の位置を返す。見つからない場合は-1を返す

プロジェクト/ファイル名: Lesson57_2/main.c

#include <stdio.h>

int findFirstDigit(char* str);

int main(void) {
    char text[100];
    int pos;

    printf("英数字を含む文字列を入力してください: ");
    scanf("%99s", text);

    // 文字列の中から最初の数字文字を探す
    pos = findFirstDigit(text);

    if (pos >= 0) {
        printf("最初の数字文字は%d文字目です。\n", pos + 1);
    } else {
        printf("数字文字は含まれていません。\n");
    }

    return 0;
}

// 文字列の中から最初に現れる数字文字の位置を探す関数
int findFirstDigit(char* str) {
    int index = 0;

    while (*str != '\0') {
        if (*str >= '0' && *str <= '9') {
            return index;
        }

        str++;
        index++;
    }

    return -1;
}

実行結果例

英数字を含む文字列を入力してください: code2026
最初の数字文字は5文字目です。

このプログラムでは、findFirstDigit関数が文字列の先頭アドレスを受け取ります。

strは、最初は文字列の先頭文字を指しています。

while文の中で*strを調べ、数字文字かどうかを判定します。数字文字でなければstr++で次の文字へ進みます。

処理意味
*str現在指している文字
str++次の文字へ進む
index++文字位置を1つ進める
return index見つかった位置を返す
return -1見つからなかったことを表す

文字列は終端文字に出会うまで続きます。そのため、while文では*strが終端文字でない間だけ処理を続けます。

図:文字列をポインタで1文字ずつたどる

この図から分かること

この図から分かるのは、文字列も配列と同じように、先頭から順番にたどれるということです。

strは最初にtext[0]を指します。str++を行うたびに、次の文字へ進みます。現在の文字は*strで確認できます。数字文字が見つかったら、その時点の位置を返します。

小文字を記号に変換して別の文字列にコピーする

次の演習では、入力された文字列を調べ、小文字のアルファベットだけを#に変換して、別の配列へコピーします。

文字列を加工するときは、読み取り元のポインタと書き込み先のポインタを別々に用意すると分かりやすくなります。

関数仕様

関数名引数戻り値処理内容
maskLowercasechar* src, char* destなしsrcを読み取り、小文字を#に変換してdestへコピーする

プロジェクト/ファイル名: Lesson57_3/main.c

#include <stdio.h>

void maskLowercase(char* src, char* dest);

int main(void) {
    char input[100];
    char result[100];

    printf("英単語を入力してください: ");
    scanf("%99s", input);

    // 小文字だけを#に変換して別の配列へ保存する
    maskLowercase(input, result);

    printf("変換後の文字列: %s\n", result);

    return 0;
}

// 小文字を#に置き換えて、別の文字列へコピーする関数
void maskLowercase(char* src, char* dest) {
    while (*src != '\0') {
        if (*src >= 'a' && *src <= 'z') {
            *dest = '#';
        } else {
            *dest = *src;
        }

        src++;
        dest++;
    }

    // 文字列の終端を入れる
    *dest = '\0';
}

実行結果例

英単語を入力してください: HelloWorld
変換後の文字列: H####W####

このプログラムでは、srcが入力文字列をたどるポインタ、destが結果文字列へ書き込むポインタです。

ポインタ役割
src入力文字列を読み取る
dest変換後の文字を書き込む

処理の流れは次のようになります。

手順内容
*srcを確認する現在の文字を読む
小文字なら#を書き込む*dest = #相当の処理を行う
小文字でなければそのままコピーする*dest = *src
src++とdest++を行う次の文字へ進む
最後に終端文字を入れる文字列として完成させる

ここで特に大切なのは、最後に終端文字を入れている点です。

*dest = '\0';

C言語の文字列は、終端文字で終わる必要があります。これを忘れると、printfで表示したときに余計な文字まで表示されたり、不安定な動作になったりする可能性があります。

2つの文字列の先頭N文字を比較する

次は、2つの文字列を入力し、先頭から指定した文字数だけ比較する演習です。

C言語には、文字列の先頭N文字を比較できるstrncmp関数があります。ポインタを使って文字列の先頭アドレスを渡し、指定した文字数だけを比較します。

関数役割
strncmp2つの文字列を先頭から指定文字数だけ比較する
戻り値が0指定範囲の文字列が一致している
戻り値が0以外指定範囲の文字列が一致していない

プロジェクト/ファイル名: Lesson57_4/main.c

#include <stdio.h>
#include <string.h>

int main(void) {
    char first[100];
    char second[100];
    int count;

    printf("1つ目の文字列を入力してください: ");
    scanf("%99s", first);

    printf("2つ目の文字列を入力してください: ");
    scanf("%99s", second);

    printf("比較する文字数を入力してください: ");
    scanf("%d", &count);

    // 先頭count文字が一致するか比較する
    if (strncmp(first, second, count) == 0) {
        printf("先頭%d文字は一致しています。\n", count);
    } else {
        printf("先頭%d文字は一致していません。\n", count);
    }

    return 0;
}

実行結果例

1つ目の文字列を入力してください: pointer
2つ目の文字列を入力してください: point
比較する文字数を入力してください: 5
先頭5文字は一致しています。

このプログラムでは、firstとsecondという2つの文字配列を用意しています。

strncmpにfirstとsecondを渡すと、それぞれの配列の先頭アドレスが渡されます。

strncmp(first, second, count)

firstとsecondは配列名なので、文字列の先頭アドレスとして扱われます。

このように、文字列を扱う標準関数でも、配列名とポインタの考え方が使われています。

文字列変換と比較で意識したいこと

文字列処理では、ポインタで文字をたどる処理と、標準関数に文字列を渡す処理がよく出てきます。

処理ポイント
文字を1つずつ調べるchar*を使って先頭から順番に進める
別の配列へコピーする読み取り元と書き込み先を分ける
変換後の文字列を作る最後に終端文字を入れる
標準関数で比較する配列名を先頭アドレスとして渡す
入力する配列サイズを超えないように入力幅を指定する

文字列は配列ですが、普通の数値配列と違って、終端文字までが1つの文字列として扱われます。

そのため、文字列を作る処理では、最後の終端文字を忘れないことがとても大切です。

2つの文字列に共通する文字を重複なしで表示する

次の演習では、2つの文字列に共通して含まれる文字を表示します。ただし、同じ文字が何度も出てきても、表示は1回だけにします。

この処理では、文字がすでに表示されたかどうかを記録するために、bool型の配列を使います。

プロジェクト/ファイル名: Lesson57_5/main.c

#include <stdio.h>
#include <stdbool.h>

int main(void) {
    char first[100];
    char second[100];
    bool printed[256] = { false };
    int i;
    int j;
    unsigned char code;

    printf("1つ目の文字列: ");
    scanf("%99s", first);

    printf("2つ目の文字列: ");
    scanf("%99s", second);

    printf("共通して含まれる文字: ");

    // firstの各文字について、secondに含まれているかを調べる
    for (i = 0; first[i] != '\0'; i++) {
        for (j = 0; second[j] != '\0'; j++) {
            if (first[i] == second[j]) {
                code = (unsigned char)first[i];

                // まだ表示していない文字だけを表示する
                if (!printed[code]) {
                    printf("%c ", first[i]);
                    printed[code] = true;
                }

                break;
            }
        }
    }

    printf("\n");

    return 0;
}

実行結果例

1つ目の文字列: banana
2つ目の文字列: band
共通して含まれる文字: b a n

このプログラムでは、firstの文字を1つずつ見て、secondの中に同じ文字があるかを探しています。

同じ文字が見つかったら、その文字を表示します。ただし、同じ文字を何度も表示しないように、printed配列で管理しています。

printed配列の役割

printed配列は、各文字がすでに表示されたかどうかを記録するための配列です。

bool printed[256] = { false };

この配列は、文字コードを添え字として使います。

たとえば、ある文字の文字コードが65なら、printed[65]がその文字に対応します。

printedの値意味
falseまだ表示していない
trueすでに表示した

文字を表示したら、対応するprintedの要素をtrueにします。

printed[code] = true;

こうすることで、同じ文字が後から出てきても、もう一度表示しないようにできます。

なぜunsigned charへ変換するのか

文字を配列の添え字として使う場合、負の値にならないようにするため、unsigned charへ変換しています。

code = (unsigned char)first[i];

char型は環境によって符号付きとして扱われることがあります。負の値を配列の添え字にすると危険です。そのため、0から255の範囲に収まるようにunsigned charへ変換してから添え字に使います。

図:共通文字を重複なしで表示する仕組み

この図から分かること

この図から分かるのは、共通文字を探す処理では、文字列同士を比較するだけでなく、すでに表示した文字を管理する必要があるということです。

firstの文字を1つずつ取り出し、secondに含まれるかを調べます。共通している文字が見つかっても、printed配列で表示済みかどうかを確認します。これにより、aやnのように何度も登場する文字でも、出力は1回だけにできます。

文字列中の数字文字の個数を数える

最後の演習では、文字列の中に含まれる数字文字の個数を数えます。

ここでは、ポインタを使って文字列を先頭から終端文字までたどります。

プロジェクト/ファイル名: Lesson57_6/main.c

#include <stdio.h>

int countDigits(char* text);

int main(void) {
    char text[100];
    int count;

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

    // 文字列の中に含まれる数字文字の個数を数える
    count = countDigits(text);

    printf("数字文字の個数: %d\n", count);

    return 0;
}

// 文字列中の数字文字を数える関数
int countDigits(char* text) {
    int count = 0;

    while (*text != '\0') {
        if (*text >= '0' && *text <= '9') {
            count++;
        }

        text++;
    }

    return count;
}

実行結果例

文字列を入力してください: item2026A5
数字文字の個数: 5

このプログラムでは、countDigits関数が文字列の先頭アドレスを受け取ります。

textは現在の文字を指すポインタです。

処理意味
*text現在の文字
数字文字か判定0から9の範囲か確認する
count++数字文字なら個数を増やす
text++次の文字へ進む
return count数字文字の個数を返す

このように、文字列を1文字ずつ調べる処理は、ポインタの基本練習としてとても役立ちます。

演習ごとの学習ポイント

今回の演習で使った考え方を整理すると、次のようになります。

演習主なテーマポインタの使い方
配列から4の倍数を表示整数配列の走査int*で配列の先頭を受け取る
最初の数字文字を探す文字列探索char*を進めながら調べる
小文字を記号に変換文字列加工読み取り元と書き込み先を別ポインタで扱う
先頭N文字を比較標準関数と文字列配列名を先頭アドレスとして渡す
共通文字を表示文字列比較と重複防止文字列を二重ループで調べる
数字文字を数える文字列走査終端文字までポインタを進める

ポインタは、配列や文字列を効率よく扱うために何度も登場します。特に、関数に配列や文字列を渡すときは、先頭アドレスが渡されていることを意識すると理解しやすくなります。

ポインタ演習で注意したいこと

ポインタを使った演習では、次の点に注意しましょう。

NULLポインタを参照しない

ポインタがNULLのまま*を使うと危険です。今回のように配列名を渡している場合は有効な先頭アドレスになりますが、ポインタ変数を使うときは、どこを指しているかを必ず確認しましょう。

配列の範囲外へ進まない

ポインタ演算で配列の外側へ進むと、存在しない場所を参照してしまいます。配列の要素数を関数に渡す、終端文字までで止めるなど、範囲を意識することが大切です。

文字列を作るときは終端文字を忘れない

Lesson57_3のように別の配列へ文字列を作る場合、最後に終端文字を入れる必要があります。これを忘れると、文字列として正しく扱えません。

scanfでは入力幅を指定する

char配列へ文字列を入力するときは、配列サイズを超えないように入力幅を指定すると安全性が高まります。

scanf("%99s", text);

char text[100]の場合、最大99文字まで入力し、最後の1文字を終端文字用に残す考え方です。

文字コードを添え字にするときはunsigned charを使う

文字を配列の添え字に使う場合、環境によってcharが負の値になる可能性があります。そのため、unsigned charへ変換してから添え字として使うと安全です。

ポインタを使いこなすための練習の進め方

ポインタの演習では、いきなり複雑な処理に進むより、次の順番で練習すると理解しやすくなります。

段階練習内容
基本配列の先頭アドレスを関数に渡す
配列操作int*で要素を順番に表示する
文字列操作char*で終端文字までたどる
変換処理srcとdestを使って別配列へコピーする
比較処理2つの文字列を調べる
応用重複管理や文字コードを使う

ポインタは、ただ覚えるだけでは理解しにくいテーマです。配列や文字列の中を実際にたどりながら、今どの値を見ているのか、ポインタを進めるとどこへ移動するのかを確認すると、感覚がつかみやすくなります。

ポインタ演習を通して身につく力

ポインタの総合演習では、C言語でよく使う重要な考え方をまとめて練習できます。

配列を関数に渡すと、関数側ではポインタとして受け取れます。整数配列では、intを使って要素を順にたどれます。文字列では、charを使って終端文字まで1文字ずつ調べられます。

文字列を加工するときは、読み取り元と書き込み先のポインタを分けて考えると、処理の流れが分かりやすくなります。また、共通文字の抽出のような応用問題では、すでに表示した文字を管理する配列を組み合わせることで、重複を防ぐこともできます。

ポインタは、配列や文字列を扱ううえで避けて通れない大切な仕組みです。今回の演習を通して、先頭アドレスを受け取る、要素をたどる、指す先の値を読む、必要に応じて値を書き込む、という基本動作をしっかり確認しておきましょう。