構造体【C言語講座 #14】

構造体【C言語講座 #14】

今回の内容は構造体です。構造体というタイトルですがついでに列挙型と共用体についてもここで説明しちゃいます。キーワードとしてはtypedef, struct, enum, union, アロー演算子あたりですかね。

文章量が前回の2倍以上になっていると思います。(なるべく削ろうと努力はしました!!)

前回の復習

前回はアドレスとポインタについてのお話しをしました。
アドレスというのはメモリ上の場所を示すために用いられている16進数の数値のことでしたね。このアドレスを保持するための変数がポインタ変数と呼ばれるのでした。

普通の変数のアドレスを取得するためには『&』をつければ良いことも学びましたね。逆にポインタの保持しているアドレスの先に格納されている値にアクセスするためにはポインタ変数の前に『*』をつける必要があるのでした。

今回もポインタ変数がちょっとだけ出てくるので理解が怪しい人は先にポインタとアドレス【C言語講座 #13】を読み直すことをお勧めしておきます。

前回の練習問題の解答例

A問題

次のソースコードは『&』と『*』を間違えているためコンパイルエラーが発生します。『&』と『*』の記号を変えたり削除したりしてコンパイルエラーが出ないプログラムにしてください。(ただしpはポインタ変数にしてください)

#include <stdio.h>

int main() {
  int x = 100;
  int *p = *x;
  printf("ポインタの保持しているアドレス: %p\n", *p);
  return 0;
}

解答例

#include <stdio.h>

int main() {
  int x = 100;
  int *p = &x;
  printf("ポインタの保持しているアドレス: %p\n", p);
  return 0;
}

よくわからなかった人は前回の記事を読んでみてください。それでもわからなかったら前回の記事のコメント欄に『わからなかった』と書いてください。(説明をもう少し詳しく書けないか考えてみます)

B問題

C++の標準関数にswap関数というものが存在します。これをC言語で再現したものが以下の自作関数になります。機能は2つの値を入れ替えるものです。この関数を実際に使ってみてください。但し関数を呼び出すときの引数にはアドレスを渡してあげる必要があることに注意してくださいね。

void swap(int *x, int *y) {
  int temp = *x;
  *x = *y;
  *y = temp;
  return;
}

解答例

#include <stdio.h>

void swap(int *x, int *y) {
  int temp = *x;
  *x = *y;
  *y = temp;
  return;
}

int main() {
  int a = 1, b = 2;
  printf("a = %d, b = %d\n", a, b);
  swap(&a, &b);
  printf("a = %d, b = %d\n", a, b);
  return 0;
}

もしC言語で競技プログラミングをする人はswapのような自作関数を作っておくことをお勧めします。

構造体

ここから今回の内容になります。まずは構造体って何って感じだと思うので、構造体について説明するところから始めていきます。

構造体というのはデータをまとめて扱うために用意された文法になります。この説明だけ聞くと『配列と何が違うの?』という質問が出てきそうなので先に答えておくと、いろんな型のデータをまとめて扱うことができるのが構造体で、逆にint型と決めたらそれのみしか扱えないのが配列になります。ちなみに構造体の中にあるデータのことメンバと呼びます。

構造体についてイメージがイマイチ出来ていない人のために簡単な例を交えてサンプルコードを書いてみます。例として生徒個票を考えてみましょう。生徒個票には様々な項目がありますよね。名前、生年月日、性別、年齢、趣味などいろいろありますが、ここでは簡単のために名前、性別、年齢の3項目だけからなる生徒個票だとしましょう。この生徒個票に対して構造体を用いると名前(char型の配列)、年齢(int型)、性別(char型)のデータをひとまとめに扱うことができるようになるわけです。

例をそのまま用いて構造体の基本形を書くとこんな感じになります。

struct 生徒個票 {
  char *名前;
  int 年齢;
  char 性別;
};

このように書くことで生徒個票の各項目(メンバ)を含む生徒個票という名前の構造体を作ることができるわけです。

struct 構造体名 {
  メンバ1;
  メンバ2;
};

例に沿った構造体をつくると以下のようなサンプルコードになります。

#include <stdio.h>

struct students {
  char *name;
  int age;
  char gender; //m(male) or f(female)
};

int main() {
  return 0;
}

実行結果

大抵の場合はこのようにプリプロセッサ指令の直下に書くことが多いですね。(特に決まってるわけではないけど)

