C结构体是否将其成员保存在连续的内存块中?[duplicate]

bq8i3lrv  于 2023-02-03  发布在  其他
关注(0)|答案(6)|浏览(125)
    • 此问题在此处已有答案**:

Struct memory layout in C(3个答案)
三年前关闭了。
假设我的代码是:

typedef stuct {
  int x;
  double y;
  char z;
} Foo;

xy,和z在内存中会紧挨着吗?指针算法会"迭代"它们吗?我的C语言有些生疏,所以我不能正确地使用程序来测试这个问题。下面是我的完整代码。

#include <stdlib.h>
#include <stdio.h>

typedef struct {
  int x;
  double y;
  char z;
} Foo;

int main() {
  Foo *f = malloc(sizeof(Foo));
  f->x = 10;
  f->y = 30.0;
  f->z = 'c';
  // Pointer to iterate.
  for(int i = 0; i == sizeof(Foo); i++) {
    if (i == 0) {
      printf(*(f + i));
    }
    else if (i == (sizeof(int) + 1)) {
      printf(*(f + i));
    }
    else if (i ==(sizeof(int) + sizeof(double) + 1)) {
      printf(*(f + i));
    }
    else {
      continue;
    }
  return 0;
}
qnyhuwrf

qnyhuwrf1#

不可以,不能保证struct成员在内存中是连续的。
根据C标准第6.7.2.1节第15点(第115 here页):
结构对象中可能有未命名的填充,但在其开头没有。
大多数情况下,类似于:

struct mystruct {
    int a;
    char b;
    int c;
};

确实与sizeof(int)对齐,如下所示:

0  1  2  3  4  5  6  7  8  9  10 11
[a         ][b][padding][c          ]
kx1ctssn

kx1ctssn2#

是也不是。
是的,一个结构体的成员被分配在一个连续的内存块中。在你的例子中,一个Foo类型的对象占用了sizeof (Foo)连续的内存字节,并且所有的成员都在这个字节序列中。
但是,不能保证成员本身是相邻的。在任意两个成员之间,或者在最后一个成员之后,可以有填充字节。标准确实保证了第一个定义的成员位于偏移量0处,并且所有成员都按照它们定义的顺序分配(这意味着有时可以通过重新排序成员来节省空间)。
通常,编译器只使用足够的填充来满足成员类型的对齐要求,但标准不要求这样做。
所以你不能(直接)迭代一个结构的成员,如果你想这样做,并且所有的成员都是相同类型的,那么就使用数组。
您可以使用<stddef.h>中定义的offsetof宏来确定(非位字段)成员的字节偏移量,有时候使用它来构建一个数据结构(用于迭代结构中的成员)会很有用,但这很繁琐,而且很少比简单地通过名称引用成员更有用--特别是当它们具有不同类型时。

irlmq6kh

irlmq6kh3#

x,y,z在内存中会不会紧挨着?
不可以。结构体内存分配布局与实现相关-不能保证结构体成员彼此相邻。一个原因是内存填充,即
指针算法可以“迭代”它们吗?
不能。只能对指向同一类型的指针执行指针算术运算。

zwghvu4y

zwghvu4y4#

x,y,z在内存中会不会紧挨着?
它们 * 可以 * 是,但不一定是。ISO C标准没有强制要求元素在结构中的位置。
一般来说,编译器会将元素放置在某个偏移量上,该偏移量对于编译到的体系结构来说是"最佳的"。因此,在32位CPU上,大多数编译器默认情况下会将元素放置在4的倍数的偏移量上(因为这将使访问效率最高)。但是,大多数编译器也有办法指定不同的放置(对齐)。
所以,如果你有这样的东西:

struct X {
    uint8_t a;
    uint32_t b;
};

那么a的偏移量将是0,但在大多数使用缺省选项的32位编译器上,b的偏移量将是4
指针算法可以"迭代"它们吗?
与示例中的代码不同,结构指针上的指针运算被定义为将地址与结构的 * size * 相加/相减。因此,如果您有:

struct X a[2];
struct X *p = a;

然后p+1 == a+1
要"迭代"元素,您需要将p转换为uint8_t*,然后逐个元素地将元素的偏移量添加到uint8_t*(使用offsetof标准宏)。

pieyvz9o

pieyvz9o5#

它取决于编译器决定的填充(这受目标架构的需求和优势影响)。C标准确实保证了struct的第一个成员之前没有填充,但在此之后,您不能假设任何事情。但是,如果sizeofstruct确实等于其每个组成类型的sizeof,则不存在填充。
你可以使用编译器特定的指令来强制不填充。在MSVC中,这是:

#pragma pack(push, 1)
// your struct...
#pragma pack(pop)

GCC的等效效应为__attribute__((packed))

ny6fqffe

ny6fqffe6#

在这种情况下,尝试使用指针算法会有多个问题。
第一个问题,正如在其他答案中提到的,是在整个结构体中可能存在填充,从而使您的计算出错。
C11工作草案6.7.2.1第15页:(黑体强调是我的
在一个结构对象中,非位字段成员和位字段所在的单元的地址按它们被声明的顺序递增。一个指向结构对象的指针,经过适当的转换,指向它的初始成员(或者如果该成员是位字段,则将其分配给它所驻留的单元),反之亦然。在结构对象内可能存在未命名的填充,但不是在其开始时。
第二个问题是指针运算是以所指向类型大小的倍数来完成的。对于结构体,如果将指向结构体的指针加1,则指针将指向结构体后面的对象。使用示例结构体Foo

Foo x[3];
Foo *y = x+1; // y points to the second Foo (x[1]), not the second byte of x[0]

6.5.6第8页:
将整数类型的表达式与指针相加或相减时,结果具有指针操作数的类型。如果指针操作数指向数组对象的元素,并且数组足够大,则结果指向从原始元素偏移的元素,以便结果数组元素与原始数组元素的下标之差等于整数表达式。换句话说,如果表达式P指向数组对象的第 i 个元素,表达式**(P)+N**(等价于N+(P))和**(P)-N**(其中N的值为 n)分别指向数组对象的第 i+n 个元素和第 in 个元素,只要它们存在。
第三个问题是执行指针运算使得结果指向超过对象末尾的一个以上的位置会导致未定义的行为,正如通过指针运算获得的指向超过对象末尾的一个元素的指针的解引用一样。所以即使你有一个包含三个int的结构体,中间没有填充,并取一个指向第一个int的指针,并递增它以指向第二个int,解引用它将导致未定义的行为。
6.5.6中的更多内容:(粗体斜体强调为我的
此外,如果表达式P指向数组对象的最后一个元素,则表达式**(P)+1指向数组对象的最后一个元素后一个元素,如果表达式Q指向数组对象的最后一个元素后一个元素,表达式(Q)-1**指向数组对象的最后一个元素。如果指针操作数和结果都指向同一个数组对象的元素,或超过数组对象的最后一个元素,则计算不应产生溢出;否则,行为未定义。如果结果指向数组对象的最后一个元素后一个元素,则不应将其用作要计算的一元 * 运算符的操作数。
第四个问题是,将一个类型的指针解引用为另一个类型会导致未定义的行为。这种类型双关的尝试通常被称为严格别名违规。下面是一个通过严格别名违规导致未定义行为的示例,即使数据类型大小相同(假设为4字节int和float)并且对齐良好:

int x = 1;
float y = *(float *)&x;

6.5第七页:
对象的存储值只能由具有以下类型之一的左值表达式访问:

  • 与对象的有效类型兼容的类型,
  • 与对象的有效类型兼容的类型的限定版本,
  • 作为与所述对象的有效类型相对应的有符号类型或无符号类型的类型,
  • 作为与所述对象的有效类型的限定版本相对应的有符号或无符号类型的类型,
  • 在其成员中包括上述类型之一的聚合或联合类型(递归地包括子聚合或包含的联合的成员),或者
  • 字符类型。

总结:不,一个C结构体不一定要把它的成员保存在连续的内存中,即使它这样做了,指针算术仍然不能做你想做的指针算术。

相关问题