【C言語/ポインタ】アドレス演算子「&」と間接演算子「*」について解説

アドレス演算子と間接演算子の解説ページアイキャッチ

このページでは、C言語で特にポインタやアドレスを扱う上で重要になるアドレス演算子 & と間接演算子 * について解説していきます。

プログラムはメモリにアクセス(メモリへのデータの保存やメモリからのデータの取得)を行いながら動作しますが、C言語においてはこのメモリのアクセスをアドレス指定で行うことが可能です。

今回紹介する &* は、このアドレス指定でメモリへのアクセスを行う際に重要になる演算子となります。これらの意味を理解しているとポインタも扱いやすくなると思いますので、是非このページでこれらの演算子の意味合いを理解していってください!

& はアドレスを取得する演算子

まずは & について解説していきます。この & は、C言語では論理積演算を行うためにも利用するのですが、特にポインタやアドレスを扱う際には「アドレスを取得する」ために利用します。

この目的で使用する & は「アドレス演算子」と呼ばれます。

アドレスとメモリ

アドレス演算子 & について説明する前に、まずはアドレスとメモリについて解説しておきます。

パソコン等のコンピュータ、テレビやゲーム機等の組み込み機器にはメモリが搭載されています。例えばパソコンのスペック表にもメモリの容量が明記されており、その容量を重視して今使用しているパソコンを選んだ方もいるのではないでしょうか?

メモリはデータの記憶が可能で、上記のような機器では、メモリにデータを保存し、必要になった際にメモリからデータを取得することで様々な処理を実現しています。

例えば PC 上で動作する計算機アプリだと、ユーザーが指定した数値や演算子をメモリに保存しておき、= が押された際にメモリから記憶しておいた数値や演算子を取得して結果を計算するような感じです。

計算機アプリがメモリを利用する様子

こんな感じで、プログラムやアプリが動作するためにはデータの記憶が必要で、その記憶にはメモリが利用されます。

ただし、メモリからデータを取得するためには、「各データをメモリのどこに保存しておいたか」が管理できるようになっていないといけません。

そのため、メモリには1バイト単位で「アドレス(番地)」が割り振られており、このアドレスによってメモリ上の位置が管理されるようになっています。ですので、データを保存したメモリのアドレスを覚えておけば、取得時にメモリのどこから取得すれば良いかがすぐに分かるようになっています(アドレスは16進数で表されることが多いです)。

メモリにアドレスが割り振られている様子

イメージとしては、タンスの引き出しに番号(アドレス)を振っておく感じです。引き出しの数が多くても、どの番号の引き出しにどの服を入れたかを覚えておけば、タンスから一瞬で必要な服を取得することができますね!

スポンサーリンク

アドレスと変数

ただし、プログラマーからすると、アドレスを利用してメモリにデータを保存したりメモリからデータを取得したりするプログラムを作成するのは大変です…。

アドレスを指定してプログラミングするのは大変

例えば計算機アプリを作ろうと思って 0x10 番地に1つ目に入力された数値を保存して、0x14 番地に入力された演算子を保存して、0x18 番地に2つ目に入力された数値を保存して…、といったようにアドレスを直に指定する必要があるとなると大変ですよね。

何より作成したソースコードがものすごく読みにくくなりそうです…。

サイズも考慮しながらプログラミングする必要がある

さらに、1つのアドレスに保存できるデータは1バイト分のみです。ですので、1つのアドレスに保存するデータは256種類のデータしか表現できません。もっと多くの種類のデータを表現したいのであれば、複数のバイトに跨ってデータを保存しておく必要があります。

つまり、どのアドレスにデータを保存したかだけでなく、そのアドレスから何バイト分のメモリにデータを保存するのかも意識しながらプログラミングする必要があります。

どのアドレスであるかだけでなく、そのアドレスから何バイト利用するかも考慮する必要がある様子

他のプログラムやアプリの邪魔をしてはいけない

もっと言えば、あなたがプログラミングして作成するプログラム・アプリは、どのアドレスのメモリを使用しても良いというわけではありません。

あなたが作成したプログラムやアプリが好き勝手にメモリにデータを保存してしまうと他のプログラム・アプリが使用しているメモリが変更され、それらのプログラムやアプリが正常に動作できなくなってしまいます。

