在C ++中分配和使用无类型内存块的正确方法是什么?

时间:2015-07-15 17:01:42

标签: c++ memory-management

到目前为止,我对这个问题的答案有两个完全相反的答案:"它是安全的"和"它未定义的行为"。我决定整体重写这个问题,以便为我和任何可能通过谷歌到达这里的人提供更好的澄清答案。

此外,我删除了C标记,现在这个问题是C ++特定的

我正在制作一个8字节对齐的内存堆,将在我的虚拟机中使用。我能想到的最明显的方法是分配一个std::uint64_t数组。

std::unique_ptr<std::uint64_t[]> block(new std::uint64_t[100]);

我们假设sizeof(float) == 4sizeof(double) == 8。我想在block中存储一个float和一个double并打印该值。

float* pf = reinterpret_cast<float*>(&block[0]);
double* pd = reinterpret_cast<double*>(&block[1]);
*pf = 1.1;
*pd = 2.2;
std::cout << *pf << std::endl;
std::cout << *pd << std::endl;

我还想存储一个C字符串,说'&#34;你好&#34;。

char* pc = reinterpret_cast<char*>(&block[2]);
std::strcpy(pc, "hello\n");
std::cout << pc;

现在我要存储&#34;你好,世界!&#34;超过8个字节,但我仍然可以使用2个连续的单元格。

char* pc2 = reinterpret_cast<char*>(&block[3]);
std::strcpy(pc2, "Hello, world\n");
std::cout << pc2;

对于整数,我不需要reinterpret_cast

block[5] = 1;
std::cout << block[5] << std::endl;

我将block分配为std::uint64_t数组,仅用于内存对齐。我也不希望它自己存储大于8个字节的内容。如果起始地址保证为8字节对齐,则块的类型可以是任何类型。

有些人已经回答说我所做的事情是完全安全的,但有些人说我肯定会调用未定义的行为。

我是否正在编写正确的代码来执行我的意图?如果没有,那么适当的方式是什么?

8 个答案:

答案 0 :(得分:13)

全局分配函数

分配任意(无类型)内存块,全局分配函数(§3.7.4/ 2);

void* operator new(std::size_t);
void* operator new[](std::size_t);

可以用来做(§3.7.4.1/ 2)。

§3.7.4.1/ 2

  

分配功能尝试分配所请求的存储量。如果成功,它将返回存储块的起始地址,其长度以字节为单位应至少与请求的大小一样大。从分配函数返回时,分配的存储的内容没有限制。未指定由连续调用分配函数分配的存储的顺序,连续性和初始值。返回的指针应适当对齐,以便可以将其转换为具有基本对齐要求(3.11)的任何完整对象类型的指针,然后用于访问分配的存储中的对象或数组(直到存储被显式解除分配为止)调用相应的释放函数。)

3.11对基本对齐要求;

有这个说法

§3.11/ 2

  

基本对齐由小于或等于所有上下文中实现所支持的最大对齐的对齐表示,等于alignof(std::max_align_t)

只是为了确保分配函数的行为必须符合这一要求;

§3.7.4/ 3

  

C ++程序中定义的任何分配和/或释放函数,包括库中的缺省版本,都应符合3.7.4.1和3.7.4.2中规定的语义。

来自C++ WD n4527的报价。

假设8字节对齐小于平台的基本对齐(看起来很像,但可以在目标平台上使用static_assert(alignof(std::max_align_t) >= 8)进行验证) - 您可以使用全局{{ 1}}分配所需的内存。分配后,可以根据您的尺寸和对齐要求对存储器进行分段和使用。

这里的另一个选择是 std::aligned_storage,它可以根据需要为你提供内存对齐。

::operator new

从这个问题来看,我假设typename std::aligned_storage<sizeof(T), alignof(T)>::type buffer[100]; 的大小和对齐都是8。

最终内存块的样子是(包括基本RAII);

T

我已经使用合适的模板struct DataBlock { const std::size_t element_count; static constexpr std::size_t element_size = 8; void * data = nullptr; explicit DataBlock(size_t elements) : element_count(elements) { data = ::operator new(elements * element_size); } ~DataBlock() { ::operator delete(data); } DataBlock(DataBlock&) = delete; // no copy DataBlock& operator=(DataBlock&) = delete; // no assign // probably shouldn't move either DataBlock(DataBlock&&) = delete; DataBlock& operator=(DataBlock&&) = delete; template <class T> T* get_location(std::size_t index) { // https://stackoverflow.com/a/6449951/3747990 // C++ WD n4527 3.9.2/4 void* t = reinterpret_cast<void*>(reinterpret_cast<unsigned char*>(data) + index*element_size); // 5.2.9/13 return static_cast<T*>(t); // C++ WD n4527 5.2.10/7 would allow this to be condensed //T* t = reinterpret_cast<T*>(reinterpret_cast<unsigned char*>(data) + index*element_size); //return t; } }; // .... DataBlock block(100); DataBlock函数等live demo herehere with further error checking etc.构建了更详细的construct示例。

关于别名的说明

看起来原始代码中存在一些别名问题(严格来说);你分配一种类型的内存并将其转换为另一种类型。

它可能在您的目标平台上按预期工作,但您不能依赖它。我在这看到的最实用的评论是:

  

"Undefined behaviour has the nasty result of usually doing what you think it should do, until it doesn’t” - hvd

您可能会使用的代码。我认为最好使用适当的全局分配函数,并确保在分配和使用所需内存时没有未定义的行为。

别名仍然适用;一旦分配了内存 - 别名适用于它的使用方式。一旦分配了任意内存块(如上面的全局分配函数)并且对象的生命周期开始(§3.8/ 1) - 应用别名规则。

get怎么样?

虽然std::allocator用于同构数据容器,而您正在寻找的类似于异构分配,但标准库中的实现(给定Allocator concept)提供了有关原始内存分配和相应的对象构造。

答案 1 :(得分:5)

更新新问题:

好消息是,您可以通过newunsigned char[size])来分配内存,这是一个简单易用的解决方案。分配有new的内存在标准中保证以适合用作任何类型的方式对齐,并且您可以使用char*安全地为任何类型设置别名。

