【问题标题】:Can C++ array end at memory boundary?C++ 数组可以在内存边界结束吗?
【发布时间】:2014-12-12 18:16:23
【问题描述】:

C++ 标准(以及与此相关的 C)允许创建(虽然不是取消引用)指向数组末尾后一个元素的指针。这是否意味着数组永远不会被分配到其最后一个元素在内存边界处结束的位置?我了解在实践中部分/所有实现可能遵循此约定,但以下哪一项是正确的:

  1. 实际上是错误的,数组可能在内存边界处结束,或者
  2. C++ 标准要求在边界之前至少结束一个元素的内存价值,或者
  3. 不是 1 也不是 2,但在实际编译器中仍然是这样,因为它使实现更容易。

C 的情况有什么不同吗?

更新: 似乎 1 是正确答案。请参阅下面 James Kanze 的回答,另请参阅 efencehttp://linux.die.net/man/3/efence - 感谢 Michael Chastain 提供指向它的指针)

【问题讨论】:

  • 我不认为标准,当然对于 C++ 要求任何关于内存分配的内容,所以 1 和 2 不会是真的,可能 3 更可能是实现细节
  • 我应该澄清我的第一条评论,因为 1 它可能在内存边界处结束,但这不是标准中指定的内容,而是由实施者决定
  • @JamesKanze :我的意思是 0x40000000 可以存储到 32 位指针变量中,而 0x100000000 不能。因此,可以很容易地使指针超过数组末尾的位置,除非该指针会溢出指针类型。只有当可寻址范围满足或超过指针类型的范围时,才会发生这种情况。我想不出一个这样的平台。显然,陷阱条件可能存在(就像您在回答中指出的那样),但可以解决这些问题。
  • @SanderDeDycker 它总是可以工作的;只是不要使用分配块的最后一个字节/字。至于可寻址范围超出指针类型范围的机器:如果您的指针不包括段寄存器,则有 Intel。
  • @abi:我更喜欢将指针视为指向内存位置“之间”的空格,并说“*foo”表示“紧跟在foo 点之后的东西”。一把 12 英寸的尺子在 0 英寸、1 英寸、2 英寸、直到 12 英寸处有 13 个标记,标记之间有 12 个空格;每 12 英寸都有一个标记在开头和结尾。在这样的情况下从观点来看,12" 标记只是一个标记,就像其他标记一样,即使它后面没有任何东西。

标签: c++ c arrays c++11


【解决方案1】:

标准明确说明了当您将指针递增到最后一个元素时会发生什么。它为您提供了一个值,该值只能用作比较来检查您是否在数组末尾或之前。指针很可能指向为其他对象有效分配的内存,但这是完全未定义(实现定义?)的行为,因此使用该指针绝对是未定义的行为。

我的意思是,过去的指针就是这样:它是当您将指针递增到最后一个元素时获得的指针,以非常便宜的方式。但请注意,比较不相关对象的指针是完全没有意义的(如果我没记错的话,甚至是未定义的行为)。因此,不同对象之间的指针“值”可能存在重叠这一事实不是问题,因为在利用这一点时,您会进入未定义行为领域..

【讨论】:

  • 第一段是完全错误的。如果你增加一个空指针,你有未定义的行为。有些实现几乎允许指针中的任何内容,有些则不允许。
  • @rubenvb:我认为你错过了一个事实,即“安全派生”指针的概念。
  • @James 这个解释更好吗?
  • 这有点误导。 “比较你是否在最后”表明它只允许operator==,但operator< 也可以。您可以检查指针是否在结尾之前。在这种情况下,允许编译器将operatoar< 优化为operator !=
【解决方案2】:

这取决于实施。至少在 Visual C++ 中,不使用任何数组绑定检查,您可以创建一个指针,该指针可以超过数组末尾的任意数量的元素。如果您取消引用它,只要您访问的内存地址在程序的分配堆/堆栈内,它仍然可以工作。您将读取/修改该内存位置中的任何值。如果地址超出分配的内存空间会报错。

调试器有检查来检测这些,因为这种编码会产生很难跟踪的错误。

【讨论】:

  • 问题不在于 VC++,而在于标准的要求。
  • you could create a pointer any number of elements past the end of the array VC++ 可能允许 UB(和“工作”),但这并不能回答问题。
【解决方案3】:

你说对了一半。假设一个假设的实现使用线性寻址的内存和表示为 16 位无符号整数的指针。还假设空指针表示为零。最后,假设您使用char *p = malloc(16); 请求16 字节的内存。然后保证您将获得一个数值小于 65520 的指针。值 65520 本身是无效的,因为正如您正确指出的那样,假设分配成功,p + 16 是一个有效的指针,必须不是空指针。

但是,现在假设一个假设的实现使用线性寻址的内存和指针,它们表示为 32 位无符号整数,但只有 16 位的地址空间。再次假设空指针表示为零。最后,再次假设您使用char *p = malloc(16); 请求16 字节的内存。那么它只保证你会得到一个数值小于或等于 65520 的指针。值 65520 本身是有效的,只要实现确保将 16 加到你的值 65536,并且减去 16 后返回 65520。即使地址 65536 处根本不存在内存(物理或虚拟),这也是有效的。

【讨论】:

  • 我喜欢这样一种假设性的考虑,即一个内存块可能会返回到如此高的位置,以至于end 指针会溢出并换行。
【解决方案4】:

一个实现必须允许一个指向末尾的指针 存在。它如何做到这一点是它的业务。在许多机器上,您 可以安全地将任何值放入指针中,没有风险(除非 你取消引用它);在这样的系统上,一个过去的 指针可能指向未映射的内存——我已经 居然在windows下遇到了一个case。

在其他机器上,只需加载指向未映射内存的指针 放入寄存器会陷入陷阱,导致程序崩溃。在 这样的机器,实施必须确保这不会 发生,或者通过拒绝使用最后一个字节或字 分配的内存,或者通过确保所有使用的指针 除了取消引用它避免了任何可能的指令 导致硬件将其视为无效指针。 (大多数这样的 系统有单独的地址和数据寄存器,并且只会 如果指针被加载到地址寄存器中,则陷阱。如果 数据寄存器足够大,编译器可以安全加载 指向数据寄存器的指针,例如比较。这是 无论如何通常是必要的,因为地址寄存器并不总是 支持比较。)

关于您的最后一个问题:C 和 C++ 在这方面完全相同; C++ 只是从 C 中继承了规则。

【讨论】:

  • 这是非常有用的。而且相当疯狂/可怕。我永远不会合理地期望我不允许操纵指向我喜欢的任何地方的指针。这实际上有点使我过去实现的东西无效(结束迭代器指向未映射的内存)。这样的系统是否通常分页(因此减少了由于大块内存被映射而陷入困境的机会)?
  • @v.oddou 有很多机器都是这种情况。从 Intel 架构开始,如果您使用段寄存器。 (将无效段加载到段寄存器中会陷入陷阱。今天的大多数编译器都不支持它,但我在 Intel 上工作过 48 位指针,仅读取随机指针值可能会陷入陷阱。)
  • @v.oddou 或者就像我曾经在 C 的早期看到的那样:“啊,在过去的好日子里,男人是男人,女人是女人,指针是整数。”看起来我们已经完成了一个完整的循环,而且今天大多数人再次认为指针必须映射到某种整数类型。 (在我学习 C 的时候,我使用的机器大多是 16 位的,但我们的内存远远超过 64K。还有各种“结构化”指针来解决它。然后,还有 36 位字寻址机器,其中char*的字节地址和大小在高位。)
  • @psyill 然后这句话涵盖了它。这是一个非常标准的习惯用法:在现代 C++ 中,for ( p = std::begin(a); p != std::end(a); ++ p ) 将留下 p 指向末尾。
  • @James Kanze 不同意“今天,32 位系统或多或少是最低要求”。对于嵌入式处理器,2014 年每年至少有数百万人使用 16 位 int,而 C 语言在那里非常流行。没有关于 2013 年制造的所有处理器的数字,但如果今天的大多数处理器都是 32 位以下的,我不会感到惊讶。
【解决方案5】:

在 §3.9.2/3 [复合类型] 中有一段有趣的段落:

指向 void 的指针或指向对象类型的指针的类型称为对象指针类型。 [...] 对象指针类型的有效值表示内存中字节的地址 (1.7) 或空指针 (4.10)。

连同 §5.7/5 [加法运算符] 中的文本:

[...] 此外,如果表达式 P 指向数组对象的最后一个元素, 表达式 (P)+1 指向数组对象的最后一个元素,如果表达式 Q 指向 超过数组对象的最后一个元素,表达式 (Q)-1 指向数组的最后一个元素 对象。

如果要求最后一个指针必须有效,则似乎无法分配以内存中最后一个字节结尾的数组。如果允许过去的指针无效,我不知道答案。

第 3.7.4.2/4 节 [解除分配功能] 规定:

使用无效指针值的影响 (包括将其传递给释放函数)是未定义的。

因此,如果必须支持比较已分配数组的过去一指针,则结束一指针必须有效。

根据我得到的 cmets,我假设一个实现可以分配一个数组,而不必关心数组的结束指针是否可用。但是我想找出标准中的相关段落。

【讨论】:

  • 您对5.7/5 的引用并没有说过去的指针必须是有效的。
  • 不,它没有明确这样做。我试图弄清楚标准的其他部分是否对使用过去指针的能力提出了要求。
  • 您的结论是错误的(实际上我曾经见过这样一种情况,即数组的末尾位于分配块的绝对末尾)。所需要的只是对此类指针的操作(解除引用除外)有效。在大多数当前环境中它们都会这样做。
  • 这意味着标准中没有要求能够使用每个可能的结束指针的能力?
  • sn-p foo* end = myarray+ arraylen; for (foo* p = myarray; p != end; p++) 有效,即使 myarray+arraylen 的计算结果为 NULL(或 (foo*)0)。你只是不应该改用p<end
猜你喜欢
  • 1970-01-01
  • 2013-09-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多