閉じる

【C言語】ポインタとは?メモリアドレスの基本を初心者向けにやさしく解説

C言語の学習で最初の大きな壁になりやすいのが「ポインタ」です。

名前を聞くだけで難しそうな印象がありますが、実はメモリ上の場所(アドレス)を扱うための仕組みにすぎません。

本記事では、C言語初心者の方を対象に、ポインタのしくみや使い方を、具体例とサンプルコードを交えながら丁寧に解説していきます。

C言語のポインタとは

ポインタとは何か

C言語におけるポインタとは、「メモリ上のアドレス(場所)を値として持つ変数」のことです。

ふつうの変数は「整数」や「小数」「文字」などのデータそのものを入れますが、ポインタは「そのデータが置かれている場所」を持ちます。

言い換えると、次のように区別できます。

  • ふつうの変数: データそのものを入れる箱
  • ポインタ変数: データのある場所(住所)を書いたメモ

日常生活にたとえると、本そのものが通常の変数、本棚の位置が書かれたメモがポインタです。

本は内容を読む対象、メモは「どこに本があるか」を教えてくれる情報になります。

この「場所(アドレス)を扱う」感覚が、ポインタを理解するうえでとても重要です。

変数とメモリアドレスの関係

コンピュータは、メモリという大きな空間を、小さな部屋の集まりとして管理しています。

それぞれの部屋には「アドレス」と呼ばれる番号がついています。

C言語で変数を宣言すると、その変数のためにメモリの一部が確保され、そこにアドレスが割り当てられます。

例として、次のような変数を考えます。

C言語
#include <stdio.h>

int main(void) {
    int x = 10;  // 整数型変数xを宣言し、10で初期化

    printf("xの値: %d\n", x);           // xに入っている値を表示
    printf("xのアドレス: %p\n", &x);   // xのアドレスを表示(&x)

    return 0;
}

出力例(環境によりアドレスは異なります):

実行結果
xの値: 10
xのアドレス: 0x7ffce5d1a9bc

この例から分かるように、変数xそのものは値10を持ち、&xは「xが置かれているメモリアドレス」を表します。

ここで重要なのは、変数には必ず「値」と「アドレス」が存在するという点です。

ふだんは値だけを意識してプログラムを書きますが、ポインタを扱うときはアドレスも明示的に扱うようになります。

なぜポインタが必要なのか

C言語にポインタが用意されている理由はいくつかありますが、初心者の方にとって重要なポイントは次の3つです。

  1. 関数に大きなデータを効率よく渡すため
    大きな配列や構造体を関数に渡すとき、値そのものをコピーするとコストがかかります。ポインタで「場所だけ」を渡せば、高速かつメモリの節約になります。
  2. 関数から複数の値を返したいときに使える
    C言語の関数は原則として戻り値を1つしか返せませんが、ポインタを使えば、関数の外にある変数を直接書き換えることができ、実質的に複数の値を返せます。
  3. 動的メモリ確保(mallocなど)を行うため
    実行時に必要な分だけメモリを確保するmallocなどの関数は、確保したメモリのアドレスを返します。このアドレスを受け取るためにポインタが必須になります。

このように、ポインタはC言語の「柔軟さ」と「効率の良さ」を支える核心的な仕組みです。

ポインタの基本構文

アドレス演算子&とは

&演算子はアドレス演算子と呼ばれ、「その変数のアドレスを求める」ために使います。

使い方はとても単純で、変数名の前に&を付けるだけです。

C言語
#include <stdio.h>

int main(void) {
    int a = 42;

    // aの値とアドレスを表示
    printf("aの値: %d\n", a);
    printf("aのアドレス: %p\n", &a);  // &a で aのアドレスを取得

    return 0;
}

&変数名という形で使われ、配列名やリテラル(例えば10'A')には使えません。

間接演算子*とは

*演算子は間接演算子(デリファレンス演算子)と呼ばれます。

ポインタが指しているアドレス先の「中身の値」にアクセスするために使います。

基本形は次のようになります。

  • p … ポインタ変数(アドレスが入っている)
  • *p … ポインタpが指す先の

次の例で確認してみましょう。

C言語
#include <stdio.h>

int main(void) {
    int x = 100;
    int *p;        // int型のポインタpを宣言

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

    printf("xの値: %d\n", x);
    printf("pに入っているアドレス: %p\n", p);
    printf("*pの値: %d\n", *p);  // pが指す先(=x)の値を表示

    // ポインタ経由でxを書き換える
    *p = 200;
    printf("xの新しい値: %d\n", x);

    return 0;
}
実行結果
xの値: 100
pに入っているアドレス: 0x7ffeeff7991c
*pの値: 100
xの新しい値: 200

