C++中的多态
C++多态
什么是多态
- 通俗来说,多态就是同一个事物在不同场景下表现出来不同的状态
- 在C++中,多态体现为同一个类派生出来的对象去调用同一函数时产生了不同的行为
多态的定义及实现
在继承中构成多态的两个前提条件
1、调用函数的对象必须是指针或者引用
2、被调用的函数必须是虚函数,并且在子类中完成了对虚函数的重写
什么是虚函数?
- 虚函数就是在类的成员函数名之前加上virtual关键字
什么是虚函数的重写?
- 当派生类中有一个跟基类虚函数函数原型完全相同的函数,就称派生类的虚函数重写了基类的虚函数,完全相同指的是:函数名、参数、返回值都相同。
- 函数重写必须在两个不同的作用域中(基类和派生类中)
- 函数重写与类的访问限定符无关
- 虚函数的重写也叫做虚函数的覆盖,但是虚函数重写也有两个例外。
一个多态的实例:
class Base {
public:
virtual void testFunc() {
std::cout << "Base::testFunc()" << std::endl;
}
public:
int _b;
};
class Derived : public Base {
public:
virtual void testFunc() {
std::cout << "Derived::testFunc()" << std::endl;
}
public:
int _d;
};
void testFunc(Base& b) {
b.testFunc();
}
int main()
{
Base b;
Derived d;
testFunc(b);
testFunc(d);
system("pause");
return 0;
}
可以看到,根据函数传递对象的不同,在函数中调用的函数也是不同的,基类调用基类的函数,派生类调用派生类中的函数
虚函数重写例外之一:协变
- 虚函数重写时重写的虚函数返回值可以不同,但是必须分别是基类指针和派生类指针或者基类引用和派生类引用。此时虚函数重写我们称之为协变
一个简单协变的例子
class Base {
public:
virtual Base* testFunc() {
return new Base();
}
};
class Derived : public Base {
public:
virtual Derived* testFunc() {
return new Derived;
}
};
虚函数重写例外之二:析构函数
- 基类中的构造函数是虚函数,那么派生类的析构函数就重写了基类的析构函数,此时两个类的函数名不同,但是也构成了虚函数的重写
- 我们鼓励将析构函数写成虚函数。
在下面这种派生类中管理了资源的场景中,将析构函数写成虚函数避免了内存泄露的发生。
class Base {
public:
Base() {
std::cout << "Base::Base()" << std::endl;
}
virtual ~Base() {
std::cout << "Base::~Base()" << std::endl;
}
};
class Derived : public Base {
public:
Derived() {
_pa = new int;
std::cout << "Derived::Derived()" << std::endl;
}
virtual ~Derived() {
if (_pa) {
delete _pa;
_pa = nullptr;
}
std::cout << "Derived::~Derived()" << std::endl;
}
private:
int *_pa;
};
int main()
{
Base* pb = new Derived;
delete pb;
system("pause");
return 0;
}
如果不将析构函数声明成虚函数,在delete对象时就会去调用基类的析构函数,派生类的析构函数调用不到导致内存泄露。所以我们最好将析构函数声明成虚函数。
抽象类
- 在虚函数的后面写上0,则这个函数就是纯虚函数。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象
- 派生类继承之后也不能实例化出对象,只有重写虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写。
C++关键字override 和 final
- override与虚函数配合使用强制重写虚函数,虚函数的意义就是实现多态,函数没有重写,虚函数就没有意义。
- final修饰基类的虚函数不能被派生类重写
C++多态原理
探究:包含虚函数类的大小
class Base {
public:
virtual void testFunc() {
std::cout << "Base::testFunc()" << std::endl;
}
int _b;
};
- 在这个基类中,根据之前的经验,函数是不被计算在类的大小中的,所以这个类的大小是四个字节
大小多了四个字节,猜测是由于类中虚函数的原因,再给基类中添加几个虚函数查看基类大小
class Base {
public:
virtual void testFunc1() {
std::cout << "Base::testFunc1()" << std::endl;
}
virtual void testFunc2() {
std::cout << "Base::testFunc2()" << std::endl;
}
virtual void testFunc3() {
std::cout << "Base::testFunc3()" << std::endl;
}
int _b;
};
大小还是8个字节,根据虚拟继承的经验,猜测前四个字节是一个指针,指向一块内存。查看内存窗口:
类中只有一个虚函数:
类中有三个虚函数:
可以看出,虚函数的个数不同,导致对象前四个字节指向的内存中存放的内容不同,存放的可能是虚函数的函数指针
验证:
class Base {
public:
virtual void testFunc1() {
std::cout << "Base::testFunc1()" << std::endl;
}
virtual void testFunc2() {
std::cout << "Base::testFunc2()" << std::endl;
}
virtual void testFunc3() {
std::cout << "Base::testFunc3()" << std::endl;
}
int _b;
};
typedef void(*PF)();
void print_func(Base &b, std::string str) {
std::cout << str.c_str() << std::endl;
PF* pf = (PF*)*(int*)&b;
while (*pf) {
(*pf)();
++pf;
}
}
int main()
{
Base b;
std::cout << sizeof(b) << std::endl;
print_func(b, "Base===>");
system("pause");
return 0;
}
根据猜想验证成功,有基类中前四个字节存放的地址指向存放虚函数函数指针的内存。我们在基类中这前四个字节的指针叫做虚指针,虚指针指向的虚函数表叫做虚表。
探究:C++多态实现的原理
class Base {
public:
virtual void testFunc() {
std::cout << "Base::testFunc()" << std::endl;
}
int _b;
};
class Derived : public Base {
public:
virtual void testFunc() override{
std::cout << "Derived::testFunc()" << std::endl;
}
};
void testFunc(Base& b, std::string str) {
std::cout << str.c_str() << std::endl;
b.testFunc();
}
int main()
{
Base b;
Derived d;
testFunc(b, "Base==>");
testFunc(d, "Derived==>");
system("pause");
return 0;
}
这是一个简单的多态函数,通过查看反汇编与内存中变量的值来观察多态的实现原理。
可以看到在反汇编中也是通过虚指针与虚表公共结合的方式来调用虚函数
静态绑定与动态绑定
- 多态体现的本质的就是使用的虚指针不同,使用不同的虚指针找到不同的虚表,调用不同的函数从而实现多态。
基类指针如何找到派生类虚表?
这就与动态绑定与静态绑定相关
- 静态绑定就是在程序编译期间确定函数行为。
- 动态绑定是在程序运行期间根据具体拿到的类型确定程序的行为,接收到派生类对象就使用派生类虚指针。
虚函数真的需要重写吗
我们根据虚表来看一段程序
class Base {
public:
virtual void testFunc1() {
std::cout << "Base::testFunc1()" << std::endl;
}
virtual void testFunc2() {
std::cout << "Base::testFunc2()" << std::endl;
}
virtual void testFunc3() {
std::cout << "Base::testFunc3()" << std::endl;
}
int _b;
};
class Derived : public Base {
public:
virtual void testFunc1() {
std::cout << "Derived::testFunc1()" << std::endl;
}
virtual void testFunc3() {
std::cout << "Derived::testFunc3()" << std::endl;
}
int _d;
};
typedef void(*PF)();
void print_func(Base &b, std::string str) {
std::cout << str.c_str() << std::endl;
PF* pf = (PF*)(*(int*)&b);
while (*pf) {
(*pf)();
++pf;
}
}
int main()
{
Base b;
Derived d;
std::cout << sizeof(b) << std::endl;
std::cout << sizeof(d) << std::endl;
print_func(b, "Base===>");
print_func(d, "Derived===>");
system("pause");
return 0;
}
- 可以看到,如果不在派生类中重写虚函数,那么传入的派生类对象还是会对调用基类中的的虚函数,那么使用虚函数就没有意义。
根据上面的现象我们可以猜想出:派生类的虚表是从基类中继承过来的,如果在派生类中重写了虚函数,只需要修改虚表内容即可,如果没有重写,虚表就与基类保持一致。