一、前言
多态作为C++三大特性之一,在C++中具有十分重要的作用。
那么今天我们就来讲一讲多态。
二、多态的概念
多态:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。为不同数据类型的实体提供统一的接口。
举个例子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。
三、虚函数
虚函数:即被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; } }
注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性)。但是为了规范,建议加上。
五、多态的定义与实现
1、定义
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。
在继承中要构成多态还有两个条件:
1、必须通过父类的指针或者引用调用虚函数。
2、 子类必须对父类的虚函数进行重写。
2、实现
class Person { public: virtual void BuyTicket() { cout << "买票——全价" << endl; } }; class Student :public Person { public: virtual void BuyTicket() { cout << "买票——半价" << endl; } }; class Soldier :public Person { public: virtual void BuyTicket() { cout << "优先买票" << endl; } }; void Func(Person& p) { p.BuyTicket(); } int main() { Person p; Student s; Soldier sd; Func(p); Func(s); Func(sd); return 0; }
代码的运行结果如下:
3、构成多态条件的理解
必须通过父类的指针或者引用调用虚函数。如下:
我们发现如果不是父类的引用的话就无法构成多态了。
子类必须对父类的虚函数进行重写。
class Person { public: virtual void BuyTicket() { cout << "买票——全价" << endl; } }; class Student :public Person { public: }; class Soldier :public Person { public: }; void Func1(Person& p) { p.BuyTicket(); } int main() { Person p; Student s; Soldier sd; Func1(p); Func1(s); Func1(sd); return 0; }
运行结果如下:
从上面我们可以看出要实现多态就必须在子类中重写虚函数。
六、协变
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。如下图:
class A {}; class B : public A {}; class Person { public: virtual A* f() {return new A;} }; class Student : public Person { public: virtual B* f() {return new B;} };
七、抽象类
在虚函数的后面写上 =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; } };
八、实现继承与接口继承
先让我们看一看下面的例子,下面代码的运行结果是什么呢?
class A { public: virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;} virtual void test(){ func();} }; class B : public A { public: void func(int val=0){ std::cout<<"B->"<< val <<std::endl; } }; int main(int argc ,char* argv[]) { B*p = new B; p->test(); return 0; }
代码运行结果是 B->1。为什么呢?这就与我们的实现继承和接口继承有关了。
1、实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
2、接口继承
虚函数的重写是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写实现,达成多态,继承的是接口。
B继承了A中func函数的接口,而B自己也可以在类里面重新对func函数的实现进行重写。
九、多态的原理
1、虚函数表
从上面的图中我们看到,除了_b成员,还多一个_vfptr放在对象的前面对象中。这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
虚函数表本质是一个存虚函数指针的指针数组。
2、同一类型的对象共用同一个虚表
3、 调用原理
我们使用下面的代码来看看多态的调用原理
class Person { public: virtual void buy() { cout << "1"; } }; class Student :public Person { public: virtual void buy() { cout << "2"; } };
从上面的图中我们可以看出不同的对象会去从自己的虚函数表中调用自己实现的虚函数。这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
4、动态绑定与静态绑定
1、静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。
2、动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
3、多态调用:运行时,去指向对象的虚表中找到函数地址,进行调用。
4、普通调用:编译链接时确定函数地址,运行时直接调用。
十、重载、重写和隐藏的区别
1、重载
两个函数在同一作用域。
函数名相同,参数不同(类型、顺序个数)。
2、重写(覆盖)
两个函数分别在基类和派生类的定义域。
函数名/参数/返回值都相同(协变除外)。
两个函数必须是虚函数。
3、隐藏(重定义)
两个函数分别在基类和派生类的作用域。
函数名相同。
两个基类和派生类的同名函数不构成重写就是重定义。
十一、多继承中的虚函数表
我们知道单继承中的子类中只有一张虚函数表,那么多继承中的子类的虚函数表是什么样的呢?
我们通过把虚函数表的地址打印出来,来观察一下多继承中虚函数表是什么样子的。
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(*VFPTR) (); void PrintVTable(VFPTR* table[]) { cout << " 虚表地址>" << table << endl; for (int i = 0; table[i] != nullptr; ++i) { printf(" 第%d个虚函数地址 :%p,->", i, table[i]); VFPTR f = table[i]; f(); } cout << endl; } int main() { Derive d; VFPTR* vTableb1 = (VFPTR*)(*(int*)&d); PrintVTable(vTableb1); VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1))); PrintVTable(vTableb2); return 0; }
运行结果如下:
观察上图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。