Cのユニオンに関する質問-1つのタイプとして保存し、別のタイプとして読み取る-それは実装定義ですか?
-
06-07-2019 - |
質問
私はK& RからCのユニオンについて読んでいましたが、理解している限り、ユニオンの単一の変数はいくつかのタイプのいずれかを保持でき、何かが1つのタイプとして保存され、別のタイプとして抽出された場合、結果は純粋に実装が定義されています。
次のコードスニペットを確認してください:
#include<stdio.h>
int main(void)
{
union a
{
int i;
char ch[2];
};
union a u;
u.ch[0] = 3;
u.ch[1] = 2;
printf("%d %d %d\n", u.ch[0], u.ch[1], u.i);
return 0;
}
出力:
3 2 515
ここでは、 u.ch
に値を割り当てていますが、 u.ch
と u.i
の両方から取得しています。実装は定義されていますか?または私は本当に愚かなことをしていますか?
他のほとんどの人にとっては非常に初心者に思えるかもしれませんが、その出力の背後にある理由を理解することはできません。
ありがとう。
解決
これは未定義の動作です。 u.i
と u.ch
は同じメモリアドレスにあります。そのため、一方への書き込みと他方からの読み取りの結果は、コンパイラー、プラットフォーム、アーキテクチャー、そして場合によってはコンパイラーの最適化レベルに依存します。したがって、 u.i
の出力は常に 515
とは限りません。
例
たとえば、私のマシンの gcc
は、 -O0
と -O2
に対して2つの異なる回答を生成します。
-
私のマシンは32ビットのリトルエンディアンアーキテクチャであるため、
-O0
では2つの最下位バイトが2と3に初期化され、2つの最上位バイトが初期化されません。したがって、ユニオンのメモリは次のようになります。{3、2、garbage、garbage}
したがって、
3 2 -1216937469
のような出力が得られます。 -
-O2
を使用すると、3 2 515
の出力が得られます。これにより、ユニオンメモリが{3、2、0、 0}
。起こるのは、gcc
が実際の値を使用してprintf
の呼び出しを最適化するため、アセンブリ出力は次のようなものになります。#include <stdio.h> int main() { printf("%d %d %d\n", 3, 2, 515); return 0; }
値515は、この質問に対する他の回答で説明されているように取得できます。本質的には、
gcc
が呼び出しを最適化したときに、初期化されないユニオンのランダム値としてゼロを選択したことを意味します。
ある組合員に書き込み、別の組合員から読むことは、ほとんど意味がありませんが、厳密なエイリアシングでコンパイルされたプログラムに役立つ場合があります。
他のヒント
言語の仕様は時間とともに変化するため、この質問に対する答えは歴史的文脈に依存します。そして、この問題はたまたま変更の影響を受けます。
K&amp; Rを読んでいると言いました。その本の最新版(現在)は、C言語の最初の標準化されたバージョン-C89 / 90について説明しています。そのバージョンのC言語では、あるメンバーを作成して別のメンバーを読み取ることは、未定義の動作です。 実装定義(これは別のことです)ではなく、未定義の動作です。この場合の言語標準の関連部分は6.5 / 7です。
今、C(技術正誤表3が適用された言語仕様のC99バージョン)の進化の少し後の時点で、型のパニングのためにユニオンを使用する、つまりユニオンのメンバーを書き、次に別のメンバーを読むことが突然合法になりました。
これを実行しようとすると、未定義の動作が発生する可能性があることに注意してください。読み込んだ値が、読み込んだ型に対して無効(「トラップ表現」と呼ばれる)になった場合、動作は未定義です。それ以外の場合、読み取る値は実装定義です。
int
から char [2]
配列にパンニングするタイプの場合、特定の例は比較的安全です。 C言語では、オブジェクトのコンテンツをchar配列として再解釈することは常に合法です(これも6.5 / 7)。
ただし、その逆は当てはまりません。ユニオンの char [2]
配列メンバーにデータを書き込んでから int
として読み取ると、トラップ表現が作成され、 undefined behavior em>。 char配列が int
全体をカバーするのに十分な長さを持っている場合でも、潜在的な危険が存在します。
ただし、特定のケースでは、 int
が char [2]
よりも大きい場合、読み取った int
は初期化されていない領域をカバーします配列の終わりを超えて、未定義の動作になります。
出力の背後にある理由は、マシン上で整数が little-endian 形式:最下位バイトが最初に保存されます。したがって、バイトシーケンス [3,2,0,0]は整数3 + 2 * 256 = 515を表します。
この結果は、特定の実装とプラットフォームに依存します。
このようなコードからの出力は、プラットフォームとCコンパイラの実装に依存します。出力を見ると、このコードをリッテンディアンシステム(おそらくx86)で実行していると思います。 515をiに入れてデバッガーで見ると、最下位バイトは3で、メモリーの次のバイトは2で、chに入れたものに正確にマッピングされます。
ビッグエンディアンシステムでこれを行った場合、(おそらく)770(16ビットintを想定)または50462720(32ビットintを想定)を取得することになります。
実装に依存し、異なるプラットフォーム/コンパイラーで結果が異なる場合がありますが、これは何が起こっているようです:
515のバイナリは
1000000011
ゼロをパディングして2バイトにします(16ビット整数と仮定):
0000001000000011
2バイトは次のとおりです。
00000010 and 00000011
2
および 3
誰かがそれらが逆になっている理由を説明してほしい-私の推測では、charsは逆ではなく、intはリトルエンディアンです。
ユニオンに割り当てられたメモリの量は、最大のメンバーを格納するために必要なメモリと等しくなります。この場合、intと長さ2のchar配列があります。intが16ビット、charが8ビットであると仮定すると、どちらも同じスペースを必要とするため、ユニオンには2バイトが割り当てられます。
char配列に3(00000011)と2(00000010)を割り当てると、結合の状態は 0000001100000010
になります。この共用体からintを読み取ると、全体が整数に変換されます。 LSBが最下位アドレスに格納されているリトルエンディアン表現を想定し、intユニオンからは、 0000001000000011
が515のバイナリになります。
注:これは、intが32ビットであっても当てはまります- Amnonの答え
32ビットシステムの場合、intは4バイトですが、初期化するのは2バイトのみです。初期化されていないデータへのアクセスは未定義の動作です。
16ビットintのシステムを使用していると仮定すると、実行していることはまだ実装定義です。システムがリトルエンディアンの場合、u.ch [0]はuiおよびu.ch 1 が最上位バイトになります。ビッグエンディアンシステムでは、逆になります。また、C標準では、 2の補数を使用して符号付き整数を表すことを実装に強制しません。値ですが、2の補数が最も一般的です。明らかに、整数のサイズも実装定義です。
ヒント:16進値を使用すると、何が起こっているかを簡単に確認できます。リトルエンディアンシステムでは、16進数の結果は0x0203になります。