閉じる

【C言語】ポインタとは何かを図解で理解する|配列・関数・構造体まで

C言語の学習で多くの人がつまずくポイントがポインタです。

何となくアドレスを扱うものだと理解していても、配列や関数、構造体と組み合わさると一気に難しく感じられます。

本記事では、図解を交えながら、ポインタの本質から配列・関数・構造体までを段階的に整理して解説します。

ポインタとは何か

ポインタの基本概念とメモリ上のイメージ

まず、ポインタを一言で言うと「メモリ上の場所(アドレス)を覚えておくための変数」です。

普通の変数が「値」を持つのに対して、ポインタ変数は「値が置かれている場所」を持ちます。

メモリのイメージを図でつかむ

上の図のように、メモリはアドレス付きの箱が縦横に並んだ広い倉庫だと考えるとイメージしやすくなります。

通常の変数aはこの箱の1つを占有し、その箱の中に値10が入ります。

一方、ポインタpは「どの箱を参照するか」という情報、つまりアドレスを保持します。

ポインタ変数の宣言と意味

ポインタ変数は次のように宣言します。

C言語
#include <stdio.h>

int main(void) {
    int  a = 10;   // 通常の int 変数
    int *p = &a;   // int へのポインタ変数 p

    printf("a の値: %d\n", a);
    printf("a のアドレス: %p\n", (void *)&a);
    printf("p の値(指しているアドレス): %p\n", (void *)p);
    printf("*p の値(ポインタが指す先の値): %d\n", *p);

    return 0;
}
実行結果
a の値: 10
a のアドレス: 0x7ffeefbff56c    (例)
p の値(指しているアドレス): 0x7ffeefbff56c
*p の値(ポインタが指す先の値): 10

この例から、p 自身もメモリ上に存在する変数であり、その中身が「a のアドレス」であることが分かります。

アドレス演算子(&)と間接演算子(*)の意味

ポインタに登場する&*は、意味が混乱しやすい記号です。

ここをきちんと整理しておきます。

& (アドレス演算子) の役割

&「その変数が格納されているアドレスを取得する」演算子です。

C言語
int a = 10;
int *p = &a;  // &a が「a のアドレス」

ここで&aは、aという変数の「場所」を指す情報です。

これをポインタpに代入しています。

* (間接演算子) の2つの顔

*には2つの使われ方があり、混乱の元になります。

  1. 宣言時に使う*
    「この変数はポインタですよ」という意味を持ちます。
    int *p;   // int へのポインタ変数 p
    
  2. 式の中で使う*
    これは間接参照演算子と呼ばれ、「ポインタが指す先の値を取り出す」という意味になります。
    int a = 10;
    int *p = &a;
    
    *p = 20;  // p が指している a の中身を 20 に書き換える
    

& と * は「逆向きの操作」

& は「場所を聞く」、* は「場所に行って中身を見る」というイメージで押さえておくと理解しやすくなります。

変数とポインタの違いを図解で理解

通常の変数とポインタ変数は、どちらも「変数」ですが、持っている中身が異なります。

変数とポインタの違いを一覧で整理

次の表で、役割の違いを整理します。

種類中身(値)主な用途
通常の変数実際のデータ(数値など)データそのものの保持
ポインタ変数アドレス(場所の情報)データの場所を参照・共有・操作

通常の変数は「データの箱」そのもの、ポインタは「箱への住所メモ」と考えると区別しやすくなります。

変数とポインタの相互作用の例

C言語
#include <stdio.h>

int main(void) {
    int  x = 5;    // 通常の変数 x
    int *px = &x;  // x を指すポインタ px

    printf("x の値: %d\n", x);
    printf("px が指す値: %d\n", *px);

    // ポインタ経由で x の値を変更
    *px = 100;

    printf("x の値(書き換え後): %d\n", x);
    printf("px が指す値(書き換え後): %d\n", *px);

    return 0;
}
実行結果
x の値: 5
px が指す値: 5
x の値(書き換え後): 100
px が指す値(書き換え後): 100

