在 C 中从末尾访问一个数组?
Access an array from the end in C?
我最近注意到在 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]);
}
这种最后访问数组的方法是否有缺陷?
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]
引用倒数第二个元素,依此类推。
虽然标准规定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
生成的代码将始终为 return 1,而为 test2
生成的代码将 return p1->x[0] 中的任何内容.我不知道标准或 gcc 文档中的任何内容表明这两个函数的行为应该不同,也不知道应该如何强制编译器生成代码以适应 p1
和 p2
的情况在必要的情况下碰巧识别已分配块的重叠部分。尽管 test1()
中使用的优化对于编写的函数来说是合理的,但我知道没有记录的标准解释将这种情况视为 UB,但如果它写入 [=20= 则定义代码的行为] 而不是 p2->x[1]
.
我会做一个 for 循环,我设置 i = 向量的长度 - 1 并且每次我不增加它,而是减少它直到它大于 0。
for(int i = vet.length;i>0;i--)
我最近注意到在 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]);
}
这种最后访问数组的方法是否有缺陷?
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]
引用倒数第二个元素,依此类推。
虽然标准规定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
生成的代码将始终为 return 1,而为 test2
生成的代码将 return p1->x[0] 中的任何内容.我不知道标准或 gcc 文档中的任何内容表明这两个函数的行为应该不同,也不知道应该如何强制编译器生成代码以适应 p1
和 p2
的情况在必要的情况下碰巧识别已分配块的重叠部分。尽管 test1()
中使用的优化对于编写的函数来说是合理的,但我知道没有记录的标准解释将这种情况视为 UB,但如果它写入 [=20= 则定义代码的行为] 而不是 p2->x[1]
.
我会做一个 for 循环,我设置 i = 向量的长度 - 1 并且每次我不增加它,而是减少它直到它大于 0。
for(int i = vet.length;i>0;i--)