C言語の基本|ポインタの基本

変数の中身だけでなく、置かれている場所にも目を向ける。アドレスの仕組みが分かれば、ポインタはぐっと身近になります。

C言語を学んでいると、避けて通れないテーマのひとつがポインタです。記号が多く、メモリという目に見えないものを扱うため、最初は難しく感じるかもしれません。

しかし、ポインタの基本的な考え方はとてもシンプルです。

普通の変数は値を保存します。それに対して、ポインタはデータが置かれている場所を扱います。

たとえば、次の変数があるとします。

int score = 80;

scoreには80という値が保存されています。しかし、コンピュータの内部では、メモリ上のどこかにscoreのための領域が用意され、その場所へ80が格納されています。

このメモリ上の場所を表す情報がアドレスです。

変数名scoreを使うと保存されている値を取り出せます。一方、&scoreと書くと、scoreが置かれているアドレスを取り出せます。

書き方表すもの
score変数に格納されている値
&score変数が置かれているアドレス

ポインタを理解する最初の一歩は、値とアドレスを区別することです。ここでは、メモリとアドレスの関係、&を使ったアドレスの取得、%pによる表示方法を順番に見ていきましょう。

アドレスとは

コンピュータのメモリは、データを保存するための小さな領域がたくさん並んでいる場所です。それぞれの領域には、位置を区別するための番号があります。この位置を表す番号がアドレスです。

身近なものに置き換えると、変数の値は建物の中にある荷物、アドレスは建物の住所のようなものです。

メモリの考え方身近なたとえ
変数荷物を保管する建物
変数の値建物内にある荷物
アドレス建物の住所
ポインタ住所を記録しておくもの

値とアドレスは、同じものではありません。

scoreの値が80であっても、scoreのアドレスが80になるわけではありません。値は変数の中身であり、アドレスはその中身が置かれている場所です。

変数には値と場所がある

次の変数を考えてみましょう。

int score = 80;

この変数には、次の情報があります。

項目内容
変数名score
int型
格納される値80
メモリ上の場所実行時に決まるアドレス
値を表す書き方score
アドレスを表す書き方&score

通常のプログラムでは、scoreと書いて値80を使います。

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

ポインタを学ぶときは、scoreがメモリ上のどこに置かれているかにも注目します。

&score

&は、変数のアドレスを取得するためのアドレス演算子です。

図1:変数の値とメモリ上のアドレス

この図から分かること

変数scoreには80という値が格納されています。同時に、scoreのための領域にはメモリ上のアドレスがあります。

scoreと書くと値80を表し、&scoreと書くとscoreが置かれているアドレスを表します。

ポインタの学習では、値とアドレスを別の情報として考えることが大切です。

メモリはアドレスによって場所を区別する

メモリには、変数だけでなく、配列や実行中のプログラムが使うさまざまなデータが保存されます。

アドレスがあることで、コンピュータはどこにどのデータが保存されているのかを区別できます。

説明用として、次のような配置を考えてみましょう。

アドレスの例保存されているもの
0x00001000変数scoreの一部
0x00001001変数scoreの一部
0x00001002変数scoreの一部
0x00001003変数scoreの一部
0x00001004別のデータ

int型が4バイトの環境なら、int型変数は4バイト分の領域を使います。変数のアドレスとして扱うのは、一般にその領域の先頭アドレスです。

ただし、int型が必ず4バイトとは限りません。型の大きさは実行環境によって異なる可能性があります。

アドレス演算子&の使い方

変数のアドレスを取得するには、変数名の前に&を付けます。

&変数名

たとえば、次の変数があるとします。

int number = 50;

numberと&numberでは、表すものが異なります。

書き方意味
number変数numberに入っている値50
&number変数numberが置かれているアドレス

値を表示するときは、int型に対応する書式指定子%dを使います。

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

アドレスを表示するときは、書式指定子%pを使います。

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

アドレスは%pで表示する

printfでアドレスを表示するときは、%pを使います。

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

この書き方に含まれる要素を整理すると、次のようになります。

要素役割
printf値や文字列を画面へ表示する
%pアドレスを表示する書式指定子
&numbernumberのアドレスを取得する
(void *)%pへ渡す形式に変換する

printfの%pに対応する引数は、voidポインタとして渡すのが標準的な書き方です。そのため、&numberを(void *)で変換しています。

