【C言語】ポインタを初心者向けに分かりやすく解説

ポインタの解説ページアイキャッチ

このページでは、C言語における “ポインタ” について解説します。

C言語学習において最も躓きやすいと言われているのが、このポインタです。

ポインタ嫌い…

よく分からない…

私もそうだったよ…

でも今は好きだね!

このページで紹介する「矢印」のイメージを持つことでかなり理解が進んだよ!

私も最初ポインタを学んだ時はポインタが理解出来ず、また使う理由も分からなかったので出来るだけ避けてプログラミングをしていました。

しかし、今ではポインタをすっかり使いこなせるようになってます。むしろポインタ好きです!ポインタなしのC言語プログラミングは考えられないです。

ポインタ学習で躓きやすい理由は下記の3つだと思います。

ポインタ学習につまづきやすい理由
  • ポインタのイメージができていない
  • そもそもポインタを使うメリットが分からないのでポインタを使わない
  • ポインタを身につけるための勉強方法がわからない

このページでは上記の3つを解決できるように、ポインタの概念から説明し、ポインタの使い方・ポインタを使うメリット・ポインタの学習方法について、わかりやすく説明したいと思います。

結構ボリュームありますが、できるだけ分かりやすく、そして図をたくさん使っているのでスラスラ読めるのではないかと思います!

ポインタとは「アドレスを格納する変数」

ではさっそくポインタについて解説していきたいと思います。

ポインタとはズバリ、「アドレスを格納する変数」です。

アドレス…?

いきなり新しい言葉が出てきたね…

じゃあまずはアドレスについて学んでいこう!

アドレスとは

ポインタは変数です。この点でいうと、int 型の変数や char 型の変数と同じです。

実際にポインタ変数に格納されているのも数値ですが、ポインタ変数に格納された数値は “アドレス” として扱われます。

アドレス

そして、アドレスとはメモリ上の位置を示すものです。アドレスと言う名の通りメモリ上の住所みたいなものです。

下の図は一番左上のマスを 0 番地とした例で、青マスは 24 番地にあることを示したものになります。

アドレスの説明図

例えばメモリが 1GB あるのであれば、1024 * 1024 * 1024 個のマスがあり、それぞれにアドレスが割り振られることになります。で、そのアドレスから、メモリ上の位置が特定できるようになっています。

例えばC言語では変数宣言を行うと、プログラム実行時にその変数がメモリ空間のどこかに配置されます。

つまり、その変数に対してアドレスが割り振られます。そして、これによりそのアドレスのメモリをプログラム内で使用することができるようになります。

例えば下記のように変数 A と変数 B の変数宣言を行ったとしましょう。

変数宣言
char A;
int B;

これにより、下の図のように変数 A と変数 B がメモリ上に配置されます(1つのマスに1つの変数が配置されるように図を書いています)。

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

例えば上の図では変数  A  が 12 番地という位置に配置され、変数 B が 16 番地という位置に配置された例を示しています。

この 12 番地、16 番地のようなメモリ空間上の位置をアドレスと呼びます(アドレスは 16 進数で表現することが多いですが、このページではアドレスは 10 進数で表現して説明していきます)。

基本的にプログラムで扱うデータ(変数など)は全てメモリ上に配置されていると考えて良いです。なので、プログラムで扱うデータには全てアドレスが割り振られています。

アドレス観点から考えた時の変数への処理

「変数がメモリ上に配置される」と考えると、変数への処理は下記のように捉えることもできます。

  • 変数への値の代入:その変数が配置されたアドレスに値を格納する
  • 変数からの値の取得:その変数が配置されたアドレスから値を取得する

例えば、下記のように変数宣言を行なった変数に値の代入を行ったとします。

変数への値の代入
char A;
A= 'K'

これにより、下の図のようにメモリに値が格納されることになります。

メモリに値を格納する様子

こんな感じで、変数への処理は、その変数が配置されたアドレスのメモリへの処理と捉えることができます。

まさにこの捉え方でプログラミングを行うのがポインタを用いたプログラムになります。

配置される位置

では変数はどこに配置されるのでしょうか?

厳密にいうとすごく難しいです…。なので簡単に説明させていただきます。

ポインタを理解する上では、プログラム実行時に OS から割り当てられた「自分のプログラム用のメモリ」のどこかに配置されると考えて良いです。

OS はプログラム(アプリなど)が実行されると、PC 上のメモリの中からそのプログラム用に必要な分のメモリを割り当ててくれます。

プログラム用のメモリの割り当て

その割り当てられたメモリの中にプログラムが自動的に変数を配置してくれます。

変数の配置

で、ここでポイントになるのが、プログラムは基本的に「自分のプログラム用のメモリ」以外はアクセスしてはダメという点です(アクセスとは値を格納したり取得したりすることです)。

変数は「自分のプログラム用のメモリ」に配置されるんだよね?

であれば「自分のプログラム用のメモリ」にアクセスするようなことなんてありえないんじゃ…?

変数以外に値を代入したりすることできないじゃん…

ポインタを使うとあり得るんだよ…

まあこの辺りは後述で解説していくよ

「自分のプログラム用のメモリ」以外のメモリは他のプログラムやアプリ、OS が使用している可能性があるので、そのメモリの中身を変更してしまうとそれらのプログラムが動作しなくなる可能性があります。

なので、基本的に「自分のプログラム用のメモリ」以外のメモリにプログラムがアクセスしようとすると「メモリアクセス違反」が発生してそのアクセスが止められます(Segmentation Fault などのエラーが出る)。

メモリアクセス違反すると PC 壊れちゃう…?

いや、アクセスする前にプログラムが止められるから壊れるようなことはないよ!

気軽に失敗してオーケー!

アドレスの取得

C言語では、この自動的に配置された変数の位置(アドレス)を取得する方法が用意されています。

C言語では「変数名の前に & 記号をつける」ことで、変数が配置されている具体的な位置、つまり変数のアドレスを取得することができます。 & はアドレス演算子と呼びます。

