MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++类析构函数自动调用的触发条件

2024-09-251.4k 阅读

C++类析构函数自动调用的触发条件

析构函数的基本概念

在C++中,析构函数是类的一种特殊成员函数,它与构造函数相对应。构造函数用于对象的初始化,而析构函数则用于在对象销毁时执行清理工作。析构函数的名称与类名相同,但前面加上波浪号(~)。例如,对于名为MyClass的类,其析构函数为~MyClass()

析构函数没有参数,也没有返回值(包括void)。一个类通常只有一个析构函数,如果没有显式定义析构函数,编译器会自动生成一个默认的析构函数。默认析构函数通常用于执行一些简单的清理工作,比如释放对象的成员变量所占用的内存(对于非动态分配的内存)。但如果类中包含动态分配的资源(如通过new运算符分配的内存、打开的文件句柄、网络连接等),则需要程序员显式定义析构函数来正确释放这些资源,以避免内存泄漏和资源泄露问题。

函数自动调用的触发条件

  1. 对象生命周期结束
    • 局部对象:当一个局部对象离开其作用域时,析构函数会自动调用。例如,在一个函数内部定义的对象,当函数执行完毕,该对象的生命周期结束,析构函数就会被调用。
#include <iostream>
class MyClass {
public:
    MyClass() {
        std::cout << "MyClass constructor called." << std::endl;
    }
    ~MyClass() {
        std::cout << "MyClass destructor called." << std::endl;
    }
};
void testFunction() {
    MyClass obj;
    std::cout << "Inside testFunction." << std::endl;
}
int main() {
    std::cout << "Before calling testFunction." << std::endl;
    testFunction();
    std::cout << "After calling testFunction." << std::endl;
    return 0;
}

在上述代码中,testFunction函数内部定义了MyClass类型的局部对象obj。当testFunction函数执行到末尾时,obj的生命周期结束,其析构函数被自动调用。运行结果如下:

Before calling testFunction.
MyClass constructor called.
Inside testFunction.
MyClass destructor called.
After calling testFunction.
  • 全局对象:全局对象在程序结束时,其析构函数会被调用。全局对象的生命周期从程序启动开始,到程序结束结束。
#include <iostream>
class GlobalClass {
public:
    GlobalClass() {
        std::cout << "GlobalClass constructor called." << std::endl;
    }
    ~GlobalClass() {
        std::cout << "GlobalClass destructor called." << std::endl;
    }
};
GlobalClass globalObj;
int main() {
    std::cout << "Inside main function." << std::endl;
    return 0;
}

这里定义了一个全局对象globalObj。当main函数执行完毕,程序即将结束时,globalObj的析构函数会被调用。运行结果为:

GlobalClass constructor called.
Inside main function.
GlobalClass destructor called.
  • 静态局部对象:静态局部对象在程序结束时,其析构函数会被调用。静态局部对象的生命周期从第一次进入其定义所在的函数且初始化开始,直到程序结束。
#include <iostream>
class StaticLocalClass {
public:
    StaticLocalClass() {
        std::cout << "StaticLocalClass constructor called." << std::endl;
    }
    ~StaticLocalClass() {
        std::cout << "StaticLocalClass destructor called." << std::endl;
    }
};
void staticLocalFunction() {
    static StaticLocalClass staticObj;
    std::cout << "Inside staticLocalFunction." << std::endl;
}
int main() {
    std::cout << "Before calling staticLocalFunction." << std::endl;
    staticLocalFunction();
    std::cout << "After calling staticLocalFunction." << std::endl;
    return 0;
}

在这个例子中,staticLocalFunction函数内部定义了静态局部对象staticObj。当main函数结束,程序即将终止时,staticObj的析构函数会被调用。运行结果如下:

Before calling staticLocalFunction.
StaticLocalClass constructor called.
Inside staticLocalFunction.
After calling staticLocalFunction.
StaticLocalClass destructor called.
  1. 对象被删除
    • 使用delete运算符:当使用new运算符动态分配的对象,使用delete运算符释放其内存时,析构函数会被调用。