他のアプリが使用するメモリを変更してしまう様子

このような事が起こらない用、Windows や Mac、Linux などの OS では、あなたのプログラムがおかしなアドレスのメモリにデータを保存したりしようとすると「メモリアクセス違反」が発生するようになっています。C言語のプログラムにおいては、メモリアクセス違反を起こしてしまった場合にプログラムは終了するようになっています。

ですので、メモリアクセス違反が起こらないよう、他のアプリやプログラムが使用していないメモリを利用するようにアドレスを指定しながらプログラミングする必要があって大変です。

上記のような理由から、アドレスを直に指定してプログラミングを行うのは困難です。

変数はアドレスを指定する場合の問題を解決してくれる

この困難さを解決してくれるのが「変数」です。

変数は、宣言を行うことでプログラム実行時に適切なアドレスのメモリ(他のプログラムやアプリが使用していないメモリ)に自動的に配置されるようになっています。この変数に代入などを行えば、変数が配置されているアドレスのメモリにデータの保存が行われます。

従って、アドレスではなく変数を利用してプログラミングすることで、あなたが作成したプログラムやアプリが他のプログラム・アプリが使用しているアドレスのメモリにデータを保存するようなことを自動的に防ぐことができるようになっています(図は int 型のサイズを4バイトとして書いています)。

変数がメモリ上に配置される様子

また、アドレスという数値ではなく変数名という文字列を利用してプログラミングできますので、変数名をうまく設定することで読みやすいソースコードにすることができます。

さらに、C言語では変数宣言時に「型」を指定する必要がありますが、この型によって変数が扱うデータのサイズが決まります。

そのため、変数宣言時に変数の型さえ上手く選んでやれば、変数を指定するだけで自動的に型に応じたサイズ分のメモリに対してデータの保存やデータの取得が行えるようになっています。

型に応じたサイズに合わせて変数がメモリ上に配置される様子

ですので、アドレスを直に指定する場合に比べればサイズはそこまで意識することなくプログラミングすることができるようになります(ただし、型のサイズを超えるデータを変数に代入するとオーバーフローが発生するため、やっぱりサイズは意識しながらプログラミングする必要があります)。

こんな感じで、変数を利用することでアドレスを意識することなく安全にメモリを扱うことができるようになります。

安全なアドレスを取得する & 演算子

このように、変数を利用することでアドレスを意識することなくプログラミングすることができるようになるのですが、残念ながら(?)、特にC言語においてはアドレスを利用せざるを得ないようなケースが結構多いです。

例えば、C言語では関数が返却できる(return できる)データの最大数は1個です。

関数から返却できるデータが1つであることを示す図

こういった、関数から複数のデータを関数呼び出し元に渡したいような場合にアドレスを利用します。要は、関数の引数にアドレスを指定し、そのアドレスに対して関数内でデータを保存してもらうようにします。そうすれば、関数呼び出し元は関数実行後にそのアドレスからデータを取得することで、結果的に2個以上のデータを関数から関数呼び出し元に渡すことができます。

引数にアドレスを指定することで複数のデータを関数から受け取れるようになる様子

ただし、関数に渡すアドレスをてきとうに選んでしまうと他のアプリやプログラムが使用しているメモリのアドレスが選ばれてしまい、それらのアプリやプログラムの動きに影響してしまう可能性があります(前述の通りメモリアクセス違反が発生します)。

したがって、このような目的で利用するアドレスも適切な位置のものを指定する必要があります(他のアプリやプログラムと干渉しないアドレス)。また、宣言した変数は必ず適切な位置のアドレスに配置されます。ですので、宣言した変数のアドレスを関数に渡してやれば安全に2個以上のデータを受け取ることができる関数呼び出しを実現できます。

変数xのアドレスに関数の実行結果を保存してもらう様子

では、変数のアドレスはどうやって取得すれば良いでしょうか?

ここで利用するのがアドレス演算子 & です。C言語では、変数の前に & 演算子を付加することで、その変数の先頭アドレスを取得することができます。

例えば上の図のように関数(calc 関数とします)に引数として変数 x のアドレスを渡したい場合、下記のように変数 x の前に & 演算子をつけて引数指定を行います。これにより変数 x のアドレスが取得され、そのアドレスが関数側に渡されることになります。

