删除类数组而不调用析构函数

Delete array of classes without calling destructors

本文关键字:调用 析构函数 数组 删除      更新时间:2023-10-16

考虑使用以下方式创建数组:

T* arr = new T[num];

现在,由于某些原因,我们理解了只需删除该数组,但不需要调用任何T析构函数。

我们都知道,如果我们写下一个:

delete arr;

则将调用CCD_ 2析构函数。如果我们这样写:

delete[] arr;

则将调用CCD_ 3析构函数。玩过指针之后,您会意识到new在结果指针之前插入unsigned long long值,该值表示已分配的T实例的数量。因此,我们试图智胜C++,它试图将该值更改为arr占用的字节数,并将其删除为(char*),希望在这种情况下,delete不会调用T实例的析构函数,而只是释放占用的内存。所以你写这样的东西:

typedef unsigned long long;
unsll & num = *(unsll)((char*)arr-sizeof(unsll));
num = num*sizeof(T);
delete ((char*)arr);

但这不起作用,C++在尝试删除它时会创建触发断点(运行时错误)。所以这是行不通的。很多其他的指针游戏都不起作用,因为至少会出现一些错误(编译或运行时)。所以问题是:

是否可以在不调用类的析构函数的情况下删除C++中的类数组

也许您想要::operator delete[](arr)

