C言語の基本|アドレスとポインタ

アドレスを覚え、場所をたどって値を操作する。ポインタの仕組みが分かれば、C言語のメモリ操作がぐっと身近になります。

C言語では、変数に入っている値だけでなく、その値がメモリ上のどこに保存されているかも扱えます。

変数が置かれている場所を表す情報がアドレスです。そして、そのアドレスを保存するための変数がポインタです。

この関係は、次の3つに分けると分かりやすくなります。

用語意味身近なたとえ
変数に格納されているデータ箱の中身
アドレス変数が置かれているメモリ上の場所箱がある住所
ポインタアドレスを保存する変数住所を書いたメモ

たとえば、int型変数scoreに80が格納されているとします。

int score = 80;

scoreには値80が入っています。同時に、scoreはメモリ上のどこかに置かれているため、その場所を表すアドレスも持っています。

このscoreのアドレスを別の変数へ保存したものがポインタです。

int *ptr = &score;

このコードでは、&scoreでscoreのアドレスを取得し、そのアドレスをポインタ変数ptrへ保存しています。

さらに、*ptrと書くと、ptrに保存されているアドレスをたどり、その場所にある値へアクセスできます。

*ptr = 90;

ptrはscoreのアドレスを保存しているため、*ptrを90にすると、scoreの値も90に変わります。

アドレスとポインタを理解すると、scanfで変数に&を付ける理由や、関数から呼び出し元の変数を変更する仕組みも見えてきます。

ポインタとは

ポインタとは、メモリ上のアドレスを保存する変数です。

普通のint型変数は整数値を保存します。

int number = 25;

一方、int型のデータを指すポインタは、int型変数のアドレスを保存します。

int *ptr = &number;

number、&number、ptr、*ptrの違いを整理すると、次のようになります。

書き方表すもの内容の例
number変数numberの値25
&numbernumberのアドレス0x1000
ptrptrに保存されたアドレス0x1000
*ptrptrが指す場所にある値25

&numberとptrには、数値として同じアドレスが入っています。

ptrそのものはアドレスを表し、*ptrはそのアドレスに保存されている値を表します。ここを分けて考えることが、ポインタを理解する大切なポイントです。

普通の変数とポインタ変数の違い

普通の変数とポインタ変数では、保存する情報が異なります。

変数の種類保存するもの
int型変数整数値
double型変数小数値
char型変数1文字
ポインタ変数メモリ上のアドレス

次の変数には整数値25が格納されます。

int number = 25;

次のポインタ変数には、numberのアドレスが格納されます。

int *ptr = &number;

ptrの中に25が直接格納されるわけではありません。ptrに入るのは、25を保存しているnumberの場所です。

図1:ポインタが変数のアドレスを保存する仕組み

この図から分かること

変数numberには値25が入っています。一方、ポインタptrにはnumberのアドレスが入っています。

ptrが保存しているのは25という値ではありません。25が保存されている場所です。

ptrからnumberへ向かう矢印をイメージすると、ポインタが特定の変数を指すという関係を理解しやすくなります。

ポインタを住所のメモとして考える

ポインタは、住所を書いたメモにたとえると分かりやすくなります。

たとえば、ある建物の住所が0x1000で、その建物の中に25という荷物があるとします。

たとえC言語での対応
建物変数number
建物の中の荷物値25
建物の住所&number
住所を書いたメモポインタptr
住所をたどって荷物を見る*ptr

住所を書いたメモは、荷物そのものではありません。しかし、その住所をたどれば荷物が置かれている場所へ到達できます。

同じように、ポインタは値そのものではなく、値が置かれている場所を保存します。そのアドレスをたどることで、元の変数の値を参照したり変更したりできます。

ポインタをリモコンとして考える

ポインタは、離れた場所にある機器を操作するリモコンにもたとえられます。

リモコンのたとえC言語での対応
操作される機器変数
機器がある場所アドレス
リモコンポインタ
リモコンから操作する*を使って値を参照・変更する

ポインタ自身が操作対象の値を持つのではありません。操作する対象がどこにあるかを知っているため、その場所へアクセスできます。

ポインタ変数を宣言する

int型のデータを指すポインタ変数は、次の形で宣言します。

int *ptr;

各部分の意味は次のとおりです。

部分意味
intptrが指す先のデータ型
*ptrをポインタ変数として宣言する
ptrポインタ変数の名前

この宣言は、ptrがint型のデータを指すためのポインタであることを表します。