変数xのアドレスを取得する様子

int x;
int ret;

ret = calc(100, 200, &x);

また、配列の要素の前に & 演算子を付加することで、その配列の要素の先頭アドレスを取得することができます。

変数xのアドレスを取得する様子

int x[2];

x[0] = calc(100, 200, &x[1]);

こんな感じで、変数や要素のアドレスを取得するのが & 演算子であり、この & 演算子を使用する目的は、「安全に使用できるメモリのアドレスを取得するため」と言えます。

実際に複数の値を返却する関数の作り方については下記ページで解説していますので、関数呼び出し側だけでなく関数側の処理をどのようにすればよいかが気になる方は下記ページを参照していただければと思います(このページでは & 演算子と * 演算子の説明に焦点を当てているため、ページを分けて解説させていただいています)。

複数の関数を返却する方法の解説ページアイキャッチ【C言語】関数から複数の値を返却する方法

また、ここまで説明してきませんでしたが、アドレス演算子 & を利用しなくてもアドレスが取得できるようなケースもあります。

例えば配列に関しては、配列名を指定するだけで配列の先頭アドレス、つまり 0 番目の要素のアドレスを取得することができます。配列の途中の要素のアドレスを取得する場合には、要素に対してアドレス演算子 & を利用する必要があります(アドレスを表示する場合は printf 関数に変換指定子 %p を指定します)。

配列のアドレスの取得

int array[100];

/* 配列arrayの先頭アドレスを表示 */
printf("%p\n", array);

/* 要素array[50]のアドレスを表示 */
printf("%p\n", &array[50]);

* はアドレスからメモリにアクセスする演算子

ここまで解説してきた通り、& 演算子では変数からメモリのアドレスを取得することができます。

ただし、アドレスは取得しただけではあまり意味がありません。そのアドレスのメモリにデータを保存したり、そのアドレスのメモリからデータを取得することで、初めてアドレスを取得した意味が生まれます(以降では、メモリにデータを保存する処理とメモリからデータを取得する処理をまとめて “メモリへのアクセス” と呼ばせていただきます)

例えば、先程挙げた関数呼び出しの例では、関数側にアドレスを渡したところで、そのアドレスのメモリへのアクセス(メモリへの実行結果の保存)が行われないとアドレスを渡した意味がありません。

この、アドレスからメモリへアクセスを行うために利用する演算子が * になります。このような目的で使用する * 演算子を「間接演算子」と呼びます。

スポンサーリンク

アドレスから * 演算子でメモリにアクセス

この間接演算子 * は、アクセスしたいメモリのアドレス(もしくはアドレスを格納したポインタ変数)の前に指定する形で使用します。

例えば下記は、アドレス 0x100 の前に間接演算子 * を指定しているため、アドレス 0x100 に対してメモリアクセスを行おうとする処理となります。1行目でアドレス 0x1001234 を保存し、2行目で アドレス 0x100 に保存されている値を取得しようとする処理になります。

てきとうなアドレスからのメモリアクセス

*(int *)0x100 = 1234;
printf("%d\n", *(int *)0x100);

(int *) でキャストをしているのは、0x100 をアドレスとして扱うためです。後述でも解説しますが、間接演算子 * はアドレスにしか利用できない演算子になります。

上記は間接演算子 * の意味を理解するのには分かりやすいとは思いますが、アドレスと変数 でも説明した通り、てきとうなアドレスに対してメモリにアクセスしてしまうとメモリアクセス違反が発生します。アドレス 0x100 もてきとうなアドレスですので、上記を実行するとメモリアクセス違反が発生します。

その一方で、& 演算子を利用することで安全なメモリのアドレスを取得することができます。ですので、変数のアドレスを & 演算子を利用して取得し、そのアドレスから * 演算子によりメモリにアクセスすればメモリアクセス違反を起こすことなくメモリアクセスを行うことができます。

例えば下記の処理では、2行目で変数 x のアドレスのメモリに保存されている数値を取得して printf で表示を行い、さらに3行目において、そのアドレスのメモリに数値 789 を保存する処理を行なっています。

変数のアドレスからのメモリアクセス

int x = 100;
printf("%d\n", *(&x));
*(&x) = 789;
printf("%d\n", x);

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

100
789

