这个结构体为什么需要一个大小值?

8
我正在阅读《OpenGL游戏编程入门第二版》,看到了这个结构体定义:
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()吗?
6个回答

6
至少有两个可能的原因:
  1. 结构体的确切定义会随着使用它的库API的发展而改变。新的字段将被添加到末尾,改变结构体的定义并改变其sizeof。但是遗留代码仍然会向相同的API函数提供“旧”的较小结构体。为了确保旧代码和新代码都能正常工作,需要运行时大小信息。严格来说,这就是nVersion字段可以用于的地方。该字段本身应足以告诉API调用代码期望使用的API版本以及在结构体中分配了多少字段。但出于额外的安全考虑,大小信息可能通过独立的nSize字段提供,这也不是一个坏主意。

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

在这种特定情况下(来自Windows API的PIXELFORMATDESCRIPTOR结构体),适用第一个原因,因为该结构体及其相关API中没有任何灵活性。


4

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


3
这种结构的代码在需要时不能只使用sizeof()来确定大小吗?
没错,这就是想要的——不要使用sizeof来确定消息的大小。这种类型的结构在服务器编程中非常普遍,特别是在使用套接字通信时,也很常见于WinAPI。
当二进制或定宽协议首次开发时,每个消息都会定义具有特定字段,每个字段都有一个独立的大小。读取这些消息的客户端代码——无论是从套接字还是某种用于进程间通信的缓冲区中——需要知道在移动到下一条消息之前为此消息读取多少数据。如果在单个帧或缓冲区中发送了多个消息,则尤其如此。
考虑一下,如果您获得了一组填充的数据缓冲区,并且其中有三个PIXELFORMATDESCRIPTOR消息。如果您知道每个消息的大小,则可以在处理缓冲区时正确地从一个消息移动到下一个消息。您如何知道每个消息的大小?
如果您“知道”消息的大小永远不会改变,那么您可以只使用sizeof(PIXELFORMATDESCRIPTOR)——但是,这种方法至少存在三个问题。首先,即使规格说明说消息的大小永远不会改变,有时候它们仍会改变,原因是原始开发人员改变了主意。这种情况会发生。其次,如果您的代码针对规格说明的一个版本开发,并且服务器基于规格说明的另一个版本发送消息,则如果消息的大小发生变化,则您的sizeof将不再反映线上消息的真实大小,非常糟糕。第三,在缓冲区中包含一个您不知道的消息时,没有什么可以与sizeof匹配,因此您将无法处理缓冲区的其余部分。
检查sizeof以确定线上消息的大小并不是一种可持续的方法。更好的方法是让协议告诉您每个消息的实时大小,并在解析消息时从缓冲区中处理相应的字节数。如果每个消息类型的消息大小都在同一位置(这是设计此类协议时的推荐做法),则甚至可以正确地从您不了解的缓冲区中提取消息。
这种方法也平滑了协议更改的升级路径。在我的工作中,有几个我编写程序的协议不包括线上消息的大小。当这些消息发生变化时,我们必须进行“热切换”,从一个客户端版本切换到下一个客户端版本,与升级服务器的确切时间协调。当有数百个遍布全球的服务器处理这些数据时,想象一下这会带来多大的痛苦。如果协议发送线上消息的大小,那么我们可以采取更加谨慎的方法来升级客户端软件,以便在服务器升级之前或之后将新版本的客户端软件投入生产。

1
假设你是一名开发人员,正在创建 Windows API。您定义了一些 API 调用,并已将其记录和发布到操作系统中。许多当前 API 调用接受结构体指针作为输入参数,以便在没有大量输入参数的情况下传递许多输入值。
现在,其他开发人员开始为您的操作系统编写代码。
几年后,您决定创建 Windows 操作系统的新版本。但是,您有一些要求:
1. 先前版本的程序必须仍然在新版本上执行 -(API 必须向后兼容)。 2. 您想扩展您的 API -(添加新的 API 调用)。 3. 您希望允许开发人员使用他们为旧版 Windows 编写的现有代码,并允许他们对新操作系统进行编译和执行。
好的,为了使您的旧程序正常工作,您的新 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所需的结构体

1
在大多数情况下,如果您使用类型为的指针访问,您可能不需要成员来指定大小;sizeof将始终为您提供正确的大小:
void func(tagPIXELFORMATDESCRIPTOR *ptr) {
    // sizeof *ptr is the correct 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对象,可能必须坚持这种模型。


1
大小字段还可以告诉接收器为结构体分配多少内存。这种技术通常用于消息传递中,特别是在嵌入式系统和需要复制消息时。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接