多态就是多种形态,不同的对象去完成会产生不同的效果!
函数重载,看起来调用同一个函数,却有着不同的行为
原理:编译时实现
一个父类对象的引用或者指针去调用同一个函数,传递不同的对象,会调用不同的函数
原理:运行时实现
被virtual关键字修饰的类成员函数就是虚函数
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
虚函数的重写也叫做覆盖:子类中有一个与父类完全相同的虚函数,他们两个的虚函数满足三同:返回值,函数名,参数列表完全相同,则称子类的虚函数重写了父类的虚函数。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);//这行传参会完成切片
return 0;
}
这里就完成了我们的多态,如果传参传的是父类对象,就会调用父类的函数;如果传参传的是子类对象,就会调用子类的函数。

本质:不同的人做相同的事情,结果不同!
注意:
//不建议这样使用
void BuyTicket() { cout << "买票-半价" << endl; }

1️⃣构成多态,跟p类型没有关系,传参传的是哪个类型的对象,调用的就是哪个类型对象中的虚函数——(跟对象有关)
2️⃣不构成多态,调用的就是p类型函数——(跟类型有关)
void Func(Person p)//此处不是指针或者引用
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
结果:

这里就没有构成多态了!


虚函数重写的两个例外:
class Person {
public:
virtual Person* BuyTicket() { cout << "买票-全价" << endl; return nullptr; }
};
class Student : public Person {
public:
virtual Student* BuyTicket() { cout << "买票-半价" << endl; return nullptr; }
};
destructorclass Person {
public:
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
Person p;
Student s;
return 0;
}
结果:

