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

至少有两个可能的原因

  1. 随着使用它的库 API 的发展,结构的确切定义会随着时间而改变。新字段将被添加到最后,改变结构的定义并改变它的sizeof. 然而,遗留代码仍会为相同的 API 函数提供“较旧”的较小结构。为了确保新旧代码都能正常工作,运行时大小信息是必要的。形式上,这就是该nVersion字段的用途。该字段本身应该足以告诉 API 调用代码期望使用的 API 版本以及它在结构中分配了多少字段。但只是为了额外的安全性,尺寸信息可能会通过一个独立的nSize字段提供,这不是一个坏主意。

  2. 该结构包含可选或灵活的信息(无论 API 版本如何)。填充代码将根据该大小决定您需要或不需要哪些信息,或者根据您请求的大小截断灵活大小的信息。如果结构末尾有一个灵活的数组成员(沿着“struck hack”之类的行),这可能特别合适。

在这种特定情况下(PIXELFORMATDESCRIPTOR来自 Windows API 的 struct ),这是第一个适用的原因,因为该结构和相关 API 没有什么灵活的。

于 2013-11-07T16:51:13.700 回答
4

这允许结构的定义随时间改变。在最后添加新字段时,大小字段会告诉您要使用哪个版本。

于 2013-11-07T16:41:53.597 回答
3

使用此结构的代码能否在需要时不仅使用 sizeof() 吗?

这就是想法——不要使用sizeof来确定消息的大小。当使用套接字通信时,这种结构在服务器编程中非常普遍,并且在 WinAPI 中也很常见。

当第一次开发二进制或固定宽度的协议时,单独的消息是用特定的字段定义的,每个字段都有单独的大小。正在读取这些消息的客户端代码(来自套接字或进程间通信中使用的某种其他类型的缓冲区)需要知道在继续处理下一条消息之前要为此消息读取多少数据。如果在单个帧或缓冲区中发送多条消息,则尤其如此。

考虑一下您是否获得了一个填充的数据缓冲区并且其中包含三个PIXELFORMATDESCRIPTOR消息。如果您知道每条消息的大小,则可以在处理缓冲区时正确地从一条消息移动到下一条消息。你怎么知道每条消息的大小?

sizeof (PIXELFORMATDESCRIPTOR)如果你知道消息的大小永远不会改变,你就可以使用——但这种方法至少存在三个问题。首先,即使规范说消息的大小永远不会改变,有时当原始开发人员改变主意时,他们还是会这样做。有时候是这样的。其次,如果您的代码是针对一个版本的规范开发的,并且服务器正在发送基于另一个版本的规范的消息,如果消息的大小发生变化,您sizeof将不再反映线路上消息的真实大小,并且会发生非常糟糕的事情。第三,如果缓冲区包含您在代码中一无所知的消息,则没有什么可以sizeof反对的,您将无法处理缓冲区的其余部分。

检查sizeof以确定线路上消息的大小不是一种可持续的方法。更好的是让协议实时告诉您每条消息有多大,并在解析消息时从缓冲区处理那么多字节。如果每个消息类型的消息大小都在同一个位置(这是设计此类协议时的推荐做法),那么您甚至可以正确地从缓冲区中提取您一无所知的消息。

这种方法还可以在协议更改时平滑升级路径。在我的工作中,我编写了几个协议,这些协议不包括线路上消息的大小。当这些消息发生变化时,我们必须将一个客户端版本“热切”到下一个客户端版本,并与服务器升级的确切时间相协调。想象一下,当有数百台服务器分散在世界各地处理这些数据时,这会带来怎样的痛苦。如果协议通过线路发送消息的大小,那么我们可以在服务器升级时采取更谨慎的方法来升级客户端软件——甚至在服务器之前或之后将新版本的客户端软件投入生产被升级了。

于 2013-11-07T16:56:55.790 回答
1

size 字段还可以告诉接收者为结构分配多少内存。

这种技术通常用于消息,特别是在嵌入式系统中以及需要复制消息时。

于 2013-11-07T16:51:45.047 回答
1

假设您是创建 Windows API 的开发人员。您已经定义、记录和发布了一些 API 调用集。您当前的许多 API 调用都接受指向结构的指针作为输入参数,以允许在没有大量输入参数的情况下传递许多输入值。

现在开发人员开始为您的操作系统编写代码。

几年后,您决定创建新版本的 Windows 操作系统。不过你有一些要求:

  1. 为以前的操作系统版本编译的程序仍必须在您的较新操作系统上执行 - (API 必须向后兼容)。
  2. 您想扩展您的 API -(添加了新的 API 调用)。
  3. 您希望允许开发人员使用他们现有的代码(他们为旧窗口编写的代码)并允许他们在新操作系统上编译和执行它。

好的 - 为了使您的旧程序能够工作,您的新 API 必须具有相同的例程和相同的参数等。

现在如何扩展你的 API?您可能会添加新的 API 调用,但如果同时 - 想要使用旧代码并使用一些新奇的功能而不对您的代码进行太多更改怎么办?

通常 API 例程需要很多信息,但是创建具有许多形式参数的例程很不方便。这就是为什么正式的参数之一是一个指向包含要传递给例程的属性的结构的指针很常见的原因。这使得 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”两种方式都将能够区分两种“相同”结构,并且您将能够从旧代码和新代码执行该 API 调用。

如果一个人很挑剔——他可能会说这些不是相同的结构。确实不是。它们只是具有相同的名称以防止更多的代码更改。

对于实际示例,请检查RegisterClassEx API 调用它需要的 WNDCLASSEX 结构

于 2013-11-07T17:44:08.267 回答
1

在大多数情况下,如果您tagPIXELFORMATDESCRIPTOR使用 type 的指针进行访问tagPIXELFORMATDESCRIPTOR*,您可能不需要成员来指定大小;sizeof将始终为您提供正确的尺寸:

void func(tagPIXELFORMATDESCRIPTOR *ptr) {
    // sizeof *ptr is the correct size
}

但是,如果您使用涉及使用不同类型指针的技巧,可能使用指针强制转换,那么结构开头的size 成员可以让您在不知道其类型的情况下确定结构的大小。

例如,您可以定义一个只包含 size 成员的类型:

struct empty {
    WORD  nSize;
};

然后,只要您nSize为您创建的每个对象小心地将成员设置为正确的值(并且只要nSize始终位于每个结构中的相同位置),您就可以在不知道其实际类型的情况下获得结构的大小:

void func(empty *ptr) {
    // sizeof *ptr is incorrect
    // ptr->nSize is the actual size (if you've done everything right)
    // That's ok if we don't need any information other than the size
}

...

tagPIXELFORMATDESCRIPTOR obj;
...
func(reinterpret_cast<empty*>(ptr));

这并不是说这是一个好主意。

如果您可以只使用适当的指针类型,而不进行指针转换,那么您应该这样做。

如果你不能,C++ 提供更简洁、更可靠的方法(尤其是继承)来定义相关类型。将我所称的empty(或者应该称为类似descriptor的)定义为一个类,然后定义tagPIXELFORMATDESCRIPTOR为一个子类要好得多。

我不熟悉 OpenGL,但我怀疑它最初是为使用 C 风格的伪继承而设计的。如果您需要在 C 或 C++ 代码中使用 OpenGL 对象,您可能必须坚持使用该模型。

于 2013-11-07T18:03:52.733 回答