【问题标题】:Access an array from the end in C?在C中从末尾访问数组?
【发布时间】:2020-08-16 23:55:37
【问题描述】:

我最近注意到,在 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]);
}

这种最后访问数组的方法有缺陷吗?

【问题讨论】:

  • FWIW,像void swap(char *a, char *b) { *a ^= *b; *b ^= *a; *a ^= *b; } 这样的过于“聪明”的代码几乎总是比只使用临时值执行明显的代码效率低。 “从a 加载寄存器1,从b 加载寄存器2,玩按位操作(希望不是直接到内存!),然后存储值”比“从a 加载寄存器1”效率低很多,从b加载寄存器2,将r2存储在a并将r1存储在b"。如果编译器没有优化“聪明”的代码,它总共可以执行 9 次加载/存储。而不是使用临时的 6 个最坏情况。
  • 即使中间值是OOB,它总是计算一个有效的地址,所以应该没问题。
  • 虽然显示的代码没有,但请注意当 a == b 时 swap() 会做什么。
  • sizeof char u[] 工作正常,但如果您要使用 alloc 系列分配数组,那么找到大小并因此使用这个技巧并非易事。在这种特殊情况下,您将其分配到堆栈上,您可以使用此构造。
  • 恕我直言,缺陷在于它不可读,因此对您的程序员同事来说可维护性较差。破译正在发生的事情的认知努力依赖于对 C 类型系统的深入了解和假设。任何时候我都更喜欢sizeof 表达式。

标签: c arrays pointers language-lawyer


【解决方案1】:

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

考虑&amp;array + 1。这是由 C 标准定义的,原因有两个:

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

所以&amp;array + 1 是定义的指针算法,它指向刚刚超出array 的末尾。

但是,根据下标运算符的定义,(&amp;array)[1]*(&amp;array + 1)。虽然定义了 &amp;array + 1,但未对其应用 *。 C 2018 6.5.6 8 明确告诉我们,关于指针运算的结果,“如果结果指向数组对象的最后一个元素,则不应将其用作评估的一元 * 运算符的操作数。 ”

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

【讨论】:

  • 6.5.3.2p3 (&amp; operator): "同样,如果操作数是 [] 运算符的结果,则既不是 &amp; 运算符,也不是隐含的一元 * [] 被评估,结果就像 &amp; 运算符被删除,[] 运算符被更改为 + 运算符。"
  • @LanguageLawyer: to&amp;(&amp;u)[1][-i - 1]),具体是&amp;的第一次使用
  • @rici (&amp;u)[1] 仍然有问题,即使您删除/替换外部 &amp;[-i - 1]
  • @LanguageLawyer (&amp;u)[1] 是(隐式)* 运算符的结果,因此在此上下文中不会根据引用的子句中的前一句对其进行评估。
  • @rici 不确定我是否理解。您将&amp;(&amp;u)[1][-i - 1] 转换为((&amp;u)[1] + (-i - 1))&amp; 在哪里应用于* 的结果?
【解决方案2】:

虽然标准规定 arrayLvalue[i] 表示(*((arrayLvalue)+(i))),它将通过获取arrayLvalue 的第一个元素的地址来处理,但当应用于数组类型值或时,gcc 有时会处理[]左值,作为一个操作符,其行为符合.member 语法的索引版本,产生一个值或左值,编译器将其视为数组类型的一部分。我不知道当数组类型的操作数不是结构或联合的成员时这是否可以观察到,但是在它存在的情况下效果是显而易见的,而且我不知道任何可以保证类似的逻辑不会应用于嵌套数组。

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];
}

gcc 为test1 生成的代码将始终返回 1,而为test2 生成的代码将返回 p1->x[0] 中的任何内容。我不知道标准或 gcc 文档中的任何内容表明这两个函数的行为应该不同,也不知道如何强制编译器生成代码以适应 p1p2 碰巧识别重叠的情况在必要的情况下分配块的一部分。尽管test1() 中使用的优化对于编写的函数来说是合理的,但我知道没有记录的标准解释会将这种情况视为UB,但如果它写入p2-&gt;x[0] 而不是@,则定义代码的行为987654332@.

【讨论】:

    【解决方案3】:

    我会做一个 for 循环,在其中我设置 i = 向量的长度 - 1 并且每次我不增加它,而是减少它直到它大于 0。 for(int i = vet.length;i&gt;0;i--)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-02-07
      • 1970-01-01
      • 2011-07-05
      相关资源
      最近更新 更多