例えば下記を実行すれば、変数 x のアドレスを表示することができます。

アドレスの取得
int x;
printf("x's address : %p\n", &x);

printf でアドレスを表示する際には、書式指定子には %p を用います(16 進数でアドレスが表示されます)。

なるほど…

これで変数のアドレスが分かるんだね!

でも、アドレスがなんか役に立つの?

ポインタを使えば役に立つよ!

その辺りをここから解説していくから焦らずに…!

ここまでのまとめ

ここまでアドレスについて解説してきましたが、本題のポインタの説明に移る前にここまでのまとめをしておきたいと思います。

  • アドレスは「メモリ上の位置」を示す値
  • プログラムを実行すると変数はメモリ上に配置され、アドレスが割り振られる
  • 配置される場所は「自分のプログラム用のメモリ」の中
  • 「自分のプログラム用のメモリ」以外の場所にアクセスするとダメ
  • 変数のアドレスはアドレス演算子 & により取得できる

スポンサーリンク

ポインタとは

そして、この変数(など)が配置されているアドレス(位置)を覚えておくのがポインタです。

このメモリ上に変数用のメモリが配置される様子やポインタの動きをプログラムの流れと一緒に見ていきましょう!

ポインタの変数宣言

まず、通常の型の変数とポインタの変数の宣言を行ってみましょう。ポインタも変数ですので、利用する場合は他の変数同様に変数宣言が必要です。

後述でも解説しますが、変数名の前に * をつけて変数宣言を行うと、その変数はプログラム内でポインタとして扱われます。

変数宣言
char x;
char y;
char *ptr;

前述の通り、変数宣言すると、その変数がメモリ上に配置されます。

なので、上記により変数 x と変数 y がメモリ空間上に配置されることになります。

さらにポインタ ptr の変数宣言でも同様に、ptr がメモリ空間上に配置されます。

つまり、変数宣言後のメモリ空間の様子は下の図のようになります(x12 番地、y16 番地、ptr44 番地に配置されたものとしています)。

xとyとptrがメモリ上に配置される様子

ポインタへの値(アドレス)の格納

続いて、変数 x と変数 y に値を格納してみましょう。

char変数への値の格納
x = 'A';
y = 'B';

メモリ空間の図で考えると、下図のように xy 用のメモリに値が格納されることになります。

xとyに値を代入した様子

続いてポインタに値を格納していきましょう!

ポインタとは前述の通り「アドレスを格納する変数」です。今回は ptrx のアドレスを格納してみましょう。

前述の通り、変数名の前に & 記号をつけることで、その変数のアドレスを取得することが可能です。 

つまり、ptrx のアドレスを格納する式は下記のように記述します。

ポインタ変数への値の格納
ptr = &x;

これにより、下の図のように、ptrx の “アドレス” が格納されることになります。

ptrに値を代入した様子

続いて ptr に格納されているアドレスを表示してみましょう!

int 型の変数などと同様に、printf 関数でポインタの値を表示することができます。

ただし、ptr にはアドレスが格納されているので、書式指定子にはアドレスを表示する際に使用する %p を用います。

アドレスの表示
printf("%p\n", ptr);

ここまでのメモリ空間上の図では x のアドレスは 12 としていますので、この printf での表示結果は 0x0C となります。

実際に上記の処理を私の PC で実行した場合は、表示結果が下記のようになりました。

0x7ffee11f2aaf

うわ…

なんじゃこの数字…

アドレスのデータへのアクセス

さらに、ポインタ変数の前に * を付けることで、このポインタに格納されたアドレスのデータにアクセスすることができます。* は間接演算子と呼ばれます。

これにより、そのアドレスの値を取得したり、そのアドレスに値を格納したりすることが可能です。

例えば下記のように値を参照するだけでなく、

ptrのアドレスに格納された値を参照する
printf("%d\n", *ptr);

下記のようにポインタの指す先を直接変更することも可能です。

ptrのアドレスに値を格納する
*ptr = 'K';

ptr には x のアドレス(12)が格納されているので、上記により 12 番地に 'K' が格納される事になります。なので、上記実行後に x の値を printf すれば、K が表示されることになります。

このように、ポインタではアドレスを格納するだけでなく、* を用いることで、そのアドレスに格納されている値にアクセスすることが可能です。

アドレスだとか 12 番地だとか頭が混乱してきた…

ポインタやめたい…

特にポインタ習いたてだと難しいよね…

でも、このアドレスなんかの数値は気にする必要ないよ!

ポインタはもっと直感的にイメージすることの方が重要なんだ!

その方がわかりやすいし、楽しくプログラミングできるよ

ポインタのイメージは矢印

ここまではあえて、アドレスの値などを用いて説明してきました。

ですが!

はっきり言ってこのアドレスの値自体は全く意識する必要はありません!

特にプログラミング入門者の方は意識することは不要です(ハードを制御するようなデバイスドライバ等を作成する場合に意識する必要が出てきます)。

ポインタで重要なのは「何を(どこを)指しているか」です。アドレスの値そのものではありません。

ポインタには前述の通りメモリ空間のアドレスが格納されていますが、アドレスが格納されているということは、そのポインタがそのアドレスのデータ(変数など)を指しているということになります。

例えば先ほどのプログラムの例では ptr は変数 x を指していることになります。

ですので、先ほど表示したようなアドレスの値を意識するよりも、ポインタが何を指しているかを考えた方が良いです。

で、これを考えるときのコツは、ポインタをメモリ空間上の矢印でイメージすることです。

ポインタにはアドレスが格納され、メモリ空間上のどこかを指しているわけですから、ポインタからどこかに矢印が伸びてるイメージを描いてみましょう!

例えば先ほどのプログラムではポインタ ptr には変数 x のアドレスが格納される、つまり ptr が変数 x を指しているということですので下のように図を書くことができます。

ptrが変数xを指す様子

要はポインタとはこの矢印です

