C++是面对对象的程序设计。
面对对象的程序设计有4大特性:分别是,抽象、封装、继承、多态。
今天我们就来总结一下多态的内容。
多态:分为静态多态和动态多态:
#include<iostream> using namespace std; int Add(int a,int b)//1 { return a+b; } char Add(char a,char b)//2 { return a+b; } int main() { cout<<Add(666,888)<<endl;//1 cout<<Add('1','2');//2 return 0; }
动态多态:其实要实现动态多态,需要几个条件——即动态绑定条件:
1、虚函数。基类中必须有虚函数,在派生类中必须重写虚函数。
2、通过基类类型的指针或引用来调用虚函数。
重写——也就是基类中有一个虚函数,而在派生类中也要重写一个原型(返回值、名字、参数)都相同的虚函数。不过协变例外。
协变:是重写的特例,基类中返回值是基类类型的引用或指针,在派生类中,返回值为派生类类型的引用或指针。
菱形继承的二义性
#include<iostream>
using namespace std;
class Grandam
{
public:
void introduce_self()
{
cout << "I am grandam." << endl; }};class Mother :public Grandam
{
public:
void introduce_self()
{
cout << "I am mother." << endl;
}
};
class Aunt :public Grandam
{
public:
void introduce_self()
{
cout << "I am aunt." << endl;
}
};
class Daughter :public Mother,public Aunt
{
public:
void introduce_self()
{
cout << "I am daughter." << endl;
}
};
int main()
{
Grandam* ptr;
Daughter d;
ptr = &d;
ptr->introduce_self();
return 0;
}
- 1
会出现编译错误。这就是二义性问题,Mother和Aunt都继承了Grandam,而Daughter多继承了Mother和Aunt,这样在调用对象d时,编译器不知道要调用来自Mother里的成员函数introduce_self(),还是来自Aunt里的introduce_self(),所以编译时会出错。
虚拟继承可以解决这种二义性的问题
#include<iostream> using namespace std; class Grandam { public: void introduce_self() { cout << "I am grandam." << endl; } }; class Mother :virtual public Grandam { public: void introduce_self() { cout << "I am mother." << endl; } }; class Aunt :virtual public Grandam { public: void introduce_self() { cout << "I am aunt." << endl; } }; class Daughter :public Mother,public Aunt { public: void introduce_self() { cout << "I am daughter." << endl; } }; int main() { Grandam* ptr; Daughter d; ptr = &d; ptr->introduce_self(); return 0; }
为什么虚拟继承可以解决这种菱形继承的二义性?
首先了解:
1.类的继承:派生类拥有其基类拥有的所有数据成员与成员函数,但访问属性不同。
2.类的派生:派生类可以加入新的基类不拥有的数据与函数。
多重继承:一个类派生出多个类,多个类派生出一个类
性质与规则:
1.声明方法:class D:public A,private B,protected C{}
2.调用顺序:先调用A的构造函数,再到B,再到C。虚基类->非虚基类->子对象->派生类(为什么?)。
问题:
1.二义性问题:派生类与基类中存在名字相同的数据或函数,使得函数无法判断与调用。
解决方法:
1.在引用数据成员或成员函数时指明其作用域。(类名::数据名)
通过制定作用域可以解决这种二义性问题
2.同名覆盖(函数重载):派生类新增的同名成员将会覆盖基类中的同名成员,不论有多少个基类,而且实现的途径是利用派生类的新增成员,既要有同名的,则在直接调用不加类名作用域时就会调用派生类的成员。
3.虚基类(虚拟继承):让继承间接共同基类时只保留一份成员。
Class A
Class B:virtual public A
Class C:virtual public A
Class D:public B,public C
此时,A是B的虚基类,A是C的虚基类。
A类 |
Int data; |
Void fun(); |
B类 |
Int A::data; |
Void A::fun() |
Int data_b; |
C类 |
Int A::data |
Void A::fun() |
Int data_c; |
D类 |
Int B::data; |
Int C::data; |
Int data_b; |
Int data_c; |
Void B::fun() |
Void C::fun() |
Int data_d; |
Void fun_d(); |
原来B与C中都有int A::data,到D中分别变成int B::data和int C:: data。但是,加了虚拟继承virtual后,B与C中的int A::data就多了一个标志virtual,变成int virtual A::data,而带有virtual的成员在D中就会被合并成一个,这就是虚拟继承的本质。
至于构造函数,与普通继承派生就会有所不同。B与C实际上是还是普通继承,所以构造函数写法不变。而D类中含有A的成员且不重复,其实已经可以把A看作是D的直接基类了(体现了虚基类的”虚”),所以D的构造函数就要调用A的构造函数,由A的构造函数来初始化D中A的数据,而不是从B与C中继承。
4.虚函数:在基类声明函数是虚拟的,不是实际存在的,在派生类中才正式定义此函数。作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数(目标是能够用同一种方式去调用同一类族中不同类的所有的同名函数)。
为什么要引入虚函数?
类族,每个类都有一个同名的函数display():A<-B<-C<-D<-E<-F。要想调用不同派生类的同名函数,就要用多个类名或者多个指向派生类的指针如a.display().b.display().....,不方便。
这里的”同一种方法”是?
使用同一基类的指针或引用,调用同一类族中不同类虚函数,如用A的指针pt指向B或C或D或E或F,pt->display()来调用。
为什么使用基类指针可以访问到派生类的成员?
基类指针指向的是基类部分这是毋庸置疑的,但是基类的同名函数声明成了virtual,而指针指向的派生类存在同名函数,所以这里有派生类的同名函数取代的基类的virtual函数变成了基类的一部分(体现虚函数的”虚”),所以使用基类指针却可以访问到派生类的成员。
虚函数与函数重载方法的区别?
函数重载的定义是:在同一作用域(如:类)中,几个函数名字相同但参数列表(参数类型,参数个数)不全相同。
虚函数的定义是:在基类中用关键词“virtual”声明成员函数,然后在派生类中正式定义或重新定义此函数,其中要求函数名、函数类型、参数类型和个数必须与基类虚函数相同。
所以函数重载和虚函数在概念上的最大差别在于函数重载针对某个类里面的同名函数而言,而虚函数是针对基类和派生类之间同名函数而言。
在使用方式方面:
函数重载需要注意作用域,在内层作用域中声明的重载函数会隐藏外层作用域中的同名函数;在调用重载函数时要注意参数匹配,注意“无匹配”和“二义性调用”等问题。
虚函数只能用virtual声明类的成员函数,不能把类外的普通函数作为虚函数,它只能用于类的继承层次结构中;在同一类族中不能再定义一个非virtual的但与虚函数具有相同参数(类型和个数)和返回类型的同名函数。
# include <iostream> using namespace std; class Base { public: virtual void fn() { cout<<"In Base Class!"<<endl; } }; class Sub:public Base { public: virtual void fn() { cout<<"In Sub Class!"<<endl; } }; class Suc : public Base { public: virtual void fn() { cout<<"In Suc class!"<<endl; } }; void test(Base &b) { b.fn(); } int main() { Base bc; Sub sc; Suc suc; test(bc); test(sc); test(suc); }
结果为:
In Base Class!
In Sub Class!
In Suc class!
虚函数的底层实现机制是通过虚函数表(参考:https://blog.csdn.net/neiloid/article/details/6934135)