在结构中,使用一个数组字段访问另一个数组字段是否合法?

时间:2017-11-03 10:59:12

标签: c++ c arrays struct

例如,请考虑以下结构:

struct S {
  int a[4];
  int b[4];
} s;

s.a[6]并期望它等于s.b[2]是否合法? 就个人而言,我觉得它必须是C ++中的UB,而我不确定C. 但是,我没有找到任何与C和C ++语言相关的内容。

更新

有几个答案提示确保没有填充的方法 在字段之间,以使代码可靠地工作。我想强调一下 如果这样的代码是UB,那么填充填充是不够的。如果是UB, 那么编译器可以自由地假设对S.a[i]S.b[j]的访问没有 重叠,编译器可以自由重新排序这样的内存访问。例如,

    int x = s.b[2];
    s.a[6] = 2;
    return x;

可以转换为

    s.a[6] = 2;
    int x = s.b[2];
    return x;

始终返回2

9 个答案:

答案 0 :(得分:61)

  

写s.a [6]并期望它等于s.b [2]是否合法?

即可。因为在C和C ++中访问数组超出范围会调用未定义的行为

C11 J.2未定义的行为

  
      
  • 将指针加到或减去数组对象和整数类型会产生一个指向超出范围的结果   数组对象,用作一元*运算符的操作数   评估(6.5.6)。

  •   
  • 数组下标超出范围,即使某个对象显然可以使用给定的下标访问(如左值表达式)   a[1][7]给出声明int a[4][5])(6.5.6)。

  •   

C ++标准draft第5.7节添加剂运营商第5段说:

  

添加或减去具有整数类型的表达式时   从指针开始,结果具有指针操作数的类型。如果   指针操作数指向数组对象的元素和数组   足够大,结果指向一个偏离的元素   原始元素使得下标的差异   结果和原始数组元素等于整数表达式。   [...] 如果指针操作数和结果都指向元素   相同的数组对象,或一个超过数组的最后一个元素   对象,评估不得产生溢出;否则,   行为未定义。

答案 1 :(得分:34)

除了@rspUndefined behavior for an array subscript that is out of range)的回答之外,我可以补充一点,通过b访问a是不合法的,因为C语言没有说明多少填充空间可以在为a分配的区域的末尾和b的开头之间,所以即使你可以在特定的实现上运行它,它也是不可移植的。

instance of struct:
+-----------+----------------+-----------+---------------+
|  array a  |  maybe padding |  array b  | maybe padding |
+-----------+----------------+-----------+---------------+

第二个填充可能会遗漏,struct object的对齐方式是a的对齐方式,与b的对齐方式相同,但C语言也没有强加第二次填充不在那里。

答案 2 :(得分:11)

ab是两个不同的数组,a定义为包含4个元素。因此,a[6]访问数组超出范围,因此是未定义的行为。请注意,数组下标a[6]定义为*(a+6),因此UB的证明实际上是由"添加运算符"与指针一起使用#34;请参阅描述此方面的C11标准的以下部分(例如this在线草案版本):

  

6.5.6加法运算符

     

添加或减去具有整数类型的表达式时   从指针开始,结果具有指针操作数的类型。如果   指针操作数指向数组对象的元素和数组   足够大,结果指向一个偏离的元素   原始元素使得下标的差异   结果和原始数组元素等于整数表达式。   换句话说,如果表达式P指向一个的第i个元素   数组对象,表达式(P)+ N(等效地,N +(P))和(P)-N   (其中N具有值n)分别指向第i + n和第i   数组对象的第i个元素,只要它们存在即可。而且,如果   表达式P指向数组对象的最后一个元素,即   表达式(P)+1指向数组对象的最后一个元素,   如果表达式Q指向一个数组的最后一个元素   对象,表达式(Q)-1指向数组的最后一个元素   宾语。 如果指针操作数和结果都指向元素   相同的数组对象,或一个超过数组的最后一个元素   对象,评估不得产生溢出;否则,   行为未定义。如果结果指向最后一个元素   对于数组对象,它不应该用作一元*的操作数   被评估的运算符。

同样的论点适用于C ++(虽然这里没有引用)。

此外,虽然由于超出a的数组边界这一事实显然是未定义的行为,但请注意编译器可能会在成员ab之间引入填充,这样 - 即使允许这样的指针算术 - a+6也不一定会产生与b+2相同的地址。

答案 3 :(得分:6)

合法吗?不会。正如其他人提到的那样,它会调用 Undefined Behavior

会起作用吗?这取决于你的编译器。这是关于未定义行为的事情:它未定义

在许多C和C ++编译器中,结构将被布局为使得b将紧跟在内存中并且将不会进行边界检查。因此,访问[6]实际上与b [2]相同,不会导致任何异常。

鉴于

struct S {
  int a[4];
  int b[4];
} s