わけのわからないアドレスの値で考えるよりも、この矢印でイメージする方が直感的にポインタの動きを確認することができるようになります。

今度はポインタ ptr に y のアドレスを格納することを考えてみましょう。

ptrに変数yを指させる
ptr = &y;

これにより ptr から伸びる矢印が変数 y を指すようになります。

ptrのアドレスを変更した様子

つまり、ポインタに値を代入するということは「ポインタから伸びる矢印の先が変わるということ」となります。

さらに、ポインタ変数の前に * を付けることで、このポインタの指す先のメモリにアクセスすることができることを先ほど説明しました。

つまりこれは矢印で考えると、矢印のの値(データ)にアクセスすることを意味しています。

ptrの指す先へのアクセスを行う様子

そして、前述した通り、このアクセスしたデータに対して表示や代入を行うことができます。

ここまでをまとめると、イメージ的には下記のようにポインタを捉えることができます。

  • ポインタ変数:矢印そのもの
  • *ポインタ変数:矢印の先のデータ

ポインタを矢印で表した時の捉え方

なので、ポインタ変数 に値(アドレス)を格納することで矢印がどこを指すかが変わります。

ポインタへの値の代入

さらに *ポインタ変数 に値を格納することで、その矢印の先のデータを変更することができます。

*ポインタへの値の代入

ん?

ちょっと面白くなってきた!

お絵描きみたいじゃん!しかもアドレスの数値とか考えるよりも分かりやすい!

そう!

ポインタはイメージ的には矢印のお絵描きだよ!

難しく考える必要はないし、この矢印のイメージをする方が理解もしやすいよ!

基本的にポインタを使うときは、この「矢印」のイメージを持ってプログラミングする方が分かりやすいと思います。

特にポインタの使い方が複雑になればなるほど、この「矢印」のイメージで考える方が私の経験的にも分かりやすいです。

ちなみに私がポインタをしっかり理解できるようになったのはこの「矢印」のイメージで考えるようになってからです!今でもプログラムがうまく動かないときはこの矢印をお絵描きしながらデバッグしています。

ポインタの基本的な使い方

ここからは、この「矢印」のイメージが定着するように、ポインタの基本的な使い方を「矢印」の図を用いながら解説していきたいと思います。

スポンサーリンク

ポインタの変数宣言

ポインタも変数なので、使用するためには変数宣言が必要です。まずは変数宣言の仕方について解説していきます。

ポインタの変数宣言の仕方

ポインタ変数は、変数名の前に * を付けて変数宣言します。

ポインタの変数宣言の仕方
int *ptr1;
char* ptr2;

* は変数名側に寄せて付けても良いですし、型名側に寄せてつけてもコンパイルは通ります(ただしコーディング規約等で企業内で定められているようなこともあります)。

この変数宣言時に使用する * は、前述した間接演算子とは意味合いが異なることに注意です。

つまり、変数宣言時に使用する * と変数利用時に使用する * とは異なるものになります(同じ * なので分かりにくい…)。

ポインタの型

ポインタにおいても型を指定して変数宣言を行う必要があります。

この型によって、そのポインタが「どの型の変数を指すか」が変わります。

具体的には、変数宣言時に指定した型と同じ型の変数を指すポインタとして扱われます。例えば上記の例で考えると、ptr1int * として変数宣言しているので int 型の変数、ptr2char * と指定しているので char 型の変数を指すポインタとして扱われます。

ただし、実は異なる型の変数を指させることも可能です。この辺りは下のページでポインタの型による違いに絡めて解説していますので、興味がある方はこのページを読み終わった後にでも読んでみてください。

ポインタの型の解説ページアイキャッチ【C言語】ポインタの「型」について解説

ポインタ変数のサイズ

ポインタ変数も int 型や char 型などの基本型の変数同様に、変数宣言を行う際に、その変数用のメモリが確保され、メモリ空間上に配置されます。

int 型や char 型などの基本型の変数の場合、型に応じてサイズが異なりますが、ポインタの場合はどの型で宣言しても同じサイズになります。

ただしコンパイルする環境によってサイズが異なります。32 bit PC の場合は 4 バイト、64 bit PC の場合は 8 バイトのサイズになるはずです。

変数宣言直後のポインタ

ポインタも、変数宣言により、他の変数同様にメモリ上に配置されます。

ポインタがメモリに配置される様子

ただし、変数宣言直後だと、ポインタには不定値(何かわからない値)が格納されており、どこを指しているかわからない状態になります。

ポインタに不定値が格納されている様子

つまり、この状態だとポインタが「自分のプログラム用のメモリ」を指しているとは限りません。

普通の変数も宣言直後には不定値が格納されているのと一緒かな?

それと一緒!

だけど、ポインタの場合はその不定値が「アドレスとして扱われる」こと、つまり「ポインタの指す先がどこか分からない」ことになるってだけだね!

この状態でポインタ変数に * を付けてデータにアクセスすると他のプログラム用のメモリにアクセスしてしまう可能性もあるので、まずはポインタの指す先を変更してからアクセスする必要があります。

ポインタの指す先を変更する(アドレスを格納する)

で、その「ポインタの指す先を変更する」方法を次に解説していきます。

ポインタにはアドレスを格納することが可能で、この時のイメージは前述した通り「矢印」です!

ポインタにアドレスを格納するということは、ポインタが指す先を変更することになります。

変数を指す

前述の通り、変数のアドレスは、変数名の前に & を付加することで取得することができます。

ですので、ポインタに変数のアドレス格納するためには下記のように記述します。

変数を指す
char x = 'A';
char *ptr;
ptr = &x;

これにより、ポインタ ptr が変数 x を指すことになります。イメージ的には ptr から伸びる矢印の先が変数 x に変わることになります。

ポインタが変数xを指す様子

他のポインタと同じアドレスを指す

他のポインタと同じアドレスを指させることも可能です。

これを行うためには、ポインタに他のポインタの値(アドレス)を代入すれば良いです。

