前回の記事 C のポインタへのキャストをちょっと理解する話 を書き終えた後、構造体へのポインタのキャストも同じように考えられるのではと思いました。 自分の理解が合っていることを確認するために、実際に簡易的なコードを書いて動かしてみました。 備忘録も兼ねて、構造体へのポインタのキャストについて理解した内容をまとめておこうと思います。
この記事は Cの構造体へのポインタのキャストを考える で解決できなかった疑問に対する自身の回答の記事です。
実行環境と型のサイズ
以下の実行環境で動作確認用のプログラムを実行しました。
- MacBook Pro (13-inch, 2017, Two Thunderbolt 3 ports)
また、型のサイズはそれぞれ以下を想定しています。
char
: 1 byteint
: 4 bytesdouble
: 8 bytes
なお、 型のサイズなどは実行環境に依存する ので、 この記事で示すプログラムの実行例と合致しない場合があります。
構造体へのポインタのキャストの動作
構造体へのポインタのキャストの動作を確認するために、 型が異なるメンバ変数で構成された構造体 をそれぞれ定義して、動作の違いを比較してみます。 例として、整数型の座標を表す構造体と実数型の座標を表す構造体をそれぞれ定義し、簡易的な動作確認プログラムを実装します。
図1 のように、メンバ変数の型が int
型で構成される構造体 struct intCoord
と、 double
型で構成される構造体 struct doubleCoord
を定義します。
struct intCoord
のメンバ変数はそれぞれ int
型で定義されているので、メンバ変数のサイズはそれぞれ 4 bytes です。
一方、 struct doubleCoord
のメンバ変数はそれぞれ double
型で定義されているので、メンバ変数のサイズはそれぞれ 8 bytes です。
この 2 つの構造体について、以下の動作を比較します。
struct intCoord*
型として扱った場合のメンバ変数のアドレスと値を確認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 は coord1
と coord2
のメモリ配置のイメージです。
printIntCoordAddrAndValue()
で構造体の各メンバ変数の値を表示すると、 coord1
は 2
, 3
と初期化した値が表示されたのに対し、 coord2
はそれぞれ 2.71828
, 3.14159
を int
にキャストした値 2
, 3
にはなりませんでした。
逆に、 printDoubleCoordAddrAndValue()
では、 coord2
は初期化時の値 2.718280
, 3.141590
が表示されたのに対し、 coord1
は 2.000000
, 3.000000
とはならず、それぞれ 0.000000
と出力されました。
動作の比較
printIntCoordAddrAndValue()
の動作の比較
printIntCoordAddrAndValue()
では、メンバ変数 x
, y
の型がそれぞれ int
型として扱われるので、それぞれの領域が 4 bytes として扱われます。
coord1
と coord2
を引数として受け取った場合の参照先の比較イメージを図3 に示します。
coord1
を printIntCoordAddrAndValue()
に渡した場合、実際のデータのメンバ変数もサイズが 4 bytes なので、参照先がメンバ変数のアドレスと一致します。
一方、coord2
を渡した場合、実際のデータのメンバ変数のサイズが 8 bytes なのに対し、 printIntCoordAddrAndValue()
ではメンバ変数の領域は 4 bytes として扱われるので、参照先とメンバ変数のアドレスが一致しません。
さらに、 coord2
は 浮動小数点型 (正確には倍精度浮動小数点型) なので、整数型とビットの並びが異なります。
coord2
について、 arg->x
は coord2.x
の前半 4 bytes 分を、 arg->y
は coord2.x
の後半 4 bytes 分を整数として扱おうとしたので、初期化した値と異なる値として表示されました。
printDoubleCoordAddrAndValue()
の動作の比較
printDoubleCoordAddrAndValue()
の動作は printIntCoordAddrAndValue()
の逆と考えられます。
printDoubleCoordAddrAndValue()
では、メンバ変数 x
, y
の型がそれぞれ double
型として扱われるので、それぞれの領域が 8 bytes として扱われます。
coord1
と coord2
を引数として受け取った場合の参照先の比較イメージを図4 に示します。
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 となります。
この 2 つの構造体に対して、次のような処理を実行してみます。
value
に格納されている値を 1 byte ごとのchar
型として表示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
のメンバ変数 vaule
は char
型の配列なので、各要素にアクセスすることで 1 byte ごとの値を取得できます。
それぞれの要素の文字と 16 進数表記の文字コードの値を表示させています。
2 の処理は struct integer*
型の引数を受け取る手続き printInteger()
として実装します。
構造体 struct integer
のメンバ変数 value
は int
型なので、 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 は、それぞれの構造体型の変数 ch4
と intg
に格納されるデータのイメージを byte 単位で表したものです。
変数 intg
のメンバ value
のサイズは 4 bytes なので、リトルエンディアンでは value
の先頭のアドレスから byte 単位で逆順にデータが格納されます。
printChar4()
では、 value
の先頭の 1 byte から順に表示しているため、 ch4
は意図した順に表示されます。
一方、 intg
は 4 bytes のデータをリトルエンディアンで格納するので、 1 byte ごとに表示すると、逆順に表示されます。
printInteger()
は逆に、 intg
を 4 bytes 単位でリトルエンディアンで扱うと意図した通りに表示されるのに対し、 ch4
で value
を 4 bytes 分リトルエンディアンで扱うとバイトーオーダーが逆転します。
まとめ
ポインタはメモリのアドレスを表し、そのアドレスから型のサイズ分をデータの格納されている領域としてみなせます。
例えば、 int*
であれば、基準のアドレスから 4 bytes 分が領域となります。
構造体へのポインタも同じように考えられます。 メンバ変数の型が一致 していれば、異なる構造体でもポインタにキャストすることで 多相性 のように扱うことができます。
5 年以上前の疑問が、前回の記事をきっかけに解決するとは思っていませんでした。 ポインタへの理解が深まったという実感とともに、改めてポインタはシンプルだが難しいと感じました。