初級C言語Q&A(14)

初出: C MAGAZINE 1996年7月号
Updated: 1996-07-26

[1つ前] [1つ後] [質問一覧] [記事一覧] [ホームページ]


 今回は4月号に引き続き、「構造体」についてのQ&Aを紹介します。特に、初期 化に関する質問に注目してみます。

構造体 (2)


Q 【構造体の初期化】

 構造体を初期化する場合は、どのように書けばよいのか。

 基本は、{}で初期値の組みを囲んでやるということです。リストのようになり ます。
struct axis {
   int x;
   int y;
};

struct axis lt = {0, 0};
 構造体の中にさらに構造体が含まれるような場合には、{}を使って、対応する 構造と同じように初期値を並べます。
struct area {
    struct axis lefttop;
    struct axis rightbottom;
};

struct area window = {{0, 0}, {1023, 767}};
 ただし、解釈に曖昧さがなければ、構造の順番に初期値を代入する場合におい ては、途中の{}を省略することができます。この場合においても、一番外の{}は 省略することはできません。
struct area window = {0, 0, 1023, 767};
 構造体と配列が複合した場合も同様です。

Q 【初期値の数】

 構造体の初期化で指定する要素の数が、構造体のメンバーの数と異なる場合に はどうなるのか。

 構造体の要素よりも多くの初期値を初期化の時に指定することはできません。 コンパイル時にエラーになるでしょう。

 構造体の要素よりも初期値が少ない場合は、仕様として許されます。この場合 は、初期値が指定されていない残りのメンバーは0に初期化されます。

struct area window = {{0}, {1023, 767}};
    /*  {{0, 0}, {1023, 767}} を指定したのと同じ */

Q 【初期化できない】

 あるプログラムをコンパイルしようとしたら、構造体の初期化の所でコンパイ ルが中断してしまう。他のコンパイラだとコンパイルできる。

 構造体が複雑になった時の{}を省略した時の解釈に問題があるかもしれません。 このような場合は、構造体の初期化の箇所を、{}を省略しないように修正すれば コンパイルできることがあります。

 コンパイラによっては、警告が一定数を超えた場合にはコンパイルを中断する ような機能を持っていることがあります。例えばBorland C++がそうです。{}を省 略した初期化を行うプログラムをこのようなコンパイラでコンパイルすると、場 合によっては非常に多くの警告が表示されるため、このチェックにひっかかって 中断してしまうことがあります。特定の警告を出ないようにするか、警告が多い 場合に処理を中断しないように指定することで問題を回避することができます。


Q 【最後の初期値のカンマ】

 構造体の初期化を行う場合に、次のように最後の要素に「,」を付けることは許 されるか。
struct area window = {0, 0, 1023, 767,};

ANSI Cでは、構造体の最後の初期値の後にも「,」を付けてよいことになってい ます。これは、次のように初期値を改行して書く時に見た目を揃える役目と、修 正時のうっかりした間違いを軽減する効果を持ちます。最後の要素だけ「,」を付 けない、という規則だと、全部「,」を付けるよりは修正ミスの確率が高くなりま す。
struct area window = {
    0,
    0,
    1023,
    767,
};

Q 【構造体に相当するデータ】

 コードのある箇所で、構造体の値を決まった値にセットしたい。しかし、次の ように書くとコンパイルできない。
struct axis {
    int x;
    int y;
};

    struct axis pos;

    pos = {640, 480};

 構造体は、それを定義する時点で初期化を行うことができますが、コードの途 中でそのような処理をすることはできません。どうしても必要なら、各要素に対 して値を一つずつ代入することになります。あるいは、次のようなコードで、型 が一致する他の構造体を初期化してやり、それを代入するという方法があります。 例のように要素が2個しかないような場合は、むしろ直接代入した方が簡単ですが、 要素の数が大きかったり複雑な構造である場合には、この方法で値をセットする と、途中の要素の設定を忘れることを防ぐ効果があり、また、比較的見やすく書 くことができるかもしれません。ただし、コンパイラはこのコードをいくぶん冗 長な実行形式として生成するかもしれません。
foo()
{
    struct axis pos;

    ...

    {
        struct axis tmp = {640, 480};

        pos = tmp;
    }

    ...
}
 もっと簡単に書くには、初期値を代入した外部変数を用意しておく方法もあり ます。
const struct axis INITIAL_VALUE = {640, 480};

foo()
{
    struct axis pos;

    ...

    pos = INITIAL_VALUE;

    ...
}
 この場合も、初期値が入った構造体があらかじめメモリ上に確保される時に多 少は冗長なコードになる可能性はありますが、殆ど無視できる程度だと思われま す。定数を複数の箇所で使う必要があるなら、外部変数として用意することにな りますが、その場合は名前の衝突に常に気を付ける必要があります。

 関数内でのみ使うのであれば、局所変数として用意するとよいでしょう。

foo ()
{
    static const struct axis INITIAL_VALUE = {640, 480};
    struct axis pos;

    ...

    pos = INITIAL_VALUE;

    ...
}

 staticで宣言しなくても動作は同じですが、 INITIAL_VALUEへの代入が関数呼び 出し毎に発生するため、オーバーヘッドが気になります。

 初期化のための関数を用意するという方法もあります。

