# ぽいんた ## ポインタはどこから来たのか ### 1 変数とメモリ 変数とは、プログラムにおいて数値などを記憶させておくためのものでした。 ではプログラムを実際コンピュータで動かす時、コンピュータのどこに記憶していたでしょうか? 答えは「メモリ」です。今日はこのメモリについてもう少し学んでみましょう。 ### 2 CPU CPU(Central Processing Unit,中央処理装置)とはざっくり言うとコンピュータの脳みそ部分にあたる部品です。CPUはプログラムを実行したり数値を計算したりします。我々が勉強しているプログラムは主にCPUへの命令を書いているのです。 プログラムを実行する過程でCPUはメモリに数値を書き込んだり読み込んだりしています。 ### 3 メモリの操作 メモリはどのようなものでしょうか? 「大きさが1byteのロッカーが1列に並んだ」 とういうイメージが近いと思います。CPUはこのロッカーに数値を入れます。 C++で変数に数値を代入した時、「どこか」のロッカーに値を入れているのです。 この「どこか」とはどこのことでしょうか? C++ではロッカー(変数をロッカーとみる)に名前をつけることによって(変数宣言)、簡単にロッカーに値を入れたり(代入)、取り出したりすることが出来ました。 しかしメモリに名前がついているわけでは無いので、CPUはそんなことは出来ません。 一体どのようにするのでしょうか? 答えは「アドレス」です。 ロッカー1個1個に「アドレス」という非負整数がふられています。 コンパイルされた命令は 「~アドレスのロッカーに4を入れろ」 とか 「~アドレスのロッカーから数値を持ってこい」 みたいな命令となっています。 > #### 物理アドレスと仮想アドレス > この「アドレス」にも2種類あって物理アドレスと仮想アドレスがあります。 > プログラムで扱うのは仮想アドレスです。今は気にしなくて大丈夫です。 > 気になる人は調べてみてくだだい。 ### 4 ポインタの必要性 今までアドレスの話をしてきましたが、今までC++を書いてきてアドレスを意識したことはありましたか? もちろんありませんよね! じゃあ何故今更こんな話をするのでしょうか? それは必要な場面があるからです。 しかし、アセンブリのようにアドレスを生で扱うのは大変なので、C言語ではポインタと仕組みがあります。 > #### C言語の誕生 > C言語は1972年にアメリカのAT&Tベル研究所のデニス・リッチーが、当時アセンブリ言語で開発されていたUNIXを高級言語で書くために開発したものです。 > OSを書くということはコンピュータの仕組みを意識したプログラミングが必要になり、従ってアドレスを意識したプログラミングがどうしても必要になります。 ### 5 「場所」を管理すること   アドレスを使うことはデータの「場所」を管理するということです。 「場所」を管理することには、便利な点がたくさんあります。 それを説明するのはもう少し具体的な話をしてからにしましょう。 ## ポインタは何者か ### 1 アドレスを取得してみよう ここまですべて日本語で書いてきたので、皆さんいい加減C言語を読みたいですよね? まず、アドレスを取得してみましょう ``` cpp #include<cstdio> int main() { int a; printf("%d\n",&a);//変数aのアドレスを10進数で表示 printf("%p\n",&a);//変数aのアドレスを16進数で表示 return 0; } ``` これを動かしてみましょう。 Warningが出ますが、気にせず動かして下さい。 ##### 実行結果 ``` 791342260 0x7ffd2f2aecb4 ``` わけのわからない結果になりましたね。人によって出力が違っていると思います。 これは何をやっているかというと変数aのアドレスを表示したのです。アドレスはを取得するには &演算子を使います。 ``` &変数名 ``` でアドレスを取得することが出来ます。見ての通り&は1項演算子なので&と変数の間にはスペースを開けません。(しつこい) アドレスは整数値なので%dで十進数表示できます。しかし、アドレスは16進数で出力するのが一般的です。%pを使うと16進数で表示できます。C言語では16進数の先頭に0xをつけます。または10〜15をa〜fを使って表します。 先程、人によって出力が違っていると言いましたがそれは何故でしょう? それは、プログラムを実行する直前まで変数がどこに確保されるか決まっていないからです。 ### 2 アドレスの変数 アドレスを取得して表示するだけなら使いどころがないですね。 今までのように変数を使ってみましょう。 ``` cpp #include<cstdio> int main() { int a; int *p;//intのポインタ変数pを宣言 p = &a;//変数aのアドレスをintのポインタ変数pに代入 printf("%p\n",&a); printf("%p\n",p); return 0; } ``` ##### 実行結果 ``` 0x7ffd2f2aecb4 0x7ffd2f2aecb4 ``` 同じ数字が出力されていればOKです。 ポインタを入れるための変数を「ポインタ変数」と呼びます。 ポインタ変数を宣言するときは、変数名の前に*を書きます。   例えばint型変数のアドレスを入れるための変数ならば、 ``` cpp int *(変数名); ``` char型変数のアドレスを入れるための変数ならば、 ``` cpp char *(変数名); ``` と書きます。 普通の変数とポインタ変数を同時に宣言することも出来ます。 上のプログラムの変数宣言部分は ``` cpp int a,*p; ```` と書くことも出来ます。(わかりにくいので推奨はしません。) 変数名のに*がつくわけでは無いので注意してください。 ポインタ変数は型ごとにあります。 このように変数にアドレスを入れることが出来ます。 ### 3 アドレスの参照 ポインタ変数にアドレスを入れただけでは何も出来ないので、実際にアドレスの場所にアクセスしてみましょう。 ``` cpp #include<cstdio> int main() { int a; int *p;//intのポインタ変数pを宣言 p = &a;//変数aのアドレスをintのポインタ変数pに代入 a = 100; *p = *p + 2; printf("%d\n",a); printf("%d\n",*p);//pのアドレスを参照する。 return 0; } ``` ##### 実行結果 ``` 102 102 ``` ポインタ変数に * をつけると「そのアドレスに書いてある場所の変数のように動作しなさい」という意味になります。これを「参照する」といいます。また*を間接演算子と言います。 つまり、 ``` cpp a = 100; a = a + 2; printf("%d\n",a); ``` と書くのと ``` cpp *p = 100; *p = *p + 2; printf("%d\n",*p); ``` と書くのは同義です。 皆さん頭がこんがらがってきたのではないでしょうか。 一般的にポインタは難しいと言われていますが、その理由の一つが記号のわかりにくさです。*が色々な使われ方をしているのでわかりづらいのです。 ``` cpp a * b; //(1)乗算の意味 int *p;//(2)ポインタ変数の宣言の意味 *p; //(3)ポインタの参照(間接演算子) ``` の3つの意味があります。 頑張って区別が出来るようになってください。 > #### 多くの人に使われるなんて想定外? > 先程も書きましたが、C言語は元々UNIXを開発するために出来たと書きました。 > そう、UNIXを開発するためにデニス・リッチーが「自分のためにつくった」のです。 > 彼が自分で使えれば良かったのでC言語には不親切なところがいくつかあります。 ### 4 ポインタ演算 ポインタは演算を行うこともできます。(加算(+) 減算(-) のみ) 早速やってみましょう。 ``` cpp #include<cstdio> int main() { int *p; p = (int*)100; printf("%d\n",p); p = p + 2; printf("%d\n",p); return 0; } ``` Warningが出ますが、気にせず動かして下さい。 ##### 実行結果 ``` 100 108 ``` あれ? ``` 100 102 ``` じゃないの?と思ったと思います。それでは ``` cpp #include<cstdio> int main() { char *p; p = (char*)100; printf("%d\n",p); p = p + 2; printf("%d\n",p); return 0; } ``` を動かしてみましょう。すると ``` 100 102 ``` となると思います。なぜこのようなことが起こったのでしょうか。 それは int → 4byte char → 1byte だからです。 ポインタ変数に1を足すということは、変数1個分ずらすという意味なのです。 ### 5 ポインタ演算で配列に迫れ! 前回配列をやりましたね。次のプログラムを動かしてみましょう。 ``` cpp #include<cstdio> int main() { int array[10]; printf("array %d\n\n",array); for(int i = 0;i < 10;i++) { printf("&array[%d] %d\n",i,&array[i]); } return 0; } ``` Warningが出ますが、気にせず動かして下さい。 ##### 実行結果 ``` array 997728176 &array[0] 997728176 &array[1] 997728180 &array[2] 997728184 &array[3] 997728188 &array[4] 997728192 &array[5] 997728196 &array[6] 997728200 &array[7] 997728204 &array[8] 997728208 &array[9] 997728212 ``` いくつか気づくことがあると思います。 ``` ・ array と &array[0]が等しい ・ &array[i + 1] と &array[i] + 1 が等しい(ポインタ演算で) ``` 実はプログラム上で配列名だけを書くとその配列の最初の要素のアドレスという意味になります。 また、配列は必ず連続したアドレスで確保されます。 これを踏まえて次のプログラムを動かしてみましょう。 ```cpp #include<cstdio> int main() { int array[5] = {0,10,20,30,40}; for(int i = 0;i < 5;i++) { printf("%d\n",*(array + i)); } return 0; } ``` ##### 実行結果 ``` 0 10 20 30 40 ``` これは ```cpp #include<cstdio> int main() { int array[5] = {0,10,20,30,40}; for(int i = 0;i < 5;i++) { printf("%d\n",array[i]); } return 0; } ``` と書くのと一緒です。 実はC言語では ``` array[i] ``` と書くと ``` *(array + i) ``` と解釈されます。これで配列の添字が0から始まる謎がわかりましたね。 つまり、最初の要素からいくつずれているかが添字になるので0スタートなのです。 次のようなことも出来ます。 ```cpp #include<cstdio> int main() { int array[5] = {0,10,20,30,40}; int *p = array; for(int i = 0;i < 5;i++) { printf("%d\n",p[i]); } return 0; } ``` これは、 ``` p[i] ``` を ``` *(p + i) ``` と解釈させているだけです。 ## ポインタはどこへ行くのか ここまでポインタについて勉強してきましたが、その存在意義はまだわかっていないのでしょう。ここではそれを説明したいと思います。 ### 1 配列 配列を動かすのポインタが役に立っていることは先程の説明でわかったと思います。その他にも役立つことがあります。それは #### 配列の交換 です。 例えば長さが100の配列a,bがあったとします。2つを交換したいとしましょう。 要素をそれぞれ交換しなければならないので100回の交換が必要になります。 しかし配列の先頭要素へのポインタを持っておけば、それを交換するだけで済みます。 さらに2つの配列が違う大きさだった時を考えてみましょう。大きい方から小さい方へコピーすることは不可能なので新しく配列を用意しなければなりません。 これもポインタを使えばただの交換になりますね。 ### 2 参照渡し 関数に変数の中身を書換えてほしいとします。 しかし、関数には変数のコピーが渡されるのでそれは出来ません。 関数にポインタを渡し、関数が渡されたアドレスを参照することで変数の中身を書き換えることが出来ます。 ### 3 メモリの動的確保 まだ習っていませんが、プログラムの途中で新たにメモリ領域を確保したり開放したりする仕組みがあります。しかしこのような場合プログラムは新たに確保したメモリがどこにあるのか知らないのでポインタを利用する必要があります。 ### 4 スマートポインタ ポインタはセキュリティ的に問題を抱えています。特にメモリの解法忘れが頻繁に起こります。 それを解決するために最近スマートポインタというものが導入されました。 kyawa はあまり詳しくないので、yumcyawizに聞いて下さい。 ## 更に理解を深めるための問い ### 1 ポインタ型のサイズはいくらか またその理由も考察せよ sizeofを使って調べてみよう。 A. ポインタ型のサイズはすべて8byte (64bitマシンの場合) コンピュータ内で扱うのアドレスのサイズはすべて一緒だから。 ### 2 アドレスはすべて同じサイズの整数値であるはずなのにどうしてint * とかchar * などの複数の型があるかの理由を考察せよ A.参照する場所からどこまでを一つの変数として見るかが型によって違う。  また、ポインタ演算の挙動が型によって異なる。 ### 3 次のプログラムを考察せよ ```cpp #include<cstdio> int main() { unsigned long a = 0x100000000; unsigned long *pl; unsigned int *pi; pl = &a; pi = (unsigned int*)&a; printf("%lu %u\n",*pl,*pi); return 0; } ``` この問題は奥が深いです。 A. このプログラムの出力は ``` 4294967296 0 ``` となります。 plとpiにはどちらも同じアドレスを指していますが、plを参照したときは、そのアドレスから8byteの領域を見ますが、piを参照したときは、そのアドレスから4byteの領域をみます。x86アーキテクチャは[リトルエンディアン](http://www.ertl.jp/~takayuki/readings/info/no05.html)で整数を保持するので、piで参照した時、下4byteしか見ないので 0 という出力になります。 ### 4 二次元配列のポインタ演算はどうなっているのか考察せよ 行列の和を求めるプログラムをポインタ縛りで書いてみよう。 A. ### 5 ポインタ型変数同士の差をとり、その結果からどのような演算であるか推論してみよう。 A. ## おすすめの本 更に理解を深めるために以下の本を是非読んでみてください [プログラムはなぜ動くのか 第2版](https://www.amazon.co.jp/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%81%AF%E3%81%AA%E3%81%9C%E5%8B%95%E3%81%8F%E3%81%AE%E3%81%8B-%E7%AC%AC%EF%BC%92%E7%89%88-%E7%9F%A5%E3%81%A3%E3%81%A6%E3%81%8A%E3%81%8D%E3%81%9F%E3%81%84%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%81%AE%E5%9F%BA%E7%A4%8E%E7%9F%A5%E8%AD%98-%E7%9F%A2%E6%B2%A2%E4%B9%85%E9%9B%84/dp/4822283151) 今の知識で十分読むことが出来ます。世界が変わります [プログラミング言語C 第2版](https://www.amazon.co.jp/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9EC-%E7%AC%AC2%E7%89%88-ANSI%E8%A6%8F%E6%A0%BC%E6%BA%96%E6%8B%A0-B-W-%E3%82%AB%E3%83%BC%E3%83%8B%E3%83%8F%E3%83%B3/dp/4320026926) デニス・リッチーの書いた本 ちょっと難しい 翻訳がひど                いので英語が得意な人は原文で読んでもいいかも ## 演習問題 #### (問1) 次の式の中で同じ意味の組をすべて挙げて下さい。ただし、int array[10] が宣言されいているとします。また、ポインタ演算の結果が同じというのも、同じ意味ということにします。 - (a) array - (b) array[4] - (c\) array + 4 - (d) *array + 4 - (e) &(array + 4) - (f) array[1] + 3 - (g) &array[1] + 3 - (h) array[0] - (i) array[0] + 4 #### (問2) ```cpp //main関数は省略してます。 int A[3][3],B[3][3]; for(int i = 0;i < 3;i++)for(int j = 0;j < 3;j++)scanf("%d",&A[i][j]);//行列Aの入力 for(int i = 0;i < 3;i++)for(int j = 0;j < 3;j++)scanf("%d",&B[i][j]);//行列Bの入力 int C[3][3]; int *pa = &A[0][0]; int *pb = &B[0][0]; int *pc = &C[0][0]; for(int i = 0; i < 3; i++) { for(int j = 0; j < 3; j++) { C[i][j] = A[i][j] + B[i][j];//ここを書き換えてください } } ``` この中の ```cpp C[i][j] = A[i][j] + B[i][j]; ``` の部分をポインタ変数 pa,pb,pcとポインタ演算と間接演算子*を用いて同じ意味になるようにしてください。 ヒント: 配列int A[3][3]は A[0][0],A[0][1],A[0][2],A[1][0],A[1][1],A[1][2],A[2][0],A[2][1],A[2][2] の順番でアドレスが連続になっている。 int B[3][3]とint C[3][3]も同様。 #### (問3) (ポインタ値 - ポインタ値 ) について考察せよ ヒント: ```cpp #include<cstdio> int main() { int array[10]; printf("%ld\n",&array[10] - &array[0]); return 0; } ``` このコード動かしてみましょう。 #### (問4) ```cpp #include<cstdio> int main() { unsigned long a = 0x100000000; unsigned long *pl; unsigned int *pi; pl = &a; pi = (unsigned int*)&a; printf("%lu %u\n",*pl,*(pi + 1)); return 0; } ``` の実行結果を「実際にプログラムを動かすこと無く」求めよ。(リトルエンディアンのマシンで実行する場合) ## 演習問題の解説 #### 問1 a) 配列arrayの先頭要素へのアドレス(即ち、&array[0]) b) 配列arrayの5番目の要素(array[4]) c) 配列arrayの先頭からint型変数4つ分ずらしたところのアドレス(&array[0] + 4) d) *(array) + 4 と評価される(array[0] + 4) e) 括弧の中は変数でないのでアドレスは取ってこれない→エラー f) 配列arrayの2番目の要素に3を足す(array[1] + 3) g) 配列arrayの2番目の要素があるアドレスから更に変数3つ分ずらす。→先頭から1つずらして更に3つずらした場所→先頭から4つずらした場所(&array[0] + 4) h) 配列arrayの1番目の要素 i) 配列arrayの1番目の要素に4を足す(array[0] + 4) 以上より c)とg),d)とi) #### 問2 左の添字が1増える(iが1増える)→変数3つ分ずれる 右の添字が1増える(jが1増える)→変数1つ分ずれる これを踏まえると ```cpp *(pc + i * 3 + j) = *(pa + i * 3 + j) + *(pb + i * 3 + j); ``` が答え。 #### 問3 そのアドレスの間にいくつ変数があるか。 #### 問4 4294967296 1 <details> <summary>仕組み</summary> "0x100000000"は2進数で以下のように表せます。 `0000000000000000000000000000000100000000000000000000000000000000` これは8バイトの数値です。 plはunsigned long型のポインタなので間接演算子を用いると全8バイトの数値になります。 これは10進数で`4294967296`です。従って*plは`4294967296`と出力されます。 piはunsigned int型のポインタなので4バイト単位で数値が扱われます。 では、この2進数を4バイト2つに2分割しましょう。 `00000000000000000000000000000001` `00000000000000000000000000000000` 前側の数値は10進数で1です。 後ろ側の数値は10進数で0です。 `*(pi + 1)`はこの前側の数値を表すので1と出力されます。 ちなみに`*pi`はこの後ろ側の数値を表します。 リトルエンディアンだと前側のアドレスのほうが大きく、ビッグエンディアンの場合は後ろ側のアドレスのほうが大きくなります。 </details>