第12章 構造体

テキスト『新・明解C言語 入門編』第12章の内容に関する補足説明を提供します.

12-1 構造体

データの関連性

List 12-1 で行っている, 対応(テキストの言葉では「関連性」)のあるデータのソートは, 構造体を使うと交換用の関数ひとつ(List 12-1 では swap_int と swap_str の2つ)で実現できます. 次のセクション説明しているように, 構造体は同一対象についてのさまざまな情報が1枚に記載されたカードのようなものです. カードのソートを行う関数をひとつ書けばよいのです. これは List 12-7 で学習します.

List 12-1 で <string.h> ヘッダをインクルードしているのは swap_str 関数の定義で strcpy 関数を使うためです. この関数については前章で学習しました(p.299).

List 12-1 の sort 関数で,swap_int 関数と swap_str 関数の引数に注意してください. 身長の値を交換する swap_int 関数は int へのポインタ型の値を引数にとります. そのため,アドレス演算子(&)を用いて,int 型の配列の要素 num[j - 1] および num[j] のアドレスを取得しています. 名前を交換する swap_str 関数は char へのポインタ型の値を引数にとります. ここで,swap_int 関数の引数につられて, &str[j - 1] および &str[j] としないよう注意してください. 配列 str(具体的には,配列 name)は文字列を要素とする配列です. 単に str[j] と書けば,&str[j][0] と同じ意味になります. これは j 番目の文字列の先頭文字へのポインタです.

同様に,main 関数において名前および身長を表示する printf 関数の引数では, name[i] は &name[i][0] と同じ意味です. これは i 番目の文字列(名前)の先頭文字へのポインタです. 第13章末尾の printf 関数の仕様を参照してください. 変換指定子が s のとき,「実引数は,文字型の配列へのポインタ」です. 名前の出力の %-8s という変換指定は, 8文字を左づめで出力するという意味になります.

構造体

構造体は配列と似ていますが,異なっている点もいろいろあります. テキストで順に学習していきます.

まずは,配列は型ではないのに対して,構造体は型であるということに注意してください. 自分が必要とする新しい型を宣言して使うことができます. テキストでは struct student という型を宣言しています. この点では,構造体は第8章(p.220)で学習した列挙体と似ています. 構造体タグ(たとえば,struct student 型での student)は列挙体タグ(たとえば,enum animal 型での animal)に相当します.

宣言と定義は区別されます(テキスト p.146「宣言と定義」を参照). 構造体を Fig.12-3 のように宣言すると, コンパイラは宣言された型(ここでは,struct student 型)の構造がわかります. すると,この型のオブジェクトのためにメモリをどのように確保すればよいかわかります. 定義は,オブジェクトの実体を作り出すこと,すなわち, メモリが割りあてられたオブジェクトが作られることを意味します.

構造体の宣言は関数の中でも外でも行うことができます. 構造体をどの関数からも自由に使えるようにしたければ, 関数の外で宣言します. このテキストのプログラムでは, どの関数よりも前に構造体の宣言が行われています.

このセクションの最後に述べられているように, 構造体の宣言と定義を同時に行うこともできます. しかし,こうした書き方はするべきではないと考えるプログラマもいます. 後のセクションで学習する typedef 宣言と共存できないためと, 宣言と定義は分けるべきという考えのためです.

構造体のメンバと . 演算子

List 12-2 から12-5 では1人の学生のデータを扱います. Fig.12-2 のカードの比喩では,1枚のカードだけを扱うことを意味します. List 12-6 も同様です.

配列の要素には添字と添字演算子を用いてアクセスできました. 構造体のメンバーには添字でアクセスすることはできません. かわりにドット演算子を用います.

メンバの初期化

初期化の方法に関しては,配列と構造体は同じです. 大括弧 { } の中に初期化子をコンマで区切って並べます.

構造体のメンバは記憶域上で宣言順に並びます. たとえば,List 12-3 での構造体型のオブジェクト takao では, メンバは Fig.12-7 のように並びます. 名前を入れるための char 型の配列 name のアドレスが最も小さく, 奨学金を入れるための long 型の schols のアドレスが最も大きくなります. これを演習 12-1 で確かめてみてください.

構造体のメンバは隙間なく並んでいるとは限りません. この点は配列と異なりますので注意してください. 配列の要素は記憶域上で隙間なく並んでいました. たとえば,int 型の大きさが 4 の環境で int 型の配列を定義すると, 各要素のアドレスは 4 ずつ離れることになります. この配列が記憶域上で占める大きさは 4 の要素数倍です. 構造体ではメンバの間に隙間があるかもしれません. もしそうなっていると,各メンバの大きさの合計は, 構造体の大きさと一致しないことになります.

演習 12-1 で,オブジェクト takao のメンバが隙間なく並んでいるかを調べた結果が下図です, 名前を保存するメンバである char 型の配列 name の大きさは 16 としました(NAME_LEN の値を 16 としました). この環境では,char 型,int型,float型,long型 の大きさはそれぞれ 1,4,4,8 でした. 各メンバのアドレスや大きさを見ると, オブジェクト takao のメンバが隙間なく並んでいることがわかります. しかし,これはいつも保証されていることではありません.

pointer to members (ex 12-01)

