119

私が質問しているのは、よく知られている「構造体の最後のメンバーの長さが可変である」というトリックです。これは次のようになります。

struct T {
    int len;
    char s[1];
};

struct T *p = malloc(sizeof(struct T) + 100);
p->len = 100;
strcpy(p->s, "hello world");

構造体がメモリに配置される方法により、必要以上のブロックに構造体をオーバーレイし、最後のメンバーを指定されたものよりも大きいかのように扱うことができ1 charます。

したがって、問題は次のとおりです。この手法は技術的に未定義の動作ですか?。そうだと思いますが、規格がこれについて何を言っているのか興味がありました。

PS:私はこれに対するC99のアプローチを知っています、私は答えが上記のトリックのバージョンに特に固執することを望みます。

4

8 に答える 8

54

C FAQが言うように:

それが合法であるか移植可能であるかは明らかではありませんが、かなり人気があります。

と:

...公式の解釈では、すべての既知の実装で機能するように見えますが、C標準に厳密に準拠していないと見なされています。(配列の境界を注意深くチェックするコンパイラーは、警告を発行する場合があります。)

「厳密に準拠する」ビットの背後にある理論的根拠は、仕様のセクションJ.2未定義動作にあります。これには未定義動作のリストが含まれます。

  • 配列の添え字は、 (a[1][7]宣言で与えられた左辺値式のように)指定された添え字でオブジェクトに明らかにアクセスできる場合でも、範囲外ですint a[4][5](6.5.6)。

セクション6.5.6のパラグラフ8には、定義された配列の境界を超えるアクセスは未定義であるという別の言及があります。

ポインタオペランドと結果の両方が同じ配列オブジェクトの要素を指している場合、または配列オブジェクトの最後の要素を1つ過ぎている場合、評価によってオーバーフローが発生することはありません。それ以外の場合、動作は定義されていません。

于 2010-09-14T17:18:31.187 に答える
35

技術的には未定義の動作だと思います。この標準は(おそらく)直接それを扱っていないので、「または動作の明示的な定義の省略による」に該当します。未定義の動作であることを示す句(C99の§4/ 2、C89の§3.16/ 2)。

上記の「ほぼ間違いなく」は、配列添字演算子の定義によって異なります。具体的には、「接尾辞式の後に角括弧[]で囲まれた式が続くのは、配列オブジェクトの添え字付きの指定です」と書かれています。(C89、§6.3.2.1/ 2)。

ここで「配列オブジェクトの」に違反していると主張することができます(配列オブジェクトの定義された範囲外で添え字を付けているため)。この場合、動作は(少しだけ)明示的に定義されておらず、単に定義されていません。それを完全に定義するものは何もないので。

理論的には、配列境界チェックを実行し、(たとえば)範囲外の添え字を使用しようとした場合にプログラムを中止するコンパイラを想像できます。実際、私はそのようなものが存在することを知りません、そしてこのスタイルのコードの人気を考えると、コンパイラが特定の状況で添え字を強制しようとしても、誰もがそうすることに我慢するだろうとは想像しがたいですこの状況。

于 2010-09-14T17:28:33.557 に答える
16

はい、それは未定義の動作です。

C言語欠陥レポート#051は、この質問に対する決定的な答えを提供します。

イディオムは一般的ですが、厳密には準拠していません

http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_051.html

C99根拠文書では、C委員会は次のように追加しています。

この構成の妥当性は常に疑わしいものでした。1つの欠陥レポートへの応答で、委員会は、スペースが存在するかどうかに関係なく、配列p-> itemsには1つのアイテムしか含まれていないため、未定義の動作であると判断しました。

于 2012-09-13T18:29:08.647 に答える
12

その特定の方法はC標準では明示的に定義されていませんが、C99には言語の一部として「構造体ハック」が含まれています。C99では、構造体の最後のメンバーは「柔軟な配列メンバー」であり、char foo[](の代わりに任意の型を使用して)として宣言されますchar

于 2010-09-14T23:53:16.070 に答える
8

それは標準によって定義されているので、公式であろうとなかろうと、誰が何を言おうと、それは未定義の振る舞いではありません。p->s、左辺値として使用される場合を除いて、と同じポインタに評価されます(char *)p + offsetof(struct T, s)。特に、これはcharmallocされたオブジェクト内の有効なポインターであり、その直後に100個(またはそれ以上、配置の考慮事項によって異なります)の連続するアドレスがありchar、割り当てられたオブジェクト内のオブジェクトとしても有効です。->によって返されるポインタにオフセットを明示的に追加する代わりにmalloc、にキャストすることによってポインタが派生したという事実char *は関係ありません。

技術的にp->s[0]は、構造体内の配列の単一要素でありchar、次のいくつかの要素(たとえばp->s[1]、からp->s[3])は、構造体内のバイトをパディングする可能性があります。これは、構造体全体への割り当てを実行すると破損する可能性がありますが、単に個別にアクセスする場合は破損しない可能性がありますメンバー、および残りの要素は、割り当てられたオブジェクト内の追加のスペースであり、配置要件に準拠している限り(またchar、配置要件がない限り)、自由に使用できます。

