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

时间:2013-11-07 16:40:12

标签: c++ struct sizeof

我正在阅读'开始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页)

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

6 个答案:

答案 0 :(得分:6)

至少有两个可能的原因

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

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

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

答案 1 :(得分:4)

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

答案 2 :(得分:3)

  

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

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

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

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

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

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

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

答案 3 :(得分:1)

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

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

答案 4 :(得分:1)

想象一下,您是一名创建Windows API的开发人员。您已定义,记录和OS发布的一组API调用。您当前的许多API调用接受指向结构的指针作为输入参数,以允许传递许多输入值而不会有大量的输入参数。

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

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

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

    现在如何扩展您的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'将能够区分两种类型的'相同'结构,并且你将能够从旧代码和新代码执行该API调用。

    如果一个人挑剔 - (s)他可能会说这些结构不一样。确实,事实并非如此。它们只是具有相同的名称,以防止更多代码更改。

    对于真实示例,请检查RegisterClassEx API callWNDCLASSEX struct it takes

答案 5 :(得分:1)

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

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

但是如果你玩耍涉及使用不同类型的指针,可能使用指针强制转换,那么结构开头的大小可以让你在不知道它的类型的情况下确定结构的大小。< / p>

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

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对象,则可能必须坚持使用该模型。