我们之前已经了解到了C++的两大特性:封装,继承,今天我们来了解一下C++的第三个特性——多态。
多态的概念
什么叫多态呢?就是同一件事,不同的人做,有不同的结果。
比如说买票,如果是一般人去买票,可能就是全价,如果是学生,军人,老师的话,买票就是半价。
好了,简单了解一下多态的概念之后,我们得了解一下,C++实现多态靠的是什么?
虚函数
虚函数的定义很简单:虚函数:即被virtual修饰的类成员函数被称为虚函数。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl;} //虚函数
};
C++实现多态,靠的就是这个虚函数。
虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public 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 P;
Func(P);
Student S;
Func(S);
}
我们运行来看看:
你看,我们用P(Person类)和S(Student类)去调用,得到的结果是不一样的,这就是多态。
但是我如果我们将Func中的Person引用去掉:
void Func(Person P)
{
P.BuyTicket();
}
那就不能形成多态:
这里就要讲到形成多态的条件了:
多态的条件
1 . 必须通过基类的指针或者引用调用虚函数(必须用父类的指针或者父类的引用)
2 . 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
为了大家了解更清晰,我把重写的概念也放在这:
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
但是大家注意一下,重写有个例外:
协变:返回值可以不同,但必须是父类关系的指针或引用。
class A
{
};
class B : public A
{
};
class Person
{
public:
virtual A* BuyTicket()
{
cout << "买票-全价" << endl;
return nullptr;
}
};
class Student : public Person
{
public:
virtual B* BuyTicket()
{
cout << "买票-半价" << endl;
return nullptr;
}
};
void Func(Person& P)
{
P.BuyTicket();
}
int main()
{
Person P;
Func(P);
Student S;
Func(S);
}
这样也是可以的:
还有,如果父类的同名函数已经有virtual修饰,那么派生类中的同名函数可以不用用virtual修饰。
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person
{
public:
void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
void Func(Person& P)
{
P.BuyTicket();
}
int main()
{
Person P;
Func(P);
Student S;
Func(S);
}
析构函数的多态
我们如果以对象来析构的话,析构函数本身没什么问题:
class Person
{
public:
~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
~Student()
{
cout << "~Student()" << endl;
}
};
int main()
{
Person P;
Student S;
}
但是如果我们用指针来接收,用delete释放空间,就会有一些问题:
class Person
{
public:
~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
~Student()
{
cout << "~Student()" << endl;
}
};
int main()
{
Person* P = new Person;
delete P;
P = new Student;
delete P;
}
我们发现这时候Student并没有去调用析构函数,因为这个时候编译器就是以类型去调用析构函数,既然都是Person的类型,那就只用调用Person的析构函数,这样会有内存泄漏的危险。那这是为什么呢?
我们在之前学习过delete的工作原理就是,会去调用析构函数,既然这里有多个对象,那么,我们也需要将析构函数声明为虚函数,实现多态。
class Person
{
public:
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
~Student()
{
cout << "~Student()" << endl;
}
};
int main()
{
Person* P = new Person;
delete P;
P = new Student;
delete P;
}
class Person
{
public:
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
~Student()
{
cout << "~Student()" << endl;
}
};
int main()
{
Person* P = new Person;
delete P;
P = new Student;
delete P;
}
这时候就没问题了:
C++11 override和final
这两个都是C++11新增的功能:
override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car
{
public:
virtual void Drive() {}
};
class Benz :public Car
{
public:
void Drive() override { cout << "Benz-舒适" << endl; }
};
int main()
{
Benz B;
}
这时候是可以正常运行的:
但是如果我把基类的virtual去掉,这时候就会编译报错:
final:修饰虚函数,表示该虚函数不能再被重写
抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。
class Car
{
public:
virtual void Drive() = 0; //纯虚函数
};
包含纯虚函数的类叫做抽象类(也叫接口
类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生
类才能实例化出对象。
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();
}
int main()
{
Test();
}
这个时候是可以初始化成功的:
但是,如果我们将下面的重写的函数注释掉,这时候就会编译报错:
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();
}
int main()
{
Test();
}
多态的原理
接下来我们来深度探究一下多态的原理:
在这之前,我们得先了解一个东西——虚函数表:
虚函数表
这里会有一道笔试题:sizeof(Base) 的大小是多少?
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
Base b;
}
我们打印出来看看:
这里我们看到是8,但是我们只有一个int,只占4个字节,那么另外4个字节是什么呢?我们打开监视窗口看一下:
我们发现,它好像多存了一个指针,** _vfptr **,对象中的这个指针我们叫做虚函数表指针(v代
表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数
的地址要被放到虚函数表中,虚函数表也简称虚表。
现在我们来看看这个表中存放了什么:
虚表嘛,顾名思义,就是存放虚函数的嘛~,那我们来看看,派生类中是否也会存在虚表和虚函数呢?
针对上面的代码我们做出以下改造
1.我们增加一个派生类Derive去继承Base
2 .Derive中重写Func1
3 .Base再增加一个虚函数Func2和一个普通函数Func3
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;
}
1 . 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分,是自己的成员。
2 . 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
3 . 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
4 . 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
5 . 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
这样我们应该可以猜测出来多态的原理了:因为不同的对象有不同的虚表,在基类函数完成重写的情况下,回到相应的虚表当中找到相应的重写函数。
拿上面的代码举例:Base想要调用Fun1( ),就去Base的虚表中找Fun1,Derive想要调用Fun1就去自己的虚表中找到自己的Fun1,这时候因为Fun1已经完成了重写,这时候 Derive就是调用的自己的Fun1。
单继承中的虚表函数
我们接下来研究派生类中的虚表:
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;
};
int main()
{
Base b;
Derive d;
return 0;
}
这个时候我们从监视窗口看看:
我们在Derive中只看到了两个函数,一个Derive的fun1,另一个是Base的fun2,诶?fun3和fun4呢?这个时候我们不禁生出了一个疑问?虚函数都会放在虚函数表中吗?。
我们打开内存查看一下虚函数表中到底存了几个虚函数:
我们看到内存中好像是存有4的地址,不过这四个地址是不是虚函数的地址,我们还不能确定。
为了保险起见我们可以先把这四个地址取出来:
这里又要和我们最痛苦的函数指针打交道了:
typedef void(*VEFT)(); //因为我们这里的函数都是void就定义void*的函数指针
void PrintVEFT(VEFT* V)
{
cout << "虚表地址:" << V << endl;
for (int i = 0; V[i] != 0; i++)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, V[i]);
VEFT f = V[i];
(*f)();//函数指针的调用
}
cout << endl;
}
这里提一下,在VS中虚函数的最后是一个0,所以我用V[i] != 0来判断是否达到虚函数表的最后。
(在Linux下可能不是这样实现的。)
好了,现在我们有打印虚函数表的函数了,那现在我们有一个问题:我该如何拿到虚函数表呢?
我们发现虚函数表通常都储存在最前面,而且是指针,一个指针不是4字节就是8字节,所以在x86平台下,我们完全可以将对象的地址强制转化成int*类型的地址,取出前四个字节的地址,这四个字节的地址就是我们虚函数表的地址。
运行一下:
我们发现是有四个函数的,那就说明一件事:监视窗口骗人!
那么虚函数都会存在虚函数表中吗?经过我们的验证证明:是的。
多继承中的虚函数表
前面我们研究了单继承的虚表函数,现在我们来研究一下多继承中的虚表函数:
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;
};
我们看到多继承当中,如果基类有虚表,那么派生类会继承基类的虚表。但是在监视窗口下,我们还是看不全虚表当中的全部内容,所以我们还是打印出来看看:
Derive d;
//第一个虚表内容
Base1* p1 = &d;
VEFT* table1 = ((VEFT*)(*(int*)(p1)));
PrintVEFT(table1);
//第二个虚表的内容
Base2* p2 = &d;
VEFT* table2 = ((VEFT*)(*(int*)(p2)));
PrintVEFT(table2);
我们发现派生类中没有被重写的虚函数会被放到第一张虚函数表中。