娘のためにその10:構造体[40分]

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

ここまでの説明で、簡単なプログラムを書くための情報は伝えたつもりだ。ここからは、ひとつレベルが高くなるが、基本であることには変わりない。まずは構造体だ。

メモリを意識するのが大事であることは、繰り返し説明してきた。そして型というものが、無味乾燥な二進数のデータでしかないメモリを、どう解釈させるかを示すものなっていた。(娘のためにその8:型

ほとんどの言語において、型を自分で作り出すことができる。C言語における構造体はその方法のひとつだ。

struct MyVector {
    float x;
    float y;
};

struct というのが新しいキーワードだ。その後ろの MyVector は適当に付けた名前で、なんでもいい。この記述により、新たな型 MyVector が誕生した。よって次のように書ける。

struct MyVector {
    float x;
    float y;
};
void setup()
{
    MyVector v;
    v.x = 10;
    v.y = 20;
}

MyVector v; のように、新たな型を使用して変数 v を宣言したことになる。その変数 v は MyVector なので、float x や float y の要素を持つことができ、それぞれに値を入れることができる。

このベクトルの例のように、物事には「複数の値が集まって意味をなすもの」が多い。構造体を使わず、二次元ベクトルを計算するために次のように書いても同じことが可能だが、とても面倒なことになる。

void setup()
{
    float vx, vy;
    vx = 10;
    vy = 20;
}

面倒な例を挙げた。float vx, vy; で vx と vy を別々に宣言しているが、これをベクトルとみなすこともできるが、複数のベクトルの演算をやろうと思ったらたいへんだ。ミスも起こしやすい。例えばベクトルの和を計算する関数を考えてみよう。

struct MyVector {
    float x;
    float y;
};

MyVector add(MyVector a, MyVector b)
{
    MyVector sum;
    sum.x = a.x + b.x;
    sum.y = a.y + b.y;
    return sum;
}

void setup()
{
    Serial.begin(9600);
    MyVector v0;
    v0.x = 10;
    v0.y = 20;
    MyVector v1;
    v1.x = 30;
    v1.y = 40;
    MyVector sum = add(v0, v1);
    Serial.println(sum.x);
    Serial.println(sum.y);
}

add という関数が、ベクトルの和を計算していることがわかるだろうか。このプログラムは実行できるので、試して欲しい。

このように新しい型を導入することで、プログラムをシンプルにすることができる。ちなみに、型はそれぞれサイズが異なることは以前伝えた通り。今回の型 MyVector は float が二つあるので 8bytes になる。

また、C言語では複数の値を返すことができないので、構造体を使うことで初めて return sum; のようにベクトルを返すことができるようになった。

さて、ここで配列についても学んでおこう。配列も、あらゆる言語に搭載されている基本機能と言える。

int a[100];

C言語ではこのように [] に数値を書いておくと、配列を宣言したことになる。次の例をみてみよう。

int a[100];
for (int i = 0; i < 100; ++i) {
    a[i] = i*i;
}

この例では、int を 100要素を格納できる変数 a を宣言し、for文の中で100要素それぞれに数値を代入している。配列に書き込む、あるいは読み込む場合には [] の中に番号を入れる。例えば次のプログラムを実行してみよう。

void setup()
{
    Serial.begin(9600);
    int a[100];
    for (int i = 0; i < 100; ++i) {
        a[i] = i*i;
    }
    Serial.println(a[50]);
}
void loop() {}

気をつけなければいけないのは、配列の先頭はゼロであることだ。このプログラムでは a[50] を指定することで、51番目に入っている値を出力していることになる。格納する際に i*i が計算されているので、2500 という値が表示されるはずだ。

int a[100]; と書いたら、a[0] から a[99] までには書き込んだり読み込んだりできるが、a[100] はアウトであることに注意が必要だ。それは宣言の外側になるので、確保したメモリの外側になってしまうのだ。こういう、ゼロから数えるやりかたを「ゼロオリジン」と呼んだりする(和製英語らしいが)。プログラムの世界ではたいていゼロから開始する(そうでない言語もある)。ゼロから開始するので、最後の要素にアクセスしたい場合はサイズよりひとつ小さいインデクスを指定する必要がある。

まずい例として、先ほどのプログラムもこう書いたらアウトだ。

    for (int i = 0; i <= 100; ++i) {
        a[i] = i*i;
    }

i < 100 だったのを、i <= 100 に変更した。< は「未満」、<= は「以下」を表す。i <= 100 にすると i が 100 のときも true となりループ内が実行されてしまうので、ループのいちばん最後に a[100] に書き込んでしまう。最後の要素は a[99] なので、これはアクセス違反だ。メモリ破壊になるので、これをやってしまったら何が起きるかは予想できない。クラッシュと呼ばれるコンピュータの不具合は、大抵はこのメモリ破壊が要因と言っていいだろう。

さて、上の例では int の配列だったが、もちろん構造体についても配列を作ることができる。

struct MyVector {
    float x;
    float y;
};
void setup()
{
    Serial.begin(9600);
    MyVector v[100];
    for (int i = 0; i < 100; ++i)
    {
        v[i].x = i;
        v[i].y = i*2;
    }
    Serial.println(v[50].x);
    Serial.println(v[50].y);
}

MyVector v[100] で二次元ベクトルを100個用意して、中身を適当に埋めている。このプログラムも実行できるので、確かめてみよう。

さて今回は、ひとつ課題を出してみよう。上のプログラムに関数 add を追加してベクトルの和を求められるようにし、50番目と51番目のベクトルを加えた結果をコンソールに出力して欲しい。できるだろうか。

つづく

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