目录
阳光总在风雨后
这里是来自M--Y的专栏:C++启()航
以下内容均为个人见解,如有不足还请指出
期待大家的点赞、收藏、评论(互三必回)诸君共勉
多态的概念
多态的概念:多种形态。在完成某个行为时,不同的对象去完成时会产生不同的状态。
比如:在买火车票时,普通是全价购买;学生是半价购买;军人是优先购买。
多态的定义及实现
1.多态构成的条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。
条件:
1.1必须通过基类的指针或者引用调用虚函数
1.2被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
那么虚函数是什么呢?
2.虚函数
虚函数:即被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 关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来在派生类依旧保持虚函数属性)【不建议使用这种不规范的写法】
};
void Func(Person& p)
{
p.BuyTicket();
}
int main(){
Person p;
Student s;
Func(p);//输出 全价买票
Func(s);//输出 半价买票
return 0;
}
虚函数重写的两个例外
1.协变(基类与派生类虚函数返回值类型不同)
基类虚函数返回基类对象的指针或引用,派生类虚函数返回基类对象的指针或引用时,称为协变。
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;}
};
2析构函数的重写(基类与派生类析构函数的名字不同)
这里基类和派生类的析构函数的名字看起来不同,其实是被编译器经过特殊处理的,编译后析构函数的名称统一处理成destructor.
3.C++11 override 和 final
1.final:修饰虚函数,表示该函数不能再被重写
class Car{public :virtual void Drive () final {}};class Benz : public Car{public :virtual void Drive () { cout << "Benz- 舒适 " << endl ;}};
2.override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class Car {public :virtual void Drive (){}};class Benz : public Car {public :virtual void Drive () override { cout << "Benz- 舒适 " << endl ;}};
4.小结
从模版到继承再到多态,我们会发现有三个很相似的知识点,分别是重载,重定义(隐藏)以及重写(覆盖),在这里再将三者间的异同点罗列一下
重载:1.两个函数出现在同一作用域 2.函数名/参数相同
重写(覆盖)1.函数分别在基类和派生类的作用域
2.函数名/参数/返回值都必须相同(协变例外)
3.两个函数多必须是虚函数
重定义(隐藏)1.两个函数分别在基类和派生类的作用域
2.函数名相同
3.两个基类和派生类的同名函数不构成重写就是重定义
抽象类
概念
在虚函数的后面写上=0,则这个函数称为纯虚函数,包含纯虚函数的类叫做抽象类(也叫接口类)。抽象类不能实例化对象,派生类继承抽象类后也不能实例化出对象,只能重写纯虚函数,派生类才能实例化出对象。
注意:虚函数的继承是一种接口继承,派生类继承是基类函数的接口,目的是为了重写,达成多态,继承是接口。如果不实现多态,不要把函数定义成虚函数。
多态的原理
1.虚函数表
class Person
{
public:
virtual void Func() {cout<<"全价买票"<<endl;}
void Func1() { cout << "Func1()" << endl; }
private:
string _name;
};
class Student : public Person
{public:
virtual void Func() { cout << "半价买票" << endl; }
private:
int _id;
};
上述代码通过观察发现除了_name成员外,还有一个vfptr(放置前后位置与代码运行平台有关),对象中的这个指针我们叫做虚函数表指针。(虚函数表存放虚函数的地址)
通过进一步对派生类的观察发现:
1.1派生类对象中也有一个虚函数表指针,由两部分构成,一部分是父类继承下来的成员,另一部分是自己的成员
1.2基类对象和派生类对象的虚函数表是不一样的。
1.3基类对象中的Func1()函数也被继承下来了,但是由于不是虚函数,所以不放入虚标中。
1.4虚函数表本质上是一个存虚函数指针的指针数组,一般情况下这个数组最后面放了一个nullptr
1.5虚表生成:①先将基类中的虚表内容拷贝一份到派生类的虚表中②Eugene派生类重写了基类的某一个虚函数,则用派生类自己的虚函数覆盖虚表中基类的虚函数③派生类自己新增的虚函数按其在派生类中声明次序增加到派生类虚表的最后。
2.多态的原理
class Person
{
public:
virtual void Func() {cout<<"全价买票"<<endl;}
void Func1() { cout << "Func1()" << endl; }
private:
string _name;
};
class Student : public Person
{public:
virtual void Func() { cout << "半价买票" << endl; }
private:
int _id;
};void F(Person& p)
{
p.Func();
}
int main()
{
Student XiaoMing;
F(XiaoMing);
return 0;
}
通过指针(引用)指向的对象,在其虚表中找到虚函数,如上述XiaoMing对象调用虚函数Student ::Func()。
注意:满足多态以后的函数调用,是运行起来以后到对象中去取的,不满足多态的函数调用调用,是在编译时就确认好的。
3.动态绑定和静态绑定
1.静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态。(函数重载、函数模版)
2.动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。(函数重写、虚函数的继承)
单继承虚表和多继承虚表之间的区别
单继承的虚表上面已经演示过了,基类虚函数被重载的全部被派生类虚函数覆盖,未被重载的被继承下来,同时按声明次序增加自己本身的虚函数
多继承的虚表
class Student
{public:
virtual void Func1() { cout << "Func1()" << endl; }
virtual void Func2() { cout << "Func2()" << endl; }
private:
int _id;
};
class Teacher
{
public:
virtual void Func1() { cout << "Func1()" << endl; }
virtual void Func2() { cout << "Func2()" << endl; }
private:
int _sln;
};
class Assistant :public Student ,public Teacher
{
public:
virtual void Func1() { cout << "Func1()" << endl; }
virtual void Func3() { cout << "Func3()" << endl; }
};//在VS中监视窗口会隐藏Func3,所以采用下述方法将其打印出来
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;//这里的代码经常会崩溃,因为编译器对虚表的处理不干净,导致虚表最后面没有放nullptr,导致越界,我下面的展示图是在Dev-C++中将这里改成vTable[i]强行打印出来的
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Assistant a;
VFPTR* vTableb1 = (VFPTR*)(*(int*)&a);
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&a + sizeof(Student)));
PrintVTable(vTableb2);
return 0;
}
下面分别是继承基类Student和基类Teacher的虚函数
对比两张图可以发现多继承派生类未重写的函数(Func3)会放在第一个基类部分的虚函数表中。