ポインタpxはあくまで「場所」を知っているだけですが、その場所の中身を書き換えることでx の値を間接的に操作できることが分かります。

配列とポインタ

配列とポインタの関係は、C言語を理解するうえで非常に重要です。

ここを押さえておくと、文字列や関数への配列渡しがぐっと分かりやすくなります。

配列名とポインタの関係

配列名は「先頭要素へのポインタ」のように振る舞う

C言語では、aのような配列名は、多くの場面で「先頭要素のアドレス」として扱われます。

つまりa&a[0]とほぼ同じ意味になります。

C言語
#include <stdio.h>

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

    printf("a の値(先頭アドレス): %p\n", (void *)a);
    printf("&a[0] の値: %p\n", (void *)&a[0]);

    return 0;
}
実行結果
a の値(先頭アドレス): 0x7ffee3d3b860   (例)
&a[0] の値:          0x7ffee3d3b860

このように、配列名自体は変数ではないものの、式の中ではint *型のポインタのように扱われます。

ポインタによる配列アクセスと添字アクセス

添字アクセスとポインタ演算は同じ意味

配列へのアクセスは通常a[1]のように書きますが、これはポインタ演算で表すこともできます。

C言語
#include <stdio.h>

int main(void) {
    int a[4] = {10, 20, 30, 40};
    int *p = a;  // a は先頭要素へのポインタに変換される

    printf("a[0] = %d, *(p + 0) = %d\n", a[0], *(p + 0));
    printf("a[1] = %d, *(p + 1) = %d\n", a[1], *(p + 1));
    printf("a[2] = %d, *(p + 2) = %d\n", a[2], *(p + 2));
    printf("a[3] = %d, *(p + 3) = %d\n", a[3], *(p + 3));

    return 0;
}
実行結果
a[0] = 10, *(p + 0) = 10
a[1] = 20, *(p + 1) = 20
a[2] = 30, *(p + 2) = 30
a[3] = 40, *(p + 3) = 40

添字演算a[i]は、実は*(a + i)というポインタ演算の糖衣構文になっています。

ポインタインクリメントとメモリ上の移動

C言語
#include <stdio.h>

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

    for (int i = 0; i < 4; i++) {
        printf("p = %p, *p = %d\n", (void *)p, *p);
        p++;  // 次の要素へ進む
    }

    return 0;
}
実行結果
p = 0x7ffee3d3b860, *p = 10
p = 0x7ffee3d3b864, *p = 20
p = 0x7ffee3d3b868, *p = 30
p = 0x7ffee3d3b86c, *p = 40

ここで、アドレスが 4 バイトずつ増えていることに注目してください。

これはintが4バイト型であるためで、ポインタ演算では「要素数単位」で移動することが分かります。

文字列(char配列)とポインタの扱い方

C言語の文字列は「末尾に ‘\0’ が付いた char の配列」です。

この文字列もポインタと非常に相性が良い構造になっています。

文字列リテラルと char 配列

C言語
#include <stdio.h>

int main(void) {
    char s1[] = "Hello";  // 配列
    char *s2  = "Hello";  // 文字列リテラルへのポインタ

    printf("s1: %s\n", s1);
    printf("s2: %s\n", s2);

    return 0;
}
実行結果
s1: Hello
s2: Hello

s1 は実体の配列s2 は文字列リテラル(読み取り専用領域)を指すポインタです。

どちらも%sで表示可能ですが、s2 を通じて文字を書き換えるのは未定義動作となります。

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

C言語
#include <stdio.h>

int main(void) {
    char str[] = "ABC";
    char *p = str;

    while (*p != '\0') {          // 終端文字までループ
        printf("%c\n", *p);       // ポインタが指す文字を表示
        p++;                      // 次の文字へ
    }

    return 0;
}
実行結果
A
B
C

このように、文字列は「char へのポインタ」と相性がよく、標準ライブラリの多くも char ポインタを受け取る形になっています