#include <iostream>
class DynamicClass {
public:
    DynamicClass() {
        std::cout << "DynamicClass constructor called." << std::endl;
    }
    ~DynamicClass() {
        std::cout << "DynamicClass destructor called." << std::endl;
    }
};
int main() {
    DynamicClass* dynamicPtr = new DynamicClass();
    std::cout << "After creating dynamic object." << std::endl;
    delete dynamicPtr;
    std::cout << "After deleting dynamic object." << std::endl;
    return 0;
}

在上述代码中,通过new创建了DynamicClass类型的动态对象,并通过指针dynamicPtr指向它。当执行delete dynamicPtr;时,DynamicClass对象的析构函数会被调用。运行结果为:

DynamicClass constructor called.
After creating dynamic object.
DynamicClass destructor called.
After deleting dynamic object.
  • 数组对象使用delete[]:对于使用new[]分配的对象数组,必须使用delete[]来释放内存,同时数组中每个对象的析构函数都会被调用。
#include <iostream>
class ArrayClass {
public:
    ArrayClass() {
        std::cout << "ArrayClass constructor called." << std::endl;
    }
    ~ArrayClass() {
        std::cout << "ArrayClass destructor called." << std::endl;
    }
};
int main() {
    ArrayClass* arrayPtr = new ArrayClass[3];
    std::cout << "After creating array of objects." << std::endl;
    delete[] arrayPtr;
    std::cout << "After deleting array of objects." << std::endl;
    return 0;
}

这里创建了一个包含3个ArrayClass对象的数组。当使用delete[] arrayPtr;时,数组中3个对象的析构函数都会依次被调用。运行结果如下:

ArrayClass constructor called.
ArrayClass constructor called.
ArrayClass constructor called.
After creating array of objects.
ArrayClass destructor called.
ArrayClass destructor called.
ArrayClass destructor called.
After deleting array of objects.
  1. 容器中对象移除
    • 标准库容器:在C++标准库容器(如std::vectorstd::liststd::map等)中,当对象从容器中移除(例如通过erase成员函数)时,对象的析构函数会被调用。
#include <iostream>
#include <vector>
class ContainerClass {
public:
    ContainerClass() {
        std::cout << "ContainerClass constructor called." << std::endl;
    }
    ~ContainerClass() {
        std::cout << "ContainerClass destructor called." << std::endl;
    }
};
int main() {
    std::vector<ContainerClass> vec;
    vec.push_back(ContainerClass());
    std::cout << "After pushing object into vector." << std::endl;
    vec.erase(vec.begin());
    std::cout << "After erasing object from vector." << std::endl;
    return 0;
}

在上述代码中,将ContainerClass对象添加到std::vector中,然后使用erase函数移除该对象。当对象被移除时,其析构函数会被调用。运行结果为:

ContainerClass constructor called.
After pushing object into vector.
ContainerClass destructor called.
After erasing object from vector.
  • 自定义容器:对于自定义的容器,如果正确实现了对象移除的逻辑,也需要在移除对象时调用其析构函数。例如,一个简单的链表容器:
#include <iostream>
class ListNode {
public:
    ListNode() : next(nullptr) {
        std::cout << "ListNode constructor called." << std::endl;
    }
    ~ListNode() {
        std::cout << "ListNode destructor called." << std::endl;
    }
    ListNode* next;
};
class MyList {
public:
    MyList() : head(nullptr) {}
    ~MyList() {
        while (head) {
            ListNode* temp = head;
            head = head->next;
            delete temp;
        }
    }
    void addNode() {
        ListNode* newNode = new ListNode();
        newNode->next = head;
        head = newNode;
    }
    void removeNode() {
        if (head) {
            ListNode* temp = head;
            head = head->next;
            delete temp;
        }
    }
private:
    ListNode* head;
};
int main() {
    MyList myList;
    myList.addNode();
    std::cout << "After adding node to MyList." << std::endl;
    myList.removeNode();
    std::cout << "After removing node from MyList." << std::endl;
    return 0;
}

在这个自定义链表容器MyList中,removeNode函数在移除节点时,会调用节点对象的析构函数。运行结果如下:

ListNode constructor called.
After adding node to MyList.
ListNode destructor called.
After removing node from MyList.
  1. 异常处理
    • 栈展开:当在函数中抛出异常,而该函数内的局部对象还未销毁时,会发生栈展开。在栈展开过程中,从异常抛出点到异常处理程序之间的所有局部对象的析构函数都会被调用。
