【C言語】ポインタを理解しよう!わかりやすくメリットを解説します!

C言語を学ぶ上で最初につまづきやすいランキング上位である『ポインタ』

私の周りのC言語を学んでいる人たちは、難しい、分からないと言っている人が多かったように感じます。

今回はC言語を始めたての方に向ける記事で、C言語におけるポインタという概念やメリットなどをわかりすく、C言語のサンプルコードを用いて解説していきます。

フリューゲルで未経験からお給料を貰いながら正社員エンジニアになる!

ポインタ

ポインタ (pointer) とは、あるオブジェクトがなんらかの論理的位置情報でアクセスできるとき、それを参照する(指し示す)ものです。

簡単に言えば、何かを指し示すものというイメージです。

パソコンのディスプレイ、もしくはスマホの画面を指さしてみてください。その人差し指がポインタということになります。イメージはそんな感じです。

今回はC言語の「特定のメモリ領域を表現する」ポインタを軸に話を進めていきます。

ポインタ変数の基礎

ポインタC言語の特徴的な機能のひとつです。ここでは、どのような機能なのかということと使い方をご紹介します。

C言語のポインタにかかわる記号

C言語において、&(アンパサンド)*(アスタリスク)という記号があります。ここでは、こういう関係が成り立ちます。

&変数名 = その変数のアドレス

*ポインタ変数の変数名 = 「ポインタ変数がさすアドレス」の値

サンプルコードを用意しましたので、コピーしていろいろいじってみてください。

#include <stdio.h>

void 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);
}

ちなみに実行結果はこうなります。

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、すなわちPassの値が出てきます。

使い方をサラッと紹介したところで値交換のサンプルコードを紹介します。

ポインタを使ったSwap関数

#include <stdio.h>

void Swap(int *x,int *y);

void 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);
}

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の略で一時的な値の保管として使っています。というのも、aの値をbにコピーしてから、bの値をaにコピーしようとすると、もうbの値はコピーされたaの値ですからデータの消滅が起こってしまいます。

a=5 b=12 
a=5 b=5  //bにaを代入
  • a=5,b=12
  • a→b=5
  • a=5,b=5
  • bの値12が消滅

ですので、新しくデータの保管を行って

a=5  b=12 tmp=? //tmpを作成
a=5  b=12 tmp=5 //tmpにaを代入
a=12 b=12 tmp=5 //aにbを代入
a=12 b=5  tmp=5 //bにtmpを代入
  • a=5,b=12,tmp
  • a→tmp=5
  • a=5,b=12,tmp=5
  • b→a=12
  • a=12,b=12,tmp=5
  • tmp→b=5
  • a=12,b=5,tmp=5

とするわけですね。

ポインタを使うメリット

簡単に言ってしまえば、ポインタとは何かを位置で示すものです。位置情報です。位置情報を使うことでどのようなメリットがあるのでしょうか。

メモリの節約

ポインタの最大のメリットはこれだと思います。メモリの節約になることです。

例えば、このようにdouble型配列の中身を10000個で生成したとき、データサイズはどのくらいになるでしょうか?

#include <stdio.h>

//配列の中身の個数
#define DATASIZE 10000

void main(void)
{
	//容量の大きな配列を定義
	double Data_1[DATASIZE];

	//データのメモリ容量を表示
	int size = sizeof Data_1;
	printf("データサイズ : %dbyte\n", size);
}

double型は一つで8byteを使います。さらにそれが10000個あるとすれば、80000byte使うことになります。

データサイズ : 80000byte

それを踏まえたうえでこのソースコードをみてください。

#include <stdio.h>
#include <stdlib.h>

//配列の中身の個数
#define DATASIZE 10000

void main(void)
{
	//容量の大きな配列を定義
	double Data[DATASIZE];

	//各値を乱数で生成
	for (int i = 0; i < DATASIZE; i++) {
		Data[i] = 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: %8.0lf\n", i, CopyData[i]);
	}
}

このソースコードでは、先ほどと同じ容量の配列を二つ用意して値はランダムで生成しています。

この際に行っているデータのコピーですが、見てわかる通り、一つ一つ値をコピーしています。これを図にするとこうなります

