C言語の基本|ポインタが変数を指す

変数の住所をポインタに渡せば、同じ値を別の入り口から操作できる。

C言語では、変数に保存されている値だけでなく、その変数がメモリ上のどこに置かれているかも扱えます。その保存場所を表す情報がアドレスです。

そして、アドレスを記憶するための変数をポインタと呼びます。

ポインタの記述には * や & が登場するため、最初は少し複雑に感じるかもしれません。ただし、基本の流れはそれほど難しくありません。

ポインタを使うときは、次の3段階で考えます。

  1. ポインタを宣言する
  2. ポインタに変数のアドレスを代入する
  3. ポインタを通して変数の値を参照または変更する

たとえば、変数 number のアドレスをポインタ p に保存すると、p は number を指している状態になります。その後は *p と書くことで、number の値を間接的に扱えます。

ここでは、ポインタが変数を指すとはどのような状態なのかを、宣言、アドレスの代入、間接参照という順番で見ていきましょう。

ポインタが変数を指すとは

一般的な変数は、数値や文字などの値を保存します。

int count = 12;

この変数 count には整数値の 12 が保存されています。また、count はメモリ上のどこかに配置されているため、その場所を表すアドレスも持っています。

ポインタは、そのアドレスを保存する変数です。

仮に count がメモリ上の 2000 番地に置かれているとすると、関係は次のようになります。

対象保存しているもの
count整数値 12
&countcount のアドレス
pcount のアドレス
*pp が指している先の値

p に &count を代入すると、p は count の場所を記憶します。この状態を、p が count を指していると表現します。

大切なのは、p が 12 を保存しているわけではないことです。p が保存するのは、12 が入っている count のアドレスです。

ポインタを使う基本の3段階

変数を指すポインタは、次の流れで用意します。

段階記述例意味
ポインタの宣言int *p;int 型の変数を指せるポインタを用意する
アドレスの代入p = &count;count のアドレスを p に保存する
間接参照*pp が指している先の値を扱う

コードとして並べると、次のようになります。

int count = 12;
int *p;

p = &count;
*p = 20;

最後の *p = 20; が実行されると、p が指している count の値が 20 に変わります。

図1:変数とポインタが持つ情報の違い

この図から分かること

変数 count は値の 12 を保存しています。一方、ポインタ p が保存しているのは、count のアドレスである 2000 番地です。

p から count へ伸びる矢印は、p が count の場所を知っていることを表しています。この状態になれば、*p を使って count の値を参照したり変更したりできます。

ポインタ型の変数を宣言する

int 型の変数を指すポインタは、次のように宣言します。

int *p;

この宣言は、p が int 型の値が置かれている場所を記憶できるポインタであることを表します。

ポインタは、指す対象の型に合わせて宣言します。

指したい変数の型ポインタの宣言
int 型int *p;
double 型double *p;
char 型char *p;

int 型の変数を指すなら int 型用のポインタ、double 型の変数を指すなら double 型用のポインタを使います。ポインタは、指している先にどの型の値があるのかを型の情報として持っているからです。

宣言しただけのポインタはまだ使えない

次の宣言では、ポインタ p のための変数は用意されています。

int *p;

しかし、この時点ではpに有効なアドレスを設定していません。関数内でこのように宣言したポインタの中身は不定であり、どこを指しているか分からない状態です。

そのため、次のようにいきなり指している先を使ってはいけません。

int *p;
*p = 10;

このコードでは、p が指している場所が決まっていないのに、その場所へ 10 を書き込もうとしています。これは予期しない動作の原因になります。

ポインタを間接参照する前に、必ず有効なアドレスを設定しましょう。

int value = 8;
int *p = &value;

これなら、p は最初から value を指しているため、*p を安全に使用できます。

ポインタに変数のアドレスを代入する

変数のアドレスを取り出すときは、アドレス演算子 & を使います。

&count

これは、count がメモリ上に置かれている場所を表します。

そのアドレスをポインタ p に代入するには、次のように書きます。

p = &count;

この代入によって、p は count を指すようになります。

ここでは、左辺が *p ではなく p であることに注目してください。

書き方表すもの
pポインタに保存されているアドレス
*pそのアドレスにある値
&countcount のアドレス

アドレスを保存する相手はポインタそのものなので、p = &count; と書きます。

宣言とアドレスの設定を同時に行う

ポインタは、宣言したあとにアドレスを代入するだけでなく、宣言と同時にアドレスを設定できます。

int count = 12;
int *p = &count;

これは、次のコードをまとめて書いた形です。

int count = 12;
int *p;

p = &count;

宣言と同時に有効なアドレスを設定すると、アドレスを設定し忘れたままポインタを使う危険を減らせます。

