在c ++中是否已经很好地定义了一个指向数组类型的一个过去的指针?
请考虑以下代码:
#include <cassert>
#include <iterator>
int main()
{
// An array of ints
int my_array[] = { 1, 2, 3 };
// Pointer to the array
using array_ptr_t = int(*)[3];
array_ptr_t my_array_ptr = &my_array;
// Pointer one-past-the-end of the array
array_ptr_t my_past_end = my_array_ptr + 1;
// Is this valid?
auto is_this_valid = *my_past_end;
// Seems to yield one-past-the-end of my_array
assert(is_this_valid == std::end(my_array));
}
常识是,取消引用一个过去的指针是未定义的行为。但是,对于指向数组类型的指针,这是否适用?
这应该是有效的似乎是合理的,因为*my_past_end
可以纯粹用指针算法解决,并产生一个指向数组中第一个元素的指针,这也恰好是一个有效的一个结束的int*
原始阵列my_array
。
然而,另一种看待它的方式是*my_past_end
正在生成一个不存在的数组的引用,它隐式转换为int*
。这个参考对我来说似乎有问题。
对于背景,我的问题是由this question提出的,特别是对this answer的评论。
编辑:这个问题不是Take the address of a one-past-the-end array element via subscript: legal by the C++ Standard or not?的副本我问的是问题中解释的规则是否也适用于指向数组类型的指针。
编辑2:删除auto
以明确表示my_array_ptr
不是int*
。
这是CWG 232。这个问题似乎主要是关于取消引用空指针,但它基本上是关于简单地取消引用不指向对象的东西意味着什么。关于这种情况没有明确的语言规则。
该问题的一个例子是:
同样,只要不使用该值,就应该允许取消引用指向数组末尾的指针:
char a[10]; char *b = &a[10]; // equivalent to "char *b = &*(a+10);"
这两种情况在实际代码中经常出现,应该允许它们。
这与OP(上面表达式的a[10]
部分)基本相同,除了使用char
而不是数组类型。
常识是,取消引用一个过去的指针是未定义的行为。但是,对于指向数组类型的指针,这是否适用?
基于它是什么类型的指针,规则没有区别。 my_past_end
是一个过去的结束指针,所以它是否取消引用它不是它指向一个数组而不是任何其他类型的事实的函数。
虽然is_this_valid
和int*
的类型从int(&)[3]
(数组到指针衰减)初始化,因此实际上没有从内存中读取 - 这对于语言规则的工作方式并不重要。 my_past_end
是一个指针,其值为past the end of an object,这是唯一重要的事情。
标准似乎表明这不是未定义的行为。
标准的相关部分如下(关于将指针类型添加到整数类型或相反的结果)
§5.7p4[expr.add]
当向指针添加或从指针中减去具有整数类型的表达式时,结果具有指针操作数的类型。如果指针操作数指向数组object84的元素,并且数组足够大,则结果指向偏离原始元素的元素,使得结果元素和原始数组元素的下标的差异等于整数表达式。 [...]表达式
(P)+1
指向数组对象的最后一个元素。 [...]如果指针操作数和结果都指向同一个数组对象的元素,或者指向数组对象的最后一个元素,则评估不应产生和溢出;否则,行为未定义。
脚注84阅读:
为此目的,不是数组元素的对象被认为属于单个元素数组;见5.3.1
(§5.3.1是关于&
和*
)
因此,为了my_array_ptr
和my_past_end
指向的目的,他们指向my_array
,好像my_array
实际上是int[1][3]
。 my_array_ptr
指向第一个元素(my_array实际上是int[3]
)。 my_past_end
指向一个过去的元素,这是明确定义的。
当你做*my_past_end
时,你创建一个int[3]
的左值。只要没有转换为prvalue,你就不会实际访问不是int[3]
的内存,就好像它是int[3]
一样。
§3.9.2p1[basic.compound]
化合物类型可以通过以下方式构建: [...] 4.对给定类型的对象或函数的引用
§3.9.2p3[basic.compound]
[...] [注意:例如,超过数组末尾的地址(5.7)将被视为指向数组元素类型的无关对象,该对象可能位于该地址[...]
请注意它是如何努力确保将过去的结束指针仍定义为对象的地址。因为引用只能引用对象,所以这允许“无效”引用,如*
(过去的结束指针),但仍然不允许空引用,因为nullptr
不指向对象。
§4.2p1[conv.array]
“
N
T
数组”或“T
未知界限数组”类型的左值或右值可以转换为“指向T
指针”类型的prvalue。结果是指向数组的第一个元素的指针。
由于正在转换左值,因此不会访问无效的内存。因此,在转换期间,创建类型为int*
的prvalue,指向与&my_array[3]
相同的地址,这是std::end(my_array)
的值。所以,它们是平等的(不出所料,指向同一地址的指针被定义为相等)
您也可以直接将my_past_end
转换为int*
并且它可以工作,因为int[3]
是int
的复合类型(int
是int[3]
的子对象),所以这将是一种不那么令人困惑的方式。
作为旁注,&my_array[4]
在C和C ++中工作的原因,即使C没有参考,是因为my_array[4]
被定义为*(my_array + 4)
,而&my_array[4]
是&*(my_array + 4)
,而cazxswpoi在C中,与将&*(expression)
转换为(expression)
相同一个rvalue(并有效断言它是一个非空指针)。由于C ++中不存在此类异常,因此使用此处显示的逻辑(my_array[4]
是无法转换为prvalue的引用)。
它似乎很模糊。标准中再也没有提到“不相关”的对象。它们可能被其他东西占用,例如:
int arr[3][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
arr[0][3]
和arr[1][0]
指向相同的内存地址。但arr[0][3] = 10;
是否意味着arr[1][0]
将更新为阅读10
?
int test() {
int arr[3][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
const int& i = arr[1][0];
arr[0][3] = 10;
return i;
}
似乎在msvc和GCC中返回10
(优化到mov eax, 10
ret
)
由于引用是指具有地址的对象,因此&(reference)
定义良好。但是,由于这些“不相关”的对象再也没有被提及过,因此使用除了它们之外的任何东西,由于字面上没有被定义,所以未定义的行为。
我相信它定义得很好,因为它不会取消引用一个过去的指针。
auto is_this_valid = *my_past_end;
my_past_end
的类型为int(*)[3]
(指向3个int
元素的数组)。因此,表达式*my_past_end
的类型为int[3]
- 与此上下文中的任何数组表达式一样,它“衰减”为类型为int*
的指针,指向数组对象的初始(第零个)元素。这种“衰变”是一种编译时操作。所以初始化只是初始化is_this_valid
,int*
类型的指针,指向my_array
的末尾。没有访问数组对象末尾的内存。