変数 x に直接数値 789 を代入する処理は行なっていませんが、3行目で変数 x のアドレスのメモリに数値 789 を保存しているため、結果的に変数 x の値が変化することになっています。

このように、変数に対して直接アクセスするのではなく、アドレスから間接的に変数(メモリ)にアクセスするので、* 演算子は間接演算子と呼ばれています(アドレスから直接メモリにアクセスしているので直接演算子といっても良さそうなものでもありますが…)。

ポイントになるのが、C言語では変数指定によるメモリアクセスだけでなく、「アドレスを指定してメモリにアクセスすることもできる」という点になると思います。

変数指定でメモリにアクセスした場合は、変数自体のメモリにアクセスすることになりますし(変数に格納されている値の変更や取得を行う)、アドレス指定でメモリにアクセスした場合は、そのアドレスのメモリにアクセスすることになります。

C言語における2つのメモリアクセス方法を図示したもの

そして、アドレスを指定してメモリにアクセスする際には、間接演算子 * を利用することが必要になります。

ポインタ変数から * 演算子でメモリにアクセス

さらにC言語においては、アドレスを変数で管理することも可能です。このアドレスを管理する変数が「ポインタ」です。

ポインタにはアドレスを格納することができ、そのアドレスを格納できてしまうという点がポインタの複雑なところになると思います。

前述の通り、C言語では “変数指定” でも “アドレス指定” でもメモリにアクセスすることができます。ポインタは変数であり、さらにアドレスを格納していますので、1つのポインタ変数から2つのメモリにアクセスできてしまうのです。1つ目は「ポインタ変数自体のメモリ」で、2つ目は「ポインタ変数に格納されたアドレスのメモリ」です。ここがポインタの便利なところでもあり、複雑なところでもあります。

ポインタ変数からアクセスできる2つのメモリの説明図

2つのメモリにアクセスできてしまうので、プログラマーはポインタを扱う際、どちらのメモリにアクセスするかを指定してやる必要があります。

ただ、指定の仕方はここまで解説してきた通りで、変数指定で「ポインタ変数自体のメモリ」にアクセスする場合は単に変数名の指定を行えば良いですし、アドレス指定で「ポインタ変数に格納されたアドレスのメモリ」にアクセスする場合には、変数名の前に間接演算子 * を付加してれば良いだけです。

ポインタ変数からのメモリアクセス

int x;
int *p;

p= &x; /* ポインタ変数p自体のメモリに&xを保存 */
*p = 1234; /* pに格納されているアドレスのメモリに1234を保存 */

ですので、ポインタ変数に単に代入を行なった場合、ポインタ変数のメモリが上書きされることになります。従って、ポインタ変数に格納されているアドレスが上書きされることになります。下記ページでも解説していますが、ポインタは直感的に捉えると他の変数やメモリを指す変数であると考えることができます。

ポインタの解説ページアイキャッチ【C言語】ポインタを初心者向けに分かりやすく解説

従って、ポインタ変数に単に代入を行なった場合、ポインタ変数の指す先が変わることになります。

ポインタ変数pに単なる代入を行なった時の動作

その一方で、ポインタ変数に間接演算子 * を利用して代入した場合、これは アドレスから * 演算子でメモリにアクセス での解説内容同様に、アドレス指定でメモリにデータを保存することになります。より具体的には、ポインタ変数に格納されているアドレスのメモリにデータを保存することになります。

ポインタ変数pに間接演算子を用いて代入を行なった時の動作

ですので、この場合は、ポインタ変数が指す先ではなく、指している先のメモリにデータが保存されることになります。

このあたりの、ポインタ変数からは2つのメモリにアクセスできる点と、アクセス先の切り替えは間接演算子 * の有無によって切り替えられる点をしっかり覚えておくことが、ポインタをうまく扱うポイントの1つになると思います。特にアドレス指定でメモリにアクセスする場合は間接演算子 * が必要であることはしっかり頭に入れておきましょう!

代入の仕方によって変化するメモリ領域の違いを表した図

この辺りを踏まえて、ポインタ変数の利用例についてみていきましょう!下記は、ポインタ変数 p から間接的に変数 x と変数 y の値を変更する例となります。

ポインタ変数からのメモリへのアクセス

int x, y;
int *p;