操作書き方
ポインタだけを宣言するint *p;
宣言と同時に初期化するint *p = &count;
宣言済みのポインタへ代入するp = &count;

宣言に現れる * は、p がポインタ変数であることを示しています。宣言後にアドレスを代入するときは、p に * を付けません。

図2:アドレスを代入すると変数を指す状態になる

この図から分かること

&count を使うと、count の値ではなくアドレスを取り出せます。そのアドレスを p に代入すると、p と count がアドレスによって結び付きます。

p の保存内容は count の値 12 ではなく、count が存在する 2000 番地です。これが、ポインタが変数を指す状態です。

ポインタが指す先の値を参照する

p が count を指している場合、*p と書くと count の値を参照できます。

int count = 12;
int *p = &count;

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

実行すると、count に保存されている 12 が表示されます。

12

式の中にある * は、ポインタが指している場所までたどり、その場所に保存されている値を取り出す働きをします。この操作を間接参照と呼びます。

得られるもの
countcount の値
&countcount のアドレス
pp に保存されているアドレス
*pp が指している先の値

p が count を指しているなら、count と *p は同じ値を表します。ただし、そこへ到達する方法は異なります。

count は変数名を使った直接参照です。*p はアドレスをたどる間接参照です。

ポインタを通して値を変更する

間接参照で取り出した値には、新しい値を代入することもできます。

*p = 25;

p が count を指しているなら、この代入によって count の値が 25 に変わります。

int count = 12;
int *p = &count;

*p = 25;

printf("%d\n", count);

実行結果は次のようになります。

25

ポインタ p の保存内容が 25 に変わったわけではありません。p が保存しているアドレスはそのままで、そのアドレスに置かれている count の値が書き換えられています。

宣言の * と間接参照の * を区別しよう

ポインタでは、同じ * が異なる場面に登場します。

使用される場面記述例役割
ポインタの宣言int *p;p がポインタであることを示す
間接参照*p = 25;p が指している先の値を表す

次のコードでは、最初の * と最後の * で役割が異なります。

int count = 12;
int *p = &count;
*p = 25;

int *p の * は、p を int 型用のポインタとして宣言する記号です。

*p = 25; の * は、p に保存されているアドレスをたどり、その場所にある値を変更するための間接演算子です。

見た目は同じでも、宣言の中にあるのか、式の中にあるのかを確認すると区別しやすくなります。

直接参照と間接参照

変数名をそのまま使う方法を直接参照、ポインタを通して値を扱う方法を間接参照と考えられます。

temperature = 24;

これは、変数名を使って temperature を直接変更しています。

*ptr = 24;

こちらは、ptr に保存されているアドレスをたどって、指している先の値を変更しています。

ptr が temperature を指していれば、どちらも temperature を 24 に変更する処理です。

書き方参照方法処理
temperature直接参照変数名で値を扱う
*ptr間接参照ポインタから変数へたどって値を扱う

ポインタから商品の在庫数を変更してみよう

ここでは、商品の在庫数を表す変数をポインタで指し、間接参照によって値を変更します。

在庫数をポインタ経由で変更するプログラム

ファイル名:11_4_1.c

#include <stdio.h>

int main(void)
{
    int stock = 35;       /* 商品の在庫数 */
    int *stock_ptr;       /* int型の変数を指すポインタ */

    stock_ptr = &stock;   /* stockのアドレスを代入する */

    printf("変更前の在庫数: %d個\n", stock);
    printf("stockのアドレス: %p\n", (void *)&stock);
    printf("stock_ptrに保存されたアドレス: %p\n", (void *)stock_ptr);
    printf("stock_ptrが指す先の値: %d個\n", *stock_ptr);

    *stock_ptr = 42;      /* ポインタを通してstockを書き換える */

    printf("変更後の在庫数: %d個\n", stock);
    printf("ポインタから確認した在庫数: %d個\n", *stock_ptr);

    return 0;
}
実行結果の例
変更前の在庫数: 35個
stockのアドレス: 0x7ffd2c41a6d4
stock_ptrに保存されたアドレス: 0x7ffd2c41a6d4
stock_ptrが指す先の値: 35個
変更後の在庫数: 42個
ポインタから確認した在庫数: 42個

アドレスの表示内容は実行環境や実行のタイミングによって変わります。実際の結果がこの例と異なっていても問題ありません。

プログラムの処理を順番に見てみよう

最初に、商品の在庫数を保存する変数 stock を用意しています。

int stock = 35;

stock には 35 が保存されます。同時に、stock はメモリ上のどこかに配置されます。

続いて、int 型の変数を指すポインタ stock_ptr を宣言します。

int *stock_ptr;

