へっぽこ社会人4年生がプログラミングを頑張る

へっぽこ社会人4年目がプログラミング系統を中心に書きたいことをつらつらと書きます

C の構造体へのポインタのキャストをちょっと理解する話

前回の記事 C のポインタへのキャストをちょっと理解する話 を書き終えた後、構造体へのポインタのキャストも同じように考えられるのではと思いました。 自分の理解が合っていることを確認するために、実際に簡易的なコードを書いて動かしてみました。 備忘録も兼ねて、構造体へのポインタのキャストについて理解した内容をまとめておこうと思います。

この記事は Cの構造体へのポインタのキャストを考える で解決できなかった疑問に対する自身の回答の記事です。

実行環境と型のサイズ

以下の実行環境で動作確認用のプログラムを実行しました。

また、型のサイズはそれぞれ以下を想定しています。

  • char: 1 byte
  • int: 4 bytes
  • double: 8 bytes

なお、 型のサイズなどは実行環境に依存する ので、 この記事で示すプログラムの実行例と合致しない場合があります。

構造体へのポインタのキャストの動作

構造体へのポインタのキャストの動作を確認するために、 型が異なるメンバ変数で構成された構造体 をそれぞれ定義して、動作の違いを比較してみます。 例として、整数型の座標を表す構造体と実数型の座標を表す構造体をそれぞれ定義し、簡易的な動作確認プログラムを実装します。

図1 のように、メンバ変数の型が int 型で構成される構造体 struct intCoord と、 double 型で構成される構造体 struct doubleCoord を定義します。 struct intCoord のメンバ変数はそれぞれ int 型で定義されているので、メンバ変数のサイズはそれぞれ 4 bytes です。 一方、 struct doubleCoord のメンバ変数はそれぞれ double 型で定義されているので、メンバ変数のサイズはそれぞれ 8 bytes です。

図1. 型が異なるメンバ変数のサイズの比較

この 2 つの構造体について、以下の動作を比較します。

  1. struct intCoord* 型として扱った場合のメンバ変数のアドレスと値を確認
  2. struct doubleCoord* 型として扱った場合のメンバ変数のアドレスと値を確認

サンプルプログラムの実装

それぞれの構造体の定義と手続きの定義の実装例を以下に示します。

#include <stdio.h>

// 整数型の座標を表す構造体
struct intCoord {
  int x;
  int y;
};

// 実数型の座標を表す構造体
struct doubleCoord {
  double x;
  double y;
};

// struct intCoord へのポインタ型の変数やそのメンバ変数のアドレスや値を表示
void printIntCoordAddrAndValue(struct intCoord *arg)
{
  printf("address of arg: %p\n", arg);

  printf("address of arg->x: %p\n", &(arg->x));
  printf("address of arg->x: %d\n", arg->x);

  printf("address of arg->y: %p\n", &(arg->y));
  printf("address of arg->y: %d\n", arg->y);
}

// struct doubleCoord へのポインタ型の変数やそのメンバ変数のアドレスや値を表示
void printDoubleCoordAddrAndValue(struct doubleCoord *arg)
{
  printf("address of arg: %p\n", arg);

  printf("address of arg->x: %p\n", &(arg->x));
  printf("address of arg->x: %lf\n", arg->x);

  printf("address of arg->y: %p\n", &(arg->y));
  printf("address of arg->y: %lf\n", arg->y);
}

