閉じる

【C言語】「*」と「&」から始めるポインタ入門:変数を間接操作してみよう

C言語を学び始めると、必ず出てくるのがポインタです。

ただ、記号も多く、最初はとっつきにくいと感じる人が多いです。

この記事では、「*」と「&」という2つの演算子に焦点を当てながら、変数をポインタ経由で間接的に操作する仕組みを、サンプルコードとともに丁寧に解説します。

初心者の方でも、読み進めながら実際に手を動かすことで、ポインタの基本が自然と身につく内容になっています。

C言語のポインタとは何かを理解しよう

ポインタとは

ポインタとは、「値そのものではなく、その値が置かれている場所(アドレス)を保存する変数」のことです。

ふつうの変数は数値や文字といったデータを直接持ちますが、ポインタ変数はメモリ上の位置情報を持ちます。

もう少しイメージで説明すると、次のように考えると理解しやすくなります。

  • 通常の変数は「本そのもの」です。
  • ポインタ変数は「本棚のどの場所に本があるかを書いたメモ」です。

この「メモ(アドレス)」を使うことで、C言語では離れた場所にある変数の中身を間接的に操作できます。

これがポインタの最大の特徴です。

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

コンピュータは、プログラムで扱うデータをメモリと呼ばれる領域に保存しています。

メモリは、細かい部屋をたくさん並べたような構造をしており、それぞれの部屋にアドレス(番地)という番号が付いています。

C言語の変数は、次のような関係になっています。

  • 変数名 … 人間にとってわかりやすいラベル
  • アドレス … メモリ上での正確な場所(PCが使う情報)
  • 値 … その場所に保管されている実際のデータ

表にすると次のように整理できます。

要素役割
変数名人間が使う名前int a;a
アドレスメモリ上の位置0x7ffeefbff5a4 など
実際に保存されているデータ10'A' など

ポインタ変数は、このうち「アドレス」を格納するための変数になります。

なぜC言語でポインタが必要になるのか

初心者の方は、「値を直接扱えばよいのに、なぜわざわざアドレスなんて扱うのか」と疑問に思うかもしれません。

ポインタが必要とされる主な理由は次の通りです。

1つ目は、関数の中から呼び出し元の変数を書き換えるためです。

C言語では、通常の引数渡しでは値のコピーしか渡されないため、呼び出し元の変数は変更できません。

ポインタを使うことで、その変数のアドレスを渡し、間接的に値を変更できます。

2つ目は、配列や文字列の扱いを効率よく行うためです。

配列の先頭アドレスをポインタで扱うことで、大量のデータをコピーせずに関数へ渡せます。

3つ目は、メモリを動的に確保して柔軟なデータ構造を作るためです。

リストや木構造、グラフなどの高度なデータ構造は、ポインタなしでは実現が難しくなります。

このように、ポインタは少し難しく感じますが、C言語の力を最大限に活かすために欠かせない仕組みです。

「*」と「&」でポインタの基本操作を覚える

ここからは、ポインタを扱ううえで最も重要な「&」演算子「*」演算子について、順番に見ていきます。

「&」演算子で変数のアドレスを取得する

&演算子は、「変数のアドレス(場所)を取得する」ために使います。

使い方はシンプルで、変数名の前に&をつけるだけです。

サンプルコード: 変数のアドレスを表示する

C言語
#include <stdio.h>

int main(void) {
    int a = 10;  // 通常のint型変数aを宣言し、10で初期化

    // 変数aの値を表示
    printf("aの値: %d\n", a);

    // 変数aのアドレスを表示
    // %pはポインタ(アドレス)を表示するための書式指定子です
    printf("aのアドレス: %p\n", (void *)&a);

    return 0;
}
実行結果
aの値: 10
aのアドレス: 0x7ffee9b5c8ac  (実際の値は環境によって異なります)

ここで&aが「aが置かれている場所」を表していることがわかります。

%pで表示される値は、人間にはわかりにくい16進数ですが、コンピュータにとっては非常に重要な情報です。

