K3の住民

最近レア社のことをほとんど書いていない自称レア社好きがゲームやプログラミングについて色々書いていく。

C/C++のポインタについていろいろ。

皆さん、こんにちは。雪圀です。

今回は久々にプログラミングについて取り扱っていきます。
ちなみにCによるWindows APIについてですが、あれは取り扱うのが結構疲れるので
今回の記事のようにリハビリ的なものである程度慣れていってから取り扱おうかなと
思います。


さて、今回ポインタについて扱っていきます。このポインタはC/C++を使うとなる
と必ず通る道であるので、C/C++を使うからには絶対に習得していきたいです。

しかし、そうであると同時にC/C++における関門の一つであります。
多くのプログラミング初心者がこのポインタで苦しめられることになると言っても
過言ではないでしょう。

実を言うと、僕もポインタを知り尽くしている、というわけではありません。では、
なぜポインタについて取り扱うのか?というと、ポインタについての知識を深めよう
と思ったからです。まぁ僕の場合、大体ここで取り扱う機械関連の記事は勉強の意味
を込めて作ってるんですけどね。
だから今までどおり「ここはちがうぞ」というのがありましたらバンバンとコメント
などでご指摘していただけると勉強になりますしありがたいです。

では、ポインタについて書いていこうと思います。

ポインタとは

ポインタを一言で説明すると、

メモリアドレスを格納できる変数

です。
最初からどういうこっちゃと思うかもしれませんのでまずメモリアドレスから解説
していきます。

メモリアドレスとは

まずプログラムにおいて変数はどのようにして宣言されているのか、です。これが
理解できないとメモリアドレスは理解できないと思います。

まず変数を用意するとき、プログラムはコンピュータのメモリという部分から全体
の一部を借り、そこに値を入れるのです。イメージするとこんな感じです。

f:id:ryoryoau24:20200426095601p:plain

しかし、それはメモリに空き容量があった場合の話。メモリに空き容量がない場合
に変数を用意すると、コンピュータに怒られてしまいます。

f:id:ryoryoau24:20200426100748p:plain

まぁそんなことが起こるのはメモリを余程酷使してるコンピュータか組込み業界
ぐらいのものなので普通のコンピュータでプログラムを作る際にはそこで心配する
必要性はまずないでしょう。

で、結局メモリアドレスって何なの?ということについてですが、簡単に言えば、
使うメモリの位置です。これは変数を宣言する際にコンピュータが自動で決めて
くれます

しかし、中にはこう思う人もいます。

変数の入れる位置(メモリアドレス)を指定したいよぉ

ポインタは、その願いを実現することが可能なのです。

ポインタの使い方

まずポインタ変数にあらかじめ宣言した変数のメモリアドレスを格納してみます。

#include <stdio.h>

int main(void)
{
	int x = 255;	// 整数型の変数x
	int *ptr;	// 整数型のポインタ変数ptr
	
	// ptrにxのメモリアドレスを格納
	ptr = &x;
	
	// *ptrを出力
	printf("*ptr = %d\n", *ptr);
	
	return 0;
}

「int *ptr;」と言った感じで変数を宣言することで変数ptrを整数型のポインタ変数
として宣言することができます。
そして、ポインタ変数であるptrにあらかじめ宣言しておいた変数xのメモリアドレス
を格納しています。
ちなみに変数のメモリアドレスを表す記号は、「&」であり、「&x」と書くことで
変数xのメモリアドレスを示します

では、これをビルドして実行してみます。

*ptr = 255

わからんって人が出てきたと思いますのでなんでこうなるのか、解説します。
まずptrはポインタ変数であり、そのポインタ変数にはxのメモリアドレスが格納
されています。したがってコンピュータは「ptrはどのメモリアドレスを指している
のか
」を確認し、xのメモリアドレスであることを確認できたら、「そのメモリ
(要はx)に格納されている値を出力する
」のです。

つまり言いたいことをまとめると

このプログラムにおいて*ptrを出力することは、xを出力することと同じである!

ということですね。

では次に、変数の値はポインタ変数を使っても書き換えることができますのでそれを
やってみます。

#include <stdio.h>

int main(void)
{
	int x = 255;	// 整数型の変数x
	int *ptr;	// 整数型のポインタ変数ptr
	
	// ptrにxのメモリアドレスを格納
	ptr = &x;
	
	// *ptrの値を2倍する
	*ptr *= 2;
	
	// xを出力
	printf("x = %d\n", x);
	
	return 0;
}