ただし、ポインタにはすでにアドレスが格納されていますので代入時に & は不要です。

例えば下記は ptr2ptr1 と同じ場所(アドレス)を指させる例になります。

他のポインタと同じところを指す
char x = 100;
char *ptr1;
char *ptr2;
ptr1 = &x;
ptr2 = ptr1;

これにより下の図のように2つの矢印が同じ場所を指すことになります。

他のポインタと同じところを指す様子

配列の先頭を指す

またポインタは配列変数を指すことも可能です。

まずはこの配列の「先頭」をポインタで指す方法について解説します。

ポインタで配列の先頭を指す方法は2つあります。

1つ目はポインタに配列名を代入することです。

配列の先頭を指す1
char arr[5];
char *ptr;
ptr = arr;

C言語の場合、配列名は配列の先頭アドレスを(便宜的に)表すようになっていますので、上記の記述でポインタに配列の先頭アドレスを指させることが可能です。

ポインタに配列の先頭を指させる様子

2つ目はポインタに配列の先頭のアドレスを代入することです。

配列の先頭とは、要は 配列名[0] のことです。前述の変数のアドレス代入時と同様に & でアドレスを取得してからポインタに代入します。

配列の先頭を指す2
char arr[5];
char *ptr;
ptr = &(arr[0]);

上記により arr[0]、つまり配列 arr の先頭をポインタに指させることができます。

ポインタに配列の先頭を指させる様子2

2つの方法で書き方は異なりますが、両方とも配列の先頭をポインタが指すことになります。

配列の途中の要素を指す

配列の場合、配列の途中の要素をポインタに指させることも可能です。

配列の途中の要素を指す
char arr[5];
char *ptr;
ptr = &(arr[3]);

この記述の場合は下の図のように arr 配列の第 3 要素(arr[3])をポインタが指すことになります。

ポインタに配列の途中を指させる様子

要は指したい要素のアドレスをポインタに格納すれば良いだけです。

ポインタに加減算を行う

また、ポインタには加減算を行うことができます。

この加算や減算を行うことで、ポインタに格納されているアドレスを加減算することができます。

ポインタへの加減算
char x;
char *ptr;
ptr = &x;
ptr = ptr + 1;
ptr = ptr - 2;

イメージ的には、+1 すると、ポインタの指す先が1変数分(配列の場合は1要素分)正の方向に進みます。

ポインタに加算を行う様子

逆に -1 すると、ポインタの指す先が1変数分(配列の場合は1要素分)負の方向に進みます。

ポインタに減算を行う様子

アドレスが単純に +1-1 されるわけではないところに注意が必要です。実際にアドレスに加算される値はポインタの型によって異なります。

この辺りは下記ページで詳しく解説していますので、このページの後にでも読んでみていただけると幸いです。

ポインタの型の解説ページアイキャッチ【C言語】ポインタの「型」について解説

また、加減算後のポインタにアクセスする時は注意が必要です。

前述の通り、基本的にプログラムでアクセスできるのは自分のプログラム用のメモリのみです。

ですので、自分のプログラム用のメモリ以外(この例だと変数 x 以外)を指した状態でアクセスするとメモリアクセス違反が発生する可能性があります。

変数以外にアクセスする様子

変数は「自分のプログラム用のメモリ」に配置されるのでアクセスするのは問題ないですが、変数以外の位置にアクセスすると、そこが「自分のプログラム用のメモリ」であるかどうかは保証されません。

ダメじゃん…

ポインタへの加減算の使い道ないじゃん…

あるよ!

要は加減算した後のアドレスが自分のプログラム用のメモリならアクセスできるわけだ

例えば配列なんかだと結構有効だよ!

「加減算後のポインタの指す先にアクセスする」ような使い方は、連続して確保されたメモリをポインタに指させている時に用います。例えば配列をポインタが指していると時とか。

この辺りも踏まえて、次はポインタの指す先へのアクセスについて解説していきたいと思います。

ポインタの指すメモリにアクセスする

続いてポインタの指すアドレスのメモリのデータにアクセスする方法について解説していきます。

アクセスとは、要は、メモリ上のデータを取得したり値を格納したりすることです。

間接演算子を用いた変数へのアクセス

前述の通り、ポインタの指す先のメモリに格納されている値にアクセスするためには変数名の前に間接演算子を付けます。間接演算子とは * です。

ポインタの指す先へのアクセス
char x = 'A';
char *ptr;
ptr = &x;
*ptr = 'K';

上記では ptr に変数 x を指させ、その後 ptr の指す先に 'K' を格納しています。

間接演算子によるアクセス

インデックスを指定してアクセス

間接演算子 * を使わずにアクセスすることも可能です。

ポインタが配列を指している場合は、下記のように配列と同じようにインデックスを指定して各要素の値にアクセスすることが可能です。

要素を指定してアクセス
char arr[5] = {'A', 'B', 'C', 'D', 'E'};
char *ptr;

ptr = arr;
ptr[0] = 'H';
ptr[3] = 'K';

ポインタでは「ポインタが指す先を配列の先頭」とみなして、配列と同様にインデックスを指定してデータにアクセスすることが可能です。

上記では ptrarr の先頭を指しているため、ptr[0] では arr[0] を、ptr[3] では arr[3] にアクセスすることができます。

ポインタでインデックスを指定してデータにアクセスする例1

ですので、上記を実行すると、arr[0] には 'H' が、arr[3] には 'K' が格納されることになります。

では、次の例ではどうなるでしょう?

要素を指定してアクセス2
char arr[5] = {'A', 'B', 'C', 'D', 'E'};
char *ptr;

ptr = arr[1];
ptr[0] = 'H';
ptr[3] = 'K';

今度は ptrarr[1] を指しているため、arr[1] を配列の先頭とみなしてデータにアクセスすることになります。つまり、ptr[0] にアクセスすれば arr[1] にアクセスすることになります。