struct axis set_struct_axis(int x, int y)
{
    struct axis pos;

    pos.x = x;
    pos.y = y;
    return pos;
}

foo ()
{
    struct axis pos;

    ...

    pos = set_struct_pos(640, 480);

    ...
}

 初期化の所のコードは多少見やすくなりますが、この場合も、本当に関数を呼 び出すコードが生成されるとオーバーヘッドが気になるところですが、コンパイ ラによってはインラインに展開するような書き方ができる場合もあります。

(参考 comp.lang.c FAQ 2.10)


Q 【配列1つだけを含む構造体】

 次のような構造体が宣言されていた。配列をそのまま使えばよいと思うのだが、 これは一体何を意図しているのだろうか?
struct array {
    int item[10];
};

 構造体と配列は似ていますが、機能として大きな違いがいくつかあります。特 に重要なことは2つあります。

構造体は、関数の引数、あるいは戻り値として使うことができ、この場合は 値が渡される。

 配列を関数の引数に指定することもできますが、関数の引数に配列を与えた場 合には、配列の内容ではなく配列の先頭を指すポインタが渡されます。呼び出さ れた関数の中で配列の要素を変更すると、呼び出した関数における配列と同じオ ブジェクトが変化することになります。構造体を関数呼び出しの引数に与えると、 渡されるのはその構造体の値のコピーですから、呼び出された関数の中で何があ っても呼び出した関数の構造体の内容は変化しません。また、関数からの戻り値 として構造体が指定されていれば、その構造体の内容を呼び出した側で受け取る ことができます。

同じ型の構造体は、代入演算子を使って代入することができる。

 配列は、たとえ要素の数が同じであっても、「=」で内容をそっくりコピーする ことはできません。memcpyのような関数を使うことが必要です。 構造体の場合は、「=」を使って内容をコピーすることができます。

 さて、配列を一つだけ含んだ構造体の意図ですが、おそらくこのどちらかの機 能を使うためのテクニックでしょう。例えば、配列のままだと、内容をコピーす るにはmemcpyのような関数を使う必要がありますが、 構造体にすることによって、代入演算子だけでコピーするように書くことができます。

/* 配列のコピー */
foo()
{
    int a[10], b[10];

    ...
    memcpy(a, b, sizeof(a));
    ...
}

/* 構造体のコピー */
foo()
{
    struct array a, b;

    ...
    a = b;
    ...
}

Q 【構造体はmemcpyできる?】

 しかし、ある本には、次のようなプログラムで構造体のコピーを行うと、正常 にコピーされないことがあるので、コピーするための関数を作るのが望ましいと 書いてあった。
struct st {
    int first;
    char second;
    int third;
};

    struct st a;
    struct st b:

    memcpy(&a, &b, sizeof(a)); /* 正常にコピーされない? */

/* コピーするための関数例 */
void st_cpy(struct st *dest, struct st *src)
{
   dest->first = src->first;
   dest->second= src->second;
   dest->third = src->third;
}

 もしそんなことが起きるとすれば、そのコンパイラはよほど珍しいのではない でしょうか。私はそのようなコンパイラを見たことがありません。少なくとも、 ANSI Cに準拠したCコンパイラなら、memcpyで全く正しく コピーされるはずです。 わざわざコピーするための関数を作るのは無駄を増やす以外のなにものでもあり ません。

 もちろん、このような場合はmemcpyを使うという選択でさえ 余計な手間をかけてコードを書くことになります。

    a = b;
 と書けば済むのですから。ただし、ANSIに準拠していない一部のコンパイラの 中には、構造体の代入を許さないものもあります。そのようなコンパイラを使う 場合には、memcpyを使ってコピーする必要がある場合もあります。

 a = b;のようにして構造体をコピーした場合には、メンバーの中 にポインタが含まれていると、その実体ではなくポインタの値がコピーされるこ とに注意する必要があります。コピーした結果、二つの構造体のポインタが共通 の一つのオブジェクトを指すことになります(NULLならば何も指しません)。この ため、一方の構造体に含まれているポインタの指す先を変更すると、もう一方の 構造体に含まれているポインタの指す先も、同時に変化してしまいます。

 構造体のコピーではなく比較をmemcmpで行うことは避けるのが 無難です。memcmpで比較を行うと、要素が全て同じ値であっても、 必ずしも0という値が戻って来る とは限りません。なぜなら、構造体の要素は連続しているとは限らないからです。 要素に隙間があると、構造体の各要素の値が同じでも、隙間のごみの内容が異な ると、memcmpが一致しないという結果を戻すかもしれません。 memcpyの場合は間のごみもまるごとコピーするので別に問題には ならないはずです。


Q 【構造体の実体の場所】

 構造体の内容を関数の引数として与えたり、戻り値として受け取る場合、実際 の情報はどこに格納されているのか?

 C言語では関数が多重に呼ばれることがありますから、戻り値はそのような場合 にも正しく受渡しできるように処理される必要があります。従って、基本的に、 他の戻り値と同じように、スタックを使って値をやりとりするような実装になっ ているコンパイラが多いと思われます。ただし、ANSIの仕様以前に作られたコン パイラの中には、固定アドレスに内容を配置するものもあったようです。この場 合は多重に関数を呼び出すと値が破壊されることになるので、注意が必要です。

