[C 言語] * と & の意味と使い方

CCS50

C 言語に出てくる * と & ってなんだろう?コンピュータサイエンスの講座を受けて、 * とかポインタとか間接参照について学んだことをメモ。

& の意味と使い方

C 言語に出てくる「 &変数」 は、「この変数のアドレスを取ってくる」という意味。
アドレスを printf() するときは、%pと書く。

使い方
printf("%p\n", &n);
int *p = &n;

 * の意味

* には 2 つの意味がある。

1. ポインタを作る。

「型 * 変数」で、「この変数はポインタです。」と宣言している。「型 * 」でポインタ型(というひとつの型の種類)と覚えたら良さそう。

ポインタには(値などが格納されている)アドレスが格納されている。

ポインタとは、メモリー上の住所(アドレス)を指し示すもの。そして、ポインタはこのアドレス先に導いてくれる。

2. このポインタが指し示すアドレスに行く(参照する)、そして値を操作することを可能にする。

この使い方をする * は、間接演算子 ( dereference operator )と呼ばれる。また、ポインタが持っているアドレスを介して値を参照することを間接参照 ( dereference )と呼ぶ。

書き方は、「型 * 変数」( * + 半角スペース + 変数 ) と書いても、「型 *変数」 ( * + 変数 )でもどちらでもいい。

* の使い方

ポインタをつくるとき

int n = 10;
int *p = NULL;	   // ➀
p = &n;		   // ➁
printf("%p\n", p); // ➂

出力結果   
0x7ffd2e22f384

① で p という名前のポインタを作る。 * の前の型 (ここでいう int ) は、変数 n の値の型を書く。
➁ で、ポインタ変数  p に、変数 n のアドレスをセットしている。int *p = &n; と書くのと同じ。
➂ でポインタが格納しているものを表示すると、 ( 変数 n の ) アドレスが表示される。 printf("%p\n", &n); と書くのと同じ。

上では、( 変数 ) n という名前のついた場所をポインタ変数 p に代入したが、名前なしの箱を作って、そのアドレスをポインタ変数に格納することもできる。
名前なしの箱は、 malloc() を使ってつくる。

int *x = malloc(sizeof(int));

<注意!>
ポインタを宣言して、すぐに何かを代入しない場合は NULL をセットしておくこと。 NULL ポインタとは何も指し示していない = アドレスがセットされていないということ。

× int *p;
〇 int *p = NULL;


ポインタに何もセットしていないと、わけのわからないアドレスが返ってくることがある。例えば、 paiza.io で以下を実行すると、なにか分からないアドレスが返ってくる。

int *p;
printf("%p\n", p);


// 出力結果
0x7ffd28e61c08

 ポインタに NULL をセットして実行すると、 (nil) が返ってくる。実行環境によっては segmentation fault となる。 nil はオブジェクトが NULL という意味。

NULL をセットしてあげないと、最悪プログラムがクラッシュする可能性があるらしい。

間接演算子としての使い方

* が間接演算子の場合、ポインターが指し示すところに行って、その値を操作することができる。

int n = 10;
int *p = NULL;	// ➀
p = &n;		// ➁
*p = 20;	// ➂
    
printf("%i\n", n);

出力結果   
20

① で p という名前のポインタを作る。
➁ で、ポインタ変数 p に、変数 n のアドレスをセットしている。
➂ で * は間接演算子と働いている。ポインタ変数  p は、変数 n のアドレスを持っているので、変数 n の場所にいって、そこに値 20 をセットしている。

➁ ではポインタ変数としての p だから * はつかない。 ➂ の  *  は演算子として記述されていて  ① にでてきた * とは意味が異なる。( 演算子と呼ばれているので、 + とか += みたいな役割と理解した。 )

使い方のまとめ

使い方とかいうか、 * の役割を図にしてみた。

* のイメージ図

間接参照の便利なところ

間接演算子を使って、参照先の値を操作できると何がいいのか?

例えば、以下のような変数の値を入れ替える関数を作ってみる。

#include <stdio.h>

void swap(int a, int b);

int main(void)
{
    int x = 1;
    int y = 2;

    printf("swap 前 : x = %i, y = %i\n", x, y);
    swap(x, y);
    printf("swap 後 : x = %i, y = %i\n", x, y);
}

void swap(int a, int b)
{
    int tmp = a;
    a = b;
    b = tmp;
}

出力結果
swap 前 : x = 1, y = 2
swap 後 : x = 1, y = 2

この swap 関数を使っても、 main() にある変数 xy の値は変わらない。
なぜなら、swap(int a, int b) の引数 a  と b に渡された値は xy の値のコピーだから。( 値だけを渡すことを値渡しと呼ぶ。 )

そして、変数 ab は処理が main() に戻ると消える。だから値を return もせずに x = b; ようなことはできない。

なぜかというと、スタックメモリが使われているから。 main() や自分で作った関数のローカル変数はスタックメモリ上に記憶される。
スタックメモリは、記憶する対象が、呼び出された順に下から積み上げられていく。呼び出された関数の処理が終了すると、そのメモリ領域は解放される。 LIFO ( Last In First Out ) 方式。

スタックメモリのイメージ図

本当は、 swap() 関数に変数 xy そのものを渡して、値を入れ替えたい。そんなときに、間接参照を使う。

int main(void)
{
    int x = 1;
    int y = 2;

    printf("swap 前 : x = %i, y = %i\n", x, y);
    swap(&x, &y);
    printf("swap 後 : x = %i, y = %i\n", x, y);
}

void swap(int *a, int *b)
{
    int tmp = *a;	// ➀
    *a = *b;		// ➁
    *b = tmp;		// ➂
}

出力結果   
swap 前 : x = 1, y = 2
swap 後 : x = 2, y = 1

swap() 関数の引数にはポインタを渡す。つまり、変数 xy のアドレスを渡している。( これはポインタ渡し、または参照渡しとも呼ばれる。) なので、 main()swap 関数を呼ぶときに swap(&x, &y); のように、 & を使って変数のアドレスを渡している。

swap 関数の中の ① で、変数 tmp に、ポインタ a が指し示すアドレスの中の値を代入する。
➁ で、ポインタ a が指し示すアドレスの中身を、ポインタ b が指し示すアドレスの中の値に書き換える。
➂ で、ポインタ b が指し示すアドレスの中身を、変数 tmp の値に書き換える。

Posted by Agopeanuts