int main(int argc, char **argv)
{
  struct intCoord coord1 = {2, 3};
  struct doubleCoord coord2 = {2.71828, 3.14159};

  // 変数 coord1 のアドレスを printIntCoordAddrAndValue() に渡す
  // メンバ変数の値 x, y がそれぞれ正しく表示される
  printf("printIntCoordAddrAndValue(&coord1)\n");
  printIntCoordAddrAndValue(&coord1);
  printf("\n");

  // 変数 coord2 を struct intCoord* にキャストして printIntCoordAddrAndValue() に渡す
  // メンバ変数の値 x, y は 2, 3 とはならない
  printf("printIntCoordAddrAndValue((struct intCoord*)&coord2)\n");
  printIntCoordAddrAndValue((struct intCoord*)&coord2);
  printf("\n");

  // 変数 coord2 のアドレスを printDoubleCoordAddrAndValue() に渡す
  // メンバ変数の値 x, y がそれぞれ正しく表示される
  printf("printDoubleCoordAddrAndValue(&coord2)\n");
  printDoubleCoordAddrAndValue(&coord2);
  printf("\n");

  // 変数 coord1 を struct doubleCoord* にキャストして printDoubleCoordAddrAndValue() に渡す
  // メンバ変数の値 x, y は 2.0, 3.0 とはならない
  printf("printDoubleCoordAddrAndValue((struct doubleCoord*)&coord1)\n");
  printDoubleCoordAddrAndValue((struct doubleCoord*)&coord1);
  printf("\n");

  return 0;
}

1 の処理は printIntCoordAddrAndValue() として、 2 の処理は printDoubleCoordAddrAndValue() として実装しました。 どちらの処理も引数 arg で受け取った引数のアドレスと、それぞれのメンバ変数のアドレスおよび値を表示します。

異なる構造体に直接キャストすることはできない ので、構造体へのポインタにキャスト することで、構造体のデータが格納されているアドレスを基準にそれぞれのメンバ変数が格納されるアドレスにアクセスします。

実行結果

実行結果は以下の通りでした。

printIntCoordAddrAndValue(&coord1)
address of arg: 0x7ff7b10183c8
address of arg->x: 0x7ff7b10183c8
address of arg->x: 2
address of arg->y: 0x7ff7b10183cc
address of arg->y: 3

printIntCoordAddrAndValue((struct intCoord*)&coord2)
address of arg: 0x7ff7b10183b8
address of arg->x: 0x7ff7b10183b8
address of arg->x: -1783957616
address of arg->y: 0x7ff7b10183bc
address of arg->y: 1074118409

printDoubleCoordAddrAndValue(&coord2)
address of arg: 0x7ff7b10183b8
address of arg->x: 0x7ff7b10183b8
address of arg->x: 2.718280
address of arg->y: 0x7ff7b10183c0
address of arg->y: 3.141590

printDoubleCoordAddrAndValue((struct doubleCoord*)&coord1)
address of arg: 0x7ff7b10183c8
address of arg->x: 0x7ff7b10183c8
address of arg->x: 0.000000
address of arg->y: 0x7ff7b10183d0
address of arg->y: 0.000000

printIntCoordAddrAndValue() で構造体のメンバ変数のアドレスを表示すると、 coord1, coord2 の両方で、構造体のアドレスから 4 bytes 進んでいます。 逆に、 printDoubleCoordAddrAndValue() では、メンバ変数のアドレスは構造体のアドレスから 8 bytes ずつ進んでいます。 図2 は coord1coord2 のメモリ配置のイメージです。

図2. メンバ変数のアドレスの配置イメージ

printIntCoordAddrAndValue() で構造体の各メンバ変数の値を表示すると、 coord12, 3 と初期化した値が表示されたのに対し、 coord2 はそれぞれ 2.71828, 3.14159int にキャストした値 2, 3 にはなりませんでした。 逆に、 printDoubleCoordAddrAndValue() では、 coord2 は初期化時の値 2.718280, 3.141590 が表示されたのに対し、 coord12.000000, 3.000000 とはならず、それぞれ 0.000000 と出力されました。

動作の比較

printIntCoordAddrAndValue() の動作の比較

printIntCoordAddrAndValue() では、メンバ変数 x, y の型がそれぞれ int 型として扱われるので、それぞれの領域が 4 bytes として扱われます。 coord1coord2 を引数として受け取った場合の参照先の比較イメージを図3 に示します。

図3. printIntCoordAddrAndValue() におけるメンバ変数への参照の比較

