C言語のきほん|なぜポインタを使うのか

ポインタを使う理由がわかると、関数の可能性が一気に広がる

C言語の関数では、return文 を使って値を返すことができます。
ただし、return文 で直接返せる値は、基本的に1つだけです。

ここまで学んできた範囲では、それでも十分に便利でした。
けれども実際には、

  • 和と差を両方返したい
  • 最小値と最大値を同時に返したい
  • 配列の各要素をまとめて更新したい

といった場面がよくあります。

こんなときに役立つのが、ポインタです。
ポインタを使うと、関数の外にある変数や配列に直接アクセスできるようになります。
その結果、関数は return文 で1つの値を返すだけでなく、複数の結果を呼び出し元に反映することができるようになります。

つまり、ポインタは単なる難しい記号ではなく、
関数の外にあるデータを操作するためのしくみ
としてとても大切です。

ここでは、「なぜポインタを使うのか」という視点から、return文 だけでは足りない場面と、ポインタを使うことで何ができるようになるのかを、やさしく丁寧に見ていきましょう。

なぜポインタを使うのか

C言語でポインタを使う大きな理由の1つは、関数から複数の結果を返したいからです。

たとえば、2つの整数 a と b を受け取って、

を全部求めたいとします。

でも return文 で直接返せる値は基本的に1つだけです。
なので、次のような考え方だけでは足りません。

return a + b;

これでは和しか返せません。

そこで、呼び出し元で変数を用意して、そのアドレスを関数へ渡します。
関数側ではポインタでそのアドレスを受け取り、指している先の変数に結果を書き込みます。

このしくみによって、関数は1つではなく、複数の結果を呼び出し元へ返せるようになります。

return文 だけではなぜ足りないのか

まずは return文 の基本を整理してみましょう。

方法返せる値
return文基本的に1つ
ポインタを使う複数の値を間接的に返せる

たとえば、次の関数は1つの値しか返せません。

int add(int a, int b)
{
    return a + b;
}

これは和を返すには十分です。
でも、同時に差も返したいとなると、return文 だけでは困ります。

C言語では、こうした場面で引数としてアドレスを渡し、その先に結果を書き込む方法を使います。
これが、「ポインタを使って複数の値を返す」という考え方です。

ポインタを使うと複数の値を返せる

では、実際の形を見てみましょう。
元の sum_diff の例を、少し親しみやすい別のシンプルな例に置き換えます。

ここでは、2つの整数の和と差を求める関数にします。

ファイル名:13_15_1.c

// ポインタを使って複数の値を返すプログラム
#include <stdio.h>

void calc_sum_and_diff(int a, int b, int *sum, int *diff);

int main(void)
{
    int total;
    int gap;

    calc_sum_and_diff(12, 5, &total, &gap);

    printf("和: %d, 差: %d\n", total, gap);

    return 0;
}

// 和と差を求めて呼び出し元へ返す関数
void calc_sum_and_diff(int a, int b, int *sum, int *diff)
{
    *sum = a + b;
    *diff = a - b;
}

実行結果例

和: 17, 差: 7

このプログラムの流れ

このプログラムでは、main 関数で total と gap という変数を用意しています。

int total;
int gap;

そして、そのアドレスを関数へ渡しています。

calc_sum_and_diff(12, 5, &total, &gap);

ここで渡しているのは、変数そのものではなく、

  • &total
  • &gap

つまり、変数のアドレスです。

関数側では、それをポインタで受け取ります。

void calc_sum_and_diff(int a, int b, int *sum, int *diff)

そして、

*sum = a + b;
*diff = a - b;

によって、sum が指している先、diff が指している先に結果を書き込みます。

つまり、関数の中から main 関数の total と gap に直接結果を入れているわけです。

この流れは図で見るととても理解しやすいです。

この図では、関数の仮引数 sum と diff が、main 関数の total と gap を指していることがポイントです。
そのため、*sum や *diff に代入すると、呼び出し元の変数そのものに結果が入ります。

ポインタを使わないとどうなるのか

比較のために、値渡しだけで書いた場合を考えてみましょう。

void calc_sum_and_diff(int a, int b, int sum, int diff)
{
    sum = a + b;
    diff = a - b;
}

この形では、sum と diff は呼び出し元の変数のコピーです。
なので、関数の中で代入しても main 側の変数は変わりません。

つまり、呼び出し元の変数を更新したいなら、アドレスを渡す必要があるわけです。

これが、ポインタを使う大きな理由の1つです。

配列を使うと一度に多くの値を返せる

ポインタを使う理由は、変数を複数返すときだけではありません。
配列の中身をまとめて更新したいときにもとても便利です。

ここでは別のシンプルな例として、配列の全要素に 10 を足す関数を作成します。

ファイル名:13_15_2.c

