C++ 析构函数
析构函数是一个成员函数,在对象超出范围或通过调用 delete
显示销毁对象时,会自动调用析构函数。
一个类有且仅有一个析构函数。如果定义类的时候没写析构函数,则编译器生成默认析构函数。如果定义了析构函数,则编译器不生成默认析构函数。
析构函数在对象消亡时即自动被调用。可以定义析构函数在对象消亡前做善后工作。例如,对象如果在生存期间用 new 运算符动态分配了内存,则在各处写 delete 语句以确保程序的每条执行路径都能释放这片内存是比较麻烦的事情。有了析构函数,只要在析构函数中调用 delete 语句,就能确保对象运行中用 new 运算符分配的空间在对象消亡时被释放。
《C++ Primer Plus(第六版)中文版》中对调用析构函数的时机的描述如下:
通常由编译器决定什么时候调用析构函数,通常不应在代码中显式调用析构函数(使用定位 new 运算符
时例外)。
-
如果创建的是静态存储类对象,则析构函数将在程序结束时自动被调用。
-
如果创建的是自动存储类对象,则析构函数将在程序执行完代码块时(该对象是在其中定义的)自动被调用。
-
如果对象是通过 new 创建的,则它将驻留在栈内存或自由存储区中,当使用 delete 来释放内存时,其析构函数将自动被调用。
主动调用析构函数
在 C++ 中,函数内声明的变量存储在栈上。当函数被调用时,会在栈上为函数分配一块内存区域,用于存储函数的参数、局部变量和返回地址等信息。当函数执行完毕后,这块内存区域就会被释放。
-
对象存储在栈上
手动调用析构函数并不会导致该对象占用的内存空间被释放。在手动调用析构函数后还能正常访问到该对象的成员。且在该方法执行结束前还会自动调用一次析构函数。
-
对象存储在堆上
手动调用析构函数也不会导致该对象占用的内存空间被释放。在手动调用析构函数后还能够正常访问到该对象的成员。对指向该对象的指针调用 delete 会自动执行析构函数,并且释放该对象占用的内存空间,此后使用原来的指针访问对象的成员函数可能会报错。例如:
#include <iostream> #include <string> #include <memory> class Person { public: Person(const std::string &name) : name_(name) {} void printName() const { std::cout << "My name is " << name_ << std::endl; } ~Person() { std::cout << "destroy object" << std::endl; } public: std::string name_; }; void fun(Person person) { std::cout << "address in fun : " << &person << std::endl; } int main(int argc, char **argv) { Person *person = new Person("Alice"); person->~Person(); std::cout << "name_ in person: " << person->name_ << std::endl; delete person; std::cout << "name_ in person: " << person->name_ << std::endl; return 0; }
执行结果分为以下两种:
-
直接报错
destroy object name_ in person: Alice destroy object Segmentation fault
进入 gdb 进行调试的结果如下:
Breakpoint 1, main (argc=1, argv=0x7ffc39546d48) at test.cpp:39 39 delete person; (gdb) p *person $1 = {name_ = "Alice"} (gdb) p person $2 = (Person *) 0x5575afe77eb0 (gdb) n destroy object 40 std::cout << "name_ in person: " << person->name_ << std::endl; (gdb) p person $3 = (Person *) 0x5575afe77eb0 (gdb) p *person $4 = {name_ = <error: Cannot access memory at address 0x5575afe77>} (gdb)
可见调用 delete 后指针仍指向原来的内存空间,尝试访问指针 person 指向的内存空间会收到报错信息,因为当前程序无法访问到 person 指向的内存。
-
还能执行,但是输出的内容和原来对象中保存的内容已经不同了
执行结果如下:
destroy object name_ in person: Alice destroy object name_ in person: // 后面有无数行空白的内容,偶尔夹杂几个奇怪的字符
进入 gdb 的调试结果如下:
Breakpoint 1, main (argc=1, argv=0x7ffe9fd139d8) at test.cpp:39 39 delete person; (gdb) p person $1 = (Person *) 0x55d080da4eb0 (gdb) p *person $2 = {name_ = "Alice"} (gdb) x /20xh person 0x55d080da4eb0: 0x4ec0 0x80da 0x55d0 0x0000 0x0005 0x0000 0x0000 0x0000 0x55d080da4ec0: 0x6c41 0x6369 0x0065 0x0000 0x0000 0x0000 0x0000 0x0000 0x55d080da4ed0: 0x0000 0x0000 0x0000 0x0000 (gdb) n destroy object 40 std::cout << "name_ in person: " << person->name_ << std::endl; (gdb) p person $3 = (Person *) 0x55d080da4eb0 (gdb) p *person $4 = {name_ = ""} (gdb) x /20xh person 0x55d080da4eb0: 0x0da4 0x5d08 0x0005 0x0000 0x1297 0xef39 0x89f8 0x608b 0x55d080da4ec0: 0x6c41 0x6369 0x0065 0x0000 0x0000 0x0000 0x0000 0x0000 0x55d080da4ed0: 0x0000 0x0000 0x0000 0x0000 (gdb)
从 gdb 调试的结果可以发现在调用 delete 后 person 指向的内存空间里的内容已经发生改变了。
在这种情况下虽然程序还有权访问 person 指向的内存空间,但是这块内存的内容早已经不是原来的对象中的内容了。
使用 delete 释放内存后指针的值不会改变,如果再使用该指针会导致未知的结果,所以使用 delete 后应将指针在 C++ 中,delete 操作只会释放指针所指向的内存,而不会删除指针本身。因此,如果在 delete 操作之后没有将指针赋值为空指针,那么这个指针仍然指向原来的内存区域,这样就会出现访问已经释放的内存区域的情况。因此,为了避免这种情况的发生,我们需要在 delete 操作之后将指针赋值为空指针。
参考:
(1) C++指针delete后还要置为null - CSDN博客. https://blog.csdn.net/qq_36570733/article/details/80043321.
(2) C++ 里 delete 指针两次会怎么样? - 知乎. https://www.zhihu.com/question/38998078.
-
本文作者:asagi
本文链接:https://www.cnblogs.com/asagi/p/17427442.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步