配列ではポインタに整数を加減する演算によって要素間を移動することができました. たとえば,配列の最初の要素を指しているポインタに 1 を加えると, 2番目の要素を指すポインタとなりました. 構造体では各メンバの大きさが異なりますので, このようなポインタ演算によってメンバ間を移動することはできません.

構造体のメンバと -> 演算子

式の中で,配列名はポインタに読み替えられました. 構造体ではこのような読み替えは行われません. 構造体へのポインタが必要なときは構造体名にアドレス演算子(&)を適用します. List 12-4 での関数 hiroko の定義を見てください. 仮引数 std は struct student へのポインタ型です. List 12-4 の main 関数では,struct student 型の構造体 sanaka にアドレス演算子を適用して, この構造体を指すポインタを得ています. このポインタが関数 hiroko の引数となっています.

テキスト p.315 で述べられている演算子の優先順位については Table 7-11(p.205)で確認してください. ドット演算子の優先順位は1,関節演算子の優先順位は2です.

構造体と typedef

構造体は int や char と同じく型なのに, 定義が struct student sanaka のように長くなる(いちいち struct というキーワードが必要になる)ことが面倒に感じられるかもしれません. そう感じるならば,typedef 宣言を用いて型の別名を作ることができます. そのようにしなければならないということではありません. キーワード struct があることで構造体であることが明示されるのだから, struct とタイプする手間ぐらい惜しむべきではないというプログラマもいるようです.

テキストでは typedef 名の最初の文字だけを大文字にしています. オブジェクト形式マクロで定義する記号定数はすべて大文字で表記していたので, こうしておけば, 記号定数と同じ typedef 名をうっかり使ってしまうことを避けられます. 第8章で学習した列挙定数も,最初の文字だけを大文字にしていました.

構造体のタグ名と typedef 名の違いが先頭文字だけ(大文字か小文字の違い)となるのは, テキストではお勧めできないと書かれています. しかし,まったく違う名前を考えることは面倒かもしれません. 意図的に同じ名前を使うことを好むプログラマもいるようです. テキストでは,構造体を定義するときに, タグ名を省略して typedef 名を与えることで, タグ名と typedef 名の衝突を避ける方針のようです. List 12-7 での,学生を表す構造体の定義を参照してください.

テキスト p.311 で,構造体の宣言と定義を同時に行うことができるということが述べられていました. その補足説明で述べたように,この方法と typedef 宣言は共存できません.


struct xyz {
    int    x;
    long   y;
    double z;
} a;

と書けば,struct xyz 型の構造体を宣言し, この型のオブジェクト a を定義したことになります. 似ているようでも,


typedef struct xyz {
    int    x;
    long   y;
    double z;
} a;

と書けば,struct xyz 型の構造体を宣言し, この型の別名として a を与えたことになります. まぎらわしいので注意してください.

構造体とプログラム

このセクションについての補足説明は現在のところありません.

集成体型

テキストで説明されているように,構造体は同じ型の構造体に丸ごと代入可能です. これは構造体の大きな利点のひとつです. 配列ではこれができないので,要素をひとつずつ代入しました. 文字の配列の場合には,strcpy 関数あるいは strncpy 関数を使いました.

構造体の値を返す関数

配列を関数の返却値とすることはできないので, もし実質的にそのようなことがしたければ, 配列の先頭要素へのポインタを返す関数を設計することになります(たとえば,List 11-6). 関数の引数を配列としたい場合も同様です. 配列を引数にとる関数定義を書くことはできますが(たとえば,List 6-11), これは実際にはポインタが使われているのでした.

構造体は型なので,関数の引数とすることも(たとえば,List 12-8), 関数の返却値とすることもできます. テキストの説明では, 構造体は「代入可能だから」関数の返却値型として利用できると書かれていますが, 単に「型だから」と理解した方が簡単でしょう. 一方,配列は型ではありません.

構造体は関数の引数にできますが, 大きな構造体ではそうした関数を書かない方がよいかもしれません. C言語では実引数のコピーが関数の仮引数に渡されるのでした(p.140). 関数に構造体を渡すとき,構造体全体がコピーされます. 大きな構造体ではコピーのために時間と記憶域を少なからず使うことになります. それならば,構造体を渡すのではなく, 構造体へのポインタを渡した方がよいかもしれません.

名前空間

テキストで学習した範囲では,タグ名を使うのは列挙体と構造体, メンバ名を使うのは構造体だけです. このテキストから先に学習を進めていくと,「共用体」というものにタグとメンバがあります.

名札(ラベル)とは,「プログラムの飛び先を示す目印」(p.65)のことです. 第3章で学習した switch 文(p.64)では, case および default という名札を使いました(case 名札は定数と組み合わせて case 1 のように使いました). 名札では識別子のあとにコロン(:)をつけます. たとえば,switch 文では,case 1: とか default: というように書きました. テキスト p.319 で例示されている main 関数での名札 x は, この位置に飛ぶ処理が何も書かれていないので, 実質的な意味はありません.

テキスト p.319 で例示されている main 関数で, 「一般的な識別子」に該当するのは変数名 x です.

構造体の配列

List 12-7 は List 12-1 とよく比較してください. そうすれば理解できるはずです.

派生型

このセクションについての補足説明は現在のところありません.

12-2 メンバとしての構造体

座標を表す構造体