// 配列を使って複数の値を更新するプログラム
#include <stdio.h>

void add_ten_to_all(int arr[], int size);

int main(void)
{
    int scores[] = {50, 60, 70, 80, 90};
    int size = (int)(sizeof(scores) / sizeof(scores[0]));

    add_ten_to_all(scores, size);

    printf("10を足した結果: ");

    for (int i = 0; i < size; i++) {
        printf("%d ", scores[i]);
    }

    printf("\n");

    return 0;
}

// 配列の全要素に10を加える関数
void add_ten_to_all(int arr[], int size)
{
    for (int i = 0; i < size; i++) {
        arr[i] += 10;
    }
}

実行結果例

10を足した結果: 60 70 80 90 100

配列を渡すときも実はポインタ

このプログラムで関数に渡しているのは、

add_ten_to_all(scores, size);

の scores です。

見た目には配列をそのまま渡しているように見えますが、関数呼び出しの場面では scores は先頭要素のアドレスとして扱われます。
そのため、関数側の

int arr[]

は、実質的には

int *arr

と同じ意味になります。

つまり、配列を渡すということは、先頭要素を指すポインタを渡しているということです。

そのため、関数側で arr[i] を変更すると、呼び出し元の配列の中身が変わります。

arr[i] は *(arr + i) と同じ意味

ここも大事なポイントです。
配列要素へのアクセスは、次の2つの書き方ができます。

arr[i]

*(arr + i)

この2つは同じ意味です。

たとえば、

arr[2]

は、

*(arr + 2)

と等価です。

つまり、添字演算子で書いていても、内部的にはポインタを使って要素へアクセスしていると考えられます。

図で配列の更新を見てみよう

この図では、関数側の arr が配列 scores の先頭を指していることを表しています。
そのため、関数の中で arr[i] を変更すると、main 側の配列要素もそのまま更新されます。

ポインタを使うとできること

ここまでの内容を整理すると、ポインタを使う理由は次のようにまとめられます。

ポインタを使う理由できること
複数の値を返したい複数の変数に結果を書き込める
配列全体を処理したい多くの要素をまとめて更新できる
呼び出し元の変数を変更したい関数の外の変数を直接操作できる
return 1つでは足りない間接的に複数の結果を返せる

つまり、ポインタは「難しいから覚える」のではなく、
関数でできることを増やすために使う
と考えると理解しやすいです。

実践問題

次の仕様に従って関数を作成し、main 関数から呼び出して結果を確認してください。

関数宣言

void calculate_three(int a, int b, int *sum, int *diff, int *prod);

機能
仮引数 a と b の加算結果、減算結果、乗算結果を
それぞれ *sum、*diff、*prod に格納する。

返却値
なし

実行結果例
main 関数で表示

整数1 > 8
整数2 > 2

結果:
和: 10
差: 6
積: 16

解答例

ファイル名:13_15_3.c

// ポインタで複数の計算結果を返すプログラム
#include <stdio.h>

void calculate_three(int a, int b, int *sum, int *diff, int *prod);

int main(void)
{
    int a, b;
    int sum, diff, prod;

    printf("整数1 > ");
    scanf("%d", &a);

    printf("整数2 > ");
    scanf("%d", &b);

    printf("\n");

    calculate_three(a, b, &sum, &diff, &prod);

    printf("結果:\n");
    printf("和: %d\n", sum);
    printf("差: %d\n", diff);
    printf("積: %d\n", prod);

    return 0;
}

// 3つの計算結果を返す関数
void calculate_three(int a, int b, int *sum, int *diff, int *prod)
{
    *sum = a + b;
    *diff = a - b;
    *prod = a * b;
}

解説

この問題では、return文 を使わずに、3つの結果を呼び出し元へ返しています。
そのために、sum、diff、prod のアドレスを関数へ渡しています。

関数側では、そのアドレスを使って

  • *sum
  • *diff
  • *prod

に結果を書き込んでいます。
これが、ポインタを使って複数の値を返す基本形です。

実践問題

次の仕様に従って関数を作成し、main 関数から呼び出して結果を確認してください。

関数宣言

void calculate_two(int a, int b, int results[2]);

機能
仮引数 a と b の加算結果と乗算結果を、それぞれ

  • results[0]
  • results[1]

に格納する。

返却値
なし

実行結果例

整数1 > 6
整数2 > 4

結果:
和: 10
積: 24

解答例

ファイル名:13_15_4.c

// 配列を使って複数の結果を返すプログラム
#include <stdio.h>

void calculate_two(int a, int b, int results[2]);

int main(void)
{
    int a, b;
    int results[2];

    printf("整数1 > ");
    scanf("%d", &a);

    printf("整数2 > ");
    scanf("%d", &b);

    printf("\n");

    calculate_two(a, b, results);

    printf("結果:\n");
    printf("和: %d\n", results[0]);
    printf("積: %d\n", results[1]);

    return 0;
}

