概念:
去完成某个行为时,调用不同的对象去完成时会产生不同的结果(状态)。
多态的构成条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
虚函数:被virtual修饰的类成员函数称为虚函数。
虚函数的重写
派生类中拥有一个和基类完全相同的虚函数(返回值类型、函数名字、参数列表完全相同),称为子类的虚函数重写了基类的虚函数
虚函数的重写例外:
- 在重写基类虚函数时,派生类的虚函数不加virtual,也可以构成重写。(基类的虚函数被继承下来了,在派生类依旧保持虚函数属性。个人认为是个错误,全加virtual修饰最好)
- 协变(基类和派生类函数返回值类型不同)。基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,称为协变。
- 析构函数的重写。(函数名称不一样)如果基类的析构函数为虚函数,此时只要派生类析构函数被定义,无论是否加virtual关键字,都与基类析构函数构成重写。
构成多态和不构成多态的区别:
构成多态:看指针或者引用指向的对象,对象类型是哪个类调用那个类的虚函数。
不构成多态:看指针或者引用的类型,调用那个类的函数。
class Person{ class Person{
public: public: //不构成多态
virtual void Buyticket() void Buyticket()
{cout << "全价票" << endl;} {cout << "全价票" << endl;}
private: private:
int _person; int _person;
}; };
class Student :public Person class Student :public Person
{ {
public: public:
virtual void Buyticket() void Buyticket()
{cout << "半价票" << endl;} {cout << "半价票" << endl;}
private: private:
int _student; int _student;
}; };
void Func(Person& p) void Func(Person& p)//Func1(Student& p)
{ {
p.Buyticket(); p.Buyticket();
} }
int main(){ int main()
Person p; {
Student s; Person p;
Person& pptr = p; Student s;
pptr.Buyticket();//全价票 Func(p);//全价票
pptr = s; Func(s);//全价票 //Func1(s)//半价票
pptr.Buyticket();//全价票 return 0;
//这种情况下,pptr的类型没有变化,故调用基类的虚函数
}
Func(p);//全价票
Func(s);//半价票
Person* pptr1 = &p;
pptr1->Buyticket();//全价票
pptr1 = &s;
pptr1->Buyticket();//半价票
return 0;
}
“三重定义”
重载:两个函数在同一作用域;函数名/参数列表相同
重写(覆盖):两个函数分别在基类和派生类的作用域;达到多态的条件:{函数名、参数、返回值都必须相同(协变例外),两个函数必须是虚函数 }
重定义(隐藏):基类及其派生类的同名函数不构成重写就是重定义。(两个函数分别在基类和派生类的作用域,函数名相同)
抽象类:
在虚函数后面加上=0,则这个函数变为纯虚函数。
包含纯虚函数的类叫做抽象类(接口类),抽象类不能实例化出对象。派生类继承后也不能实例化处对象。派生类只有重写了纯虚函数才能实例化处对象,即纯虚函数规范了派生类对虚函数的重写
我们在写一个父类的时候,很抽象在现实中没有对应实体(不会用来实例化处对象)就可以将这个父类定义为抽象类。
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数(即继承了函数的实现)
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,继承的是接口,目的是为了重写,实现多态。
我们可以看一下下面代码的运行结果:
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
这是因为虚函数的继承是一种接口继承,对函数的实现重写了,而函数接口被继承下来了
虚函数表:
在VS2013环境下:
class Base{
public:
virtual void Fun1()
{
cout << "Base:Fun1" << endl;
}
private:
int _val;
};
int main(){
Base b;
return 0;
}
在b对象中,我们发现除了_val成员,还多了_vfptr放在对象前面。对象中的这个指针,叫做虚函数表指针。一个含有虚函数的类中都至少有一个虚函数指针,因为虚函数的地址要被放到虚函数表中,虚函数表也称为虚表。
class Base{
public:
virtual void Fun1()
{
cout << "Base:Fun1" << endl;
}
void Fun2()
{
cout << "Base:Fun2" << endl;
}
virtual void Fun3()
{
cout << "Base:Fun3" << endl;
}
private:
int _val;
};
class Derive :public Base
{
public:
virtual void Fun1()
{
cout << "Derive:Fun" << endl;
}
};
int main(){
Base b;
Derive d;
return 0;
}
通过上面的代码及调试窗口,我们可以了解到,
fun2不是虚函数,所以在虚函数表中并没有它。但它被派生类继承下来了,是实现继承。
派生类的虚表:
-
先将基类的虚表内容拷贝一份到派生类虚表中。
-
如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖掉虚表中基类的虚函数
-
派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
虚表的本质:
虚表的本质是一个存虚函数指针的指针数组,最后以nullptr结尾。
我们可以使用代码验证一下:
class Base{
public:
virtual void Fun1()
{
cout << "Base:Fun1" << endl;
}
void Fun2()
{
cout << "Base:Fun2" << endl;
}
virtual void Fun3()
{
cout << "Base:Fun3" << endl;
}
private:
int _val;
};
class Derive :public Base
{
public:
virtual void Fun1()
{
cout << "Derive:Fun" << endl;
}
};
typedef void(*VFPTR)();
int main(){
Base b;
Derive d;
VFPTR* pb=(VFPTR*)(*(int *)(&b));
for (int i = 0; pb[i] != nullptr; ++i)
{
cout << "Base"<<i<<":"<<pb[i] << endl;
}
VFPTR* pd = (VFPTR*)(*(int *)(&d));
for (int i = 0; pd[i] != nullptr; ++i)
{
cout << "Derive" << i << ":" << pd[i] << endl;
}
return 0;
}
多态的原理:
当类中声明虚函数时,编译器会自动在每个对象当中添加一个指向虚函数表的指针,指向一个虚函数表(虚函数表是一个存储类成员函数指针的数据结构,虚函数都会被放入其中,由编译器生成并维护),当指针或者引用指向哪个对象,就去哪个对象中找到对应的虚函数来进行调用,,由此实现多态行为。
静态绑定(又称前期绑定、早绑定),在程序编译期间确定了程序的行为,也称静态多态(如函数重载)
动态绑定(又称后期绑定、晚绑定),在程序运行期间,根据具体拿到的类型确定程序的具体行为(调用具体的函数),也称动态多态。(此时讲的多态)。
单继承的虚函数表:
多继承的虚函数表:
C++11:
final:修饰类,这个类变成最终类,不能被继承
修饰虚函数,这个虚函数不能被重写。
override:检查派生类虚函数是否重写了基类的某个虚函数,如果没有重写,编译报错。