p = &x;
*p = 100;

p = &y;
*p = 200;

printf("%d\n", x);
printf("%d\n", y);

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

100
200

上記においてポイントは4つあります。

まず1点目は下記部分になります。下記においてはポインタ変数 p の変数宣言を行なっています。

ポインタ変数の変数宣言

int *p;

ここでも * を使用していますが、変数宣言時に * を指定した場合、この * は変数がポインタであることを示すための記号として作用します。間接演算子として作用しているわけではないので注意してください。

2点目は下記部分になります。下記ではポインタ変数に間接演算子を利用せずに代入を行なっているため、ポインタ変数 p 自体にアドレスが格納されることになります。

ポインタ変数へのアドレスの格納1

p = &x;

ポインタ変数へのアドレスの格納2

p = &y;

& 演算子によって変数のアドレスを取得し、そのアドレスをポインタ変数 p に保存しています。これによって変数 p には変数 x や変数 y のアドレスが格納され、ポインタ変数 p が変数 x や変数 y を指すことになります。

ポインタ変数が様々な変数のアドレスを指す様子

3点目は下記になります。下記においては間接演算子 * を利用しているため、ポインタ変数 p に格納されているアドレスのメモリに 100200 の数値の保存が行われることになります。

ポインタ変数からのメモリへのアクセス1

*p = 100;

ポインタ変数からのメモリへのアクセス2

*p = 200;

上記の2つの処理に関しては、左辺は全く同じですが、ポインタ変数 p に格納されているアドレスが異なるため、それぞれで別のメモリに数値 100 と数値 200 が保存されることになります。より具体的には、変数 x のアドレスのメモリに 100 が、変数 y のアドレスのメモリに 200 が保存されることになります。

4点目は変数 p から複数の変数のメモリを変更できた点になります。間接演算子を用いることで、変数 p から変数 x のアドレスのメモリと変数 y のアドレスのメモリの2つに数値の保存ができています。

通常の変数では、基本的には、その変数のアドレスのメモリにしか保存できません。例えば変数 x に数値を代入した場合、必ず変数 x のアドレスのメモリにその数値が保存されることになります。変数 x に数値を代入して変数 y のアドレスのメモリにその数値を保存するようなことはできないのです。

各変数から変更可能なメモリ

その一方で、間接演算子 * を利用することで同じポインタ変数から様々な変数のアドレスのメモリに対してデータを保存したり、データの取得を行ったりすることが可能です。

ポインタ変数から間接的に変更可能な変数のメモリ

ポインタ変数は変数指定による代入でポインタ変数自体の変更も可能であり、さらに間接演算子 * を利用することでアドレスを介して間接的にメモリを変更することができますので、ポインタ変数では様々な変数のアドレスのメモリを変更することができるようになります。

また、これによって変更できるのは変数のアドレスのメモリだけではありません。C言語では malloc という関数が存在し、この関数によりプログラム実行中に動的に必要なメモリを取得するようなこともできます(このメモリも他のプログラムやアプリに干渉しないものになります)。

malloc 関数などで取得したメモリは変数ではありませんので、変数指定で直接アクセスすることができません。こういったメモリであっても、ポインタ変数でメモリのアドレスを覚えておき、そのアドレスに対して間接演算子を用いることでメモリへのアクセスを行うことができます。

こちらの malloc 関数については下記ページで解説していますので、詳しくは下記ページを参照していただければと思います。

malloc解説ページのアイキャッチ【C言語】malloc関数(メモリの動的確保)について分かりやすく解説

他にもハードウェアを制御するような時も、アドレスと間接演算子を用いるような事もあります。

こんな感じで、間接演算子 * を利用することで、アドレスやポインタ変数から間接的に様々なメモリへのアクセスを行うことが可能です。

逆に言えば、間接演算子を利用すればメモリのどこにでもアクセスできてしまうことになります。前述の通り、適切でないアドレスのメモリにアクセスするとメモリアクセス違反が発生してしまいますので、間接演算子を利用する場合は、間接演算子を作用させるアドレスに注意しながらプログラミングを行なっていく必要があります。

間接演算子が使用できるのはアドレスに対してのみ

ここまでポインタ変数に対する間接演算子 * の利用について解説してきました。

