第13回|C言語で配列とポインタの関係を学ぶ:同じ要素に別の書き方でアクセスできることを理解する

当サイトでは、コンテンツの一部に広告を掲載しています。
第13回|C言語で配列とポインタの関係を学ぶ:同じ要素に別の書き方でアクセスできることを理解する

はじめてのC言語 | 第13回

目次

はじめに

前回は、ポインタの基本を学びました。
今回は、その続きとして「配列とポインタの関係」を整理します。

この回の目的は次の5点です。

  • 配列名がどのように扱われるかを理解する
  • array[i]*(array + i) が同じ要素を表すことを確認する
  • ポインタを使った配列走査の基本を学ぶ
  • 配列を関数に渡すときの基本形を理解する
  • 範囲外アクセスの危険性を理解する

配列名はどう扱われるのか

配列を宣言すると、複数の要素が連続した領域に並んで保存されます。
たとえば int numbers[3] = {10, 20, 30}; では、3個の int が並んで配置されます。

配列名 numbers は、多くの式の中で「先頭要素のアドレス」のように扱われます。
つまり、numbers は多くの場合 &numbers[0] と同じ場所を指します。

ただし、配列名そのものが普通のポインタ変数になるわけではありません。
初学者向けには、まず「配列名は先頭要素を指すように使える」と理解しておけば十分です。

先頭要素のアドレスを確認する

ソースコード

array_address.c という名前で保存します。

#include <stdio.h>

int main(void) {
    int numbers[3] = {10, 20, 30};

    printf("%p\n", (void *)numbers);
    printf("%p\n", (void *)&numbers[0]);

    return 0;
}

実行手順

1. 作業ディレクトリに移動する

cd ~/Desktop

2. コンパイルする

clang array_address.c -o array_address

3. 実行する

./array_address

実行結果の例:

0x16b2ff2a0
0x16b2ff2a0

アドレスの値そのものは実行環境で変わることがあります。
重要なのは、2行が同じ場所を表していることです。

コードの読み方

numbers

printf("%p\n", (void *)numbers);

ここでの numbers は、配列の先頭要素のアドレスとして使われています。

&numbers[0]

printf("%p\n", (void *)&numbers[0]);

これは、配列の0番目の要素 numbers[0] のアドレスです。
そのため、多くの場合 numbers と同じ値になります。

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

C言語では、配列の要素アクセスはポインタ計算と強く結びついています。

numbers[0]
*(numbers + 0)

この2つは同じ要素を表します。

同様に、次も同じ意味です。

numbers[1]
*(numbers + 1)

numbers + 1 は、先頭から1個先の int 要素を指すアドレスです。
1バイトではなく、int 1個分だけ進む点が重要です。

同じ値になることを確認する

サンプルコード

array_pointer_access.c という名前で保存します。

#include <stdio.h>

int main(void) {
    int numbers[3] = {10, 20, 30};

    printf("%d\n", numbers[0]);
    printf("%d\n", *(numbers + 0));
    printf("%d\n", numbers[1]);
    printf("%d\n", *(numbers + 1));

    return 0;
}

実行結果:

10
10
20
20

ポインタを使って配列を走査する

配列は添字だけでなく、ポインタを進めながら読むこともできます。

#include <stdio.h>

int main(void) {
    int numbers[4] = {10, 20, 30, 40};
    int *p = numbers;
    int i;

    for (i = 0; i < 4; i++) {
        printf("%d\n", *(p + i));
    }

    return 0;
}

実行結果:

10
20
30
40

int *p = numbers; の意味

numbers はここでは先頭要素のアドレスとして使われます。
そのため、int *p = numbers; と書くと、pnumbers[0] を指すポインタになります。

*(p + i) の意味

p + i は、p から i 個先の要素を指します。
そこに * を付けると、その位置の値を取り出せます。

配列を関数に渡すときの基本

配列を関数に渡すとき、実際には先頭要素のアドレスが渡されます。
そのため、関数側ではポインタとして受け取る形になります。

#include <stdio.h>

void print_array(int *arr, int size) {
    int i;

    for (i = 0; i < size; i++) {
        printf("%d\n", arr[i]);
    }
}

int main(void) {
    int numbers[4] = {10, 20, 30, 40};

    print_array(numbers, 4);

    return 0;
}

