C++:析构函数
一、什么是析构函数
析构函数是类中一种特殊的成员函数,但其功能和构造函数是相反的,当对象结束其生命周期时,系统会自动调用该对象的析构函数进行清理工作(如释放内存中分配给该对象的空间,关闭打开的文件等)。另外析构函数没有返回值,不需要参数,也不能被重载且一个类中有且只能有一个析构函数。但和构造函数相似,析构函数的函数名和类名相同,只不过需要在函数名向加上一个~。
语法:~ 类名(){/*...析构函数体...*/}
特别注意:
• 如果用户没有显式地在类中定义析构函数,编译器会在类中生成一个默认的析构函数。且任何对象在被系统销毁时,都会调用该对象的析构函数。
1 #include<iostream> 2 #include<string> 3 using namespace std; 4 class Student{ 5 public: 6 Student(){ 7 cout<<"调用了默认构造函数"<<endl; 8 } 9 Student(string name,int age):Name(name),Age(age){} 10 Student(const Student& stu);//拷贝构造函数 11 Student& operator=(const Student& stu);//赋值函数 12 ~Student(){ //析构函数 13 cout<<"调用了析构函数"<<endl; 14 } 15 private: 16 string Name; 17 int Age; 18 }; 19 int main(){ 20 Student stu1; 21 return 0; 22 }
• main函数中,析构函数的调用发生在语句“return 0;”之后
二、对象的析构顺序
先来看一个例子
1 #include<iostream> 2 #include<string> 3 using namespace std; 4 class Student{ 5 public: 6 Student(){ 7 cout<<"调用了默认构造函数"<<endl; 8 } 9 Student(string name,int age,int id):Name(name),Age(age),ID(id){ 10 cout<<"创建对象ID号为"<<this->ID<<"的对象"<<endl; 11 } 12 Student(const Student& stu);//拷贝构造函数 13 Student& operator=(const Student& stu);//赋值函数 14 ~Student(){ //析构函数 15 cout<<"析构对象ID号为"<<this->ID<<"的对象"<<endl; 16 } 17 private: 18 string Name; 19 int Age; 20 int ID;//对象的ID号 21 }; 22 int main(){ 23 Student stu1("Tomwenxing",23,1); 24 Student stu2("Ellen",22,2); 25 Student stu3("Jack",21,3); 26 Student stu4("Dick",23,4); 27 cout<<"---------------分界线---------------------"<<endl; 28 return 0; 29 }
由上例可知,对象创建完毕后会被存放在内存中的一个栈中,因此根据栈的“先进后出”的原则,最后被创建的对象由于最晚添加到栈中,故会被最先析构;而最早创建的对象由于位于栈底,故最后才会被析构。
三、为什么编写析构函数
在C++中,如果用户在自定义的类中没有编写析构函数,那么编译器会在类中自动生成一个默认的析构函数,但通常情况下编译器生成的默认析构函数的函数体为空,即该析构函数什么工作也不做,例如上例中的Student类,如果不手动定义该类的析构函数,那么系统默认生成的析构函数如下:
1 Student::~Student(){}//该析构函数什么工作也不做
但有时候我们需要析构函数在销毁对象时完成一些清理工作。例如C++要求如果用new在内存中动态开辟了空间,则必须由相应的delete对该内存空间进行释放,因此如果我们自定义的类中含有指针成员变量,并在该类的构造函数中用new为该指针在内存中动态的分配空间,那么为了避免内存泄漏,就必须在该类的析构函数中使用delete来释放在内存中动态开辟的空间:
1 #include<iostream> 2 #include<string> 3 using namespace std; 4 class Example{ 5 public: 6 Example()=default; 7 Example(string message,int v):ptr(new string) ,ID(v){ //用new为指针ptr分配内存空间 8 *ptr=message; //将信息拷贝到动态分配的内存空间中 9 cout<<"创建对象ID为"<<this->ID<<"的对象"<<endl; 10 } 11 ~Example(){ 12 cout<<"析构对象ID为"<<this->ID<<"的对象"<<endl; 13 delete ptr;//释放ptr所指的内存空间 14 ptr=NULL; //将指针ptr赋值为空 15 cout<<"delete完毕!"<<endl; 16 } 17 private: 18 string *ptr;//指针 19 int ID; 20 }; 21 22 int main(){ 23 Example example1("Tomwenxing",1); 24 Example example2("JackMa",2); 25 return 0; 26 }
再比如我们可以在构造函数中打开文件,而在析构函数中关闭文件;或在构造函数中打开和数据库的连接,而在析构函数中关闭和数据库的连接。等等......
因此简单来说就是在构造函数中获取系统资源,而在析构函数中释放这些系统资源以便这些系统资源被重新利用。
四、一个重要的原则——三法则(rule of three)
如果用户显示定义了类中析构函数、拷贝构造函数或赋值函数中的任何一个,那么另外两个函数通常也必须显式定义。
Question:什么时候需要显示定义?
Answer:当自定义类中有指针成员变量或需要利用某种系统资源时(如文件资源、数据库资源等)时,往往需要显示定义析构函数、拷贝构造函数和赋值函数。