C言語の航海日誌(4)

参考書の通読や写経も続けていますが、今日はちょっと寄り道というか予習というか、そんな感じでポインタの自習を。

たとえば、こんなコードがあったとして。

fruits.c
#include <stdio.h>

int main(void)
{
    int apple = 3;
    int lemon = 8;

    printf("リンゴ: %d\n", apple);
    printf("レモン: %d\n", lemon);

    return 0;
}

こんな結果になる。

リンゴ: 3
レモン: 8

はい。

で、今回のテーマであるポインタ、というのはどういう時に使うかっていうと、同じ値を複数の関数の間で渡したりするときに使う。という認識が僕にはあるので、上記をポインタの練習用にアレンジすると、たとえばこんな。

fruits_practice.c
#include <stdio.h>

void practice(int *apl, int *lmn)
{
    printf("リンゴ: %p\n", apl);
    printf("レモン: %p\n", lmn);

    printf("*リンゴ: %d\n", *apl);
    printf("*レモン: %d\n", *lmn);
}

int main(void)
{
    int apple = 3;
    int lemon = 8;

    practice(&apple, &lemon);

    return 0;
}

結果。

リンゴ: 0x7fff54bad4f8
レモン: 0x7fff54bad4f4

*リンゴ: 3
*レモン: 8

はい。やってることとしては、main関数のほかにpractice関数というのを作って、そっちに「&apple, &lemon」という形で、appleと lemonのアドレスを渡している。
「アドレス」というのは、ここでは「0x7fff54bad4f8」とか出ている、そういうやつ。

ちなみに、ポインタの練習といえば通常、そのアドレスを渡した先(ここではpractice関数)で数値を入れ替えてみたりするわけだけど、この段階ではまだやらない。
まずはそれ以前の確認をきちんとしたいから。

さて、ポインタ関連の説明に触れ始めた頃、こうした構造というか状況を見て少し混乱したのは、このpractice関数のほうで引数を受け取る(=変数を宣言する)ときに、

void practice(int *apl, int *lmn)

というふうにしているんだけど、この「*apl, *lmn」というのはその後のプリントで「3, 8」と出ていることからもわかるように、アドレスではなく数値というか、値である。

つまり、main関数から投げた段階ではアドレスが示されているのに、それを受け取ったときにはアドレスではない「値」が入っているようなので、なぜ&どこで、それが変わってるの? という疑問を持った。
直感的には、受け取る側のほうでも、送る側と同じ「アドレス」をもらっているように見えると腑に落ちやすいのだけど。

具体的にいうと、現在このようになっているところが、

void practice(int *apl, int *lmn)
{
    printf("リンゴ: %p\n", apl);
    printf("レモン: %p\n", lmn);

    printf("*リンゴ: %d\n", *apl);
    printf("*レモン: %d\n", *lmn);
}

こんなふうになっている、とか。

void practice(int &apl, int &lmn) // <= & でアドレスを受け取る
{
    printf("リンゴ: %p\n", &apl); // <= & でアドレスを表示
    printf("レモン: %p\n", &lmn);

    printf("*リンゴ: %d\n", apl); // <= 無印で値を表示
    printf("*レモン: %d\n", lmn);
}

あるいは、こんな。

void practice(int apl, int lmn) // <= 無印でアドレスを受け取る
{
    printf("リンゴ: %p\n", apl); // <= 無印 でアドレスを表示
    printf("レモン: %p\n", lmn);

    printf("*リンゴ: %d\n", *apl); // <= * で値を表示
    printf("*レモン: %d\n", *lmn);
}

しかし実際はこうなっている。

void practice(int *apl, int *lmn) // <= * で値を受け取る
{
    printf("リンゴ: %p\n", apl); // <= 無印 でアドレスを表示
    printf("レモン: %p\n", lmn);

    printf("*リンゴ: %d\n", *apl); // <= * で値を表示
    printf("*レモン: %d\n", *lmn);
}

最後の(そして実際の)状態でも受け取った後の整合性は取れているのだけど、受け取る瞬間がなんか「ん〜?」という感じではある。

ただし、確かに上の素朴な代案にしてもやはり問題はあって、最初の代案だとポインタ型の変数と通常のint型の変数との違いが見てわからないし、後者の案だと受け取っている段階の見え方がこれまた通常のint型の変数(仮引数)の宣言とまったく変わらないので、それはそれで問題ありそう。

まあこれはこれで妥当な落としどころなのかなあ、と思うけれど……。(とくにオチや結論はない)

さて元の例(fruits_practice.c)に戻ってもう少し理解の確認を進めたい。
このコードでは、まず二つある関数のうち下のmain関数のほうで、「apple」に3、「lemon」に8を代入している。

そして、それらをpractice関数に投げるときに、アドレス「&apple, &lemon」に切り替えて渡している。

