構造体

目次

アルゴリズムとデータ構造

コンピュータでのさまざまな情報処理を考える場合,その設計に必要なものは「アルゴリズム」と「データ構造」である.

今回は「データ構造」の表現手法として重要な構造体について詳しく学ぶ.
構造体(structure)とは,C言語に限らず 既存のデータ型を組み合わせ,新しいデータ型をユーザが定義する機能である.
データや値(=何らかの情報)は,お互いに関連がある場合が多く,この様なデータをまとめて管理できると都合が良い.
例:住所録(名前,住所,電話番号など),複素数(実部,虚部),3次元座標(x, y, z),ベクトル(u, v, w),行列・テンソル(mxn),音声ファイル(サンプリング周波数,ステレオかモノラルか,左チャンネル・右チャンネル),画像ファイル(幅,高さ,色数,圧縮形式,画像本体)など

構造体とは

互いに関連のある複数の数値,文字,配列などを組みあわせて,概念的に新しい型を作り出す手法を「構造体」(structure)と呼ぶ.

構造体にはいろいろな変数を含むが,それらを構造体の メンバ変数,または,単に「メンバ」と呼ぶ.
データ構造の一種である配列は,全ての要素が同じ型の集合であるのに対し,構造体はそれぞれのメンバが異なる型を持つことができる. (同じ型でも良い.)

複数のメンバ変数を有する構造体の定義は,struct キーワードを用いて次の通りに記述する.

struct 構造体名
{
    型名 変数名1;
    型名 変数名2;
    型名 変数名3;
    ・・・
    型名 変数名N;
};                   // ここにセミコロン必要

例えば,複素数(実部と虚部で構成される)を表す構造体 Complexの定義は,以下のような書き方となる.

struct Complex
{
    double re;      // 実部  real part
    double im;      // 虚部  imaginary part
};

// 文法上,関数の定義によく似ているが,() がつかないのと,最後の ; が異なる.

このように書くと,

である,という意味となる.

構造体のメンバ変数には,組込型(int, floatなどのプリミティブ型)だけでなく,その配列や文字列,ポインタ変数,さらには別の(定義済みの)構造体が含まれても良い.
したがって,構造体にどのような型の変数を,何個含めるか,即ち,どのような情報を概念的にまとめて扱うかを定義する事は, まさにそのプログラムの処理すべき情報表現の設計そのものである.

次に,このように定義した構造体を用いて,新しく変数を定義するときは,構造体名と変数名を順に記述する.
これは組み込み型の変数定義と同様である. つまり,構造体名=型名のような役割を果たすと考えればわかりやすい、

Complex z;       // 構造体(ここではComplex型)の変数定義
int i;           // 組み込み型変数の定義
char str[100];   // 配列の定義

メンバ変数へのアクセス

構造体内のメンバ変数に値を代入したり,逆に値を取り出して別の変数に代入するには,以下のドット演算子( . )を用いる.

(構造体型の)変数名.メンバ変数名

ドット演算子を含め,「構造体変数名.メンバ変数名」で,一つの変数と見ればわかりやすい.

#include <stdio.h>

struct Complex     // 複素数クラス
{
    double re;    // 実部
    double im;    // 虚部
};

int main(void)
{
    Complex z;           // 構造体型の変数定義

    z.re = 1.0;    // メンバ変数に代入
    z.im = 2.0;    // メンバ変数に代入

    printf("z = (%lf , %lf)\n", z.re , z.im );    // メンバ変数の参照

    return 0;
}

練習問題

  1. 上記のサンプルプログラムの動作チェックを行い,画面に表示される数値を確認せよ.
  2. 複素数の表示形式をカッコ書きの(実部,虚部)ではなく、数学で用いられる $a+bi$ の形式に変更してみよう.
  3. 実行例
    z = 1.00000+2.00000 i
    
    ヒント:数値に常に符号を表示するには,書式指定を "%+lf" とする.

構造体変数は通常の変数と同様,関数の引数とすることができ,構造体変数を一つ渡せば,メンバ変数がすべて(値渡しで)渡されるので非常に便利である.
戻り値についても同様であり,複数の値を return 文で返したい場合は,構造体を使うという方法もある.

