12、虚函数的应用、虚析构函数
- 运行时类型信息(RTTI)
- 动态类型转换(dynamic_cast)
- typeid操作符
- 虚 析构函数
- 空虚析构函数
一个类中,除了构造函数和静态成员函数外,任何函数都可以被声明为虚函数
运行时类型信息(RTTI)
动态类型转换(dynamic_cast)
- 用于将基类类型的指针或引用转换为其子类类型的指针或引用
- 前提是子类必须从基类多态继承 (即基类包含至少一个虚函数)
- 动态类型转换会对所需转换的基类指针或引用做检查,如果其指向的对象的类型与所要转换的目标类型一致,则转换成功,否则转换失败。
- 针对指针的动态类型转换,以返回空指针(NULL)表示失败,针对引用的动态类型转换以抛出
bad_cast
异常表示失败
// 动态类型转换 :基类类型指针转换为子类类型指针
// 基类类型引用转换为子类类型引用
#include <iostream>
using namespace std;class A { // 编译器根据A类信息,将制作一张虚函数表 "A"...|A::foo的地址
public:virtual void foo(){}
};class B : public A { // 编译器根据B类信息,将制作一张虚函数表 "B"...|A::foo的地址
};class C : public B { // 编译器根据C类信息,将制作一张虚函数表 "C"...|A::foo的地址
};class D {}; // 编译器根据D类信息,不制作虚函数表int main( void ){B b; // |虚表指针| --> 编译器根据B类信息制作的虚函数表A* pa = &b; // B* --> A* (子类类型指针 --> 基类类型指针)cout << "---------------------dynamic_cast 运行期间做的转换-----------------------" << endl; B* pb = dynamic_cast<B*>(pa); // pa-->b对象所占内存空间-->虚表指针 --> 编译器根据B类信息制作的虚函数表->"B"cout << "A* pa --> B* pb: " << pb << endl;C* pc = dynamic_cast<C*>(pa); // pa-->b对象所占内存空间-->虚表指针 --> 编译器根据B类信息制作的虚函数表->"B"cout << "A* pa --> C* pc: " << pc << endl;D* pd = dynamic_cast<D*>(pa); // pa-->b对象所占内存空间-->虚表指针 --> 编译器根据B类信息制作的虚函数表->"B"cout << "A* pa --> D* pd: " << pd << endl;cout << "---------------------static_cast 编译期间做的转换-----------------------" << endl; pb = static_cast<B*>(pa); // 即合理且安全 A* -->B*的反向 可以隐式转换cout << "A* pa --> B* pb: " << pb << endl;pc = static_cast<C*>(pa); // 有风险 A*-->C*的反向 可以隐式转换cout << "A* pa --> C* pc: " << pc << endl;// pd = static_cast<D*>(pa); // 不合理 A*-->D*的反向 不可以隐式转换
// cout << "A* pa --> D* pd: " << pd << endl;return 0;
}
typeid操作符
#include <typeinfo>
- 返回type info类型对象的常引用
- type info类的成员函数name(),返回类型名字符串
- type info类支持“==”和“!=”操作符,可直接用于类型相同与否的判断
- 当其作用于基类类型的指针或引用的目标对象时
- 若基类不包含虚函数 typeid所返回类型信息由该指针或引用本身的类型决定
- 若基类包含至少一个虚函数,即存在多态继承,typeid所返回类型信息由该指针或引用的实际目标对象的类型决定
// typeid操作符 -- 获取对象的类型信息
// 无法获取对象常属性信息
#include <iostream>
#include <typeinfo>
using namespace std;class A { // 编译器根据A类信息,将制作一张虚函数表 "A"...|A::foo的地址 virtual void foo(){}
};class B : public A { // 编译器根据B类信息,将制作一张虚函数表 "B"...|A::foo的地址
};int main( void ){B b;// |虚表指针| --> 编译器根据B类信息制作的虚函数表A* pa = &b;A& ra = b;cout << "pa指针的目标对象的类型:" << typeid(*pa).name() << endl;// pa->b对象所占内存空间-->虚表指针-->B类虚函数表-->"B"cout << "ra引用的目标对象的类型:" << typeid(ra).name() << endl;// ra->b对象所占内存空间-->虚表指针-->B类虚函数表-->"B"int m;const type_info& rty = typeid(m);// 1. 获取m的类型信息(类名、类大小、类版本...)// 2. 创建一个type_info类对象// 3. 将获取到的m的类型信息保存到type_info对象的私有成员变量中// 4. 返回type_info类对象的常引用string rn = rty.name();cout << "m的类型:" << rn << endl;const int n = 10;cout << "n的类型:" << typeid(n).name() << endl;cout << (typeid(m) == typeid(n)) << endl;cout << (typeid(m)!=typeid(n)) << endl;return 0;
}
虚 析构函数
delete一个基类指针 (指向子类对象)
-
实际被调用的仅仅是基类的析构函数
-
基类的析构函数只负责析构子类对象中的基类子对象
-
基类的析构函数不会调用子类的析构函数
-
在子类中分配的资源将无法得到释放
-
如果将基类的析构函数声明为虚函数,那么实际被调用的将是子类的析构函数
-
子类的析构函数将首先释放子类对象自己的成员,然后再调用基类的析构函数释放该子类对象的基类部分,最终实现完美的资源释放
// 虚析构函数 -- delete一个基类类型指针(指向子类对象),能够正确的调用子类的析构函数
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
using namespace std;class A{
public:A():m_a(open("./file",O_CREAT | O_RDWR, 0644)){//【int m_a = open(..);】定义m_a,初值为文件描述符 -->文件表等内核信息(动态资源)cout << "A()被调用 -- 打开file文件" << endl;}virtual ~A(){ // 虚析构函数close(m_a);cout << "~A()被调用 -- 关闭file文件" << endl;// 释放m_a本身所占内存空间}
private:int m_a;
};
class B : public A{
public:B():m_b(open("./cfg",O_CREAT | O_RDWR, 0644)){//【A();】定义基类子对象,利用基类子对象.A()//【int m_b = open(...);】定义m_b,初值为文件描述符-->文件表等内核信息(动态资源)cout << "B()被调用 -- 打开cfg文件" << endl;}~B(){ // 虚析构函数close(m_b);cout << "~B()被调用 -- 关闭cfg文件" << endl;// 对于基类子对象,利用基类子对象.~A()// 释放m_b/基类子对象本身所占内存空间}
private:int m_b;
};int main( void ){A* p = new B; // 定义B堆对象,利用B类堆对象.B()delete p; // p->析构函数(~B()) 释放B类堆对象本身所占内存空间return 0;
}
空虚析构函数
- 没有分配任何动态资源的类,无需定义析构函数
- 没有定义析构函数的类,编译器会为其提供一个缺省析构函数,但缺省析构函数并不是虚函数
- 为了保证delete一个指向子类对象的基类指针时,能够正确调用子类的析构函数,就必须把基类的析构函数定义为虚函数,即使它是一个空函数
- 任何时候,为基类定义一个虚析构函数总是无害的
一个类中,除了构造函数和静态成员函数外,任何函数都可以被声明为虚函数