一週間で身につくC言語の基本

おさえておきたいプログラミングの基本

【応用編:5日目】 構造体

複数のデータをひとまとまりにしたものが構造体です。

5-1.構造体

(1) 構造体とは何か

例えば学校で学生のデータベースを作るとします。一人の学生の登録データが、学生番号、名前、年齢であったとします。

そういったプログラムを作る時、関連する変数がバラバラになっていると、取扱が非常に不便です。そこで便利なのが、構造体(こうぞうたい)という概念です。構造体とは、複数の変数をひとまとめにするものです。(図5-1)

図5-1.構造体の概念
構造体の概念

学生番号を表す整数型の変数id、名前を表す文字列name、年齢を表す整数型変数ageをひとまとめにして構造体にすると、以下のようになります。

構造体テンプレートの定義
struct student{ int id; char name[256]; int age; };

このような、構造体の定義を構造体テンプレートと言います。structが、構造体を表すキーワードであり、そのあとのstudentが構造体の名前になります。構造体の名前は、任意につけることができます。{}の中に、ひとまとめにする変数を定義します。最後は;(セミコロン)で終了します。

この構造体を実際に使用するには以下のようにすると、dataという名前の構造体変数を定義できます。

構造体変数の定義
struct student data;

では、実際に来れに代入したり、出力したりするサンプルを見てみましょう。

listex5-1:main.c
#include <stdio.h>
#include <string.h>

//	学生のデータを入れる構造体
struct student{
	int id;			//	学生番号
	char name[256];	//	名前
	int age;		//	年齢
};

int main(int argc,char** argv){
    struct student data;
	data.id = 1;	//	番号を設定
	strcpy(data.name,"山田太郎");	//	名前を設定
	data.age = 18;	//	年齢を設定
	//	データの内訳を表示
	printf("学生番号:%d 名前:%s 年齢:%d\n",data.id,data.name,data.age);
    return 0;
}
実行結果
学生番号:1 名前:山田太郎 年齢18

(2) 構造体のメンバ

構造体の成分の変数のことを、メンバと言います。このメンバにアクセスするには、通常以下のように行います。

構造体変数の成分へのアクセス
(構造体変数名).(メンバ)

このサンプルでは、構造体変数はdataなので、そのメンバidへのアクセスは、間に"."(ピリオド)をつけて、"data.id"とします。このサンプルでは、13~15行目で、それぞれの成分に値を代入しています。先頭に"data."がついているだけで、それぞれの成分へのアクセスは、普通の変数とは変わりません。(図5-2)

図5-2.構造体のメンバへのアクセス
構造体の概念

C言語には、このほかに、構造体によくにた共用体(きょうようたい)という概念も存在します。興味のある方は、以下のサイトを参考にしてみてください。

→ 共用体について

3-2.構造体配列

(1) 複数の構造体データを扱う場合

冒頭で述べたような学生データベースを作成する場合、複数の学生データを扱う必要があります。そのような場合には構造体の配列を利用する必要があります。では、構造体の配列とはどのように定義すればよいのでしょうか。

そこで次は、構造体を配列にして使用する例を紹介します。以下のプログラムを実行してみてください。

listex5-2:main.c
#include <stdio.h>
#include <string.h>

//	学生のデータを入れる構造体
struct student{
	int id;	//	学生番号
	char name[256];     //	名前
	int age;		//	年齢
};

//	構造体の名前をtypedefで定義
typedef struct student student_data;

int main(int argc, char** argv) {
	int i;
	student_data data[] = {
		{ 1,"山田太郎",18 },
		{ 2,"佐藤良子",19 },
		{ 3,"太田隆",18 },
		{ 4,"中田優子",18 }
	};
	//	データの内訳を表示
	for(i = 0; i < 4; i++){
		printf("学生番号:%d 名前:%s 年齢:%d\n",data[i].id,data[i].name,data[i].age);
	}
    return 0;
}
実行結果
学生番号:1 名前:山田太郎 年齢:18 学生番号:2 名前:佐藤良子 年齢:19 学生番号:3 名前:太田隆 年齢:18 学生番号:4 名前:中田優子 年齢:18

(2) typedef

配列変数について説明する前に、以下の処理について説明しましょう。

構造体名の変更
typedef struct student student_data;