假设没有额外的填充,结构实际上只是一种查看包含8个整数的内存块的方法。您可以将其转换为(int*)((int*)s)[6]将指向与s.b[2]相同的内存。

你应该依赖这种行为吗?绝对不。 未定义意味着编译器不必支持此功能。编译器可以自由地填充结构,这可以使&(s.b [2])==&(s.a [6])的假设不正确。编译器还可以添加对数组访问的边界检查(虽然启用编译器优化可能会禁用这样的检查)。

我过去曾经历过这种影响。拥有像这样的结构

是很常见的
struct Bob {
    char name[16];
    char whatever[64];
} bob;
strcpy(bob.name, "some name longer than 16 characters");

现在鲍勃。将会是什么"超过16个字符"。 (这就是为什么你应该总是使用strncpy,BTW)

答案 4 :(得分:5)

正如@MartinJames在评论中提到的,如果你需要保证ab在连续的内存中(或至少能够被视为这样,(编辑)除非您的架构/编译器使用一个不寻常的内存块大小/偏移和强制对齐,需要添加填充),你需要使用union

union overlap {
    char all[8]; /* all the bytes in sequence */
    struct { /* (anonymous struct so its members can be accessed directly) */
        char a[4]; /* padding may be added after this if the alignment is not a sub-factor of 4 */
        char b[4];
    };
};

您无法直接从b访问a(例如a[6],就像您提到的那样),但您可以访问a的元素b 1}}和all使用all[6](例如b[2]指的是与8相同的内存位置。)

