構造体について

構造体は,C言語において複雑なデータ構造を扱うために不可欠です.しかし,構造体は,配列やポインタよりも遅れて詳細が規格化されたため,これまでの解説があてはまらない部分があり,注意が必要です.K&R 6章と合わせて,本解説を読んで下さい.

課題プログラムの理解 (4):CSV入力の解釈と名簿データ登録

今回は,CSV入力された文字列を分解(解析)し,名簿データとして登録する流れを作り上げることが目標です.前回までの詳細化の結果を思い出してから,先に進んでください.

構造体の定義

ここでは,日付を表す構造体dateを定義することを考えます.C言語では,

struct date {
  int y;
  int m;
  int d;
};     // ← 定義の際は,閉じカッコの後のセミコロンが必要なことに注意.

のようにして,構造体を定義することができます.

この構造体は,dateという構造体タグを持ち,それぞれint型のy,m,dというメンバを持っているといいます.

この時点では,型を定義しているだけで,具体的な変数を宣言しているわけではありません.struct date型の変数を宣言するには,更に,以下のように記述します.

struct date d = {2012, 5, 1};

この例では,struct date型の変数dを宣言するとともに,初期値として,y,m,dのメンバに,それぞれ2012,5,1を与えています.

プログラム中で各メンバを読み書きするには,d.xのように,変数名とメンバ名を.で繋ぎます.

1
2
3
4
5
6
7
struct date d;

d.y = 2012;
d.m = 5;
d.d = 1;

printf("%d-%d-%d\n", d.y, d.m, d.d);

実行結果:

2012-5-1

変数名dとメンバ名dが被っていますが,両者は区別されます.

構造体へのポインタ

基本

構造体は,効率のためにポインタ経由で受渡されることが多いため,構造体へのポインタをよく利用します.構造体へのポインタでも&*を利用します.

Listing 8 pr04-example1.c : 構造体へのポインタの例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

struct date {
  int y;
  int m;
  int d;
};

int main()
{
  struct date d = {2012, 5, 1};
  struct date *dp = &d; /* d の先頭アドレスを dp に */

  /* dp 経由でメンバにアクセス */
  printf("dp1: %d-%d-%d\n", (*dp).y, (*dp).m, (*dp).d);
  printf("dp2: %d-%d-%d\n", dp->y, dp->m, dp->d);  // 同義
  printf("dp3: %d-%d-%d\n", dp[0].y, dp[0].m, dp[0].d); // これも同義
  return 0;
}

実行結果:

dp1: 2012-5-1
dp2: 2012-5-1
dp3: 2012-5-1

リスト中,(*dp).yとあるように,カッコが必要です.これは,*演算子に比べて,.演算子の優先順位が高いからです.

構造体のポインタを利用すると,(*dp).yのような記述を頻繁に使うため,より簡便な記法として,dp->yが用意されています.この記法を用いると,

printf("%d-%d-%d\n", (*dp).y, (*dp).m, (*dp).d);

は,

printf("%d-%d-%d\n", dp->y, dp->m, dp->d);

と書けます.

構造体の配列とポインタ

以下の例は,dpが構造体の配列の先頭を指している場合の例です.

Listing 9 pr04-example2.c : 構造体の配列に対するポインタの例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>

struct date {
  int y;
  int m;
  int d;
};

int main()
{
  struct date da[] = {
    {2012, 5, 1},     /* da[0] */
    {2012, 5, 2}      /* da[1] */
  };
  struct date *dp = da;

  /* dp 経由でメンバにアクセス */
  printf("dp[1]: %d-%d-%d\n", (*(dp+1)).y, (*(dp+1)).m, (*(dp+1)).d);
  printf("dp[1]: %d-%d-%d\n", (dp+1)->y, (dp+1)->m, (dp+1)->d);  // 同義
  printf("dp[1]: %d-%d-%d\n", dp[1].y, dp[1].m, dp[1].d);        // これも同義
  return 0;
}

実行結果:

dp[1]: 2012-5-2
dp[1]: 2012-5-2
dp[1]: 2012-5-2

この場合,配列の(0番目から数えて)1番目は,

(*(dp+1)).y

と書けますが,これは,同様の置き換えを用いて,

(dp+1)->y

と書けます.また,ポインタと配列の可換性を使って,

dp[1].y

とも書けます.

dp[1]->y

ではダメですね.理由を説明できますか?講義中に答えてもらうかもしれません.

メモリ配置の様子もイメージしながら,考えてみてください.

../_images/p04-array-of-struct-table1.svg

Figure 9 構造体の配列の例

構造体の入れ子とポインタ