実はここまで作った構造体というのは鋳型みたいなもので、実際に使う際には実体を作ってあげなければ使えません。(消しゴムハンコで彫刻刀で彫ったばかりの段階みたいな感じ)

ここから下では構造体から実体を作ってみます。まずは上の例をそのまま流用して以下の生徒個票を作ってみましょう。

名前: Tsuyoshi
年齢: 50
性別: 男

#include <stdio.h>

struct students {
  char *name;
  int age;
  char gender; //m(male) or f(female)
};

int main() {
  struct students person;
  person.name = "Tsuyoshi";
  person.age = 50;
  person.gender = 'm';
  return 0;
}

実行結果

10行目で実体を作成して、11行目から13行目で情報を代入しています。それぞれの処理についてもう少し見ていきましょう。

10行目の形をよく見てください。

struct 構造体名 実体名;

となっています。これを『struct 構造体名』までをセットにして型のように見立ててもらうと分かるのですが変数なんかの宣言と同じことをしています。その証拠に以下のような自作関数のコードを書いてもエラーになりません。

struct 構造体名 関数名(引数) {
  struct 構造体名 st;
  return st;
}

tc1402.cのコードの11行目から13行目については代入処理を行っています。『.』は日本語でいう『の』にあたるもので、今回のコードだと『personのnameに”Tsuyoshi”を代入』みたいな感じです。構造体のメンバ(項目)には基本的に『.』を用いてアクセスすることになるので覚えておいてくださいね。

上のところまでを一旦整理するためのサンプルコードをおいておきます。

#include <stdio.h>

struct goods {
  char *name;
  int price;
  int remain;
};

struct goods makeGoods(char *c, int a, int b);

int main() {
  struct goods items[3];
  for(int i = 0; i < 3; i++) {
    char type[100];
    int x, y;
    printf("商品名を入力してください : ");
    scanf("%s", type);
    printf("値段を入力してください : ");
    scanf("%d", &x);
    printf("在庫数を入力してください : ");
    scanf("%d", &y);
    items[i] = makeGoods(type, x, y);
  }
  return 0;
}

struct goods makeGoods(char *c, int a, int b) {
  struct goods temp;
  temp.name = c;
  temp.price = a;
  temp.remain = b;
  return temp;
}

実行結果

『struct 構造体名』の配列を作ることができます。構造体を使うときは大体配列と初期化用の自作関数を作ることになると思うので、上記のサンプルコード(tc1403.c)が理解できるレベルには理解を深めておきましょう。

ここまでくるとひとつの気になってくることがあります。それは「『構造体名』だけでよくない?毎回structを書くのが面倒くさいんだけどなー」という意見です。確かにstructをいちいち書くのは面倒くさいですよね。これを解決する文法として『typedef』があります。

構造体の鋳型の方を作成するときにtypedefを使います。例えば以下のサンプルコードがその例になります。

#include <stdio.h>

typedef struct pair {
  int a;
  int b;
} p;

p makePair(int x, int y);

int main() {
  p array[5];
  for(int i = 0; i < 5; i++) {
    int x, y;
    scanf("%d %d", &x, &y);
   array[i] = makePair(x, y);
  }
  return 0;
}

p makePair(int x, int y) {
  p temp;
  temp.a = x;
  temp.b = y;
  return temp;
}

実行結果

先ほどまで『struct 構造体名』で書かれていたものがtypedefを使って構造体を作るだけでstructをつけない書き方ができるようになります。このときに使うのは構造体の一番最後につけた名前になります。(上のサンプルコードの場合は『p』ですね)

typedefを用いた構造体の作り方は以下のような形になります。サンプルコードからも分かる通り2箇所にある構造体名は一致している必要はありません(一致していても構いません。なんなら下の1行目の構造体名の部分は省略できちゃいます)。

typedef struct 構造体名 {
  メンバ1;
  メンバ2;
} 構造体名;

基本的な構造体についての説明は終わったのでもう少し掘り下げていきます。まずは構造体の実体をポインタにしてみたらどうなるのかというお話をしていきます。

いきなりですが下のサンプルコードを書いてみてください。

#include <stdio.h>

typedef struct {
  int a;
  int b;
} pair;

pair makePair(int x, int y);

int main() {
  pair p, *q;
  p = makePair(2, 3);
  q = &p;
  printf("%d, %d\n", p.a, p.b);
  printf("%d, %d\n", q->a, q->b);
  return 0;
}

