38

私が理解している限り、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.chu.i

他のほとんどの人にとっては非常に初心者に見えるかもしれませんが、その出力の背後にある理由を理解することはできません.

ありがとう。

4

6 に答える 6

30

これは未定義の動作です。u.iu.ch同じメモリアドレスにあります。したがって、一方に書き込み、他方から読み取る結果は、コンパイラ、プラットフォーム、アーキテクチャ、場合によってはコンパイラの最適化レベルに依存します。したがって、 の出力u.iは常に とは限りません515

たとえばgcc、私のマシンでは、 と に対して 2 つの異なる答えが生成され-O0ます-O2

  1. 私のマシンは 32 ビットのリトル エンディアン アーキテクチャを採用しているため、-O0最下位 2 バイトが 2 と 3 に初期化され、最上位 2 バイトが初期化されていません。したがって、ユニオンのメモリは次のようになります。{3, 2, garbage, garbage}

    したがって、次のような出力が得られ3 2 -1216937469ます。

  2. を使用すると、あなた-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、呼び出しを最適化したときに、初期化されていない共用体のランダム値としてゼロが選択されたことを意味します。

通常、ある共用体メンバーに書き込み、別のメンバーから読み取ることはあまり意味がありませんが、厳密なエイリアシングでコンパイルされたプログラムでは役立つ場合があります

于 2009-11-28T12:00:11.000 に答える
19

言語の仕様は時間とともに変化するため、この質問に対する答えは歴史的な文脈によって異なります。そして、この問題はたまたま変更の影響を受けた問題です。

あなたはK&Rを読んでいると言いました。その本の最新版(現在)は、C言語の最初の標準化されたバージョンであるC89/90について説明しています。そのバージョンのC言語では、ユニオンの1つのメンバーを書き込み、別のメンバーを読み取ることは未定義の動作です。実装は定義されていませんが(これは別のことです)、未定義の動作です。この場合の言語標準の関連部分は6.5/7です。

さて、C(Technical Corrigendum 3が適用された言語仕様のC99バージョン)の進化のある後の時点で、型のパンニングにユニオンを使用すること、つまりユニオンの1つのメンバーを書き込んでから別のメンバーを読み取ることが突然合法になりました。

それを行おうとすると、未定義の動作が発生する可能性があることに注意してください。読み取った値が、読み取ったタイプに対してたまたま無効(いわゆる「トラップ表現」)である場合でも、動作は未定義です。それ以外の場合、読み取る値は実装定義です。

あなたの特定のint例は、char[2]配列から型のパンニングに対して比較的安全です。C言語では、オブジェクトのコンテンツをchar配列として再解釈することは常に合法です(ここでも6.5 / 7)。

ただし、その逆は当てはまりません。ユニオンの配列メンバーにデータを書き込み、char[2]それをとして読み取ると、トラップ表現が作成され、未定義の動作intが発生する可能性があります。char配列が全体をカバーするのに十分な長さであっても、潜在的な危険が存在します。int

ただし、特定のケースでは、intがたまたまより大きい場合char[2]int読み取りは配列の終わりを超えた初期化されていない領域をカバーし、これも未定義の動作につながります。

于 2009-11-28T16:24:17.423 に答える
9

出力の背後にある理由は、マシンでは整数がリトル エンディアン形式で格納されているためです。最下位バイトが最初に格納されます。したがって、バイト シーケンス [3,2,0,0] は整数 3+2*256=515 を表します。

この結果は、特定の実装とプラットフォームによって異なります。

于 2009-11-28T12:05:26.573 に答える
5

これは実装に依存しており、プラットフォームやコンパイラによって結果が異なる可能性がありますが、これが起こっているようです:

バイナリの515は

1000000011

ゼロをパディングして 2 バイトにします (16 ビット int を想定):

0000001000000011

2 バイトは次のとおりです。

00000010 and 00000011

どちら23

誰かが逆になっている理由を説明してくれることを願っています-私の推測では、文字は逆になっていませんが、intはリトルエンディアンです。

ユニオンに割り当てられるメモリの量は、最大のメンバーを格納するために必要なメモリと同じです。この場合、長さ 2 の int 配列と char 配列があります。int が 16 ビットで char が 8 ビットであると仮定すると、どちらも同じスペースを必要とするため、共用体には 2 バイトが割り当てられます。

char 配列に 3 つ (00000011) と 2 つ (00000010) を割り当てると、union の状態は になり0000001100000010ます。この共用体から int を読み取ると、全体が整数に変換されます。LSB が最下位アドレスに格納されるリトル エンディアン表現を想定すると、共用体から読み取られる int0000001000000011は 515 のバイナリになります。

注: これは、int が 32 ビットの場合でも当てはまります。Amnon の回答を確認してください。

于 2009-11-28T12:00:46.397 に答える
5

このようなコードからの出力は、プラットフォームと C コンパイラの実装によって異なります。あなたの出力は、リテ エンディアン システム (おそらく x86) でこのコードを実行していると思わせます。515 を i に入れ、デバッガーで見ると、最下位バイトが 3 になり、メモリ内の次のバイトが 2 になることがわかります。これは、ch に入れたものと正確にマップされます。

ビッグ エンディアン システムでこれを行った場合、(おそらく) 770 (16 ビット整数を想定) または 50462720 (32 ビット整数を想定) が得られます。

于 2009-11-28T12:04:27.967 に答える
3

32 ビット システムを使用している場合、int は 4 バイトですが、初期化するのは 2 バイトだけです。初期化されていないデータへのアクセスは未定義の動作です。

16 ビットの int を持つシステムを使用していると仮定すると、実行していることはまだ実装定義です。システムがリトル エンディアンの場合、u.ch[0] は ui の最下位バイトに対応し、u.ch 1は最上位バイトになります。ビッグ エンディアン システムでは、逆になります。また、C 標準では、符号付き整数値を表すために2 の補数を使用することを実装に強制していませんが、2 の補数が最も一般的です。明らかに、整数のサイズも実装定義です。

ヒント: 16 進数値を使用すると、何が起こっているかを簡単に確認できます。リトルエンディアン システムでは、16 進数の結果は 0x0203 になります。

于 2009-11-28T12:25:56.117 に答える