ポインタでインデックスを指定してデータにアクセスする例2

配列の途中からを部分的な配列の先頭とみなして使用したい場合などに、ポインタを使うと有効です。

アドレスを加減算で変化させてアクセス

またアドレスそのものを加減算で変化させることで、配列の各要素にアクセスすることも可能です。

アドレスを変化させてアクセス
char arr[5] = {'A', 'B', 'C', 'D', 'E'};
char *ptr;

ptr = arr;
*(ptr + 3) = 'K';

例えば上記では、ptr は配列 arr の先頭を指します。

ポインタが配列の先頭を指す様子

そして ptr + 3 することで、矢印が ptr から3要素分正方向に移動した位置を指すことになります。

ポインタに加減算した様子

さらに、間接演算子でその矢印の指す先に 'K' を格納しています。

加減算後のポインタにアクセスする様子

つまり、これにより arr[3]'K' が格納されたことになります。

自分のプログラム用のメモリ以外にアクセスしないように注意

ここまでいくつかポインタからのメモリのアクセス方法について解説しましたが、特に注意していただきたいのが「自分のプログラム用のメモリ」以外にはアクセスしないことです。

前述の通り、これをやってしまうと他のプログラムのメモリを壊してしまうことがあります(壊す前にメモリアクセス違反でプログラムがエラー終了する)。

例えば、ここまで解説してきた「インデックスを指定してアクセス」と「アドレスを変化させてアクセス」は配列 arr 用のメモリがメモリ空間上で連続しているために可能な操作になります。

配列 arr の先頭アドレスを 12 番地とすると、arr[0] のアドレスは 12arr[1]13arr[2]14・・・という風に、配列の各要素はメモリ上に連続して存在することになります。

配列が連続メモリ領域に配置される様子

ですので、配列 arr の先頭を指すポインタ ptr  で考えると、ptr 〜 ptr+4 は必ず自分のプログラム用のメモリ(配列 arr)を指すことになるのでアクセスが可能です(前述の通り、変数は必ず自分のプログラム用のメモリ上に配置されます)。

一方で ptr+5 の場合は配列 arr の外側を指すことになるため、そのメモリが自分のプログラム用のメモリとは限りません。なので、アクセスするとメモリアクセス違反が発生する可能性があるので注意が必要です。

例えば配列ではなく単なる変数においても、変数の配置位置の隣が自分のプログラム用のメモリとは限りません。

ですので、変数のアドレス + 1 したメモリにアクセスすると、メモリアクセス違反が発生する可能性があります。

変数の隣にアクセスしてメモリアクセス違反が発生する様子

必ずメモリアクセス違反になるの?

ならない場合もあるよ

たまたま他の変数のメモリにアクセスすることもあるからね

必ずメモリアクセス違反になってくれた方が、プログラムがおかしいことが分かって嬉しいんだけどね…

逆に、メモリが連続しているのであれば配列以外でも「インデックスを指定してアクセス」と「アドレスを変化させてアクセス」の操作を行うことが可能です。

例えば malloc 関数などを使えば「自分のプログラム用の連続したメモリ」を確保することができます。

ですので、malloc 関数で確保したメモリは実際は配列ではないのですが、配列と同様に「インデックスを指定してアクセス」と「アドレスを変化させてアクセス」によるアクセスを行うことができます。

MEMO

ちなみに、自分のプログラム用のメモリ以外をポインタで “指すだけ” であればメモリアクセス違反は発生しません

ただし、アクセスした時にメモリアクセス違反が発生します

[/codebox]

スポンサーリンク

変数以外のデータを指す

ここまではポインタが変数を指す例のみを紹介してきましたが、ポインタが指すことの出来るのは変数のみではありません。

アドレスを直で指定してポインタに指させる

ここまでアドレスはアドレス演算子 & を用いて変数から取得してきました。

が、実はアドレスを直で指定してポインタに指させるようなことも可能です。

例えば下記はポインタ ptr100 番地を指させる例になります。

アドレスを直で指定
#include <stdio.h>
  
int main(void) {
    char *ptr;

    ptr = (char*)100;

    *ptr = 'K';

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

    return 0;
}

ただし、100 番地は「自分のプログラム用のメモリ」ではないため、実行するとメモリアクセス違反(Segmentation Fault)が発生してエラー終了します。

 

ダメじゃん

使いどころないじゃん…

それを実感してもらうための例だよ!

このアドレスを直で指定する使い方の問題点は、ポインタに「自分のプログラム用のメモリ」を指させることができないところです。

たまたま偶然、自分のプログラム用のメモリを指させることになって上手く動くことはあるかもしれませんが、まずあり得ないでしょう。

なので、ポインタにアドレスを直で指定することもできるのですが、基本的にやらないようにしましょう(実際にメモリアクセス違反が発生するかなどを試してみるのは良いと思います)。

で、こんな感じでメモリアクセス違反を発生させないためにも、ちゃんとポインタに「自分のプログラム用のメモリ」を指させる必要があります。

このために、ここまでの例では、変数宣言して自分のプログラム用のメモリに配置された変数からアドレス演算子 & でアドレスを取得し、それをポインタに指させるようにしてきたというわけです。

実はこのアドレスを直で指定する使い方は全く使い道がないというわけではありません

組み込みプログラミングなどでは上記のようにポインタに直でアドレスを指定して利用するようなこともあります!

メモリを動的に確保してポインタに指させる

ここまで何度も言ってきたように、ポインタには「自分のプログラム用のメモリ」を指させることが重要です。

そのために、ここまで変数ばかりをポインタに指させてきましたが、変数をポインタに指させるためには、必ずその変数の変数宣言を行う必要があります。

ですが、実はC言語では変数宣言しなくても「自分のプログラム用のメモリ」を好きなサイズ分、プログラム実行時に取得することができる関数が用意されています。

その関数の1つが malloc 関数です。malloc 関数については下記でかなり詳しく解説していますので、下記ページを参考にしていただければと思います。

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