pair makePair(int x, int y) {
  pair p;
  p.a = x;
  p.b = y;
  return p;
}

実行結果

2, 3
2, 3

ポインタにする方法は前回の記事でも述べた通り『*』をつけるだけですね。(11行目)

普通の変数pは12行目で自作関数のmakePairを呼び出して初期化を行っています。その変数pのアドレスを13行目でポインタ変数qに代入しています。ここまではなんとなく感覚通りで問題ないと思います。問題は14行目と15行目の違いですよね。これは14行目のpが『普通の変数』なのでメンバにアクセスするときには『.』を使う必要があり、15行目のqは『ポインタ変数』なのでメンバにアクセスするときには『->(アロー演算子)』を使う必要があるというだけの違いです。文法上そうなっているのでしょうがないです。(諦めて覚えてくださいね)

以上が構造体についてのお話でした。今回の記事のメインだけあってなかなか理解しづらいところだと思います。わからなかったところは後でゆっくり読み返すなりプログラムを写経するなりして理解できるまで粘ってみてください。

構造体とは複数種類の型のデータをひとまとまりに扱うことができるもののことを指す。このとき内側に定義されているものをメンバと呼ぶ。

構造体のメンバにアクセスする際には『.』を、構造体のポインタでメンバにアクセスする際には『->(アロー演算子)』を使う。

列挙型

ここからは列挙型enumについて話していきます。

列挙型というのは、新しい型を作ることができるものです。(私も自作関数のところであげた関数電卓のコードのヘッダーファイルの中に使っています)

新しい型と言ってもそんなに大袈裟なものではないです。例えば曜日を管理する変数が欲しいとします。曜日は7つしかないので0~6までの数値さえあれば区別できますよね。これをint型で管理してもいいのですが、enum(列挙型)を使うことで人が見てわかりやすい形で管理することができるわけです。

何を言っているのか言葉だけじゃわかりづらいと思うので早速コードを試してもらいましょう。

#include <stdio.h>

enum week {
  Sunday,
  Monday,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday
};

int main() {
  enum week w; //week型の変数wを宣言
  w = Monday;
  printf("%d\n", w);
  return 0;
}

実行結果

1

このように『enum week』を構造体のときと同様にひと塊に見立ててあげると変数の宣言と見ることができます。このenum week型の変数にはSunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturdayまたは数値しか代入できないようになります。これが新しい型を作るという話です。

また実行結果が1となっているのはenum week型が内部の処理的には数値に変換されているためです。enumの中の要素が上から順に0, 1, 2, ……と自動的に割り当てられています。

上のコードでも分かると思いますが一応enumの基本形を載せておきます。形自体は構造体に似ていますね。

enum 列挙型の名前 {
  値の定義1,
  値の定義2
};

typedefを使った書き方もでき、構造体の時と同様に2箇所の列挙型の名前は同じにする必要はありません。(1行目の列挙型の名前は省略できます)

typedef enum 列挙型の名前 {
  値の定義1,
  値の定義2
} 列挙型の名前;

列挙型が実は内部的には数値で管理されていることを先ほど述べました。実は列挙型に対して数値演算が行えます。(あまり使う場面はありませんが)

列挙型をうまく使うと以下のように本来C言語には搭載されていないはずのbool型(Boolean型)を再現することができます。

#include <stdio.h>

typedef enum {
  false = 0,
  true = 1
} bool;

int main() {
  bool flag = true;
  if(flag) printf("成り立っている\n");
  else printf("成り立っていない\n");
  return 0;
}

実行結果

成り立っている

さりげなくenumの中で数値を指定していますが、こちら側が指定しない場合は自動的にうえから0, 1, 2, ……と割り振られていくので特に気にしなくても大丈夫です。

正直enumの使い道はタグづけとかRPGゲームの麻痺状態や毒状態みたいなものとかぐらいなので、ここまでしっかりと勉強しておけば列挙型(enum)については十分すぎるくらいでしょう。(enumの内側は数値なのでswitch-case文と相性がいいのも覚えておくと役に立つかもしれないですね(情報過多ぎみ))

列挙型とは新しい型を作ることができる仕組みのことで、内部では整数値で管理されている。そのためswitch-case文との相性が良い。

共用体

残りは共用体unionについて話していきます。

