のむログ

技術メモ / 車 / 音楽 / 雑記 / etc...

こちらは旧ブログになります。

新ブログはこちらに移行しました🙇

【C/C++】簡単な構造体を使ったプログラムの書き方

はじめに

同じデータを持つ情報を扱う際には、共通のものを一つのデータとして扱った方が便利です。その際に構造体を使います

構造体の操作方法

構造体とは、「いろいろな種類のデータをまとめて、1つのかたまりにしたもの」です。 たとえば、「名前, 性別, 年齢, 身長, 体重」などのデータを一人分だけまとめたもののことを言います。 構造体を構成する要素を、構造体のメンバと呼ぶ。上の例では、 「名前」「性別」「年齢」「身長」「体重」などが、メンバにあたる。

宣言方法

構造体は、一つのデータ型であり、その型枠を初めに宣言する必要があります。 その後、その型枠を型とする変数を宣言する形で構造体の実体(オブジェクト)を宣言し、それを使用することができる。

構造体の型枠の宣言と、その型枠をもつ構造体変数の宣言は次のようになされる.

struct 構造体タグ名 {メンバの並び};   /* 型枠の宣言 */

struct 構造体タグ名 構造体変数名;     /* 構造体変数の宣言 */

これに先ほどのメンバを当てはめ、構造体変数の宣言を行うと

struct Person {
    char name[20];
     int sex;
  // char sex;
     int age;
  double height;
  double weight;
};

struct Person p;

ここでint sexchar sexの二つ書いているのは、charの場合だとman, womanという感じですが、intだと0: man, 1: womanみたいにできるのでその方が楽かもしれませんね。今回はintで定義したいと思います。(しかしサイズ的にはcharは1ですがintは4なので、メモリを節約したい人はcharの方がいいかもしれませんね) また、構造体変数の宣言時にstructと書くのが面倒ならば、

typedef struct Person {
    char name[20];
     int sex;
  // char sex;
     int age;
  double height;
  double weight;
} person_s;

person_s p;

とかけば、構造体オブジェクトの宣言の際の記述を減らすことができます。

構造体メンバの初期化

各メンバの初期化方法として次のようなものがあります

person_s p = {"Tom", 0, 20, 175.2, 66.5};

// または

person_s p;
strcpy(p.name, "Tom");
p.sex  = 0;

各メンバの値がすでに分かっている場合は前者のように一括代入が可能である。 しかし、ほとんどの場合は各メンバの値はわかっていないことが多いです。そのような時には、後者のやり方です。構造体変数を定義し構造体メンバを参照して代入します。 このように「.」を使って参照する方法を直接参照といいます。

構造体の代入

同じ型を用いた構造体変数同士であれば、構造体を代入することができます。

person_s p1 = {"Tom", 'M', 19, 175.2, 69.5};
person_s p2;
person_t p3;
    
p2 = p1; // 代入OK
p2 = p3; // 代入NG

構造体の配列

構造体もintなどと同様に配列を用いて宣言することができます。

#define PERSON_NUM 5

person_s p[PERSON_NUM];

p[3]のnameにアクセスしたい場合はp[3].nameでアクセスすることができます。 また構造体配列の初期化方法は先ほどと同じようにすることができます。

person_s p[PERSON_NUM] = {
    {"Bob",      'M', 19, 165.4, 72.5},
    {"Alice",    'F', 19, 161.7, 44.2},
    {"Tom",      'M', 20, 175.2, 66.3},
    {"Stefany",  'F', 18, 159.3, 48.5},
    {"Leonardo", 'M', 19, 172.8, 67.2}
};

// または

person_s p[PERSON_NUM];

strcpy(p[3].name, "Tom");
p[3].sex  = 0;

構造体をメンバに持つ構造体

これまでは構造体メンバにintやcharといったものを宣言してきたが、次からは構造体メンバに構造体を持った構造体を宣言します。

typedef struct {
    person_s boy;   /* カップルのうちの男の子の情報 */
    person_s girl;  /* カップルのうちの女の子の情報 */
    int month;      /* 交際歴(月数) */
} couple_s;

couple_s cpl = {
    {"Tom",     0, 20, 175.2, 66.3},
    {"Stefany", 1, 18, 159.3, 48.5},
    8
};

この時、構造体person_sを構造体メンバに持った構造体couple_sを宣言しました。 ここから、couple_sのメンバboyのメンバnameにアクセスするときにはcpl.boy.nameでアクセスすることができます。 また、このように代入することもできます

person_s newboy  = {"Leonardo", 0, 19, 172.8, 67.2};
person_s newgirl = {"Sara",     1, 19, 162.5, 49.3};