先頭に出ているtypedefは、既存の型に新しい名前(別名)を付けるためのキーワードで、このプログラムの場合、studentという構造体をstudent_dataというなまえに変更するという事を意味します。

再定義した構造体でのデータの定義
student_data s;

のように、先頭に"struct"キーワードをつけることなく構造体変数を定義することが可能です。

次に、構造体変数への値の代入ですが、初期値の設定の場合、16行目から21行目のように、通常変数の場合のように、{}を使って値を一度に複数定義することができます。外側の{}の中に、定義する値の数だけ、{}でメンバを定義して、間を,(コンマ)で区切ります。メンバの値の定義は、メンバの並び順に正しく代入する必要があります。(図5-3)

図5-3.構造体配列とメンバの初期化
構造体配列とメンバの初期化

5-3.構造体ポインタ

(1) 構造体のポインタを活用する

最後に、構造体とポインタの使い方について説明していきましょう。以下のプログラムは、listex5-2と同じ処理をポインタを使った処理に書き換えたものです。少し長いですが、入力して実行してみてください。

listex5-3:main.c
#include <stdio.h>
#include <string.h>

//	学生のデータを入れる構造体
typedef struct{
	int id;			//	学生番号
	char name[256];	//	名前
	int age;		//	年齢
}student_data;

//	構造体のデータを表示する関数
void setData(student_data*,int,char*,int);
void showData(student_data*);

int main(int argc, char** argv) {
    student_data data[4];
	int i;
	int id[] = { 1,2,3,4 };
	char name[][256] = { "山田太郎","佐藤良子","太田隆","中田優子" };
	int age[] = { 18,19,18,18 };
	//	データの設定
	for(i = 0; i < 4; i++){
		setData(&data[i],id[i],name[i],age[i]);
	}
	//	データの内訳を表示
	for(i = 0; i < 4; i++){
		showData(&data[i]);
	}
    return 0;
}

//	データのセット
void setData(student_data* data,int id,char* name,int age){
	data->id = id;				//	idのコピー
	strcpy(data->name,name);	//	名前のコピー
	data->age = age;			//	年齢のコピー
}
//	データの表示
void showData(student_data* data){
	printf("学生番号:%d 名前:%s 年齢:%d\n",data->id,data->name,data->age);	
}

実行結果は、listex5-1と同じなので省略します。

(2) アロー演算子

本題であるポインタの構造体について説明しましょう。通常の構造体では、メンバにアクセスするのに"."を用いますが、ポインタの場合は、"->"(アロー演算子)を用います。(表5-1)

表5-1.通常の構造体とポインタの構造体(student_data)
通常の構造体構造体のポインタ
定義student_data datastudent_data* pData
メンバdata.id
data.name
data.age
data->id
data->name
data->age

showData()関数およびshowData()関数では、ポインタの形式でデータが渡ってきます。そのため、33~35行目、もしくは39行目では、アロー演算子が用いられています。ここで、値の表示およびデータの代入が行われています。

(3) 関数の処理

次にこれらの関数の中でどのような処理が行われているのかを見ていきましょう。

showData()関数は、引数として渡された構造体ポインタのpDataに対して、同じく引数として渡された値(学生番号、名前、年齢)をセットします。(図5-4)

図5-4.setData関数の処理
setData関数の処理

引数として渡される構造体のアドレスは、22~24行目のforループによって、&data[0]~&data[3]が順に与えられるので、data[0]~data[3]に値が設定されます。

showData関数は、引数として渡される構造体のメンバの値を表示します。(図5-5)

図5-5.showData関数の処理
showData関数の処理

この関数も、showData()関数の場合と同様に26~28行目のforループにより&data[0]~&data[3]が順に与えられるので、data[0]~data[3]に値が表示されます。

通常、構造体を関数の引数として渡す場合は、このサンプルのようにアドレスを渡すのが普通です。では、いったいなぜそのようなことをするのでしょう?次でそのことを詳しく説明しましょう。

5-4.ポインタ渡しとデータ渡し

(1) 構造体を引数として渡す

ポインタ渡しとデータ渡しの違いを理解するために、まずは以下のプログラムを入力・実行してみてください。

listex5-4:main.c
#include <stdio.h>

//	データを入れる構造体
typedef struct{
	int a;
	double d;
}num_data;