共用体というのはメモリに制約をつけた構造体のようなもので、どれか一つのメンバだけを保持できるもののことを指します。具体例を挙げて話すとlong型のメンバ(4Byte)とchar型のメンバ(1Byte)の2つからなるunion(共用体)を作成したとします。このとき共用体の情報量は一番大きなメンバに合わせて4Byteにします。このメモリ上に確保した4Byteの領域を上書きして使うことでメモリの使用量を抑えています。

具体的なソースコードを書いて共用体の勉強をしていきましょう!

#include <stdio.h>

union character {
  char c;
  int n;
};

int main() {
  union character ch;
  ch.n = 12;
  ch.c = 'a';
  printf("共用体のchar型が保持している文字は%cです\n", ch.c);
  printf("共用体のint型が保持している数値は%dです\n", ch.n);
  return 0;
}

実行結果

共用体のchar型が保持している文字はaです
共用体のint型が保持している数値は97です

実行結果について少し説明しておきますが、ascii文字コードにおいて『a』は『0x61(= 97)』なので、13行目のprintf関数の結果が『97』になっています。このことからもunionのメンバ間で同じ領域を使っていることが分かると思います。

もしunionではなくstruct(構造体)を使用した場合は、10行目で『ch.n = 12』と代入が行われている数値がそのまま残るので13行目の出力結果は『12』になります。

unionの基本形を下に載せておきます。組み込みでメモリを節約しなければならない場面などではない限り構造体を使えば済む話なので使用頻度はかなり低いのが現状ですね。(共用体名と変数名は片方省略しちゃっても大丈夫です)

union 共用体名 {
  メンバ1;
  メンバ2;
} 変数名;

上で変数名と書いてある通り、変数名をつけて共用体を作れば宣言できちゃいます。以下のサンプルコードを眺めてもらえれば分かると思うので参照してください。(union 共用体名 変数名;という宣言が一切書かれていない点に注目)

#include <stdio.h>

union numType {
  int a;
  double b;
} num;

int main() {
  num.a = 10;
  printf("int型の値は%dです\n", num.a);
  return 0;
}

実行結果

int型の値は10です

またunionのポインタを宣言すると構造体のときと同じように、アロー演算子を用いなければメンバにアクセスできなくなります。一応unionのポインタで作ったやつを載せておきますが構造体のポインタが理解できていればそれと同じ要領でかけるので流し読み程度で大丈夫です。

#include <stdio.h>

union numType {
  int a;
  double b;
};

int main() {
  union numType *num;
  num->a = 10;
  printf("int型の値は%dです\n", num->a);
  return 0;
}

実行結果

int型の値は10です
共用体とはメモリを節約した構造体のことである。恒常的に保存しておけるのはひとつのメンバだけである。組み込みの開発などの使えるメモリが限られた場合のみ出番がある程度。

まとめ

今回の記事はだいぶ分厚くなりましたね……。(少し反省気味)

構造体、列挙型、共用体をそれぞれ説明してきました。正直構造体以外はなくても困らないとは思うのですが、使いこなせると非常に便利なので余裕がある方は是非習得してくださいね。

今回の内容をざっとまとめておきます。

  • 構造体とは複数の種類のデータ型の値をまとめて扱うことができるもののことで、メンバのアクセスには『.』や『->』を使う。
  • 列挙型とは新しい型を作るためのもので、内部では整数値で管理されている。
  • 共用体とはメモリを節約した構造体のようなもので複数のメンバに対して同時に値を保持することができない。メンバのアクセスには『.』や『->』を使う。

次回は自作ヘッダーファイルについて勉強していきましょう!今回は練習問題を減らして分量を削ります。物足りない人は復習をしておいてくださいね。

練習問題

A問題

次のソースコードは『.』と『->』を間違えているためコンパイルエラーが発生します。『.』と『->』の記号を変えてコンパイルエラーが出ないプログラムにしてください。(ただしpはポインタ変数にしてください)

#include <stdio.h>

typedef struct {
  char *name;
  int age;
  char gender;
} students;

void setStudents(students *t, char *c, int a, char b) {
  t->name = c;
  t.age = a;
  t.gender = b;
  return;
}

int main() {
  students st;
  char ch[] = "takeshi";
  int x = 55;
  char s = 'm';
  setStudents(&st, ch, x, s);
  printf("%s, %d, %c\n", st.name, st.age, st->gender);
  return 0;
}

最後まで記事を見ていただきありがとうございます。また別の記事でお会いできることを祈っております。

Print Friendly, PDF & Email

C言語カテゴリの最新記事