(编辑:您可以分别使用42*sizeof(int)替换上面代码中的sizeof(int)a,以更有可能与架构的对齐方式相匹配,尤其是如果代码需要更具可移植性,那么你必须小心避免对ball#include <stdio.h> union overlap { char all[2*sizeof(int)]; /* all the bytes in sequence */ struct { /* anonymous struct so its members can be accessed directly */ char a[sizeof(int)]; /* low word */ char b[sizeof(int)]; /* high word */ }; }; int main() { union overlap testing; testing.a[0] = 'a'; testing.a[1] = 'b'; testing.a[2] = 'c'; testing.a[3] = '\0'; /* null terminator */ testing.b[0] = 'e'; testing.b[1] = 'f'; testing.b[2] = 'g'; testing.b[3] = '\0'; /* null terminator */ printf("a=%s\n",testing.a); /* output: a=abc */ printf("b=%s\n",testing.b); /* output: b=efg */ printf("all=%s\n",testing.all); /* output: all=abc */ testing.a[3] = 'd'; /* makes printf keep reading past the end of a */ printf("a=%s\n",testing.a); /* output: a=abcdefg */ printf("b=%s\n",testing.b); /* output: b=efg */ printf("all=%s\n",testing.all); /* output: all=abcdefg */ return 0; } 中的字节数进行任何假设。但是,这将是研究可能是最常见的(1字节,2字节和4字节)内存对齐方式。)

这是一个简单的例子:

Base = declarative_base()

class Thing(Base):
    __tablename__ = 'thing'
    uid = Column(Integer, Sequence('Thing_id_seq'), primary_key=True)
    name = Column(String)
    def __repr__(self):
        return "something"

class ThingEntry(Base):
    __tablename__ = 'thingentry'
    uid = Column(Integer, Sequence('ThingEntry_id_seq'), primary_key=True)
    foo = Column(Integer, ForeignKey('foo.uid'))
    entity = Column(Integer, ForeignKey('thing'))

class Quu(Base):
    __tablename__ = 'quu'
    uid = Column(Integer, Sequence('Quu_id_seq'), primary_key=True)
    name = Column(String)
    description = Column(String)
    def __repr__(self):
        return "something"

class QuuEntry(Base):
    __tablename__ = 'quuentry'
    uid = Column(Integer, Sequence('QuuEntry_id_seq'), primary_key=True)
    foo = Column(Integer, ForeignKey('foo.uid'))
    entity = Column(Integer, ForeignKey('quu'))

答案 5 :(得分:3)

,因为访问数组超出范围会在C和C ++中调用 Undefined Behavior

答案 6 :(得分:1)

简答:不。你处于未定义行为的境地。

长答案:否。但这并不意味着您无法以其他更粗略的方式访问数据...如果您正在使用GCC,您可以执行以下操作( dwillis答案的详细说明):

struct __attribute__((packed,aligned(4))) Bad_Access {
    int arr1[3];
    int arr2[3];
};

然后你可以通过(Godbolt source+asm进行访问:

int x = ((int*)ba_pointer)[4];

但是该强制转换会违反严格的别名,因此g++ -fno-strict-aliasing只能安全。您可以将结构指针强制转换为指向第一个成员的指针,但之后您又回到了UB中,因为您正在访问第一个成员之外。

或者,就是不要这样做。拯救未来的程序员(可能是你自己)是这个混乱的心痛。

另外,虽然我们在这,但为什么不使用std :: vector?这不是万无一失的,但在后端它有防范这种不良行为的守卫。

<强>附录:

如果你真的关心表现:

假设您有两个相同类型的指针,您正在访问它们。编译器很可能会假设两个指针都有机会干扰,并且会实例化其他逻辑以防止你做一些愚蠢的事情。

如果你庄严地向编译器发誓你不想尝试别名,那么编译器会很好地奖励你: Does the restrict keyword provide significant benefits in gcc / g++

结论:不要邪恶;你未来的自我,和编译器会感谢你。

答案 7 :(得分:1)

Jed Schaff的回答是正确的,但不太正确。如果编译器在ab之间插入填充,则他的解决方案仍然会失败。但是,如果您声明:

typedef struct {
  int a[4];
  int b[4];
} s_t;

typedef union {
  char bytes[sizeof(s_t)];
  s_t s;
} u_t;

您现在可以访问(int*)(bytes + offsetof(s_t, b))以获取s.b的地址,无论编译器如何布局结构。 offsetof()宏已在<stddef.h>中声明。

表达式sizeof(s_t)是一个常量表达式,在C和C ++中的数组声明中是合法的。它不会给出一个可变长度的数组。 (对于之前误读C标准的道歉。我认为这听起来不对。)

在现实世界中,结构中的两个连续int数组将按照您期望的方式布局。 (你可能能够设计一个非常人为的反例,方法是将a的范围设置为3或5而不是4,然后让编译器将a和{b对齐{1}}在一个16字节的边界上。)除了复杂的方法试图得到一个除了标准的严格措辞之外没有任何假设的程序,你需要某种防御性编码,例如static assert(&both_arrays[4] == &s.b[0], ""); 。这些不会增加运行时开销,如果你的编译器正在做一些会破坏你程序的东西,那么它就会失败,只要你不在断言中触发UB。

如果您希望以可移植的方式保证两个子阵列都打包到连续的内存范围中,或者以另一种方式拆分内存块,则可以使用memcpy()复制它们。

答案 8 :(得分:0)

当程序试图在一个结构字段中使用越界数组下标来访问另一个结构的成员时,标准不对任何实现必须执行的操作施加任何限制。因此,越界访问是非法的&#34; 严格遵守程序,使用此类访问的程序不能同时100%移植并且没有错误。另一方面,许多实现确实定义了此类代码的行为,而仅针对此类实现的程序可能会利用此类行为。

此类代码存在三个问题:

  1. 虽然许多实现以可预测的方式布局结构,但标准允许实现在除第一个之外的任何结构成员之前添加任意填充。代码可以使用sizeofoffsetof来确保结构成员按预期放置,但其他两个问题仍然存在。

  2. 给出类似的东西:

    if (structPtr->array1[x])
     structPtr->array2[y]++;
    return structPtr->array1[x];
    

    编译器通常认为使用structPtr->array1[x]将产生与之前在&#34中使用相同的值,如果&#34;条件,即使它会改变依赖于两个数组之间别名的代码行为。

  3. 如果array1[]有例如4个元素,编译器给出类似的东西:

    if (x < 4) foo(x);
    structPtr->array1[x]=1;
    
  4. 可能会得出结论,由于没有定义的x不小于4的情况,它可以无条件地调用foo(x)

    不幸的是,虽然程序可以使用sizeofoffsetof来确保结构布局没有任何意外,但他们无法测试编译器是否承诺避免优化类型#2或#3。此外,标准对于在以下情况下的含义有点模糊:

    struct foo {char array1[4],array2[4]; };
    
    int test(struct foo *p, int i, int x, int y, int z)
    {
      if (p->array2[x])
      {
        ((char*)p)[x]++;
        ((char*)(p->array1))[y]++;
        p->array1[z]++;
      }
      return p->array2[x];
    }
    

    标准非常明确,只有当z在0..3的范围内时才会定义行为,但由于该表达式中p->数组的类型是char *(由于衰减),因此它是&#39;不清楚使用y访问中的强制转换会产生任何影响。另一方面,由于将指向结构的第一个元素的指针转换为char*应该产生与将结构指针转换为char*相同的结果,并且转换的结构指针应该可用于访问所有字节其中,使用x的访问似乎应定义为(至少)x = 0..7 [如果array2的偏移量大于4,则会影响{{{ 1}}需要点击x的成员,但array2的某些值可以使用已定义的行为执行此操作]。

    恕我直言,一个很好的补救方法是以不涉及指针衰减的方式在数组类型上定义下标运算符。在这种情况下,表达式xp->array[x]可以邀请编译器假设&(p->array1[x])为0..3,但xp->array+x需要编译器允许其他值的可能性。我不知道是否有任何编译器这样做,但标准并不要求它。