ただ、アドレスはメモリ上の位置を示すものであるものの単なる数値ですので、通常の long 型の変数等にも格納することが可能です(ただしコンパイル時に警告が発生するはずです)。

long型変数へのアドレスの格納

int x;
long p;

p = &x;
printf("%p\n", &x);
printf("%p\n", p);

上記の実行結果は次のようになり、変数 p に変数 x のアドレスが格納されていることが確認できると思います。

0x7ffeefbff4ac
0x7ffeefbff4ac

ただし、間接演算子 * はアドレスに対してのみしか使用することができません(ポインタは格納した値がアドレスとして扱われるので間接演算子 * の利用は OK)。

ですので、上記のように通常の変数でもアドレスの値は格納することはできるのですが、通常の変数に格納した値はアドレスではなく単なる数値として扱われるため、間接演算子を使用することができません。

例えば下記のようなソースコードをコンパイルすると、

long型変数に対する間接演算子のりよう

int x;
long p;

p = &x;
*p = 100;

4行目のところで下記のようにコンパイルエラーが発生します(環境によってエラーの文言は異なると思います)。

error: indirection requires pointer operand ('long' invalid)

つまり、通常の変数にアドレスを格納したところで間接演算子を利用することができないため、そのアドレスからメモリにアクセスすることができません。

基本的にアドレスを利用するのは、そのアドレスのメモリにアクセスすることが目的になります。ですので、通常の変数にアドレスを格納したところで、その変数は使い道のない変数になってしまいます。

アドレスを覚えておくだけであれば通常の変数でも目的は果たすことはできるかもしれませんが、それでもソースコードの可読性が下がります(なぜ、ここでポインタ変数ではなく通常の変数にアドレスを格納しているのかが謎)。

こういった理由から、アドレスを格納する変数にはポインタ変数を利用するようにしましょう。

MEMO

一応言っておくと、(int *)(long *) などでポインタ型(アドレスを扱う変数の型)にキャストしてやれば、通常の変数でも間接演算子 * を利用することも可能です

スポンサーリンク

まとめ

このページでは、C言語におけるアドレス演算子 & と間接演算子 * について解説しました!

C言語においてはアドレス指定でメモリにアクセスすることが可能であり、そのアドレスを取得するのにアドレス演算子 & を利用し、アドレスからメモリにアクセスする際に間接演算子 * を利用します。

特にポインタを扱う際には、このアドレス演算子 & と間接演算子 * が非常に重要になってきます!

最初はややこしく感じるかもしれませんが、使っているうちに慣れてくる演算子だと思いますので、どんどんポインタを使ったプログラムを作成してみることが理解への近道になると思います。

また、アドレス演算子 & と間接演算子 * を利用した「複数の値を返却する関数」を実現する方法についても下記ページで解説していますので、理解を深めるためにもぜひ読んでみてください!

複数の関数を返却する方法の解説ページアイキャッチ【C言語】関数から複数の値を返却する方法

オススメの参考書

C言語一通り勉強したけど「ポインタがよく分からない」「ポインタの理解があやふや」「もっとC言語の理解を深めたい」という方には、下記の「C言語ポインタ完全制覇」がオススメです!

この本の主な内容は下記の通りで、通常の参考書では50ページくらいで解説するポインタを、この本では約 "360ページ" 使って幅広く・深く解説しています。

  • C言語でのメモリの使い方
  • 配列とポインタの関係性
  • ポインタのよくある使い方
  • ポインタの効果的な使い方

一通りC言語を学んだだけだと "理解があやふやになってしまいがち" "疑問に思いがち" な内容に対する明確な解説が多いため、特にポインタやC言語の理解があやふやという方にはオススメの本です。

また、C言語においてポインタはまさに "肝" となる機能ですので、ポインタについてより深く学ぶことでC言語全体の理解を深めることにもつながります。

ポインタ・C言語についてより深く理解するための本としては現状1番のオススメの本です。

ただし、他の入門書等で "一通りC言語を学んでいる" 方向けの解説になっているので、"C言語を始めるにあたっての最初の入門書" として利用すると難易度が高いので注意してください。

入門用のオススメ参考書は下記ページで紹介していますので、こちらも是非参考にしていただければと思います。

https://daeudaeu.com/c_reference_book/

同じカテゴリのページ一覧を表示