娘のためにその11:ポインタ[40分]

娘に読ませる以外の意図はなく、よって質問や指摘には対応していません。すなわちネットの浄化作用が働いていない不正確な内容になりますので、正しい情報を求める方は閲覧をご遠慮ください。公開しているのは、通信手段としての利便性のためです。

いよいよポインタの説明をするときが来た。ポインタは難しいとよく言われるが、ここまでの話を理解していれば決して難しくはないはずだ。また、プログラミング言語は無数にあるが、ポインタを意識させない(裏でうまいことやってくれる)言語はたくさんある。むしろC言語特有と言ってもいいかもしれない。だが、やはりポインタは重要なのだ。きちんと理解した上で、それを使わなくて済む言語ではその利便性を教授する、「なんと便利な!」と実感するのが正しい。すべてのプログラムがメモリを相手にしていることは変わらないのだから。

前回、構造体の話をした。娘のためにその10:構造体

struct MyVector {
    float x;
    float y;
};

例えば上のように構造体を宣言したとしよう。そして関数 func が MyVector の定数倍(スカラー倍)を計算するとしよう。

struct MyVector {
    float x;
    float y;
};

MyVector func(MyVector v, float value)
{
    MyVector r;
    r.x *= value;
    r.y *= value;
    return r;
}

void setup()
{
    Serial.begin(9600);
    MyVector v;
    v.x = 10;
    v.y = 20;
    MyVector v2 = func(v, 100);
    Serial.println(v2.x);
    Serial.println(v2.y);
}

void loop(){}

func の定義を見ればわかるように、func は引数 v の x,y 各要素に引数 value を乗算した結果の MyVector を返す。結果、呼び出しているほうの v2.x には 1000, v2.y には 2000 が格納されるのが実行により確認できるだろう。実際に実行してプログラムの動作を確認してみよう。

余談だが、サンプルプログラムの実行はコピペでも良いが、たまに手で打ち込むと良い。キーボードのブラインドタッチは本質的ではないけれど、手元を見ずに打てるようになると思考の妨げにならないぶん、ものすごく有利になる。例えるなら、目の悪い人がメガネをかけるかかけないか、ぐらいの差があるので、意図的に練習するようにしよう。なおタイピングの練習をするときに大事なのは、 !"#$;:@[] などの記号も下を見ずに打てるようになることだ。タイピングゲームで遊ぶのも良いが、アルファベットだけの訓練にならないよう気をつけよう。

さて、ここの MyVector v や MyVector v2 にはスタックメモリが使用されていることは以前述べたとおりだ。v と v2 、ふたつの値でスタックメモリを消費しているわけだ。ここで取得したいのはベクトルの定数倍だった。では「引数で渡す v の中身を直接変更する関数」は作れないのだろうか?

うん、「引数で渡す v の中身を直接変更する関数」の意味がわかりにくいと思う。例えば、MyVector が float 二つで 8byte のうちはまだいい。が、ものすごく巨大であることもあり得るのだ。そうすると、いくつもの MyVector を生成しながら演算するのは大きな処理の無駄になる場合がある。よって、たったひとつの MyVector だけを用意して、それを定数倍するプログラムを書きたいことがある、というわけだ。

それを実現するには、v が確保しているスタックメモリのアドレスを関数に渡すことだ。すべてのメモリにはアドレスがあるのだから、関数には何バイトもある巨大な型を渡す代わりにアドレスを渡し、アドレスが指し示す中身を変更するプログラムを記述する。これは次のプログラムのようになる。ポインタの登場だ。

struct MyVector {
    float x;
    float y;
};

void func(MyVector* v, float value)
{
    v->x *= value;
    v->y *= value;
}

void setup()
{
    Serial.begin(9600);
    MyVector v;
    v.x = 10;
    v.y = 20;
    func(&v, 100);
    Serial.println(v.x);
    Serial.println(v.y);
}

void loop(){}

funcは引数の MyVector の各要素に value を乗算するところは前回と変わらない。異なるのは、ポインタを使用して「引数のアドレスが指し示す先」に計算結果を格納しているということだ。

順番に確認していこう。setup で func を呼び出すところ。

    MyVector v;
    v.x = 10;
    v.y = 20;
    func(&v, 100);

func(&v, 100) のように、変数v の前に &(アンパサンド)をつけている。C言語ではこれで「変数が格納されているメモリのアドレス」を得ることができる。

