【C言語】
構造体の宣言,初期化,代入,比較,ポインタ,配列

■この記事の概要

この記事では、C言語の構造体の基本操作について解説しています。構造体の宣言、初期化、代入、配列としての使用、ポインタによるアクセス方法が具体例付きで紹介されています。また、memcmpを用いた構造体の比較時の注意点や、構造体内のパディングによる影響についても解説し、安全で効率的なコーディング手法を提案しています。


構造体枠の宣言

//構造体枠の宣言
typedef struct {
    char    		*名前;	//8bye
    unsigned char	英語;	//1byte
	unsigned char   数学;	//1byte
	unsigned char   理科;	//1byte
	//1byteの穴	gcc  -Wpadded
    int     		学生番号;//4bye
} 	構造体枠;

まずグループ化したい複数の変数をまとめて構造体の枠(型)を宣言します。
枠を宣言しただけでまだメモリ上に領域はとられていません。

構造体変数の定義と初期化

//構造体変数の定義と初期化
構造体枠 構造体変数A; 	//構造体変数の定義
構造体枠 構造体変数B = {0};//全メンバ0で初期化
構造体枠 構造体変数C = {	//メンバ別に初期化
		.名前="山田太郎",
		.英語=100,
		.数学=99,
		.理科=98,
		.学生番号=314159
};

次に構造体の変数を定義します。これでメモリ上に領域が確保されます。
・構造体変数Aは領域は確保されていますがメンバはゴミの値です。
・構造体変数Bは先頭メンバだけでなく全てのメンバが0で初期化されます。
・構造体変数Cはメンバを個別に初期化する方法です。


構造体メンバの参照

void 構造体メンバの参照(void){
	printf("名前=%s\t学生番号=%d:%d:%d:%d\n",
		構造体変数C.名前,
		構造体変数C.学生番号,
		構造体変数C.英語,
		構造体変数C.数学,
		構造体変数C.理科
	);
}

構造体のメンバは
「構造体変数.メンバ」で参照できます。


構造体の配列

構造体枠	構造体の配列[]={
    [0] ={"山田太郎",100,99,98,14142},
	[1] ={"鈴木次郎",97,96,95,17320},
	[2] ={"佐藤三郎",94,93,92,22360},
	[3] ={NULL},
};

構造体も普通に配列で宣言できます。


構造体ポインタによるメンバの参照

void 構造体ポインタによるメンバの参照(void){
	構造体枠 *p = &構造体の配列[0];
	
	for(; p->名前 != NULL; p++){
		printf("名前=%s\t学生番号=%d\n",
			p->名前,
			p->学生番号
		);
	};
}

構造体ポインタとアロー演算子->を使って
構造体メンバを参照する方法です。


構造体の代入

void 構造体の代入(void){
	構造体枠	変数;
	
	変数 = 構造体変数C;
	
	printf("名前=%s\t学生番号=%d\n",
		変数.名前,
		変数.学生番号
	);
}

構造体は=演算子を使って普通に代入(コピー)できます。


構造体の穴

void 構造体の穴(void){
	構造体枠	全体;
	printf("構造体枠=%zu\n",sizeof(構造体枠));
	
	size_t 	合計=	sizeof(全体.名前)+
					sizeof(全体.学生番号)+
					sizeof(全体.英語)+
					sizeof(全体.数学)+
					sizeof(全体.理科);
	
	if(合計 != sizeof(全体)){
		printf("メンバの合計=%zu と sizeof(全体)=%zu は違う\n",
			合計,
			sizeof(全体));
	}	
}

構造体のメンバは境界調整と言って区切りの良い番地に割り付けられます。
区切りの良い番地はCPUやコンパイラによって違うのですが、奇数番地からは割り着かないと覚えておくと間違いはないでしょう。

このコードでgccの場合メンバー「理科」の後ろに1byteの穴が空きます。

