LoginSignup
1
1

More than 5 years have passed since last update.

初心者がポインタをテキトーに考える[C言語]

Last updated at Posted at 2018-06-26

はじめに

C言語初心者ながら他人にポインタを教える場面がありましたが、うまく説明できませんでした…
なのであるかわからない次の機会のために自分なりにC言語のポインタについて初歩の初歩から考えていきます。
訂正・誤りや付加知識等あれば言っていただけると助かります。

本題の前に…

「pointer」の和訳

なんとなく解ると思いますが「(何かしらを)指すもの、指す人」というような意味です。
pointer.jpg

では、C言語における「ポインタ」は何を指しているのでしょうか?

メモリとかいう表

ポインタが何を指しているか気になるところではありますが、そのまえにメモリについて軽く触れておきましょう。
コンピュータの中にはメモリと呼ばれるアドレスと数値データのセットがズラッとならんだ大きな表があります。
table_normal.jpg

他の何かに例えると…?

[アドレスとデータ]の関係は[住所と住宅]だったり[出席番号と生徒]の関係などに例えられたりします。
ですがここでは、アドレスをロッカーの番号データをロッカーの中身に置き換えて考えていきます。
さっきの表みたいに書くとこんな感じになります。
rocker_normal.jpg

ロッカーの番号が分かると、その番号のロッカーの中身が分かります。
同じように、アドレスが分かると、そのアドレスにある数値データが分かります。

その逆は必ずしも成り立つとは言えません。
例えばロッカーの中身が靴だとわかっても1番のロッカーか1001番のロッカーかわかりません.

ポインタは何を「指す」のか

一回間を置きましたが、C言語における「ポインタ」は何を「指す」のでしょうか?
ポインタはアドレスを指します
もっと言うとポインタはアドレスを指すことで、その場所にあるデータについて操作できるようになります。

例えばさっきの表において、ポインタが1001を指す(保持している)とデータは23であることがわかったり、違う数値データに変更したりできます。
table_pointer.jpg


他の何かに例えると…?

先ほどと同じようにロッカーに置き換えて考えると、ポインタはロッカーの鍵と例えられるでしょうか。
例えばロッカーの鍵が1001番のものであれば、その中身(靴)を見ることもできますし、それを取っちゃうこともできます。
もちろん空いた1001番のロッカーに別の何かを入れることもできます。
つまり、鍵を手にすることで1001番ロッカーを自由にすることができるのです。
rocker_pointer.jpg


ポインタを利用する時に出てくるよくわからん記号

記号たちの投げやりな説明

謎の記号1つ目 : & (アンド)
たぶん参考書などを最初から読み進めている人はscanf関数でお世話になっている謎の記号。
変数の前につけると、そのデータが格納されている場所(アドレス)を教えてくれます。


さっきの表に戻って考えてみると…

table_and.jpg
&を変数の前につけることでそのアドレスが得られますが、アドレスを変更することはできません


謎の記号2つ目 : * (アスタリスク)
乗算の演算子がいきなりわけのわからないタイミングで登場。
ポインタを利用するときは以下の二つの機能を持っていると考えれば大丈夫だと思います。
機能-1 : この変数をポインタとして使います
変数の宣言時に付随させることで、お前(変数)はポインタだ!ってできます。強い。
(例) int *pointer;  char *poiner;   など

機能-2 : このアドレスにあるデータを操作します
アドレスの前につくと、その場所をいじる権利を手に入れられます。権力強すぎない??
(例) *pointer;

記号を実際に使ってみる

説明はこのあたりにしておいて、実際に使ってみましょう。

& (アンド)を使ってみる

以下のコードを入力してみてください。

sample_and.c
int hoge=10;
printf("%p\n",&hoge);

どんな値が出力されましたか?
おそらく0xから始まる数値が出たと思います。この0xは16進数であることを示します。
これはint型変数hogeが格納されているアドレスを示しています。

* (アスタリスク)を使ってみる

まず機能-1と機能-2を合わせて見ていきましょう。

sample_ast_1.c
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);

出力は以下のようになります。

output
1 -> 10
2 -> 20

これを表にして考えてみましょう。
table_ast_1.jpg
図中にある丸付きの数字の順に見ていきましょう。

  1. int hoge=10; -> 変数hogeが宣言され、任意のアドレスを占有します。今回は仮に1001番地を占有したとします。
  2. &hoge -> &の機能で変数hogeのアドレスを取得。
  3. p = &hoge; -> 取得したアドレス(&hoge)をポインタにセット。
  4. *p -> ポインタの示すアドレス(&hoge)の場所にあるデータを取得。
  5. *p = 20; -> ポインタの示すアドレス(&hoge)の場所にデータ(20)を代入。 といった感じです。

今度は機能-2の方を特に見ていきます。

sample_ast_2.c
int hoge=10;
printf("hoge's address : %p\n",&hoge);
printf("hoge is '%d'\n",*(&hoge));

自分の環境では出力は以下のようになりました。

output
hoge's address : 0x7fff31658004
hoge is '10'

このコードで新しい部分は*(&hoge)の部分です。
先程、アスタリスクには「アドレスの前につくと、その場所のデータを操作できる」機能があると言いました。
そのため&hogeは変数hogeのアドレスを示すため、その先頭にアスタリスクをつけると「変数hogeのデータを操作できる」ようになります。