#include <stdio.h>
#include <math.h>     // sqrt関数用

struct Complex     // 複素数クラス
{
    double re;    // 実部
    double im;    // 虚部
};


double zabs(Complex c)      // 複素数の絶対値を返す関数.引数は構造体である Complex 型
{
    return sqrt( c.re * c.re + c.im * c.im);
}

int main(void)
{
    Complex z;           // 構造体変数の定義

    // 基本的には,メンバ変数にそれぞれ代入する.
    z.re = 1.0;
    z.im = 2.0;
    
//    z = {10.0, 20.0};		// このような代入が可能なコンパイラもある.

    printf("z = (%lf , %lf)\n", z.re , z.im );
    printf("abs(z) = %lf\n", zabs(z) );         // 関数の呼び出し

    return 0;
}

練習問題

  1. 上記のサンプルプログラムの動作チェックを行い,画面に表示される数値を確認せよ.
    複素数の値を変更し,正しく動作するか確認せよ.
  2. 2つの複素数の和を計算し,返す関数zadd()を作成せよ.
    上記の例の通り,表示は main() で行い,zadd() では計算処理のみを行うこと.
    ヒント:zadd() 関数は,引数を複素数型2つ受け取り,戻り値型は ???
  3. 実行例:
    
    z1 = (1.00000, 2.00000)
    z2 = (-2.00000, -4.00000)
    z1+z2 = (-1.00000, -2.00000)
    

いろいろな構造体の例

よくある構造体の例をいくつか示す.
ここに示したのは一例であり,変数名や構造体名などは任意につけることができる.

例:2次元空間座標(直交座標)
struct Coord          // coordinate:座標
{
    double x;  // x 座標
    double y;  // y 座標
};


このようにまとめて書いてもよい.
struct Coord
{
    double x, y;  // x,y 座標
};


例:2次元空間座標(極座標表現)
struct Coord_polar
{
    double r;      // r
    double theta;  // theta
};


構造体内に配列があってもOK.
struct Coord
{
    double x[2];  // x[0], x[1]は x座標, y座標,または r, theta
};
3次元座標の場合
struct Coord
{
    double x, y, z;  // x,y 座標
};

または
struct Coord
{
    double x[3];  // x[0]が x 座標, x[1]が y 座標, x[2]が z 座標
};
例:2次元ベクトル
struct Vector2D
{
    double x0, y0;  // 始点
    double x1, y1;  // 終点
};


構造体内に定義済の別の構造体を用いてもOK.
struct Vector2D
{
    Coord beg;  // 始点
    Coord end;  // 終点
};
画像ファイル
struct BitmapImage
{
    unsigned int width;  // 画像の幅
    unsigned int height; // 画像の高さ
    int type;            // 画像の種類,モノクロ or カラー
    int* img;            // 画像本体を指すポインタ
};
ゲームキャラクターのステータス
struct BitmapImage
{
    int Lv;   // レベル
    int HP;   // ヒットポイント
    int MP;   // マジックポイント
    int Exp;  // 経験値
    int Pw;   // 攻撃力
    int De;   // 守備力
};

構造体の定義と,変数の定義

データ表現を決める「構造体の定義」と,その変数を実際にメモリ上に確保する「構造体変数の定義」は同時に行うこともできるが,通常は別々に行う.

例えば,以下のような構造体の定義はソースファイルの先頭付近,または別途ヘッダファイル(.h)を作成し,それを #include して使う.

// 構造体の定義,即ちデータ構造の定義である.
// 使用するソースコードの上方に記述するか,ヘッダファイル (.h) として別ファイルを作る.

//  例:住所録
struct AddressRecord
{
    char name[40];         // 氏名

    int postal_code1;    // 郵便番号,最初の3桁
    int postal_code2;    // 郵便番号,最後の4桁

    char address[80];      // 住所
    char phone_number[20]; // 電話番号
};

一方,構造体型の変数の定義は,実際に使用する場所(通常は.cppファイル)の中で行われる.
通常の変数と同様,構造体変数の配列や,構造体を指すポインタも定義できる.

