C言語での構造体の使い方まとめ【型、宣言、初期化、データ参照、一括代入、ポインタ等】

この記事の要点

  • 構造体とは、様々なデータ型の変数を、1つにまとめて操作できるようにしたもの
  • 構造体を直接操作するときはドット演算子、ポインタ経由で操作するときはアロー演算子を使う
  • 関数に渡す際は、値渡しとポインタ渡しを意識して使い分ける

以前、C言語とはどのような言語か、またC言語が利用される業務について解説していました。

C言語での副業はなぜ難しい?業務の種類と必要なスキルからわかるその理由

C言語でプログラミングする際、構造体を使っていますか?

構造体は、使わなくても一応はコーディング可能なため、初心者の方にはあまりなじみがないかもしれません。

しかし、構造体を使うことでコードがわかりやすくなるため、ある程度以上の規模のプログラムではほぼ必須になります。

当記事を読めば、構造体の使い方やドット演算子、アロー演算子の違い、値渡しとポインタ渡しの使い分けなどがわかります。

構造体とは

構造体とは、様々なデータ型の変数を、1つにまとめて操作できるようにしたものです。

構造体を使うことで、コーディングが楽になる、コードがわかりやすくなるといったメリットがあります。

例えば、プログラム内で顧客情報を扱うとき、年齢や名前などのデータをそれぞれ変数で持つより、構造体にまとめたほうがわかりやすくなります。

また、関数の戻り値は1つしか指定できませんが、構造体を使うことで複数のデータをまとめて戻せます。

構造体の使い方

さっそく、具体的な構造体の使い方についてご説明します。

構文

構造体の構文は以下の通りです。

構造体を定義

struct タグ名{
    データ型 メンバ名1;
    データ型 メンバ名2;
    データ型 メンバ名3;
    …
};

「メンバ」とは、構造体の1つ1つの要素のことですが、構造体内の変数と思っていただいて問題ありません。

構造体の定義では、構造体の中にどのようなデータ型のメンバを持つのかを指定します。

構造体型の変数を宣言

struct タグ名 変数名;

定義した構造体は、intなどと同じく1つのデータ型として扱います

「struct タグ名」までが1つの型になるようなイメージです。

構造体のメンバへアクセス

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

構造体のメンバへアクセスする際は、「.」を使います。

これは、ドット演算子といいます。

サンプルプログラムは以下の通りです。

// 構造体を定義
struct customer{
    char name[128];
    int age;
    char address[256];
};
// 構造体型の変数を作成
struct customer c;
// 構造体に値をセット
strcpy(c.name, "ABC太郎");
c.age = 20;
strcpy(c.address, "東京都港区××");

printf("名前は%s、年齢は%d、住所は%sです。", c.name, c.age, c.address); // 名前はABC太郎、年齢は20、住所は東京都港区××です。

ここでは、顧客情報を1つの構造体「customer」にまとめています。

customerを定義したことで、「struct customer」という構造体型の変数を定義できるようになります。

そして、ドット演算子を使って構造体の各メンバに値をセットしたり取得したりできます。

構造体の初期化

構造体も、配列などと同様に{}を使って初期化できます。

全てのメンバを一括で初期化できるので、便利です。

ただしこのとき、構造体の宣言時と同じ順番で初期値を指定しなければなりません。

// 宣言と初期化を同時に行う
struct customer c = {“ABC太郎”, 20, “東京都港区××”};

構造体の一括コピー

構造体をコピーしたいときは、以下のように代入するとすべてのメンバの値を一括でコピーできます。

struct customer c = {"ABC太郎", 20, "東京都港区××"};
struct customer copy = c;
printf("名前は%s、年齢は%d、住所は%sです。", copy.name, copy.age, copy.address); // 名前はABC太郎、年齢は20、住所は東京都港区××です。

ポインタ経由での構造体の操作

先ほどはドット演算子を使って構造体を直接操作するサンプルをご紹介しました。

次は、ポインタを経由して構造体を操作する方法をご紹介します。

構造体を指すポインタ型の変数名->メンバ名

ポインタを経由する場合は、「->」を使います。

これは、アロー演算子といいます。

サンプルはこちらです。

// 構造体を定義
struct customer{
    char name[128];
    int age;
    char address[256];
};
// 構造体型の変数を作成
struct customer c;
// ポインタ変数に構造体へのポインタを渡す
struct customer *p = &c;
// ポインタ経由で構造体にアクセス
strcpy(p->name, "ABC太郎");
p->age = 20;
strcpy(p->address, "東京都港区××");

printf("名前は%s、年齢は%d、住所は%sです。", c.name, c.age, c.address); // 名前はABC太郎、年齢は20、住所は東京都港区××です。
printf("名前は%s、年齢は%d、住所は%sです。", p->name, p->age, p->address); // 名前はABC太郎、年齢は20、住所は東京都港区××です。