ポインタの使い方

ポインタ型変数の宣言

宣言するときは、データ型 *変数名またはデータ型* 変数名という形を取ります。
複数の変数を宣言するときは`データ型* 変数名'の方は型がわかりずらくなるので注意が必要です。

declare_pointer.c
int *a;     //ポインタ
int* b;     //これもポインタ
int *c1,c2; //c1はポインタだが、c2はポインタではない
int* d1,d2; //d1はポインタだが、d2はポインタではない

ポインタをいじる

宣言したポインタにアドレスを入れてみましょう。
ここでやることは普通の変数と大差ありません。

ctrl_pointer.c
int a=10;
int *p;
p = &a;

以上です。ですがこれだけだとアドレスが入ったただの箱です。(まぁ変数はそんなものですが…)
ちゃんと「指し示す」機能を使いましょう。その機能を実現するのが「*」です。これを先頭につけて、

ctrl_pointer.c
int a=10;
int *p;
p=&a;
*p=20;      //*を先頭につける
printf("*p is '%d'\n",*p);
printf(" a is '%d'\n", a);
output
*p is '20'
 a is '20'

「*」の権力を活用してアドレスpの中身を20に変えただけなのに、変数aの値も20になっています。
ここでどこかで貼ったような画像を、もう一回貼ります。
table_usage_1.jpg

この5番の動作が終了するとどうなるでしょうか?

table_usage_2.jpg
ってことです。

ポインタと〇〇

ポインタと引数

まず普通の変数を引数に取る関数の動作を見ていきます。

val.c
#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;
}
output
arg in func is '777'
val is '0'

valとargの値の変遷としては以下のようになっています。
valは終始変わらず、argは孤立して関数内で変化しています。
また表における変遷のイメージはこんな感じです。
(画像作成中)

次に引数にポインタ型の変数を取る関数の動作を見ていきましょう。

ref.c
#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;
}
ans_3
*arg in func is '777'
ref is '777'

valとargの値の変遷としては以下のようになっています。
先ほどと異なり、valは関数func内のargと同じタイミングで同じ変化をしています。
また表における変遷のイメージはこんな感じです。
(画像作成中)

ポインタと配列

配列はint array[]={0,1,2};のように配列を宣言して、array[0]というように[]を利用して配列の各要素にアクセスします。
ではこのarrayは何なのでしょうか? []を取っ払ってみましょう。

what_is_array.c
int array[]={0,1,2};
printf("array is '%p'\n", array);
output
array is '0x7ffeba62f4d0'

変換指定子でバレバレですが、arrayはアドレスを示しています。

ということで(?)、*&、さらには配列でいつも使っている[]を使っていろいろしてみます。

play_array.c
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]);
output
1. *array    -> '0'
2. p_char[0] -> 'a'
3. p_char[2] -> '2'

いろいろ出てきました。順番に見ていきましょう。
1つ目、array*を付けたところ、0が得られました。つまり*の機能によって、arrayの指すアドレスの場所にある値0が得られたということです。0が配列の先頭要素であるので、どうやらarrayは配列の先頭のアドレスを示しているようです。

配列とアドレスの関係を表の形で見てみましょう。
(画像作成中…)

ポインタと文字列

とりあえずコードを見てみましょう。

ctrl_pointer.c
printf("\"str1\" is '%p'\n","str1");
printf("\"str2\" is '%p'\n","str2");
output
"str1" is '0x4005e4'
"str2" is '0x4005f9'

いつも出力するときになんとなく使っていた""ですが、どうやらこれもアドレスを返してくるみたいです。
""の機能としては各文字を文字コードに変換し、char型の配列に入れたあと、最後尾に0を付け足します。
この0は文字に直すと\nで、NULL文字と言われます。文字列の終わりを示す文字ですね。
そして最後に作成した配列の先頭アドレスを戻してくる。といった感じになってます。
""の動作をメモリの表で見るとこんな感じです。
(画像作成中…)

データ型をつける理由

ctrl_pointer.c
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);
output
[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-

ques_1.c
#include<stdio.h>

int main(void){
  int a=20;
  int *p = &a;
  printf("%d\n",5*(*p));

  return 0;
}


出力結果はこちら
ans_1
100

問題2 *と&の機能 -2-

ques_2.c
#include<stdio.h>

int main(void){
  int a=20;
  printf("%d\n",(*&a)/4);

  return 0;
}


出力結果はこちら
ans_2
5


問題3 ポインタと引数

ques_3.c
#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;
}


出力結果はこちら
ans_3
a is ' 10', *ref is ' 10'
a is '  1',  val is '  1'


問題4 ポインタと配列

ques_4.c
#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;
}


出力結果はこちら
ans_4
*(array+2)   -> 20
(array+1)[2] -> 30
p[5]         -> 50
(p+1)[0]     -> 10


問題5 ポインタと文字列

ques_5.c
#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;
}


出力結果はこちら
ans_5
g
hoge
oge
fuga
ga


問題6 ポインタとデータ型

ques_6.c
#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;
}


出力結果はこちら
ans_6
p_char   is '0x7fff8000'
p_char+1 is '0x7fff8001'
p_int    is '0x7fff8000'
p_int+1  is '0x7fff8004'


1
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1