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

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

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

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

4

7 回答 7

8

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

于 2013-07-14T18:33:20.507 回答
6

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

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

此外,我相信堆损坏错误是由于您在初始化时没有ch为 null byte留出足够的空间\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
于 2013-07-14T18:43:29.680 回答
3

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

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

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

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

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

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

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

除了家庭作业/学习练习之外,几乎没有什么情况应该直接实现 C 风格的字符串而不是使用std::string.

一般来说,动态数组也是如此(尽管不那么严格)。改为使用std::vector

于 2013-07-14T18:40:58.810 回答
2

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

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

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

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

于 2013-07-14T18:43:10.850 回答
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结束时调用的(也就是结束时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 没有属性。

但是您忘记了指针是基本类型,因此它没有析构函数。要销毁指针指向的对象(即,最终确定指针对象的生命),请delete在析构函数体中使用 use 运算符。

于 2013-07-14T18:54:11.653 回答
1

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

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

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

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

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

于 2013-07-14T18:45:43.040 回答
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/ 这将帮助你了解主题,一切都会到位。

于 2013-07-14T19:02:40.290 回答