「*」演算子でポインタが指す値を参照する

*演算子は、ポインタと組み合わせると「そのアドレスに格納されている中身(値)を取り出す」ために使います。

この操作を間接参照と呼びます。

サンプルコード: ポインタを使って値を参照する

C言語
#include <stdio.h>

int main(void) {
    int a = 10;       // 通常のint型変数
    int *p = &a;      // int型へのポインタpを宣言し、aのアドレスで初期化

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

    // ポインタpに格納されているアドレスを表示
    printf("pの値(指しているアドレス): %p\n", (void *)p);

    // *pで、pが指しているアドレスにある「値」を取り出す
    printf("*pの値(間接参照した結果): %d\n", *p);

    return 0;
}
実行結果
aの値: 10
aのアドレス: 0x7ffee9b5c88c
pの値(指しているアドレス): 0x7ffee9b5c88c
*pの値(間接参照した結果): 10

ポインタ変数pにはアドレスが入り、そのアドレスを*pと書くことで「そこに入っている値」を取り出せることが、この例からわかります。

宣言時の「」と式中の「」の違い

ポインタを学ぶときに混乱しやすいのが、宣言のときのと、式の中で使うが意味的に違うという点です。

  • 宣言時の* …「この変数はポインタ型です」という意味
  • 式中の* …「ポインタが指す先の値を取り出します」という意味(間接参照)

宣言と式の「*」をまとめて確認するコード

C言語
#include <stdio.h>

int main(void) {
    int a = 10;

    // 宣言時の* : 「int型へのポインタp」を宣言している
    int *p = &a;

    // 式中の* : 「pが指している先にある値」を参照している
    int b = *p;  // この時点でbには10が代入される

    printf("aの値: %d\n", a);
    printf("*pの値(間接参照): %d\n", *p);
    printf("bの値: %d\n", b);

    return 0;
}
実行結果
aの値: 10
*pの値(間接参照): 10
bの値: 10

宣言のは「型の一部」、式のは「演算子」と意識して区別すると、頭の中が整理しやすくなります。

ポインタ変数の宣言と初期化の基本

ポインタ変数を扱うときは、「どの型へのポインタなのか」を必ず指定します。

これは、ポインタが指すデータのサイズを知るために必要です。

たとえば、次のように宣言します。

  • int *p; … int型へのポインタ
  • double *dp; … double型へのポインタ
  • char *cp; … char型へのポインタ

サンプルコード: ポインタの宣言と初期化

C言語
#include <stdio.h>

int main(void) {
    int a = 10;
    int *p = &a;  // int型変数aのアドレスで初期化

    // 初期化されていないポインタは危険なので、NULLを代入しておくのが安全です
    int *q = NULL; // まだどこも指していないポインタ

    printf("aの値: %d\n", a);
    printf("pが指している値: %d\n", *p);
    printf("pが指しているアドレス: %p\n", (void *)p);
    printf("qの値(アドレス): %p\n", (void *)q);

    return 0;
}
実行結果
aの値: 10
pが指している値: 10
pが指しているアドレス: 0x7ffee9b5c88c
qの値(アドレス): (nil)  または 0x0

初期化していないポインタを使うことは非常に危険です。

どこを指しているかわからないため、プログラムが異常終了したり、予期せぬ動作を引き起こします。

指す先がまだない場合はNULLを代入しておく習慣をつけると安全です。

ポインタで変数を間接的に操作する方法

ここからは、ポインタを使って変数の値を書き換える「間接操作」に焦点を当てて説明します。

ポインタ経由で値を書き換える仕組み

ポインタ変数には、ある変数のアドレスが入っています。

その状態で*ポインタ変数 = 値;と代入すると、「そのアドレスにある変数」に新しい値を代入することになります。

ポイントは次の2段階のイメージです。

  1. ポインタが指している「場所」をたどる
  2. その場所にある「値」を変更する

これにより、元の変数に直接触れなくても、その中身を変えることができるわけです。

実例コードで見るポインタによる値の変更

サンプルコード: ポインタ経由で変数を書き換える

