1.多态的概念
- 通俗来说,就是各种形态,具体点就是为了完成某个行为,当不同的对象取完成时会产生出不同的状态
2.多态的实现条件
- 前提:必须在继承的方式下
- 基类中必须要有虚函数,而且子类必须要对基类中的虚函数进行重写
- 虚函数调用:必须(基类)指针或者引用调用虚函数
- 多态体现:在程序运行时,根据基类的指针或者引用指向不同类的对象调用响应的虚函数
- 虚函数:被关键字virtual修饰的成员函数称为虚函数
// 多态
class Fruit
{
public:
virtual void Print()
{
cout << "Fruit::Print()" << endl;
}
protected:
string f_;
};
class Apple : public Fruit
{
public:
virtual void Print()
{
cout << "Apple::Print()" << endl;
}
protected:
int a_;
};
class Orange : public Fruit
{
public:
virtual void Print()
{
cout << "Orange::Print()" << endl;
}
protected:
int o_;
};
void TestPrint(Fruit& f)
{
f.Print();
}
// 注意:在程序运行时,根据p所引用对象的不同,最终调用的虚函数就不一样
// 在程序编译时,无法确定具体要调用那个虚函数---因为:在编译阶段,
// 编译器根本不知道p到底指向的是那个类的对象
int main()
{
Fruit f;
TestPrint(f);
Apple a;
TestPrint(a);
Orange o;
TestPrint(o);
}
3.重写
- 前提:一定在继承体系中,子类对父类中的虚函数进行重写
- 基类的函数一定要是虚函数
- 子类要重写基类虚函数:子类虚函数的原型必须要与父类虚函数的原型一致
原型一致:返回值类型,函数名字(参数列表)必须要完全相同 - 重写例外:① 协变:基类虚函数必须返回基类的指针或引用,子类虚函数必须要返回子类的指针或引用–>返回值类型不同
class A{};
class B : public A{};
// 协变
class Base
{
public:
virtual Base& Test1()
{
cout << "Base::Test1()" << endl;
return *this;
}
virtual A* Test2()
{
cout << "Base::Test2()" << endl;
return nullptr;
}
};
class Derived : public Base
{
public:
virtual Derived& Test1()
{
cout << "Derived::Test1()" << endl;
return *this;
}
virtual B* Test2()
{
cout << "Derived::Test2()" << endl;
return nullptr;
}
};
void TestVirtual(Base& b)
{
b.Test1();
b.Test2();
}
int main()
{
Base b;
TestVirtual(b);
cout << "--------------" << endl;
Derived d;
TestVirtual(d);
return 0;
}
- ② 析构函数重写:只要基类中的析构函数是虚函数,如果子类的析构函数给出则构成重写—函数名字不同
- 子类虚函数可以不用加virtual关键字—一般情况下建议加上
- 子类和基类虚函数的访问权限可以不用
- c++11提供:override:让编译器在编译期间,帮助检测子类是否对基类的虚函数重写成功
- final:可以修改类—>表明该类不可以被继承
可以修饰虚函数—>表明虚函数不能被子类重写
- 重写和同名隐藏
4.区分:函数重载,同名隐藏(重定义),重写(覆盖)
- 函数重载:在相同的作用域,函数名字相同,参数列表不同(参数个数,参数类型,类型次序),与函数返回值类型无关
- 注意:在继承体系中,如果基类和子类两个成员函数名字相同,参数列表不同–不是函数重载
5.抽象类
- 抽象类:包含纯虚函数的类
- 抽象类不能实例化
- 抽象类是一定要被继承的,而且后序的子类要对纯虚函数进行重写
- 如果子类没有重写纯虚函数,则子类也是抽象类
#include <iostream>
using namespace std;
#include <string>
#include <windows.h>
class Work
{
public:
void toEmployee()
{
cout << "工人" << endl;
}
void toProgrammer()
{
cout << "经理" << endl;
}
};
class Person
{
public:
// Employee信息不全面,所以工作的方法没有办法实现
// 纯虚函数
virtual void toWork(Work& w) = 0;
protected:
string name_;
int age_;
};
class Emolyee : public Person
{
public:
virtual void toWork(Work& w)
{
w.toEmployee();
}
};
class Programmer :public Person
{
public:
virtual void toWork(Work& w)
{
w.toProgrammer();
}
};
void TestWork(int n)
{
Work w;
Person* p = nullptr;
for (int i = 0; i < n; i++)
{
if (rand() % 2)
{
p = new Emolyee();
}
else
{
p = new Programmer();
}
p->toWork(w);
delete p;
Sleep(1000);
}
}
int main()
{
TestWork(10);
return 0;
}
6.多态的实现原理
- 对象模型
- 注意:类中包含虚函数,如果永华没有显式定义构造函数,则编译器会给用户生成一份默认的构造函数,在生成的构造函数会填充对象前4个字节的内容;如果用户显式定义了构造函数,则编译器会修改用户定义的构造函数–增加一条给对象前4个字节赋值的语句,即:对象创建好之后,对象前4个字节中的内容已经被填充好了
① 如果派生类么有对基类的虚函数进行重写或者自己也没有添加任何的虚函数,那么派生类和基类虚表中的内容完全一致----但是基类和派生类使用的不是同一张虚表
② 如果派生类重写了基类某个虚函数,编译器就会用重写的虚函数替换原虚表相同偏移量位置的基类虚函数
③ 虚表中放置的是虚函数的入口地址,如果能够拿到虚表中的内容,即拿到了虚函数的入口地址,则可以调用该函数
- 什么情况下,编译器会默认生成构造函数?
① A类中包含B类的对象,A类没有显式定义构造函数,B类带有无参的或者全缺省的构造函数
② B类继承A类,A类没有显式定义任何构造函数,B类带有无参或全缺省的构造函数
③ 在虚拟继承中,编译器一定会给派生类生成构造函数—对象大小多了4个字节:虚基表地址
④ 类中如果包含有虚函数,编译器一定会给该类生成构造函数
- 虚表构建过程
- 编译器在编译期间,按照虚函数在类中声明的先后次序依次添加到虚表中
- 子类虚表的构建过程
① 将基类虚表中的内容拷贝一份放置到派生类虚表中
② 如果派生类重写了基类中某个虚函数,则使用派生类自己的虚函数替换相同偏移量位置的基类虚函数地址
③ 对于派生类新增的虚函数,按照其在类中声明的先后次序放在虚表中 - 注意:
① 虚表是在编译期间产生的
② 派生类和基类不会共享一张虚表,即使虚表中放置的虚函数的地址相同
③ 相同类的不同对象之间共享同一张虚表
④ 虚表中内容是只读的,不允许被修改—放在代码段
- 虚函数调用
- 获取对象地址,然后从对象地址获取虚表位置
- 传递参数&this指针
- 从虚表中获取对应虚函数的入口地址
- 调用该虚函数