先ほどとの違いは、ポインタ変数pを経由して構造体を操作しているため、ドット演算子ではなくアロー演算子を使っているところです。

ポインタを経由した場合でも、構造体のデータそのものが変更されている点に注目してください。

その証拠に、変数cのメンバの値を出力しても、ポインタ変数pを経由して出力しても、どちらも結果は同じになります。

構造体の配列

構造体は、他のデータ型と同様に配列に格納できます。

// 構造体を定義
struct customer{
    char name[128];
    int age;
    char address[256];
};
struct customer array[3] = {
    {"ABC太郎", 20, "東京都港区××"},
    {"DEF次郎", 18, "京都府京都市××"},
    {"GHI三郎", 30, "大阪府大阪市××"}
};
for(int i = 0; i < 3; i++){
    printf("名前は%s、年齢は%d、住所は%sです。", array[i].name, array[i].age, array[i].address);
}

配列の各要素には、

変数名[数字]

のようにアクセスします。

今回は配列の要素1つ1つが構造体なので、構造体のメンバにアクセスするには

変数名[数字].メンバ名

とします。

サンプルを実行すると、配列に格納した構造体の情報が順に出力されます。

関数と構造体

定義した構造体は、データ型の一種となるため、関数の引数や戻り値に指定できます。

構造体に限った話ではありませんが、C言語では引数にデータを渡す方法が、「値渡し」と「ポインタ渡し」の2通りあります。

値渡し

構造体型の変数を引数に指定すると、値渡しになります。

値渡しでは、引数に指定したデータそのものではなく、そのコピーを渡します。

struct customer c = {"ABC太郎", 20, "東京都港区××"};
func(c); // 値渡し

こうすると、変数cが持つ構造体を丸ごとコピーして、そのコピーを関数に渡します。

そのため、関数内でいくら引数のデータを操作しても、コピーのデータが変わるだけで、変数cが持つ構造体には影響がありません

ポインタ渡し

構造体へのポインタを引数に指定すると、ポインタ渡しになります。

struct customer c = {"ABC太郎", 20, "東京都港区××"};
func(&c);

こうすると、関数内ではポインタを経由して変数cが持つ構造体のデータそのものを操作できます

また、構造体のコピーが発生しないので、値渡しよりメモリを節約できます。

もしポインタについて難しいと感じていたり、ポインタの使い方やメリットがわからない場合は、ポインタを効果的に習得する方法を解説しているこちらの記事を参考にしてください。

初心者がC言語のポインタでつまずく3つの理由と理解するためのコツ

値渡しとポインタ渡しで構造体を関数の引数とした例

より具体的なサンプルプログラムを見てみましょう。

#include <stdio.h>

// student型を定義
typedef struct {
  int age;
  int id;
  char name[20];
} student;

// 値渡しで引数を受け取る関数
void print_student(student s) {
  printf("年齢:%d\n", s.age);
  printf("学生番号:%d\n", s.id);
  printf("名前:%s\n", s.name);
}

// ポインタ渡しで引数を受け取る関数
void change_student(student *s) {
  s->age = 20; // ポインタ経由でメンバにアクセスする場合はアロー演算子 -> を使う
  s->id = 1234;
}

int main(void) {
  // student型の変数st1,st2を宣言
  student st1 = {18,1111,"Alice"};
  student st2 = {19,2222,"Bob"};

  // 値渡しでprint_student関数にst1を与える
  print_student(st1);

  // ポインタ渡しでchange_student関数にst2へのアドレス(&st2)を与える
  change_student(&st2);

  // st1,st2それぞれの内容を表示する
  printf("st1:\n");
  print_student(st1); // st1は変更されていない

  printf("st2:\n");
  print_student(st2); // st2はchange_student関数で変更された

}
実行結果:

年齢:18
学生番号:1111
名前:Alice
st1:
年齢:18
学生番号:1111
名前:Alice
st2:
年齢:20
学生番号:1234
名前:Bob

まとめ

適切に構造体が使われず、大量のローカル変数が記述されたコードは、読みづらく、メンテナンスも困難です。

逆に、構造体を使いこなせるようになると、コードが整理されすっきりします。

ドット演算子とアロー演算子、値渡しとポインタ渡しなど、ポインタが絡むとややこしいのがネックですが、苦労に見合うだけのメリットは確実にあります。

値渡しではメモリ消費や処理時間が増える可能性がありますが、元の構造体変数が変更されることはありません。

ポインタ渡しではメモリ消費や処理時間が少なくて済みますが、元の構造体変数が変更される可能性があります。目的や状況に応じて使い分けましょう。

ぜひ意識的に構造体を活用してみてください。

ポインタと混同しやすい配列についても詳しく解説しています。

C言語の配列は、コピーにひと手間かかるなど、初心者がつまずきやすいポイントの1つなので、こちらの記事を参考にしてください。

C言語での配列の使い方入門【初期化・コピー・定義・ポインタとの違い】