8

「Beginning OpenGL Game Programming Second Edition」を読んでいて、次の構造体定義に出くわしました。

typedef struct tagPIXELFORMATDESCRIPTOR 
{
    WORD  nSize;    // size of the structure
    WORD  nVersion; // always set to 1
    DWORD dwFlags;  // flags for pixel buffer properties
    ...
}

「構造体の最も重要なフィールドの最初のものは nSize です。このフィールドは常に、次のように構造体のサイズと等しく設定する必要があります: pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR); これは簡単で、データの一般的な要件です。ポインターとして渡される構造体. 多くの場合、構造体は、さまざまな操作を実行するときに、そのサイズと、それに割り当てられているメモリの量を知る必要があります. サイズ フィールドを使用すると、この情報に簡単かつ正確にアクセスできます." (24ページ)

構造体にユーザーがサイズを渡す必要があるのはなぜですか? この構造体を使用するコードは、必要に応じて sizeof() を使用するだけではありませんか?

4

6 に答える 6

6

それには少なくとも2つの理由が考えられます

  1. 構造体の正確な定義は、それを使用するライブラリ API が開発されるにつれて、時間とともに変化します。新しいフィールドが最後に追加され、構造体の定義が変更され、そのsizeof. それでも、レガシ コードは、同じ API 関数に「古い」小さな構造体を提供します。古いコードと新しいコードの両方が機能することを確認するには、実行時のサイズ情報が必要です。正式には、これがnVersionフィールドの使用目的です。そのフィールドだけで、呼び出し元のコードが使用する予定の API のバージョンと、構造体に割り当てるフィールドの数を API に伝えるのに十分なはずです。ただし、セキュリティを強化するために、サイズ情報が独立したnSizeフィールドを介して提供される場合がありますが、これは悪い考えではありません。

  2. 構造体には、オプションまたは柔軟な情報が含まれています (API バージョンに関係なく)。充填コードは、そのサイズに基づいて必要な情報と不要な情報を決定するか、要求されたサイズに基づいて柔軟なサイズの情報を切り捨てます。これは、構造体の最後に柔軟な配列メンバーがある場合に特に適切です (「ストライク ハック」などの行に沿って)。

この特定のケース ( PIXELFORMATDESCRIPTORWindows API の構造体) では、その構造体と関連する API に柔軟性がないため、これが最初の理由です。

于 2013-11-07T16:51:13.700 に答える
4

これにより、構造の定義を時間の経過とともに変更できます。最後に新しいフィールドが追加されると、サイズ フィールドは使用するバージョンを示します。

于 2013-11-07T16:41:53.597 に答える
1

あなたが Windows API を作成する開発者であると想像してください。いくつかの API 呼び出しのセットが定義され、文書化され、OS がリリースされました。現在の API 呼び出しの多くは、構造体へのポインターを入力引数として受け入れ、膨大な量の入力引数を持たずに多くの入力値を渡すことができます。

これで、開発者はあなたの OS 用のコードを書き始めます。

数年後、Windows OS の新しいバージョンを作成することにしました。ただし、いくつかの要件があります。

  1. 以前の OS バージョン用にコンパイルされたプログラムは、新しい OS でも実行する必要があります (そのためには、API に下位互換性が必要です)。
  2. API を拡張したい - (新しい API 呼び出しが追加されました)。
  3. 開発者が既存のコード (古いウィンドウ用に記述したもの) を使用できるようにし、新しい OS でコンパイルして実行できるようにしたいと考えています。

OK - 古いプログラムが動作するためには、新しい API に同じ引数を持つ同じルーチンが必要です。

API を拡張するにはどうすればよいでしょうか。新しい API 呼び出しを追加することはできますが、同時に、古いコードを使用し、コードに多くの変更を加えずに新しいファンシーな機能を使用したい場合はどうでしょうか?

通常、API ルーチンは多くの情報を必要としますが、多くの仮引数を持つルーチンを作成するのは不便です。そのため、仮引数の 1 つが、ルーチンに渡したいプロパティを含む構造体へのポインターであることがよくあります。これにより、API の拡張が容易になります。例えば:

古いコード:

struct abc
{
   int magicMember; // ;-) 
   int a;
   int b;
   int c;
};

void someApiCall( struct abc *p, int blaBla );

ルーチンの署名を変更せずに詳細情報を提供して「someApiCall」を拡張することにした場合は、構造を変更するだけです。

あなたの新しいコード:

// on new OS - defined in a header with the same name as older OS
// hence no includes changes 

struct abc
{
   int magicMember; // ;-) 
   int a;
   int b;
   int c;
   int new_stuff_a;
   int new_stuff_b;
};

void someApiCall( struct abc *p, int blaBla );

ルーチンの署名を保持すると同時に、古いコードと新しいコードの両方が機能するようにしました。唯一の秘密は、構造体のリビジョン番号として扱うことができるmagicMemberです。または、新しいバージョンで新しいメンバーを追加するだけの場合は、構造体のサイズです。どちらの方法でも、「someApiCall」は 2 種類の「同じ」構造体を区別でき、古いコードと新しいコードの両方からその API 呼び出しを実行できます。

うるさい人なら、これらは同じ構造ではないと言うかもしれません。確かにそうではありません。それ以上のコード変更を防ぐために、それらは同じ名前になっています。

実際の例については、 RegisterClassEx API 呼び出しWNDCLASSEX 構造体を確認してください。

于 2013-11-07T17:44:08.267 に答える