変数のアドレスを同時に設定するなら、次のように書きます。

int number = 25;
int *ptr = &number;

&numberでnumberのアドレスを取得し、それをptrへ代入しています。

ポインタの型が必要な理由

ポインタには、指している先の型があります。

int *int_ptr;
char *char_ptr;
double *double_ptr;
宣言指す対象
int *int_ptrint型のデータ
char *char_ptrchar型のデータ
double *double_ptrdouble型のデータ

ポインタが指す先の型が分かれば、C言語はその場所にあるデータをどのような型として読み書きすればよいか判断できます。

int型を指すポインタなら、その場所のデータをint型として扱います。char型を指すポインタなら、char型として扱います。

&で変数のアドレスを取得する

&はアドレス演算子です。変数名の前に付けると、その変数のアドレスを取得できます。

&number

numberがメモリ上の0x1000に置かれていると仮定すると、&numberは0x1000というアドレスを表します。

ptr = &number;

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

処理の流れは次のとおりです。

  • numberのためのメモリ領域を用意する
  • numberに値25を保存する
  • &numberでnumberのアドレスを取得する
  • 取得したアドレスをptrへ保存する

*で指している先の値へアクセスする

ポインタ変数の前に*を付けると、そのポインタが指している場所にある値へアクセスできます。

*ptr

ptrがnumberのアドレスを保存しているなら、*ptrはnumberの値を表します。

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

このコードでは、numberに入っている値が表示されます。

値を書き換えることもできます。

*ptr = 40;

ptrはnumberを指しているため、numberの値が40に変わります。

コード起こること
ptr = &number;ptrにnumberのアドレスを保存する
*ptrnumberの値を表す
*ptr = 40;numberの値を40に変更する

宣言の*と式* のを区別する

*は、使われる場所によって読み方が変わります。

ポインタ変数を宣言するときは、ポインタであることを表します。

int *ptr;

宣言後の式で使うときは、ptrが指している先の値を表します。

*ptr
使われる場所コード意味
宣言int *ptr;ptrはint型を指すポインタ
*ptrptrが指す先のint型の値
代入先*ptr = 40;ptrが指す先の値を40にする

同じ*でも、宣言にあるのか、値を扱う式にあるのかを確認すると読みやすくなります。

&と*の関係

&と*は、アドレスと値を行き来するために使います。

int number = 25;
int *ptr = &number;

この状態では、次の関係があります。

表すもの
numbernumberの値25
&numbernumberのアドレス
ptrptrに保存されたnumberのアドレス
*ptrptrが指しているnumberの値25

考え方を順番に並べると、次のようになります。

  • &numberでnumberのアドレスを取得する
  • 取得したアドレスをptrへ保存する
  • *ptrでptrが指す場所の値へアクセスする

アドレスを表示する方法

printfでポインタが保存しているアドレスを表示するときは、%pを使います。

printf("%p\n", (void *)ptr);

%pへ渡すポインタは、voidポインタへ変換します。

部分役割
%pアドレスを表示する
ptrポインタに保存されたアドレス
(void *)ptrptrをvoidポインタへ変換する

アドレスの具体的な表示形式は処理系によって異なります。また、同じプログラムでも実行するたびに異なるアドレスが表示される場合があります。

ポインタを使ってファン回転数を変更する

ポインタ経由でファン回転数を書き換えるプログラム

ファイル名:11_3_1.c

#include <stdio.h>

int main(void)
{
    int fan_speed = 1200;       /* ファンの回転数 */
    int *ptr = &fan_speed;      /* fan_speedのアドレスを保存 */

    printf("変更前の回転数: %d rpm\n", fan_speed);
    printf("ptrが保存しているアドレス: %p\n", (void *)ptr);
    printf("ptrが指している先の値: %d rpm\n", *ptr);

    *ptr = 1500;                /* fan_speedを間接的に変更 */

    printf("変更後の回転数: %d rpm\n", fan_speed);
    printf("変更後にptrから見た値: %d rpm\n", *ptr);

    return 0;
}
実行結果の例

アドレスは実行環境によって変わります。次は表示例です。

変更前の回転数: 1200 rpm
ptrが保存しているアドレス: 0x000000a1b2c3d4e0
ptrが指している先の値: 1200 rpm
変更後の回転数: 1500 rpm
変更後にptrから見た値: 1500 rpm

通常の変数を用意する