//	二種類の値設定関数
void dealData1(num_data data);		//	値渡し
void dealData2(num_data* pData);	//	ポインタ渡し

int main(int argc, char** args) {
    num_data n1 = { 1, 1.2f },n2 = { 1, 1.2f };
	printf("n1のアドレス:0x%x n2のアドレス:0x%x\n",&n1,&n2);
	dealData1(n1);
	dealData2(&n2);
	printf("n1.a = %d n1.d = %f\n",n1.a,n1.d);
	printf("n2.a = %d n2.d = %f\n",n2.a,n2.d);
    return 0;
}

void dealData1(num_data data)
{
	printf("a=%d f=%f\n",data.a,data.d);
	printf("dealData1にわたってきたデータのアドレス:0x%x\n",&data);
	//	値の変更
	data.a = 2;
	data.d = 2.4;
}
void dealData2(num_data* pData)
{
	printf("a=%d f=%f\n",pData->a,pData->d);
	printf("dealData2にわたってきたデータのアドレス:0x%x\n",pData);
	//	値の変更
	pData->a = 2;
	pData->d = 2.4;
}
実行結果(変数のアドレスは実行のたびに異なる)
n1のアドレス:0x2c728 n2のアドレス:0x2cf710 a=1 f=1.200000 dealData1にわたってきたデータのアドレス:0x2cf630 a=1 f=1.200000 dealData2にわたってきたデータのアドレス:0x2cf710 n1.a = 1 n1.d = 1.200000 n1.a = 2 n2.d = 2.400000

関数dealData1()、dealData2()はいずれも引数として渡された構造体のデータを表示し、値を設定しています。

dealData1()関数は引数として構造体の値そのものを渡し(値渡し)、dealData2()関数は構造体のポインタを渡しています。(ポインタ渡し)

実行結果より、値渡しの場合は、dealData1()関数に渡った引数のアドレスはもとの数とは異なります。これに対しポインタ渡しのdealData2()の場合は、同じアドレスとなります。(図5-6)

そのため、showData1()では、main側で引数として渡した変数n1とdealData1関数の引数であるdataは値は同じでも異なる変数となるため、値の変更を行っても、n1には反映されません。

これに対し、showData2()の場合は、pDataとn2はアドレスが同じであることから、pDataのメンバの値を設定することは、n2の値を設定することと同じであるため、値の変更が反映されます。

図5-6.構造体変数の引数のポインタ渡しとデータ渡しの違い
構造体変数の引数のポインタ渡しとデータ渡しの違い

これが、データ渡しとポインタ渡しの違いです。

(2) データ渡しの問題点

ポインタを渡す理由は二つあります。一つは、通常、構造体のデータのサイズは大きくなる傾向があり、引数としてそのままの値を渡すと、スタック領域を圧迫してしまったり、データのコピーという無駄な処理が起こり、二重の意味でリソースを無駄にしてしまうからです。

そして、もうひとつの理由は、ポインタ渡しであれば、関数の中で値の設定などが出来るからです。値渡しでは、前述のようにコピーが発生する上に、引数として渡ってきた構造体のデータを変更しても、呼び出しもとの値に反映されません。

練習問題 : 問題4.

一週間で学べるコースの一覧
Udemy
...
2024/10/01

Udemyでも学びましょう!

一週間でわかるC言語・C++言語がオンライン講座になりました!動画音声によってさらにわかりやすくなりました!! 1講座で2つの言語を学ぶことができる上に、練習問題の回答もダウンロードできます。

Read →
Impress一週間シリーズ
1週間でC言語の基礎が学べる本
2024/10/01

書籍化された一週間シリーズ

本講座が「1週間でC言語の基礎が学べる本」として書籍化されました!サイトの内容プラスアルファでより学習しやすくなっています!Impressより発売中です!!

Read →
Impress一週間シリーズ
...
2024/10/01

書籍化された一週間シリーズ

一週間シリーズは書籍化されています。こちらもどうぞ!

Read →
プログラマーなら欲しいグッズ
プログラミンググッズ

プログラミンググッズ

快適なプログラミング環境を構築したい人々にぜひとも揃えてほしいグッズです。

Read →
制作・管理
シフトシステム株式会社

シフトシステム株式会社

このサイトはシフトシステム株式会社によって制作・管理がなされています。

Read →