どうも。惑星直列の潮汐力を利用して自殺するにはまだ1兆年ほど待たないといけない&そこまで待つなら太陽の超新星爆発で死んだほうが現実的(?)だと思ったので、やっぱりオーソドックスな自殺方法を試そうとしている者です。
前回、「メモリの中は広大だ」では変数について解説しました。今回はいよいよC言語では難解と思われているポインタについて解説します。
なぜこんな記事を書いているのか
「メモリの中は広大だ」に続く形となりますが、変数やポインタ、特にポインタについてよくわからないという方が多いそうです。この理由は「その知識を理解するための前提となる知識が間違っているため。」、つまり、ポインタの理解の前提となる変数の知識が間違っているためなのですが、事もあろうに、大学の工学部の教員ですら理解を間違えている始末です。
じゃあ、人に頼らずネットで検索すればいいじゃないかと思うかもしれませんが、その方法は有効でした。 2008年までは。今やネットはまとめサイトやらNAVERやらQiitaやらなんかよくわからん金儲けのためのアフィブログのお陰でこういった有用な情報は塵となってどこかに消えてしまいました。なので、せめてものお慰みとして、まともな解説をQiitaに置いておきたいと考えたのです。
前回のおさらい
「メモリの中は広大だ」では次の事を解説しました:
- 変数は箱ではない
- プログラミングでの変数とは、メモリ上のどこかに、いくらかのサイズで、保存されている値に、名前が付いたもの
- 上記のために変数には最大値、最小値があって、それらは変数のサイズ(ビット数)に依存している
前回では、変数の「いくらかのサイズで」に関しては解説をしましたが、「メモリ上のどこかに」関する解説はしませんでした。つまり、今回はこの「メモリ上のどこかに」関する解説をします。
メモリアドレスについて
「メモリの中は広大だ」では、たとえ、C言語では比較的の大きな型である64ビットであっても、メモリ上の非常に小さな領域しか使用されない事を解説しました。言い換えれば、それだけメモリはとても広く、また多くの値を保存できます。しかし、多くの値を保存していくと、**どこにその値を保存したのか分からなくなってしまいます。**このため、メモリ上の領域を1バイト(8ビット)毎に区切って順番に数字を割り当てて管理しています。この順番に割り当てられた数字を、メモリアドレスと言います。
ポインタとは
ここまで理解できていれば、ポインタも直球で理解できると思います。というわけで、ポインタとは、メモリアドレスを値に持った変数です。言い換えれば、メモリ上のどこかに、いくらかのサイズで、メモリアドレスを値として保存されているものに名前が付いたものです。 注意すべき点として、ポインタは非常に多くの機能を持っていますが、**意味としては単なる変数です。**難しく考える必要はありません。
ポインタの使い方と機能
変数とは、メモリ上のどこかに、いくらかのサイズで、保存されている値に、名前が付いたものです。つまり、変数はメモリ上に存在する値、サイズ、そして、どのアドレスにその値が存在するのかという情報を持っています。ポインタはそのアドレスを値とした変数です。
それじゃ、どうやってポインタの値を宣言するのか・・・についてですが、例えばC言語の場合は次のように宣言します。
# include<stdio.h> //コンソールに入出力するためのヘッダファイルをビルド時に含める
# include<stdlib.h> //終了コードを利用するためを利用するために必要なヘッダファイルをビルド時に含める。
int main() {
int a = 0; // 取りあえず符号付き整数型変数としてaを宣言する
printf("Value of a: %d\n", a); // aの値を表示
printf("Address of a: 0x%08x\n", &a); // aのメモリアドレスを出力する
int *a_p = &a; // aのポインタとして、a_pを宣言する (1)
printf("Address of a_p: 0x%08x\n", a_p); // a_pの値、つまりaのメモリアドレスを出力する
(*a_p) = 1; // a_pの値(aのアドレス)の先の値、つまりaの値を書き換える
printf("Value of a: %d\n", a); // aの値を表示
return EXIT_SUCCESS; // アプリが正常に終了したことを示すために、EXIT_SUCCESSを返す
}
プロのコード書きはこのようなコメントはおろか、そもそも超絶綺麗なコードの書き方をマスターしているのでコメントは書きませんが、今回は初心者向けなので、一行ずつ丁寧に書きました。が、そんな事はさておき、注目すべき所は、(1)に関する部分です。つまり、int *a_p = &a;
です。ポインタは宣言する変数の名前の先頭に*
を加えることによって、ポインタを宣言することができます。また、宣言した後の変数の先頭に&
を付けることによって、その変数のアドレスを取得することができます。
で、どこで使うの?
って思った諸氏は素晴らしい感覚をお持ちだとお思います。というわけで次。
ポインタの使いどころ
変数の操作
さて問題です。あるエンジニアAは入力st
をnum
分、増加させる関数acc
を書きました。制約として、入力st
の値とnum
は符号付き32ビット整数値の最大値・最小値と同一とします。
void acc(int st, int num) {
int tmp = st;
st = tmp + num;
}
一応、中訳のほうでプロの人からの指摘が入らないように解説をしておきます1がそれはさておき、設計を終えたAは作成した関数を試すために下のコードのような簡易プロトタイプを書き加えました。main関数内の変数result
の値はどうなるでしょうか?
# include <stdio.h>
# include <stdlib.h>
void acc(int st, int num) {
int tmp = st;
st = tmp + num;
}
int main() {
int result = 50;
acc(result, 123456);
printf("%d\n", result);
return EXIT_SUCCESS;
}
普通の感覚だと、result
には123506が入っていると思うでしょう。関数acc
は仕様からして「入力st
をnum
分、増加させる関数」なのでその考えは正しいです。しかし、**その正しい考えが必ずしも書かれたコードに反映されているとは限りません。**つーわけで下が上のコードをビルドして実行した結果です:
[hyamamoto@hyamamoto-home (pts/1) ~]$ gcc -o main main.c
[hyamamoto@hyamamoto-home (pts/1) ~]$ ./main
50
はい。期待されている出力は123506ですが、不思議なことに0と出力されています。
さてなぜか
というわけで、なぜこうなっているのかを検証するため、次のようにコードを書き換えます。
# include <stdio.h>
# include <stdlib.h>
void acc(int st, int num) {
int tmp = st;
st = tmp + num;
printf("Address of st: 0x%08x\n", &st); // stのメモリアドレスを表示
}
int main() {
int result = 50;
acc(result, 123456);
printf("%d\n", result);
printf("Address of result: 0x%08x\n", &result); // resultのメモリアドレスを表示
return EXIT_SUCCESS;
}
メモリアドレスは、変数がメモリ上のどこに保存されたのかを示すものなので、仮にresultとstのメモリアドレスが異なっていれば、関数accはresultへではなく、どこか別の場所に保存しているという事になります。
というわけで、以下がコードをビルド&実行した結果です。
[hyamamoto@hyamamoto-home (pts/3) ~/study]$ gcc main.c
[hyamamoto@hyamamoto-home (pts/3) ~/study]$ ./a.out
Address of st: 0xa4873a7c
50
Address of result: 0xa4873aa4
**はい違っていますね。**accはちゃんとresultに値を保存していない。
どうすればresultを保存できるか
ここでポインタの出番ですよ! ポインタはメモリアドレスを値として持っている変数です。つまり、resultのメモリアドレスさえ分かれば、resultの値を参照したり、resultの値を書き換えたり出来るわけです。
というわけで、コードを次のように書き換えます。
# include <stdio.h>
# include <stdlib.h>
void acc(int *st, int num) {
int tmp = *st; // stの値が示している先の値を変数tmpに保存する
(*st) = tmp + num; // アドレスstに保存されている値を書き換える
printf("Address of st: 0x%08x\n", st); // stはメモリアドレスを値に持ってるのでこれを表示
}
int main() {
int result = 0;
acc(&result, 123456);
printf("%d\n", result);
printf("Address of result: 0x%08x\n", &result); // resultのメモリアドレスを表示
return EXIT_SUCCESS;
}
さあ、ビルドしてコードを実行してみましょう。
[hyamamoto@hyamamoto-home (pts/3) ~/study]$ gcc main.c
[hyamamoto@hyamamoto-home (pts/3) ~/study]$ ./a.out
Address of st: 0x77615174
123456
Address of result: 0x77615174
はい、変数result
のアドレスとst
の値、つまりst
に保存されているメモリアドレスが同じになり、result
の値が書き換えられている事が分かりました。めでたしめでたし。
おまけ? 頭がおかしくなるやつ
ちなみに、ポインタも変数なので・・・
# include<stdio.h>
# include<stdlib.h>
int main() {
int a = 0;
printf("Value of a: %d\n", a);
printf("Address of a: 0x%08x\n", &a);
int *a_p1 = &a; // aへのポインタ
int **a_p2 = &a_p1; //a_p1へのポインタ
int ***a_p3 = &a_p2; //a_p2へのポインタ
int ****a_p4 = &a_p3; //a_3へのポインタ
int *****a_p5 = &a_p4; //a_p4へのポインタ
int ******a_p6 = &a_p5; //a_p5へのポインタ
int *******a_p7 = &a_p6; //a_p6へのポインタ
int ********a_p8 = &a_p7; //a_p7へのポインタ
int *********a_p9 = &a_p8; //a_p8へのポインタ
int **********a_p10 = &a_p9; //a_p9へのポインタ
(**********a_p10) = 123456789; //a_p10から「遡るように」aの値を書き換える
printf("%08x %08x\n", &a, a_p1);
printf("%08x %08x\n", &a_p1, a_p2);
printf("%08x %08x\n", &a_p2, a_p3);
printf("%08x %08x\n", &a_p3, a_p4);
printf("%08x %08x\n", &a_p4, a_p5);
printf("%08x %08x\n", &a_p6, a_p7);
printf("%08x %08x\n", &a_p7, a_p8);
printf("%08x %08x\n", &a_p8, a_p9);
printf("%08x %08x\n", &a_p9, a_p10);
printf("Value of a: %d\n", a);
return EXIT_SUCCESS;
}
こんな事もできます(^q^)
いや、冗談じゃありませんよこんなコード!! 僕のような気が狂った人間でもこんなのは最早書きません。しかし、ポインタも変数なので、ポインタのメモリアドレスを値として持つポインタ、そのメモリアドレスを持つポインタ、といった、正にマトリョーシカの如く、ポインタを宣言する事が実際には可能です。
ちなみに、これをビルドして実行した結果は・・・
[hyamamoto@hyamamoto-home (pts/3) ~/study]$ gcc main.c
[hyamamoto@hyamamoto-home (pts/3) ~/study]$ ./a.out
Value of a: 0
Address of a: 0x866771f4
866771f4 866771f4
866771f8 866771f8
86677200 86677200
86677208 86677208
86677210 86677210
86677220 86677220
86677228 86677228
86677230 86677230
86677238 86677238
Value of a: 123456789
こうなってます。ここで読者の諸氏の中には鋭い考え方を持っていて、変数を連続で宣言するとそれらはメモリ上にどのような配置のされ方をするのか分かっちゃう人がいるかもしれません。それに関してはまたいつか、多分応用編みたいな形で書こうかと思います。
まとめ
今回は、次の事について解説しました
- ポインタとは何か
- ポインタの使い方
- ポインタの実際の利用用途
ところが、実はまだポインタには、配列(変数の列)や文字列と連携させたり、構造体のポインタ、共用体など、様々な、変態じみた使い方があります。このため、次の初心者向け解説記事では配列とポインタについて解説していこうと思います。
次回までの宿題っぽいもの
エンジニアBは次のコードを書きました。
# include <stdio.h>
# include <stdlib.h>
int main() {
char test[] = "a quick brown fox jumps over the lazy dog";
scanf("%s", test);
printf("%s\n", test);
return EXIT_SUCCESS;
}
さて、このコード、一見すると普通に見えますが、実は非常に危険な箇所があります。それはどこでしょうか?そしてなぜ危険なのでしょうか?
・・・解説は次回の初心者向けシリーズで
-
本当はこういった数値の増加には
+=
という演算子があって、この演算子を先の例で用いるとst += num;
となり、st = st + num
と等価ですが、今回はこのst = st + num
が分からないという人を考慮し、あえてtmp
という変数を宣言して演算を実行しています。 ↩