// 構造体変数の定義.
// これよりも前方に構造体の定義が必要!

AddressRecord card;             // 構造体 AddressRecord 型の変数 card を定義.
AddressRecord cards[100];       // 構造体 AddressRecord 型の変数 card を100個分定義.
AddressRecord *p=&cards[0];     // 構造体を指すポインタ

プログラム中では,変数の定義をして初めてメモリ上に領域が確保されることを思い出そう.
構造体の定義だけでは,メモリ上には何も配置されない.あくまでデータ構造を定義したに過ぎない.

参考:旧スタイル(C++言語ではなくC言語)の構造体定義

もともとの C 言語の文法規則では,構造体変数の定義時に,必ず struct キーワードを書かなければならなかった.
つまり,

struct AddressRecord
{
    ... // 構造体の定義
};

int main(void)
{
    // 変数の定義時に毎回 struct と書く必要あり
    struct AddressRecord card;
    struct AddressRecord card2;
    ...
}

のように,structキーワードを毎回書く必要があったが,C++ではこの制約がなくなり,表記が簡単になった.
何らかの理由で,古めのコンパイラを使用する場合や,昔のソースコードを読む際には場合は注意.

構造体変数の参照・代入

では構造体変数のメンバ変数に,実際に値を代入してみよう.
代入や参照については一般の変数と何ら変わりはない.

注:以下のプログラムでは,名前や住所などの文字列変数に全角文字を使用すると文字化けすることがあるので,ローマ字で記している.

#include <stdio.h>
#include <string.h>

struct AddressRecord
{ 
    char name[40];
    int postal_code1;
    int postal_code2;
    char address[80];
    char phone_number[20];
};

int main(void)
{
    // 構造体変数の定義
    AddressRecord card;

    //  メンバ変数にそれぞれ代入
    card.postal_code1 = 214;      // card.postal_code1 で,1つの int 型変数とみなせばよい.
    card.postal_code2 = 8571;

    //  文字列は直接代入できない. strcpy() を使用するべし.

    // card.name = "Rikou Taro"                   //  NG.
    strcpy(card.name,  "Rikou Taro");                   // OK. card.name は,配列名=先頭アドレス

    strcpy(card.address,  "Higashimita 1-1-1 Tama-ku Kawasaki"); // 上と同じ
    strcpy(card.phone_number,  "044-934-xxxx");       // 上と同じ

    //  構造体の各メンバ変数の中身を,画面に表示
    printf("%d - %d\n", card.postal_code1, card.postal_code2 );
    printf("%s\n", card.name);
    printf("%s\n", card.address);
    printf("%s\n", card.phone_number);

    return 0;
}

練習問題

  1. 上記のサンプルプログラムの動作チェックを行い,画面に表示される文字を確認せよ.

  2. 次に,このデータを画面ではなく,テキストファイルに出力してみよう.
    ファイル名は char fname[] = "database.txt"; とする.
    可能であれば,1行に1人分のデータを コンマ区切り(.csv) で出力するとよい.

    ヒント:printf(...) の個所を fprintf(fp, ...) とすればよい. もちろん fopen(), fclose() などの処理を適切に行うこと.

構造体変数の初期化

構造体変数についても「定義と同時に初期化」が可能である.
(配列の場合と同様,構造体変数の「定義と同時に」「定数を」設定することが可能)
構造体変数の初期化は,以下のように変数の定義と同時に,メンバ変数の記述順に,初期化したい定数をカンマ区切りで列挙する.

さらに,配列とは異なり, = 演算子による構造体変数の代入が可能である.

以下は,構造体の定義で示した AddressRecord 構造体を用いた例である.

#include <stdio.h>

// 構造体の定義
struct AddressRecord
{ 
    char name[40];
    int postal_code1;
    int postal_code2;
    char address[80];
    char phone_number[20];
};

int main(void)
{
    // 構造体変数の定義と同時に初期化.
    // メンバ変数の定義順に,変数をカンマ区切りで並べて書く. 
    AddressRecord ar = {"Meiji Jiro", 101, 8301, "Kandasurugadai 1-1 Chiyoda-ku", "03-3296-xxxx"};

    // 以下はOK.同じ種類の構造体変数同士の代入. 
    AddressRecord ar2;
    ar2 = ar;

    return 0;
}
参考:C++で拡張された構造体=クラス