このように、*pは「pが指している変数そのもの」として振る舞います。

ただし、pが正しいアドレスを指していないと、デリファレンス(*p)は非常に危険です。

これについては後半で詳しく説明します。

ポインタ変数の宣言と初期化の書き方

ポインタ変数の宣言は、基本的に次の形になります。

  • 型名 *ポインタ変数名;

例えば、整数型intへのポインタは次のように書きます。

C言語
int *p;   // int型の値を指すポインタ変数p

初期化まで一緒に行う場合は、次のように書きます。

C言語
#include <stdio.h>

int main(void) {
    int n = 10;

    int *p = &n;  // nのアドレスで初期化

    printf("nの値: %d\n", n);
    printf("*pの値: %d\n", *p);

    return 0;
}

ここで重要なのは、ポインタには必ず「有効なアドレス」か「NULL」を代入してから使うということです。

宣言しただけで中身を設定しないポインタは、何が入っているか分からない危険な状態になります。

int型とint型ポインタ

int型」と「int *型」は、まったく別物です。

これをしっかり区別しておくことが、ポインタ理解の第一歩です。

  • int x; … 整数そのものを持つ変数
  • int *p; … 整数が格納されているアドレスを持つ変数

次の表で整理してみます。

種類中身(値)主な用途
int型int x;10, -5, 100 などの整数計算対象そのものの値を保持
int型ポインタint *p;0x7ff… のようなアドレス値整数のある場所を指し示す

「ポインタ」はあくまでアドレスを扱う型であり、指す先の型(int, char, doubleなど)を必ず意識する必要があります。

NULLポインタとは

NULLポインタとは、「どの有効なメモリアドレスも指していない」ことを明示するための特別な値です。

C言語ではNULLマクロが定義されており、これをポインタに代入することで、「このポインタはまだどこも指していない」と表現できます。

C言語
#include <stdio.h>

int main(void) {
    int *p = NULL;  // どこも指していないことを明示

    if (p == NULL) {
        printf("pはどこも指していません。\n");
    }

    // *p = 10;  // ← これは絶対にしてはいけない(クラッシュの原因)

    return 0;
}
実行結果
pはどこも指していません。

未使用のポインタは必ずNULLで初期化し、使う前にNULLチェックを行う習慣をつけると、安全なポインタ操作につながります。

配列とポインタの関係を理解する

配列名とポインタの関係

C言語では、配列とポインタには密接な関係があります。

特に重要なのは、「配列名」はほとんどの場合、その配列の先頭要素のアドレスとして扱われるという性質です。

C言語
#include <stdio.h>

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

    printf("a[0]の値: %d\n", a[0]);
    printf("aの値(先頭アドレス): %p\n", a);      // 配列名a
    printf("&a[0]の値: %p\n", &a[0]);            // 先頭要素のアドレス
    printf("&a[1]の値: %p\n", &a[1]);            // 2番目の要素のアドレス

    return 0;
}

出力例(アドレスは環境により異なります):

実行結果
a[0]の値: 10
aの値(先頭アドレス): 0x7ffee7744890
&a[0]の値: 0x7ffee7744890
&a[1]の値: 0x7ffee7744894

このように、配列名a&a[0]とほぼ同じ意味で使われます。

ただし、「配列名自体」は代入先にはできないという違いがあります。

C言語
int a[3];
int *p;

p = a;      // OK: pに配列の先頭アドレスを代入
// a = p;   // NG: 配列名に代入することはできない

添字演算とポインタ演算(a[i]と*(a+i)の関係)

配列の要素にアクセスするa[i]という書き方は、実は*(a + i)の糖衣構文(短く書ける表現)です。

  • a … 配列の先頭アドレス
  • a + i … 先頭からi個分だけ進めたアドレス
  • *(a + i) … そのアドレスにある「値」

次のコードで動きを確認してみます。

C言語
#include <stdio.h>

