析构函数是否自动为成员变量解除分配堆内存

Does a destructor automatically deallocates heap memory for member variables?

本文关键字:变量 解除分配 内存 成员 是否 析构函数      更新时间:2023-10-16

我对析构函数有一些疑问。

class cls
{
    char *ch;
public:
    cls(const char* _ch)
    {
        cout<<"nconstructor called";
        ch = new char[strlen(_ch)];
        strcpy(ch,_ch);
    }
    ~cls()
    {
        //will this destructor automatically delete char array ch on heap?
            //delete[] ch; including this is throwing heap corruption error
    }
    void operator delete(void* ptr)
    {
        cout<<"noperator delete called";
        free(ptr);
    }
};
int main()
{
    cls* cs = new cls("hello!");
    delete(cs);
    getchar();
}

此外,由于析构函数是在删除时自动调用的,为什么当所有逻辑都可以在析构函数中编写时,我们需要显式删除?

我对运算符删除和析构函数感到非常困惑,无法弄清楚它们的具体用法。详细的描述将非常有帮助。

编辑:我基于答案的理解:对于这种特殊情况,默认析构函数会损坏 char 指针,因此我们需要先显式删除 char 数组,否则会导致内存泄漏。如果我错了,请纠正我。

好吧,默认析构函数会释放成员变量使用的内存(即成员指针本身ch不复存在(,但它不会自动释放成员指针引用的任何内存。所以你的例子中存在内存泄漏。

delete

是一个函数(尽管你可以重载它(;是的,在析构函数中编写释放的逻辑是一种很好的做法。但是,假设析构函数将自动执行释放是不正确的。问题是析构函数将在对象的生存期结束时被调用,但它的作用取决于您为其编写的代码。也就是说,您应该在析构函数中调用delete[] ch

~cls()
{
    delete[] ch;
    ch = nullptr;
}

此外,我相信堆损坏错误来自这样一个事实,即您在初始化ch时没有为空字节留出足够的空间。还应使用成员初始值设定项列表。将构造函数更改为以下内容:

cls(const char* _ch) : ch(new char[1+strlen(_ch)])
{
    std::cout << "nconstructor called";
    std::strcpy(ch, _ch);
}

可以对代码进行许多改进。即使用std::string并遵循三法则。您的代码也不需要operator delete()重载。 cs应进行堆栈分配:

#include <iostream>
#include <string>
class cls
{
    std::string ch;
    public:
        cls() { std::cout << "default constructor calledn"; }
        cls(std::string _ch) : ch(_ch)
        {
            std::cout << "constructor calledn";
        }
        cls(cls const& other) : ch(other.ch)
        {
            std::cout << "copy-constructor calledn";
        }
        ~cls() { std::cout << "destructor calledn"; }
};
int main()
{
    cls cs("hello!");
    std::cin.get();
} // <-- destructor gets called automatically for cs

不,析构函数不会神奇地delete ch指向的内存。如果您调用了new(您在构造函数中调用了(,那么您还必须在适当的时间调用delete

析构函数在销毁对象时执行。这可能是当自动对象(即堆栈上分配的内容(即将超出范围时,或者当您显式delete分配了 new 的对象时。

通常,将new视为分配内存的一种方式,将构造函数视为获取该内存并将其转换为对象的方式,将析构函数视为获取对象并销毁它,留下一大块内存,delete视为获取该内存块并解除分配它。

为了方便起见,当您调用new编译器将在分配您请求的内存后为您调用构造函数,当您调用delete编译器将自动为您调用析构函数时。

收到堆损坏错误,因为您有一个缓冲区 oveflow:您没有为 strcpy 附加的空终止字节分配空间。

请记住,C 字符串是一个字节序列,后跟一个空字节。这意味着长度为 5 的字符串实际上需要 6 个字节来存储。

还要记住,你可以而且应该使用std::string而不是 C 样式的数组来省去麻烦,并避免在已经有一个非常健壮且功能齐全的实现可供您使用时编写容易出错的代码。

除了家庭作业/学习练习之外,几乎没有一种情况是您应该直接实现 C 样式字符串而不是使用 std::string

通常,动态数组也是如此(尽管不那么严格(。请改用std::vector

覆盖特定类的删除运算符没有用。这就是全局删除运算符的用途。

你应该做的是在析构函数中的 ch 上删除 []。这必须显式完成,因为删除运算符仅解除分配直接分配给存储类实例的内存。当您在构造函数中分配更多内存时,您必须在销毁时释放它。

根据经验,您可以假设析构函数和析构函数需要对称编码。对于构造函数中的每个新内容,都必须是 de 析构函数中的删除。

哦,顺便说一句:您不能将C++分配器(新建/删除(与 C 分配器(malloc/free(混合使用。您在C++中分配的内容,您必须在C++中释放,反之亦然。

C++内存大块是基于RAII的。这意味着当变量的生存期结束时,将调用析构函数。

例如:

class Foo
{
public:
   Foo() { cout << "Constructor!!!" << endl; }
   ~ Foo() { cout << "Destructor!!!" << endl; }
};
int main()
{
   Foo my_foo_instance;
}

指纹:

构造 函数!!!
破坏者!!!

因为构造函数是在my_foo_instance的初始化中调用的(在声明中(,而析构函数是在my_foo_instance的生存期结束时(即在main()结束时(调用的。

此外,此规则适用于任何上下文,包括类属性:

class Foo1
{
public:
   Foo1() { cout << "Foo1 constructor!!!" << endl; }
   ~ Foo1() { cout << "Foo1 destructor!!!" << endl; }
};
class Foo2
{
private:
    Foo1 foo1_attribute;
public:
   Foo2() { cout << "Foo2 constructor!!!" << endl; }
   ~ Foo2() { cout << "Foo2 destructor!!!" << endl; }
};
int main()
{
   Foo2 my_foo2_instance;
}

指纹:

Foo1 构造函数!!
Foo2 构造函数!!
Foo2 析构函数!!
Foo1 析构函数!!

该程序的跟踪是:

  • 主要起点
  • my_foo2_instance的初始化:调用 Foo2 构造函数
  • 首先,Foo2 初始化其属性:调用 Foo1 构造函数
  • Foo1
  • 没有属性,因此 Foo1 执行其构造函数体: cout << "Foo1 constructor" << endl;
  • 属性初始化后,Foo2 执行其构造函数体: cout << "Foo2 constructor" << endl;
  • 主作用域结束,因此my_foo2_instance生存期结束:调用 Foo2 析构函数
  • Foo2 析构函数执行其主体:cout << "Foo2 destructor" << endl;
  • 析构函数之后,Foo2 属性的生存期结束。所以:调用 Foo1 析构函数
  • Foo1 析构函数执行其主体:cout << "Foo1 destructor" << endl;
  • 在析构函数之后,Foo1 属性的生存期结束。但是 Foo1 没有属性。

但是你忘记了指针它是一个基本类型,所以它没有析构函数。若要销毁指针指向的对象(即,微调 pointee 对象的生存期(,请在析构函数主体中使用 delete 运算符。

析构函数从不自行解除分配任何内容。析构函数只会隐式调用类子对象的析构函数,并执行您放入析构函数主体的任何代码。由于在您的情况下,子对象ch是原始指针类型,因此它没有析构函数。因此,在您的情况下不会执行任何操作。由于分配内存的是你,所以是你负责解除分配它。简而言之,是的,您的析构函数中确实需要该delete[] ch

如果希望自动释放该内存,请使用智能指针类而不是原始指针。在这种情况下,类的析构函数将自动调用智能指针子对象的析构函数,这将为您释放内存。在您的特定示例中,更好的主意是使用 std::string 将字符串存储在类对象中。

在您的情况下,堆腐蚀是由于您为字符串分配的内存不足,这会导致越界写入strcpy。它应该是

ch = new char[strlen(_ch) + 1];
strcpy(ch,_ch);

终止零字符需要额外的空格。

我的看法:

1(简短的回答是否定的。

2(至于"为什么不",请考虑以下示例:

cls create()
{
   cls Foo("hello"); // This allocates storage for "ch"
   return Foo; 
} // Return variable is by value, so Foo is shollow-copied (pointer "ch" is copied).
  // Foo goes out of scope at end of function, so it is destroyed. 
  // Do you want member variable "ch" of Foo to be deallocated? Certainly not! 
  // Because this would affect your returned instance as well!

建议:

如果你想看看你的代码是否泄漏存储,你可以使用一个优秀的工具valgrind,http://valgrind.org/

至于阅读什么才能更好地理解这个主题,我会推荐标准的C++文献,并查看智能指针,例如唯一指针http://www.cplusplus.com/reference/memory/unique_ptr/这将帮助您理解该主题,一切都将落实到位。