#include <iostream>
class ExceptionClass {
public:
    ExceptionClass() {
        std::cout << "ExceptionClass constructor called." << std::endl;
    }
    ~ExceptionClass() {
        std::cout << "ExceptionClass destructor called." << std::endl;
    }
};
void throwException() {
    ExceptionClass obj;
    std::cout << "Before throwing exception." << std::endl;
    throw std::runtime_error("Exception thrown.");
    std::cout << "This line will not be executed." << std::endl;
}
int main() {
    try {
        throwException();
    } catch (const std::runtime_error& e) {
        std::cout << "Caught exception: " << e.what() << std::endl;
    }
    return 0;
}

throwException函数中,定义了ExceptionClass类型的局部对象obj,然后抛出异常。在异常抛出后,obj的析构函数会被调用,因为栈展开过程会清理局部对象。运行结果如下:

ExceptionClass constructor called.
Before throwing exception.
ExceptionClass destructor called.
Caught exception: Exception thrown.
  • 析构函数中的异常:如果在析构函数中抛出异常,除非在析构函数内部捕获并处理了该异常,否则程序会调用std::terminate终止运行。这是因为析构函数通常用于清理资源,在析构函数中抛出异常可能导致资源无法正确清理,进而引发未定义行为。
#include <iostream>
class BadDestructorClass {
public:
    BadDestructorClass() {
        std::cout << "BadDestructorClass constructor called." << std::endl;
    }
    ~BadDestructorClass() {
        std::cout << "BadDestructorClass destructor start." << std::endl;
        throw std::runtime_error("Exception in destructor.");
        std::cout << "BadDestructorClass destructor end." << std::endl;
    }
};
int main() {
    try {
        BadDestructorClass obj;
    } catch (const std::runtime_error& e) {
        std::cout << "Caught exception: " << e.what() << std::endl;
    }
    return 0;
}

在上述代码中,BadDestructorClass的析构函数抛出异常。由于这个异常没有在析构函数内部处理,程序会调用std::terminate,不会输出“Caught exception”相关信息。实际运行时,程序可能直接崩溃或显示与std::terminate相关的错误信息。

注意事项

  1. 基类和派生类析构函数
    • 调用顺序:在派生类对象销毁时,首先调用派生类的析构函数,然后调用基类的析构函数。这与构造函数的调用顺序相反,构造函数是先调用基类构造函数,再调用派生类构造函数。
#include <iostream>
class BaseClass {
public:
    BaseClass() {
        std::cout << "BaseClass constructor called." << std::endl;
    }
    ~BaseClass() {
        std::cout << "BaseClass destructor called." << std::endl;
    }
};
class DerivedClass : public BaseClass {
public:
    DerivedClass() {
        std::cout << "DerivedClass constructor called." << std::endl;
    }
    ~DerivedClass() {
        std::cout << "DerivedClass destructor called." << std::endl;
    }
};
int main() {
    DerivedClass derivedObj;
    std::cout << "After creating derived object." << std::endl;
    return 0;
}

运行结果为:

BaseClass constructor called.
DerivedClass constructor called.
After creating derived object.
DerivedClass destructor called.
BaseClass destructor called.
  • 虚析构函数:如果基类指针指向派生类对象,并且通过基类指针删除该对象,为了确保派生类的析构函数被正确调用,基类的析构函数应该声明为虚函数。否则,只有基类的析构函数会被调用,派生类的析构函数不会被调用,从而导致资源泄漏。
#include <iostream>
class BaseClass {
public:
    BaseClass() {
        std::cout << "BaseClass constructor called." << std::endl;
    }
    // Without virtual destructor, derived class destructor may not be called.
    ~BaseClass() {
        std::cout << "BaseClass destructor called." << std::endl;
    }
};
class DerivedClass : public BaseClass {
public:
    DerivedClass() {
        std::cout << "DerivedClass constructor called." << std::endl;
    }
    ~DerivedClass() {
        std::cout << "DerivedClass destructor called." << std::endl;
    }
};
int main() {
    BaseClass* basePtr = new DerivedClass();
    delete basePtr;
    return 0;
}