学習の最初は、&numberがアドレスを表していることを押さえれば大丈夫です。

アドレスは通常16進数のような形式で表示される

アドレスは、次のような16進数形式で表示されることがあります。

0x000000A1B2C3D4E0

実際の表示形式は、OS、コンパイラ、実行環境によって異なります。

アドレスについて大切なのは、数字そのものを覚えることではありません。

  • アドレスはデータが置かれている場所を表す
  • 同じプログラムでもアドレスが変わることがある
  • サンプルと異なるアドレスが表示されても問題ない
  • 値とアドレスは別の情報である

現在のコンピュータでは、実行するたびに変数の配置場所が変わることもあります。そのため、アドレスを特定の数値と決めつけないようにしましょう。

値とアドレスを表示する

バッテリー残量の値と保存場所を表示するプログラム

ファイル名:11_1_1.c

#include <stdio.h>

int main(void)
{
    int battery_level = 75;

    printf("現在のバッテリー残量は%d%%です。\n", battery_level);
    printf("battery_levelの保存場所は%pです。\n",
           (void *)&battery_level);

    return 0;
}
実行結果の例

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

現在のバッテリー残量は75%です。
battery_levelの保存場所は0x000000a1b2c3d4e0です。

変数の宣言を確認する

次の文では、int型の変数battery_levelを宣言し、75で初期化しています。

int battery_level = 75;

この変数には、バッテリー残量を表す値75が保存されます。

項目内容
変数名battery_level
int型
75
アドレス実行時に決まるメモリ上の場所

値を表示する部分

1つ目のprintfでは、battery_levelの値を表示しています。

printf("現在のバッテリー残量は%d%%です。\n", battery_level);

%dはint型の整数を表示する書式指定子です。引数にbattery_levelを指定しているため、変数の中に入っている75が表示されます。

表示文字列内の%%は、画面へ%を1つ表示するための指定です。

この部分で表示しているのはアドレスではなく、変数の中身です。

アドレスを表示する部分

2つ目のprintfでは、battery_levelがメモリ上のどこに置かれているかを表示しています。

printf("battery_levelの保存場所は%pです。\n",
       (void *)&battery_level);

処理は次のように考えられます。

  • &battery_levelで変数のアドレスを取得する
  • (void *)でvoidポインタへ変換する
  • %pでアドレスを表示する

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

表す情報表示に使う書式指定子
battery_level値75%d
&battery_level保存場所%p

図2:変数名と&変数名の違い

この図から分かること

battery_levelは、変数に保存された値75を表します。

&battery_levelは、変数battery_levelが置かれているメモリ上のアドレスを表します。

変数名の前に&が付くかどうかで、値を扱っているのか、場所を扱っているのかが変わります。

実行結果のアドレスが異なっても問題ない

プログラムを実行すると、アドレスはサンプルとは異なる値になるのが普通です。

たとえば、ある実行では次のように表示されるかもしれません。

0x000000a1b2c3d4e0

別の実行では、次のような値になることもあります。

0x000000f5e6d7c8b0

表示されるアドレスが違っても、プログラムの誤りとは限りません。

確認したいのは、次の2点です。

  • battery_levelを表示すると75になる
  • &battery_levelを%pで表示するとアドレスが表示される

アドレスの具体的な数値より、値と場所の違いに注目しましょう。

アドレスを通常の整数として表示しない

アドレスはint型の値とは限りません。そのため、アドレスの表示に%dを使うのは適切ではありません。

正しい表示方法は%pです。

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

次のような表示は避けます。

printf("%d\n", &battery_level);
表示するもの適切な書式指定子
int型の値%d
アドレス%p
size_t型の値%zu

データの型や意味に合った書式指定子を使うことが大切です。

scanfでもアドレスが使われている

scanfを使った入力では、すでにアドレスを利用しています。

int number;

scanf("%d", &number);

scanfは、入力された値を保存する場所を知る必要があります。そのため、numberの値ではなく、&numberによってnumberのアドレスを渡します。

処理の流れは次のように考えられます。

  • scanfが整数を読み取る
  • &numberで保存先を確認する
  • そのアドレスに対応する変数numberへ値を書き込む

printfとscanfでの使い方を比較すると、違いが分かりやすくなります。

