析构函数会自动释放成员变量的堆内存吗?

5

我有几个关于析构函数的疑问。

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();
}

同时,由于在删除时析构函数会自动调用,为什么我们需要显式删除,当所有逻辑都可以写在析构函数中时?
我对delete运算符和析构函数感到非常困惑,无法理解它们的具体用途。详细的描述将非常有帮助。
编辑:根据答案,我的理解是:对于这种情况,默认析构函数会损坏char指针,所以我们需要先显式删除char数组,否则会导致内存泄漏。如果我错了,请纠正我。

2
附注:不要混用 new[]freenew[] 必须与 delete[] 配对使用。 - DCoder
2
同时,你的构造函数应该调用 ch = new char[strlen(_ch)+1] 来考虑由 strlen 复制的空字符。 - quamrana
5
请,请不要编写自己的 operator delete - Mr Lister
2
你也违反了三法则 - Flexo
1
一个问题:这只是为了理解C++内存管理的练习,还是你正在尝试实现自己的字符串类? - Manu343726
显示剩余5条评论
7个回答

8

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


看到我的编辑了吗,我收到了堆损坏错误。那么我该如何防止泄漏呢? - Saksham
除非你重新编写destructor显式删除对象,否则请勿模仿。 - gpalex
2
@Saksham,请查看我的有关在构造函数中调用new的评论。 - quamrana
你不应该重载 delete 运算符。delete[]free 不是一样的。 - nullptr
@gpalex 谢谢,我会在我的回答中加入“默认”这个词。 - nullptr

6

delete不是一个函数(虽然你可以重载它);并且,把释放逻辑写在析构函数中是个好习惯。但是,假设析构函数会自动执行释放操作是不正确的。事实上,析构函数将在对象生命周期结束时被调用,但它执行的操作取决于你编写的代码。也就是说,你应该在析构函数内部调用 delete[] 释放 ch

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

此外,我认为堆栈损坏错误来自于您在初始化ch时没有为空字节\0留足够的空间。您还应该使用成员初始化列表。将构造函数更改为以下内容:
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 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
只要保持不变式,ch 的初始化时间并不重要。但是你关于 1+ 是正确的。 - quamrana
+1,但请同时提及“三法则(rule of three)”,并指出可以通过使用std::stringstd:vector<char>来避免释放问题。 - jxh
1
我认为分配点并不是堆破坏的根本原因。实际上,他只分配了 strlen(_ch) 而不是(像你做的那样)strlen(_ch) + 1,这才导致了堆破坏,而不是他没有使用初始化列表的原因。 - junix
@0x499602D2:你所说的“delete is not a function (though you can overload it)”,是什么意思?在C++中,函数可以进行重载。 - TrueY
@TrueY delete 是一个运算符,在 C++ 中你有能力重载运算符作为类的成员函数或友元函数(或者是全局函数)。 - David G

4
不,析构函数不会神奇地为您删除ch指向的内存。如果您调用了new(在构造函数中),则必须在适当的时间调用delete
当对象被销毁时,析构函数将执行。这可以是在自动对象(即在堆栈上分配的对象)即将超出范围时,或者在您显式使用new分配对象时。
通常,将new视为获取分配内存的一种方式,将构造函数视为获取该内存并将其转换为对象的一种方式,将析构函数视为获取对象并销毁它的一种方式,留下一块内存,并将delete视为获取该块内存并释放它的一种方式。
作为您的方便,当您调用new时,编译器将在分配所请求的内存后为您调用构造函数,并且当您调用delete时,编译器将自动调用析构函数。
您正在遇到堆破坏错误,因为您有缓冲区溢出:您没有为strcpy添加的空终止字节分配空间。
请记住,C字符串是由一个字节序列后跟一个空字节组成的。这意味着长度为5的字符串实际上需要6个字节来存储。
还要记住,您可以并且应该使用std::string而不是C样式数组,以节省麻烦并避免在非常强大和功能齐全的实现已经可供使用时编写容易出错的代码。
除了作业/学习练习之外,在几乎没有情况下,您应该直接实现C样式字符串,而不是使用std::string
同样(虽然稍微宽松一些),对于动态数组总体也是如此。请改用std::vector

2
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;
}

打印:

Prints:

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没有属性。
但是你忘了指针是一个基本类型,因此它没有析构函数。要销毁指针所指向的对象(即结束被指对象的生命周期),请在析构函数体中使用delete运算符。

2

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

你应该在析构函数中对ch进行delete[]。这必须明确地完成,因为删除操作符仅释放直接分配给类实例存储的内存。由于在构造函数中分配了更多的内存,因此必须在销毁时释放。

一般来说,可以假设构造函数和析构函数需要编写对称代码。对于在构造函数中使用new,必须在析构函数中使用delete。

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


1
析构函数不会自动释放任何东西。析构函数只会隐式调用类子对象的析构函数并执行您放入析构函数主体的任何代码。由于在您的情况下,子对象ch是原始指针类型,因此它没有析构函数。所以在您的情况下什么都不会发生。由于是您分配了内存,因此您需要负责释放它。简而言之,是的,在您的析构函数中需要使用delete[] ch
如果要自动释放该内存,请使用智能指针类而不是原始指针。在这种情况下,您的类的析构函数将自动调用智能指针子对象的析构函数,该析构函数将为您释放内存。在您的特定示例中,更好的想法是使用std::string来存储类对象中的字符串。
在您的情况下,堆栈破坏是由于为字符串分配的内存不足引起的,这导致strcpy中的越界写入。应该是:
ch = new char[strlen(_ch) + 1];
strcpy(ch,_ch);

额外的空间是为了终止零字符而需要的。

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++文献,并查看智能指针,例如unique pointer http://www.cplusplus.com/reference/memory/unique_ptr/,这将帮助您理解这个主题并使一切变得清晰明了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接