関数とポインタ

ポインタは関数と組み合わせることで「呼び出し元の変数を直接書き換える」ことが可能になります。

さらに、配列を関数に渡すとき、暗黙にポインタとして扱われる点も重要です。

関数にポインタを渡す

「値渡し」と「アドレス渡し」の違い

C言語では引数はすべて値渡しですが、「アドレスという値」を渡すことで、実質的に参照渡しのようなことができます。

C言語
#include <stdio.h>

void increment(int *p) {
    // p が指す先の値を 1 増やす
    (*p)++;
}

int main(void) {
    int x = 10;

    printf("呼び出し前: x = %d\n", x);
    increment(&x);  // x のアドレスを渡す
    printf("呼び出し後: x = %d\n", x);

    return 0;
}
実行結果
呼び出し前: x = 10
呼び出し後: x = 11

関数にポインタを渡すことで、関数の外側の変数を書き換えられることが分かります。

配列を関数に渡すときのポインタ挙動

配列引数は実質「ポインタ引数」

配列を関数に渡すとき、関数側では次の2つの書き方ができます。

C言語
void func1(int a[10]);   // 書き方1: 配列のように書く
void func2(int *a);      // 書き方2: ポインタとして書く

どちらの宣言も意味は同じで、引数はint *型として扱われます。

関数に配列を渡すと、配列名は先頭要素へのポインタに変換されるからです。

C言語
#include <stdio.h>

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

int main(void) {
    int arr[3] = {1, 2, 3};
    print_array(arr, 3);  // 先頭アドレスが渡される

    return 0;
}
実行結果
a[0] = 1
a[1] = 2
a[2] = 3

ここでは配列をそっくりコピーしているわけではなく、先頭要素のアドレスのみが渡されている点が重要です。

関数ポインタとは何かと基本的な使い方

関数そのものも、実はメモリ上に配置された「コードの場所」を持っており、その先頭アドレスを扱うのが関数ポインタです。

基本的な宣言と呼び出し

C言語
#include <stdio.h>

// 2つの int を引数に取り、int を返す関数
int add(int a, int b) {
    return a + b;
}

int main(void) {
    // 関数ポインタの宣言
    int (*fp)(int, int);

    // add 関数のアドレスを代入
    fp = add;   // &add と書いてもよい

    // 関数ポインタ経由で呼び出し
    int result1 = add(3, 4);      // 通常の呼び出し
    int result2 = fp(3, 4);       // 関数ポインタを使った呼び出し
    int result3 = (*fp)(3, 4);    // これも同じ意味

    printf("result1 = %d\n", result1);
    printf("result2 = %d\n", result2);
    printf("result3 = %d\n", result3);

    return 0;
}
実行結果
result1 = 7
result2 = 7
result3 = 7

関数ポインタのイメージ

関数ポインタは、コールバック関数イベントハンドラなど、柔軟な設計を行う際に多用されます。

構造体とポインタ

構造体とポインタを組み合わせると、複雑なデータ構造(リンクリストやツリーなど)を表現できるようになります。

ここでは、基本的な操作から動的メモリ確保までを解説します。

構造体へのポインタとメンバアクセス

構造体ポインタと -> 演算子

構造体のメンバにアクセスするには.演算子を使いますが、ポインタ経由の場合は->演算子を使います。

C言語
#include <stdio.h>

// 人を表す構造体
struct Person {
    char name[32];
    int  age;
};

int main(void) {
    struct Person alice = {"Alice", 20};
    struct Person *p = &alice;    // 構造体へのポインタ

    // 通常のアクセス
    printf("name: %s, age: %d\n", alice.name, alice.age);

    // ポインタ経由のアクセス
    printf("name: %s, age: %d\n", (*p).name, (*p).age);
    printf("name: %s, age: %d\n", p->name, p->age); // -> 演算子で簡略化

    return 0;
}
実行結果
name: Alice, age: 20
name: Alice, age: 20
name: Alice, age: 20

