多态的概念
多态,通俗来说就是多种形态,具体就是去完成某个行为或功能,当不同的对象去完成时会产生出不同的状态。比如,乘火车买票这个行为,当普通人这个对象买票时,买的是全价;当学生这个对象买票时,买的是半价;军人这个对象买票时,是优先买票。
多态是在不同继承关系的类对象,去调用同一个函数,产生了不同的行为。比如Student继承了Person,Person对象买全票价,Student对象买半票价。
多态定义的构成条件:
1.调用函数的对象必须是指针或引用。
2.被调用的函数必须是虚函数,并且完成了虚函数的重写。
什么是虚函数?
虚函数就是在类的成员函数前面加上virtual关键字。
什么是虚函数的重写?
派生类中有一个根基类完全相同的虚函数,就称子类的虚函数重写了基类的虚函数。完全相同是指:函数名、参数、返回值都相同。虚函数的重写也叫作虚函数的覆盖。
看下面的例子:
上图中Peter和Tom两个对象都调用Func函数,但完成的功能却不一样,这就是虚函数的重写带来的效果。
但虚函数重写也有例外,那就是协变:重写的虚函数的返回值可以不同,但必须分别是基类和派生类的指针或引用。
例如下面的例子:
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;
}
};
还有一种不规范的重写行为:在派生类中重写的成员函数可以不加virtual关键字,也是可以构成重写的,,因为继承后基类的虚函数被继承下来了,在派生类中依然保持虚函数的属性,我们只是重写了它。但是这样做是非常不规范的,一般不这样使用。
析构函数的重写问题
基类中的析构函数如果是虚函数,那么派生类的析构函数就重写了基类的析构函数。这里他们的函数名不相同,看起来违背了重写的规则,但其实并不是这样的,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,这也说明基类的析构函数最好写成虚函数。
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。而虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。例如前面提到的买票行为,基类虚函数实现的是买票全价,而派生类继承基类虚函数的接口(函数名、参数、返回值相同),然后通过重写,使得函数里面的功能不同,从而达成多态。所以如果不实现多态,不要把函数定义成虚函数。
总结:重载、覆盖(重写)、隐藏(重定义)三个概念的对比
抽象类
在虚函数的后面写上=0,这个函数就为纯虚函数。包含纯虚函数的类叫抽象类(也叫接口类),抽象类不能实例化出对象,派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。因此,纯虚函数规范了派生类必须重写,纯虚函数更体现了接口继承。
总结一下纯虚函数的作用:
1.纯虚函数构成抽象类。
2.纯虚函数不能实例化出对象,也就没什么意义,所以纯虚函数就强制派生类重写纯虚函数。
举个例子:
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
void Test()
{
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
}
C++11中的override和final也可以修饰虚函数
final修饰基类的虚函数不能被派生类重写
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() //此处报错,无法重写final函数
{
cout << "Benz-舒适" << endl;
}
};
override修饰派生类虚函数强制完成重写,如果没有重写编译器就会报错。
class Car
{
public:
virtual void Drive(){}
};
class Benz:public Car
{
public:
virtual void Drive() override
{
cout << "Benz-舒适" << endl;
}
};
在实际中多建议使用纯虚函数+override的方式来强制重写纯虚函数,因为虚函数的意义就是通过重写来实现多态,如果没有重写,虚函数就没有意义。
多态的原理
先看一道面试题:
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _a = 1;
};
//sizeof(Base)是多少?
调试监视窗口:
通过测试我们发现结果是8,除了_a成员,还多了一个_vfptr放在对象的前面(但有些平台是放在对象的后面,跟平台有关,此平台是vs2013),对象中的这个指针我们叫做虚函数指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址要放到虚函数的表中。虚函数表也称为虚表,那派生类中的虚表放在哪儿呢?
请看代码:
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _a = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
调试监视窗口:
通过观察和测试,我们发现:
派生类对象中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,一部分是自己的成员。基类b对象和派生类d对象的虚表是不一样的,这里Func1完成了重写,所以虚表中存的是重写Drive::Func1,所以虚函数的重写也叫覆盖。覆盖就是指虚表中虚函数的覆盖,重写是语法上的叫法,覆盖是原理层的叫法。Func2继承下来后是虚函数,也放进了虚表,Func3继承了下来,但不是虚函数,所以不会放进虚表。
虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。如图所示:
总结一下派生类的虚表生成:
1.先将基类中的虚表内容拷贝一份到派生类虚表中。
2.如果派生类重写了基类中的某个虚函数,则用派生类自己的虚函数覆盖虚表中基类的虚函数。
3.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
容易混淆的问题:虚函数存在哪里?虚表存在哪里?
解答:虚表存的是虚函数的指针,虚函数的指针指向的是虚函数的地址。虚函数和普通函数一样。都是存在代码段(常量区)。
经过以上的分析,回到上面买票的例子中
当people指向Peter对象时,people.BuyTicket在Peter 的虚表中找到虚函数是People::BuyTicket,同理当people指向Tom对象时,people.BuyTicket在Tom的虚表中找到虚函数是Student::BuyTicket。这样就实现出了不同对象去完成同一个行为时,展现出不同的形态。
所以要达到多态,要有两个条件:一个是对象的指针或引用调用虚函数,一个是虚函数覆盖。
满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象中去找的,不满足多态的函数调用编译时就确认好了。
动态绑定与静态绑定
静态绑定又称前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,如:函数重载
动态绑定又称后期绑定(晚绑定),是在程序运行期间根据具体拿到的对象确定程序的具体行为,调用具体的函数,也称为动态多态。
单继承和多继承关系的虚函数表
单继承中的虚函数表
看以下代码:
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;
};
int main()
{
Base b;
Derive d;
return 0;
}
调试监视窗口:
仔细观察监视窗口我们发现看不见func3和func4。这是为什么呢?这里其实是编译器的监视窗口故意隐藏了这两个函数,也可以认为是编译器的一个bug。所以我们只能通过使用代码打印出虚表中的函数。
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;
};
typedef void(*VFptr)();
void PrintVTable(VFptr VTable[])
{
cout << "虚表地址" << VTable << endl;
for (int i = 0; VTable[i] != nullptr; ++i)
{
printf("第%d个虚函数地址:0x%x,->", i, VTable[i]);
VFptr f = VTable[i];
f();
}
cout << endl;
}
int main()
{
Base b;
Derive d;
//思路:
//取出b、d对象的前4个字节,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
// 1.先取b的地址,强转成一个int*的指针
// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
// 4.虚表指针传递给PrintVTable进行打印虚表
// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。
VFptr* VTable = (VFptr*)*(int*)&b;
PrintVTable(VTable);
VFptr* vTable = (VFptr*)*(int*)&d;
PrintVTable(vTable);
return 0;
}
运行结果:
多继承中的虚函数表
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;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));
PrintVTable(vTableb2);
return 0;
}
运行结果:
多态常见的面试问题:
1. 什么是多态?
2. 什么是重载、重写(覆盖)、重定义(隐藏)?
3. 多态的实现原理?
4. inline函数可以是虚函数吗?答:不能,因为inline函数没有地址,无法把地址放到虚函数表中。
5. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式 无法访问虚函数表,所以静态成员函数无法放进虚函数表。
6. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始 化的。
7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义 成虚函数。
8. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针对象或者是 引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
9. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数是在编译阶段就生成的,一般情况下存在常量区的。
10. C++菱形继承的问题?虚继承的原理?答:注意这里不要把虚函数表和虚基表搞混了。
11. 什么是抽象类?抽象类的作用?答:参考(抽象类)。抽象类强制重写了虚函数,另外抽象类体现出 了接口继承关系。