int main(void) {
    int a[5] = {10, 20, 30, 40, 50};
    int *p = a;  // 配列aの先頭アドレスをpに代入

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

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

このように、a[i]・(a + i)・(p + i)は同じ値を指すことが分かります。

配列とポインタを同じように扱える場面が多いのは、この性質によるものです。

文字列とcharポインタ

C言語の文字列は、実体としてはchar型の配列です。

例えば、次の宣言は文字列”ABC”を表します。

C言語
char str[] = "ABC";  // {'A', 'B', 'C', '
char str[] = "ABC";  // {'A', 'B', 'C', '\0'} という配列
'} という配列

ここで"ABC"は「先頭文字’A’のアドレス」として扱われるため、次のようなchar *ポインタに代入できます。

C言語
#include <stdio.h>

int main(void) {
    char str[] = "Hello";
    char *p = str;        // 先頭アドレスを代入

    printf("str[0]: %c\n", str[0]);
    printf("*p: %c\n", *p);

    // ポインタを進めて文字列を走査
    printf("1文字ずつ表示: ");
    for (int i = 0; p[i] != '
#include <stdio.h>
int main(void) {
char str[] = "Hello";
char *p = str;        // 先頭アドレスを代入
printf("str[0]: %c\n", str[0]);
printf("*p: %c\n", *p);
// ポインタを進めて文字列を走査
printf("1文字ずつ表示: ");
for (int i = 0; p[i] != '\0'; i++) {
printf("%c ", p[i]);
}
printf("\n");
return 0;
}
'; i++) { printf("%c ", p[i]); } printf("\n"); return 0; }
実行結果
str[0]: H
*p: H
1文字ずつ表示: H e l l o

文字列処理の標準関数(例えばstrlenstrcpy)は、ほとんどがcharポインタを引数に取る形になっています。

そのため、文字列 = char配列 + charポインタという関係を理解しておくことが重要です。

関数引数での配列とポインタ

C言語では、配列名を関数に渡すと、先頭要素へのポインタとして渡されるという性質があります。

そのため、次の2つの関数宣言は、ほぼ同じ意味になります。

C言語
void func1(int a[], int n);     // 配列らしい書き方
void func2(int *a, int n);      // ポインタとしての書き方

どちらも、呼び出し側でint arr[10];を渡せます。

C言語
#include <stdio.h>

// 配列風の宣言
void print_array1(int a[], int n) {
    for (int i = 0; i < n; i++) {
        printf("%d ", a[i]);  // a[i]でアクセス
    }
    printf("\n");
}

// ポインタ風の宣言
void print_array2(int *a, int n) {
    for (int i = 0; i < n; i++) {
        printf("%d ", *(a + i));  // *(a + i)でアクセス
    }
    printf("\n");
}

int main(void) {
    int data[5] = {1, 2, 3, 4, 5};

    print_array1(data, 5);  // 配列名を渡す
    print_array2(data, 5);  // 同じく配列名を渡す

    return 0;
}
実行結果
1 2 3 4 5 
1 2 3 4 5

このように、関数の引数として配列を渡す = ポインタを渡すという仕組みになっていることを理解すると、ヘッダファイルや標準ライブラリ関数の宣言が読みやすくなります。

C言語初心者がつまずきやすいポイントと注意点

ポインタと変数の混同

初心者の方がもっとも混乱しやすいのは、「変数そのもの」と「そのアドレスを持つポインタ」を混同してしまうことです。

次のようなコードを例に、丁寧に見比べてみます。

C言語
#include <stdio.h>

int main(void) {
    int x = 10;
    int *p = &x;  // pはxのアドレスを持つ

    printf("xの値: %d\n", x);     // xそのものの値
    printf("pの値(アドレス): %p\n", p);   // pが持つアドレス
    printf("*pの値: %d\n", *p);   // pが指す先(x)の値

    return 0;
}

ここで混同しないためのポイントは、「pにはアドレスが入っていて、*pがxそのもの」と覚えることです。

  • x … 値10
  • &x … xのアドレス
  • p … &x(アドレス)
  • *p … x(値10)

「&とは互いを打ち消すペアになりやすい」という視点も役立ちます。

例えば(&x) == xとなります。

未初期化ポインタとダングリングポインタの危険性

ポインタには非常に危険な落とし穴が2つあります。

1つ目は未初期化ポインタです。

宣言だけして値を代入していないポインタには、不定なゴミアドレスが入っています。

C言語
#include <stdio.h>

int main(void) {
    int *p;        // 初期化していないポインタ(危険)
    // *p = 10;    // これを実行すると、どこを書き換えるか分からず危険

    return 0;
}

この状態で*pにアクセスすると、プログラムがクラッシュしたり、意図しないメモリを書き換えてバグの原因になります。

2つ目はダングリングポインタ(ぶら下がりポインタ)です。

すでに無効になったメモリを指し続けているポインタのことを指します。

典型例は、次のような関数からローカル変数のアドレスを返してしまうケースです。

C言語
#include <stdio.h>

int *danger_function(void) {
    int x = 123;   // 関数内のローカル変数
    return &x;     // ローカル変数のアドレスを返す(危険)
}

int main(void) {
    int *p = danger_function();  // pはすでに無効なアドレスを指す

    // printf("%d\n", *p);  // どんな値になるか分からない(未定義動作)

    return 0;
}

関数が終了した時点で、そのローカル変数は無効になります。

それなのに、そのアドレスを外で使おうとすると、どんな挙動になるかは保証されません。

未初期化ポインタとダングリングポインタを防ぐためには、次のような習慣が大切です。

  • ポインタを宣言したら必ずNULLで初期化する
  • メモリをfreeしたら、そのポインタをNULLに代入しておく
  • 関数からは「有効期間が十分に長い」もののアドレスだけを返す(グローバル変数、static変数、動的確保したメモリなど)

&と*のつけ忘れ・つけすぎエラーを防ぐ考え方

ポインタを使い始めると、&を付け忘れたり、*を付けすぎたりしてコンパイルエラーになることが増えます。

これを防ぐためには、「今扱っているのは値なのか、アドレスなのか」を常に意識することが重要です。

例として、整数を読み取るscanfを考えてみます。

C言語
#include <stdio.h>

int main(void) {
    int x;

    printf("整数を入力してください: ");
    scanf("%d", &x);  // &x を渡すことに注意

    printf("入力された値: %d\n", x);

    return 0;
}

scanf「読み込んだ値を書き込む先のアドレス」を引数に取る関数です。

そのため、scanf("%d", x);ではなくscanf("%d", &x);と書く必要があります。

ここでの考え方はシンプルです。

  • 関数が「値を書き込む」必要があるとき → アドレス(&変数)を渡す
  • すでにポインタを持っているとき → そのままポインタを渡す(新たに&をつけない)

「&をつけるとアドレスになる」「をつけるとアドレスから中身の値に戻る」という対応関係を頭の中で整理しておくと、&やの数を間違えにくくなります。

ポインタの型違いによるコンパイルエラーと警告

ポインタには「指す先の型」があります。

例えばint *char *は、どちらも「ポインタ型」ではありますが、別々の型です。

これを意識しないと、コンパイル時に警告やエラーが出ることがあります。

C言語
#include <stdio.h>

int main(void) {
    int x = 10;
    char *cp;

    // cp = &x;  // 型が違うため、本来は危険(多くのコンパイラが警告)

    return 0;
}

なぜ型が大事かというと、ポインタ演算p + 1をしたとき、「何バイトぶん進めるのか」を決めるのが指す先の型だからです。

  • int *p の場合: p + 1sizeof(int)バイト進む
  • char *p の場合: p + 1sizeof(char)バイト進む(通常1バイト)

このため、違う型のポインタを勝手に代入するのは危険であり、コンパイラも警告を出します。

例外的に、void *は「どんな型のアドレスも格納できる汎用ポインタ」として定義されています。

mallocvoid *を返しますが、これを使うときは必ず目的の型にキャストします。

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

int main(void) {
    // intを10個分確保
    int *arr = (int *)malloc(sizeof(int) * 10);

    if (arr == NULL) {
        printf("メモリ確保に失敗しました。\n");
        return 1;
    }

    // 使用例: 0〜9を代入
    for (int i = 0; i < 10; i++) {
        arr[i] = i;
    }

    // 確認表示
    for (int i = 0; i < 10; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    free(arr);   // 確保したメモリを解放
    arr = NULL;  // ダングリングポインタ防止

    return 0;
}
実行結果
0 1 2 3 4 5 6 7 8 9

ポインタの型が正しいかどうかを気にしながらコーディングすることで、コンパイル時の警告を減らし、安全なプログラムを書くことができます。

まとめ

ポインタは、「値そのもの」ではなく「値のある場所(アドレス)」を扱うための変数です。

変数とメモリアドレスの関係を意識し、&でアドレスを取り、*でその中身にアクセスするという基本を押さえれば、仕組み自体は決して魔法のようなものではありません。

配列や文字列、関数引数との関係も、先頭アドレスとポインタ演算として理解すると一気に見通しが良くなります。

未初期化ポインタやダングリングポインタなどの危険を避けつつ、まずは簡単な例からポインタ操作に慣れていくことが、C言語上達への近道です。

この記事を書いた人
エーテリア編集部
エーテリア編集部

プログラミングの基礎をしっかり学びたい方向けに、C言語の基本文法から解説しています。ポインタやメモリ管理も少しずつ理解できるよう工夫しています。

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

URLをコピーしました!