您可以不调用构造函数就调用析构函数吗?

时间:2019-07-04 01:25:59

标签: c++ constructor initialization malloc destructor

我一直试图在不需要时不初始化内存,并且正在使用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

尽管尚未构造数组索引。这是“健康”的吗?也就是说,我可以简单地将析构函数视为“只是一个函数”吗? (除了析构函数对数据成员相对于我指定的指针位于何处的隐含了解)

仅需说明:我不是在寻找有关正确使用c ++的警告。我只想知道在使用这种无构造方法时是否应该警惕。

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

4 个答案:

答案 0 :(得分:6)

否,这是不确定的行为。对象的生存期始于对构造函数的调用完成,因此,如果从不调用构造函数,则从技术上讲,该对象将永远不存在。

在您的示例中,这可能“似乎”正确运行,因为您的结构很琐碎(int ::〜int是无操作的)。

您还在泄漏内存(析构函数销毁给定的对象,但是通过malloc分配的原始内存仍然需要free d)。

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

我也会添加它:如果您不使用new放置并且显然是必需的(例如struct包含一些容器类或vtable等),您将遇到真正的麻烦。在这种情况下,几乎可以肯定的是,对于非常脆弱的代码,省略新的放置调用将为您带来0的性能收益-无论哪种方式,这都不是一个好主意。

答案 1 :(得分:2)

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

因此规则是:如果您没有将内存初始化为特定类型,则不能将该内存解释并使用为该类型的对象; (以charunsigned char为例外)。

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

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

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

后来,您写道:

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

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

后来:

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

您再次使用了test类型的内存!调用析构函数也使用该对象!这也是UB。

对于您而言,您很幸运,没有有害的事情发生,并且您得到了一些合理的信息。但是,UB仅取决于编译器的实现。它甚至可以决定格式化磁盘,但仍然符合标准。

答案 2 :(得分:1)

  

也就是说,我可以简单地将析构函数视为“只是一个函数”吗?

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

请勿跳过将要使用的对象的构造函数。这是未定义的行为。此外,构造函数的琐碎程度越小,如果您跳过某些错误,则出错的可能性就越大。就是说,随着您存更多钱,您会赚更多钱。跳过使用过的对象的构造函数并不是提高效率的一种方法,而是一种编写残破代码的方法。低效,正确的代码胜过无效的高效代码。

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

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

答案 3 :(得分:0)

您当前从不存在的对象访问字段时拥有UB。

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

struct test
{
    int num; // no = 3

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

Demo

出于可读性考虑,您可能应该将其包装在专用类中,例如:

struct uninitialized_tag {};

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

    int num;
};

Demo