一.抽象类
如果在虚函数的后面加上=0,并且不进行实现,这样的虚函数就叫做纯虚函数.
而包含纯虚函数的类,也叫做抽象类或者接口类.
抽象类不能实例化出对象,因为它具有的信息不足以描述一个对象,派生类继承后也只有在重写纯虚函数后才能实例化出对象.
抽象类就像是一个蓝图,为派生类描述好一个大概的架构,派生类必须实现完这些架构,至于要在这些架构上做些什么,增加什么,都是派生类自己的问题.
class preson {
virtual void print() = 0;
};
class student :public preson {
virtual void print() {
cout << "I am a student" << endl;
}
};
class teacher :public preson {
virtual void print() {
cout << "I am a teacher" << endl;
}
}
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现.
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口.所以如果不实现多态,不要把函数定义为虚函数.
二.多态的原理
虚函数表
class preson {
public:
virtual void print() {
cout << "preson" << endl;
}
virtual void test1() {
cout << "1test1" << endl;
}
void test2() {
cout << "1test1" << endl;
}
int _age;
};
class student :public preson{
public:
virtual void print() {
cout << "student" << endl;
}
void test() {
cout << "2test2" << endl;
}
int _stuNum;
};
int main() {
preson p;
cout << sizeof(p) << endl;
return 0;
}
查看对象p的大小后发现为8个字节,和实际想象的只有一个变量_age大小应该为4字节不一致.
由上图可以看到对象p里面处理变量_age以外,还有个指针_vfptr.这个指针指向了一个函数指针数组,这个函数指针数组也就是虚函数表,其中的每一个成员指向的都是虚函数,这个_vfptr被称为虚函数表指针.
多态的实现也正是借助这个虚函数表
观察上面的虚函数表可以得知,如果派生类实现了某个虚函数的重写,那么在派生类的虚函数表中,重写的虚函数就会覆盖掉原有的虚函数,如:preson::print被替换为student::print.而没有完成重写的preson::test1则依旧保留在子类的虚函数表中.
总结: 派生类会继承基类的虚函数表,如果派生类完成了重写,则会将重写的虚函数覆盖掉原有的函数.所以指针或引用指向哪一个对象,就要调用对象中虚函数表中对应位置的虚函数,来实现多态.
为什么必须要通过指针或者引用才能构成多态?
如果将派生类对象赋值给基类对象,会因为对象分割,导致它的内存布局整个被修改,完全转换为基类对象的类型,虚函数表也与基类相同,所以不能实现多态.
而如果使用基类指针或者引用指向派生类对象,虽然指向的是派生类对象,但是它们的内存布局是兼容的,它不会像赋值一样改变派生类对象的内存结构,所以派生类对象的虚函数表得到了保留,所以它可以通过访问派生类对象的虚函数表来实现多态.
单继承中: 派生类虚函数表的生成过程:
- 首先派生类会将基类的虚函数表拷贝过来
- 如果派生类完成了对虚函数的重写,则用重写后的虚函数覆盖掉虚函数表中继承下来的基类虚函数
- 如果派生类自己又新增了虚函数,则添加到虚函数表的最后面
多继承中: 派生类虚函数表的生成过程:
- 首先派生类会将所有基类的虚函数表拷贝过来
- 如果派生类完成了对虚函数的重写,则用重写后的虚函数覆盖对应的基类虚函数
- 如果派生类自己又新增了虚函数,则添加到第一个基类拷贝过来的虚函数表后面
虚函数表的存储位置
虚函数存在于虚函数表中,虚函数表又存储在哪里呢?
虚函数表指针存在于对象当中,虚函数存在于虚函数表中,虚函数表存在于代码段(编译阶段生成)
动态绑定和静态绑定
对象的静态类型: 对象在声明时采用的类型.是在编译期确定的.
对象的动态类型: 目前所指对象的类型,是在运行期决定的.对象的动态类型可以更改,但是静态类型无法更改.
静态绑定: 绑定的是对象的静态类型,某特性(比如函数)依赖于对象的静态类型,发送在编译期.
动态绑定: 绑定的是对象的动态类型,某特性(比如函数)依赖于对象的动态类型,发送在运行期.
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态.比如:函数重载
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称之为多态
常见问题解答:
- 内联函数可以是虚函数吗?
不可以,内联函数没有地址,无法放入到虚函数表中.
- 静态成员函数可以是虚函数吗?
不可以,静态成员函数没有this指针,无法访问虚函数表.
- 构造函数可以是虚函数吗?
不可以,虚函数表指针也是对象的成员之一,是在构造函数初始化列表初始化时才生产的,不可能是虚函数.
- 析构函数可以是虚函数吗?
可以,最好将基类的析构函数声明为虚函数,防止使用基类指针或者引用指向派生类对象时,派生类的析构函数没有调用,可能会导致内存泄漏.
- 对象访问虚函数快还是普通函数快?
如果不构成多态,虚函数和普通函数的访问是一样快的.但是如果构成多态,调用虚函数就得到虚函数表中查找,就会导致速度变慢,所以普通函数更快一点.