構造体のパディングバイトと重複する可能性が何らかの形で鼻の悪魔を引き起こす可能性があることを心配している場合は、構造体の最後にパディングがないことを保証する値で1inを置き換えることでこれを回避できます。[1]これを行うための単純ですが無駄な方法は、最後に配列がないことを除いて同一のメンバーで構造体を作成しs[sizeof struct that_other_struct];、配列に使用することです。次に、は、の構造体の配列の要素として、およびの構造体の終わりに続くアドレスのcharオブジェクトとしてp->s[i]明確に定義されます。i<sizeof struct that_other_structi>=sizeof struct that_other_struct

編集:実際、適切なサイズを取得するための上記のトリックでは、配列自体が他の要素のパディングの途中ではなく最大の配置で始まるように、配列の前にすべての単純な型を含む共用体を配置する必要がある場合もあります。繰り返しになりますが、私はこれが必要であるとは思いませんが、私はそこにいる言語弁護士の中で最も妄想的な人のためにそれを提供しています。

編集2:標準の別の部分があるため、パディングバイトとの重複は間違いなく問題ではありません。Cでは、2つの構造体がそれらの要素の初期サブシーケンスで一致する場合、共通の初期要素にいずれかのタイプへのポインターを介してアクセスできる必要があります。結果として、より大きな最終配列と同一struct Tであるがより大きな最終配列を持つ構造体が宣言された場合、要素はの要素s[0]と一致する必要があり、これらの追加要素の存在は、より大きな構造体の共通要素にアクセスすることによって影響を受けたり影響を受けたりすることはありません。へのポインタを使用します。s[0]struct Tstruct T

于 2010-09-14T22:34:02.920 に答える
8

はい、それは技術的に未定義の動作です。

「構造体ハック」を実装するには、少なくとも3つの方法があることに注意してください。

(1)サイズ0の末尾の配列を宣言します(レガシーコードで最も「人気のある」方法)。ゼロサイズの配列宣言はCでは常に違法であるため、これは明らかにUBです。コンパイルしたとしても、言語は制約に違反するコードの動作について保証しません。

(2)最小の有効サイズ-1(あなたの場合)で配列を宣言します。p->s[0]この場合、ポインタを取得してそれを超えるポインタ演算に使用しようとする試みp->s[1]は、未定義の動作です。たとえば、デバッグ実装では、範囲情報が埋め込まれた特別なポインタを生成できます。これは、を超えてポインタを作成しようとするたびにトラップされますp->s[1]

(3)たとえば、10000のような「非常に大きい」サイズの配列を宣言します。宣言されたサイズは、実際に必要になる可能性のあるものよりも大きくなるはずです。この方法では、アレイのアクセス範囲に関してUBは使用できません。ただし、実際には、もちろん、常に少量のメモリを割り当てます(実際に必要な量だけ)。これの合法性についてはよくわかりません。つまり、オブジェクトの宣言されたサイズよりも少ないメモリをオブジェクトに割り当てることがどれほど合法であるか疑問に思います(「割り当てられていない」メンバーにアクセスしないと仮定します)。

于 2010-09-14T22:57:53.113 に答える
4

標準では、配列の終わり以外のものにアクセスできないことは非常に明確です。(そして、配列の終了後にポインターを1つ超えてインクリメントすることさえ許可されていないため、ポインターを経由することは役に立ちません)。

そして「実践で働く」ために。gcc / g ++オプティマイザーが標準のこの部分を使用しているのを見たので、この無効なCを満たすと間違ったコードが生成されます。

于 2012-05-08T14:44:38.063 に答える
1

コンパイラが次のようなものを受け入れる場合

typedef struct {
  int len;
  char dat [];
};

'dat'の添え字をその長さを超えて受け入れる準備ができている必要があることはかなり明らかだと思います。一方、誰かが次のようなコードを記述した場合:

typedef struct {
  int何でも;
  char dat [1];
} MY_STRUCT;

その後、somestruct->dat[x]にアクセスします。コンパイラには、xの値が大きい場合に機能するアドレス計算コードを使用する義務はないと思います。本当に安全になりたいのであれば、適切なパラダイムは次のようになります。

#define LARGEST_DAT_SIZE 0xF000
typedef struct {
  int何でも;
  char dat [LARGEST_DAT_SIZE];
} MY_STRUCT;

次に、(sizeof(MYSTRUCT)-LARGEST_DAT_SIZE +desired_array_length)バイトのmallocを実行します(desired_array_lengthがLARGEST_DAT_SIZEより大きい場合、結果が未定義になる可能性があることに注意してください)。

ちなみに、長さゼロの配列を禁止するという決定は不幸なものだったと思います(Turbo Cのような古い方言はそれをサポートしています)。長さゼロの配列は、コンパイラがより大きなインデックスで動作するコードを生成する必要があることの兆候と見なすことができるからです。 。

于 2010-09-17T15:35:14.133 に答える