在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*
。
答案 0 :(得分:7)
这是CWG 232。这个问题似乎主要与取消引用空指针有关,但从根本上讲与简单取消引用未指向对象的含义有关。对于这种情况,没有明确的语言规则。
问题中的一个例子是:
类似地,只要不使用该值,就应允许取消引用指向数组末尾的指针:
char a[10]; char *b = &a[10]; // equivalent to "char *b = &*(a+10);"
这两种情况在实际代码中经常出现,应予以允许。
除了使用a[10]
而不是数组类型外,这基本上与OP(上述表达式的char
部分)相同。
通常的看法是,取消引用过去的指针是一种不确定的行为。但是,这是否适用于指向数组类型的指针?
根据指针的类型,规则没有区别。 my_past_end
是一个过去的指针,因此是否用UB对其取消引用并不是因为它指向数组而不是其他类型的事实。
虽然is_this_valid
和int*
的类型是从int(&)[3]
(数组到指针的衰减)初始化的,所以这里实际上没有从内存中读取任何内容,这对语言规则的工作方式。 my_past_end
是一个指针,其值为past the end of an object,这是唯一重要的事情。
答案 1 :(得分:1)
该标准似乎表明这是不是未定义的行为。
该标准的相关部分如下(关于将指针类型添加到整数类型或以其他方式添加的结果)
§5.7p4[expr.add]
将具有整数类型的表达式添加到指针或从指针中减去时,结果将具有指针操作数的类型。如果指针操作数指向数组object 84 的元素,并且数组足够大,则结果指向与原始元素偏移的元素,以使结果和下标的下标之间存在差异。原始数组元素等于整数表达式。 [...]表达式
(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[基本化合物]
可以通过以下方式构造复合类型:
[...]
4. 引用给定类型的对象或功能
§3.9.2p3[basic.compound]
[...] [注意:例如,将数组末尾的地址(5.7)视为指向数组元素类型的不相关对象,该对象可能是位于该地址[...]
注意如何非常努力地确保过去指针仍然被定义为对象的地址。由于引用只能引用对象,因此这允许使用*
(末尾指针)之类的“无效”引用,但仍不允许空引用,因为nullptr
并不指向对象。 / p>
§4.2p1[转换数组]
可以将类型为“
N
T
的左值或右值或“T
的未知边界的数组”转换为类型为“ {{1的指针}}”。结果是一个指向数组第一个元素的指针。
由于左值正在转换,因此不会访问无效的内存。因此,在转换期间,将创建类型为T
的prvalue,它指向与int*
相同的地址,即&my_array[3]
的值。因此,它们将是相等的(毫无疑问,指向相同地址的指针被定义为相等)
您还可以将std::end(my_array)
直接转换为my_past_end
,这样就可以了,因为int*
是int[3]
s的复合类型(int
是int
的子对象,因此这将是一种不太混乱的方法。
请注意,即使int[3]
被定义为&my_array[4]
,而{{ 1}}是my_array[4]
,C中的*(my_array + 4)
与将&my_array[4]
转换为右值(并有效地断言它是非空指针)相同。由于C ++中不存在此类异常,因此将使用此处显示的逻辑(&*(my_array + 4)
是无法转换为prvalue的引用)。
看起来确实很含糊。标准中不再提及“无关”对象。它们可能被其他东西占据,例如:
&*(expression)
(expression)
和my_array[4]
指向相同的内存地址。但是int arr[3][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
意味着arr[0][3]
将被更新为读取arr[1][0]
吗?
arr[0][3] = 10;
似乎在msvc和GCC中返回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;
}
)
由于引用引用的是带有地址的对象,因此10
的定义很明确。但是由于这些“不相关”的对象再也不会被提及,因此使用除地址之外的任何其他方法,由于其字面上未定义,因此是未定义的行为。
答案 2 :(得分:-1)
我认为它定义明确,因为它不会取消引用过去的指针。
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
的结尾。不能访问数组对象末尾的内存。