C言語ポインタと配列を完全理解!関係性と使い方を徹底解説

プログラミング・IT

C言語を学んでいると、「ポインタと配列って何が違うの?」「配列名がポインタになるってどういうこと?」と混乱することはありませんか?

実は、ポインタと配列は似ているようで違うものなんです。

でも、この2つの関係を理解すると、C言語の理解が一気に深まります。

今回は、ポインタと配列の関係性、違い、そして実際の使い方まで、初心者でも分かるように丁寧に解説していきます。

スポンサーリンク

ポインタと配列の基本を復習

まず、それぞれの基本を確認しましょう。

配列とは

配列は、同じ型のデータを連続して並べて格納する仕組みです。

int arr[5] = {10, 20, 30, 40, 50};

この配列は、メモリ上で連続した場所に格納されます。

メモリアドレス    値
0x1000          10  (arr[0])
0x1004          20  (arr[1])
0x1008          30  (arr[2])
0x100C          40  (arr[3])
0x1010          50  (arr[4])

int型は通常4バイトなので、各要素のアドレスは4ずつ増えています。

ポインタとは

ポインタは、変数のアドレス(メモリ上の場所)を格納する変数です。

int *p;  // int型へのポインタ変数

ポインタを使うと、変数のアドレスを通じて間接的にその変数にアクセスできます。

int x = 100;
int *p = &x;  // xのアドレスをpに代入

printf("%d\n", *p);  // 100が出力される(*pでxの値にアクセス)

配列名とポインタの不思議な関係

ここが多くの人が混乱するポイントです。

配列名は先頭要素のアドレスを表す

重要なポイント:配列名は、その配列の先頭要素のアドレスと同じ意味になります。

int arr[5] = {10, 20, 30, 40, 50};

printf("%p\n", arr);      // 配列の先頭アドレス
printf("%p\n", &arr[0]);  // 先頭要素のアドレス
// この2つは同じ値を表示する!

つまり、arr&arr[0] は等価なんです。

配列名をポインタに代入できる

配列名がアドレスを表すので、ポインタ変数に代入できます。

int arr[5] = {10, 20, 30, 40, 50};
int *p;

p = arr;  // これでOK!pはarrの先頭要素を指す

printf("%d\n", *p);  // 10が出力される

これは実質的に p = &arr[0]; と同じ意味です。

ポインタで配列要素にアクセスする方法

ポインタを使って配列の各要素にアクセスできます。

基本的なアクセス方法

#include <stdio.h>

int main(void) {
    int arr[5] = {10, 20, 30, 40, 50};
    int *p = arr;  // pは配列の先頭を指す

    printf("%d\n", *p);        // 10 (arr[0]と同じ)
    printf("%d\n", *(p + 1));  // 20 (arr[1]と同じ)
    printf("%d\n", *(p + 2));  // 30 (arr[2]と同じ)

    return 0;
}

重要な等価関係

  • arr[i]*(arr + i) と等価
  • &arr[i]arr + i と等価

ポインタのインクリメント

ポインタを+1すると、次の要素を指すようになります。

#include <stdio.h>

int main(void) {
    int arr[5] = {10, 20, 30, 40, 50};
    int *p = arr;

    printf("%d\n", *p);  // 10
    p++;                 // 次の要素へ
    printf("%d\n", *p);  // 20
    p++;                 // さらに次へ
    printf("%d\n", *p);  // 30

    return 0;
}

注意点:型のサイズ分だけ移動する

p++ は単に+1するのではなく、その型のサイズ分だけアドレスが進みます

  • char型ポインタ:+1バイト
  • int型ポインタ:+4バイト(通常)
  • double型ポインタ:+8バイト(通常)

配列記法とポインタ記法の比較

実は、ポインタ変数でも配列と同じ記法が使えます。

#include <stdio.h>

int main(void) {
    int arr[5] = {10, 20, 30, 40, 50};
    int *p = arr;

    // どちらの書き方でもOK
    printf("%d\n", arr[2]);  // 30
    printf("%d\n", p[2]);    // 30(ポインタでも[]が使える!)

    printf("%d\n", *(arr + 2));  // 30(配列でもポインタ演算が使える!)
    printf("%d\n", *(p + 2));    // 30

    return 0;
}

すべて同じ結果になります!

配列とポインタの重要な違い

似ているようで、実は大きな違いがあります。

違い1:実体とアドレス

配列

配列を宣言すると、実際にデータを格納する領域が確保されます。

int arr[5];  // 20バイト(4バイト×5)のメモリが確保される

ポインタ

ポインタ変数は、アドレスを格納する変数でしかありません。

int *p;  // アドレスを格納する変数(通常4または8バイト)

違い2:変更可能性

これが最も重要な違いです。

配列名は定数ポインタのように振る舞う

配列名自体は変更できません。

int arr[5] = {10, 20, 30, 40, 50};

arr++;  // エラー!配列名は変更できない
arr = arr + 1;  // エラー!

ポインタ変数は変更できる

ポインタは普通の変数なので、指す先を変更できます。

int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;

p++;  // OK!pが次の要素を指すようになる
p = arr + 3;  // OK!pが4番目の要素を指すようになる

違い3:sizeof演算子の結果

#include <stdio.h>

int main(void) {
    int arr[5] = {10, 20, 30, 40, 50};
    int *p = arr;

    printf("sizeof(arr) = %zu\n", sizeof(arr));  // 20(配列全体のサイズ)
    printf("sizeof(p) = %zu\n", sizeof(p));      // 4 or 8(ポインタ変数のサイズ)

    return 0;
}

配列にsizeofを使うと配列全体のサイズが返りますが、ポインタではポインタ変数自体のサイズしか返りません。

ループでの配列アクセス