void func(MyVector* v, float value)
{
    v->x *= value;
    v->y *= value;
}

func のほうの引数にも変化がある。void func(MyVector* v のように、MyVector の後ろに * (アスタリスク)がついている。MyVector と MyVector* は別の型で、* がついているほうはポインタと呼ばれる。要するにアドレスだ。

さらに変化しているのはアクセス方法だ。v.x *= value だったのが、v->x *= value になっている。-> は、ポインタの先のメモリの要素にアクセスする、という意味になる。この記述の分かり難さがポインタの障壁だと思うのだが、憶えるしかない。

話の進行が早すぎた。改めて、ポインタを解説する。

変数はメモリに格納される、そのメモリのアドレスを「型」として扱えるようにするのがポインタだ。

int a = 10;

この記述により、変数 a に 10 を格納した。数byte のスタックメモリが確保され、そのメモリに 10 が書き込まれたわけだ。

int a = 10;
int* b;

次に int* b; で int のポインタ型で b を宣言した。int* は「int が格納されているメモリのアドレスを格納できる型だ。ちなみにC言語の技術書などでは

int *b;

のように変数のほうに * をくっつけて書くように解説している本が多い。どちらでも良いのだが、「型」とイメージするには int* と型としてまとめたほうが圧倒的に理解しやすい。( int *b; と書く主義の人たちにも言いぶんはある)

int a = 10;
int* b = &a;

int* というポインタ型をもつ b に、 a が格納されているメモリのアドレスを格納したい場合はこう書く。a は中身の 10 を表すが、 &a とすればアドレスを表すのだ。

int a = 10;
int* b = &a;
Serial.println(*b);

b というアドレスがあった場合、そのアドレスに格納されている内容を取得するには *b と、変数の前に * (アスタリスク)をつける。これで、コンソールには 10 が出力される。

理解できただろうか。混乱するかもしれないが、面倒なのは単に記述の複雑さだ。メモリやアドレスのことを意識できれば、決して難しくはない。とはいえ、私も初めてメモリの講釈を友人から受けたときは、しばらく考え込んだのを憶えている(あれは学生時代、場所は餃子屋だった)。しっかり考えて、納得するのが大事だ。

アドレスの中身を取得するのは *b のように変数の前に * をつける。しかし構造体の要素(メンバとよく言う)については、 -> という(若干奇妙な)記号を使用した。もういちど、先ほどのコードを書いてみる。

void func(MyVector* v, float value)
{
    v->x *= value;
    v->y *= value;
}

v はアドレスで、そのさきのメンバx,y へのアクセスに -> を使用した。なおアドレスでなかった場合、x,y へのアクセスには . (ピリオド)を使用した。仮にポインタでなかった場合の func を書いてみるとこうなる。

void func(MyVector v, float value)
{
    v.x *= value;
    v.y *= value;
}

引数の型を MyVector* から MyVector に変えると、v->x を v.x にしなければいけないわけだ。この対応が間違っているとコンパイルエラーとなる。

さて、引数がポインタだったとして、その中身を得るには *v のようにアスタリスクをつける方法もあるのだった。このルールに従えば、上の関数の中身は次のようにも記述できる。

void func(MyVector* v, float value)
{
    (*v).x *= value;
    (*v).y *= value;
}

変数がポインタだったとして、その変数を (*v) で実体にしてしまえば、. (ピリオド)でアクセスできるというわけだ。(*v) の () は単に優先順位を示したもので、*v.x と書くと *(v.x) と認識されてしまう。

書いていて改めて思うが、なかなかフクザツだ。慣れも必要だろう。いまはとにかく、ポインタ型の変数にはアドレスが格納されていることを理解しよう。

さて、最後はクイズにする。

struct AAA {
    int x;
    int y;
    int z;
};

void func(AAA* a)
{
    a->z = a->x + a->y;
}

void setup()
{
    Serial.begin(9600);
    AAA a;
    a.x = 2;
    a.y = 5;
    a.z = 100;
    func(&a);
    Serial.println(a.z);
}
void loop() {}

このプログラムを実行すると、コンソールに出力される数値(つまり a.z の値)はいくつだろうか?よくプログラムを見て、当ててみて欲しい。そして実行して、答え合わせをしてみよう。

つづく

この記事が気に入ったらサポートをしてみませんか?