标准参考,3.7.3.1 / 2,分配函数:

  

返回的指针应适当对齐,以便它可以   转换为任何完整对象类型的指针,然后用于   访问分配的存储中的对象或数组

原始问题的原始答案:

至少在3.10 / 15中的C ++ 98/03中我们有以下内容,这显然使它仍然是未定义的行为(因为你通过一个类型中没有枚举的类型访问该值)例外清单):

  

如果程序试图通过访问对象的存储值   行为所属的左值以外的左值   未定义):

     

- 对象的动态类型,

     

- 对象动态类型的cvqualified版本,

     

- 与对象的动态类型对应的有符号或无符号类型

     

- 对应于对象动态类型的cvqualified版本的有符号或无符号类型,

     

- 在其成员中包含上述类型之一的聚合或联合类型(包括递归地,子聚合或包含联合的成员),

     

- 一种类型,是对象动态类型的(可能是cvqualified)基类类型,

     

- char或unsigned char类型。

答案 2 :(得分:2)

在这里进行了很多讨论并给出了一些略微错误的答案,但是我还要总结好点,我只想总结一下:

  • 正好遵循标准的文本(无论什么版本)......是的,这是未定义的行为。请注意,标准甚至没有术语严格别名 - 只是一套规则来强制执行它,无论实现可以定义什么。

  • 了解&#34;严格别名&#34;背后的原因规则,它应该可以很好地适用于任何实现这么长,因为floatdouble都不会超过64位。

  • 该标准不保证您对floatdouble(有意)的大小有任何保证,而 首先是限制性的。

  • 你可以通过确保你的&#34;堆&#34;来解决所有这些问题。是已分配的对象(例如,使用malloc()获取它)并通过char *访问对齐的广告位并将偏移量移位3位。

  • 你仍然需要确保你在这样一个插槽中存储的任何东西都不会超过64位。 (这在可移植性方面很难)

简而言之:您的代码应该对任何&#34; sane&#34;实施,只要尺寸限制不是问题(意味着:标题中问题的答案很可能没有),但它仍然是未定义的行为(意味着:答案)到你的最后一段

答案 3 :(得分:1)

pc pfpd都是将block中指定的内存作为uint64_t访问的不同类型,因此对于说&#39; {{1共享类型为pffloat

一个人违反了严格的别名规则,一次使用一种类型写入并使用另一种类型读取,因为编译可以重新排序操作,认为没有共享访问。但是,这不是你的情况,因为uint64_t数组仅用于赋值,它与使用uint64_t分配内存完全相同。

顺便说一句,从任何类型转换为char类型时,严格别名规则都没有问题,反之亦然。这是用于数据序列化和反序列化的常见模式。

答案 4 :(得分:1)

我做得很简单:如果使用

分配块,所有代码都使用定义的语义
std::unique_ptr<char[], std::free>
    mem(static_cast<char*>(std::malloc(800)));

由于

  1. 允许每种类型使用char[]
  2. 进行别名
  3. malloc()保证返回一个足够对齐所有类型的内存块(SIMD除外)。
  4. 我们将std::free作为自定义删除工具传递,因为我们使用的是malloc(),而不是new[],因此默认情况下调用delete[]将是未定义的行为。

    如果您是纯粹主义者,也可以使用operator new

    std::unique_ptr<char[]>
        mem(static_cast<char*>(operator new[](800)));
    

    然后我们不需要自定义删除器。或

    std::unique_ptr<char[]> mem(new char[800]);
    

    避免static_castvoid*char*。但operator new可由用户替换,因此我总是对使用它有点谨慎。 OTOH;无法替换malloc(仅限于特定于平台的方式,例如LD_PRELOAD)。

答案 5 :(得分:0)

是的,因为pf指向的内存位置可能会重叠,具体取决于floatdouble的大小。如果他们没有,那么阅读*pd*pf的结果将得到很好的定义,但不是从blockpc阅读的结果。

答案 6 :(得分:0)

C ++和CPU的行为是截然不同的。尽管标准提供了适用于任何对象的内存,但CPU强加的规则和优化使得任何给定对象的对齐“未定义” - 短的数组可以合理地为2字节对齐,但是3字节结构的数组可能是8字节对齐。可以在存储和使用之间创建和使用所有可能类型的联合,以确保不会破坏对齐规则。

union copyOut {
      char Buffer[200]; // max string length
      int16 shortVal;
      int32 intVal;
      int64 longIntVal;
      float fltVal;
      double doubleVal;
} copyTarget;
memcpy( copyTarget.Buffer, Block[n], sizeof( data ) );  // move from unaligned space into union
// use copyTarget member here.

答案 7 :(得分:-3)

如果您将此标记为C ++问题, (1)为什么使用uint64_t []而不是std :: vector? (2)在内存管理方面,你的代码缺乏管理逻辑,它应该跟踪哪些块正在使用,哪些是免费的,以及跟踪连续块,当然还有分配和释放块方法。 (3)代码显示使用内存的不安全方式。例如,char *不是const,因此可以写入块并覆盖下一个块。 reinterpret_cast被认为是危险的,应该从内存用户逻辑中抽象出来。 (4)代码不显示分配器逻辑。在C世界中,malloc函数是无类型的,在C ++世界中,运算符new是类型化的。你应该考虑像new运算符这样的东西。