さらに、それらを受け取る側では、「int *apl, int *lmn」というふうに受け取っていて、このときに「*apl」と「*lmn」の中に入っているのは「0x7fff54bad4f8」とかのアドレスではなくて、「3, 5」という値が入ってる。

じゃあmain関数から投げたアドレスはどこ行ったの? と言うと、それは「apl, lmn」に入ってる。

それがわかるのは以下の行で、

    printf("リンゴ: %p\n", apl);
    printf("レモン: %p\n", lmn);

実行するとこのように出ている。

リンゴ: 0x7fff54bad4f8
レモン: 0x7fff54bad4f4

%pがポインタ型の変数(ここではapl, lmn)のアドレスを示してくれる。

Perlとの比較

ここまでのことを踏まえつつ、ぼくが思い浮かべるのはやはりPerlのリファレンスで、それと重ねながら考えてみると、ぼくはPerlのリファレンスっていうのは、複数のファイルをひとつのzipに圧縮するとか、あるいは海外旅行へ行くときの膨大な荷物を旅行カバンにパッキングすることなんかに似ていると思っている。

たとえば、以下のような配列があったとして、

my @fruits = qw/apple orange grape lemon/;

これを目的のサブルーチンへ、参照渡しで(値のコピーではなく値の現物ごと)渡そうと思ったら、配列のままでは渡せないので、一旦リファレンスにしてから渡す。

fruits.pl
#!/usr/bin/env perl
use strict;
use warnings;
use feature 'say';

my @fruits = qw/apple orange grape lemon/;

say $fruits[0]; #=> apple

foo(\@fruits);

sub foo {
    my $bar = shift;
    $bar->[0] = 'I ate an apple!';
}

say $fruits[0]; #=> I ate an apple!

この中の

foo(\@fruits);

というのが、ぼくのイメージで言うと、フルーツの詰め合わせを真空パックして外からは触れられないようにみっちり梱包して宅配に出す、みたいな感覚。あるいは上記の旅行カバンに詰めるとか、zipに固める、というイメージ。

で、サブルーチン foo の

    my $bar = shift;

でその梱包された荷物を受け取って、その中のリンゴを食べたっていうのが上のコードの話です。

じゃあ、同じことをCのポインタでやろうとしたらどうなるか、と想像してみると、ん〜、こんな感じか?

fruits_eat.c
#include <stdio.h>

void eat(int *apl, int *lmn)
{
    *apl -= 2;
    *lmn -= 4;
}

int main(void)
{
    int apple = 3;
    int lemon = 8;

    eat(&apple, &lemon);

    printf("リンゴ: %d\n", apple);
    printf("レモン: %d\n", lemon);

    return 0;
}

実行。

リンゴ: 1
レモン: 4

ふむ。最初にmain関数からリンゴを3個、レモンを8個送る。で、それを受け取ったeat関数のほうでそれぞれ2個と4個食べて、残りがリンゴ1個とレモン4個になりました、というコードですが。

こうして考えると、送付元でやっていることは、Perlならリファレンス化、Cなら「&」を付けてアドレス化。
そして受け取り側でやることは、Perlなら「->」とか「@$foo」とか「%$bar」とかでデリファレンスして操作することだけど、Cなら「*」を付けて操作する、ということかな。

なるほど。ぼくはCの場合、「*」を付けることで「アドレス」を「値」に戻すのかと思っていたけど、「*」は値そのものに戻す(コピーする)のではなく、デリファレンスした状態で中身をいじる、といったことだと思えば腑に落ちやすいかもしれない。
言い換えると、あくまでアドレスを経由した操作である、というような。

ちなみに、今参考にしている以下の本によれば、

C言語体当たり学習 徹底入門 (標準プログラマーズライブラリ)

C言語体当たり学習 徹底入門 (標準プログラマーズライブラリ)

「&」は「アドレス演算子」、「*」は「間接参照演算子」と言うらしく、なるほどここまで考えてみると、確かにそんな感じかなという気もしてくる。

まあ上記のように、Perlでは送付元から送ったリファレンスを、受け取り側でもリファレンスとして受け取るのに対して、Cでは送付元からアドレスを送って、しかし受け取り側ではそれを(いわばデリファレンスした後の)ポインタ型の変数で受け取るわけで、その若干ながら確かなズレがちょっと、まだ結構気にかかってはいますが。

ということで、大変煩雑な感じの内容になりましたが、自分的にはそれなりの理解を得られたかなという気もします。

今後の予告としては、構造体、共用体、malloc() などの写経にそろそろ入ってくるので、それらを理解することが楽しみです。

現時点ではまだ、上のサンプルコードとか、あるいはポインタ界のお約束コードことswap関数ぐらいしか書けないので、もう少しCならではのコードで出来ることを増やしていきたいところです。