在C语言中如何从数组末尾访问元素?

10

最近我注意到在C语言中,对于以下声明:array&array之间存在重要区别:

char array[] = {4, 8, 15, 16, 23, 42};

前者是指向字符的指针,而后者是指向包含6个字符的数组的指针。另外,值得注意的是,写作 a[b] 只是 *(a + b) 的语法糖。实际上,你可以写 2[array],这符合标准。

因此,我们可以利用这些信息来编写如下代码:

char last_element = (&array)[1][-1];

&array的大小为6个字符,因此(&array)[1])是指向数组右侧的字符指针。通过查看[-1],因此我正在访问最后一个元素。

有了这个,例如我可以交换整个数组:

void swap(char *a, char *b) { *a ^= *b; *b ^= *a; *a ^= *b; }

int main() {
    char u[] = {1,2,3,4,5,6,7,8,9,10};

    for (int i = 0; i < sizeof(u) / 2; i++)
        swap(&u[i], &(&u)[1][-i - 1]);
}

这种通过数组末尾访问数组的方法是否存在缺陷?


11
这段话的意思是,过于“聪明”的代码通常比使用临时变量来完成显而易见的操作要低效。使用位运算符进行交换操作需要耗费更多的时间和内存资源,而直接使用临时变量则会更加高效。如果编译器没有对这种“聪明”代码进行优化,它可能需要进行9次数据加载和存储操作,而使用临时变量最坏情况下只需要6次。 - Andrew Henle
1
当代码中a == b时,注意swap()函数的作用。 - Jens
1
sizeof char u[] 可以正常工作,但如果您使用 alloc 系列分配数组,则不容易找到大小并使用此技巧。在这种特殊情况下,您正在堆栈上分配它,您可以使用此结构。 - Luv
1
在我看来,问题在于它不易读懂,因此对于你的同行程序员来说难以维护。解密正在发生的事情所需的认知努力依赖于 C 类型系统的深入了解和假设。我更喜欢使用 sizeof 表达式。 - Jens
“[array]是指向char的指针”是错误的。这是前两段中唯一的、单一的错误,但是这里有一个小问题:array确实是由6个char组成的整个数组。不是指向数组的指针,也不是指向元素的指针,而是整个数组。这就是为什么sizeof(array)可以返回数组的大小。然而,在sizeof&运算符之外,数组在所有其他上下文中都会衰变成为指向其第一个元素的指针。这就是为什么它看起来像是一个指针,但实际上并不是。 - cmaster - reinstate monica
显示剩余2条评论
3个回答

12

C标准没有定义(&array)[1]的行为。

考虑&array + 1。C标准对此进行了定义,原因如下:

  • 进行指针算术运算时,结果从数组的第一个元素(索引为0)到最后一个元素之外的一个元素是有定义的。
  • 进行指针算术运算时,指向单个对象的指针就像指向具有一个元素的数组的指针。在这种情况下,&array是指向单个对象的指针(它本身是一个数组,但指针算术是基于指向数组的指针,而不是基于指向元素的指针)。

因此,&array + 1是定义良好的指针算术,指向array的最后一个元素之后的位置。

然而,根据下标运算符的定义,(&array)[1]等同于*(&array + 1)。虽然&array + 1是定义良好的,但对其应用*运算符并不被允许。C 2018 6.5.6 8 明确告诉我们,关于指针算术的结果:“如果结果指向数组对象的最后一个元素之后的一个元素,则不得将其用作求值的一元*运算符的操作数。”

由于大多数编译器的设计方式,问题中的代码可以根据您的需要移动数据。但是,这不是您应该依赖的行为。您可以使用char *End = array + sizeof array / sizeof *array;获取到指向数组最后一个元素之后的好指针。然后您可以使用End[-1]引用最后一个元素,End[-2]引用倒数第二个元素,以此类推。


@LanguageLawyer:针对&(&u)[1][-i - 1]),特别是第一次使用& - rici
@LanguageLawyer:在我看来,lvalue转换是评估lvalue的方式。为了进行lvalue转换,需要确定对象的身份。 - rici
@languagelawyer:但是你只能评估一个表达式。如果该表达式转换为非左值,则您将有不同的表达式要评估,而左值评估不适用。 - rici
@languagelawyer:没错,但正如我之前所说,“对象标识的确定”是某些评估的一部分,并不意味着每种“对象标识的确定”情况都是该特定评估的一部分。在转换为非左值的情况下,6.5.6适用于评估,并且明确允许使用“超出末尾一个”的情况。 - rici
@languagelawyer:也许我可以两种方式都考虑一下。但它并没有评估lvalue。您可以确定对象的身份,而不必将其作为lvalue的评估的一部分。 (尽管我个人认为为了进行指针算术而识别对象是不必要的。)无论如何,我不认为我会说服您,而且我没有权威发言的位置,因为我不在C委员会上。因此,我怀疑继续已经过长的评论链已经没有任何意义了。 - rici
显示剩余18条评论

1
尽管标准规定arrayLvalue[i]表示(*((arrayLvalue)+(i))),这将通过获取arrayLvalue的第一个元素的地址进行处理,但gcc有时会将[](当应用于数组类型值或lvalue时)视为一个操作符,其行为类似于.member语法的索引版本,产生一个值或lvalue,编译器将把它视为数组类型的一部分。我不知道当数组类型操作数不是结构体或联合体的成员时是否会观察到这一点,但在它是的情况下,效果显然可证明,我不知道是否有任何保证类似逻辑不会应用于嵌套数组。
struct foo {unsigned char x[12]};
int test1(struct foo *p1, struct foo *p2)
{
    p1->x[0] = 1;
    p2->x[1] = 2;
    return p1->x[0];
}
int test2(struct foo *p1, struct foo *p2)
{
    char *p;
    p1->x[0] = 1;
    (&p2->x[0])[1] = 2;
    return p1->x[0];
}

test1生成的代码总是返回1,而test2生成的代码将返回p1->x[0]中的任何内容。我不知道标准或gcc文档中是否有任何建议这两个函数应该有不同行为的内容,也不知道如何强制编译器生成适应在分配的块的重叠部分中可能发生的情况下,p1p2恰好相互重叠的代码。虽然对于所编写的函数,test1()中使用的优化是合理的,但我不知道标准文件的任何已记录解释会将该情况视为UB,但如果将其写入p2->x[1]而不是p2->x[0],则定义代码的行为。


0
我会使用for循环,其中我将i设置为向量长度-1,每次不是增加它,而是减少它,直到它大于0。 for(int i = vet.length;i>0;i--)

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