cpl.boy  = newboy;
cpl.girl = newgirl;
cpl.month = 1;

関数と構造体

関数の引数に構造体

ここまでは構造体を単体で利用していましたが、より高度なプログラムを書くために関数と一緒に使っていきたいと思います。 ここで、例題として直線の式の構成を表す構造体を考えます

typedef struct {
    double a;
    double b;
} sample_s;

次の関数printFormula( sample_s f )はsample_s構造体を受け取り、式を表示させる関数です。

void printFormula( sample_s f ) {

    printf("%.3lf x + %.3lf", f.a, f.b);
}

int main ( void ) {
    
    // 1, 2どちらの初期化方法でも良い
    // 1
    sample_s f = { 3.5, 2.0 };
    
    // 2
    sample_s f;
    f.a = 3.5;
    f.b = 2.0;
    
    printFormula( f );
    
    return 0;    
}

3.50 x + 2.00が表示されます。

関数の返り値に構造体

構造体を返り値に指定して関数を定義します。次の関数addFormulaはsample_s構造体を2つ引数にして傾き,切片の和をsample_s構造体にして返します。

sample_s addFormula ( sample_s f1, sample_s f2 ) {

    sample_s f3;
    f3.a = f1.a + f2.a;
    f3.b = f1.b + f2.b;
    
    return f3;
}

int main ( void ) {
    
    sample_s f1 = { 1.5, 5.0 };
    sample_s f2 = { 4.0, 2.5 };
    sample_s f3;
    
    f3 = addFormula( f1, f2 );
    printFormula( f3 );
    
    return 0;
}

5.50 x + 7.50が表示されると思います。

関数の引数に構造体ポインタ

次の関数は構造体ポインタ(3つ)を引数とし、最後の構造体ポインタがさす場所に結果を代入します。

void addFormulaPtr( sample_s *f1, sample_s *f2, sample_s *f3 ) {
    
    (*f3).a = (*f1).a + (*f2).a;
    (*f3).b = (*f1).b + (*f2).b;
}

int main ( void ) {
    
    sample_s f1 = { 5.0, 1.0 };
    sample_s f2 = { 3.3, 1.5 };
    sample_s f3;
    
    addFormulaPtr( &f1, &f2, &f3 );
    printFormula( f3 );
    
    return 0;
}

8.30 x + 2.50が表示されます。

また、addFormulaPtrは次で説明する間接メンバ参照演算子(アロー演算子)を用いて以下のように書くことができる。

void addFormulaPtr( sample_s *f1, sample_s *f2, sample_s *f3 ) {
    
    f3->a = f1->a + f2->a;
    f3->b = f1->b + f2->b;
}

このように「->」を使って参照することを間接参照といいます。

構造体のメンバを直接参照するための演算子 "." は、 「メンバ直接参照演算子」 あるいは単に「メンバ参照演算子」あるいは「ドット演算子」などと呼ばれる。 また、ポインタを介して構造体のメンバを間接参照するための演算子 "->" は、 「メンバ間接参照演算子」あるいは「アロー(矢印)演算子」などと呼ばれる。

ついでに共用体

構造体とよく似たものに、共用体(union)と呼ばれるものがある。それは構造体と同じく、いろいろな型のデータをまとめたものであり、その定義の仕方、メンバへの参照の仕方なども構造体と同じである。ただ、違っているのは、そのメンバが同じメモリを共用しているという点である.

したがって、共用体型の一つの変数のメンバたちは、そのうちのどれか一つしか使用できず、同時には使用できない.

例えば、人のデータ(名前,性別,年齢,...)をまとめたものを共用体として作った場合、その定義は次のようになる.

union _person {         /* _person がタグ名 */
    char name[20];         /* 文字配列型のメンバ name */
     int sex;          /* 文字型メンバ sex */
     int age;          /* 整数型メンバ age */
    double height;     /* 倍精度実数型メンバ height */
    double weight;     /* 倍精度実数型メンバ weight */
};

union _person p;       /* p という名前の union _person 型変数を宣言 */

この共用体型変数 p のメンバは,次の図のように同じメモリ上に重なるように配置される。

f:id:nomunomu0504:20190502021044p:plain

したがって,共用体全体のメモリサイズはそのメンバの最大のメモリサイズに一致する。(この union _person の場合は, name が 20 バイトで最大であるから,共用体全体のサイズも 20 となる)

共用体には、非常に特殊な使い道があるがここではその説明はしないでおきます。

f:id:nomunomu0504:20190411151221p:plain:w0