coord1printIntCoordAddrAndValue() に渡した場合、実際のデータのメンバ変数もサイズが 4 bytes なので、参照先がメンバ変数のアドレスと一致します。 一方、coord2 を渡した場合、実際のデータのメンバ変数のサイズが 8 bytes なのに対し、 printIntCoordAddrAndValue() ではメンバ変数の領域は 4 bytes として扱われるので、参照先とメンバ変数のアドレスが一致しません。

さらに、 coord2浮動小数点型 (正確には倍精度浮動小数点型) なので、整数型とビットの並びが異なります。 coord2 について、 arg->xcoord2.x の前半 4 bytes 分を、 arg->ycoord2.x の後半 4 bytes 分を整数として扱おうとしたので、初期化した値と異なる値として表示されました。

printDoubleCoordAddrAndValue() の動作の比較

printDoubleCoordAddrAndValue() の動作は printIntCoordAddrAndValue() の逆と考えられます。

printDoubleCoordAddrAndValue() では、メンバ変数 x, y の型がそれぞれ double 型として扱われるので、それぞれの領域が 8 bytes として扱われます。 coord1coord2 を引数として受け取った場合の参照先の比較イメージを図4 に示します。

図4. printDoubleCoordAddrAndValue() におけるメンバ変数への参照の比較

coord2 は実際のメンバ変数のサイズがそれぞれ 8 bytes なので、 printDoubleCoordAddrAndValue() でそれぞれ arg->x, arg->y の参照先と実際のメンバ変数のアドレスが一致します。 一方、 coord1 のメンバ変数のサイズはそれぞれ 4 bytes なので、 参照先と実際のメンバ変数のアドレスは一致しません。

加えて、 coord1 について、 arg->y は実際には coord1領域外 を参照しています。 確保されていないアドレス であったり、 他の変数で確保された領域 を参照する可能性があります。 実行例では、 arg->y の値は 0.000000 と出力されましたが、実際には値は不定です。

異なる構造体へのポインタのキャストの実験

型のサイズが合えば、キャストするメンバ変数の型が異なる場合でも意図したように動作させられるのではと考えました。 例えば、サイズ 4 bytes の int 型は、サイズ 1byte の char 型 4 つ分とちょうど同じサイズになるはずです。

図5 のように、要素数 4 の char 型の配列をメンバに持つ構造体 struct char4 と、 int 型の変数をメンバに持つ構造体 struct integer を考えます。 どちらも value のメモリサイズは 4 bytes となります。

図5. 構造体のメンバ変数のサイズのイメージ

この 2 つの構造体に対して、次のような処理を実行してみます。

  1. value に格納されている値を 1 byte ごとの char 型として表示
  2. value に格納されている値を 4 bytes の int 型として表示

実装例

1 と 2 の処理をそれぞれ実装したプログラムを以下に示します。

#include <stdio.h>

// char 型 4 つで 4 bytes のメンバ変数を持つ構造体を定義
struct char4 {
  char value[4];
};

// int 型 1 つで 4 bytes のメンバ変数を持つ構造体を定義
struct integer {
  int value;
};

// それぞれの value の要素の文字と文字コードの 16 進数表記を表示
void printChar4(struct char4 *arg)
{
  for (int i = 0; i < 4; i++) {
    char ch = arg->value[i];
    printf("%c: 0x%x\n", ch, ch);
  }
}

// value の値を 16 進数表記で表示
void printInteger(struct integer *arg)
{
  printf("value: 0x%x\n", arg->value);
}

