能不调用构造函数就调用析构函数吗?

Can you call the destructor without calling the constructor?

我一直在尝试在不需要时不初始化内存,并且正在使用 malloc 数组来这样做:

这是我的 运行:

#include <iostream>

struct test
{
    int num = 3;

    test() { std::cout << "Init\n"; }
    ~test() { std::cout << "Destroyed: " << num << "\n"; }
};

int main()
{
    test* array = (test*)malloc(3 * sizeof(test));

    for (int i = 0; i < 3; i += 1)
    {
        std::cout << array[i].num << "\n";
        array[i].num = i;
        //new(array + i) i; placement new is not being used
        std::cout << array[i].num << "\n";
    }

    for (int i = 0; i < 3; i += 1)
    {
        (array + i)->~test();
    }

    free(array);

    return 0;
}

输出:

0 ->- 0
0 ->- 1
0 ->- 2
Destroyed: 0
Destroyed: 1
Destroyed: 2

尽管没有构建数组索引。这是"healthy"吗?也就是说我可以简单的把析构函数当成"just a function"吗? (除了析构函数隐式知道数据成员相对于我指定的指针的位置)

只是说明一下:我不是在寻找有关正确使用 c++ 的警告。我只是想知道在使用这种无构造方法时是否应该注意一些事情。

(脚注:我不想使用构造函数的原因是因为很多时候,内存根本不需要初始化而且这样做很慢)

不,这是未定义的行为。对象的生命周期在对构造函数的调用完成后开始,因此,如果从未调用构造函数,则该对象在技术上永远不存在。

这可能 "seems" 在您的示例中表现正确,因为您的结构很简单(int::~int 是一个 no-op)。

你也在泄漏内存(析构函数破坏了给定的对象,但是通过malloc分配的原始内存仍然需要freed)。

编辑:您可能还想看看 ,因为这是一个非常相似的情况,只是使用堆栈分配而不是 malloc。这给出了标准中关于对象生命周期和构造的一些实际引用。

我还要补充一点:如果你不使用 placement new 并且它显然是必需的(例如 struct 包含一些容器 class 或 vtable 等)你要去到 运行 陷入真正的麻烦。在这种情况下,省略 placement-new 调用几乎肯定会给非常脆弱的代码带来 0 性能优势——无论哪种方式,这都不是一个好主意。

是的,析构函数只不过是一个函数。您可以随时调用它。但是,在没有匹配的构造函数的情况下调用它是一个坏主意。

所以规则是:如果你没有将内存初始化为特定类型,你可能不会将该内存解释为该类型的对象并使用;否则它是未定义的行为。charunsigned char 作为例外)。

让我们逐行分析您的代码。

test* array = (test*)malloc(3 * sizeof(test));

此行使用系统提供的内存地址初始化指针标量array。请注意,内存 未针对 任何类型 初始化。这意味着你 不应该 将这些内存视为任何对象(即使是像 int 这样的标量,抛开你的 test class 类型)。

后来,您写道:

std::cout << array[i].num << "\n";

这将内存用作 test 类型,这违反了上述规则,导致未定义的行为。

以后:

(array + i)->~test();

你又用了test类型的内存!调用析构函数也使用对象!这也是UB

在你的情况下,你很幸运,没有发生任何有害的事情,你得到了一些合理的东西。但是,UB 完全取决于您的编译器的实现。它甚至可以决定格式化您的磁盘,而那仍然是 standard-conforming.

That is to say, can I simply treat the destructor as "just a function"?

没有。虽然它在许多方面与其他函数相似,但析构函数有一些特殊功能。这些归结为类似于手动内存管理的模式。正如内存分配和释放需要成对进行一样,构造和销毁也是如此。如果你跳过一个,就跳过另一个。如果你叫一个,就叫另一个。如果您坚持手动内存管理,构造和销毁的工具是 placement new 并显式调用析构函数。 (使用 newdelete 的代码将分配和构造合并为一步,而销毁和释放合并为另一步。)

不要跳过将要使用的对象的构造函数。这是未定义的行为。此外,构造函数越不重要,如果您跳过它,出现严重错误的可能性就越大。也就是说,当你存得越多时,你就会损失得越多。跳过已用对象的构造函数并不是提高效率的方法——这是一种编写损坏代码的方法。低效、正确的代码胜过无效的高效代码。

有点沮丧: 这种 low-level 管理可能会成为大量时间投资。只有在有实际的性能回报机会时才走这条路。不要仅仅为了优化而通过优化使代码复杂化。还要考虑更简单的替代方案,这些替代方案可能会以更少的代码开销获得类似的结果。也许构造函数除了以某种方式将对象标记为未初始化外不执行任何初始化? (细节和可行性取决于所涉及的class,因此超出了这个问题的范围。)

一点鼓励:如果你考虑标准库,你应该意识到你的目标是可以实现的。我将 vector::reserve 作为可以在不初始化内存的情况下分配内存的示例。

您当前有 UB,因为您从 non-existing 对象访问字段。

您可以通过执行构造函数 noop 让字段未初始化。编译器可能会轻易地不进行初始化,例如:

struct test
{
    int num; // no = 3

    test() { std::cout << "Init\n"; } // num not initalized
    ~test() { std::cout << "Destroyed: " << num << "\n"; }
};

Demo

为了便于阅读,您应该将其包装在专用 class 中,例如:

struct uninitialized_tag {};

struct uninitializable_int
{
    uninitializable_int(uninitialized_tag) {} // No initalization
    uninitializable_int(int num) : num(num) {}

    int num;
};

Demo