C++中未初始化的内存

Uninitialized memory in C++

operator new 在 C++ 中分配和初始化内存(通过调用默认构造函数)。如果我希望内存未初始化怎么办?在那种情况下我该如何分配内存?

在 C 中我可以使用 malloc 例如它只会分配内存,而不是初始化它。

人们使用两种主要技术来延迟 object 的创建。我将展示它们如何申请单个 object,但您可以将这些技术扩展到静态或动态数组。

对齐存储

第一种方法是使用 std::aligned_storage,这是一个考虑了对齐的美化 char 数组:

template<typename T>
class Uninitialized1 {
    std::aligned_storage_t<sizeof(T)> _data;

public:
    template<typename... Args>
    void construct(Args... args) {
        new (&_data) T(args...);
        std::cout << "Data: " << *reinterpret_cast<T*>(&_data) << "\n";
    }
};

请注意,为了保留重点,我省略了完美转发等内容。

这样做的一个缺点是无法让 constexpr 构造函数将值复制到 class。这使得它不适合实现 std::optional.

工会

另一种方法使用普通的旧联合。对于对齐存储,您必须小心,但对于联合,您必须加倍小心。不要假设我的代码是 bug-free 原样。

template<typename T>
class Uninitialized2 {
    union U {
        char dummy;
        T data;

        U() {}
        U(T t) : data(t) {
            std::cout << "Constructor data: " << data << "\n";
        }
    } u;

public:
    Uninitialized2() = default;
    Uninitialized2(T t) : u(t) {}

    template<typename... Args>
    void construct(Args... args) {
        new (&u.data) T(args...);
        std::cout << "Data: " << u.data << "\n";
    }
};

一个联合存储了一个strongly-typed object,但是我们在它之前放了一个简单构造的虚拟对象。这意味着联合(以及整个 class)的默认构造函数可以变得微不足道。然而,我们也可以选择直接初始化第二个联合成员,即使是以 constexpr 兼容的方式。


我遗漏的一件非常重要的事情是您需要手动销毁这些 object。您将需要手动调用析构函数,这应该会打扰您,但这是必要的,因为编译器不能保证首先构造 object 。请帮自己一个忙,研究这些技术,以便学习如何正确使用它们,因为有一些非常微妙的细节,确保每个 object 都被正确销毁等事情可能会变得棘手。


我(勉强)tested these code snippets有小class和driver:

struct C {
    int _i;

public:
    explicit C(int i) : _i(i) {
        std::cout << "Constructing C with " << i << "\n";
    }

    operator int() const { return _i; }
};

int main() {
    Uninitialized1<C> u1;
    std::cout << "Made u1\n";
    u1.construct(5);

    std::cout << "\n";

    Uninitialized2<C> u2;
    std::cout << "Made u2\n";
    u2.construct(6);

    std::cout << "\n";

    Uninitialized2<C> u3(C(7));
    std::cout << "Made u3\n";
}

Clang 的输出如下:

Made u1
Constructing C with 5
Data: 5

Made u2
Constructing C with 6
Data: 6

Constructing C with 7
Constructor data: 7
Made u3

将分配与构造分开是可能的,但有点棘手。 (Bjarne Stroustrup 和我详细讨论了这个问题,大约在 1985 年。)您需要做的是使用 ::operator new 来获取原始内存。稍后,如果对象类型需要,您可以使用 placement-new 或其他任何方法进行初始化。这就是 STL 容器的默认分配器如何将分配和构造分开。

这为 U 类型的对象获取原始内存:

U *ptr = (U*) ::operator new (sizeof(U));
  // allocates memory by calling: operator new (sizeof(U))
  // but does not call U's constructor

说到 STL...您可以为 ::std:: 容器指定您自己的分配器。例如,如果您使用 std::vector<float> 分配浮点数数组,它会毫无理由地将它们初始化为零。 (vector<float> 有专门化。)你可以自己动手:std::vector<float, my_own_allocator>。

下面的自定义分配器 link 继承了默认分配器的功能来完成几乎所有的事情——包括原始内存的分配。它覆盖了 construct() 的默认行为 - 以便什么也不做 - 当实际的构造函数是微不足道的并且不能抛出异常时。

-->

看看它是如何使用 placement new 的。

::new(static_cast<void*>(ptr)) U;
 // Calls class U constructor on ptr.

您的分配器甚至可以这样写:在为调试进行编译时,它用非法数字 (NaN) 填充数组,并在为释放进行编译时使内存保持未初始化状态。我见过的一些最严重的错误是在默认零起作用时出现的——直到它们不起作用。不信任默认值。

还有一个大写字母……避免提前优化。您因不对对象进行两次初始化而节省的计算机周期是否真的值得付出努力?

new operator in C++ both allocates and initializes memory(by calling default constructor).

恕我直言 - 您误解了新操作员的作用。

  • new 运算符分配了一块足够大的内存来容纳对象。

  • 'new' 初始化内存。

  • 当可用时,ctor 不需要初始化内存,由您控制。


What if I do not want the memory to be un-initialized? [SIC] How do I allocate memory in that case?

  • 编译器提供的默认构造函数什么都不做。在class Foo中,你可以使用"Foo() = default;"命令编译器提供一个默认的ctor。

  • 您可以命令编译器禁止默认构造函数。对于class Foo,"Foo() = delete;"除非你提供一个,否则不会有默认的ctor。

  • 您可以定义自己的默认构造函数,它不会对内存造成任何影响。对于您的 class,构造函数可能没有初始化列表,并且主体为空。


注意:有许多嵌入式系统要求 ctor 要么被禁止,要么被实现为不执行任何操作(这会导致内存设备状态发生变化)。研究术语"warm-start"(软件重置而不影响设备中的数据流。)vs "cold-start"(软件和设备重启)vs "power-bounce".

在一些嵌入式系统中,内存映射i/o设备没有映射到动态内存中,OS不管理它。在这些情况下,程序员通常为这些对象提供 no-op ctor 和 no dtor。