gccの場合
gcc -Wpaddedで構造体の穴が見つかりますが
穴の空き方はコンパイラに依存するので
注意が必要です。


構造体の比較

void 構造体の比較(void){
	int ret = memcmp(&構造体変数A,&構造体変数C,sizeof(構造体変数C));
	if(ret == 0){
		puts("AとCは同じ");
	}else{
		puts("AとCは違う");
	}
}

構造体の比較にmemcmpを使うのは
メジャールールCERT-Cでは非推奨です

しかしmemcmpを使わずに大量メンバを個別に比較すると比較漏れが発生するので
構造体の穴が無ければmemcmpを使ったほうが楽で安全ではないかと筆者は考えます。

参考:
https://www.jpcert.or.jp/sc-rules/c-exp04-c.html

https://wiki.sei.cmu.edu/confluence/display/c/EXP42-C.+Do+not+compare+padding+data


構造体のまとめ

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

//構造体枠の宣言
typedef struct {
    char    		*名前;	//8bye
    unsigned char	英語;	//1byte
	unsigned char   数学;	//1byte
	unsigned char   理科;	//1byte
	//1byteの穴	gcc  -Wpadded
    int     		学生番号;//4bye
} 	構造体枠;

//構造体変数の定義と初期化
構造体枠 	構造体変数A; 	//構造体変数の定義
構造体枠	構造体変数B={0};//全メンバ0で初期化
構造体枠    構造体変数C ={	//メンバ別に初期化
		.名前="山田太郎",
		.英語=100,
		.数学=99,
		.理科=98,
		.学生番号=314159
};

void 構造体メンバの参照(void){
	printf("名前=%s\t学生番号=%d:%d:%d:%d\n",
		構造体変数C.名前,
		構造体変数C.学生番号,
		構造体変数C.英語,
		構造体変数C.数学,
		構造体変数C.理科
	);
}

構造体枠	構造体の配列[]={
    [0] ={"山田太郎",100,99,98,14142},
	[1] ={"鈴木次郎",97,96,95,17320},
	[2] ={"佐藤三郎",94,93,92,22360},
	[3] ={NULL},
};

void 構造体ポインタによるメンバの参照(void){
	構造体枠 *p = &構造体の配列[0];
	
	for(; p->名前 != NULL; p++){
		printf("名前=%s\t学生番号=%d\n",
			p->名前,
			p->学生番号
		);
	};
}

void 構造体の代入(void){
	構造体枠	変数;
	
	変数 = 構造体変数C;
	
	printf("名前=%s\t学生番号=%d\n",
		変数.名前,
		変数.学生番号
	);
}

//gcc -Wpaddedで構造体の穴が見つかる
void 構造体の穴(void){
	構造体枠	全体;
	printf("構造体枠=%zu\n",sizeof(構造体枠));
	
	size_t 	合計=	sizeof(全体.名前)+
					sizeof(全体.学生番号)+
					sizeof(全体.英語)+
					sizeof(全体.数学)+
					sizeof(全体.理科);
	
	if(合計 != sizeof(全体)){
		printf("メンバの合計=%zu と sizeof(全体)=%zu は違う\n",
			合計,
			sizeof(全体));
	}	
}

//構造体の比較にmemcmpを使うのは推奨されていない
//https://www.jpcert.or.jp/sc-rules/c-exp04-c.html
//https://wiki.sei.cmu.edu/confluence/display/c/EXP42-C.+Do+not+compare+padding+data
void 構造体の比較(void){
	int ret = memcmp(&構造体変数A,&構造体変数C,sizeof(構造体変数C));
	if(ret == 0){
		puts("AとCは同じ");
	}else{
		puts("AとCは違う");
	}
}

int main(void){
	構造体メンバの参照();
	構造体ポインタによるメンバの参照();
	構造体の代入();
	構造体の穴();
	構造体の比較();	
}

gccではコンパイル可能です。
メンバを増やしたり初期化を変えたりして
遊んでみて下さい。