変数の参照方法、アドレス、ポインタ変数、配列と独自関数でのポインタ変数の関連と利用 勉強しなおしの記録というか学習ノート-6日目-
Unreal Engin4すごい!(語彙力)ってなったので、少しいじりつつ、むかーしかじった事あるC,C++の学びなおしの学習ノート。この連載記事の詳しい趣旨と注意事項は1日目をご覧ください。
昨日の勢いに任せて、ポインタまでやってしまいますよ!…ですが、その前にアドレスのお話です。
目次
変数の参照方法と、アドレスと、それからポインタ
『金子みすゞ』であれば、「みんな違ってみんな良い」になるわけですが、これの場合は逆に密接にかかわっている上何なら「みんな違うようで実はみんな同じ」という言い方も(さすがにすっ飛ばし過ぎ)なんですよね。
参照方法 –直接参照と関節参照
今までやってきた↓のような変数の取り扱いは、直接参照と言って、データを格納する変数(箱)の場所に直接値を取りに行ったり入れに行ったりしていたわけです。ちなみに、こういった、値の渡し方を値渡し(本当にそのまま)と言います。
#include <stdio.h>
void main() {
int a = 1;
printf("a = %d ", a); // 実行結果:a = 1
}
ん?直接参照?じゃあ、直接じゃない参照方法あるの?あります(食い気味)
その名も関節参照(そのまま)といい、間接的に変数に格納されている値をやり取りする参照方法です。
「間接的」って具体的にどうやるのかと思われるかと思います。いままでの記事で何となく触れてきたメモリ(記憶領域)のアドレスからやり取りするのです。
※メモリ(記憶領域)に関しましては、こちらの記事で詳しく書いておりますので、よろしければご覧ください。
変数のアドレスを扱う –&演算子
アドレスをやり取りするって、どうやるの?実は、今までに書いた記事を順を追ってやってきてくださっているなら、一度やっているんです。
この記事の、入力を取り扱うscanf()関数のところを抜粋して再掲載させていただきます。
int a;
printf("何か数値を入力してください: ");
scanf("%d", &a);
printf("%d", a);
初見では、疑問に思われた方も多かったのではないかと思います。この入力した数値を変数”a”に格納するときに”&a”とされている、この「“&”って何?」って。
これはアドレス取得演算子なのです。
1つ前の記事ではビット論理積(and)の演算子とご紹介しましたが、1つの変数の前にくっつけて使うと、その変数のデータ領域が確保されているメモリのアドレスが取得できるんです。
だから、先日の記事でscanf()関数を使って入力を変数に格納したやり方は、実は間接参照だったわけです。
そして、直接参照での値の渡し方に『値渡し』という名前があったように、この関節参照の方にも渡し方に名前があり、こちらは『参照渡し』と言います。
アドレスを格納する専用の変数 –ポインタ変数
さて、出てきました『ポインタ』。思わず条件反射的に身構えてしまう私なのですが、よく考えれば突破できるはず…!!と思って玉砕してみますね!(砕けてどうする)
まずは、ポインタ変数というのの扱い方を説明します。
- 宣言
書式「型 *変数名;」
これで宣言は終わりです。この”*”をつけて宣言した変数がポインタ変数になります。
- アドレスを取得する
書式「ポインタ変数 = &変数」
“&”が先頭についている変数から、アドレスを取得しています。そして、ここで注意です!ポインタ変数と、アドレスを格納しようとしている変数の型は同じものにしましょう。
- アドレスを取得した元の変数を、アドレスから関節参照
書式「*ポインタ変数」
“*”を先頭につけたポインタ変数が、ポインタ変数自身の中に格納されているアドレスを辿って、そのアドレス元のメモリアドレスに格納されている変数を参照します。
では実際に使ってみます。
#include <stdio.h>
void main() {
// int型変数とポインタ変数の宣言
int a = 100, *b;
// 間接参照による参照渡し
b = &a;
printf(" a = %d,\n*b = %d", a, *b);
}
これだけでは、一応使えたけれど、「そんなまだるっこしいことしなくても、直接参照で、値渡ししちゃえばいいんじゃないの?」と思われると思いますので、次に、直接参照と関節参照、その最たる違いが判るように、例を挙げてみます。
※少し前は”%x”でアドレスを表示していた気がするんでビックリしたんですが、今は”%p”でアドレスを表示するようです。(ほかの環境で違ったら教えてくださいm(_ _)m)
#include <stdio.h>
void main() {
int a = 1, b = 2, *c, d;
c = &a;
d = b;
printf("a = %d,\t&a = %p\n", a, &a);
printf("b = %d,\t&b = %p\n", b, &b);
printf("c = %p,\t*c = %d\n", c, *c);
printf("d = %d,\t&d = %p\n", d, &d);
printf("------------------------\n");
a = 5;
b = 6;
printf("a = %d,\t&a = %p\n", a, &a);
printf("b = %d,\t&b = %p\n", b, &b);
printf("c = %p,\t*c = %d\n", c, *c);
printf("d = %d,\t&d = %p\n", d, &d);
}
実行結果↓
a = 1, &a = 0133F9E0
b = 2, &b = 0133F9DC
c = 0133F9E0, *c = 1
d = 2, &d = 0133F9D4
------------------------
a = 5, &a = 0133F9E0
b = 6, &b = 0133F9DC
c = 0133F9E0, *c = 5
d = 2, &d = 0133F9D4
※上の実行結果で、”&a”など、アドレスが表示されているところは、上では「0133F9E0」というになっていますが、毎回実行する度、違うメモリアドレスが割り当てられるため、違った結果になっても何もおかしくはないのでご安心ください。
途中に線が引かれているところで、それぞれの参照元のa, bの値を変更しています。
直接参照して値を値渡しでbから受け取ったdは、もともとの値”2″であるのに対し、
間接参照して値を参照渡しでaから受け取ったcは、もともとの値”1″から”5″に変わっています。
つまり、直接参照での値渡しは、その変数に格納されている値のコピーを渡されただけの、それぞれが別の物であるのに対し、
間接参照での参照渡しは、その変数が格納されているメモリアドレスをコピーして渡され、参照する時はアドレスを辿るため、同じ変数の同じ値を同期的に扱うことができる訳です。
分かりやすく表でまとめてみます。
参照方法 | 値の渡し方 | 説明 | 値の変更の反映 |
直接参照 | 値渡し | 値を直接変数に渡し、変数の値を渡す時は、コピーなので、元のものと渡したものは別物。 | 別物なので出来ない |
間接参照 | 参照渡し | メモリアドレスを渡し、変数の値を渡す時はメモリアドレスを辿って行うので、値は常に同じ物を扱うことができる。 | 同じものなので出来る |
※ここからは個人のイメージです。
他のところでは本職の方(言い方)に怒られる気がするので、(((゜Д゜;))))ガクガクブルブル
そこんところご了承くださいm(_ _;)m
直接参照による値渡しは、いわゆる『コピペ』
間接参照による参照渡しは、いわゆるクラウド的な『常時同期』
※ほろほろり個人のイメージです。個人によるイメージは以上。
ちなみに、参照渡しでポインタ変数からもともとの変数の値を変える場合もご紹介します。
- ポインタ変数→アドレス元の変数への値の参照渡し
書式「*変数 = 値」
実際に使ってみた例を挙げてみます。
int a = 1, *c;
c = &a;
printf("a = %d,\t&a = %p\n", a, &a);
printf("c = %p,\t*c = %d\n", c, *c);
printf("------------------------\n");
*c = 6;
printf("a = %d,\t&a = %p\n", a, &a);
printf("c = %p,\t*c = %d\n", c, *c);
実行結果↓
a = 1, &a = 004FF8A0
c = 004FF8A0, *c = 1
------------------------
a = 6, &a = 004FF8A0
c = 004FF8A0, *c = 6
ポインタと配列
scanf()関数での入力でchar型の配列に値を渡す時に、”&”が付かなかった謎について
気づいてしまいましたか……君のような勘のいい方大好きです(変えすぎてて元ネタ伝わるかな)
先日書いた記事の、該当部分のソースコード持ってきて再掲載。
char a[12]; // 文字列は配列として変数に格納されているので、配列として変数を用意。
scanf("%s", a);
printf("%s", a);
と、いうわけで、解説していくわけですが、そのためにまず最初に↓のソースコード実行してみてください。
char a[12];
printf("%p\n", a);
実行結果↓
008FFB68
ということで、配列の変数名を”%p”で表示させてみたら、アドレスが出てきます。
配列のアドレス
上の項での謎のアドレスの正体を暴くため、とりあえず配列のアドレスすべてとともに表示してみましょう。
char st[] = "abcdefghi";
printf("st(%%s) = %s\tst(%%p) = %p\n", st, st);
for (int i = 0; i < sizeof(st) / sizeof(char); i++) {
printf("st[%d] = %c\t&st[%d] = %p\n", i, st[i], i, &st[i]);
}
実行結果↓
st(%s) = abcdefghi st(%p) = 00F5F7F8
st[0] = a &st[0] = 00F5F7F8
st[1] = b &st[1] = 00F5F7F9
st[2] = c &st[2] = 00F5F7FA
st[3] = d &st[3] = 00F5F7FB
st[4] = e &st[4] = 00F5F7FC
st[5] = f &st[5] = 00F5F7FD
st[6] = g &st[6] = 00F5F7FE
st[7] = h &st[7] = 00F5F7FF
st[8] = i &st[8] = 00F5F800
st[9] = &st[9] = 00F5F801
上の例のように、”st”を”%p”で表示してアドレスと、”st[0]”のアドレスが一致するのがお分かりになるかと思われます。
そして、もう一つ、お気づきになるでしょうか?配列のアドレスが、1つづつズレた連番となっていることが!ちなみに、1つづつズレているのは、この配列が文字列でchar型だからです。char型は1バイトなため、1つづつズレているわけです。
それから、st[9]で、表示されている空欄は、ここには「\0」が入っているからです。「\0」とは「何もない」ということを表します。
「何もない」のに入ってるっていうと何か違和感を感じるかもしれませんが、C言語では配列最後には「\0」が入っているのが仕様なのです。これが入っているから、配列の最後がコンピュータに認識してもらえるので、これ結構大事です!
添え字なしの配列変数名からは、配列の先頭要素のアドレスに参照している
配列を格納している変数で、”[]”で添え字をつけていない、変数名だけの場合は、その配列の先頭要素のアドレスを意味します。変数名には配列の先頭アドレスが格納されているのです。
書式指定子”%s”で表示する場合、その最初のアドレスから、配列の要素に格納されている値が「\0」になるまで繰り返し要素を参照して表示してくれるわけです。
ということで、ちょっと実験してみます。
一文字しか格納しないchar型の変数”c”を、参照渡しでポインタ変数”st”にアドレスを渡して、その”st”に格納されている”c”のアドレスから”%s”の書式指定子で表示させてみたらどうなるのか。
char c= 'a',*st;
st = &c;
printf("st(%%s) = %s\tst(%%p) = %p\n", st, st);
実行結果↓
st(%s) = al st(%p) = 00EFF923
といった具合に’a’以降にいくつか文字化け文字が表示します。(運よく”l”が出てしまいましたが…)
これは初期化されていないメモリのアドレスにも、何かしら文字が格納されることがあり、それを表示させたため、文字化けする文字が表示されてしまったわけです。
逆に考えると、文字列を格納する場合は、最後に”\0″を格納すれば、明示的に文字列の終わりを示すことができる訳です。(ただし、最近のコンパイラさん優秀みたいで、昔みたいに文字化け文字が表れませんでした…orz)
char st[] = { 'H','e','l','l','o',' ','W','o','r','l','d','\0' };
printf("st(%%s) = %s\tst(%%p) = %p\n", st, st); //実行結果:st(%s) = Hello World st(%p) = 00AFFD94
文字化けさせたかったバージョン↓
char st[] = { 'H','e','l','l','o',' ','W','o','r','l','d' };
printf("st(%%s) = %s\tst(%%p) = %p\n", st, st);
とはいっても、こんな一文字一文字入力するのは手間ですし、文字列は普通に「”」で括って、一気に格納してしまうのが楽ですし見やすいと思いますよ。
ポインタ変数から、配列の各要素にアクセス
乱数を発生させ、その乱数を10個格納する配列を、参照渡ししたポインタ変数から参照してみます。
今回は乱数を使うので、「stdlib.h」と、「time.h」をインクルードしてください。
乱数の発生方法は、一週間で身につくC言語の基本|第1日目:数値の扱いを参考にさせていただき、
ポインタ変数を配列のように[]に添え字を入れて使うやり方は、
配列とポインタの奇妙な関係 – 苦しんで覚えるC言語を参考にさせていただきましたm(_ _)m
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void main() {
int randNum[10], *copyNum;
copyNum = randNum;
srand((unsigned)time(NULL));
for (int i = 0; i < 10; i++) {
randNum[i] = rand() % 10 + 1;
}
for (int i = 0; i < 10; i++) {
printf("元々の数値:%d\t間接参照した数値:%d\n", randNum[i], copyNum[i]);
}
}
実行結果↓
元々の数値:5 間接参照した数値:5
元々の数値:5 間接参照した数値:5
元々の数値:5 間接参照した数値:5
元々の数値:3 間接参照した数値:3
元々の数値:4 間接参照した数値:4
元々の数値:9 間接参照した数値:9
元々の数値:6 間接参照した数値:6
元々の数値:5 間接参照した数値:5
元々の数値:5 間接参照した数値:5
元々の数値:8 間接参照した数値:8
私が最初に学習した時は、この例を書く時に参考にさせていただいた 配列とポインタの奇妙な関係 – 苦しんで覚えるC言語 のページでも解説されている、 *(ポインタ変数 + 要素番号) という方法でやったのですが、これはあまりわかりやすくない上、時代に合わない古いやり方だそうですorz…ので、恐らくは上の例が一番やりやすいのではないかと思います。
ポインタと関数
先日書いた記事で紹介した自作関数の作り方では、引数を取って、そこから値をreturnで返して、結果を反映させていたわけですが、そのときのやり方は、直接参照による値渡しで引数を関数に渡していました。
ですが関数の目的によっては、最初から参照渡しで引数に渡せば、returnしなくても即反映する、便利な参照渡しの使い方ができる訳です。
というわけで、上の項の乱数を少し改変して、逆順の配列を作る参照渡し引数の自作関数を作った例↓
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define ARRAY_ELEM 10 //配列の要素数
// プロトタイプ宣言
void rand_numbers_set_array(int*);
void reverse_copy_array(int*, int*);
void main() {
int randNums[ARRAY_ELEM], copyNums[ARRAY_ELEM];
srand((unsigned)time(NULL));
rand_numbers_set_array(randNums);
reverse_copy_array(randNums, copyNums);
for (int i = 0; i < ARRAY_ELEM; i++) {
printf("元々の数値:%d\t間接参照した数値:%d\n", randNums[i], copyNums[i]);
}
}
// 乱数を配列にセットする独自関数
void rand_numbers_set_array(int *a) {
for (int i = 0; i < ARRAY_ELEM; i++) {
a[i] = rand() % 10 + 1;
}
}
// 整数値の配列を逆順にコピーする独自の関数
void reverse_copy_array(int *a, int *b) {
for (int i = 0; i < ARRAY_ELEM; i++) {
b[i] = a[ARRAY_ELEM - 1 - i];
}
}
実行結果↓
元々の数値:1 間接参照した数値:6
元々の数値:9 間接参照した数値:9
元々の数値:4 間接参照した数値:1
元々の数値:1 間接参照した数値:9
元々の数値:6 間接参照した数値:2
元々の数値:2 間接参照した数値:6
元々の数値:9 間接参照した数値:1
元々の数値:1 間接参照した数値:4
元々の数値:9 間接参照した数値:9
元々の数値:6 間接参照した数値:1
関数ポインタ
以前の記事で、型が変数だけではなく、関数にも付くという話をしたと思うのですが、つまりは変数でポインタがあれば、関数にもポインタがあるということです(説明が無理やり)
書式「型 (*)(引数)」
ここでも、変数同様関数の型は、関数ポインタとアドレス元の関数を揃えてください。
ただし変数ポインタと違い、関数名のところを「()」で囲い、その「()」内で”*”を付け、ポインタの宣言をします。引数がある場合は、その次の()内で記述しましょう。ただし、ここの引数用の()は省略してはいけません。引数があってもなくてもつけましょう。
例↓
#include <stdio.h>
// プロトタイプ宣言
void f1();
void f2();
void main() {
void (*fp)() = f1;//関数ポインタにf1のアドレスを格納
printf("f1 = %p\t%p = *fp\n", f1, *fp);
fp();
fp = f2;//関数ポインタにf2のアドレスを格納しなおす
printf("f2 = %p\t%p = *fp\n", f2, *fp);
fp();
}
void f1() {
printf("1\n");
}
void f2() {
printf("2\n");
}
実行結果↓
f1 = 00B51000 00B51000 = *fp
1
f2 = 00B51020 00B51020 = *fp
2
ということで、後ろに「()」を付けない関数名にはアドレスが格納されていることがわかりました。(こういう部分配列と似ていますね。)
すぐに実行しない場合は、「()」を外します。そうすれば、()を後ろにつけていない関数名は、アドレスが格納されているので、ポインタ変数と同じように、関数ポインタに参照渡しすることができます。
また、関数ポインタを関数の引数として別の関数に参照渡しして使用する場合を、以前の記事で作った、立方体の体積を求める関数を関数ポインタ用に改変して載せてみます。
#include <stdio.h>
// プロトタイプ宣言
void info_str(int(*)(int), int);
int cube(int);
void line();
void main() {
int a[3] = { 3,5,11 };
for (int i = 0; i < 3; i++) {
info_str(cube, a[i]);
}
}
// 立方体の体積計算結果を表示する関数
void info_str(int (*f)(int), int len) {
printf("一辺が%dの立方体の体積: %d\n", len, f(len));
line();
}
// 立方体の体積を算出する関数
int cube(int len) {
return len * len * len;
}
// 区切りのための線を表示する関数
void line() {
printf("--------------------------------\n");
}
実行結果↓
一辺が3の立方体の体積: 27
--------------------------------
一辺が5の立方体の体積: 125
--------------------------------
一辺が11の立方体の体積: 1331
--------------------------------
といったように、関数ポインタも、部品として、変数と一緒に受け取って、関数の中で関数と引数を組み合わせて実行する、といった使い方ができるかと思います。
おまけ:ポインタのポインタ
ポインタのポインタ…ってなに?って混乱される方多いのではないでしょうか?実は私もここで大変混乱しましたw
そのアドレスに格納されているアドレスを参照するためのものです…。とりあえず実際に例を挙げてみます。
2次元配列を使う場面なんかで役立ちます。
#include <stdio.h>
void main() {
char* st[3] = { "Tokyo","tokkyo","kyokakyoku" };
char** ppst; // ポインタのポインタ
ppst = st;
for (int i = 0; i < 3; i++) {
printf("%s\n", ppst[i]);
}
}
実行結果↓
Tokyo
tokkyo
kyokakyoku
解説しますと、まず、
char* st[3] = { "Tokyo","tokkyo","kyokakyoku" };
文字列の配列ですから、二次元の配列が出来ています。
「st」というのが、配列の先頭アドレスが格納されていることまではいいですよね?
で、[]をつけない配列名の「st」は、{ “Tokyo”,”tokkyo”,”kyokakyoku”}の配列の先頭、つまり、”Tokyo”を格納しているアドレスなわけです。
それに”*”がついているためポインタになり、そのメモリアドレスの場所に格納されているのは、値ではなくアドレスが格納されていることになります。
つまり、「*st[3]」は、アドレスを格納している配列というわけです。
だから、アドレスを格納しているアドレスにアクセスするために、ポインタのポインタが必要なわけです。
なので、ポインタのポインタでアドレスを格納している場所のアドレスの場所の値を参照しているわけなのです…。もうこれ、わけわかんなくなりそうなので、表にしてみます。
st | &st[0] | ppst | &ppst[0] | “Tokyo”の場所のアドレス |
&st[1] | &ppst[1] | “tokkyo”の場所のアドレス | ||
&st[2] | &ppst[2] | “kyokakyoku”の場所のアドレス |
*st | st[0] | &st[0][0] | *ppst | ppst[0] | &ppst[0][0] | ‘T’の場所のアドレス | “%s”指定子で「Tokyo」と表示される |
st[1] | &st[1][0] | ppst[1] | &ppst[1][0] | ‘t’の場所のアドレス | “%s”指定子で「tokkyo」と表示される | ||
st[2] | &st[2][0] | ppst[2] | &ppst[2][0] | ‘k’の場所のアドレス | “%s”指定子で「kyokakyoku」と表示される |
今はこれが精いっぱい。m(_ _;)m
あとがき
(これを書き終えるまでは)メンタル的に死ぬかと思いました_(´ཀ`」 ∠)_が、書き始めると、「あ!ここわかる!前〇×ゼミでやった!」みたいな感じになって意外と生きてました。 笑
解説やりとげてみると、そうでもないですが、過去の大混乱した時の思い出(トラウマ)がw
でも、こういう風に再勉強して、それで記事に書けるくらい調べまわって、っていうことすると、どうしてもインプットするだけとは比べ物にならないくらい読み込むから、理解力つくし、実感としても成長を感じられますし、やっぱりアウトプットって大事ですね。って思い知りました。
実は記憶領域の開放とか確保とかどうのこうのする関係で、ポインタがかかわってくる部分があるんですが、まだ触れてないので、明日はそこちょっとやって、UE4触りたい!
ご意見・ご感想・ご質問、ここ違ってるよとか、もっといい方法あるよといったご指摘などございましたら、お手数ですがコメント欄やtwitterアカウントほろほろり(@_horo_horori)へお願いしますm(_ _)m