-
2020-09-04 23:35:43
上节我们讲到,构造函数不能是虚函数,因为派生类不能继承基类的构造函数,将构造函数声明为虚函数没有意义。
这是原因之一,另外还有一个原因:C++ 中的构造函数用于在创建对象时进行初始化工作,在执行构造函数之前对象尚未创建完成,虚函数表尚不存在,也没有指向虚函数表的指针,所以此时无法查询虚函数表,也就不知道要调用哪一个构造函数。下节将会讲解虚函数表的概念。
析构函数用于在销毁对象时进行清理工作,可以声明为虚函数,而且有时候必须要声明为虚函数。
为了说明虚析构函数的必要性,请大家先看下面一个例子:
#include <iostream> using namespace std; //基类 class Base{ public: Base(); ~Base(); protected: char *str; }; Base::Base(){ str = new char[100]; cout<<"Base constructor"<<endl; } Base::~Base(){ delete[] str; cout<<"Base destructor"<<endl; } //派生类 class Derived: public Base{ public: Derived(); ~Derived(); private: char *name; }; Derived::Derived(){ name = new char[100]; cout<<"Derived constructor"<<endl; } Derived::~Derived(){ delete[] name; cout<<"Derived de
更多相关内容 -
C++虚析构函数的使用分析
2020-12-31 18:10:59在C++中,不能声明虚构造函数,但可以声明虚析构函数。多态性是指不同的对象对同一消息有不同的行为特性。虚函数作为运行时多态性的基础,主要是针对对象的,而构造函数是在对象产生之前运行的,因此虚构造函数是没有... -
c++虚析构函数的必要性
2020-04-08 17:23:06我们知道,用C++开发的时候,用来做基类的类的析构函数一般都是虚函数。 可是,为什么要这样做呢?下面用一个小例子来说明: 1#include<iostream> 2 using namespace std; 3 class Base 4 { 5 public:...我们知道,用C++开发的时候,用来做基类的类的析构函数一般都是虚函数。
可是,为什么要这样做呢?下面用一个小例子来说明:
1#include<iostream> 2 using namespace std; 3 class Base 4 { 5 public: 6 Base() {} 7 virtual ~Base(); 8 }; 9 10 class Subclass :public Base 11 { 12 public: 13 Subclass() {} 14 ~Subclass(); 15 }; 16 Base::~Base() 17 { 18 cout << "Base destructor is called." << endl; 19 } 20 21 Subclass::~Subclass() 22 { 23 cout << "Subclass destructor is called." << endl; 24 } 25 26 int main() 27 { 28 Base *b = new Subclass; 29 delete b; 30 return 0; 31 }
输出结果:
Subclass destructor is called.
Base destructor is called.这个很简单,非常好理解。
但是,如果把类Base析构函数前的virtual去掉,那输出结果就是下面的样子了:Base destructor is called.
也就是说,类Base的析构函数根本没有被调用!一般情况下类的析构函数里面都是释放内存资源,而析构函数不被调用的话就会造成内存泄漏。我想所有的C++程序员都知道这样的危险性。当然,如果在析构函数中做了其他工作的话,那你的所有努力也都是白费力气。
所以,文章开头的那个问题的答案就是--这样做是为了当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用。
当然,并不是要把所有类的析构函数都写成虚函数。因为当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。 -
C++之虚析构函数的必要性
2021-08-04 00:26:24C++之虚析构函数的必要性 C++程序员经常会面对的问题就是内存泄漏,如果操作不当发生内存泄漏,那么将会是毁灭性的Bug。在继承里,大部分基类中的析构函数通常会声明为虚函数,这样会避免指针占用的堆内存得不到释放...C++之虚析构函数的必要性
C++程序员经常会面对的问题就是内存泄漏,如果操作不当发生内存泄漏,那么将会是毁灭性的Bug。在继承里,大部分基类中的析构函数通常会声明为虚函数,这样会避免指针占用的堆内存得不到释放,从而造成内存泄漏的情况。
#include <iostream> using namespace std; class People { public: People(char *name_parameters, int age_parameters); People(const People &people); ~People(); virtual void display(); char *get_name(); int get_age(); private: char *name; int age; }; People::People(char *name_parameters, int age_parameters) : name(name_parameters), age(age_parameters) { cout << "people class constructed is created" << endl; } People::People(const People &people) { cout << "people copy constructed is used" << endl; } People::~People() { cout << "people destructed is used" << endl; } int People::get_age() { return this->age; } char *People::get_name() { return this->name; } void People::display() { cout << "name:" << name << ",age:" << age << endl; } class Teacher : public People { public: Teacher(char *name, int age, int salary); ~Teacher(); void display(); int get_salary(); private: int salary; }; Teacher::Teacher(char *name, int age, int salary) : People(name, age), salary(salary) { cout << "Teacher class constructed is created" << endl; } Teacher::~Teacher() { cout << "Teacher destructed is used" << endl; } int Teacher::get_salary() { return this->salary; } void Teacher::display() { cout << "name2:" << get_name() << ",age:" << get_age()\ << "salary:" << salary << endl; } int main() { People *ptr = new People("people", 20); ptr = new Teacher("child", 10, 1000); delete ptr; return 0; }
输出: people class constructed is created people class constructed is created Teacher class constructed is created people destructed is used
上述程序可以观察到,基类中没有使用到虚析构函数,导致delete调用基类析构函数在释放内存时,没有将派生类对象产生的堆内存释放掉,这样就容易造成内存泄漏。那么为什么没有调用派生类的析构函数呢?因为这里函数是非虚函数,通过指针访问非虚函数时,编译器会根据指针的类型来访问析构函数,现在指针的类型是People,那么在没有虚函数的情况下就只能调用People的析构函数。下面将基类的析构函数声明为虚析构函数。
#include <iostream> using namespace std; class People { public: People(char *name_parameters, int age_parameters); People(const People &people); virtual ~People(); virtual void display(); char *get_name(); int get_age(); private: char *name; int age; }; People::People(char *name_parameters, int age_parameters) : name(name_parameters), age(age_parameters) { cout << "people class constructed is created" << endl; } People::People(const People &people) { cout << "people copy constructed is used" << endl; } People::~People() { cout << "people destructed is used" << endl; } int People::get_age() { return this->age; } char *People::get_name() { return this->name; } void People::display() { cout << "name:" << name << ",age:" << age << endl; } class Teacher : public People { public: Teacher(char *name, int age, int salary); ~Teacher(); void display(); int get_salary(); private: int salary; }; Teacher::Teacher(char *name, int age, int salary) : People(name, age), salary(salary) { cout << "Teacher class constructed is created" << endl; } Teacher::~Teacher() { cout << "Teacher destructed is used" << endl; } int Teacher::get_salary() { return this->salary; } void Teacher::display() { cout << "name2:" << get_name() << ",age:" << get_age()\ << "salary:" << salary << endl; } int main() { People *ptr = new People("people", 20); ptr = new Teacher("child", 10, 1000); delete ptr; return 0; }
输出: people class constructed is created people class constructed is created Teacher class constructed is created Teacher destructed is used people destructed is used
通过上述程序的修改,声明基类的虚析构函数之后,delete掉基类的指针,会调用派生类的析构函数和基类的析构函数释放内存。因为虚函数不会在意指针的类型,而是观察指针指向的对象,根据指针指向的对象来调用成员(成员变量和成员函数)。也就是说,指针指向哪个对象,就调用哪个对象的成员。指针ptr指向了派生类的对象,那么在释放内存时会优先调用派生类的析构函数,再根据delete机制调用基类的析构函数,这样就解决了内存泄漏的问题。
构造函数不能是虚函数
主要有两个原因:
-
派生类不能继承基类的构造函数,因此把基类的构造函数声明为虚函数没有意义,无法实现多态;
-
c++中的构造函数用来在创建对象时进行初始化工作,在执行构造函数的时候,对象尚未创建完成,虚函数表这个时候还不存在, 也没有指向虚函数表的指针,所以此时还无法查询虚函数表。也就不知道调用哪一个构造函数。
虚析构函数的原理分析
- 由于基类的析构函数为虚函数,所以派生类会在所有属性的前面形成虚表,而虚表内部存储的就是基类的虚函数。
- 当delete基类的指针时,由于派生类的析构函数与基类的析构函数构成多态,所以得先调动派生类的析构函数;之所以再调动基类的析构函数,是因为delete的机制所引起的,delete 基类指针所指的空间,要调用基类的析构函数。
-
-
C++虚函数、虚析构函数浅析
2021-06-03 11:36:50C++虚函数全面解析虚函数的工作原理:虚析构函数:C++中的静态联编和动态联编 学习过C++的都知道可以通过虚函数实现多态。在基类中定义一个虚函数,在派生类中可以重写这个虚函数,实现派生类自己的特性。 虚函数的...C++虚函数浅析
学习过C++的都知道可以通过虚函数实现多态。在基类中定义一个虚函数,在派生类中可以重写这个虚函数,实现派生类自己的特性。虚函数的工作原理:
C++规定了函数名参数返回值,没有规定实现,可以根据需要自行实现内容。通常编译器处理虚函数的方法是给每个对象添加一个隐藏成员。该成员保存了一个指向函数地址的数组指针,这个数组指针也就是虚函数表。虚函数表中保存了对象中所有虚函数的地址(包括继承的基类的虚函数地址),如果派生类多重继承就会存在多个虚函数表,派生类本身的虚函数表会出现在继承顺序第一个基类后面,下面举例演示一下:
class a{ public: virtual void base_a1(); virtual void base_a2(); ] class b{ public: virtual void base_b1(); virtual void base_b2(); ] class c{ public: virtual void base_c1(); virtual void base_c2(); ] class d:public a,public b,public c{ virtual void derive_d1(); virtual void derive_d2(); } int main(){ d derive_d; }
上面代码中定义derive_d的虚函数表结构如下图:
在对象所占内存的开始位置存在隐藏成员指针a、指针b、指针c,他们分别指向对应的虚函数表,对象自己的虚函数表在第一个继承的基类的后面。class d:public a,public b,public c{ void base_b2(); virtual void derive_d1(); virtual void derive_d2(); }
如果派生类重写了基类的函数(方法),那么在上图base_b2处的地址将变成指向派生类实现base_b2方法的地址。此时用派生类的对象在调用base_b2方法时调用的就是派生类重写的base_b2方法。
虚析构函数:
为什么基类的析构函数必须声明称虚函数呢?大家可能都知道,基类的析构函数不声明成虚函数时在释放时候有可能会导致子类的析构函数调用不到的情况。
class Base{ public: Base() { qDebug()<<"base"; } ~Base() { qDebug()<<"~base"; } }; class Derive:public Base{ public: Derive() { qDebug()<<"Derive"; } ~Derive() { qDebug()<<"~Derive"; } }; int main(){ Base *p=new Derive(); delete(p); } 输出: base Derive ~base
上面代码执行情况就会先调用基类的构造函数然后调用派生类的构造函数,然后调用基类的析构函数。这就导致了内存泄露,为派生类分配了内存但是并没有释放。如果main函数像下面这样实现就不会出现这种情况:
int main(){ Derive *p=new Derive; delete(p); } 输出: base Derive ~Derive ~base
因为派生类的析构函数会调用基类的析构函数。虽然这种方法可以避免内存泄露,但是有时候某种情况下必须定义父类指针去指向一个子类的对象所以还是存在风险。只要将Base类如下声明就可以完全避免这个问题。
class Base{ public: virtual ~Base(); } 输出: base Derive ~Derive ~base
这样无论你是定义子类指针去指向一个子类对象还是用父类定义指针去指向子类对象都不会有内存泄露的问题。
到这里可能大家都知道,但是作者在这里提出一个疑问,为什么父类声明成虚构造函数就能在delete时候既调用到子类的析构函数又能调用到父类的析构函数呢?这就不得不提一下C++中的静态联编和动态联编了。C++中的静态联编和动态联编
在提到动态和静态的时候我不禁想起了C语言中的动态库和静态库,其实动态联编和静态联编的区别与动态库和静态库的区别十分相似。
程序在调用函数时候调用哪个函数,执行哪段代码是由编译器负责的,在C语言中每个函数名不同调用起来就十分简单,但是C++中可以重载的缘故,编译器必须确定什么时候执行哪个函数调用哪段代码,在编译过程中可以确定调用哪段代码的被称为静态联编(早期联编)。由于C++中存在虚函数,使用哪个函数调用哪段代码在编译时并不能确定就像之前例子中说的定义父类的指针去指向一个子类的对象,这时编译器无法做出正确的判断,不知道是该调用父类的析构函数还是子类的析构函数。编译器必须在程序运行时调用正确的虚函数代码,这就被称为动态联编。动态联编和虚函数是息息相关的(虚函数采用动态联编非虚函数采用静态联编)。动态联编效率会低于静态联编,所以不要声明没有必要的虚函数,这样会导致效率降低。
介绍完静态联编和动态联编,我们回到上面的问题:为什么父类声明成虚构造函数就能在delete时候既调用到子类的析构函数又能调用到父类的析构函数呢?
因为在定义为非虚析构函数时会采用静态联编,调用析构函数时因为是静态联编,在编译时会按照指针定义的类型也就是父类的类型,所以在调用哪个析构函数做选择时会选择父类的析构函数;但是在将基类的析构函数定义为虚析构函数后,就会采用动态联编,调用析构函数时会按照指针实际所指向的类型,也就是子类的类型,调用的自然就是子类的析构函数。上面说到过子类的虚构函数会调用父类的析构函数,所以父类子类的析构函数都被调用到了。
注意:
- 内联函数不能是虚函数,因为内联函数是在编译阶段展开的,而虚函数是在运行时动态调用的,编译时无法展开。
- 构造函数不能是虚函数,因为构造函数是在创建对象时候调用的,在创建派生类对象时会先调用基类的构造函数再调用派生类的构造函数。构造函数声明成虚函数是毫无意义的。
- 静态成员函数不能是虚函数,静态函数相当于普通函数和类、实例并没有关系所以也不存在多态,而虚函数是一种特殊的成员函数用来实现运行时多态的。所以静态成员函数声明成虚函数毫无意义。
以上内容均是作者查阅资料加自己理解,如有疑问,望读者不吝赐教,谢谢!
参考文献《C++ primer plus》(第六版)中文版 -
C++ 为什么析构函数一般写成虚函数
2021-04-25 21:32:07由于类的多态性,基类指针可以指向派生类的对象,如果删除该基类的指针,就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。 如果析构函数不被... -
C++中虚析构函数的作用
2018-09-02 10:26:22C++中的虚析构函数到底什么时候有用的,什么作用呢。一.虚析构函数的作用 总的来说虚析构函数是为了避免内存泄露,而且是当子类中会有指针成员变量时才会使用得到的。也就说虚析构函数使得在删除指向子类对象的基类... -
基类虚析构函数的必要性
2021-06-30 16:25:13基类析构函数是否为虚影响派生类析构函数的调用 class Base //基类 { public: ~Base(); //此处将基类析构函数设置为常规析构 }; Base::~Base() //基类析构函数 { cout << "Base Destruct" << ... -
析构函数声明无效_C++基类的析构函数为何要声明为虚函数
2020-10-22 17:41:13C++的类中,构造函数用于初始化对象及相关操作,构造函数是不能声明为虚函数的,因为在执行构造函数前...下面通过一个例子来说明下基类析构函数声明为虚函数的必要性。#include using namespace std;class base {p... -
C++虚析构函数
2020-02-14 08:14:11一句话总结:通常来说,如果基类中存在一个指向动态分配内存的成员变量,并且基类的析构函数中定义了释放该动态分配内存的代码,则应该将基类的析构函数声明为虚函数。不然会造成内存泄漏。 在类中,构造函数用于... -
C++学习笔记——虚析构函数的必要性
2020-02-13 23:33:42如果一个类中定义了虚函数,那=析构函数也应说明未虚函数。 delete运算符和析构函数一起工作,当使用delete删除一个对象时,delete隐藏着对析构函数的一次调用。 &... -
为什么C++中析构函数一般写成虚函数
2021-07-24 10:54:01为什么C++中析构函数一般写成虚函数 由于类的多态性,基类指针可以指向派生类的对象,如果删除该基类的指针,就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的... -
c++5.6虚析构函数
2021-04-17 11:09:04一个类中只允许一个虚析构函数(析构函数无参数) 虚析构函数的定义和使用 //虚析构函数 #include <iostream> using namespace std; class Base { public: Base( ){} virtual ~Base(){cout << "Base ... -
C++中析构函数
2020-06-09 16:37:53下面记录一下一个小示例,目的是说明虚析构函数的必要性。若析构函数不是虚函数,会发生什么,vs2010下创建控制台输出程序,下面看代码,以及运行实例; classObject.cpp #include "stdafx.h" #include <iostream... -
虚析构函数
2022-01-22 19:14:13因为不作基类的情况下,这个类就没有必要定义其他虚函数,采用虚析构函数会为类增加一个虚函数表指针的长度,使得类对象占用的空间增加,还有可能降低其可移植性。如下代码所示: #include "stdafx.h" class Base... -
C++虚析构函数的使用(可能造成内存泄露问题)
2018-02-28 15:17:25问题:C++虚拟函数的简单范例本程序通过VC++ 6.0编译与测试,两段程序分别演示了不使用虚析构函数和使用虚析构函数的结果,具体代码如下://未使用虚析构函数,程序目的:删除子类和父类的变量 #include <... -
详解析构函数出现的必要性
2021-05-19 06:48:55详解析构函数出现的必要性输入输出需要内存!传统C语言应对策略申请动态内存的好处析构函数闪亮登场什么是析构函数:析构函数的好处:wuli康康的参考资料输入输出需要内存!同学你好,你已经学了用scanf,cin指令... -
C++中为何析构函数总是虚函数?
2020-07-30 12:22:48如果这是必要的,那么为什么C++不把虚析构函数直接作为默认值? 答案: 编译器总是根据类型来调用类成员函数。但是一个派生类的指针可以安全地转化为一个基类的指针。这样删除一个基类的指针的时候,C++不管这个指针... -
浅谈C++基类的析构函数为虚函数
2017-03-01 20:34:40一旦一个类包含虚函数,它就应该包含一个虚析构器。如果一个类不用作基类或者不需具有多态性,便不应该为它声明虚析构器。 1、原因: 在实现多态时, 当用基类指针操作派生类, 在析构时候防止只... -
C++编程经验(2):为虚基类做虚析构函数的必要性
2021-08-01 16:45:42这个要提一下,如果记不住就记住:如果不做虚析构函数,会有内存泄漏 解释 定义一个基类的指针p,在delete p时,如果基类的析构函数是虚函数,这时只会看p所赋值的对象,如果p赋值的对象是派生类的对象,就会调用... -
为什么析构函数必须是虚函数?为什么默认的析构函数不是虚函数?
2020-03-26 07:52:04今天我们来谈一谈面试 C++ 工程师时经常被谈到的一个问题:为什么析构函数必须是虚函数?为什么默认的析构函数不是虚函数? 首先,我们看一下百度百科对虚函数是怎么定义的: 在某基类中声明为 virtual并在一个或多个... -
C/C++基类的析构函数定义为虚函数的必要性
2019-08-13 15:16:47基类指针可以指向派生类的对象(多态性),如果删除该指针delete []p;就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的...所以,将析构函数声明为虚函数是十分必要的。 为什么基类的析构函... -
c++之虚析构函数的必要性
2021-02-19 09:26:26c++之虚析构函数的必要性 构造函数不能是虚函数,主要有两个原因: 1、 派生类不能继承基类的构造函数,因此把基类的构造函数声明为虚函数没有意义,无法实现多态; 2、 c++中的构造函数用来在创建对象时进行... -
C++:什么情况下需要将析构函数定义为虚函数?
2016-10-17 15:55:22C++:什么情况下需要将析构函数定义为虚函数。 -
亲测:C++析构函数定义成virtual函数的必要性
2018-09-14 16:18:30关于为什么要将析构函数定义成虚函数,为了加深印象,写了个小测试代码对比了一下: #include <iostream> using namespace std; class Base{ public: Base() { cout << "...