日記の練習です。
15章「ポインタ変数の仕組み」
15.2 「変数とメモリの関係」
15.2.3 複数の変数の番号
int 型変数のアドレスを表示する。
pointer_test.c
#include <stdio.h> int main(void) { int i1, i2, i3; printf("i1(%p)\n", &i1); printf("i2(%p)\n", &i2); printf("i3(%p)\n", &i3); return 0; }
compile & run
$ gcc -Wall -o pointer_test pointer_test.c $ ./pointer_test i1(0x7ffff82de8ac) i2(0x7ffff82de8b0) i3(0x7ffff82de8b4)
4バイト毎の連番になっている。 int 型変数のサイズは4バイト。
15.2.4 配列の番号
配列とその要素のアドレスを表示する。
pointer_array.c
#include <stdio.h> int main(void) { int array[10]; printf("array__(%p)\n", array); printf("array[0](%p)\n", &array[0]); printf("array[1](%p)\n", &array[1]); printf("array[2](%p)\n", &array[2]); return 0; }
compile & run
$ gcc -Wall -o pointer_array pointer_array.c $ ./pointer_array array__(0x7ffc3714c1a0) array[0](0x7ffc3714c1a0) array[1](0x7ffc3714c1a4) array[2](0x7ffc3714c1a8)
int 型配列の要素も4バイト毎の連番になっている。そして配列(`array` &演算子をつけていない)は配列の最初の要素と同じ番号になっている。
15.3「&付けが必要な変数の正体」
15.3.1 &付き変数の正体
& は変数のアドレスを表示するための演算子
- &演算子
- アドレス演算子
15.3.2 すべては値渡しである
引数という仕組みで関数に数値を渡すことができる。関数を呼び出すときに変数を指定した場合には、変数に記憶されている値が、呼び出された関数の実印数にコピーされる。引数で関数に渡されるデータは、すべて数値であるということ。これを「値渡し」と呼ぶ。
数値を受け取った関数では、それを元にいろいろ計算して、結果を戻り値として返します。
変数の中身を変更したい場合、 &演算子を使ってアドレスを求めてこのアドレスの数値を関数に渡します。関数でそのアドレスのメモリを書き換えてやれば呼び出し側の変数を書き換えられます。これが変数のアドレスを知る必要がある理由です。
15.4 アドレスを記憶する変数
15.4.1 ポインタという単語
「関数に変数のアドレスを渡すと変数の中身を変更できる」と説明しましたが、そのためには「アドレスの値を記憶することができる変数」が必要になります。それが「ポインタ変数」です。
世間ではアドレスを記憶する変数を「ポインタ」と呼んでいますが、これは本当はあまり正確な呼び方ではありません。「ポインタ」という呼び方は総称であり、正確には3種類に分かれています
- ポインタ型
- int 型、double 型と同じような型です。ただしポインタ型にはそれらと異なる特徴があります
- ポインタ値
- ポインタで扱える数値、要するにアドレスのことです。整数や実数といった数値の区別と同様に、ポインタ値という区別があります
- ポインタ変数
- ポインタ型で宣言されたポインタ値を記憶できる変数のことです。int 型の変数や double 型の変数と基本的には同じことです
15.4.2 ポインタ型
ポインタ型はほかの型から作り出される派生型である。ポインタ型はほかの型とポインタ型を合体させて作ります。
int 型とポインタ型を合体させると、 int へのポインタ型という型ができます。
それぞれの型によってサイズが異なるので、ポインタ型には、「どの型の変数のアドレスなのか」わかる必要あるということ。
「15.5.1 ポインタ変数の宣言」でその意味が示される。
15.4.3 ポインタ値
ポインタ値とはポインタ型の変数が記憶できる数値のことですが、これは、要するに、変数のアドレスの値のことです。
ポインタ値は整数値ですが、 int 型の整数値とは意味が全く異なります。目的が全く別なのです。
これ以上、あまり明確には言及されず終わり。
15.4.4 ポインタ変数
ポインタ変数とは、ポインタ型で宣言された実際の変数のことです。この変数には、その元となった型の変数のアドレスを自由に代入できます。その元となった型の変数のアドレスを自由に代入できます。さらに、記憶しているアドレスのメモリを読んだり書き換えたりすることができます。
ポインタ変数の役目は、記憶しているアドレスのメモリを操作することです。普段はポインタ変数(それぞれの型の「変数のアドレス」が収まっている変数)として振る舞っているのですが、メモリ操作が必要なときには普通の変数(それぞれの型の「メモリ」が収まっている変数)に変身する必要があるのです。
ポインタ変数モードと通常変数モードの2つのモードを備えており、必要に応じて、それぞれを切り替えて使えることができるのです。
- ポインタ変数モード
- 変数のアドレスの代入と、足し算引き算だけ
- 必要なのはアドレスの記憶だけ
- 通常変数モード
- 通常の変数とまったく同じに機能する
- 通常の変数と同様にさまざまな演算子を使って計算することが可能
- そのときに使われるメモリは、「ポインタ変数モード」で記憶したアドレスになる
15.5 ポインタ変数を使ってみる
15.5.1 ポインタ変数の宣言
p という名前の、「 int へのポインタ型の変数」を宣言する書き方。
int *p;
int* p;
「 int へのポインタ型の変数」である p という意味だと(「 int 型とポインタ型を合体させる」という意味だと)、2つ目の宣言の方がそれっぽいけど、下記のように2つ以上の変数を宣言するときに破綻する。
int* p1, p2;
だからこうする。
int *p1, *p2;
`*p` という変数に見えるけど、あくまでも「 int へのポインタ型の変数」という `p` である。
15.5.2 アドレスを代入する
pointer_sub.c
#include <stdio.h> int main(void) { int *p, i; p = &i; printf("p = %p\n", p); printf("&i = %p\n", &i); return 0; }
& 演算子を使って得られるアドレスはポインタ型。
$ gcc -Wall -o pointer_sub pointer_sub.c $ ./pointer_sub p = 0x7fffc1c13e4c &i = 0x7fffc1c13e4c
15.5.3 モードの切り替え
ポインタ変数 `p` に * 演算子(間接参照演算子)をつけて、通常変数モードに切り替えている。ポインタ変数の基本的な使い方は、
ポインタ変数モードのときに読み書きしたいメモリのアドレスを代入して、その後、通常変数モードに切り替えてそのメモリを操作する
#include <stdio.h> int main(void) { int *p, i; p = &i; *p = 10; printf("*p = %d\n", *p); printf("i = %d\n", i); return 0; }
$ gcc -Wall -o pointer_mode_change pointer_mode_change.c $ ./pointer_mode_change *p = 10 i = 10
変数 `i` と通常変数モードの `*p` はまったく同じメモリ領域を使っているということ。
奇っ怪な記号 `*`
`*` は3通りの意味をもっており混乱の原因となります。3つの区別について。
- 乗算演算子
- 掛け算
- 5 * 8
- 間接参照演算子
- ポインタ変数を「通常変数モード」にする演算子
- ポインタ変数を「宣言するときに使用する記号」
int *p
- これは「 int へのポインタ変数」である `p` を宣言している
間接参照演算子 `*` と、「宣言するときに使用する記号」`*` は、なんの関係もない、まったく別の記号!! C 言語!お前ってヤツは!
*1:苦しんで覚えるC言語 - 秀和システム あなたの学びをサポート! https://www.shuwasystem.co.jp/book/9784798030142.html
*2:苦しんで覚えるC言語 | MMGames | 工学 | Kindleストア | Amazon https://www.amazon.co.jp/dp/B07H2WH1F4