(请参见http://en.cppreference.com/w/cpp/memory/new/operator_delete)

但这仍然有着不明确的行为,是一个可怕的想法。

在不调用析构函数的情况下解除分配的一种简单方法是分离分配和初始化。当您正确处理对齐时,可以使用放置new(或标准分配器对象的功能)在分配的块内创建对象实例。最后,您可以使用适当的解除分配函数来解除分配块。

我想不出在任何情况下这样做都是明智的:它带有强烈的过早优化和X/Y问题的味道(通过想象不切实际的Y作为解决方案来处理问题X,然后只询问Y)。

new表达式被设计为将分配与初始化耦合起来,这样它们就可以作为要么全有要么全无的操作来执行。这种耦合,以及用于清理和释放的耦合,是正确性的关键,它也大大简化了事情(即,内部存在不必处理的复杂性)。脱钩需要有一个很好的理由。避免析构函数调用,例如出于优化目的,并不是一个好的理由。

我只想谈谈的具体问题

在C++中删除一个类数组而不调用它们的析构函数,这可能吗?

简短的回答是

长期的答案是肯定的,但也有一些注意事项,特别是考虑到(即资源清理)的析构函数是什么,避免调用类析构函数通常是个坏主意。

在我继续回答之前,应该注意的是,这是专门用来回答你的问题的,如果你使用C++(与纯C相比),使用这些代码是可行的(因为它是兼容的),但如果你需要以这种方式生成代码,你可能需要重新思考你的一些设计,因为如果使用不当,这样的代码可能会导致错误/错误和一般未定义的行为。

TL;DR 如果您需要避免析构函数,则需要重新思考您的设计(即使用复制/移动语义或STL容器)。

您可以使用mallocfree来避免构造函数和析构函数调用,示例代码:

#include <iostream>
#include <cstdio>
class MyClass {
    public:
        MyClass() : m_val(0)
        {
            this->init(42);
            std::cout << "ctor" << std::endl;
        }
        ~MyClass()
        {
            std::cout << "dtor" << std::endl;
        }
        friend std::ostream& operator<<(std::ostream& stream, const MyClass& val)
        {
            stream << val.m_val;
            return stream;
        }
        void init(int val)
        {
            /* just showing that the this pointer is valid and can
            reference private members regardless of new or malloc */
            this->_init(val);
        }
    private:
        int m_val;
        void _init(int val)
        {
            this->m_val = val;
        }   
};
template < typename Iterator >
void print(Iterator begin, Iterator end)
{
    while (begin != end) {
        std::cout << *begin << std::endl;
        ++begin;
    }
}
void set(MyClass* arr, std::size_t count)
{
    for (; count > 0; --count) {
        arr[count-1].init(count);
    }
}
int main(int argc, char* argv[])
{
    std::cout << "Calling new[10], 10 ctors called" << std::endl;
    MyClass* arr = new MyClass[10]; // 10 ctors called;
    std::cout << "0: " << *arr << std::endl;
    set(arr, 10);
    print(arr, arr+10);
    std::cout << "0: " << *arr << std::endl;
    std::cout << "Calling delete[], 10 dtors called" << std::endl;
    delete[] arr; // 10 dtors called;
    std::cout << "Calling malloc(sizeof*10), 0 ctors called" << std::endl;
    arr = static_cast<MyClass*>(std::malloc(sizeof(MyClass)*10)); // no ctors
    std::cout << "0: " << *arr << std::endl;
    set(arr, 10);
    print(arr, arr+10);
    std::cout << "0: " << *arr << std::endl;
    std::cout << "Calling free(), 0 dtors called" << std::endl;
    free(arr); // no dtors
    return 0;
}

需要注意的是,将newfree和/或T0与delete混合会导致未定义的行为,因此调用MyClass* arr = new MyClass[10];然后调用free(arr);可能无法按"预期"工作(因此为UB)。

在C++中不调用构造函数/析构函数会引起的另一个问题是继承。上面的代码将与基本类的mallocfree一起使用,但如果你开始引入更复杂的类型,或者从其他类继承,继承类的构造函数/析构函数将不会被调用,事情会很快变得很糟糕,例如:

#include <iostream>
#include <cstdio>
class Base {
    public:
        Base() : m_val(42)
        {
            std::cout << "Base()" << std::endl;
        }
        virtual ~Base()
        {
            std::cout << "~Base" << std::endl;
        }
        friend std::ostream& operator<<(std::ostream& stream, const Base& val)
        {
            stream << val.m_val;
            return stream;
        }
    protected:
        Base(int val) : m_val(val)
        {
            std::cout << "Base(" << val << ")" << std::endl;
        }
        void _init(int val)
        {
            this->m_val = val;
        }
        int m_val;
};
class Child : public virtual Base {
    public:
        Child() : Base(42)
        {
            std::cout << "Child()" << std::endl;
        }
        ~Child()
        {
            std::cout << "~Child" << std::endl;
        }
        void init(int val)
        {
            this->_init(val);
        }
};
template < typename Iterator >
void print(Iterator begin, Iterator end)
{
    while (begin != end) {
        std::cout << *begin << std::endl;
        ++begin;
    }
}
void set(Child* arr, std::size_t count)
{
    for (; count > 0; --count) {
        arr[count-1].init(count);
    }
}
int main(int argc, char* argv[])
{
    std::cout << "Calling new[10], 20 ctors called" << std::endl;
    Child* arr = new Child[10]; // 20 ctors called;
    // will print the first element because of Base::operator<<
    std::cout << "0: " << *arr << std::endl;
    set(arr, 10);
    print(arr, arr+10);
    std::cout << "0: " << *arr << std::endl;
    std::cout << "Calling delete[], 20 dtors called" << std::endl;
    delete[] arr; // 20 dtors called;
    std::cout << "Calling malloc(sizeof*10), 0 ctors called" << std::endl;    
    arr = static_cast<Child*>(std::malloc(sizeof(Child)*10)); // no ctors
    std::cout << "The next line will seg-fault" << std::endl;
    // Segfault because the base pointers were never initialized
    std::cout << "0: " << *arr << std::endl; // segfault
    set(arr, 10);
    print(arr, arr+10);
    std::cout << "0: " << *arr << std::endl;
    std::cout << "Calling free(), 0 dtors called" << std::endl;
    free(arr); // no dtors
    return 0;
}

上面的代码是兼容的,在g++和Visual Studio上编译时没有出错,但由于继承的原因,当我试图打印malloc之后的第一个元素时,两者都会崩溃(因为基类从未初始化)。

因此,您确实可以在不调用构造函数和析构函数的情况下创建和删除对象数组,但这样做会导致大量额外的场景,您需要意识到并考虑这些场景,以避免未定义的行为或崩溃。如果您的代码是这种情况,则需要确保析构函数不被调用,您可能需要重新考虑您的整体设计(甚至可能使用STL容器或智能指针类型)。

希望这能有所帮助。