7-虚函数
1、没有虚函数的程序
- 对象的自恰性
- 对同样的函数调用,各个类的对象都会做出恰当的响应
- 通过基类类型指针调用普通成员函数只能调用基类的成员函数
- 即便这个基类类型的指针指向子类对象,调用的也为基类的成员函数。
- 一旦调用子类所特有的成员函数,将引发编译错误。
- 编译器仅根据指针的类型确定调用哪个类的普通成员函数
class Shape{
public:
void Draw(){ cout << "Shape:Draw" << endl; }
int m_x;
int m_y;
};
class Rect:public Shape{
public:
void Draw(){ cout << "Rect:Draw" << endl; }
int m_rx;
int m_ry;
};
class Circle:public Shape{
public:
void Draw(){ cout << "circle:Draw" << endl; }
int m_radius;
};
int main(void){
cout<<"-----利用对象调用普通成员函数------"<<endl;
//利用对象调用非虚成员函数,哪个类对象就调用哪个类的非虚成员函数(自恰性)
Shape s;
s.Draw();
Rect r;
r.Draw();
Circle c;
c.Draw();
cout << "-----利用指针调用普通成员函数------" << endl;
Shape* ps = &s;
ps->Draw(); // Shape::Draw
ps = &r;
ps->Draw(); // Shape::Draw
//ps->foo(); //err
// 编译器简单而且粗暴根据指针本身的类型来确定到底调用哪个类的
return 0;
}
2、虚函数
- 形如
class 类名 {
virtual 返回类型 函数名 (形参表) {
…
}
};
- 覆盖
如果子类的成员函数和基类的虚函数具有相同的函数签名,那么该成员函数就也是虚函数,无论其是否带有virtual关键字,且与基类的虚函数构成覆盖关系 - 通过基类类型指针调用虚函数
- 如果基类型指针指向基类对象,调用基类的原始版本虚函数。
- 如果基类型指针指向子类对象,调用子类的覆盖版本虚函数。
class Shape{
public:
virtual void Draw(){ cout << "Shape:Draw" << endl; } // 虚函数
int m_x;
int m_y;
};
class Rect:public Shape{
public:
void Draw(){ cout << "Rect:Draw" << endl; } // 虚函数 (编译器补充virtual)与基类Draw构成覆盖关系
int m_rx;
int m_ry;
};
class Circle:public Shape{
public:
virtual void Draw(){ cout << "Circle:Draw" << endl; }// // 虚函数 (编译器不补充virtual),与基类Draw构成覆盖关系
int m_radius;
};
int main(void){
cout<<"-----利用对象调用普通成员函数------"<<endl;
//利用对象调用非虚成员函数,哪个类对象就调用哪个类的非虚成员函数(自恰性)
Shape s;s.Draw();Rect r;r.Draw();Circle c;c.Draw();
cout << "-----利用指针调用虚成员函数------" << endl;
Shape* ps = &s;
ps->Draw(); // Shape::Draw
ps = &r;
ps->Draw(); // Rect:Draw
ps = &c;
ps->Draw(); // Circle:Draw
// 根据 指针 指向的 对象的类型 来确定到底调用 哪个类的Draw
return 0;
}
3、多态
- 如果子类提供了对基类虚函数的有效覆盖,那么通过一个基类型指针(指向子类对象),或者基类型引用(引用子类对象),调用该虚函数,实际被调用的将是子类中的覆盖版本,而非基类中的原始版本,这种现象称为多态。
- 多态的重要意义在于,一般情况下,调用哪个类的成员函数是由指针或引用本身的类型决定的,而当多态发生时,调用哪个类的成员函数是由指针或引用的实际目标对象的类型决定的。
3.1 多态的条件
- 基类必须要有 虚函数,子类必须提供覆盖版本
- 必须利用 基类类型指针(必须指向子类对象)调用 虚函数
- 必须利用 基类类型引用(必须引用子类对象)调用 虚函数
当具备上述条件时,多态才可以表现(最终调用的为子类覆盖版本虚函数)
3.2 this指针和多态
调用虚函数的指针也可以是基类中的this指针,同样能满足多态的条件,但在构造和析构函数中除外
class Shape{
public:
void foo(){
cout << "foo中构造的为";
this->Draw();
}
Shape(){
cout << "构造中调用的版本为" ;
this->Draw();
}
~Shape(){
cout << "析构中调用的版本为";
this->Draw();
}
virtual void Draw(){ cout << "Shape:Draw" << endl; } // 虚函数
};
class Rect:public Shape{
public:
void Draw(){ cout << "Rect:Draw" << endl; } // 虚函数 (编译器补充virtual)与基类Draw构成覆盖关系
};
int main(void){
Rect d;
d.foo();
return 0;
}
3.3 虚函数表
class A{
public: // 编译器根据A类信息,将制作一张虚函数表A::foo的地址 A::bar的地址
virtual void foo(){ cout << "A::foo " << endl; }
virtual void bar(){ cout << "A::bar " << endl; }
};
class B: public A{
public:// 编译器根据B类信息,将制作一张虚函数表B::foo的地址 A::bar的地址
void foo(){ cout << "B::foo" << endl; }
};
int main(void){
A a;// |虚表指针|
cout << "基类对象a的大小:" << sizeof(a) << endl;// 8
B b;// |虚表指针|
cout << "基类对象b的大小:" << sizeof(b) << endl;// 8
void(**p)() = *((void(***)())&a);
p[0]();// A::foo
p[1]();// A::bar
void(**q)() = *((void(***)())&b);
q[0]();// B::foo
q[1]();// A::bar
return 0;
}
- 动态绑定
当编译器看到通过指针或引用调用虚函数的语句时,并那么通过一个不急于生成有关函数调用的指令,相反它会用一段代码替代该语句,这段代码在运行时才能被执行,完成如下操作:
(1) 确定指针或引用的目标对象所占内存空间
(2) 从目标对象所占内存空间中找到虚表指针
(3) 利用虚表指针找到虚函数表
(4) 从虚函数表中获取所调用虚函数的入口地址
(5) 根据入口地址调用该函数
int main(void){
C07_A a;// |虚表指针|
C07_B b;// |虚表指针|
A *pa = &b;
pa->foo();
//1.根据pa找到b对象所占内存空间
//2.从b对象所占内存空间中获取 虚表指针
//3.利用 虚表指针 找到编译器根据B类信息制作的虚函数表
//4.从虚函数表中获取虚函数的入口地址
//5.利用 函数入口地址调用函数
return 0;
}
- 动态绑定对性能的影响
-虚函数表本身会增加进程内存空间的开销
-与普通函数调用相比,虚函数调用要多出几个步骤,会增加运行时间的开销
-动态绑定会妨碍编译器通过内联来优化代码,虚函数不能内联
-只有在确实需要,多态特性的场合才使用虚函数,否则尽量使用普通函数
4、纯虚函数(抽象方法)
- 形如
class 类名 {
virtual 返回类型 函数名 (形参表) = 0;
};
- 抽象类
- 拥有纯虚函数的类称为抽象类
- 抽象类不能实例化为对象
- 抽象类的子类如果不对基类中的全部纯虚函数提供有效的覆盖,那么该子类就也是抽象类
- 纯抽象类
- 全部由纯虚函数构成的抽象类称为纯抽象类或接口
class _C{ // 抽象类
public:
virtual void foo() = 0;// 纯虚函数
};
class D :public C{
public:
void foo(){}
};
int main(void){
// C07_C C;;
// new C07_C;
return 0;
}
5、运行时类型信息(RTTI)
5.1、动态类型转换
- 动态类型转换(dynamic_cast)
- 用于将基类类型的指针或引用转换为其子类类型的指针或引用,前提是子类必须从基类多态继承(即基类包含至少一个虚函数)
- 动态类型转换会对所需转换的基类指针或引用做检查,如果其指向的对象的类型与所要转换的目标类型一致,则转换成功,否则转换失败。
- 针对指针的动态类型转换,以返回空指针(NULL)表示失败,针对引用的动态类型转换,以抛出bad_cast异常表示失败。
class C07_AA{
};
class C07_BB :public C07_AA{};
class C07_CC :public C07_BB{};
class C07_DD{};
int main(void){
C07_BB b;
C07_AA* pa = &b; // B* -> A*(子类类型指针--> 基类类型指针)
cout << "-------dynamic_cast---------" << endl;
C07_BB* pb = dynamic_cast<C07_BB*>(pa);//A*.->B*(基类类型指针->子类类型指针),能够转换的前提是多态继承
// pa->b对象所占内存空间->虚表指针->编译器根据B类信息制作的虚函数表->"B"
cout <<"A*pa--->B*pb:" << pb <<endl;
C07_CC* pc = dynamic_cast<C07_CC*>(pa);//A*.->C*(基类类型指针->子类类型指针)// 失败
cout << "A*pa--->C*pc:"<<pc <endl;
C07_DD*pd = dynamic_cast<C07_DD*>(pa);// 失败
cout <<"A*pa--->D*pd:" << pd <<endl;
cout << "-------static_cast---------" << endl;
pb = static_cast<C07_BB*>(pa);
cout << "A*pa--->B*pb:" << pb << endl;
pc = static_cast<C07_CC*>(pa);
cout << "A*pa--->C*pc:" << pc << endl;
return 0;
}
运行结果:
5.2 typeid操作符
- typeid操作符
- # include <typeinfo>
- 返回type_info类型对象的常引用
- type_info类的成员函数name(),返回类型名字符串
- type_info类支持“==”和“!=”操作符,可直接用于类型相同与否的判断
- 当其作用于基类类型的指针或引用的目标对象时
- 若基类不包含虚函数 typeid所返回类型信息由该指针或引用本身的类型决定
- 若基类包含至少一个虚函数,即存在多态继承,typeid所返回类型信息由该指针或引用的实际目标对象的类型决定
//typeid:操作符:可以获取对象的类型信息,但是无法获取对象本身的常属性信息☐
int main(void){
int n = 10;
const type_info& rt = typeid(n);
//1.获取的类型信息(类名,类版本,类大小,·,)
//2.定义一个type1nfo类对象
//3.将第一步获取的类型信息保存到第二步创建的type1nfo类对象的各个"私有"成员变量中
//4.返回这个type_info类对象的常引用
cout << rt.name() << endl;
C07_BB b;
C07_AA* pa = &b;
C07_AA& a = b;
cout << typeid(*pa).name() << endl;// // pa->b对象所占内存空间->虚表指针->编译器根据B类信息制作的虚函数表->"B"
cout << typeid(a).name() << endl;
return 0;
}
6、虚析构函数
class A{
public :
A():m_f(open("./cfg1",O_CREAT|O_RDWR,0644)){
cout<<"A() 打开了文件"<<endl;
}
~A(){
// 释放m_f本身所占的内存空间
close(m_f);
cout<<"~A()"<<endl;
}
private:
int m_f;
};
class B:public A{
public:
B():m_b(open("./cfg2",O_CREAT|O_RDWR,0644)){
cout<<"B()"<<endl;
}
~B(){
close(m_b);
cout<<"~B()"<<endl;
}
private:
int m_b;
};
int main(){
A* b=new B;
delete b;// 释放B类所占内存
return 0;
}
运行结果:
- delete一个基类指针(指向子类对象)
- 实际被调用的仅仅是基类的析构函数
- 基类的析构函数只负责析构子类对象中的基类子对象
- 基类的析构函数不会调用子类的析构函数
- 在子类中分配的资源将无法得到释放
- 解决办法:将基类的析构函数变成虚函数
class A{
public :
A():m_f(open("./cfg1",O_CREAT|O_RDWR,0644)){
cout<<"A() 打开了文件"<<endl;
}
~A(){
// 释放m_f本身所占的内存空间
close(m_f);
cout<<"~A()"<<endl;
}
private:
int m_f;
}
运行结果
-
如果将基类的析构函数声明为虚函数,那么实际被调用的将是子类的析构函数
-
子类的析构函数将首先释放子类对象自己的成员,然后再调用基类的析构函数释放该子类对象的基类部分,最终实现完美的资源释放
虚析构函数的作用:当用户delete一个基类类型指针(指向子类对象),能够正确调用子类的析构函数 -
空虚析构函数
- 没有分配任何动态资源的类,无需定义析构函数
- 没有定义析构函数的类,编译器会为其提供一个缺省析构函数,但缺省析构函数并不是虚函数
- 为了保证delete一个指向子类对象的基类指针时,能够正确调用子类的析构函数,就必须把基类的析构函数定义为虚函数,即使它是一个空函数
任何时候,为基类定义一个虚析构函数总是无害的,一个类中,除了构造函数和静态成员函数外,任何函数都可以被声明为虚函数
总结 函数的几种关系
隐藏:基类和子类中 有 原型相同的普通(非虚)成员函数
覆盖:基类和子类中 有 原型相同的虚成员函数