この時点では、stock_ptr に stock のアドレスはまだ入っていません。そこで次の文を使って、stock のアドレスを代入します。

stock_ptr = &stock;

&stock は stock のアドレスです。そのアドレスが stock_ptr に保存されるため、stock_ptr は stock を指す状態になります。

2つのアドレスが一致する理由

プログラムでは、次の2つを表示しています。

(void *)&stock
(void *)stock_ptr

&stock は stock のアドレスです。stock_ptr にはそのアドレスを代入しているため、表示される2つのアドレスは同じになります。

表示する内容意味
&stockstock が置かれているアドレス
stock_ptrstock_ptr に保存されているアドレス

この一致によって、stock_ptr が stock を指していることを確認できます。

なお、%p でアドレスを表示するときは、対応する引数を void * 型へ変換して渡します。

間接参照で同じ値を確認する

次の表示では、stock_ptr が指している先の値を取り出しています。

printf("stock_ptrが指す先の値: %d個\n", *stock_ptr);

stock_ptr は stock を指しているため、*stock_ptr が表すのは stock の値です。変更前は stock に 35 が入っているので、ここでも 35 が表示されます。

ポインタから元の変数を書き換える

在庫数を変更しているのが次の文です。

*stock_ptr = 42;

これは、stock_ptr 自体へ 42 を代入する処理ではありません。stock_ptr が指している先に 42 を代入しています。

stock_ptr の指す先は stock なので、処理後の状態は次のようになります。

対象変更前変更後
stock3542
stock_ptrstock のアドレス同じアドレス
*stock_ptr3542

stock_ptr に保存されているアドレスは変わりません。そのアドレスの先にある stock の値だけが 35 から 42 に変わります。

値を変えても指す場所は変わらない

*stock_ptr = 42; を実行しても、stock_ptr は引き続き stock を指しています。

値の変更と、指す場所の変更は別の操作です。

*stock_ptr = 42;

この記述は、指している先の値を変更します。

stock_ptr = &another_stock;

こちらは、stock_ptr に別の変数のアドレスを代入し、指す対象を変更します。

記述変化するもの
*stock_ptr = 42;指している先の値
stock_ptr = &another_stock;ポインタが保存するアドレス

この違いを理解すると、p と *p を混同しにくくなります。

図3:間接参照で指している先の値を変更する

この図から分かること

*stock_ptr = 42; が変更するのは、stock_ptr が指している先の値です。stock_ptr に保存されているアドレスそのものは変わりません。

したがって、処理後も stock_ptr は同じ変数 stock を指しています。変化したのは stock の中身だけです。

ポインタでよくある間違い

ポインタを使い始めたときは、次のような点でつまずきやすくなります。

間違いやすい点確認すること
p と *p を混同するp はアドレス、*p は指している先の値
アドレスの代入時に * を付けるアドレスは p = &x; で代入する
宣言しただけのポインタを間接参照する有効なアドレスを設定してから使う
指す先とポインタ自身の変更を混同する*p の変更と p の変更を区別する
異なる型のアドレスをそのまま代入する指す先の型に合うポインタを使う

特に重要なのは、ポインタを宣言しただけで間接参照しないことです。

int *p;

この段階では、p に有効なアドレスが入っているとは限りません。

int value = 10;
p = &value;

このように有効な変数のアドレスを設定してから、*p を使います。

p と *p を言葉で読み分けよう

コードを読むときは、記号をそのまま眺めるより、意味を言葉にすると理解しやすくなります。

  • p は、p に保存されているアドレス
  • &value は、value のアドレス
  • p = &value; は、value のアドレスを p に保存する
  • *p は、p が指している先の値
  • *p = 20; は、p が指している先の値を 20 にする

この読み方を繰り返すと、ポインタのコードを落ち着いて追えるようになります。

ポインタが変数を指す流れを整理しよう

変数を指すポインタの基本は、次の順番です。

  1. 操作したい変数を用意する
  2. 変数の型に合うポインタを宣言する
  3. & を使って変数のアドレスを取得する
  4. 取得したアドレスをポインタに保存する
  5. * を使って指している先の値を参照または変更する

この流れをコードにすると、次の形になります。

int value = 8;
int *p = &value;

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

*p = 15;
printf("%d\n", value);

ポインタが変数を指す状態を理解すると、変数を直接扱う方法と、アドレスを経由して間接的に扱う方法の両方が見えるようになります。

この考え方は、関数から呼び出し元の変数を変更する処理、配列要素へのアクセス、文字列の走査、動的に確保したメモリの利用などにもつながります。まずは p、&value、*p がそれぞれ何を表しているのかを区別しながら、短いコードで動きを確かめてみてください。