终止指向数组类型的结束指针

时间:2018-10-09 18:12:31

标签: c++ arrays language-lawyer dereference

在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*

3 个答案:

答案 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_validint*的类型是从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_ptrmy_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的复合类型(intint的子对象,因此这将是一种不太混乱的方法。

请注意,即使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的结尾。不能访问数组对象末尾的内存。