int main(int argc, char **argv)
{
  struct char4 ch4 = { { 'h', 'o', 'g', 'e' } };
  struct integer intg = { 0x686f6765 };

  // それぞれの構造体のアドレスを表示
  printf("address of ch4: %p\n", &ch4);
  printf("address of intg: %p\n", &intg);
  printf("\n");

  // 変数 ch4 のアドレスを printChar4() に渡す
  printf("printChar4(&ch4)\n");
  printChar4(&ch4);
  printf("\n");

  // 変数 intg を struct char4* にキャストして printChar4() に渡す
  printf("printChar4((struct char4*)&intg)\n");
  printChar4((struct char4*)&intg);
  printf("\n");

  // 変数 intg のアドレスを printInteger() に渡す
  printf("printInteger(&intg)\n");
  printInteger(&intg);
  printf("\n");

  // 変数 ch4 を struct integer* にキャストして printInteger() に渡す
  printf("printInteger((struct integer*)&ch4)\n");
  printInteger((struct integer*)&ch4);
  printf("\n");

  return 0;
}

1 の処理は struct char4* 型の引数を受け取る手続き printChar4() として実装します。 構造体 struct char4 のメンバ変数 vaulechar 型の配列なので、各要素にアクセスすることで 1 byte ごとの値を取得できます。 それぞれの要素の文字と 16 進数表記の文字コードの値を表示させています。

2 の処理は struct integer* 型の引数を受け取る手続き printInteger() として実装します。 構造体 struct integer のメンバ変数 valueint 型なので、 value にアクセスすることで 4 bytes の値をひとまとまりとして扱うことができます。

注意

ch4.value を文字列として扱う場合、文字列の終端を表す '\0' も格納する必要があります。 この例では、 ch4.value を文字列としてではなく、 byte 単位で値がどのように扱われるかの確認で利用しているので、初期化時に '\0' を格納していません。

実行結果

実行結果は以下の通りです。

address of ch4: 0x7ff7bde8e3d8
address of intg: 0x7ff7bde8e3d0

printChar4(&ch4)
h: 0x68
o: 0x6f
g: 0x67
e: 0x65

printChar4((struct char4*)&intg)
e: 0x65
g: 0x67
o: 0x6f
h: 0x68

printInteger(&intg)
value: 0x686f6765

printInteger((struct integer*)&ch4)
value: 0x65676f68

1 の処理について、 printChar4(&ch4) では h, o, g, e の順に表示されているのに対し、 printChar4((struct char4*)&intg) では e, g, o, h と逆順に出力されました。 逆に 2 の処理では、 printInteger(&intg) は代入した値 0x686f6765 と表示されたのに対し、 printInteger((struct integer*)&ch4) では 0x65676f68 と byte 単位で逆順の値が表示されました。

これは、筆者の動作環境では、バイトーオーダーが トルエンディアン になっているからです。 図6 は、それぞれの構造体型の変数 ch4intg に格納されるデータのイメージを byte 単位で表したものです。 変数 intg のメンバ value のサイズは 4 bytes なので、リトルエンディアンでは value の先頭のアドレスから byte 単位で逆順にデータが格納されます。

図6. 構造体のメンバ変数のメモリと格納されている値のイメージ

printChar4() では、 value の先頭の 1 byte から順に表示しているため、 ch4 は意図した順に表示されます。 一方、 intg は 4 bytes のデータをリトルエンディアンで格納するので、 1 byte ごとに表示すると、逆順に表示されます。

printInteger() は逆に、 intg を 4 bytes 単位でリトルエンディアンで扱うと意図した通りに表示されるのに対し、 ch4value を 4 bytes 分リトルエンディアンで扱うとバイトーオーダーが逆転します。

まとめ

ポインタはメモリのアドレスを表し、そのアドレスから型のサイズ分をデータの格納されている領域としてみなせます。 例えば、 int* であれば、基準のアドレスから 4 bytes 分が領域となります。

構造体へのポインタも同じように考えられます。 メンバ変数の型が一致 していれば、異なる構造体でもポインタにキャストすることで 多相性 のように扱うことができます。

5 年以上前の疑問が、前回の記事をきっかけに解決するとは思っていませんでした。 ポインタへの理解が深まったという実感とともに、改めてポインタはシンプルだが難しいと感じました。

参考文献