// 2つの計算結果を配列に格納する関数
void calculate_two(int a, int b, int results[2])
{
    results[0] = a + b;
    results[1] = a * b;
}

解説

配列を渡すと、先頭要素のアドレスが関数へ渡されます。
そのため、関数側で results[0] や results[1] に値を入れると、呼び出し元の配列にも反映されます。

これは「複数の値を返す箱を配列でまとめて渡している」と考えると分かりやすいです。

実践問題

次の仕様に従って関数を作成し、main 関数から呼び出して結果を確認してください。

関数宣言

void find_first_last(const int arr[], int size, int *first, int *last);

機能
要素数 size の配列 arr の先頭要素を first の指す変数に、末尾要素を last の指す変数に返す。

返却値
なし

実行結果例

配列要素が {11, 25, 38, 49, 52} の場合

配列要素: 11 25 38 49 52
先頭要素: 11
末尾要素: 52

解答例

ファイル名:13_15_5.c

// 配列の先頭要素と末尾要素を返すプログラム
#include <stdio.h>

void find_first_last(const int arr[], int size, int *first, int *last);

int main(void)
{
    int values[] = {11, 25, 38, 49, 52};
    int size = (int)(sizeof(values) / sizeof(values[0]));
    int first, last;

    printf("配列要素: ");
    for (int i = 0; i < size; i++) {
        printf("%d ", values[i]);
    }
    printf("\n");

    find_first_last(values, size, &first, &last);

    printf("先頭要素: %d\n", first);
    printf("末尾要素: %d\n", last);

    return 0;
}

// 配列の先頭要素と末尾要素を返す関数
void find_first_last(const int arr[], int size, int *first, int *last)
{
    *first = arr[0];
    *last = arr[size - 1];
}

解説

この問題では、1つの配列から2つの結果を返しています。
もし return文 だけで書こうとすると、先頭要素か末尾要素のどちらかしか返せません。
そこでポインタを使うことで、2つの結果を同時に返しています。

実践問題

次の仕様に従って関数を作成し、main 関数から呼び出して結果を確認してください。

関数宣言

void get_average_range(const int arr[], int size, double *average, int *range);

機能
要素数 size の配列 arr の平均値を average の指す変数に、
最大値と最小値の差 範囲 を range の指す変数に返す。

返却値
なし

実行結果例

配列要素: 12 18 25 10 30
平均値: 19.00
範囲: 20

配列要素が {12, 18, 25, 10, 30} の場合

解答例

ファイル名:13_15_6.c

// 配列の平均値と範囲を返すプログラム
#include <stdio.h>

void get_average_range(const int arr[], int size, double *average, int *range);

int main(void)
{
    int values[] = {12, 18, 25, 10, 30};
    int size = (int)(sizeof(values) / sizeof(values[0]));
    double average;
    int range;

    printf("配列要素: ");
    for (int i = 0; i < size; i++) {
        printf("%d ", values[i]);
    }
    printf("\n");

    get_average_range(values, size, &average, &range);

    printf("平均値: %.2f\n", average);
    printf("範囲: %d\n", range);

    return 0;
}

// 平均値と範囲を返す関数
void get_average_range(const int arr[], int size, double *average, int *range)
{
    int sum = 0;
    int min = arr[0];
    int max = arr[0];

    for (int i = 0; i < size; i++) {
        sum += arr[i];

        if (arr[i] < min) {
            min = arr[i];
        }

        if (arr[i] > max) {
            max = arr[i];
        }
    }

    *average = (double)sum / size;
    *range = max - min;
}

解説

この問題では、

  • 平均値 double 型
  • 範囲 int 型

という、型の異なる2つの結果を同時に返しています。
return文 では1つしか返せないので、こういう場面でもポインタはとても役立ちます。

また、関数の中では配列を読み取るだけなので、

const int arr[]

として受け取っている点も大切です。
これにより、配列を誤って書き換えないことを示せます。

ポインタを使う意味を整理しよう

最後に、「なぜポインタを使うのか」を短く整理すると次のようになります。

理由内容
複数の値を返したい変数や配列に結果を書き込める
呼び出し元のデータを更新したい関数の外の変数や配列を変更できる
配列を効率よく扱いたい多くの要素を一度に処理できる
return文 の制限を補いたい1つ以上の結果を扱える

ポインタは最初は少し難しく見えますが、
関数の外にあるデータを操作するための道具
と考えると、かなり理解しやすくなります。

特に、

  • 1つの値なら return文
  • 複数の値ならポインタ

という考え方を持っておくと、関数設計の判断がしやすくなります。