抽象类
纯虚函数
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。
class A
{
public:
virtual void func() = 0;
};
纯虚函数不需要写函数的定义,他有类似声明一样的结构。
抽象类概念
我们把具有纯虚函数的类,叫做抽象类。
所谓抽象就是,不符合常理的,偶然的,和平常的类不太一样的类。抽象一般是我们人自己想出来的形象,在现实世界当中没有对应的实体,同样的抽象类在现实世界当中没有对应的实体。
所以,以上描述就引出了抽象类最大的特点:抽象类不能实例化对象。
而且,如果某一个子类的父亲是一个抽象类,那么这个子类也是不能实例化对象的,因为子类继承了父类当中的纯虚函数。那么子类就包含了纯虚函数,主要是包含纯虚函数的类就是抽象类。
那么,怎样才能让上述的 B 子类能够 实例化对象呢?我们只需要在 B 当中重写一些 A 当中的纯虚函数就可以 让 子类(B类)实例化对象了。
// 这是一个抽象类
class A
{
public:
virtual void func() = 0;
};
// 重写之后就可以 实例化B 对象了
class B : public A
{
public:
virtual void func()
{
cout << "B::func()" << endl;
}
};
int main()
{
B b;
return 0;
}
使用抽象类作为父类,实现出来的子类的多态使用方式,是和普通的一样的。只不过在 父类 当中不在有虚函数表,因为 父类是抽象类,抽象类不能实例化对象,也就不存在虚函数表了。
抽象类当中的多态 和 普通父类的多态在这里多做了一件什么事情呢?
- 我们知道,父类当中的虚函数,如果子类不进行重写,那么这个虚函数是没有任何作用的;而在抽象类当中,就强制其子类要对抽象类当中的纯虚函数进行重写。
- 而且这里是 间接性的 强制重写,和之前我们提到的 override 检车重写是不一样的。而且 override 是修饰在派生类当中的,抽象类强制重写,语法是建立在父类当中的。
多态当中需要注意的点
- inline(内联)函数能不能是虚函数?可以,但是,当一个 inline函数 称为虚函数之后,这个函数就不在是inline函数了,因为虚函数需要把地址放到虚表当中,而且内联函数是没有地址的。
- 静态成员函数不能是 虚函数。如果 把 static 和 virtual 放在一起修饰的话,编译器会直接报错。这是因为,静态成员函数没有 this 指针,静态成员函数是使用 类型::成员函数名 的方式调用的,这样的方式是不能访问到对象当中的虚表的,所以静态成员函数是无法放进虚表的;还有个解释是,静态成员函数是属于类的,不是属于对象的,而只有实例化对象出来之后才能构造处虚表。
- 构造函数不能是虚函数,如果在构造函数前加 virtual 修饰,就会报错。虚函数表是在编译的时候生成的,而指向虚表的指针是在 构造函数初始化列表的最端初始化的,初始化之后才会指向虚函表, 初始化之前都是随机值。那么,如果构造函数成为了虚函数,而虚函数的作用就是多态,那么在构造函数调用前,虚函数表指针都没有初始化,怎么去找这个构造函数的虚函数呢?所以,构造函数是不能成为虚函数的。
- 析构函数可以是虚函数。而且,我们最好把父类当中的析构函数定义为虚函数,让子类进行重写。比如这个例子(A是父类,B是子类):A* p = new B; delete p; 此时,因为 p 指针的类型是 父类A的类型,所以,在调用 detele 的时候,所调用的析构函数就会是 父类 A 的析构函数,那么就会出现问题,但是如果 重写之后,就会调用子类的析构函数。
- 一个类当中调用普通函数快还是调用虚函数快?如果是普通对象,一样快;如果是指针函数或引用函数,普通函数快。因为此时构成多态。但是,其实两者之间调用时间,差别不大。