1.什么是多态
在继承的基础上,我们都知道派生类继承到了父类的成员变量和成员方法,并且派生类可以直接进行访问,前提是public继承,并且父类的成员变量也要是public属性,那么,如果一个父类有许多个子类,比如一个动物类作为父类,猫狗鱼鸟虫作为子类继承它,当父类中有一个属性为“类别”,那么这些派生类去初始化这个变量时会将这个变量在每个派生类中都初始化一次,并且每个变量的值都不一样,每个值对应着每个种类,这就叫做多态。
所以平移到C++中,多态简而言之就是多种形态,此处的多种形态代表了派生类的多种形态,而这些派生类的公共形态,就存于父类中,并且在每个派生类中都有一份。
在C++中,多态也分为两种,一种是静态的多态,也叫做编译时多态,像是函数重载,模板,不同的参数调用会有不同的操作,返回不同的结果,还有一种叫做动态的多态,也就是类中的多态,这种多态又叫做运行时多态,就是今天要讲的。
class Person
{
public:
virtual void BuyTicket()
{
cout << "全价买票" << endl;
}
};
class Student :public Person
{
public:
//子类中满足三同(函数名、参数、返回值)的虚函数,叫做重写(覆盖)
virtual void ButTicket()
{
cout << "半价买票" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps); // 全价买票
Func(st); // 半价买票
return 0;
}
在C++中,如果两个类构成继承关系,并且在用父类的指针或引用去调用了子类,这时在语法层面就构成了多态,像上面的程序中,Func函数中的BuyTicket()接口看似是父类在调用,可实际如果在主函数中传入父类指针是调用父类的接口,传入子类的指针就会调用子类的接口,从而实现多态。
2.虚函数
在C++中,被virtual关键字修饰的函数叫做虚函数,在继承中我们说过,对于菱形继承这种会使代码冗余的操作,我们使用虚函数会进行很好的优化,即在孙子类中,自己父类和爷爷类的重复变量和函数只会存一份,也就是说虚函数会将重写的函数只初始化一份放在虚函数表中,
在C++的多态中,如果子类的一个虚函数与父类中的虚函数函数名、参数、返回值全部相同,那么就称子类的虚函数重写了基类的虚函数。
注意:派生类在继承下父类的虚函数后,即使不将自己重写的函数声明为虚函数,这个函数依旧为虚函数,因为它继承了父类虚函数的特性,虽然这种方式在语法上没错,但是不建议这么写。
那么类中的析构函数能否为虚函数呢?在C++中,派生类如果继承下了父类的析构函数,并且父类已经将析构函数声明为虚函数,此时无论子类是否在析构函数前面加vitrual,派生类的析构函数与父类的析构函数依然构成重写,那么即使是看起来并不满足名字参数返回值相同的条件,为什么还依然构成重写呢?其实是在编译器内部,析构函数会被特殊处理,就类似于C++的new关键词,其实内部是调用了operator new()和构造函数,所以析构函数也是一样被特殊处理成了destructor,意为销毁器,所以可以理解为底层父类子类调用的函数名字是一样的。
//析构函数是虚函数,是否构成重写 - 构成
//析构函数名被特殊处理成了destructor
class Person
{
public:
virtual ~Person() { cout << "~Person" << endl; }
};
class Student :public Person
{
public:
virtual ~Student() { cout << "~Student" << endl; }
};
int main()
{
//普通对象,析构函数是否为虚函数,是否完成重写,都正确调用了
//Person p;
//Student s;
Person* p1 = new Person; // operator new + 构造函数
Person* p2 = new Student;
// 析构函数 + operator delete
// p1->destructor()
delete p1;
// p2->destructor()
delete p2;
//动态申请了对象,如果给了父类的指针管理,那么需要析构函数是虚函数
return 0;
}
3.final和override
可以看出在C++中对于重写的要求还是很严格的,所以C++也别制定了两个关键字,专门用来“监督”类和类中函数的重写情况。
//多态:不同类型的对象,去完成同一件事情,结果不同
//final override
//设计一个不可以被继承的类
class A final
{
private:
A(int a = 0)
:_a(a)
{}
public:
static A CreateOBJ(int a = 0)
{
return A(a);
}
protected:
int _a;
};
//间接限制,子类构造函数无法调用父类构造函数初始化成员,没办法实例化对象 - C++98
//直接限制 - C++11
class B ://public A
{
protected:
};
class C
{
public:
virtual void f() //final - 限制不可以被重写
{
cout << "C::fun()" << endl;
}
};
class D :public C
{
public:
virtual void f()
{
cout << "D::fun()" << endl;
}
};
所以final关键字就是将父类的函数声明为不可重写,也就是说这个方法派生类无法继承下去,也就间接将此类设置了无法被继承的类。
class Car
{
public:
virtual void Drive()
{}
};
//override放在子类重写的虚函数的后面,检查是否完成重写
//没有重写就会报错
class Benz :public Car
{
public:
virtual void Drive() override
{
cout << "cozy" << endl;
}
};
int main()
{
A aa = A::CreateOBJ(10);
return 0;
}
override关键字就是用来检查派生类必须要重写的函数是否完成了重写,如果没有,则会报错。
了解过重写后,结合上次说过的隐藏和C++中的重载概念,一起来做个对比吧。
1.重载:两个函数要在同一个作用域 && 函数名/参数要相同。
2.重写(隐藏):两个函数分别在基类和派生类的作用域 && 函数名/参数/返回值都必须相同(析构函数除外) && 两个函数都必须是虚函数。
3.隐藏(重定义):两个函数分别在基类和派生类的作用域 && 函数名相同 && 两个基类和派生类的同名函数不构成重写就是重定义。
4.抽象类
在C++中,如果一个类实现了一个虚函数,并且在此虚函数后面加上了=0,就代表此虚函数是纯虚函数,那么包含纯虚函数的类就叫做纯虚类,纯虚类不能初始化出对象,派生类继承其后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象,纯虚函数规范了派生类必须要对其进行重写。
class Car
{
public:
//纯虚函数只声明不实现,实现没有价值,因为包含纯虚函数的类不能实例化出对象
virtual void Drive() = 0;
//virtual void Drive() = 0
//{
// cout << "virtual void Drive() = 0" << endl;
//}
void f()
{
cout << "void fun()" << endl;
}
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "virtual void Drive()" << endl;
}
};
int main()
{
Car* c = new Benz;
c->Drive();
c->f();
return 0;
}
所以在C++中,普通函数的继承是一种实现继承,派生类继承了基类的实现,想得到的就是父类函数的功能,而虚函数的继承是一种接口继承,派生类继承的是基类的接口,然后自己进行重写,从而达成多态,所以如果不实现多态,就不要把函数定义为虚函数。
5.虚函数表
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
在C++中,此类的大小为8bytes,除了_b是int类型占4字节意外,还会观察到有一个指针名为_vptr,此指针就是用来存储虚函数的指针,在底层叫做虚函数表。
一个含有虚函数的类都有一个虚函数表指针,因为虚函数地址要被放到虚函数表中,虚函数表也成为虚表,那么在构成多态的派生类中此虚表中又存了什么呢?
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
通过测试发现,Drive类中有两类函数,一类是自己本身就有的,还有一类继承Base的。
而基类Base和派生类Drive的虚函数表是不一样的,由于这里的Func1()完成了重写,所以Drive类中的虚表存的是Drive::Func1(),也就是重写后的Func1(),所以重写也叫做覆盖,覆盖就是指虚函数表中对于虚函数的覆盖,重写是语法层的叫法,覆盖是物理层实现的做法。
Func2()继承下来后是虚函数,放进了Drive类的虚表,但是因为Drive类并没有对Func2()重写,所以Func2()还是基类的,Func3()由于并不是虚函数,所以根本不会放进虚表中。
虚表的本质是一个指针数组,本质是数组,用来存放指向函数的指针,所以一般情况下这个数组最后都加了一个nullptr。
因为C++的重写是针对派生类通过重写本身的虚函数来对继承下来的父类的虚函数进行覆盖,从而构成多态,所以多态的原理也就不难发现,其实就是:基类的指针/引用,指向谁,就去谁的虚函数表里找到对应位置的虚函数进行调用。
class Person
{
public:
virtual void BuyTicket()
{
cout << "全价买票" << endl;
}
protected:
int _a = 0;
};
class Student :public Person
{
public:
virtual void BuyTicket()
{
cout << "半价买票" << endl;
}
protected:
int _b = 0;
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student John;
Func(John);
//多态的原理:基类的指针/引用,指向谁,就去谁的虚函数表里找到对应位置的虚函数进行调用
return 0;
}