递增指向0大小的动态数组的指针是否未定义?


34

AFAIK,尽管我们无法创建大小为0的静态内存数组,但是我们可以使用动态数组来做到这一点:

int a[0]{}; // Compile-time error
int* p = new int[0]; // Is well-defined

如我所读,p行为就像一个过去的元素。我可以打印p指向的地址。

if(p)
    cout << p << endl;
  • 尽管我确定我们不能像使用迭代器(过去元素)那样取消引用该指针(过去元素),但是我不确定是否要增加该指针p?是否像迭代器一样具有未定义的行为(UB)?

    p++; // UB?

4
UB “ ...任何其他情况(即尝试生成不指向同一数组的元素或不指向末尾的元素的指针)都会调用未定义的行为...。” from:en.cppreference.com / w / cpp / language / operator_arithmetic
理查德·克里斯滕

3
好吧,这类似于其中std::vector包含0项目的a 。begin()已经等于,end()因此您无法递增指向开头的迭代器。
Phil1970年

1
@PeterMortensen我认为您的编辑更改了最后一句话的含义(“我确定->我不确定为什么”),请您仔细检查一下?
法比奥说,请

@PeterMortensen:您编辑的最后一段变得不太可读。
Itachi Uchiwa,

Answers:


32

指向数组元素的指针可以指向有效元素,也可以指向末尾。如果以超过末尾一个以上的方式递增指针,则该行为是不确定的。

对于大小为0的数组,p已经指向末尾一个,因此不允许对其进行递增。

有关+运算符,请参见C ++ 17 8.7 / 4 (++具有相同的限制):

f表达式P指向具有n个元素x[i]的数组对象的x元素,如果0≤i+j≤n ,则表达式P + JJ + P(其中Jj)指向(可能是假设的)元素x[i+j];否则,行为是不确定的。


2
所以,唯一的情况x[i]是一样的x[i + j]是当两个ij的值为0?
拉米日元

8
@RamiYen x[i]是作为相同的元件x[i+j],如果j==0
interjay

1
gh,我讨厌C ++语义的“暮光之城” ... +1。
einpoklum

4
@ einpoklum-reinstateMonica:确实没有暮光区。即使对于N = 0情况,也只是C ++是一致的。对于N个元素的数组,有N + 1个有效的指针值,因为您可以指向数组的后面。这意味着您可以从数组的开头开始,然后将指针增加N次以到达结尾。
MSalters

1
@MaximEgorushkin我的答案是关于当前语言所允许的。相反,您希望它允许的讨论不合时宜。
interjay

2

我想你已经有了答案;如果您看得更深一些:您已经说过,增加一个后端迭代器就是UB,那么:这个答案在什么迭代器中?

迭代器只是一个具有指针的对象,并且递增该迭代器实际上是在递增其具有的指针。因此,在许多方面,根据指针来处理迭代器。

int arr [] = {0,1,2,3,4,5,6,7,8,9};

int * p = arr; // p指向arr中的第一个元素

++ p; // p指向arr [1]

正如我们可以使用迭代器遍历向量中的元素一样,我们可以使用指针遍历数组中的元素。当然,要做到这一点,我们需要获得指向第一个元素的指针,以及指向最后一个元素的指针。如前所述,我们可以使用数组本身或通过获取第一个元素的地址来获取指向第一个元素的指针。我们可以通过使用数组的另一个特殊属性来获得一个场外指针。我们可以将不存在的元素的地址放在数组的最后一个元素之后:

int * e =&arr [10]; //指针刚经过arr中的最后一个元素

在这里,我们使用下标运算符为不存在的元素建立索引;arr有十个元素,所以arr中的最后一个元素位于索引位置9。我们只能用该元素的地址来初始化e。像场外迭代器(第3.4.1节,第106页)一样,场外指针也不指向元素。结果,我们可能不会取消引用或增加端头指针。

这来自Lipmann的C ++入门5版。

所以是UB不要这样做。


-4

从最严格的意义上讲,这不是未定义行为,而是实现定义的。因此,尽管不建议您计划支持非主流体系结构,但是您可以做到。

interjay给出的标准引号是一个很好的表示UB,但是在我看来,它只是第二好命,因为它处理指针-指针算术(有趣的是,一个显式是UB,而另一个则不是UB)。有一段直接涉及问题中的操作:

[... expr.post.incr] / [expr.pre.incr]
操作数应为指向完全定义的对象类型的指针。

哦,等等,一个完全定义的对象类型?就这样?我的意思是,真的,键入?因此,您根本不需要任何对象?
需要花费大量的阅读才能真正发现其中的定义可能不够明确。因为到目前为止,它看起来像是完全允许您这样做,没有任何限制。

[basic.compound] 3声明一个指针可能具有什么类型,而不是其他三个指针,您的操作结果显然将归入3.4:无效指针
但是,它并没有说不允许您使用无效的指针。相反,它列出了一些非常常见的正常情况(例如存储持续时间结束),在这些情况下指针经常变为无效。因此,这显然是可以允许的事情。确实:

[basic.stc] 4
通过无效的指针值进行的间接传递以及将无效的指针值传递给释放函数均具有未定义的行为。无效指针值的任何其他使用都具有实现定义的行为。

我们在此处执行“其他”操作,因此它不是未定义行为,而是实现定义的,因此通常是允许的(除非实现明确表示不同)。

不幸的是,这还不是故事的结局。尽管最终结果从现在开始没有任何变化,但是随着您搜索“指针”的时间延长,结果变得更加混乱:

[basic.compound]
对象指针类型的有效值表示内存中字节地址或空指针。如果类型T的对象位于地址A,则无论该值如何获得称其指向该对象。
[注意:例如,将数组末尾的地址视为指向该数组元素类型的不相关对象,该对象可能位于该地址。[...]。

读为:好的,谁在乎!只要指针指向内存中的某个位置,我就好吗?

[basic.stc.dynamic.safety]指针值是安全派生的指针[blah blah]

读取为:可以,安全派生。它没有解释这是什么,也没有说我真正需要它。安全地得出结论。显然,我仍然可以拥有非安全派生的指针。我猜想取消引用它们可能不是一个好主意,但是完全可以允许它们。它没有说其他。

一个实现可能具有宽松的指针安全性,在这种情况下,指针值的有效性不取决于它是否是安全得出的指针值。

哦,所以没关系,只是我的想法。但是等等...“可能不会”吗?这意味着,也可能如此。我怎么知道?

备选地,实现可以具有严格的指针安全性,在这种情况下,不是安全导出的指针值的指针值是无效的指针值,除非所引用的完整对象具有动态存储持续时间并且先前已声明为可到达

等待,所以甚至有可能需要调用declare_reachable()每个指针吗?我怎么知道?

现在,您可以将转换为intptr_t,它定义明确,给出了安全派生指针的整数表示形式。当然,对于该整数,它是完全合法的,并且定义明确,可以根据需要对其进行递增。
是的,您可以将intptr_tback 转换为指针,该指针也定义明确。只是,不是原始值,不再保证您有一个安全派生的指针(显然)。总而言之,就标准而言,虽然是实现定义的,但这是100%合法的事情:

[expr.reinterpret.cast] 5
可以将整数类型或枚举类型的值显式转换为指针。指针转换为足够大小的整数并返回相同的指针类型原始值;指针和整数之间的映射否则由实现定义。

抓住

指针只是普通的整数,只有您碰巧将它们用作指针。哦,如果那是真的!
不幸的是,存在一些根本不正确的体系结构,仅生成无效的指针(不取消引用,仅将其放在指针寄存器中)会导致陷阱。

这就是“实现定义”的基础。也就是说,而且增加一个指针,只要你想,事实,请你当然原因溢出,该标准没有要处理的。应用程序地址空间的末尾可能与溢出的位置不一致,甚至您甚至都不知道特定体系结构上的指针是否存在诸如溢出之类的问题。总而言之,这是一场噩梦般的混乱,与可能的收益没有任何关系。

另一方面,处理一个过去的对象条件很容易:实现必须简单地确保没有对象被分配,以便地址空间中的最后一个字节被占用。因此,这是明确定义的,因为它可以保证有用且微不足道。


1
您的逻辑有缺陷。“所以你根本不需要物体吗?” 通过专注于单个规则来误解标准。无论您的程序格式是否正确,该规则都与编译时间有关。关于运行时间还有另一条规则。只有在运行时,您才能真正谈论某个地址处对象的存在。您的程序需要满足所有规则;编译时的编译时规则和运行时的运行时规则。
MSalters

5
您会遇到类似的逻辑缺陷:“好,谁在乎!只要指针指向内存中的某个位置,我就很好吗?”。不,您必须遵守所有规则。关于“一个数组的结束是另一个数组的开始”的困难语言只是给予实现连续分配内存的权限;它不需要在分配之间保留可用空间。这确实意味着您的代码可能与一个数组对象的结尾和另一个数组对象的起点都具有相同的值A。
MSalters

1
“陷阱”不是可以通过“实现定义”行为来描述的东西。请注意,interjay发现了对+运算符的限制(从哪个运算符++流出),这意味着未定义在“ one-after-the-end”之后的指向。
马丁·邦纳

1
@PeterCordes:请务必阅读basic.stc,第4段。它说:“间接未定义行为。对无效指针值的任何其他使用都具有实现定义的行为”。通过使用该术语的其他含义,我不会使人们感到困惑。这是确切的措辞。这不是未定义的行为。
戴蒙

2
您几乎不可能发现后期增量的漏洞,但是您没有引用完整的章节来说明后期增量的作用。我现在不打算自己调查这个问题。同意如果有一个,那是意料之外的。无论如何,如果ISO C ++为平面内存模型定义了更多东西@MaximEgorushkin,那将是一件好事,还有其他一些原因(例如指针环绕)不允许使用任何东西。查看有关在64位x86中指针比较应该签名还是不签名的
彼得·科德斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.