【C言語】ポインタを理解しよう!わかりやすくメリットを解説します!
C言語を学ぶ上で最初につまづきやすいランキング上位である『ポインタ』
私の周りのC言語を学んでいる人たちは「難しい」「分からない」と言っている人が多かったように感じます。
今回はC言語を始めたての方に向ける記事で、C言語におけるポインタという概念やメリットなどをわかりすく、C言語のサンプルコードを用いて解説していきます。
C言語のポインタを理解しよう!
ポインタ (pointer) とは、あるオブジェクトがなんらかの論理的位置情報でアクセスできるとき、それを参照する(指し示す)ものです。
簡単に言えば、何かを指し示すものというイメージです。
パソコンのディスプレイ、もしくはスマホの画面を指さしてみてください。
その人差し指がポインタということになります。
イメージはそんな感じです。
今回はC言語の「特定のメモリ領域を表現する」ポインタを軸に話を進めていきます。
C言語のポインタ変数の基礎
ポインタはC言語の特徴的な機能のひとつです。
ここでは、どのような機能なのかということと使い方をご紹介します。
C言語のポインタにかかわる記号
C言語において、&(アンパサンド)と*(アスタリスク)という記号があります。
ここでは、以下の関係が成り立ちます。
&変数名 = その変数のアドレス
*ポインタ変数の変数名 = 「ポインタ変数がさすアドレス」の値
サンプルコードを用意しましたので、コピーしていろいろいじってみてください。
#include <stdio.h>
int main(void)
{
int A;
int* B;
A = 3;
B = &A;
printf("Aのアドレス=%p Aの値=%d\n", &A, A);
printf("Bのアドレス=%p Bの値=%p Bの中身=%d\n", &B, B, *B);
return 0;
}
ちなみに実行結果はこうなります。
Aのアドレス=0093FBDC Aの値=3
Bのアドレス=0093FBD0 Bの値=0093FBDC Bの中身=3
もし下のような変数とポインタ変数があるならば、
アドレス | 変数名 | 値 |
---|---|---|
0x0061FF2C | A | 7 |
0x0061FF28 | B | 0x0061FF2C |
- &A=0x0061FF2C
- &B=0x0061FF28
- *B=7 (0x0061FF2Cすなわち変数Aの値)こいつが一番大事
- *A(「*」はポインタ変数にしかつかないため)
という関係が成り立ちます。
ポインタ変数の使い方
パスワードという意味でPassという変数を作りましょう。
int Pass=1234;
アドレス | 変数名 | 値 |
---|---|---|
0x0061FF2C | Pass | 1234 |
変数Passの値とアドレスを表示してみましょう。その際、以下の関係を利用しましょう。
&Pass=0x0061FF2C
printf("値%d アドレス%p\n", Pass, &Pass);
実行結果はこのようになります。
値1234 アドレス0x0061FF2C
次にポインタ変数を作ります。このポインタ変数は特殊でルールがあります。
ポインタ変数にはアドレスを入れる
このルールを忘れないでください。
なぜかというと、ポインタ変数とはそもそも、参照したい変数をアドレスを使って呼び出すというのが目的なので、変数自体には参照したい変数のアドレスを入れる必要があるためです。
では、変数Passを参照するポインタ変数Pointerを作成します。
値には変数Passのアドレス0x0061FF2Cすなわち&Passを入れておきます。
int *Pointer;
Pointer = &Pass;
アドレス | 変数名 | 値 |
---|---|---|
0x0061FF2C | Pass | 1234 |
0x0061FF28 | Pointer | &Pass(0x0061FF2C) |
このような関係になっています。
ここはよく理解してほしい重要なところです。
最後に、PointerからPassの値を参照してみましょう。
printf("%d\n",*Pointer);
ポインタ変数PointerがPassのアドレスを指示していますので、*PointerでPassの値を参照することができます。
1234
実行結果は1234、すなわちPassの値が出てきます。
使い方をサラッと紹介したところで値交換のサンプルコードを紹介します。
ポインタを使ったswap関数
#include <stdio.h>
void swap(int *x, int *y);
int main(void)
{
int a=5,b=12;
printf("a=%d,b=%d\n",a,b);
swap(&a,&b);
printf("a=%d,b=%d\n",a,b);
return 0;
}
void swap(int *x, int *y)
{
int tmp;
tmp=*x;
*x=*y;
*y=tmp;
}
簡単に言えば値を交換するプログラムを関数化したものですね。
実行結果は以下のようになります。
a=5,b=12
a=12,b=5
しっかりと値が交換されていますね。
ポインタ変数x,yにそれぞれa,bのアドレスを持ってきていますので、交換前はこういう関係になっています。
アドレス | 変数名 | 値 |
---|---|---|
0x0061FF00 | a | 5 |
0x0061FF04 | b | 12 |
アドレス | 変数名 | 値 |
---|---|---|
0x0061FF08 | (関数内のポインタ変数)x | 0x0061FF00 |
0x0061FF0C | (関数内のポインタ変数)y | 0x0061FF04 |
これより、*x=5,*y=12として値の参照ができます。
ポインタの話ではないですが、tmpとはtemporaryの略で一時的な値の保管として使っています。
C言語でポインタを使うメリット
簡単に言ってしまえば、ポインタとは何かを位置で示すものです。
位置情報です。
では、位置情報を使うことでどのようなメリットがあるのでしょうか。
メモリの節約
ポインタの最大のメリットはこの「メモリの節約になる」ということです。
例えば、以下のようにdouble型配列の中身を10000個で生成したとき、データサイズはどのくらいになるでしょうか?
#include <stdio.h>
// 配列の中身の個数
#define DATASIZE 10000
int main(void)
{
// 容量の大きな配列を定義
double Data_1[DATASIZE];
// データのメモリ容量を表示
int size = sizeof Data_1;
printf("データサイズ : %dbyte\n", size);
return 0;
}
double型は一つで8byteを使います。
さらにそれが10000個あるとすれば、8×10000=80000byte使うことになります。
それを踏まえたうえでこのプログラムをみてください。
#include <stdio.h>
#include <stdlib.h>
// 配列の中身の個数
#define DATASIZE 10000
int main(void)
{
// 容量の大きな配列を定義
double Data[DATASIZE];
// 各値を乱数で生成
for (int i = 0; i < DATASIZE; i++) {
Data[i] = (double)rand() / rand();
}
// 表示する配列を格納する配列を用意
double CopyData[DATASIZE];
// 各値をコピーデータにコピー
for (int i = 0; i < DATASIZE; i++) {
CopyData[i] = Data[i];
}
// データを出力
for (int i = 0; i < DATASIZE; i++) {
printf("SumpleData[%d] \t: %4.4lf\n", i, CopyData[i]);
}
return 0;
}
このプログラムでは、先ほどと同じ容量の配列を二つ用意して値はランダムで生成しています。
この際に行っているデータのコピーですが、見てわかる通り、一つ一つ値をコピーしています。
これを表にするとこうなります。
アドレス | 変数名 | 値 | データサイズ |
---|---|---|---|
0x009EC2EC | CopyData[0] | 2.1304 | 8byte |
0x009EC2EC | CopyData[1] | 0.9808 | 8byte |
0x009EC2EC | CopyData[2] | 4.6147 | 8byte |
0x009EC2EC | CopyData[3] | 0.4364 | 8byte |
このように、配列の各要素はdouble型のデータのコピーです。
すべて8byteずつで10000個、合計80000byte占有しているということです。
つまり、Dataという配列と丸々おんなじ配列を作っているということになります。
何が言いたいかというと、
80000byteのデータをもう一つ作っていること自体がメモリの無駄遣いだ!
と言いたかったのです。
じゃあどうするか?
その答えが「ポインタ」です。
どのようにメモリを節約するか、見てみたほうが理解が早いでしょう。
表示のところで、ちょっとポインタの特殊な使い方をしています。
#include <stdio.h>
#include <stdlib.h>
// 配列の中身の個数
#define DATASIZE 10000
int main(void)
{
// 容量の大きな配列を定義
double Data[DATASIZE];
// 各値を乱数で生成
for (int i = 0; i < DATASIZE; i++) {
Data[i] = (double)rand() / rand();
}
// 表示する配列のアドレスを格納するポインタを用意
double* pData;
// DataのアドレスをpDataにコピー
pData = Data;
// データを出力
for (int i = 0; i < DATASIZE; i++) {
printf("SumpleData[%d] \t: %4.4lf\n", i, *(pData + i));
}
return 0;
}
このように書くとデータの値ではなくデータのメモリアドレスを参照するポインタで表現することができます。
アドレス | 変数名 | 値 | データサイズ |
---|---|---|---|
0x0074C588 | pData | 0x0074C5A0(&Data[0]) | 4byte |
実際にポインタを使った例も使わなかった例も実行結果はこのようになります。
SumpleData[0] : 2.1304
SumpleData[1] : 0.9808
SumpleData[2] : 4.6147
...
SumpleData[9997] : 2.7206
SumpleData[9998] : 1.1182
SumpleData[9999] : 1.625
では本当にポインタによってメモリの消費が抑えられているのでしょうか?
定数定義したDATASIZEの値を大きくしてみた結果、
ポインタを使わなかった例では、DATASIZE=64000程でコンパイルの際にエラーが出ました。
一方ポインタを使用した例では、DATASIZE=128000程までエラーが出ませんでした。
2倍ですね。これが何を意味するかというのはお気づきでしょう。
コピーを作っている分メモリ容量が2倍に跳ね上がっているんです。
これによりわかることは、
ポインタを使うことでコピーを作らずに位置情報だけでデータを参照することができる
ということです。これがポインタの一番大きなメリットです。
おわかりいただけたでしょうか。
- メモリ節約
- 処理速度向上
- ほかにもいろいろ...
最後に
ポインタというのは非常に使い勝手がいいと同時に、エラーの原因によくなりうる厄介なものでもあります。
ですので、紛らわしくてこんがらがると思いますが、ポインタとアドレスの関係だったりこんがらがりそうなところから突き詰めてみて、完全にマスターしてみてください。
最後に、省メモリ化について、不動産屋のたとえ話をします。
ポインタを使わないで、値をコピーするのは、同じデータをもう一個作ることです。
不動産屋でいえば、お客様のために物件をもう一個作るのと同じです。
逆に、ポインタを使う場合は、位置情報を保存するだけなのでデータの複製は起こりません。
不動産屋でいえば、物件の住所のみを記録しているのと同じです。
家が二つあるか一つだけかの違いです。
プログラミングの世界では、物件を複製するのか、住所を記録してアクセスするのか、この使い分けが必要になります。
物件を複製した場合は、片方の家に変更を加えても、もう片方には何の影響もありませんね。
一方住所の場合、家は一つだけなのですから、すべての変更が反映されますね。
ポインタというのは、このように同じものを指し示す道具となりえるので、関数によって値を変えてもらうときにはポインタ渡しということをしたりするんです。
と、この話はここまでにして、終わりましょう。
以上、「【C言語】ポインタを理解しよう!わかりやすくメリットを解説します!」でした。
最後まで読んでいただきありがとうございます。