析构函数是否会自动为成员变量释放堆内存?

时间:2013-07-14 18:30:16

标签: c++

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

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数组,否则会导致内存泄漏。如果我错了,请纠正我。

7 个答案:

答案 0 :(得分:9)

好吧,默认析构函数释放成员变量使用的内存(即成员指针ch本身不再存在),但自动释放成员指针引用的任何内存。因此,您的示例中存在内存泄漏。

答案 1 :(得分:6)

delete不是一个函数(虽然你可以重载它);是的,好的做法是在析构函数中写入解除分配的逻辑。但是假设解析器将自动执行释放是不正确的。问题是析构函数将在对象生命周期的末尾被调用,但是它取决于你为它编写的代码。也就是说,您应该在析构函数内的delete[]上调用ch

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

此外,我认为堆损坏错误来自于您没有为ch初始化空字节\0留下足够的空间。您还应该使用member-initializer列表。将构造函数更改为:

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

可以对您的代码进行许多改进。即,使用std::string并跟随Rule Of Three。您的代码也不需要operator delete()重载。 cs应该是堆栈分配:

#include <iostream>
#include <string>

class cls
{
    std::string ch;
    public:
        cls() { std::cout << "default constructor called\n"; }

        cls(std::string _ch) : ch(_ch)
        {
            std::cout << "constructor called\n";
        }

        cls(cls const& other) : ch(other.ch)
        {
            std::cout << "copy-constructor called\n";
        }

        ~cls() { std::cout << "destructor called\n"; }
};

int main()
{
    cls cs("hello!");

    std::cin.get();

} // <-- destructor gets called automatically for cs

答案 2 :(得分:3)

不,析构函数不会为你delete神奇地ch指出new。如果你打电话给delete(你在构造函数中做过),那么你也必须在适当的时候打电话给delete

析构函数在对象被销毁时执行。这可能是当一个自动对象(在堆栈上分配的东西)即将超出范围时,或者当您明确new一个用new分配的对象时。

通常,将delete视为获取内存的一种方式,将构造函数视为获取内存并将其作为对象的方式,将析构函数视为获取对象并将其销毁,留下一大块内存和new占用大块内存并释放它。

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

您遇到堆损坏错误,因为您有一个缓冲区溢出:您没有为std::string追加的空终止字节分配空间。

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

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

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

一般来说,动态数组也是如此(尽管不那么严格)。请改用{{1}}。

答案 3 :(得分:2)

覆盖特定类的删除操作符没有用处。这就是全局删除操作符的用途。

你应该做的是析构函数中ch的delete []。这必须明确地完成,因为delete运算符只释放直接分配用于存储类实例的内存。当您在构造函数中分配更多内存时,必须在销毁时释放它。

根据经验,您可以假设con和析构函数需要对称编码。对于构造函数中的每个new,必须在de destructor中删除。

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

答案 4 :(得分:2)

C ++ memory magnament基于RAII。这意味着在变量的生命周期结束时调用析构函数。

例如:

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

int main()
{
   Foo my_foo_instance;
}

打印:

  

构造!!!
  析构函数!!!

因为构造函数是在my_foo_instance的初始化中调用的(在声明处),并且当my_foo_instance的生命周期结束时调用析构函数(即{{1}的末尾) })。

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

main()

打印:

  

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

该计划的痕迹是:

  • 主要开始
  • 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; } 的初始化:调用Foo2构造函数
  • 首先,Foo2初始化其属性:调用Foo1构造函数
  • Foo1没有属性,因此Foo1执行其构造函数体:my_foo2_instance
  • 在属性初始化之后,Foo2执行其构造函数体:cout << "Foo1 constructor" << endl;
  • 主要范围结束,cout << "Foo2 constructor" << endl;生命周期结束:调用Foo2析构函数
  • Foo2析构函数执行其正文:my_foo2_instance
  • 在析构函数之后,Foo2属性的生命周期结束。所以:调用Foo1析构函数
  • Foo1析构函数执行其正文:cout << "Foo2 destructor" << endl;
  • 在析构函数之后,Foo1属性的生命周期结束。但是Foo1没有属性。

但是你忘了的是指针是一个基本类型,所以它没有析构函数。要销毁指针所指向的对象(那就是,终结指向对象的生命),在析构函数体中使用use cout << "Foo1 destructor" << endl;运算符。

答案 5 :(得分:1)

析构函数永远不会自行释放任何内容。析构函数只会隐式调用类子对象的析构函数,并执行放入析构函数体内的任何代码。因为在您的情况下,子对象ch是原始指针类型,它没有析构函数。所以在你的情况下什么都不会做。因为是你分配了内存,所以你负责解除内存。简而言之,是的,你需要在析构函数中使用delete[] ch

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

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

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

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

答案 6 :(得分:0)

我的看法:

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/ 这将有助于您理解该主题,一切都将落实到位。