このプログラムの前からの変更点は、「*ptrの値を2倍にする処理の追加」と「出力
する変数を*ptrからxに変更
」のみです。

では、ビルドして実行してみます。

x = 510

はい、どうでしょう。xの値は最初255だったはずなのに2倍されて510になっている
ことが確認できたのではないでしょうか。実際に2倍したのは*ptrの値であるにも
関わらずです。
実は*ptrとxのメモリアドレスは同じなので「*ptrのメモリを書き換えた」ということ
は、「xのメモリを書き換えた」ことと同義となるのです。
つまり

このプログラムにおいて*ptrとxはメモリアドレスが同じであるがゆえに、値も同じと
なる

のです。

これでポインタの基礎の基礎は以上となります。

ポインタのポインタ

まず、以下のことを覚えておきましょう。

ポインタにおいて、*がついていた場合は格納されたメモリアドレスの変数として、
ついていない場合はポインタ変数として扱われる

なんでこれをわざわざ?と思うかもしれませんが、これを言っておかないと今から
説明する話がややこしくてわからなくなるからです。

では、解説に戻ります。

ポインタ変数も、メモリアドレスを格納する立派な変数です。つまり、ポインタ変数
そのものにもメモリアドレスがあるのです。
つまり先ほどのプログラムは、以下のようにメモリ配置されていたわけですね。

f:id:ryoryoau24:20200426142720p:plain

では、ポインタ変数にもメモリアドレスがあるということは、このように考えること
はできないでしょうか?

ポインタ変数のメモリアドレスは、また別のポインタ変数に格納することができる
のではないか

と。言うまでもありませんね、可能です。

ポインタ変数のメモリアドレスが格納されることを、「ポインタのポインタ」とか
言ったりします。

では早速使ってみましょう。変数の値を変えてみます。

#include <stdio.h>

int main(void)
{
	int x = 255;	// 整数型の変数x
	int *ptr;	// 整数型のポインタ変数ptr
	int **ptrptr;	// 整数型のポインタのポインタ変数ptrptr
	
	// ptrにxのメモリアドレスを格納
	ptr = &x;
	
	// ptrptrにptrのメモリアドレスを格納
	ptrptr = &ptr;
	
	// **ptrptrの値を3倍する
	**ptrptr *= 3;
	
	// xを出力
	printf("x = %d\n", x);
	
	return 0;
}

ポインタのポインタを使うためには、*を2つ使って宣言する必要があります。そう
しないとエラーを吐いたりします。
それと同時に、ptrptrを使ってxを書き換えるためには、**ptrptrと*を2つ付ける必要
があります

まぁ、そりゃそうだわな。だってポインタのポインタだもの。

ということで、ビルドして実行してみます。

x = 765

今回は3倍しましたので、このとおりxは3倍になってますね。

ちなみに・・・

