このページでは、C言語で特にポインタやアドレスを扱う上で重要になるアドレス演算子 &
と間接演算子 *
について解説していきます。
プログラムはメモリにアクセス(メモリへのデータの保存やメモリからのデータの取得)を行いながら動作しますが、C言語においてはこのメモリのアクセスをアドレス指定で行うことが可能です。
今回紹介する &
と *
は、このアドレス指定でメモリへのアクセスを行う際に重要になる演算子となります。これらの意味を理解しているとポインタも扱いやすくなると思いますので、是非このページでこれらの演算子の意味合いを理解していってください!
Contents
&
はアドレスを取得する演算子
まずは &
について解説していきます。この &
は、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個です。
こういった、関数から複数のデータを関数呼び出し元に渡したいような場合にアドレスを利用します。要は、関数の引数にアドレスを指定し、そのアドレスに対して関数内でデータを保存してもらうようにします。そうすれば、関数呼び出し元は関数実行後にそのアドレスからデータを取得することで、結果的に2個以上のデータを関数から関数呼び出し元に渡すことができます。
ただし、関数に渡すアドレスをてきとうに選んでしまうと他のアプリやプログラムが使用しているメモリのアドレスが選ばれてしまい、それらのアプリやプログラムの動きに影響してしまう可能性があります(前述の通りメモリアクセス違反が発生します)。
したがって、このような目的で利用するアドレスも適切な位置のものを指定する必要があります(他のアプリやプログラムと干渉しないアドレス)。また、宣言した変数は必ず適切な位置のアドレスに配置されます。ですので、宣言した変数のアドレスを関数に渡してやれば安全に2個以上のデータを受け取ることができる関数呼び出しを実現できます。
では、変数のアドレスはどうやって取得すれば良いでしょうか?
ここで利用するのがアドレス演算子 &
です。C言語では、変数の前に &
演算子を付加することで、その変数の先頭アドレスを取得することができます。
例えば上の図のように関数(calc
関数とします)に引数として変数 x
のアドレスを渡したい場合、下記のように変数 x
の前に &
演算子をつけて引数指定を行います。これにより変数 x
のアドレスが取得され、そのアドレスが関数側に渡されることになります。
int x;
int ret;
ret = calc(100, 200, &x);
また、配列の要素の前に &
演算子を付加することで、その配列の要素の先頭アドレスを取得することができます。
int x[2];
x[0] = calc(100, 200, &x[1]);
こんな感じで、変数や要素のアドレスを取得するのが &
演算子であり、この &
演算子を使用する目的は、「安全に使用できるメモリのアドレスを取得するため」と言えます。
実際に複数の値を返却する関数の作り方については下記ページで解説していますので、関数呼び出し側だけでなく関数側の処理をどのようにすればよいかが気になる方は下記ページを参照していただければと思います(このページでは &
演算子と *
演算子の説明に焦点を当てているため、ページを分けて解説させていただいています)。
また、ここまで説明してきませんでしたが、アドレス演算子 &
を利用しなくてもアドレスが取得できるようなケースもあります。
例えば配列に関しては、配列名を指定するだけで配列の先頭アドレス、つまり 0
番目の要素のアドレスを取得することができます。配列の途中の要素のアドレスを取得する場合には、要素に対してアドレス演算子 &
を利用する必要があります(アドレスを表示する場合は printf
関数に変換指定子 %p
を指定します)。
int array[100];
/* 配列arrayの先頭アドレスを表示 */
printf("%p\n", array);
/* 要素array[50]のアドレスを表示 */
printf("%p\n", &array[50]);
*
はアドレスからメモリにアクセスする演算子
ここまで解説してきた通り、&
演算子では変数からメモリのアドレスを取得することができます。
ただし、アドレスは取得しただけではあまり意味がありません。そのアドレスのメモリにデータを保存したり、そのアドレスのメモリからデータを取得することで、初めてアドレスを取得した意味が生まれます(以降では、メモリにデータを保存する処理とメモリからデータを取得する処理をまとめて “メモリへのアクセス” と呼ばせていただきます)
例えば、先程挙げた関数呼び出しの例では、関数側にアドレスを渡したところで、そのアドレスのメモリへのアクセス(メモリへの実行結果の保存)が行われないとアドレスを渡した意味がありません。
この、アドレスからメモリへアクセスを行うために利用する演算子が *
になります。このような目的で使用する *
演算子を「間接演算子」と呼びます。
スポンサーリンク
アドレスから *
演算子でメモリにアクセス
この間接演算子 *
は、アクセスしたいメモリのアドレス(もしくはアドレスを格納したポインタ変数)の前に指定する形で使用します。
例えば下記は、アドレス 0x100
の前に間接演算子 *
を指定しているため、アドレス 0x100
に対してメモリアクセスを行おうとする処理となります。1行目でアドレス 0x100
に 1234
を保存し、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言語においては、アドレスを変数で管理することも可能です。このアドレスを管理する変数が「ポインタ」です。
ポインタにはアドレスを格納することができ、そのアドレスを格納できてしまうという点がポインタの複雑なところになると思います。
前述の通り、C言語では “変数指定” でも “アドレス指定” でもメモリにアクセスすることができます。ポインタは変数であり、さらにアドレスを格納していますので、1つのポインタ変数から2つのメモリにアクセスできてしまうのです。1つ目は「ポインタ変数自体のメモリ」で、2つ目は「ポインタ変数に格納されたアドレスのメモリ」です。ここがポインタの便利なところでもあり、複雑なところでもあります。
2つのメモリにアクセスできてしまうので、プログラマーはポインタを扱う際、どちらのメモリにアクセスするかを指定してやる必要があります。
ただ、指定の仕方はここまで解説してきた通りで、変数指定で「ポインタ変数自体のメモリ」にアクセスする場合は単に変数名の指定を行えば良いですし、アドレス指定で「ポインタ変数に格納されたアドレスのメモリ」にアクセスする場合には、変数名の前に間接演算子 *
を付加してれば良いだけです。
int x;
int *p;
p= &x; /* ポインタ変数p自体のメモリに&xを保存 */
*p = 1234; /* pに格納されているアドレスのメモリに1234を保存 */
ですので、ポインタ変数に単に代入を行なった場合、ポインタ変数のメモリが上書きされることになります。従って、ポインタ変数に格納されているアドレスが上書きされることになります。下記ページでも解説していますが、ポインタは直感的に捉えると他の変数やメモリを指す変数であると考えることができます。
【C言語】ポインタを初心者向けに分かりやすく解説従って、ポインタ変数に単に代入を行なった場合、ポインタ変数の指す先が変わることになります。
その一方で、ポインタ変数に間接演算子 *
を利用して代入した場合、これは アドレスから * 演算子でメモリにアクセス での解説内容同様に、アドレス指定でメモリにデータを保存することになります。より具体的には、ポインタ変数に格納されているアドレスのメモリにデータを保存することになります。
ですので、この場合は、ポインタ変数が指す先ではなく、指している先のメモリにデータが保存されることになります。
このあたりの、ポインタ変数からは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
自体にアドレスが格納されることになります。
p = &x;
p = &y;
&
演算子によって変数のアドレスを取得し、そのアドレスをポインタ変数 p
に保存しています。これによって変数 p
には変数 x
や変数 y
のアドレスが格納され、ポインタ変数 p
が変数 x
や変数 y
を指すことになります。
3点目は下記になります。下記においては間接演算子 *
を利用しているため、ポインタ変数 p
に格納されているアドレスのメモリに 100
や 200
の数値の保存が行われることになります。
*p = 100;
*p = 200;
上記の2つの処理に関しては、左辺は全く同じですが、ポインタ変数 p
に格納されているアドレスが異なるため、それぞれで別のメモリに数値 100
と数値 200
が保存されることになります。より具体的には、変数 x
のアドレスのメモリに 100
が、変数 y
のアドレスのメモリに 200
が保存されることになります。
4点目は変数 p
から複数の変数のメモリを変更できた点になります。間接演算子を用いることで、変数 p
から変数 x
のアドレスのメモリと変数 y
のアドレスのメモリの2つに数値の保存ができています。
通常の変数では、基本的には、その変数のアドレスのメモリにしか保存できません。例えば変数 x
に数値を代入した場合、必ず変数 x
のアドレスのメモリにその数値が保存されることになります。変数 x
に数値を代入して変数 y
のアドレスのメモリにその数値を保存するようなことはできないのです。
その一方で、間接演算子 *
を利用することで同じポインタ変数から様々な変数のアドレスのメモリに対してデータを保存したり、データの取得を行ったりすることが可能です。
ポインタ変数は変数指定による代入でポインタ変数自体の変更も可能であり、さらに間接演算子 *
を利用することでアドレスを介して間接的にメモリを変更することができますので、ポインタ変数では様々な変数のアドレスのメモリを変更することができるようになります。
また、これによって変更できるのは変数のアドレスのメモリだけではありません。C言語では malloc
という関数が存在し、この関数によりプログラム実行中に動的に必要なメモリを取得するようなこともできます(このメモリも他のプログラムやアプリに干渉しないものになります)。
malloc
関数などで取得したメモリは変数ではありませんので、変数指定で直接アクセスすることができません。こういったメモリであっても、ポインタ変数でメモリのアドレスを覚えておき、そのアドレスに対して間接演算子を用いることでメモリへのアクセスを行うことができます。
こちらの malloc
関数については下記ページで解説していますので、詳しくは下記ページを参照していただければと思います。
他にもハードウェアを制御するような時も、アドレスと間接演算子を用いるような事もあります。
こんな感じで、間接演算子 *
を利用することで、アドレスやポインタ変数から間接的に様々なメモリへのアクセスを行うことが可能です。
逆に言えば、間接演算子を利用すればメモリのどこにでもアクセスできてしまうことになります。前述の通り、適切でないアドレスのメモリにアクセスするとメモリアクセス違反が発生してしまいますので、間接演算子を利用する場合は、間接演算子を作用させるアドレスに注意しながらプログラミングを行なっていく必要があります。
間接演算子が使用できるのはアドレスに対してのみ
ここまでポインタ変数に対する間接演算子 *
の利用について解説してきました。
ただ、アドレスはメモリ上の位置を示すものであるものの単なる数値ですので、通常の long
型の変数等にも格納することが可能です(ただしコンパイル時に警告が発生するはずです)。
int x;
long p;
p = &x;
printf("%p\n", &x);
printf("%p\n", p);
上記の実行結果は次のようになり、変数 p
に変数 x
のアドレスが格納されていることが確認できると思います。
0x7ffeefbff4ac 0x7ffeefbff4ac
ただし、間接演算子 *
はアドレスに対してのみしか使用することができません(ポインタは格納した値がアドレスとして扱われるので間接演算子 *
の利用は OK)。
ですので、上記のように通常の変数でもアドレスの値は格納することはできるのですが、通常の変数に格納した値はアドレスではなく単なる数値として扱われるため、間接演算子を使用することができません。
例えば下記のようなソースコードをコンパイルすると、
int x;
long p;
p = &x;
*p = 100;
4行目のところで下記のようにコンパイルエラーが発生します(環境によってエラーの文言は異なると思います)。
error: indirection requires pointer operand ('long' invalid)
つまり、通常の変数にアドレスを格納したところで間接演算子を利用することができないため、そのアドレスからメモリにアクセスすることができません。
基本的にアドレスを利用するのは、そのアドレスのメモリにアクセスすることが目的になります。ですので、通常の変数にアドレスを格納したところで、その変数は使い道のない変数になってしまいます。
アドレスを覚えておくだけであれば通常の変数でも目的は果たすことはできるかもしれませんが、それでもソースコードの可読性が下がります(なぜ、ここでポインタ変数ではなく通常の変数にアドレスを格納しているのかが謎)。
こういった理由から、アドレスを格納する変数にはポインタ変数を利用するようにしましょう。
一応言っておくと、(int *)
や (long *)
などでポインタ型(アドレスを扱う変数の型)にキャストしてやれば、通常の変数でも間接演算子 *
を利用することも可能です
スポンサーリンク
まとめ
このページでは、C言語におけるアドレス演算子 &
と間接演算子 *
について解説しました!
C言語においてはアドレス指定でメモリにアクセスすることが可能であり、そのアドレスを取得するのにアドレス演算子 &
を利用し、アドレスからメモリにアクセスする際に間接演算子 *
を利用します。
特にポインタを扱う際には、このアドレス演算子 &
と間接演算子 *
が非常に重要になってきます!
最初はややこしく感じるかもしれませんが、使っているうちに慣れてくる演算子だと思いますので、どんどんポインタを使ったプログラムを作成してみることが理解への近道になると思います。
また、アドレス演算子 &
と間接演算子 *
を利用した「複数の値を返却する関数」を実現する方法についても下記ページで解説していますので、理解を深めるためにもぜひ読んでみてください!
オススメの参考書(PR)
C言語一通り勉強したけど「ポインタがよく分からない」「ポインタの理解があやふや」「もっとC言語の理解を深めたい」という方には、下記の「C言語ポインタ完全制覇」がオススメです!
この本の主な内容は下記の通りで、通常の参考書では50ページくらいで解説するポインタを、この本では約 "360ページ" 使って幅広く・深く解説しています。
- C言語でのメモリの使い方
- 配列とポインタの関係性
- ポインタのよくある使い方
- ポインタの効果的な使い方
一通りC言語を学んだだけだと "理解があやふやになってしまいがち" "疑問に思いがち" な内容に対する明確な解説が多いため、特にポインタやC言語の理解があやふやという方にはオススメの本です。
また、C言語においてポインタはまさに "肝" となる機能ですので、ポインタについてより深く学ぶことでC言語全体の理解を深めることにもつながります。
ポインタ・C言語についてより深く理解するための本としては現状1番のオススメの本です。
ただし、他の入門書等で "一通りC言語を学んでいる" 方向けの解説になっているので、"C言語を始めるにあたっての最初の入門書" として利用すると難易度が高いので注意してください。
入門用のオススメ参考書は下記ページで紹介していますので、こちらも是非参考にしていただければと思います。
https://daeudaeu.com/c_reference_book/