アドレス 変数名
0x009EC2EC Data[0] 41
0x009D8A58 CopyData[0] 41

配列の番号は何でもいいのですが、このように値を抽出してCopyDataの同じ番号の場所に値を入れています。

結局何が言いたいかというと、

80000byteのデータをもう一つ作っていること自体がメモリの無駄遣いだ!

と言いたかったのです。

じゃあどうするか?それがポインタなんです。

その前に不動産屋のたとえ話をしましょう。

今私が行ったコピーは、をそのままコピーして不動産屋の隣に建てるタイプです。お客さんに見せたい物件を横に建てられるんですからまあ便利。紹介するときは実際に隣の家に行けばいいのですからね。

でも明らかにアホですよね?理由は明確です。

 

実際に二つのおんなじ家が建っている状況です。お客様に見せるためだけにもう一つ家を建てる必要がありますか?それに土地代がもったいないですよ。

もうなにをすればいいかわかりましたか?

 

住所です

 

見せたい物件の住所さえわかっていれば、家は一つでいいんですよ。二個も建てる必要はありません。

このたとえ話を今度はポインタに置き換えて考えてみましょう

今私が行ったコピーは、大きなデータをそのままコピーしてそのコピーしたデータを表示するタイプです。

でも明らかにアホですよね?理由は明確です。

 

実際に二つの同じデータが存在する状況です。データを表示するのにもうひとつ同じデータを作る必要がありますか?それにメモリがもったいないですよ。

もう何をすればいいかわかりましたか?

 

ポインタです。

 

見せたいデータのアドレスさえ分かっていれば、データは一つでいいんですよ、二つも必要ありません。

とこのように置き換えることができます。たとえがぴったりはまっていますね。

メリットがわかったところで実際にどうやってメモリの節約をするか書いてみましょう。

#include <stdio.h>
#include <stdlib.h>

//配列の中身の個数
#define DATASIZE 10000

void main(void)
{
	//容量の大きな配列を定義
	double Data[DATASIZE];

	//各値を乱数で生成
	for (int i = 0; i < DATASIZE; i++) {
		Data[i] = rand();
	}

	//表示する配列のアドレスを格納するポインタを用意
	double* pData;

	//DataのアドレスをpDataにコピー
	pData = Data;

	//データを出力
	for (int i = 0; i < DATASIZE; i++) {
		printf("SumpleData[%d]  \t: %8.0lf\n", i, *(pData + i));
	}
}

このように書くとデータの値ではなくデータのメモリアドレスを参照するポインタで表現することができます。

アドレス 変数名
0x0074C5A0 Data[0] 41
0x0074C588 (ポインタ変数)pData 0x0074C5A0

実際にポインタを使った例も使わなかった例も実行結果はこのようになります。

SumpleData[0]   :       41
SumpleData[1]   :    18467
SumpleData[2]   :     6334
SumpleData[3]   :    26500
...
SumpleData[9997]        :     1546
SumpleData[9998]        :    31880
SumpleData[9999]        :    18796

では本当にポインタによってメモリの消費が抑えられているのでしょうか?

定数定義したDATASIZEの値を大きくしてみた結果、

ポインタを使わなかった例では、DATASIZE=64000程でコンパイルの際にエラーが出ました。

一方ポインタを使用した例では、DATASIZE=128000程までエラーが出ませんでした。

2倍ですね。これが何を意味するかというのはお気づきでしょう。

コピーを作っている分メモリ容量が2倍に跳ね上がっているんです。

これによりわかることは、

ポインタを使うことでコピーを作らずに位置情報だけでデータを参照することができる

ということです。これがポインタの一番大きなメリットです。おわかりいただけたでしょうか。

ポインタのメリット

 

メモリ節約

処理速度向上

ほかにもいろいろ…

最後に

ポインタというのは非常に使い勝手がいいと同時に、エラーの原因によくなりうる厄介なものでもあります。ですので、紛らわしくてこんがらがると思いますが、ポインタとアドレスの関係だったりこんがらがりそうなところから突き詰めてみて、完全にマスターしちゃってください。

最後まで読んでいただきありがとうございます。

コメントを残す

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