処理必要な情報指定例
printfで値を表示する変数の値number
scanfで値を保存する変数の場所&number

scanfで&を付ける理由が分かると、これまで書いてきた入力処理とポインタの考え方がつながってきます。

配列もメモリ上に置かれている

配列も変数と同じようにメモリ上へ配置されます。配列の特徴は、同じ型の要素が連続して並ぶことです。

次の配列を考えてみましょう。

int data[5] = {10, 20, 30, 40, 50};

int型が4バイトの環境で、data[0]のアドレスが0x1000だったと仮定します。

配列要素アドレスの例
data[0]100x1000
data[1]200x1004
data[2]300x1008
data[3]400x100C
data[4]500x1010

隣の要素へ進むたびに、アドレスは4バイト分ずつ進んでいます。

これは説明用の例です。実際のアドレスやint型の大きさは、実行環境によって異なります。

図3:配列要素が連続して並ぶメモリ

この図から分かること

配列の要素は、メモリ上で連続して配置されます。

int型が4バイトなら、隣り合う要素の先頭アドレスは4バイトずつ進みます。double型の配列で各要素が8バイトなら、アドレスの間隔も8バイトになると考えられます。

この連続した配置が、ポインタを使って配列を順番にたどれる理由につながります。

配列の各要素にもアドレスがある

配列全体だけでなく、各要素にもアドレスがあります。

&data[0]
&data[1]
&data[2]

それぞれが、各要素の保存場所を表します。

書き方表すもの
data[0]先頭要素の値
&data[0]先頭要素のアドレス
data[1]2番目の要素の値
&data[1]2番目の要素のアドレス

通常、配列名dataは、多くの式の中で先頭要素のアドレスとして扱われます。

この性質は、ポインタと配列の関係を学ぶときに詳しく扱います。最初の段階では、配列の各要素もメモリ上に場所を持ち、その場所が連続していると理解しておけば大丈夫です。

なぜアドレスを扱うのか

簡単な計算や表示だけなら、変数の値を使うだけでもプログラムを書けます。それでもC言語でアドレスが重要なのは、元のデータが置かれている場所を指定して処理できるからです。

利用場面アドレスが役立つ理由
scanfによる入力入力値を書き込む変数の場所を指定できる
関数から変数を変更する呼び出し元の変数がある場所を渡せる
配列の処理先頭要素の場所を基準に順番に処理できる
文字列の処理先頭文字の場所から \0 までたどれる
動的メモリ確保されたメモリの場所を保持できる

ポインタは、こうしたアドレスを保存して利用するための仕組みです。

ポインタはアドレスを保存するための変数

通常の変数は、整数や小数などの値を保存します。

int number = 50;

ポインタ変数は、ほかの変数のアドレスを保存します。

この段階では、次の対応を押さえておきましょう。

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

ポインタ変数の宣言や、ポインタが指す場所にある値を取り出す方法では、*を使います。

ただし、最初から&と*を同時に覚えようとすると混乱しやすくなります。まずは、変数には値とアドレスがあり、&変数名でアドレスを取得できることを確実に押さえるのがおすすめです。

&の役割を読み取るコツ

コード内に&が出てきたら、まず何の前に付いているかを確認します。

&number

この場合は、numberのアドレスです。

&data[2]

この場合は、data[2]のアドレスです。

読み方を整理すると、次のようになります。

読み取る内容
numbernumberの値
&numbernumberのアドレス
data[2]dataの3番目の要素の値
&data[2]dataの3番目の要素のアドレス

&を見つけたら、値ではなく場所を扱っていると考えると読みやすくなります。

最初に押さえたい基本

ポインタの学習を進める前に、次の点を確認しておきましょう。

確認すること内容
変数の値変数名で取り出す
変数の場所&変数名で取り出す
アドレスの表示%pを使う
%pへ渡す形式(void *)へ変換する
アドレスの数値実行環境や実行時によって変わる
配列の配置同じ型の要素が連続して並ぶ
scanfで&を使う理由入力値の保存場所を渡すため

ポインタを理解する入口は、変数には中身としての値と、置かれている場所としてのアドレスがあると考えることです。

この違いが自然に分かるようになると、次に学ぶポインタ変数の宣言、*による間接参照、関数へアドレスを渡す処理、配列とポインタの関係も理解しやすくなります。