10

在 C 中,可以使用然后使用指针算法来分配动态数组malloc(sizeof(T) * N),然后使用指针算法来获取此动态数组中 i 偏移处的元素。

在 C++ 中,可以operator new()以与相同的方式使用malloc(),然后放置 new(例如,可以在 Herb Sutter 的“Exceptional C++:47 个工程难题、编程问题和解决方案”一书中查看第 13 项的解决方案)。如果您没有,则此问题的解决方案摘要将是:

T* storage = operator new(sizeof(T)*size);

// insert element    
T* p = storage + i;
new (p) T(element);

// get element
T* element = storage[i];

对我来说,这看起来是合法的,因为我要求一块具有足够内存的内存来保存大小 = 的 N 个对齐元素sizeof(T)。由于sizeof(T)应该返回对齐的元素大小,并且它们一个接一个地放置在一块内存中,因此在这里使用指针算法是可以的。

但是,然后我被指向如下链接:http://eel.is/c++draft/expr.add#4http://eel.is/c++draft/intro.object#def:object并声称在 C++operator new()中,它不返回数组对象,因此与 ANSI C 相比,它返回​​的内容并将其用作数组的指针算术是未定义的行为。

我不擅长这么低级的东西,我真的想通过阅读以下内容来理解:https ://www.ibm.com/developerworks/library/pa-dalign/或者这个:http://jrruethe。 github.io/blog/2015/08/23/placement-new/但我仍然无法理解 Sutter 是否完全错了?

我确实明白这alignas在以下结构中是有意义的:

alignas(double) char array[sizeof(double)];

(c) http://georgeflanagin.com/alignas.php

如果数组似乎不在边界中double(可能跟随char在 2 字节读取处理器上运行的结构中)。

但这是不同的——我已经从堆/空闲存储中请求了内存,特别是请求 operator new 返回内存,它将保持元素对齐到sizeof(T).

总结一下,如果这是 TL;DR:

  • 是否可以malloc()在 C++ 中用于动态数组?
  • operator new()是否可以在没有alignas关键字的旧 C++ 中使用和放置新的动态数组?
  • 在返回的内存上使用指针算术是否未定义行为operator new()
  • Sutter 是否建议可能在某些古董机器上破坏的代码?

对不起,如果这是愚蠢的。

4

4 回答 4

10

分配内存上的指针算术问题,如您的示例所示:

T* storage = static_cast<T*>(operator new(sizeof(T)*size));
// ...
T* p = storage + i;  // precondition: 0 <= i < size
new (p) T(element);

技术上未定义的行为早已为人所知。这意味着std::vector不能以纯粹作为库的明确定义的行为来实现,但需要从实现中获得超出标准中的额外保证。

标准委员会的意图绝对不是使其无法std::vector实施。当然,Sutter 是正确的,这样的代码旨在被明确定义。标准的措辞需要反映这一点。

P0593是一个提案,如果被接受纳入标准,或许能够解决这个问题。同时,继续编写上述代码是可以的;没有主要的编译器会将其视为 UB。

编辑:正如评论中指出的那样,我应该说当我说 storage + i将在 P0593 下定义良好时,我假设元素storage[0], storage[1], ...storage[i-1]已经构建。尽管我不确定我对 P0593 的理解是否足够好,无法得出结论,它也不会涵盖尚未构建这些元素的情况

于 2018-11-23T19:28:37.603 回答
3

C++ 标准包含一个开放的问题,即对象的底层表示不是“数组”而是unsigned char对象的“序列”。尽管如此,每个人都将它视为一个数组(这是有意的),因此编写如下代码是安全的:

char* storage = static_cast<char*>(operator new(sizeof(T)*size));
// ...
char* p = storage + sizeof(T)*i;  // precondition: 0 <= i < size
new (p) T(element);

只要void* operator new(size_t)返回正确对齐的值。使用sizeof-multiplied 偏移量来保持对齐是安全的。