Q 【ポインタを含む構造体のコピー】

 次のような構造体をコピーしようとした。
struct table {
    int number;
    char *key;
};

    struct table a, b;
    ...
    a = b;
構造体を複製したつもりだったが、a.keyを変更すると b.keyまで変わってしまう。 a.keyも複製するにはどうすればよいか。

 構造体の代入は、その構造体のメンバを複製するだけに過ぎません。メンバに ポインタが含まれている場合は、ポインタの指す先はコピーされないので、結果 として同じオブジェクトを指すことになります。  このような場合は、ポインタの指している内容を別にコピーして、ポインタを 置き換えてやればOKです。keyの指す先が文字列であるとします。
    a = b;
    a.key = malloc(strlen(b.key) + 1);
    strcpy(a.key, b.key);
 実際は、エラーチェックを行う必要があります。すなわち、b.keyNULLでないことを確認して、malloc の戻り値もNULLでないことを確認して、その後で strcpyを実行するようにします。

ネットの質問

このコーナーは、WWWで出た質問や、ネットで見掛けた質問を紹介します。

Q 【移植】

DOSで動作させるために開発したC言語のプログラムは、UNIXやWindowsで使うこ とができるか。

 できることもあるし、できないこともあります。その環境に依存した処理は、 DOSという環境であっても、例えばpc98用とPC/AT用で異なるコードが必要になり ますが、同様のことは他の処理系への移植の場合にさらにシビアになります。ANSI Cで決められている標準関数はどのような環境下でも一応あるはずですが、完全に 同じ動作が期待できるとは限りません。

 特に注意すべき箇所としては、intのサイズの差や、文字コードの違い、利用で きるメモリの量などです。

 Windows環境で動作するプログラムの場合、DOSと根本的にライブラリ等が異な りますので、書き直すことになるでしょう。

 最初からマルチプラットフォームでプログラムを利用するつもりであれば、そ のような開発環境を使うという選択も検討することがあります。


Q 【NULLと0】

NULLが整数の0と一致するという保証はあるのか?

ANSIの仕様では、NULL0と比較するような場合に は常に一致することが保証されています。言いかえれば、NULL == 0 という式は、常に1という値になるはずです。 処理系によっては、NULLポインタの実装上の値が0 でないかもしれませんが、Cのプログラムから見た場合には、内部表現にかかわらず、 0という値はNULLと一致することが保証されるように コンパイラがうまくやってくれることになっています。

Q 【演算子 ->】

->という演算子の意味が分からない。

->」の意味を理解するには、構造体とポインタの両方を理 解しなければならないので、C言語を使いはじめた人には難しいかもしれません。 要するに、 (*p).xp->x というのは全く同じ意味です。

 すなわち、ある構造体の領域を指すポインタpがある時、 *pは構造体の実体を意味しますから、そのメンバーである xを表現すると(*p).xとなります。 p->xという表現は、これと全く同じ意味として解釈されます。

    struct axis *p;

    p = malloc(sizeof(struct axis));
    p->x = 800;
    p->y = 600;

                   struct axis
     p  -------→ +----------+
                  |   800    | x
                  +----------+
                  |   600    | y
                  +----------+

Q 【->の存在理由】

(*p).xp->xが全く同じであるなら、 なぜこのような書き方が特別に用意されているのか。

 確かに、(*p).xp->xは全く同じ意味になるので、 「->」という表現がなくてもプログラムは書けます。 にもかかわらず、特別に演算子が用意されているのは、 プログラム中にこのようなデータ構造がしばしば現れるという事実が理由になっ ているようです。K&Rにも「構造体へのポインタはよく使われる」という記述があ りますが、特に、構造体の領域をmallocで獲得したり、 関数への引数として構造体を指定する場合には、構造体そのものではなく、 ポインタだけを渡すことがよくあります。

 (*p).xと書く場合には括弧を省略することができません。 なぜなら、演算子 「*」よりも「.」の方が優先度が高いので、 *p.xと書いた場合には*(p.x)と解釈されてしまうか らです。幸い、この書き方はコンパイルした時にエラーとなるので発見できますが、 (*p).xという書き方は、括弧が必要であるため他の括弧と重なり、 プログラムを読みにくくする原因となりがちです。そこで、見やすいプログラム を簡単に書けるように「->」という表現が用意されたのでしょう。

 「->」という書き方は、ポインタの指す先の領域を矢印で示 しているのだと思えば、比較的イメージしやすいと思います。


  ※ c.l.c FAQ : comp.lang.c FAQ list
     
http://www.eskimo.com/~scs/C-faq.top.html
     文中の項目番号は新しい版に対応しています。旧版とは異なります。

(C) 1996 Phinloda, All rights reserved
無断でこのページへのリンクを貼ることを承諾します。問い合わせは不要です
内容は予告なく変更することがあります。