C++言語では,「構造体」に「メンバ変数」だけでなく「メンバ関数」を持たせることができる「クラス」が導入された.
(C++言語は開発当初,C with classes と呼ばれていた.)

構造体では,複数の変数をまとめるデータ表現を可能としていたが,さらに「クラス」ではデータと,それに関連する処理(関数)をも一体化できる.
(そのほか,クラスにはデータ隠蔽化のためのアクセス指定子や,初期化処理や終了処理を行うコンストラクタ・デストラクタ,継承などの機能が追加されている.)

class クラス名
{
private:    // アクセス指定子

    //  メンバ変数
    型名 変数名;

public:    // アクセス指定子
    //  メンバ関数
    戻り値型 関数名(引数, ...);
};

構造体の配列

先の例では,一人分のアドレスを格納できる構造体 struct AddressRecord を定義した.
通常アドレス帳には何人ものデータを記録したいので,このような場合には,構造体を使って配列変数を定義する.
例えば,100人分の連絡先を記録するには,普通の配列と同じく

const int N = 100;
AddressRecord cards[N];      // 構造体の配列

と定義すればよい.
変数 cards は配列なので,それぞれの要素にアクセスするには,

for(int i=0; i<N; i++) {

    cards[i].postal_code1 = 101;
    cards[i].postal_code2 = 8301;

    strcpy(cards[i].name, "Meiji Jiro");
    strcpy(cards[i].address, "Kandasurugadai 1-1, Chiyoda-ku");
    strcpy(cards[i].phone_number, "03-3296-xxxx");
	...
}

とすれば良い.(この例だとfor分で 100件分のレコードにすべて同じデータが入る)
配列アクセスのための [ ] 演算子と,構造体のメンバアクセスのための . 演算子の順番に気をつけよう

上記の例で,プログラム中での表記と,その意味(=型)をまとめると,

cards           構造体配列 card の先頭のアドレス
cards[0]        構造体配列 card の先頭の要素(=AddressRecord型の1個の変数)

cards[0].postal_code1    構造体配列 card の先頭の要素 card[0] のメンバ変数 postal_code1 の値(int型)
cards[0].name            構造体配列 card の先頭の要素 card[0] のメンバ変数である文字列 name の先頭アドレス
cards[0].name[0]         構造体配列 card の先頭の要素 card[0] のメンバ変数である文字列 name の先頭の 1 文字(1文字,char型)

構造体配列の初期化

構造体変数の配列を初期化することも可能である.
二次元配列の初期化とよく似ている.

#include <stdio.h>

// 構造体の定義
struct AddressRecord
{ 
    char name[40];
    int postal_code1;
    int postal_code2;
    char address[80];
    char phone_number[20];
};

int main(void)
{
    const int N = 3;

    // メンバ変数の定義順に,定数を記述.
    AddressRecord cards[N]={{"Meiji Taro",   101, 8301, "Kandasurugadai 1-1, Chiyoda-ku", "03-3296-xxxx"},
                            {"Meiji Jiro",   102, 8302, "Kandasurugadai 2-2, Chiyoda-ku", "03-3297-xxxx"},
                            {"Meiji Saburo", 103, 8303, "Kandasurugadai 3-3, Chiyoda-ku", "03-3298-xxxx"} };

    return 0;
}

練習問題

  1. 上記プログラムを参考に,全員分の全データを画面に表示するプログラムを作成せよ.
    1行に1人分のデータを出力すること.なるべく列がそろうようにすると見やすい.
  2. 実行例
    
    Name         PostalCode  Address                       Phone number
    -------------------------------------------------------------------
    Meiji Taro   101-8301    Kandasurugadai 1-1 Chiyoda-ku 03-3296-xxxx
    Meiji Jiro   102-8302    Kandasurugadai 2-2 Chiyoda-ku 03-3297-xxxx
    Meiji Saburo 103-8303    Kandasurugadai 3-3 Chiyoda-ku 03-3298-xxxx
    