構造体が入れ子になっていても同じです.(参考:K&R p.156)

例として,以下のようなprofile構造体について,考えてみます.この,profile構造体は,メンバとして,date構造体のbirthdayと,date構造体へのポインタbirthday_pを持っています.

Listing 10 pr04-example3.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <stdio.h>

struct date {
  int y;
  int m;
  int d;
};

struct profile {
  struct date  birthday;
  struct date *birthday_p;
};

int main()
{
  struct date d1 = {2011, 10, 10};
  struct date d2 = {2012,  5,  1};
  struct profile pro;
  struct profile *pro_p = &pro;

  pro.birthday   = d1;
  pro.birthday_p = &d2;

  printf("pro: ")
  printf("%d-%d-%d, ",
         pro.birthday.y,
         pro.birthday.m,
         pro.birthday.d
  );
  printf("%d-%d-%d\n",
         pro.birthday_p->y,
         pro.birthday_p->m,
         pro.birthday_p->d
  );

  printf("pro_p: ")
  printf("%d-%d-%d, ",
         pro_p->birthday.y,
         pro_p->birthday.m,
         pro_p->birthday.d
  );
  printf("%d-%d-%d\n",
         pro_p->birthday_p->y,
         pro_p->birthday_p->m,
         pro_p->birthday_p->d
  );
  return 0;
}

実行結果:

pro: 2011-10-10, 2012-5-1
pro_p: 2011-10-10, 2012-5-1

ポインタと実体の区別をできるようになりましょう.

構造体のメンバのアドレス

構造体のメンバのアドレスが欲しいときはどうすればよいのでしょうか?以下のコード例を見て考えてみてください.

Listing 11 pr04-example4.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>

struct month_table {
   int  month_n;     // eg. 1, 2, ...
   char month_s[4];  // eg. "Jan", "Feb", ...
};

int main() {
   struct month_table mt[] = {
      {1, "Jan"},
      {2, "Feb"}
   };
   printf("(1) %d\n", mt[0].month_n);      // int型
   printf("(2) %p\n", &mt[0].month_n);     // int*型(int型のポインタ; mt[0].month_nのアドレス)

   printf("(3) %s\n", mt[1].month_s);      // char[]型 (char型の配列; mt[1].month_s[]の先頭アドレス)
   printf("(4) %c\n", mt[1].month_s[0]);   // char型
   printf("(5) %s\n", &mt[1].month_s[0]);  // char*型(char型のポインタ; mt[1].month_s[0]のアドレス)
   printf("(6) %c\n", *mt[1].month_s);     // char型

   printf("(7) %c\n", *(mt[1].month_s + 1)); // char型 mt.month_s[1] と同じ.
   printf("(8) %c\n", *mt[1].month_s + 1);   // char型 *mt.month_s + 1 と同じ.

   return 0;
}

実行結果:

