C言語のポインタ【自分用】
2024年11月からの案件でC言語を使用して開発することになったので、いろいろと参考にして自分なりにポインタについてまとめてみた。
前の現場ではCOBOLという言語を触っていたが、客先から「ポインタとかあるけど聞いたことある?」と質問されたので、ポインタが肝なのかと思い、書籍で学習したところ、案の上、ん?という感じだったので、追加で動画学習し、ようやく理解できた。
1.ポインタとは何か(さくっと)
ポインタは、アドレスを指し示す仕組みで、ショートカット機能的なもの。
使用する理由は以下の3つ。
①別の関数から値を操作するため。
外から値を操作する。複数の戻り値を返す。
②配列にアクセスするため。
配列の先頭要素の場所だけ記憶する。
③データ構造を作るため。
例えば、連結リストの場合、データ本体と次のアドレスが2つで1つになっているため、
アドレスで指し示す「ポインタ」が必要になる。
2.物理的なメモリICの仕組み
日頃、自分たちがプログラムの中で使用している変数や関数、配列、int apple =10; などは、
0と1の電気信号に変換され、メモリの中に収納されている。
コンピュータの中にはメモリIC(集積回路)が何千個もあり、データは全てこの中に保存されている。
int apple = 10; の形や数字が具体的にあるわけではなく、0と1の電気信号・電気のON/OFFで保存されている。
メモリICを拡大すると以下のようなイラストになる。
側面に11本ずつ、合計22本のピンがささっており、それぞれのピンには役割がある。
ポインタに関わるのはA0~A9の10本のピンである、
AはAdressの略で、データの保存に使用されるピンである。
それぞれのピンに対して、0か1でデータを保存していき、A0~A9で1バイトのデータの大きさになる。
1つのピンに対して、2通りなので、2^10=1024通りの場所を保存できることになる。
つまり、000000000~1111111111の中から保存する場所を選べる。
では、int apple = 10;はどうやって保存されるのだろうか?
intは4バイトの大きさをもったデータ型なので、
0000000000~1111111111の中から、
- 0000000001
- 0000000010
- 0000000011
- 0000000100
という4つのアドレスを確保することになる。
3.メモリICの論理的な仕組み
さきほどは物理的な説明だったので、今度は論理的に理解していく。
ちなみに、
物理的:実際の形、物理的に手で触れられる
論理的:人間が考えやすい、扱いやすい(実際の形とは違う)
メモリIC=ビルディングと捉えると分かりやすい。
上図のように、1階~1024階の建物と考えると、1フロアにつき1バイトのデータが入っていることになる。
例えば、int apple = 10; の場合、int型が4バイトなので、4フロア分使用する。
int array = [3]; なら、4バイトのデータ型が3つ分あるので、4×3=12フロア分使用する。
4.数値の代入と出力
さて、ここからが本題。
まず、いかなる場合においても、ポインタにおいては、以下2つが前提条件である。
今後プログラムを書く上で困ったら、必ずここに立ち返る必要がある。
どのように使用するのか実際のプログラムで確認していく。
上記のプログラムの動きは以下の通りである。
4行目でint型の変数appleに10を代入し、5行目でappleの値を出力して確認している。
6行目がポインタ変数の宣言である。変数の前に*をつけることでポインタ変数を宣言できる。
データ型の直後にをつける方法と、変数の直前にをつける方法がある。
上記どちらの書き方でも良いが、複数のポインタ変数を定義する場合に、
int* p, t; だと、pはポインタ変数、tは普通の変数となってしまうため、②の書き方がベター。
7行目では、宣言したポインタ変数pに変数appleを代入している。
代入するときは、ポインタ変数の前提条件①にもあるように、数値は持てないので、
&演算子を使用して、参照先の変数のアドレスを渡している。
8行目では、ポインタ変数の中身を出力したいので、ポインタ変数の前提条件②にもあるように、
*をつけて、pが指す先のモノの中身(appleのアドレスにある数値)にアクセスしている。
👇実行結果
👇良くない書き方
7行目において、 *p = apple;
理由① appleと書いてしまうと、ポインタ変数に数値を代入していることになる。
理由② *pと書いてしまうと、その先のモノ(参照先の数値)を表してしまうので、*pではなく
p(それ自体)に代入して、参照先を示してあげる必要がある。
5.別の関数から値を操作
ポインタ変数を使う理由①について、どのようにプログラムを書くのか説明していく。
1回目でもともと入っている変数の値10を表示させ、計算用の関数の中でその変数に処理を加え、
変数の中身を25に書き換えて、2回目で再表示させる簡単なプログラムを作る。
まず、以下は間違ったプログラム。
👇実行結果
想定だと2回目で25の値が表示されるはずなのに書き換わっていない。
それもそのはず。
C言語はmain関数から別の関数に引数を渡すとき、一時的に変数の中身の値をコピーしているため、値はそのままで書き換わらないのである。
(これを、「値渡し」という。⇔※「参照渡し」)
※参照渡しは、変数への参照を渡して、変数を共有するもの。
これを前提としてプログラムを見ると理解できる。
処理の順番としては以下の通り。
行番号 | 処理内容 | 変数appleの中身 | 仮引数xの中身 |
---|---|---|---|
11 | 関数を呼び出しappleを引数として渡す。 | 10 | ブランク |
3 | 仮引数xに引数を渡す(値渡し) | 10 | 10 |
4 | xに25を代入する | 10 | 25 |
つまり、関数内の変数xが25に書き換わっただけで、もともとの変数appleは何も変わっていないのである。
では、どうすればよいのか。
ここで活躍するのがポインタである。
なぜなら、ポインタは変数が存在する場所を指し示すからである。
以下が正しいプログラムである。
👇実行結果
処理の順に沿って、大事なポイントを確認していく。
行番号 | 処理内容 | 変数appleの中身 | 仮引数x(ポインタ変数)の中身 |
---|---|---|---|
11 | 関数を呼び出しappleのアドレスを引数として渡す。(&演算子使用) | 10 | ブランク |
3 | 仮引数xに引数を渡す(ポインタ渡し) ※ポインタ変数xの宣言(*をつける) | 10 | 変数appleの場所 |
4 | 仮引数xに25を代入する ※中身を書き換えたいので、*をつける | 25 | 25 |
少し混乱しやすいのが、4行目の部分。
*をつけるのかつけないのかで理解に苦しむ人も多いようだ。
11行目でappleのアドレスを引数として、3行目のcalc関数で仮引数x(ポインタ変数)にその値を渡している。
つまり、 int *x = &apple; となっている。
ポインタ変数xには変数appleの場所が入っているため、場所に数値は入れることができない。
ポインタ変数はあくまでもアドレスを入れるための変数であり、数値を入れることができない。
もっと分かりやすく言うと、int *x; で定義しているのは、*x ではなく、 x だ!!!!
これでも分からない。むしろ混乱したわ、ふざけんな時間返せと思いました?
大丈夫です。私もそう思いました。
迷宮入りさせる原因は、こいつです。👇
なぜこうもでかでかと、、表示させる必要があった・・・って?
いや、ほんとにこの*が曲者だったんですわ。
ポインタを学習する上で頻繁に出てくるこの記号→ *
実は、この記号、C言語では3つの意味があり、それぞれ役割が違うんです。
①乗算演算子
掛け算の時に使用。 kakezan = 3*8; のように使用。
②間接参照演算子
ポインタ変数を書き換え可能な状態にする時に使用する記号。
*p =25; のようにして使用。
③ポインタ変数を宣言する時の記号(名前ついてない)
宣言のときにのみ使用される。int* p; または int *p; のように使用。
これで全て解決なんです・・・!!ここまで本当に時間がかかった・・・。
ポインタ変数を宣言する時は当然のように*をつける必要があるので、そこは特に何も考える必要はなく、重要なのは、
代入時の右辺とポインタ変数の前提条件を意識することだ!!!!!
具体的な例を出すなら、代入時、
①以下のようにアドレスを代入するなら、* は不要!
int *p;
p = &apple;
なぜなら、ポインタ変数pはアドレスしか代入できないという前提条件があるので、
&演算子を使用しているappleはアドレスを指しており、理にかなっているからだ。
②以下のように、値を代入するなら、* は必要!
int *p;
*p = 10;
なぜなら、ポインタ変数pはアドレスしか代入できないという前提条件があるにも関わらず、値が代入されてしまっているからだ。
ということは、参照先のものを書き換える必要があるから値を代入しているのであり、この場合、2行目の「*」は間接演算子として使用されている。
6.複数の戻り値を返す
C言語では、1つの戻り値しか返せないため、returnを使用してどうにかすることはできない。
そのため、ポインタで無理やり場所を作る必要がある。
関数に引数を渡して足し算と引き算をした値を返すプログラムを作成する。
以下は間違ったプログラムである。
👇実行結果
今回のポイントは、戻り値が2つ返せるように、足し算の結果と引き算の結果を出力するための変数tasizanとhikizanを14、15行目で定義しているところである。
しかし想定では、足し算の結果は40、引き算の結果は10となるはずだった。
なぜこうなったか、はもう先ほどのプログラムと同様で、値渡しによりtasizanとhikizanの中の値がコピーされただけだからだと分かる。
そこでまたポインタが活躍する。
以下は正しいプログラムである。
👇実行結果
まず13行目のcalc関数の呼び出しの際、足し算結果と引き算結果の引数は&演算子をつけて、アドレスを渡している。
3行目では、足し算結果と引き算結果の仮引数はポインタ変数として宣言しているので*をつけている。
4,5行目では、n1,n2といった数値が入った変数をポインタ変数に入れることになるので、ポインタ変数のルールに則り、*をつけて足し算と引き算の結果を代入している。
7.ポインタと構造体
ポイントと構造体についても例を挙げて説明していく。
以下は、構造体で定義された変数内の値を書き換えて表示するプログラムである。
👇実行結果
3~7行目が構造体で、10行目で構造体を使用して値を代入している。
ここで入れた値を書き換えたいのでポインタ変数を使用する。
11行目がポインタ変数の宣言なので、*を使用している。
ここで注意することは、構造体の変数と型をそろえる必要がある点。
構造体の中の変数のデータ型はバラバラなので、型をそろえないとデータにアクセスできないことがあるからだ。
12行目では変数appleのアドレスを&演算子を使用して代入している。アドレスなので*は不要。
13行目~15行目でポインタ変数が指し示す先の変数内の値をそれぞれ出力している。
このとき、指し示す先の中身の値を表示したいので、*は必要になる。
ただ、「.」が優先されてしまうので、( )を使用して記す必要がある。
しかし、毎回( )をつけるのは非常に面倒なので、アロー演算子を使用した書き方のほうがベター。
以下のように使用する。
-> がアロー演算子である。矢印なのでポインタ変数に入っているアドレス先にアクセスしていることが視覚的に分かりやすい。
この場合、*は不要である。
とりあえずこんな感じで。また編集するかも。
参考資料
1.『苦しんで覚えるC言語』
2.ゼロからわかる!ポインタ完全入門【C言語でポインタを完全マスター】
3.【C言語】ポインタがわかりません。教えてください。【プログラミング】