要は malloc 関数は、プログラム実行時に指定したサイズ分の「自分のプログラム用のメモリ」を追加する関数です。

mallocでメモリを追加する様子

この malloc 関数で追加したメモリは「自分のプログラム用のメモリ」かつ、メモリ上に「連続して配置されたメモリ」となります。

イメージとしては指定したサイズ分の配列を追加するようなもので、配列同様に扱うことが可能です(インデックスを指定してアクセスしたり、アドレスの加減算を行なってアクセスしたり)。

ただし、通常の変数とは異なり、この追加したメモリには変数名のようなものは付けられません。

ですので、変数名を指定してこのメモリにアクセスすることは不可能で、必ずポインタにこのメモリを指させ、ポインタからアクセスする必要があります。

関数を指す

ちょっと上級者向けですが、ポインタではプログラム内の関数を指すことも可能です。

関数を指すポインタを関数ポインタと呼びます。関数ポインタを構造体のメンバに関数を用いることでクラス(のようなもの)をC言語で実現することもできます。

関数ポインタについては下のページで解説していますので、興味のある方はこのページを読み終わった後にでも見てみてください。

関数ポインタの解説ページアイキャッチC言語の関数ポインタについて解説

ポインタを学び始めた方であれば、関数もポインタで指すこと可能であることを頭の片隅に置いておくくらいで良いと思います(プログラムの規模が大きくなったり並列処理プログラミングなどを実装しだすと、この関数ポインタが活躍します)。

NULL を指す

ポインタを扱う上で非常に重要な意味合いを持つものが NULL です。

NULL とは「何もない空の状態」みたいな意味ですが、C言語のポインタにおいては下記の2つの意味で主に使用されます。

  • どこも指していない状態
  • 関数エラー時の戻り値

どこも指していない状態

前述でも触れましたが、変数は変数宣言された直後だと不定値(どんな値か分からない値)が格納されています。

これはポインタにおいても同様です。ポインタはメモリ空間上を指すものですので、変数宣言直後はどこを指しているか分からない状態と考えられます。

この状態でポインタの指す先にアクセスしようとすると、メモリアクセスエラーが発生し、プログラムが落ちて即終了する危険な状態です。

不定アドレスへのアクセス
int *ptr;

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

これを防ぐために NULLを用います。

要は、ポインタが「まだどこも指していない状態の時には明示的に NULL を指させる」ようにします。

これにより、ポインタが NULL を指しているときは「まだどこも指していない状態」であることが判断できるようになります。

この判断を NULL チェックと言ったりします。

この判断を行うようにすることで、ポインタが NULL の場合はポインタの指す先へのアクセスを行わないように制御することができ、メモリアクセスエラーを防ぐことが可能になります。

例えば下記が NULL チェックを行なう例になります。

NULLチェック
int *ptr = NULL;

/* いろんな処理 */

if(ptr != NULL){
    printf("*ptr = %d\n", *ptr);
}

/* いろんな処理 */ の中で ptr がセットされた時だけ *ptr によるポインタの指す先へのアクセスが実行され、ptr がセットされなかった場合(つまりまだどこも指していない場合)はアクセスしないように制御しています。

ポイントは、ポインタに NULL を指させておくようにすることで、まだどこも指していない事を判断できるようになるという点です。

NULL にアクセスすることが安全という意味ではありません(むしろ NULL にアクセスすることは禁止されています)。

関数エラー時の戻り値

戻り値がポインタである関数の場合、エラー時の戻り値を NULL としている関数が非常に多いです。

ですので、このような関数実行時にエラーが発生したかどうかは、実行後に戻り値が NULL であるかどうかを調べることにより確認することが可能です。

エラーチェック
int *ptr;
ptr = (int*)malloc(100 * 1024 * 1024 * 1024);
if(ptr == NULL){
    /* エラー処理 */
}

アドレス NULL にアクセスすることは基本的に禁止されています。

ですので、関数から受け取ったアドレスに何も考えずにそのままアクセスすると、もし関数が NULL を返却している場合は NULL にアクセスすることになってしまいます。

これを防ぐためには、関数から受け取ったアドレスが NULL であるかどうかを判断し、NULL である場合はエラー終了するような処理を行いましょう。

スポンサーリンク

ポインタのメリット

ポインタを使用する主なメリットは下記の3つです。

  • できることが増える
  • 高速化できる
  • 省メモリ化できる

特に重要なのは「高速化できる」と「省メモリ化できる」です。

現在数あるプログラミング言語がある中で、いまだにC言語が使用される理由は高速であることと省メモリであることです。

読みやすい・書きやすいプログラミング言語は他にもいくらでもありますが、この2つの強みがあるので、特に「安く性能の良いものを開発する」ことが重要である組み込み製品ではC言語がいまだに広く利用されています。

そしてこの2つの強みを最大限に活かすために必要なのがポインタの利用です。ポインタを使わなければ、これらの強みを活かすことができません。

C言語で実装する最大の理由は高速で省メモリであることですので、この2つの強みを活かせるポインタの利用は、C言語においては必須であると考えて良いです。

それではこのポインタを用いるメリットと、なぜこのようなメリットがあるのかを1つ1つ見ていきましょう!ここをしっかり理解することが、ポインタ理解への近道です。

できることが増える

ポインタを使うことでC言語で実現できることがグッと増えます。というか、ポインタが使えないとかなり不便です。

具体的には自分で作れる関数の幅が広がります。例えば下記のプログラムについて考えてみましょう。

ポインタを使わない例
#include <stdio.h>
 
void func(char a, char b){
    char tmp;
    tmp = a;
    a = b;
    b = tmp;
}

int main(void){
    char x;
    char y;

    x = 'K';
    y = 'L';

    func(x, y);

    printf("x = %c, y = %c\n", x, y);

    return 0;

}

実行結果は下記となります。

x = K, y = L

func 関数に x と y を引数として渡し、関数内で値を入れ替えたはずなのにそれが反映されてません。