配列の全要素を処理する典型的なパターンを見てみましょう。

配列記法を使う方法

#include <stdio.h>

int main(void) {
    int arr[5] = {10, 20, 30, 40, 50};
    int i;

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

    return 0;
}

これが最も分かりやすい書き方です。

ポインタ演算を使う方法

#include <stdio.h>

int main(void) {
    int arr[5] = {10, 20, 30, 40, 50};
    int *p;

    for (p = arr; p < arr + 5; p++) {
        printf("%d ", *p);
    }
    printf("\n");

    return 0;
}

ポインタをインクリメントしながら要素にアクセスします。

どちらを使うべき?

現代のC言語では、配列記法(arr[i])の使用が推奨されます。

理由:

  • コードが読みやすい
  • 意図が明確
  • 現代のコンパイラは最適化が優秀

ポインタ演算は、昔は高速化のために使われましたが、今ではコンパイラが自動的に最適化してくれます。

関数への配列の渡し方

関数に配列を渡す際、実際にはポインタが渡されます。

配列を引数に渡す

#include <stdio.h>

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

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

    printArray(numbers, 5);

    return 0;
}

ポインタで受け取る(等価)

実は、以下の書き方も全く同じ意味です。

void printArray(int *arr, int size) {
    // 処理内容は上と全く同じ
}

int arr[]int *arr は、関数の引数としては完全に同じです。

重要な注意点

関数内でsizeofを使うと、配列全体のサイズは分かりません。

void printArray(int arr[], int size) {
    printf("%zu\n", sizeof(arr));  // ポインタのサイズが返る(4 or 8)
    // 配列全体のサイズは分からない!
}

だから、配列のサイズは別の引数で渡す必要があります。

文字列とポインタ

文字列は文字の配列なので、ポインタと深く関係します。

文字配列

char str1[10] = "Hello";

これは配列なので、str1自体は変更できません。

文字列リテラルとポインタ

char *str2 = "Hello";

この場合、”Hello”は文字列リテラル(読み取り専用)で、str2はそのアドレスを指します。

違いに注意

char str1[10] = "Hello";
str1[0] = 'h';  // OK(配列の内容を変更)

char *str2 = "Hello";
str2[0] = 'h';  // 未定義動作!文字列リテラルは変更できない

ポインタと配列の実用例

実際のプログラムでどう使うか見てみましょう。

配列の合計を計算

#include <stdio.h>

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

    for (i = 0; i < size; i++) {
        total += arr[i];  // または total += *(arr + i);
    }

    return total;
}

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

    result = sum(numbers, 5);
    printf("合計: %d\n", result);  // 150

    return 0;
}

配列の要素を2倍にする

#include <stdio.h>

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

    for (i = 0; i < size; i++) {
        arr[i] *= 2;  // 元の配列を直接変更
    }
}

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

    doubleArray(numbers, 5);

    for (i = 0; i < size; i++) {
        printf("%d ", numbers[i]);  // 20 40 60 80 100
    }

    return 0;
}

ポインタ(配列)を渡すと、関数内で元の配列を変更できます。

よくある間違いと注意点

初心者がつまづきやすいポイントをまとめます。

間違い1:配列名をインクリメント

int arr[5] = {10, 20, 30, 40, 50};

arr++;  // エラー!配列名は変更できない

正しい方法

int *p = arr;
p++;  // OK

間違い2:ポインタを初期化せずに使う

int *p;
*p = 100;  // 危険!pが何を指しているか不明

正しい方法

int x;
int *p = &x;
*p = 100;  // OK

間違い3:関数内でsizeofを使う

void func(int arr[]) {
    int size = sizeof(arr) / sizeof(arr[0]);  // 間違い!
    // arrはポインタなので正しく計算できない
}

正しい方法

void func(int arr[], int size) {
    // サイズを引数で受け取る
}

int main(void) {
    int numbers[5] = {1, 2, 3, 4, 5};
    int size = sizeof(numbers) / sizeof(numbers[0]);  // ここで計算

    func(numbers, size);
    return 0;
}

間違い4:文字列リテラルの変更

char *str = "Hello";
str[0] = 'h';  // 未定義動作!

正しい方法

char str[] = "Hello";  // 配列として宣言
str[0] = 'h';  // OK

まとめ:配列とポインタの使い分け

配列とポインタの関係を理解すると、C言語の理解が深まります。

配列とポインタの関係

  • 配列名は先頭要素のアドレスを表す
  • arr[i]*(arr + i) と等価
  • 配列名をポインタに代入できる
  • ポインタ変数でも配列記法が使える

重要な違い

項目配列ポインタ
実体データを格納する領域を確保アドレスを格納する変数
変更配列名自体は変更不可ポインタ変数は変更可能
sizeof配列全体のサイズポインタ変数のサイズ
初期化宣言時に領域確保明示的に代入が必要

使い分けのポイント

配列を使う場面

  • サイズが決まっている場合
  • データをまとめて管理したい場合
  • 自動的にメモリを確保したい場合

ポインタを使う場面

  • 動的にメモリを確保する場合(malloc)
  • 関数間でデータを効率的に渡す場合
  • データ構造(リスト、ツリー等)を作る場合

コーディングのベストプラクティス

  1. 配列アクセスには[]を使う(arr[i])が読みやすい
  2. 配列サイズは別途渡す(sizeof問題を避ける)
  3. ポインタは必ず初期化する
  4. 文字列リテラルは変更しない

C言語のポインタと配列は、最初は難しく感じるかもしれません。

でも、「配列名はアドレス」「ポインタは変数」という基本を押さえれば、必ず理解できます。

たくさんコードを書いて、実際に動かしながら学んでいきましょう!

コメント

タイトルとURLをコピーしました