在上述代码中,如果BaseClass的析构函数不是虚函数,运行结果为:

BaseClass constructor called.
DerivedClass constructor called.
BaseClass destructor called.

可以看到,DerivedClass的析构函数没有被调用。将BaseClass的析构函数声明为虚函数:

#include <iostream>
class BaseClass {
public:
    BaseClass() {
        std::cout << "BaseClass constructor called." << std::endl;
    }
    virtual ~BaseClass() {
        std::cout << "BaseClass destructor called." << std::endl;
    }
};
class DerivedClass : public BaseClass {
public:
    DerivedClass() {
        std::cout << "DerivedClass constructor called." << std::endl;
    }
    ~DerivedClass() {
        std::cout << "DerivedClass destructor called." << std::endl;
    }
};
int main() {
    BaseClass* basePtr = new DerivedClass();
    delete basePtr;
    return 0;
}

运行结果为:

BaseClass constructor called.
DerivedClass constructor called.
DerivedClass destructor called.
BaseClass destructor called.

此时,DerivedClass的析构函数被正确调用。 2. 对象成员析构顺序

  • 按声明顺序:类的成员对象的析构顺序与它们在类中声明的顺序相反。例如:
#include <iostream>
class MemberClass1 {
public:
    MemberClass1() {
        std::cout << "MemberClass1 constructor called." << std::endl;
    }
    ~MemberClass1() {
        std::cout << "MemberClass1 destructor called." << std::endl;
    }
};
class MemberClass2 {
public:
    MemberClass2() {
        std::cout << "MemberClass2 constructor called." << std::endl;
    }
    ~MemberClass2() {
        std::cout << "MemberClass2 destructor called." << std::endl;
    }
};
class OuterClass {
public:
    MemberClass1 member1;
    MemberClass2 member2;
    OuterClass() {
        std::cout << "OuterClass constructor called." << std::endl;
    }
    ~OuterClass() {
        std::cout << "OuterClass destructor called." << std::endl;
    }
};
int main() {
    OuterClass outerObj;
    std::cout << "After creating outer object." << std::endl;
    return 0;
}

运行结果为:

MemberClass1 constructor called.
MemberClass2 constructor called.
OuterClass constructor called.
After creating outer object.
OuterClass destructor called.
MemberClass2 destructor called.
MemberClass1 destructor called.

可以看到,member2先于member1被构造,而在析构时,member2后于member1被析构。

  1. 静态成员与析构函数
    • 静态成员不依赖对象:静态成员不属于任何对象实例,因此它们不受对象析构的影响。静态成员的生命周期从程序启动开始,到程序结束结束。例如:
#include <iostream>
class StaticMemberClass {
public:
    StaticMemberClass() {
        std::cout << "StaticMemberClass constructor called." << std::endl;
    }
    ~StaticMemberClass() {
        std::cout << "StaticMemberClass destructor called." << std::endl;
    }
    static int staticValue;
};
int StaticMemberClass::staticValue = 0;
int main() {
    StaticMemberClass obj1;
    std::cout << "After creating obj1." << std::endl;
    {
        StaticMemberClass obj2;
        std::cout << "After creating obj2." << std::endl;
    }
    std::cout << "Static value: " << StaticMemberClass::staticValue << std::endl;
    return 0;
}

在这个例子中,staticValue是静态成员,无论obj1obj2的析构,staticValue都不会被销毁,其值在程序运行期间保持有效。运行结果为:

StaticMemberClass constructor called.
After creating obj1.
StaticMemberClass constructor called.
After creating obj2.
StaticMemberClass destructor called.
Static value: 0
StaticMemberClass destructor called.

总结析构函数自动调用的重要性

析构函数自动调用确保了对象所占用的资源能够被及时、正确地释放,避免了内存泄漏和资源泄露等问题。了解析构函数自动调用的触发条件,对于编写健壮、高效的C++程序至关重要。在实际编程中,尤其是在处理动态分配资源、复杂对象关系以及异常处理等场景时,正确利用析构函数的自动调用机制,可以提高程序的稳定性和可靠性。同时,注意析构函数调用过程中的各种细节,如基类与派生类析构函数的关系、对象成员的析构顺序等,有助于避免潜在的编程错误。