これは、C言語における関数呼び出しでは、引数で指定された変数はその変数そのものではなくその変数を “複製したもの” を関数側に渡すようになっていることが原因です。

つまり、関数内では引数で渡された変数ではなく、複製された変数を用いて処理が行われます。値としては同じものが格納されていますが、これらは全く別の変数となります。

通常の変数が複製される様子

ですので、いくら関数の中で引数のデータの値を変更したところで呼び出し側の変数の値には影響しません。

処理した結果を呼び出し側に渡したいのであれば return で戻り値として渡す必要があります。

ただし、return で渡せるデータは1つなので、関数が呼び出し元に渡せる結果は1つのデータのみとなってしまいます。

これを解決するのがポインタです。次は下記のプログラムについて考えてみましょう。

ポインタを利用した例
#include <stdio.h>
  
void func(char *a, char *b){
    char tmp;
    tmp = *a;
    *a = *b;
    *b = tmp;
}

int main(void){
    char x;
    char y;
    char *px;
    char *py;

    x = 'K';
    y = 'L';

    px = &x;
    py = &y;

    func(px, py);

    printf("x = %c, y = %c\n", x, y);

    return 0;

}

こちらの結果は下記の通りです。

x = L, y = K

しっかり関数実行により値が入れ替わっていますね!

前述の通り関数は実行時に引数として渡されたデータの複製が作成されます。

これは引数がポインタの時でも全く同じです。ただしポインタの場合はポインタに格納されたアドレスが複製されることになります。

したがって、変数としては全く別のものでも、関数呼び出しにより複製されたポインタも複製元のポインタが指している場所を同様に指すことになります。

具体的には、上のプログラムで言うと func 関数の引数 a と b は元々 pxpyが指していた xy をそれぞれ指すことになります。

ポインタ変数が複製される様子

したがって func 関数内でも *a で x の値に、*b で y の値にアクセスすることができ、その値を変更することが可能です。

上記プログラムでは func 関数内で *a と *b に値を代入することで x と y の2つの値を変更しています。

ポイントは、ポインタを使わない場合は関数で呼び出し元に渡すことができる処理結果は1つのみだったのが、ポインタを利用することで、関数の呼び出し元に関数内での処理結果を複数渡すことができるようになったところです。

つまり、ポインタを使用することで自分が作成できる関数の幅が広がります。

 

わざわざ関数作らなければいいんじゃないの…?

そしたらポインタいらないじゃん…

 

いや関数化した方がソースコードが読みやすくなったり簡潔に書けたりすることもあるからね!

必要に応じて関数化した方がいいよ!

で、その関数化するときに、必要であればポインタを使おうよ!っていう話だね

ソースコードの規模が大きくなれば大きくなるほど、ソースコードの読みやすさは重要になっていきます。

特に製品向けの大規模なC言語プログラムを作成するような場合は、必要に応じて関数を作成すること、そして必要に応じてポインタを利用することを心がけた方が良いです。

高速化ができる

ポインタを使用することでプログラムの高速化を行うことが可能です。

下記は funcAfuncB をそれぞれ10億回繰り返し呼び出した時の処理時間を表示するプログラムです。funcAfuncB の違いは引数がポインタであるかどうかのみです。

funcAfuncB は両方とも何も処理をしない関数なので単純に呼び出し時間を表示することになります。

引数の違いによる処理時間の違い
#include <stdio.h>
#include <time.h>

#define N 1000000000

struct data {
    double d1;
    double d2;
    double d3;
    double d4;
    double d5;
    double d6;
    double d7;
    double d8;
    double d9;
    double d10;
};

void funcA(struct data d){
}

void funcB(struct data *d){
}

int main(void){
    long long i;
    struct data d;
    clock_t start, end;

    start = clock();
    for(i = 0; i < N; i++){
        funcA(d);
    }
    end = clock();
    printf("funcA:%.3f[sec]\n",
        ((double)end - (double)start) / CLOCKS_PER_SEC);

    start = clock();
    for(i = 0; i < N; i++){
        funcB(&d);
    }
    end = clock();
    printf("funcB:%.3f[sec]\n",
        ((double)end - (double)start) / CLOCKS_PER_SEC);

    return 0;

}

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

funcA:11.689[sec]
funcB:2.678[sec]

両方とも空の関数を呼び出しているだけですが、処理時間に大きな差が出ています。

この差はできることが増えるで前述した関数実行時のデータの複製により発生しています。

funcA では引数が data 構造体そのものなので data 構造体がまるまる複製されます。一方で、funcB では引数がポインタですので、ポインタのみが複製されます。

