1.多态的概念
多态:多种形态。具体说就是不同对象完成同一动作产生不同的行为。
2.多态的定义及实现
2.1构成多态的条件
多态是指同一继承体系中,分属不同继承关系的类对象,去调用同一函数,产生不同的行为。
在继承中构成多态还需要满足两个条件:
1. 调用函数的必须是对象的指针或引用;
2. 被调用的函数必须是虚函数,并且完成了虚函数的重写。
什么是虚函数?
在类的成员函数前加上virtual关键字就构成了虚函数。
class Base {
public:
virtual void func()
{
cout << "Base::func" << endl;
}
};
怎样构成虚函数的重写?
派生类中有一个跟基类完全相同的虚函数(完全相同是指函数名、参数、返回值均相同),派生类的虚函数就完成了对基类虚函数的重写,虚函数的重写也称覆盖。
虚函数重写的例外:协变
一个例外,重写的虚函数的返回值类型可以不同,但必须是基类指针/引用和派生类指针/引用。(较少使用)以下就构成协变:
class A
{};
class B : public A
{};
class Base {
public:
virtual A* func()
{
cout << "Base::func" << endl;
return new A;
}
};
class Derive :public Base {
public:
virtual B* func()
{
cout << "Derive::func" << endl;
return new B;
}
};
不规范的重写行为:
在派生类中重写基类虚函数可以不加virtual关键字,也是构成重写的。因为基类的虚函数被继承到了派生类中,依然保持虚函数属性,只是重写了它,但这是不规范的行为。
析构函数的重写(virtual析构函数)
基类的析构函数如果是虚函数,那么派生类的虚函数就重写了基类的析构函数,虽然他们的函数名不同,我们可以理解为编译器做了特殊处理,使两者函数名统一。
如果要删除一个具有非虚析构函数的派生类对象,却显示地通过指向该对象的一个基类指针对它应用delete运算符,那么C++标准会指出这一行为是未定义的。
为解决这一问题,在基类中创建virtual析构函数。如果基类析构函数声明为virtual,那么任何派生类的析构函数都是virtual并重写基类的析构函数。如果对一个基类指针用delete运算符来显示地删除它所指的类层次中的某个对象,那么系统会根据该指针所指对象调用相应类的析构函数(发生多态)。
构造函数不能是virtual函数:虚函数的调用需要一个指向虚函数表的指针找到虚函数表,再在虚函数表中找到虚函数地址来调用虚函数,然而这个指针是存在对象的内存空间上的,如果构造函数是虚函数,就需要去虚函数表中找到并调用,但是此时对象还没有被初始化,没有对应的内存空间,也就没有对应的指向虚函数表的指针,也就找不到虚构造函数,无法调用。因此构造函数不能是virtual函数。
接口继承和实现继承
普通成员函数的继承是接口继承和实现继承,派生类继承了基类的成员函数的接口(声明)和具体实现;虚函数(非纯)的继承必须是接口继承,至于是否继承基类该函数实现(重写形成覆盖),由派生类自己决定。
纯虚函数只具体指定接口继承。
简单的非纯虚函数具体指定接口继承及缺省实现继承。
非虚函数具体指定接口继承以及强制性实现继承。(《Effective C++》条款34)
2.2重载、重写、重定义的对比
3.抽象类
纯虚函数
一个纯虚函数是在声明时“初始化值为0”的虚函数:
virtual void draw() = 0; //pure virtual function
通过声明类的一个或多个virtual函数为纯virtual函数,可以使一个类成为抽象类。
纯虚函数不提供函数的具体实现,每个派生的具体类必须重写所有基类的纯虚函数的定义,提供这些函数的具体实现。(否则派生类依然是抽象类,不是具体类)
4.C++11中的override和final
C++11之前,派生类可以覆盖基类任何虚函数。在C++11中,基类的虚函数在原型中声明为final,如:
virtual function() final;
那么,该函数在任何派生类中都不能被覆盖。
同时将一个类声明为final可防止被继承。
派生类中某函数声明为override是指该函数是对其父类中完全相同的函数的重写。
通过override可检查派生类是否正确重写了父类中的函数。(父类中需要有该函数,父类中该函数应该是virtual,且不能被final修饰)
class Base {
public:
virtual void func()
{}
};
class Derive :public Base {
public:
virtual void func() override
{/*...*/}
};
5.多态的原理
5.1虚函数表
sizeof(Base) == 4
class Base {
public:
virtual void func()
{}
};
通过观察Base类对象结构可发现,有一个指针__vfptr,这个指针叫做虚函数表指针,一个含有虚函数的类都至少有一个虚函数表指针,指向虚函数表的首地址,虚函数表中存储虚函数的地址,虚函数表也简称虚表。
对上述代码改造如下,再观察
class Base {
public:
virtual void func1()
{}
virtual void func2()
{}
void func3()
{}
};
class Derive :public Base {
public:
virtual void func1() override
{}
};
void Test()
{
Base b;
Derive d;
}
对于Base类对象,虚函数func1、func2在虚函数表中,因为func3不是虚函数,所以不在虚函数表中。
对于派生类Derive对象,Derive类也有自己的虚表,由于派生类重写了其父类的func1(),因此在虚函数表中,重写后的func1()覆盖了继承下来的func1(),func2没有被重写因此与基类中相同。
通过观察发现:
- 派生类对象d中也有一个虚函数表指针,d对象由两部分组成,一部分是继承自基类的,一部分是自己的,虚函数表指针存在于继承自基类的部分。
- 虚函数表实质上是一个存放虚函数指针的指针数组,最后一项为nullptr。
- 派生类虚表的生成:先将基类的虚表拷贝一份到派生类虚表中,如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖基类的虚函数,派生类自己声明的虚函数按其声明顺序追加在其虚表后。
- 还有注意:一个错误的说法“虚函数存在虚表中,虚表存在对象中。” 而正确的是对象中存的是指向虚表的指针,虚表中存的指向虚函数的函数指针。虚函数和普通函数一样,是存在代码段的。通过验证,在vs编译器下,虚表也是存在代码段的。
5.2多态的原理
class Base {
public:
virtual void func()
{
cout << "Base::func()" << endl;
}
};
class Derive :public Base {
public:
virtual void func() override
{
cout << "Derive::func()" << endl;
}
private:
int a;
};
void Test(Base* pBase)
{
pBase->func();
}
int main()
{
Base b;
Derive d;
Test(&b);
Test(&d);
return 0;
}
图中红色pBase指向Base类对象b,执行pBase->func()会在Base类的虚表中找到func()地址执行;蓝色pBase指向Derive类对象d,执行pBase->func()会在Dreive类的虚表中找到重写后的func()地址执行。这样就实现了不同对象完成同一动作,会产生不同行为。
满足多态的函数调用不是在编译时确定的,是运行起来后到对象中找到地址并调用的。
不满足多态的函数调用是在编译时确定好的。
5.3[动态绑定和静态绑定]
静态绑定在程序编译阶段确定了程序的行为(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,例如函数重载。
动态绑定在程序运行阶段根据具体拿到的类型确定程序的行为,调用相应的函数。(晚绑定)动态多态。
只有虚函数才使用的是动态绑定,其他的都是静态绑定。
深入理解C++的动态绑定和静态绑定
6.单继承和多继承关系的虚函数表
6.1单继承的虚函数表
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
class Derive :public Base {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
virtual void func4() { cout << "Derive::func4" << endl; }
private:
int b;
};
通过监视窗口观察,Derive类的func3(),func4()被编译器隐藏了。我们可以通过以下方式打印出虚函数表观察到func3和func4是在虚函数表中的。
typedef void(*pFunc) (); //对应func()
void PrintVTable(pFunc* vtable)
{
while (*vtable != nullptr) //虚表最后一项为nullptr
{
printf("%p -> ", *vtable);
(*vtable)();
++vtable;
}
}
void Test()
{
Base b;
Derive s;
Base* ptr1 = &b;
Base* ptr2 = &s;
//将对象指针强转为int*,再解引用(只看其前四个字节),
//拿到虚表的首地址(因虚表内存储函数指针,所以该指针应为指向函数指针的指针,强转为pFunc*)
pFunc* pvtable1 = (pFunc*)(*((int*)ptr1));
PrintVTable(pvtable1);
pFunc* pvtable2 = (pFunc*)(*((int*)ptr2));
PrintVTable(pvtable2);
}
6.2多继承的虚函数表
class Base1
{
public:
virtual void func1(){ cout << "Base1:func1()" << endl; }
virtual void func2(){ cout << "Base1:func2()" << endl; }
private:
int b1;
};
class Base2
{
public:
virtual void func1(){ cout << "Base2:func1()" << endl; }
virtual void func2(){ cout << "Base2:func2()" << endl; }
private:
int b2;
};
class Derive :public Base1, public Base2
{
public:
virtual void func1(){ cout << "Derive:func1()" << endl; }
virtual void func3(){ cout << "Derive:func3()" << endl; }
private:
int d1;
};
int main()
{
Derive d;
return 0;
}
查看d对象的内存构造:
可以看出,d对象中存在两个虚函数表指针,因为d对象继承了两个类Base1和Base2,Derive对func1进行了重写,在两个虚函数表中的func1()都被覆盖,但是不见func3(),像前面一样,我们打印一下虚函数表
typedef void(*pFunc) (); //对应func()
void PrintVTable(pFunc* vtable)
{
while (*vtable != nullptr) //虚表最后一项为nullptr
{
printf("%p -> ", *vtable);
(*vtable)();
++vtable;
}
}
int main()
{
Derive d;
pFunc* pvtable1 = (pFunc*)(*(int*)&d);
cout << "vtable1:" << endl;
PrintVTable(pvtable1);
pFunc* pvtable2 = (pFunc*)(*(int*)((char*)(int*)&d + sizeof(Base1)));
//(char*)(int*)&d 在虚表1指针的地址的基础上,
//((char*)(int*)&d + sizeof(Base1)) 先转化为char*,再往后加一个Base1的大小,得到虚表2指针的首地址处,
//(int*)((char*)(int*)&d + sizeof(Base1)) 再强转为int*,拿到虚表2指针的地址,
//解引用拿到虚表2指针
cout << "vtable2:" << endl;
PrintVTable(pvtable2);
return 0;
}
可以观察到func3()存在于虚表1中,即多继承中派生类新增虚函数的虚函数指针存储在它继承的第一个基类的虚表中。
静态成员函数可以是虚函数吗?
答:静态成员函数可以不通过对象来调用,没有隐藏的this指针。
而访问虚函数表必须要是基类对象的指针或引用来调用虚函数,直接用类名调用无法访问虚函数表(如果存在虚静态函数,直接用类名就无法访问)。所以静态成员函数不适合作为虚函数。
virtual成员函数的关键是动态类型绑定的实例调用。然而,静态函数和任何类的实例都不相关,它是class的属性。