p->name は (*p).name の糖衣構文です。

構造体配列とポインタの関係

構造体も配列にすれば、「構造体の配列」として複数データをまとめて扱えます。

このときも、配列名とポインタの関係は基本的に同じです。

C言語
#include <stdio.h>

struct Point {
    int x;
    int y;
};

int main(void) {
    struct Point points[3] = {
        {1, 2},
        {3, 4},
        {5, 6}
    };

    struct Point *p = points;  // 先頭要素へのポインタ

    // 添字アクセス
    printf("points[1]: (%d, %d)\n", points[1].x, points[1].y);

    // ポインタと -> を用いたアクセス
    printf("p[1]:       (%d, %d)\n", p[1].x, p[1].y);
    printf("*(p+1):     (%d, %d)\n", (p + 1)->x, (p + 1)->y);

    return 0;
}
実行結果
points[1]: (3, 4)
p[1]:       (3, 4)
*(p+1):     (3, 4)

ここでもp[i] は *(p + i) の別表現であることを確認できます。

動的メモリ確保(malloc)と構造体ポインタ

実用的なプログラムでは、必要になったタイミングでメモリを確保し、柔軟に構造体を生成することがよくあります。

そのときに使うのがmallocなどの動的メモリ確保関数です。

malloc で構造体を1つ確保する

C言語
#include <stdio.h>
#include <stdlib.h>  // malloc, free

struct Person {
    char name[32];
    int  age;
};

int main(void) {
    // struct Person 型のメモリを 1 つ分動的に確保
    struct Person *p = (struct Person *)malloc(sizeof(struct Person));
    if (p == NULL) {
        // メモリ確保に失敗した場合
        fprintf(stderr, "メモリの確保に失敗しました\n");
        return 1;
    }

    // メンバに値を設定
    // 簡略のため安全性を考慮しない strcpy 相当処理を記述
    p->name[0] = 'B';
    p->name[1] = 'o';
    p->name[2] = 'b';
    p->name[3] = '\0';
    p->age = 30;

    printf("name: %s, age: %d\n", p->name, p->age);

    // 使用後は必ず解放
    free(p);

    return 0;
}
実行結果
name: Bob, age: 30

malloc で確保したメモリは必ず free で解放する必要があります。

これを怠るとメモリリークを引き起こします。

構造体配列をまとめて確保する

C言語
#include <stdio.h>
#include <stdlib.h>

struct Point {
    int x;
    int y;
};

int main(void) {
    int n = 3;

    // Point 構造体を n 個分まとめて確保
    struct Point *ps = (struct Point *)malloc(sizeof(struct Point) * n);
    if (ps == NULL) {
        fprintf(stderr, "メモリの確保に失敗しました\n");
        return 1;
    }

    // 各要素に値を設定
    for (int i = 0; i < n; i++) {
        ps[i].x = i;
        ps[i].y = i * 10;
    }

    // 値を表示
    for (int i = 0; i < n; i++) {
        printf("ps[%d] = (%d, %d)\n", i, ps[i].x, ps[i].y);
    }

    // 解放
    free(ps);

    return 0;
}
実行結果
ps[0] = (0, 0)
ps[1] = (1, 10)
ps[2] = (2, 20)

このように、動的に確保した構造体配列も、ポインタと配列の関係を理解していれば自然に扱えるようになります。

まとめ

本記事では、C言語のポインタについて、メモリのイメージから始めて、配列・文字列・関数・構造体・動的メモリ確保までを一通り整理しました。

ポイントは、ポインタは「値ではなく場所(アドレス)を扱う変数」であり、& と * はその場所と中身を行き来するための操作だということです。

配列名が先頭要素へのポインタとして振る舞うこと、関数や構造体とも自然に結びつくことが分かれば、ポインタは怖いものではなくなります。

ぜひ図解イメージを頭に描きながら、自分でも小さなサンプルを書いて手を動かしてみてください。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!