つまり複製するデータのサイズは funcAfuncB で下記のように異なります。

  • funcA80 バイト(double 型のサイズ * 10
  • funcB8 バイト(ポインタのサイズ [4 バイトの場合もあり])

サイズが大きいほど複製にも時間がかかるため、構造体そのものをまるまる複製する funcA の方が時間がかかっています。

値渡しとポインタ渡しのコピーサイズの違い

今回は関数呼び出しに注目しましたが、関数呼び出し時以外でも大きなデータを扱う時はコピーが発生すると時間がかかってしまいます。

例えば、大きなサイズの画像データなどは画像データそのものをコピーすると処理時間が長くなってしまいます。

しかしそれをポインタで指す先を設定するだけで処理できるのであれば一瞬で処理を完了させることが可能です。

画像データのコピーとポインタコピーとの違い

このようにポインタを使えばデータのコピー時間を短縮することができ、これによりプログラムの高速化を行うことが可能です。

スポンサーリンク

省メモリ化ができる

先ほど解説した高速化できるでも解説したようにデータそのものをコピーすると、元々あったコピー元のデータのメモリ+コピー先のデータのメモリが必要になりますので、元々のデータに対して2倍のサイズのメモリが必要になってしまいます。

ポインタを扱えばコピー自体を減らす、コピーするデータのサイズを減らすといったことが可能でありプログラム実行に必要なメモリを削減することが可能です。

画像データのコピーとポインタコピーとのメモリ使用量の違い

また malloc 関数によるなどによる動的なメモリ確保を行うことで、必要になった時に必要な分だけメモリを確保するような動きが実現可能になります。

ポインタの勉強方法

おそらく、ここまで解説してきた内容を理解していただけたのであれば、ポインタの入門としては完璧だと思います!しっかりポインタのイメージを掴んでいただけたのではないかと思います。

次に必要なのはポインタを使ってプログラミングしてポインタに慣れることだと思います。

そのためにオススメの題材をここで紹介していきたいと思います。

リスト構造をプログラミングしてみる

ポインタをしっかり使いこなすための勉強方法として一番オススメなのが「リスト構造のプログラミング」です。

リスト構造はデータをリスト上に繋ぎ合わせて管理するデータ構造です。この繋ぎ合わせはまさに「ポインタで他のデータを指す」操作になります。

ですので、ポインタを矢印としてイメージしやすいです。そのためリスト構造はポインタを身につけるにはうってつけの題材だと思います。

リスト構造の各要素をポインタで繋ぐ様子

またリスト構造では、データが追加された時はメモリの動的確保を、データが削除された時はメモリの解放とデータの繋ぎ合わせをそれぞれ行う必要があるため、ポインタを使用する箇所が大変多いです。

使用箇所が多い分、ポインタに慣れることができると思います。

必要なデータを動的確保で追加する様子

リスト構造については下記ページで解説していますので、ポインタを身に付けたいと思っている方は是非読んでみてください。

リスト構造の解説ページアイキャッチ【C言語】リスト構造について分かりやすく解説【図解】

スポンサーリンク

ポインタのポインタ(ダブルポインタ)を使ってみる

また、ポインタの矢印のイメージをもっと身につけたい場合はポインタのポインタ(ダブルポインタ)に挑戦してみるのもオススメです!

ポインタのポインタはその名の通り、ポインタを指すポインタです。

実際に使ってみると、ポインタとポインタを矢印でつなぐイメージをより根強く持つことできると思います。

ポインタのポインタについては下記ページで解説していますので、是非こちらを参考にしてみてください。矢印いっぱい書いて説明しているので、矢印のイメージもつきやすいと思います。

ポインタのポインタ解説ページアイキャッチ【C言語】ポインタのポインタ(ダブルポインタ)を解説【図解】

ポインタの型について理解する

ポインタに慣れてきた、ポインタのイメージがしっかりついてきたという型には「ポインタの型」についても是非理解していただきたいです。

このページでは「ポインタの型」についてはほぼ触れていませんが、実はポインタの使いこなしにはこのポインタの型をしっかり理解しておく必要があります。

また、ポインタの型を使いこなすことで、より幅広いC言語プログラミングも行うことができます。

ポインタの型については下記ページで詳しく説明していますので、是非こちらも読んでみてください!

ポインタの型の解説ページアイキャッチ【C言語】ポインタの「型」について解説

まとめ

このページではC言語のポインタについて解説しました!

ポインタは難しいと思われがちですが、ポインタを矢印としてイメージすれば、より簡単に、そしてより分かりやすく学習することができます。

なので、とにかくポインタのプログラミングで詰まった時は、この矢印を図で書いて動作を確認することをお勧めします!

 

  • ポインタは概念さえ理解すれば難しくない
  • 文章の説明よりも図の方がイメージしやすい
  • ポインタを使うことで作成できる関数の幅が広がり、省メモリ・高速なプログラムが作成可能
  • ポインタを身につけるにはリスト構造が最適

6 COMMENTS

Kちゃん

いつもわかりやすい解説をありがとうございます。
解説の中で数字の配列をchar型で宣言しているのが不思議でした。
これを機にchar型もint型と実質は同じということを改めて理解できたので結果オーライでしたが、初心者の人が見たときに、違和感を感じるかもしれないです。

下記の部分です。
char arr[5] = {1, 2, 3, 4, 5};
char *ptr;
int i;

返信する
daeu

Kちゃんさん

コメントありがとうございます!

一応補足だけしておくと、char と int はデータのサイズが違うので、表現できる値が異なります。

■char

サイズ:1バイト
表現できる値:-128 〜 128

で、この中の一部の値が文字に対応しており、
「文字として表示する」ことで、アルファベット等の文字が表示されます。

例えば

a = 100;
printf("%c\n", a); 
printf("%d\n", a);

の場合、a に格納されているのは同じ「100」ですが、
一つ目の printf では「d」が表示され(%c で文字として表示しているため)、
二つ目の printf では「100」が表示されます(%d で数字として表示しているため)。

■int

サイズ:4バイト
表現できる値:-2147483648 〜 2147483647

ご指摘の内容ですが、素直にまさにその通りだと思いました。
char 型なので文字の配列にした方が、ポインタの解説の内容がスッキリ頭に入ってきますよね…。

ちょっと時間がある時に文字の配列に修正しようと思います。
貴重なご意見、大変ありがとうございます。

返信する
Kちゃん

あ!データのサイズについては理解が甘かったです!
ありがとうございます!

返信する
ki

大変分かりやすい解説、ありがとうございます。

一点、NULLチェックの判断条件ですが、
ptrがNULLではなかったら、なので、!=が正しいのではないでしょうか??

int *ptr = NULL;

/* いろんな処理 */

if(ptr == NULL){    //ここ
printf(“*ptr = %d\n”, *ptr);
}

返信する
daeu

ki さん

ページ読んでいただいていありがとうございます!
またコメントありがとうございます!

ki さんのおっしゃる通りです。完全に判断が逆になっていました…。
ですので、早速修正させていただきました。

ご指摘ありがとうございます。全く気づいていなかったので、大変助かりました…。
また、もし間違いがあったり、よくわからない点などありましたら、気軽にコメントいただけると幸いです。

返信する

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です