はじめに
C言語初心者ながら他人にポインタを教える場面がありましたが、うまく説明できませんでした…
なのであるかわからない次の機会のために自分なりにC言語のポインタについて初歩の初歩から考えていきます。
訂正・誤りや付加知識等あれば言っていただけると助かります。
本題の前に…
「pointer」の和訳
なんとなく解ると思いますが「(何かしらを)指すもの、指す人」というような意味です。
では、C言語における「ポインタ」は何を指しているのでしょうか?
メモリとかいう表
ポインタが何を指しているか気になるところではありますが、そのまえにメモリについて軽く触れておきましょう。
コンピュータの中にはメモリと呼ばれるアドレスと数値データのセットがズラッとならんだ大きな表があります。
ポインタは何を「指す」のか
一回間を置きましたが、C言語における「ポインタ」は何を「指す」のでしょうか?
ポインタはアドレスを指します。
もっと言うとポインタはアドレスを指すことで、その場所にあるデータについて操作できるようになります。
例えばさっきの表において、ポインタが1001を指す(保持している)とデータは23であることがわかったり、違う数値データに変更したりできます。
ポインタを利用する時に出てくるよくわからん記号
記号たちの投げやりな説明
- 謎の記号1つ目 : & (アンド)
- たぶん参考書などを最初から読み進めている人はscanf関数でお世話になっている謎の記号。
- 変数の前につけると、そのデータが格納されている場所(アドレス)を教えてくれます。
- 謎の記号2つ目 : * (アスタリスク)
- 乗算の演算子がいきなりわけのわからないタイミングで登場。
- ポインタを利用するときは以下の二つの機能を持っていると考えれば大丈夫だと思います。
-
- 機能-1 : この変数をポインタとして使います
- 変数の宣言時に付随させることで、お前(変数)はポインタだ!ってできます。強い。
- (例) int *pointer; char *poiner; など
- 機能-2 : このアドレスにあるデータを操作します
- アドレスの前につくと、その場所をいじる権利を手に入れられます。権力強すぎない??
- (例) *pointer;
記号を実際に使ってみる
説明はこのあたりにしておいて、実際に使ってみましょう。
& (アンド)を使ってみる
以下のコードを入力してみてください。
int hoge=10;
printf("%p\n",&hoge);
どんな値が出力されましたか?
おそらく0xから始まる数値が出たと思います。この0xは16進数であることを示します。
これはint型変数hogeが格納されているアドレスを示しています。
* (アスタリスク)を使ってみる
まず機能-1と機能-2を合わせて見ていきましょう。
int hoge=10;
int *p; //機能-1,pをポインタとして宣言する
p = &hoge; //pにアドレスをセットする
printf("1 -> %d\n",*p); //機能-2,アドレスpの場所を操作(データを取得)する
*p = 20; //機能-2,アドレスpの場所を操作(データを代入)する
printf("2 -> %d\n",*p);
出力は以下のようになります。
1 -> 10
2 -> 20
これを表にして考えてみましょう。
図中にある丸付きの数字の順に見ていきましょう。
-
int hoge=10;
-> 変数hoge
が宣言され、任意のアドレスを占有します。今回は仮に1001番地を占有したとします。 -
&hoge
->&
の機能で変数hoge
のアドレスを取得。 -
p = &hoge;
-> 取得したアドレス(&hoge)をポインタにセット。 -
*p
-> ポインタの示すアドレス(&hoge)の場所にあるデータを取得。 -
*p = 20;
-> ポインタの示すアドレス(&hoge)の場所にデータ(20)を代入。 といった感じです。
今度は機能-2の方を特に見ていきます。
int hoge=10;
printf("hoge's address : %p\n",&hoge);
printf("hoge is '%d'\n",*(&hoge));
自分の環境では出力は以下のようになりました。
hoge's address : 0x7fff31658004
hoge is '10'
このコードで新しい部分は*(&hoge)
の部分です。
先程、アスタリスクには「アドレスの前につくと、その場所のデータを操作できる」機能があると言いました。
そのため&hoge
は変数hogeのアドレスを示すため、その先頭にアスタリスクをつけると「変数hogeのデータを操作できる」ようになります。
ポインタの使い方
ポインタ型変数の宣言
宣言するときは、データ型 *変数名
またはデータ型* 変数名
という形を取ります。
複数の変数を宣言するときは`データ型* 変数名'の方は型がわかりずらくなるので注意が必要です。
int *a; //ポインタ
int* b; //これもポインタ
int *c1,c2; //c1はポインタだが、c2はポインタではない
int* d1,d2; //d1はポインタだが、d2はポインタではない
ポインタをいじる
宣言したポインタにアドレスを入れてみましょう。
ここでやることは普通の変数と大差ありません。
int a=10;
int *p;
p = &a;
以上です。ですがこれだけだとアドレスが入ったただの箱です。(まぁ変数はそんなものですが…)
ちゃんと「指し示す」機能を使いましょう。その機能を実現するのが「*」です。これを先頭につけて、
int a=10;
int *p;
p=&a;
*p=20; //*を先頭につける
printf("*p is '%d'\n",*p);
printf(" a is '%d'\n", a);
*p is '20'
a is '20'
「*」の権力を活用してアドレスpの中身を20に変えただけなのに、変数aの値も20になっています。
ここでどこかで貼ったような画像を、もう一回貼ります。
この5番の動作が終了するとどうなるでしょうか?
ポインタと〇〇
ポインタと引数
まず普通の変数を引数に取る関数の動作を見ていきます。
#include<stdio.h>
void func(int arg){
arg = 777;
printf("arg in func is '%d'\n", arg);
}
int main(void){
int a=0;
int val = a;
func(val);
printf("val is '%d'\n",val);
return 0;
}
arg in func is '777'
val is '0'
valとargの値の変遷としては以下のようになっています。
valは終始変わらず、argは孤立して関数内で変化しています。
また表における変遷のイメージはこんな感じです。
(画像作成中)
次に引数にポインタ型の変数を取る関数の動作を見ていきましょう。
#include<stdio.h>
void func(int *arg){
*arg = 777;
printf("*arg in func is '%d'\n", *arg);
}
int main(void){
int a=0;
int *ref = &a;
func(ref);
printf("ref is '%d'\n",*ref);
return 0;
}
*arg in func is '777'
ref is '777'
valとargの値の変遷としては以下のようになっています。
先ほどと異なり、valは関数func内のargと同じタイミングで同じ変化をしています。
また表における変遷のイメージはこんな感じです。
(画像作成中)
ポインタと配列
配列はint array[]={0,1,2};
のように配列を宣言して、array[0]
というように[]
を利用して配列の各要素にアクセスします。
ではこのarray
は何なのでしょうか? []
を取っ払ってみましょう。
int array[]={0,1,2};
printf("array is '%p'\n", array);
array is '0x7ffeba62f4d0'
変換指定子でバレバレですが、arrayはアドレスを示しています。
ということで(?)、*
や&
、さらには配列でいつも使っている[]
を使っていろいろしてみます。
char value='a';
char array[]={'0','1','2','3','4','5'};
char *p_char;
printf("1. *array -> '%c'\n",*array);
p_char = &value;
printf("2. p_char[0] -> '%c'\n", p_char[0]);
p_char = array;
printf("3. p_char[2] -> '%c'\n", p_char[2]);
1. *array -> '0'
2. p_char[0] -> 'a'
3. p_char[2] -> '2'
いろいろ出てきました。順番に見ていきましょう。
1つ目、array
に*
を付けたところ、0
が得られました。つまり*
の機能によって、array
の指すアドレスの場所にある値0
が得られたということです。0
が配列の先頭要素であるので、どうやらarray
は配列の先頭のアドレスを示しているようです。
配列とアドレスの関係を表の形で見てみましょう。
(画像作成中…)
ポインタと文字列
とりあえずコードを見てみましょう。
printf("\"str1\" is '%p'\n","str1");
printf("\"str2\" is '%p'\n","str2");
"str1" is '0x4005e4'
"str2" is '0x4005f9'
いつも出力するときになんとなく使っていた""
ですが、どうやらこれもアドレスを返してくるみたいです。
""
の機能としては各文字を文字コードに変換し、char型の配列に入れたあと、最後尾に0
を付け足します。
この0
は文字に直すと\n
で、NULL文字と言われます。文字列の終わりを示す文字ですね。
そして最後に作成した配列の先頭アドレスを戻してくる。といった感じになってます。
""
の動作をメモリの表で見るとこんな感じです。
(画像作成中…)
データ型をつける理由
char val_char;
char *p_char=&val_char;
printf("[p_char is %p] -> [p_char+1 is %p]\n", p_char, p_char+1);
int val_int;
int *p_int=&val_int;
printf("[p_int is %p] -> [p_int+1 is %p]\n", p_int, p_int+1);
[p_char is 0x7fffd35b6113] -> [p_char+1 is 0x7fffd35b6114]
[p_int is 0x7fffd35b6114] -> [p_int+1 is 0x7fffd35b6118]
この出力で見て欲しいのは左右のアドレスの差です。
char型の方では113から1増えて114になっています。しかし、int型の方では114から4増えて118になっています。
(以下随時追記します。)
問題 : 出力はどうなる??
以下にコードを示すのでその出力を考えてみてください。
問題1 *と&の機能 -1-
#include<stdio.h>
int main(void){
int a=20;
int *p = &a;
printf("%d\n",5*(*p));
return 0;
}
出力結果はこちら
100
問題2 *と&の機能 -2-
#include<stdio.h>
int main(void){
int a=20;
printf("%d\n",(*&a)/4);
return 0;
}
出力結果はこちら
5
問題3 ポインタと引数
#include<stdio.h>
void arg_ref(int *arg){
*arg = 10;
}
void arg_val(int arg){
arg = 100;
}
int main(void){
int a;
a=1;
int *ref = &a;
arg_ref(ref);
printf("a is '%3d', *ref is '%3d'\n", a, *ref);
a=1;
int val = a;
arg_val(val);
printf("a is '%3d', val is '%3d'\n", a, val);
return 0;
}
出力結果はこちら
a is ' 10', *ref is ' 10'
a is ' 1', val is ' 1'
問題4 ポインタと配列
#include<stdio.h>
int main(void){
int array[]={0,10,20,30,40,50};
int *p=array;
printf("*(array+2) -> %d\n", *(array+2));
printf("(array+1)[2] -> %d\n", (array+1)[2]);
printf("p[5] -> %d\n", p[5]);
printf("(p+1)[0] -> %d\n", (p+1)[0]);
return 0;
}
出力結果はこちら
*(array+2) -> 20
(array+1)[2] -> 30
p[5] -> 50
(p+1)[0] -> 10
問題5 ポインタと文字列
#include<stdio.h>
int main(void){
char *p=(char *)"hoge";
printf("%c\n",p[2]);
printf(p); putchar('\n');
printf(&p[1]); putchar('\n');
printf("%s\n","fuga");
printf("%s\n",&"fuga"[2]);
return 0;
}
出力結果はこちら
g
hoge
oge
fuga
ga
問題6 ポインタとデータ型
#include<stdio.h>
int main(void){
char *p_char = (char *)0x7FFF8000;
int *p_int = (int *)0x7FFF8000;
printf("p_char is '%p'\n", p_char);
printf("p_char+1 is '%p'\n", p_char+1);
printf("p_int is '%p'\n", p_int);
printf("p_int+1 is '%p'\n", p_int+1);
return 0;
}
出力結果はこちら
p_char is '0x7fff8000'
p_char+1 is '0x7fff8001'
p_int is '0x7fff8000'
p_int+1 is '0x7fff8004'