構造体へのポインタ

配列へのアクセスでは,ポインタを用いて行われる場合がある.
構造体へのポインタの定義は,その他の変数のポインタの定義と何ら変わらない.

// 普通の構造体変数を指す場合
AddressRecord card;            // 構造体
AddressRecord *p = &card;      // 構造体を指すポインタの定義(と初期化)

// 構造体の配列を指す場合
AddressRecord card_all[100];           // 構造体の配列
AddressRecord *p2 = &card_all[0];      // 構造体を指すポインタの定義.p2 = card_all と等価.

構造体を指すポインタでは,ポインタの指す値へのアクセス方法が,2種類ある.
1つは,通常のポインタ演算子 * である.構造体のメンバにアクセスするには,続けてドット演算子を用いる.

もう1つは,アロー演算子(->)であり,構造体を指すポインタ専用の記号である.

(*p).postal_code1 = 111;       // 通常のポインタ演算子
p->postal_code1   = 102;       // アロー演算子.構造体専用

(*(p2+10)).postal_code1 = 111;
(p2+10)->postal_code1   = 102;

どちらを使っても結果は同じであるので,わかりやすいほうを使用すればよい.

練習問題

  1. 上記の AddressRecord 構造体配列を指すポインタ p2 をインクリメントして,アドレス値の変化を確認してみよう.
    p2と,p2+1 の値をそれぞれ画面に表示してみよう.)
  2. 構造体のメンバ変数を増減して,アドレス値がどのように変化するか確かめてみよう.
#include <stdio.h>

// 構造体の定義
struct AddressRecord {
    ????;
};

int main(void)
{
    AddressRecord card_all[100];           // 構造体の配列
    AddressRecord *p2 = &card_all[0];      // ポインタ

    printf("%p %p\n", p2, p2+1);
    return 0;
}


    

練習問題

  1. 複素数を表す構造体を定義し,2つの複素数の積と商を返す関数 zmul(),zdiv() を作成,動作確認しよう.
    この関数では計算のみ行い,キーボード入力や画面出力はmain() で行うこと.

    ヒント:zmul(), zdiv() の引数は複素数2個,戻り値は複素数1個.

    実行例(数値は正しいとは限らない)
    
    z1=?  1.0  2.0    <-入力
    z2=? -2.0 -4.0    <-入力
    
    z1 * z2 = (3.20000, -0.30000)    <-出力
    z1 / z2 = (0.21873, -1.22763)    <-出力
    
  2. 2次元ベクトルを表す構造体 Vector2D を定義し,2つのベクトルの内積を返す関数 ip() を作成,動作確認しよう.(ip = inner product)

    実行例1(数値は正しいとは限らない)
    
    v1? =  2.0  0.5    <-入力
    v2? = -1.0 -1.0    <-入力
    v1 * v2 = 2.50000    <-出力
        
    実行例2(数値は正しいとは限らない)
    v1? = 1.0 0.0    <-入力
    v2? = 0.0 1.0    <-入力
    v1 * v2 = 0.00000    <-出力
    
  3. 日付の比較

    西暦年(year),月(month),日(day)をメンバ変数に持つ構造体 Date を定義したうえで, 2つの年月日をキーボードから入力すると,どちらが先か判定して表示するプログラムを作成せよ.
    入力時に月日に対して範囲チェックなどのエラー処理を入れるとよい.
    (可能であればうるう年の判定も実装してみよう.)

    #include <stdio.h>
    
    // 構造体の定義
    struct ???? {
        ????;
    };
    
    int main(void)
    {
        ...
        return 0;
    }
    
    
    実行例1:
    yyyymmdd(1)? 2001 03 31    <-入力1
    yyyymmdd(2)? 2002 02 28    <-入力2
    2001 03 31 が先です.    <-出力
    
    実行例2:
    yyyymmdd(1)? 2020 03 31    <-入力1
    yyyymmdd(2)? 2020 03 30    <-入力2
    2020 03 30 が先です.    <-出力
    
    実行例3:
    yyyymmdd(1)? 2031 13 31    <-入力1
    入力エラー!