(1) 1
(2) 0x7ffd9bed9ac0
(3) Feb
(4) F
(5) Feb
(6) F
(7) e
(8) G
(7') e
(8') G

一見して複雑な書き方は,気付かずにバグを埋め込みがちです.例えば,一番最後の2つの例 (7) や (8) は,

{
   char *p1 = mt[1].month_s;       // char*型; 文字列 "Feb" の先頭アドレス
   printf("(7') %c\n", *(p1 + 1)); // char型; 'e': 文字列 "Feb" の2文字目
}
{
   char c = *mt[1].month_s;        // char型; 'F'
   printf("(8') %c\n", c + 1);     // char型; 'G': 'F' のASCIIコードに +1 (cf. Practice 1)
}

であれば,それぞれ見覚えのあるイディオムになっていますよね.両者の意図の違いも,より明確になっているようです.

複雑なプログラムを見かけた時は,分解して考えてみることも大事です.時には,ポインタを使って "別名" をつけたり,ポインタを引数とする関数を作ることで,実装の意図を汲み取りやすくすることもできます.(参考:練習4-4 new_profile() 関数の実装

気になる人は,C言語における演算子の優先順位についても調べてみてください.(参考: K&R p.160, pp.65-66)

構造体と配列の違い

struct dateは,y,m,dの整数3つを保存するオブジェクトです.では,3つの整数を要素として持つ配列と何処が違うのでしょうか.

配列は,0, 1, 2, ... のインデックス(添字)を使ってアクセスしますが,構造体は,y,m,dのメンバ名を使ってアクセスします.この点がもっとも大きな違いですが,それ以外にもいくつか違いがあります.

ここで,struct date dがメモリ中に保存されている様子を下図に示します.int型の配列xの場合と比較してみましょう.

1
2
struct date d = {2012, 5, 1};
int x[] = {2012, 5, 1};
../_images/p04-struct-date-table1.svg

Figure 10 構造体と配列の比較

以下は,練習1の解説の再掲です.配列には,以下の性質がありましたね.

  • C言語の式は,1つの値を返すことしかできない.配列は,複数の値を持つため,全部を一度に値として返せない.

  • そこで,配列に関しては,代わりに先頭(ラベル)のアドレス (5012) を返す.

xは,図の例でいうと,{2012, 5, 1}の3つの整数からなる配列を値として持っています.しかし,C言語では,式は,1つの数を返すことになっているので,{2012, 5, 1}を返すことはできません.したがって,xの先頭アドレスである5012を代表として返すようになっているのです.

しかし,構造体にはこのルールは,あてはまりません.K&R 6.2節には,構造体に対して許される演算が示されています.

  • コピーすること,すなわちそれを1つの単位として代入すること(配列とは違う!!)

  • &でそのアドレスを求めること

  • そのメンバにアクセスすること

また,以下のことも注記されています

  • コピーと代入は,関数から値を返すこと,関数に値を渡すことも含まれる(配列とは違う!!)

  • 構造体の比較はできない (==で比較できない)

以下,構造体の特性を示すサンプルコードです.

Listing 12 pr04-example5.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>

struct date {
  int y;
  int m;
  int d;
};

int main()
{
  struct date  d1 = {2011, 5, 1};
  struct date  d2;
  struct date *dp;

  d2 = d1;  /* コピーすること,すなわちそれを1つの単位として代入すること */
  dp = &d1; /* &でそのアドレスを求めること */

  d2.y = 2012;                                 /* そのメンバにアクセスすること(1) */
  printf("d1 = %d-%d-%d\n", d1.y, d1.m, d1.d); /* そのメンバにアクセスすること(2) */
  printf("d2 = %d-%d-%d\n", d2.y, d2.m, d2.d); /* そのメンバにアクセスすること(3) */

//if (d1 == d2) /* これはエラー:構造体そのものを == で比較できない */
  if (d1.y == d2.y && d1.m == d2.m && d1.d == d2.d) /* こう書く必要がある */
    printf("d1 == d2\n");
  else
    printf("d1 != d2\n");
  return 0;
}

実行結果:

d1 = 2011-5-1
d2 = 2012-5-1
d1 != d2

比較演算子 == の扱い

構造体は==で比較すると文法違反(コンパイルできない)です.全てのメンバが一致した場合に一致したとみなすならば,こう書く必要があります:

if (d1.y == d2.y && d1.m == d2.m && d1.d == d2.d)

配列は==で比較すること自体は可能です.ただし,先頭アドレスどうしを比較するという意味なので,期待した結果になりませんね.以下,配列(文字列)を==で比較して期待した動作にならない例です:

1
2
3
4
5
6
7
char s1[] = "ABC";
char s2[] = "ABC";

if (s1 == s2)
  printf("s1 == s2\n");
else
  printf("s1 != s2\n");

実行結果:

s1 != s2

中身"ABC"は同じでも,双方の先頭アドレスは異なるので,==による比較ではダメですね.こうすると期待した動作になるでしょう:

if (strcmp(s1, s2) == 0)

この事は重要です.文字列どうしの比較に,なぜstrcmpを使うのか,説明できるようになっておいて下さい.

代入演算子 = の扱い

構造体は,コピーすること,すなわちそれを1つの単位として代入することが可能です.

1
2
3
4
struct date  d1 = {2011, 5, 1};
struct date  d2;

d2 = d1;  /* コピーすること,すなわちそれを1つの単位として代入すること */

配列は,どうでしょう.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
char s1[4] = "ABC";
char s2[4];
int i = 0;

// s2 = s1; // 文法エラー

// こう書くのが正解: 意味が分からない人は,K&R P.129 参照
while (s2[i] = s1[i])
  i++;

printf("s2: %s\n", s2);
s2: ABC

配列の先頭を表すs1,s2は,定数ですから,=で代入はできません.配列の要素を1つ1つコピーする必要があります.

while (s2[i] = s1[i]) // == ではないことに注意
  i++;

これは,標準ライブラリのstrcpy()と同じです.

配列ではなく,ポインタの場合はどうでしょうか.

1
2
3
4
5
6
char s1[] = "ABC";
char *p2;

p2 = s1; // OK だが,意図と違うかも

printf("p2: %s\n", p2);

実行結果:

p2: ABC

一見うまくいっているように見えます.p1p2が同じ"ABC"の先頭を指すことが目的であれば,意図通りです.上記の場合は,s1の中身が書き変わると,p2が指す文字列も変化することに注意してください.

1
2
3
4
5
6
7
char s1[] = "ABC";
char *p2;

p2 = s1;
s1[0] = 'X';

printf("p2: %s\n", p2);

実行結果:

p2: XBC

文字列s1の変化が文字列p2に及ばないようにするためには,

  1. 新たなメモリ領域を用意する (malloc()など)

  2. p2がその新たな領域の先頭を指すようにする

  3. s1の内容をp2が指す領域にコピーする (strcpy()など)

1
2
3
4
5
6
7
8
char s1[] = "ABC";
char *p2;

p2 = (char*)malloc(strlen(s1) + 1);
strcpy(p2, s1);
s1[0] = 'X';  // この影響を受けない

printf("p2: %s\n", p2);

実行結果:

p2: ABC

構造体を返り値とする関数

構造体と配列の違いで説明した:

  • コピーと代入は,関数から値を返すこと,関数に値を渡すことも含まれる(配列とは違う!!)

について,もう少し掘り下げてみます.

構造体は,関数の戻り値としてまるごと全部を返すことが可能なので,配列がポインタを経由して値をやりとりするのとは大きく違います.

以下の違いを意識してください.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>

struct date {
  int y;
  int m;
  int d;
};

/* これは可能 */
struct date make_date(int y, int m, int d)
{
  struct date d1;
  d1.y = y;
  d1.m = m;
  d1.d = d;

  return d1; /* {y, m, d} が返却される */
}

/* これは不可 */
int *make_array(int y, int m, int d)
{
  int x[3];

  x[0] = y;
  x[1] = m;
  x[2] = d;

  /* xの先頭アドレスが返却されるが,make_array終了後のxは無効なので不正 */
  return x;
}

int main()
{
  struct date d;
  char *x;

  d = make_date(1, 2, 3);
  x = make_array(1, 2, 3);  // ダメ!!

  printf("d: %d-%d-%d\n", d.y, d.m, d.d);
  printf("x: %d-%d-%d\n", x[0], x[1], x[2]);

  return 0;
}

実行結果:

d: 1-2-3
x: 0-0-0 /* おかしな値が返って来る.*/

make_date()は,構造体のメンバをまるごと返却しています.一方,make_array()では,xの先頭アドレスが返却されています.しかし,make_array()終了後は,関数内のローカル変数xのために確保されていたメモリは解放され,消えてしまいます.

より正確に表現すると,make_array()終了後の処理の中で,同じメモリアドレス上の値は,別の用途で上書きされる場合があります.make_array()から得たアドレスは,アドレスであることには間違いないので,アクセスはできてしまいます.しかし,そこで得られる値は,期待したものではないでしょう.

構造体のメモリ効率

構造体の中に配列が必要になった場合を考えます.以下の例を見てください.

  • pro1は,nameとして構造体の中に直接配列を用意した例.

  • pro2は,nameをポインタにして,実体を外部に置いた例.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct profile1 {
  int age;
  char name[300];
};
struct profile2 {
  int age;
  char *name;
};

struct profile1 pro1 = {
  20, "Jugemu Jugemu ...."
};

struct profile2 pro2 = {
  20, "Jugemu Jugemu ...."
};

int main()
{
  printf("sizeof(pro1): %ld\n", sizeof(pro1));
  printf("sizeof(pro2): %ld\n", sizeof(pro2));
  return 0;
}

実行結果(注:計算機環境によって異なる結果が得られる場合がある):

sizeof(pro1): 304
sizeof(pro2): 8

sizeof(pro1)とは,pro1を格納するのに必要なメモリサイズのことです.pro1は,pro2より多くのメモリを必要としています.

pro1,pro2の様子を図示すると違いがよく分かります:

../_images/p04-struct-date-table2.svg

Figure 11 構造体のメモリ効率

配列が大きくなると,代入や関数間での受渡しの際に,多くの値のコピーが発生するため,効率が悪くなります.そこで,上記pro2のように,メンバの配列をポインタにして,構造体の大きさを抑えることがあります.ただし,pro2nameの先にある2000番地から始まる配列は,別途用意しなければなりません.

上図の例では,宣言時に値を用意していますが,通常は,malloc()などの関数を利用して,データ領域を用意します.

下の例は,malloc()の利用例と,文字列データのコピー方法についての典型例です.配列としてメモリを確保した場合の処理と比較してみてください.

Listing 13 pr04-example6.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

struct profile1 {
  int age;
  char name[300];
};

struct profile2 {
  int age;
  char *name;
};

struct profile1 *new_profile1(struct profile1 *p, int age, char *name)
{
  p->age = age;

  strncpy(p->name, name, 299);
  p->name[299] = '\0';
      // Todo: We should avoid Magic numbers!!
      // See also K&R Section 1.4 (p.18).

  return p;
}

struct profile2 *new_profile2(struct profile2 *p, int age, char *name)
{
  p->age = age;

  p->name = (char *)malloc(strlen(name) + 1);
  strcpy(p->name, name);

  return p;
}

int main()
{
  struct profile1 pro1;
  struct profile2 pro2;

  new_profile1(&pro1, 20, "Jugemu Jugemu ...");
  new_profile2(&pro2, 20, "Jugemu Jugemu ...");

  printf("Pro1:\n  Age: %d\n  Name: %s\n", pro1.age, pro1.name);
  printf("Pro2:\n  Age: %d\n  Name: %s\n", pro2.age, pro2.name);

  return 0; // Note: This program contains Memory leaking problem.
}
Pro1:
  Age: 20
  Name: Jugemu Jugemu ...
Pro2:
  Age: 20
  Name: Jugemu Jugemu ...

new_profile2() 関数

まずは,new_profile2()関数のうち,特に30--31行目に注目してみましょう.

32
33
p->name = (char *)malloc(strlen(name) + 1);
strcpy(p->name, name);

31行目のmalloc()は,以下の意味があります.

  • strlen(name) + 1の大きさのメモリを確保,つまり,文字列nameの長さ+1(+1'\0'のぶん)のメモリを新たに確保して,その先頭アドレスを返す

  • 返却された先頭アドレスを(char *)とみなして(キャストして),p->nameに代入する

こうすることで,p->nameは,新たに確保されたメモリの先頭を指します.

次に,32行目でstrcpy()を用いて,namep->nameの領域にコピーしています.

このように,任意長の文字列を保存する場合は,malloc()strcpy()を併用した書き方がよく用いられます.

ここで,単にポインタの代入をする,すなわち,

p->name = name;

のような書き方は間違いです.代入演算子 = の扱いを思い出してください.

new_profile1() 関数

それでは,文字列データを配列として確保している場合,つまり,new_profile1()関数に注目してみましょう.

18
19
strncpy(p->name, name, 299);
p->name[299] = '\0';

配列を定義していれば,すでにメモリは確保されていますから,そこにコピーするだけです.単なる代入p->name = name;に問題があることはすでに指摘しましたが,ここでも同様です.

ただし,先ほどのstrcpy()関数ではなく,strncpy()関数を利用しています.また,わざわざ\0を代入する処理も追加されているようです.

まず,strcpy()strncpy()の違いは何でしょうか?わからない人はmanコマンドで,各関数の仕様を確認してください.

$ man 3 strncpy

さて,話を戻して.new_profile1()new_profile2()で,文字列コピーのためになぜ関数を使い分けているのでしょうか?そして,strncpy()を使った場合に,\0を代入する処理が,なぜ必要なのでしょうか?

考えてみてください.例えば,「練習2-2 1行入力関数 get_line() の作成」で考えたであろうことは,その一因かもしれません.

これは,講義中はもちろん,試問でも聞くかもしれませんよ.

malloc() 関数を利用した実装について考えよう

このmalloc()を利用した動的メモリ確保には,以下の利点があります.

  • 静的方法のprofile1と比べて,300文字を越える長い名前にも対応できる.

  • profile1は,短い名前の人にも常に300バイトを必要とするが,malloc()で実際に必要な長さ分だけ確保する場合は,少ないメモリで済む.

  • 何らかの関数内で確保したメモリ領域は,そのアドレスを保持しておけば,関数の処理を終えた後でも利用できる(通常,関数内の変数は終了と同時に利用できなくなる.)

一方,欠点は,以下の通りです.

  • 自分でメモリを確保しなければならない

  • 不要になった場合は,free()関数を用いて自分で解放しなければならない

  • 性能(実行スピード)が低下する恐れがある

特に,不要になったメモリの解放忘れは,メモリリークと呼ばれるバグで,しばしば問題になります.また,メモリの確保解放を頻繁に繰り返すと,性能低下を招くこともあります.

char *p = (char)malloc(100);
// 何らかの処理
free(p);
// freeした後,ポインタ p が指す先のデータ (mallocで確保したメモリ) は使えない.

最後に

上記の「欠点」は,本当に欠点でしかないのでしょうか?今回の実装例も読みながら,よく考えた方がいいかもしれません.