在 C++17 中,有一个宏STDCPP_DEFAULT_NEW_ALIGNMENT,它指定了 "normal" 的最大安全对齐方式,如果需要更大的对齐方式void* operator new(size_t)void* operator new(std::size_t size, std::align_val_t alignment)应该使用它。

在早期版本的 C++ 中,没有这种区别,这意味着void* operator new(size_t)需要以与任何对象的对齐方式兼容的方式来实现。

至于能够直接在 上进行指针运算T*,我不确定标准是否需要它。但是,很难以无法正常工作的方式实现 C++ 内存模型。

于 2018-11-23T20:52:04.013 回答
1

对于所有最近广泛使用的 posix 兼容系统,即 Windows、Linux (& Android ofc.) 和 MacOSX,以下适用

是否可以将 malloc() 用于 C++ 中的动态数组?

是的。使用reinterpret_cast将结果转换void*为所需的指针类型是最佳实践,它会在动态分配的数组中产生,如下所示:type *array = reinterpret_cast<type*>(malloc(sizeof(type)*array_size); 注意,在这种情况下,不会对数组元素调用构造函数,因此它仍然是未初始化的存储,没有不管是什么typefree用于释放时也不调用析构函数


是否可以在没有 alignas 关键字的旧 C++ 中对动态数组使用 operator new() 和放置 new?

是的,但是如果您使用自定义位置(即不是来自 malloc/new 的位置)提供新位置,您需要注意对齐。普通运算符 new 和 malloc 将提供本机字对齐的内存区域(至少在分配大小 >= 字大小时)。这一事实以及结构布局和大小已确定以便正确考虑对齐的事实,如果使用 malloc 或 new,您无需担心 dyn 数组的对齐。 人们可能会注意到,字长有时比最大的内置数据类型(通常是long double)要小得多,但它必须以相同的方式对齐,因为对齐不是关于数据大小,而是关于内存地址的位宽总线用于不同的访问大小。


在运算符 new() 返回的内存上使用指针算术是否未定义行为?

不,只要你尊重进程的内存边界——从这个角度来看,它的new工作方式基本上与malloc,此外,new实际上在绝大多数实现中调用 malloc 以获得所需的区域。事实上,指针算术本身永远不会无效。但是,计算结果为指针的算术表达式的结果可能指向允许区域之外的位置,但这不是指针算术的错误,而是有缺陷的表达式。


Sutter 是否建议可能在某些古董机器上破坏的代码?

我不这么认为,只要使用了正确的编译器。(不要将 avr 指令或 128 位宽的内存 mov 编译成旨在在 80386 上运行的二进制文件)当然,在具有不同内存大小和布局的不同机器上,相同的文字地址可能会访问不同目的/状态的区域/existence,但是除非您将驱动程序代码写入特定硬件,否则为什么要使用文字地址?... :)

于 2018-11-23T19:47:41.720 回答
0

您可以使用“老式”来做到这一点malloc,它为您提供了一块内存,可以满足相应平台上最严格的对齐方式(例如 a 的对齐方式long long double)。因此,您将能够将任何对象放入这样的缓冲区中,而不会违反任何对齐要求。

鉴于此,您可以基于这样的内存块为您的类型的数组使用placement new:

struct MyType {
    MyType() {
        cout << "in constructor of MyType" << endl;
    }
    ~MyType() {
        cout << "in destructor of MyType" << endl;
    }
    int x;
    int y;
};

int main() {

    char* buffer = (char*)malloc(sizeof(MyType)*3);
    MyType *mt = new (buffer)MyType[3];

    for (int i=0; i<3; i++)  {
        mt[i].~MyType();
    }
    free(mt);
}

请注意 - 与放置 new 一样 - 您必须注意显式调用析构函数并在不同的步骤中释放内存;你不能使用deleteordelete[]函数,它结合了这两个步骤,从而释放了他们不拥有的内存。

于 2018-11-23T19:32:11.310 回答