最初に、ファンの回転数を保存するint型変数fan_speedを用意しています。

int fan_speed = 1200;

この時点でfan_speedには1200が格納されています。

fan_speedはメモリ上のどこかに配置され、その場所にはアドレスがあります。

ポインタを宣言してアドレスを保存する

次に、int型を指すポインタptrを宣言しています。

int *ptr = &fan_speed;

この文は、次の2つを同時に行っています。

  • int型を指すポインタptrを用意する
  • fan_speedのアドレスをptrへ保存する
内容
&fan_speedfan_speedのアドレス
ptrfan_speedのアドレスを保存するポインタ
*ptrfan_speedに格納されている値

この時点で、ptrはfan_speedを指しています。

ポインタに保存されたアドレスを表示する

次のprintfでは、ptrに保存されているアドレスを表示しています。

printf("ptrが保存しているアドレス: %p\n", (void *)ptr);

ptrにはfan_speedのアドレスが入っています。そのため、ptrを%pで表示すると、&fan_speedと同じ位置を表すアドレスが表示されます。

アドレスの値そのものは、実行環境や実行時によって異なります。

ポインタが指している先の値を表示する

次のprintfでは、*ptrを表示しています。

printf("ptrが指している先の値: %d rpm\n", *ptr);

ptrは fan_speedを指しているため、*ptrはfan_speedの値1200を表します。

表示する式表示内容
fan_speed1200
ptrfan_speedのアドレス
*ptr1200

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

次の文では、ptrが指している先の値を1500へ変更しています。

*ptr = 1500;

ptrが指している先はfan_speedです。そのため、実際に変更されるのはfan_speedの値です。

項目変更前変更後
fan_speed12001500
ptrが保存するアドレスfan_speedのアドレス同じアドレス
*ptr12001500

値を書き換えても、ptrに保存されているアドレスは変わりません。変化するのは、ptrが指している場所の中身です。

図2:ポインタ経由で指している先の値を変更する

この図から分かること

*ptr = 1500を実行すると、ptr自身の中身が1500になるわけではありません。

ptrには引き続きfan_speedのアドレスが保存されています。変更されるのは、そのアドレスをたどった先にあるfan_speedの値です。

ポインタ経由の代入では、ポインタが指す対象の中身を変更すると考えましょう。

ptrと*ptrを混同しない

ポインタの学習では、ptrと*ptrを同じものとして読まないことが大切です。

書き方表すもの
ptrアドレス
*ptrそのアドレスにある値

たとえば、ptrが0x1000を保存し、0x1000に1200が格納されているなら、次の関係になります。

内容
ptr0x1000
*ptr1200

ptrへ別のアドレスを代入すると、指す対象が変わります。

ptr = &other_number;

一方、*ptrへ値を代入すると、現在指している対象の値が変わります。

*ptr = 1500;

&と*の動きを図で整理する

&は、変数の値からアドレスを取り出すために使います。

&fan_speed

*は、ポインタに保存されたアドレスをたどり、その場所にある値へアクセスするために使います。

*ptr

この2つを順番に読むと、次の流れになります。

fan_speed
   ↓ &でアドレスを取得
&fan_speed
   ↓ ptrへ保存
ptr
   ↓ *で指している先へアクセス
*ptr

図3:&と*でアドレスと値を行き来する

この図から分かること

&dataは、変数dataのアドレスを表します。そのアドレスをポインタpへ保存すると、pはdataを指すようになります。

*pは、pに保存されたアドレスをたどった先の値です。

&は変数からアドレスを取得し、*はアドレスから指している先の値へアクセスする働きを持っています。

ポインタが必要になる場面

簡単なプログラムなら、普通の変数だけで処理できる場合もあります。それでもポインタが重要なのは、別の場所にあるデータを直接扱えるからです。

代表的な利用場面は次のとおりです。

利用場面ポインタが役立つ理由
scanfによる入力入力値を書き込む変数の場所を渡せる
関数から変数を変更する呼び出し元の変数へ直接アクセスできる
配列の処理先頭要素の場所を基準に要素をたどれる
文字列の処理先頭文字から \0 まで順番に処理できる
動的メモリ確保したメモリの場所を保存できる

ポインタは単なる記号の学習ではありません。メモリ上のデータを柔軟に扱うための仕組みです。

値を渡す場合とアドレスを渡す場合

関数へ普通の値を渡すと、関数には値のコピーが渡されます。

