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

Why does this struct need a size value?

本文关键字:一个 结构 为什么      更新时间:2023-10-16

我正在阅读"开始OpenGL游戏编程第二版",并遇到了这个结构定义:

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

"结构中第一个更重要的字段是nSize。这 字段应始终设置为等于结构的大小,例如 this: pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR);这是 简单明了,是数据结构的常见要求 作为指针传递。通常,结构需要知道其大小和大小 在执行各种操作时,已为其分配了内存。尺寸 字段允许轻松准确地访问此信息。"(第24页)

为什么结构需要用户将大小传递给它?使用此结构的代码可以在需要时不仅仅使用 sizeof() 吗?

至少有两个可能的原因

  1. 结构的确切定义将随着时间的推移而变化,因为使用它的库 API 会发展。新字段将添加到末尾,更改结构的定义并更改其sizeof。然而,遗留代码仍将为相同的 API 函数提供"较旧"的较小结构。为了确保旧代码和新代码都有效,运行时大小信息是必需。从形式上讲,这就是nVersion字段的用途。该字段本身应该足以告诉 API 调用代码预期使用的 API 版本以及它在结构中分配的字段数。但只是为了额外的安全性,尺寸信息可能通过独立的nSize字段提供,这不是一个坏主意。

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

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

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

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

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

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

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

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

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

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

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

此技术通常用于消息,尤其是在嵌入式系统中以及需要复制消息时。

假设您是一名创建 Windows API 的开发人员。您定义、记录了一组 API 调用并发布了操作系统。您当前的许多 API 调用都接受指向结构的指针作为输入参数,以允许传递许多输入值,而无需大量输入参数。

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

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

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

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

现在如何扩展您的 API?您可以添加新的 API 调用,但如果同时想要使用旧代码并使用一些新的花哨功能而不对代码进行大量更改怎么办?

通常 API 例程需要很多信息,但创建具有许多正式参数的例程是不方便的。这就是为什么一个正式的 args 经常是指向包含要传递给例程的属性的结构的指针。这使得 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 结构

在大多数情况下,如果使用类型 tagPIXELFORMATDESCRIPTOR* 的指针访问tagPIXELFORMATDESCRIPTOR,则可能不需要成员来指定大小; 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 对象,则可能必须坚持使用该模型。

相关文章: