首页 > 解决方案 > 在 C++ 中为零大小的分配返回唯一地址的基本原理是什么?

问题描述

在 C++ 中为零大小的分配返回唯一地址的基本原理是什么?

背景:C11 标准中提到了 malloc(7.20.3 内存管理功能):

如果请求的空间大小为零,则行为由实现定义:要么返回空指针,要么行为好像大小是某个非零值,但返回的指针不应用于访问对象。

也就是说,正如我所看到的,malloc对于零大小的分配总是成功的,因为你只能使用零大小分配的指针来调用其他一些内存分配函数free,比如它:

此外,C11(也是 7.20.3)没有指定从 malloc 返回的地址必须是唯一的,只是它们必须指向不相交的内存区域:

如果分配成功,则返回的指针经过适当对齐,以便可以将其分配给指向任何类型对象的指针,然后用于访问已分配空间中的此类对象或此类对象的数组(直到空间被显式释放) . 已分配对象的生命周期从分配一直延伸到解除分配。每个这样的分配都应产生一个指向与任何其他对象不相交的对象的指针。

所有零大小的对象都是不相交的 AFAICT,这意味着malloc可以为多个零大小的分配返回相同的指针(例如NULL会很好),或者每次都返回不同的指针,或者某些指针返回相同的指针等。

然后 C++98 出现了两个原始内存分配函数:

void* operator new(std::size_t size);
void* operator new(std::size_t size, std::align_val_t alignment);

请注意,这些函数仅返回原始内存:它们不会创建或初始化任何 AFAICT 类型的任何对象。

你这样称呼他们:

#include <iostream>
#include <new>
int main() {
    void* ptr = operator new(std::size_t{0});
    std::cout << ptr << std::endl;
    operator delete(ptr, std::size_t{0});
    return 0;
}

C++17 标准的[new.delete.single]部分解释了它们,但我看到的关键保证在[basic.stc.dynamic.allocation]

即使请求的空间大小为零,请求也可能失败。如果请求成功,则返回的值应是与任何先前返回的值 p1 不同的非空指针值 (7.11) p0,除非该值 p1 随后被传递给操作员删除。此外,对于 21.6.2.1 和 21.6.2.2 中的库分配函数,p0 应表示与调用者可访问的任何其他对象的存储不相交的存储块的地址。通过作为零大小请求返回的指针的间接效果是未定义的。 38

也就是说,它们必须始终在成功时返回不同的指针。这与malloc.

我的问题是:这种变化背后的基本原理是什么?(也就是说,在 C++ 中为零大小的分配返回唯一地址之后)

理想情况下,答案只是指向探索替代方案并激发其语义的论文(或其他来源)的链接。通常我会为这些 C++98 问题选择 The Design and Evolution of C++,但第 10 节(内存管理)没有提及任何相关内容。否则,某种权威参考会很好。


免责声明:我在reddit上问过,但我问得不够好,所以我没有得到任何有用的答案。我想请问您,如果您只有一个假设,请随时将其作为答案发布,但请提及这只是一个假设。

此外,在 reddit 上,人们继续讨论零大小的类型,我是否有更改标准的建议等。这个问题是关于在传递大小等于零时原始内存分配函数的语义。如果诸如零大小类型之类的主题与您的答案相关,请包括它们!但尽量不要因切线问题而出轨。

此外,在 reddit 上,人们还提出了诸如“这是出于优化目的”之类的论点,但实际上无法提及更具体的内容。我希望答案中的“因为优化”更具体。例如,一位 redditor 提到了别名优化,但我想知道哪种别名优化适用于无法取消引用的指针,并且无法让任何人对此发表评论。因此,如果您要提及优化,也许可以举一个小例子来丰富讨论。

标签: c++clanguage-lawyerstandardsnew-operator

解决方案


问题是C++ 中的对象(无论大小)必须具有唯一的标识。因此不同的共存对象(无论它们的大小)必须具有不同的地址,因为假设两个比较相等的指针指向同一个对象

如果您承认零大小的对象可以具有相同的地址,那么您将无法再区分两个地址是否是同一个对象。


许多关于“新不返回对象”问题的评论。

请在这种情况下忘记 OOP 术语:

C++ 规范对“对象”一词的含义有一个精确的定义。

CPP参考:对象

尤其是:

C++ 程序创建、销毁、引用、访问和操作对象。在 C++ 中,对象是一个存储区域,它具有

  • 大小(可以用 sizeof 确定);
  • 对齐要求(可以通过 alignof 确定);
  • 存储持续时间(自动、静态、动态、线程本地);
  • 生命周期(受存储期限或临时限制);
  • 类型;
  • 值(可能是不确定的,例如对于默认初始化的非类类型);
  • 可选地,名称。

以下实体不是对象:值、引用、函数、枚举器、类型、非静态类成员、位域、模板、类或函数模板特化、命名空间、参数包和 this。

变量是非静态数据成员的对象或引用,由声明引入。

在更改联合的活动成员时以及需要临时对象时,对象由定义、新表达式、抛出表达式创建。


推荐阅读