渡すもの関数内で扱うもの元の変数
値のコピー直接は変更されない
アドレス元の変数の保存場所ポインタ経由で変更できる

たとえば、main関数にあるnumberを別の関数から変更したい場合は、numberの値ではなく、&numberによってアドレスを渡します。

受け取る側の関数は、そのアドレスをポインタで受け取り、*を使って元の変数へアクセスします。

関数へアドレスを渡す基本イメージ

void update(int *ptr)
{
    *ptr = 20;
}

呼び出す側では、変数のアドレスを渡します。

int number = 10;
update(&number);

処理は次のようにつながります。

  • numberに10を格納する
  • &numberでnumberのアドレスを取得する
  • updateへnumberのアドレスを渡す
  • updateのptrがnumberを指す
  • *ptr = 20によってnumberを20へ変更する

この仕組みによって、別の関数から呼び出し元の変数を変更できます。

scanfとポインタの関係

scanfは、ポインタの考え方が使われている身近な例です。

int number;

scanf("%d", &number);

scanfは入力された値を、呼び出し元のnumberへ書き込む必要があります。そのため、numberの値ではなく、numberのアドレスを渡します。

書き方意味
numbernumberに入っている値
&numbernumberが置かれている場所
scanf("%d", &number)入力値の保存場所としてnumberのアドレスを渡す

もしnumberの値だけを渡しても、scanfは入力値をどこへ保存すればよいか判断できません。

&numberによって保存場所を受け取ることで、その場所へ入力値を書き込めます。

scanfでchar配列に&を付けない理由

整数変数への入力では&numberを使います。

scanf("%d", &number);

一方、char配列へ文字列を入力するときは、一般に配列名へ&を付けません。

char text[100];

scanf("%99s", text);

配列名textは、多くの式の中で先頭要素へのポインタに変換されます。そのため、文字列の保存先としてtextをそのまま渡せます。

入力対象scanfへ渡すもの
int型変数number&number
char配列texttext

この違いも、配列名とポインタの関係を学ぶと理解しやすくなります。

ポインタを使う前に有効なアドレスを設定する

ポインタを宣言しただけでは、正しい変数を指しているとは限りません。

int *ptr;

この状態で*ptrを使ってはいけません。ptrには、有効なアドレスが設定されていないからです。

次のように、指す対象のアドレスを設定してから使います。

int number = 25;
int *ptr = &number;

その後で、*ptrを利用できます。

*ptr = 40;

安全に読むための順番は次のとおりです。

  • 指される変数を用意する
  • &で変数のアドレスを取得する
  • ポインタへアドレスを保存する
  • *で指している先へアクセスする

型の合うアドレスを保存する

int型を指すポインタには、int型変数のアドレスを保存します。

int number = 25;
int *ptr = &number;

double型を指すポインタには、double型変数のアドレスを保存します。

double rate = 1.5;
double *rate_ptr = &rate;

指す先の型を合わせることで、ポインタが指しているデータの大きさや扱い方を正しく判断できます。

よくあるつまずき

つまずきやすい点正しい考え方
ptrに値が直接入っていると思うptrにはアドレスが入る
ptrと*ptrを同じものだと思うptrはアドレス、*ptrは指す先の値
&と*を同じ働きだと思う&はアドレス取得、*は指す先へのアクセス
*ptrへの代入でptrが変わると思う変わるのはptrが指す先の値
未設定のポインタを使う有効なアドレスを設定してから使う
%dでポインタを表示するアドレスは%pで表示する
アドレスの数値を覚えようとする実行環境や実行時で変わるものとして見る

コードを読むときの確認手順

ポインタを使ったコードでは、次の順番で確認すると理解しやすくなります。

  • ポインタが何型のデータを指すのか確認する
  • どの変数のアドレスを保存しているか確認する
  • &で取得しているアドレスを確認する
  • ptrと*ptrのどちらを使っているか確認する
  • 値を変更しているのか、指す場所を変更しているのか確認する
  • *を使う前に有効なアドレスが設定されているか確認する
  • アドレスの表示に%pが使われているか確認する

ポインタを読むときは、何の値を持っているかではなく、何の場所を覚えているかから考えるのがコツです。

値、アドレス、ポインタ、指している先の値を分けて考えられるようになると、&と*の役割が自然につながります。この土台ができれば、関数へのポインタ渡し、配列とポインタの関係、文字列操作、動的メモリ確保も理解しやすくなります。