ポインタのポインタが可能ということは、ポインタのポインタのポインタが可能
いうことです。
さらに言えば、ポインタのポインタのポインタが可能ということは、ポインタの
ポインタのポインタのポインタが可能
ということですね。
さらにさらに、ポインタのポイ(ry

はい、自重します。

ポインタ変数のインクリメント

ポインタ変数も変数なので、インクリメント(加算)できます
では、ポインタ変数をインクリメントするとどうなるのでしょうか?

答えは簡単。ポインタ変数に格納されているメモリアドレスが変わります
これをすることでどのようなメリットがあるのか、早速例を出してみたいと思い
ます。
たとえば、以下のようなプログラムがあったとします。

#include <stdio.h>

int main(void)
{
	char a = 'a';	// 文字型の変数a
	char b = 'b';	// 文字型の変数b
	char *ptr;	// 文字型のポインタ変数ptr
	
	// ptrにaのメモリアドレスを格納
	ptr = &a;
	
	// ptrをインクリメントする
	ptr++;
	
	// *ptrを出力
	printf("*ptr = \'%c\'\n", *ptr);
	
	return 0;
}

ここで、今使っているコンピュータは、宣言した変数を順に配置していくと仮定して
おきます。そうじゃないのもあったはずなので、結果が異なることもあると先に
言っておきます
。すみません。

では、ビルドして実行してみます。

*ptr = 'b'

どういうわけか'b'と出力されました。「なんで?bのメモリアドレスは格納して
いなかったはずなのに・・・」そうお思いでしょう。
しかし、メモリ配置がどのようになっているのか見てみるとそれは単純明快である
ことがわかります。

まずはaのメモリアドレスを0xXXXXXXX0、bのメモリアドレスを0xXXXXXXX1であると
しておきます。
ということは、ptrにaのメモリアドレスを格納したとき、ptrの値は0xXXXXXXX0と
なっています。

f:id:ryoryoau24:20200426145812p:plain

次に、ptrをインクリメントしてみます。すると、メモリアドレス0xXXXXXXX0から
1加算されるわけですので・・・

f:id:ryoryoau24:20200426150823p:plain

このように0xXXXXXXX1となり、ptrに格納されているのはbのメモリアドレスになり、
*ptrを出力するとbが出力される、というわけですね。

ちなみにこのように書いても'b'と出力されます。

#include <stdio.h>

int main(void)
{
	char a = 'a';	// 文字型の変数a
	char b = 'b';	// 文字型の変数b
	char *ptr;	// 文字型のポインタ変数ptr
	
	// ptrにaのメモリアドレスを格納
	ptr = &a;
	
	// *(ptr+1)を出力
	printf("*(ptr+1) = \'%c\'\n", *(ptr+1));
	
	return 0;
}

前のプログラムと違うのは、ptrの値はaのメモリアドレスのままということです。
だから「*ptr = 'b'」ではなく、「*(ptr+1) = 'b'」と出力させるようにしています。

*(ptr+1) = 'b'

サブルーチンで変わった値をメインルーチンに反映

では最後に少しだけ応用させて今回は終わりにしましょう。

今回は2つの変数の値を入れ替える関数を作ってみようと思います。・・・まぁよくある奴ですけど。

ちなみに2つの変数の値を入れ替えるためには2つの変数のほかにもう1つ、入れ替える
用の変数が必要です。どのようにするのかは今回割愛しようと思います。これ以上
詳細に書くと長くなる。

では、ポインタなしでやるとどうなるのかやってみましょう。

#include <stdio.h>

void swap(int numA, int numB);

int main(void)
{
	int num1 = 15;	// num1の値は15
	int num2 = 31;	// num2の値は31
	
	// ここでnum1とnum2の値を入れ替える
	swap(num1, num2);
	
	// するとnum1は31、num2は15になっている・・・はず
	printf("num1 = %d\nnum2 = %d\n", num1, num2);
	
	return 0;
}

// 入れ替え関数
void swap(int numA, int numB)
{
	int tmp;
	
	tmp  = numA;
	numA = numB;
	numB = tmp;
}

ではビルドして出力を確認してみます。

num1 = 15
num2 = 31

あれ?num1とnum2の値が入れ替わっていない・・・?
それもそのはず、swap()関数の引数にnum1とnum2を代入しただけなのですから、
メインルーチンの値が変化するはずないんですよね。
ではどうすればいいのか?num1とnum2のメモリに直接書き込むようにすればいいん
です

はい、というわけでポインタの出番です。ポインタを使ってみましょう。

#include <stdio.h>

void swap(int *numA, int *numB);

int main(void)
{
	int num1 = 15;	// num1の値は15
	int num2 = 31;	// num2の値は31
	
	// ここでnum1とnum2の値を入れ替える
	swap(&num1, &num2);
	
	// するとnum1は31、num2は15になっている
	printf("num1 = %d\nnum2 = %d\n", num1, num2);
	
	return 0;
}

// 入れ替え関数
void swap(int *numA, int *numB)
{
	int tmp;
	
	tmp   = *numA;
	*numA = *numB;
	*numB = tmp;
}

ではビルドして結果を確認してみましょう。

num1 = 31
num2 = 15

ちゃんとnum1とnum2の値が入れ替わっていることを確認できました。

最後に

割と難しかったと思いますが、ポインタについてはここまでです。

ほかにも覚えておくべきことはたくさんありますが・・・まぁこれが理解できれば
ある程度応用はきくんじゃないかなぁと思います。

ただ、注意点として、このポインタはご覧の通りメモリアドレスを指定できる代物
なので書いちゃいけないメモリに書き込んでしまう可能性もあります
まぁ汎用OSであればそういうことは絶対起こらないとは思いますが。
それぐらいですかね。


今回はこれぐらいにしておきます。