下面再来看看这个代码:
class Person {
public:
~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
~Student() { cout << "~Student()" << endl; }
};
int main()
{
Person* p1=new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
这份两个函数并没有构成多态,我们析构的时候,想要父类调用父类的析构,而子类调用子类的析构,而并没有构成多态,就会跟类型有关,p1 p2都是person类型的,都会去调用子类的析构函数。万一子类对象中有些动态开辟的资源,没有被释放,就会很危险!

构成多态即可解决这个问题!重写里面的虚函数,加上virtual关键字:

1.final
限制类的继承
修饰虚函数,表示该虚函数不能被重写
如何设计一个类无法被继承?
可以父类的构造函数私有(private)
class A
{
private:
A(int a)
:_a(a)
{}
public:
static A CreateObj(int a=10)
{
return A(20);//创建一个对象出来
}
protected:
int _a = 10;
};
// 间接限制,子类的构造函数无法调用父类的构造函数初始化,没办法实例化出对象,因为是private私有权限
class B :public A
{
};
int main()
{
A aa=A::CreateObj(100);
// A类的构造函数是私有的,类内能用,但是在类外却不能实例化
// 所以调用一个公共的函数接口,来实例化
return 0;
}
以上的方法太过复杂,C++11新增加了一个语法,final关键字
// 直接限制
class A final
{
protected:
int _a = 10;
};
class B :public A
{
};
限制虚函数的重写
class C
{
public:
virtual void f() final
{
cout << "C::f()" << endl;
}
};
class D :public C
{
public:
virtual void f()
{
cout << "D::f()" << endl;
}// 无法重写final函数
};
2.override
放在子类重写的虚函数的后面,检查是否完成重写,没有重写就报错
class Car {
public:
virtual void Drive() {}
};
class Benz :public Car {
public:
virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
特别爱考

虚函数的后面写上=0,则这个函数就是纯虚函数。包含纯虚函数的类就叫做抽象类(也叫接口类),抽象类不能实例化出对象。纯虚函数一般只声明,不实现,因为没有价值。
子类继承后也是不能实例化出对象的,只有重写纯虚函数。纯虚函数规范了子类必须重写,另外纯虚函数更体现了接口继承。
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
void Test()
{
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
}
为什么这里的指针可以调用?而对象调用就会崩溃?
要用后面的原理来解释!
对比纯虚函数与override
1. 纯虚函数的类,本质上强制子类去完成虚函数的重写
2. override只是语法上检查是否完成重写
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,从而达成多态,所以如果不实现多态,不要把函数定义成虚函数。
常考笔试题:sizeof(Base)是多少呢?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
char c = 'A';
};
int main()
{
cout << sizeof(Base) << endl;// 12
return 0;
}
我们观察发现是12字节!除了显示的对象成员(_b,c)以外,还有一个_vfptr放在对象的前面,(有了虚函数,就会多存在这个指针)这个指针我们叫做虚函数表指针,简称虚表指针——virtual function pointer

一个含有虚函数的类中,都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中。
如下:含有多个虚函数

具体展示:

原理:

模型解析:我们可以看到,student有两部分构成,一部分是从父类person中继承的,另外的是自己剩下的一部分。并且父子的虚表是各不相同的,分别存储了自己的虚函数,其中子类继承的虚函数如果重写了,就会被覆盖。
多态原理总结:基类的指针或者引用,调用谁,就去谁的数函数表中找到对应的位置的虚函数,就实现了对应的多态的功能。
如上述所示:
传递的是Mike对象,就直接去父类虚表中寻找该虚函数的地址
传递的是Johnson对象,先会完成切片操作,相当于现在是子类中父类那部分对象的别名,再去子类中继承自父类中去寻找该类中重写的虚函数的地址,进而完成调用,实现多态。
那么为什么必须是父类的指针或者引用?对象不行呢?
我们知道普通的函数调用,都是编译时确定地址,而虚函数的调用是运行时确定地址。
传入的是对象:就是传入的值,是值拷贝
我们再来看看对象的引用r1和对象本身p,切片过后内存布局,如下图所示:
我们注意到,引用的r1其实就是Johnson中继承自分类那部分对象的别名,里面虚表是一样的
对象切片的时候,我们会把值(注意_a变量)给拷贝过去,不会把子类的虚表指针给赋值过去,如果赋值成功,父类的虚表指针会指向子类的虚表,那么多态就不可能实现了。

易错:同类型的对象,虚表指针是相同的,指向同一张虚表
如下所示,三个同类型的person对象都指向同一张虚表。

易错:普通函数和虚函数的存储位置是否一样?
答案:一样的!都是在代码段
只不过虚函数要把地址存一份到虚表中,方便实现多态
注意:如果子类的虚函数是私有的,那么也是能够实现多态的。
从语法上来看,重写是一种接口继承,继承父类的接口,重写函数体内容,编译器检查不出来,因为是运行时,去p指向对象的虚表中,找到虚函数的地址,所以私有的限制不起作用。

总结:
虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
派生类的虚表生成:
a.先将父类中的虚表内容拷贝一份到子类虚表中
b.如果子类重写了父类中某个虚函数,用子类自己的虚函数覆盖虚表中父类的虚函数
c.子类自己新增加的虚函数按其在子类中的声明次序增加到子类虚表的最后。

我们观察虚函数的地址时,发现虚表中存放的地址和函数真实的地址是不一样的,那是因为VS对此作了处理

函数重载多态。class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
class Derive :public Base {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
virtual void func4() { cout << "Derive::func4" << endl; }
private:
int b;
};
typedef void(*VF_PTR)(); // 等价于 typedef void(*)() VF_PTR;函数指针比较特别
void PrintVTable(VF_PTR* table)// void PrintVTable(VF_PTR _table[])
{
for (int i = 0; table[i] != nullptr; i++)
{
printf("vft[%d]:%p\n",i,table[i]);
}
}
int main()
{
Base b;
PrintVTable((VF_PTR*)(*(int*)&b));
Derive d;
return 0;
}
思路:取出b对象的头4bytes,就是虚表的指针

// 多继承中的虚表
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
typedef void(*VF_PTR)(); // 等价于 typedef void(*)() VF_PTR;函数指针比较特别
void PrintVTable(VF_PTR* table)// void PrintVTable(VF_PTR _table[])
{
for (int i = 0; table[i] != nullptr; i++)
{
printf("vft[%d]:%p\n",i,table[i]);
}
}
int main()
{
Base1 b1;
Base2 b2;
Derive d;
PrintVTable((VF_PTR*)(*(void**)&d));
PrintVTable((VF_PTR*)(*(void**)((char*)&d+sizeof(Base1))));
return 0;
}

实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用。