C言語
#include <stdio.h>

int main(void) {
    int a = 10;       // 元の変数
    int *p = &a;      // aのアドレスを格納するポインタ

    printf("変更前: aの値 = %d\n", a);

    // ポインタ経由でaを書き換える
    *p = 20;          // 「pが指す先の値」に20を代入

    printf("変更後: aの値 = %d\n", a);
    printf("ポインタ経由で参照した値 = %d\n", *p);

    return 0;
}
実行結果
変更前: aの値 = 10
変更後: aの値 = 20
ポインタ経由で参照した値 = 20

この例では、*p = 20;と書いただけなのに、変数aの値が10から20に変化しています。

これがまさに、ポインタによる「間接操作」です。

間接操作で元の変数に影響が出る理由

なぜ*p = 20;と書くだけでaが変わるのでしょうか。

この理由を整理してみます。

  1. pにはaのアドレス&aが入っている
  2. *pは、「そのアドレスにある値」を意味する
  3. よって*p = 20;は、「aが置かれている場所に20を代入する」という操作になる
  4. 結果として、元の変数aの値が書き換わる

ポインタは「どの変数を操作するか」の手がかりになっていると考えると、間接操作のイメージがつかみやすくなります。

図で理解するポインタと変数のつながり

文字だけではイメージしにくいので、簡易的な図で表現してみます。

次のようなコードを例に考えます。

C言語
int a = 10;
int *p = &a;
*p = 20;

このとき、メモリのイメージは次のようになります。

*p = 20;を実行すると、次のように変化します。

変わるのは「アドレスが指す先の中身」であり、ポインタ自身が持つアドレスの値はそのままという点が重要です。

関数とポインタで「値を書き換える」を体験しよう

ここまでで、ポインタを使えば変数を間接的に書き換えられることがわかりました。

次は、関数にポインタを渡して値を書き換える実例を通して、ポインタの便利さを体験してみましょう。

値渡しとポインタ渡しの違い

C言語では、関数に引数を渡すとき、基本的には値渡しになります。

値渡しでは、引数の値をコピーして関数に渡すため、関数内で値を変更しても、呼び出し元には影響がありません。

一方で、ポインタ渡しでは、変数のアドレスを関数に渡すため、関数の中からでも元の変数を間接的に書き換えることができます。

サンプルコード: 値渡しの場合

C言語
#include <stdio.h>

// 値渡しで引数を受け取る関数
void add_ten_value(int x) {
    x = x + 10;  // ここで変更しているのは「コピーされたx」
}

int main(void) {
    int a = 5;

    printf("呼び出し前: a = %d\n", a);
    add_ten_value(a);  // aの値(5)がコピーされて渡される
    printf("呼び出し後: a = %d\n", a);

    return 0;
}
実行結果
呼び出し前: a = 5
呼び出し後: a = 5

このように、値渡しでは元の変数aは変わりません

サンプルコード: ポインタ渡しの場合

C言語
#include <stdio.h>

// ポインタ渡しで引数を受け取る関数
void add_ten_pointer(int *x) {
    // xは「int型変数のアドレス」が入っているポインタ
    // *xで、そのアドレスにある実際の変数の値を参照・変更できる
    *x = *x + 10;
}

int main(void) {
    int a = 5;

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

    return 0;
}
実行結果
呼び出し前: a = 5
呼び出し後: a = 15

ポインタ渡しでは、関数内で*xを操作することで、呼び出し元の変数aの値を直接変えることができます

関数引数にポインタを使って変数を書き換える

ポインタを引数に取る関数は、次のような場面でよく使われます。

  • 2つの変数の値を入れ替える(swap)
  • 入力関数で、読み取った値を呼び出し元に返す
  • 計算結果を複数返したいとき(戻り値では1つしか返せないため)

ここでは、典型的なswap関数の例を見てみます。

サンプルコード: swap関数で2つの変数を入れ替える

C言語
#include <stdio.h>