実行結果:

10
20
30
40

なぜ要素数も渡すのか

関数に渡された arr は、配列全体そのものではなく、先頭要素を指す情報として扱われます。
そのため、関数の中だけでは要素数が自動では分かりません。

このため、配列と一緒に要素数も別に渡す必要があります。

添字で受け取る書き方もできる

関数引数では、次のような書き方もよく使います。

void print_array(int arr[], int size)

この書き方でも、関数の中では arr は配列そのものではなく、先頭要素を指すものとして扱われます。
初学者の段階では、int *arr とほぼ同じ意味で使われると理解して問題ありません。

初心者がつまずきやすい点

配列名は普通のポインタ変数ではない

numbers は多くの場合ポインタのように使えますが、int *p と完全に同じものではありません。

たとえば次のような代入はできません。

numbers = numbers + 1;

配列名そのものを書き換えることはできません。
一方で、ポインタ変数なら次のように書けます。

int *p = numbers;
p = p + 1;

numbers + 1 は1バイト先ではない

numbers + 1 は、次の要素を指します。
int 配列なら int 1個分だけ進みます。

そのため、「アドレスに 1 を足すと1バイト進む」と考えないようにします。

範囲外アクセスは危険

次のようなコードは危険です。

int numbers[3] = {10, 20, 30};
printf("%d\n", numbers[3]);

有効な添字は 0 から 2 までです。
numbers[3] は範囲外アクセスであり、未定義動作になります。

同じことはポインタでも起こります。

printf("%d\n", *(numbers + 3));

これも安全ではありません。

よくあるエラー

warning: incompatible pointer types

原因: 配列やポインタの型が一致していません。
対処: int 配列なら int * で受け取るようにします。

例:

int numbers[3] = {10, 20, 30};
int *p = numbers;

実行結果がおかしい、または異常終了する

原因: 範囲外アクセスをしている可能性があります。
対処: 配列の要素数を確認し、0 から size - 1 の範囲だけを使います。

関数の中で要素数が分からない

原因: 配列を関数に渡すと、先頭要素を指す情報として扱われるためです。
対処: 配列と一緒に要素数も引数で渡します。

練習用コード

添字とポインタの両方で表示する

#include <stdio.h>

int main(void) {
    int values[5] = {2, 4, 6, 8, 10};
    int i;

    for (i = 0; i < 5; i++) {
        printf("%d %d\n", values[i], *(values + i));
    }

    return 0;
}

配列の合計を求める

#include <stdio.h>

int sum_array(int *arr, int size) {
    int i;
    int sum = 0;

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

    return sum;
}

int main(void) {
    int values[4] = {5, 10, 15, 20};

    printf("%d\n", sum_array(values, 4));

    return 0;
}

実行結果:

50

まとめ

今回のポイントは次のとおりです。

  • 配列名は多くの場合、先頭要素のアドレスとして使われる
  • array[i]*(array + i) は同じ要素を表す
  • ポインタ計算は要素の型単位で進む
  • 配列を関数に渡すときは、先頭要素を指す形で渡される
  • 関数の中では要素数が自動で分からないため、別に渡す必要がある
  • 範囲外アクセスは未定義動作であり、行ってはいけない

この回では、配列とポインタが強く結びついていることを確認しました。
配列の添字アクセスは、内部ではポインタの考え方とつながっています。

次回予告

次は、文字列処理の基本を学びます。
文字列は char 配列として扱うため、今回の内容を理解していると strlenstrcpy の動きも整理しやすくなります。

さらに学びたいあなたへ

用途ごとに選ぶ Linux のおすすめ本

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

のいのアバター のい UNIX Cafe マスター

Macintosh Color Classicから始まった旅は、長いWindows時代を経て、Windows10のサポート終了をきっかけにUNIXの世界へ戻ってきました。UNIX Cafeでは、UNIX・Linux・そしてMacな世界を、むずかしい言葉を使わず、物語のように書いています。プログラミングは、アイデアをコンピューターに伝えるための言葉です。簡単な単語と文法を覚えれば、誰でもコマンドを使えます。ぜひ一度、やさしいプログラミングの世界をのぞいてみてください。

Created by UNIX Cafe

目次