// 2つのint型変数の値を入れ替える関数
// 引数は「int型へのポインタ」
void swap(int *x, int *y) {
    int temp;

    // *x は「xが指す変数の値」
    // *y は「yが指す変数の値」
    temp = *x;
    *x = *y;
    *y = temp;
}

int main(void) {
    int a = 3;
    int b = 7;

    printf("入れ替え前: a = %d, b = %d\n", a, b);

    // aとbのアドレスを渡す
    swap(&a, &b);

    printf("入れ替え後: a = %d, b = %d\n", a, b);

    return 0;
}
実行結果
入れ替え前: a = 3, b = 7
入れ替え後: a = 7, b = 3

このように、関数に変数のアドレスを渡し、関数内でポインタ経由で値を操作することで、呼び出し元の変数を自由に書き換えることができます

配列とポインタの関係と間接操作

C言語では、配列名は「配列の先頭要素のアドレス」を表すという性質があります。

たとえばint arr[3];という配列があるとき、arr&arr[0]とほぼ同じ意味になります。

この性質を利用すると、配列をポインタとして関数に渡し、配列の中身を間接的に操作できます。

サンプルコード: ポインタ経由で配列の要素を書き換える

C言語
#include <stdio.h>

// 配列(の先頭アドレス)を受け取り、要素を書き換える関数
void increment_all(int *p, int size) {
    int i;
    for (i = 0; i < size; i++) {
        // p[i] は *(p + i) と同じ意味
        p[i] = p[i] + 1;  // 各要素を1ずつ増やす
    }
}

int main(void) {
    int arr[3] = {1, 2, 3};
    int i;

    printf("変更前: ");
    for (i = 0; i < 3; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    // 配列名arrは先頭要素のアドレスとして関数に渡される
    increment_all(arr, 3);

    printf("変更後: ");
    for (i = 0; i < 3; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}
実行結果
変更前: 1 2 3 
変更後: 2 3 4

この例では、関数increment_allがポインタpを通して、呼び出し元の配列arrの中身を直接変更しています。

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

ポインタは強力な機能ですが、そのぶん間違えるとバグやクラッシュの原因になりやすいです。

初心者が特につまずきやすい点と、その注意点をまとめます。

まず、初期化していないポインタを使わないことが重要です。

どこを指しているかわからないポインタに対して*p = 10;などと書くと、予期せぬメモリを書き換えてしまい、プログラムが落ちたり、原因不明の動作を引き起こします。

指す先がまだ決まっていないポインタにはNULLを代入しておき、使う前に必ず有効なアドレスが入っているか確認するようにします。

次に、型に合ったポインタを使うことも重要です。

int型の変数にはint *double型の変数にはdouble *といったように、必ず対応する型のポインタで扱う必要があります。

型が合わないポインタを使うと、誤ったサイズで値を解釈してしまい、やはり不具合の原因になります。

また、ポインタ演算や配列との関係は、慣れるまで混乱しやすい部分です。

最初のうちは、p[i]*(p + i)&a*pなどの記号を、何度も小さなサンプルコードで確認しながら覚えるとよいです。

紙に図を書いて、「どの変数がどのアドレスを持っているか」「ポインタがどこを指しているか」を視覚的に整理すると理解がぐっと進みます。

最後に、「ポインタは怖いから避ける」のではなく、「仕組みを丁寧に理解して正しく使う」ことが大切です。

C言語の多くのライブラリや実践的なコードはポインタを前提にしているため、ポインタを理解することがC言語上達への大きな一歩になります。

まとめ

この記事では、C言語のポインタについて、「&」でアドレスを取得し、「*」でそのアドレス先の値を参照・変更するという基本から、変数や配列を間接的に操作する方法、関数へのポインタ渡しによる値の書き換えまでを解説しました。

ポインタは最初こそ難しく感じますが、小さなサンプルで「アドレス」と「値」の関係を一つずつ確かめていくことで、確実に理解が深まります。

ぜひ実際にコードを入力して動かしながら、ポインタ入門を自分のものにしていってください。

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

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

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

URLをコピーしました!