前面介绍了封装和继承,本文就来介绍面向对象三大特征的最后一个—多态,多态就是面对不同对象时所展现的不同状态,下面就一起来看看吧
多态通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
举个例子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票
多态就是指不同继承关系的类对象,去调用同一函数,产生了不同的行为
实现多态需要借助虚函数表(虚表),而构成虚表又需要虚函数,最后再使用虚表指针来进行函数定位和调用
被virtual修饰的类成员函数称为虚函数
举个例子
class A
{
public:
virtual void fun() { cout << "A::fun()" << endl; }
};
上面的func() 就是虚函数,下面再来见见多态吧
#include
using namespace std;
class Person
{
public:
virtual void BuyTicket() { cout << "成人全价票" << endl; }
};
class Student : public Person
{
public:
virtual void BuyTicket() { cout << "学生半价票" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}

是否满足多态:满足继承前提,Func()函数中通过基类的引用p调用函数,被调用的函数BuyTicket()是虚函数,且派生类对基类的虚函数BuyTicket()进行了重写
上面说到了重写,那到底什么是虚函数的重写呢?
下面我们去掉派生类中的virtual关键字看看运行结果

这里为什么还是构成重写呢?
virtual关键字时,也可以构成重写,这里叫做接口继承(因为继承后基类的虚函数被继承下来了,继承了参数,也就是拷贝了一份基类的虚函数的参数),在派生类依旧保持虚函数属性,但是该种写法不是很规范,不建议这样使用。这里如果只是把基类中的virtual修饰关键字去掉能否构成虚函数重写呢?

显然是不能的,这是因为此时已经不满足派生类对基类的虚函数进行重写了,也就是不满足多态了,这里p.BuyTicket()中的p对象是Person类型的,所以两次调用都是调用的基类的BuyTicket()函数
最后再来看看父子类中的函数都不加virtual的情况

结果是必然的,因为此时已经不满足多态条件中的任何一条了。但是观察得到这里构成了继承中的隐藏,但是这里是基类对象去调用的基类成员函数,这里虽然构成隐藏,但是这里没有用子类对象去调用子类的成员函数BuyTicket(),也就不体现隐藏关系,只有子类调用才会体现隐藏关系
讲个经验:
满足多态看调用对象,不满足多态看调用对象类型
例外一:析构函数重写(虚函数名不同)
#include
using namespace std;
class Person
{
public:
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person
{
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}

上述中满足多态两个条件:通过基类Person的指针调用虚函数,同时也Person()构成了虚函数,并且子类的虚函数Studen()对父类进行了重写,但是这里的虚函数重写违反了派生类虚函数与基类虚函数的三同(返回值类型、函数名字、参数列表完全相同)中的函数名相同,但是为什么还成立呢?
destructor,这就可能存在析构错误调用的问题,因此可以利用 virtual 修饰父类的析构函数,这样子类在继承时,会自动形成多态,确保不同对象的析构函数能被成功调用,避免内存泄漏这里如果把基类的析构函数中的virtual关键字去掉呢?

这里发现程序直接崩溃了
p1和p2都是Person类型,所以都是调用的~Person()函数,析构了两次导致程序崩溃例外二:协变(返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(不常用,了解即可)
#include
using namespace std;
class A {};
class B : public A {};
class Person
{
public:
virtual A* f() {
cout << "Person() -> f()" << endl;
return new A;
}
};
class Student : public Person
{
public:
virtual B* f() {
cout << "Student() -> f()" << endl;
return new B;
}
};
void fun(Person& p)
{
p.f();
}
int main()
{
Person p;
fun(p);
Student s;
fun(s);
return 0;
}

下面来看一个题目
#include
using namespace std;
class A
{
public:
virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
virtual void test() { func(); }
};
class B : public A
{
public:
void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{
B* p = new B;
p->test();
return 0;
}
请问结果是什么?
解析:

题目改编一:
#include
using namespace std;
class A
{
public:
virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
virtual void test() { func(); }
};
class B : public A
{
public:
void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{
A* p = new B; //B* --> A* 对象赋值转换
p->test();
return 0;
}
最终结果和原题一样输出 B->1
题目改编二:
#include
using namespace std;
class A
{
public:
virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
};
class B : public A
{
public:
void func(int val = 0) { std::cout << "B->" << val << std::endl; }
virtual void test() { func(); }
};
int main(int argc, char* argv[])
{
B* p = new B;
p->test();
return 0;
}
解析:不满足多态

final和override是C++11中新增的两个多态相关的关键字
override关键字:

可见因为参数列表不同,无法完成重写,override关键字可以在编译之前就将其检查出来,以保证完成实现多态
final关键字:

可见父类的虚函数加上了final关键字后无法被子类的虚函数重写

三者总结:
重载:就是函数重载,函数名相同参数不同构成重载,不同的函数参数最终修饰结果不同,确保链接时不会出错
重写(覆盖):父子类中,当出现虚函数且符合重写的三同原则时,就会发生重写(覆盖)行为
重定义(隐藏):父子类中,当子类中的函数名与父类中的函数名相同时,会隐藏父类同名函数,默认调用子类的函数,可以通过 :: 指定调用
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类)
纯虚函数也可以与普通虚函数构成重写,也能实现多态,但是抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
#include
using namespace std;
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;
}
};
int main()
{
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
return 0;
}

抽象类适合用于描述无法拥有实体的类,比如动物、植物等,这些都是不能直接使用的,需要经过继承赋予特殊属性后,才能作为一个对象
先来看一道笔试题,这里问sizeof(Base)是多少?
#include
using namespace std;
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _a = 0;
int _b = 1;
char _c;
};
int main()
{
Base B;
cout << sizeof(B) << endl;
return 0;
}
在32位平台下,如果按照内存对齐的规则,这里应该是12字节大小,但真是如此吗?

这多出来的4个字节是哪里来的呢,调整到64位平台下,大小会变成24,大小能随平台变化而发生变化,我们第一个想到的自然是指针了,通过监视再来看一下

这里我们发现果然多了一个void**类型的指针_vfptr的指针
对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function,ptr代表pointer)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表,多态也就是依靠虚表指针+虚表实现了的
多态满足条件中有个条件是:必须通过基类的指针或者引用调用虚函数。那么这里怎么来实现的用引用或者指针就可以调用不同类的成员函数?为什么用普通类型就不能实现调用不用类的成员函数呢?
#include
using namespace std;
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person
{
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person pxx;
Func(pxx);
Student sxx;
Func(sxx);
return 0;
}

这就验证了,_vfptr指针就是用来调用不同类的成员函数的
先来看看非多态调用

不是多态时,直接按照对象的类型来调用对应的成员函数
再来看看多态调用

上述观察到,进行执行p.BuyTicket()语句时,编译器是不知道调用哪个类的成员函数的,当完成p.BuyTicket()语句后, 编译器做出了处理工作,直接跳转到Person::BuyTicket()成员函数。
证明一下在执行p.BuyTicket()语句时编译器是不知道调用哪个类的成员函数

虚函数的调用过程
满足虚函数重写
满足父类的指针或引用
有虚函数的类的对象中都有虚表指针_vfptr,虚表指针所指向的就是对应类的虚函数表(虚表),即virtual function table -> vft,虚表中存储的是虚函数指针,可以在调用函数时根据不同的地址调用不同的方法
下面来看一段代码
#include
using namespace std;
class Person
{
public:
virtual void func1() { cout << "Person::fun1()" << endl; };
virtual void func2() { cout << "Person::fun2()" << endl; };
void func3() { cout << "Person::fun3()" << endl; };
};
class Student : public Person
{
public:
virtual void func1() { cout << "Student::fun1()" << endl; };
virtual void func4() { cout << "Student::fun4()" << endl; };
};
int main()
{
Person p;
Student s;
return 0;
}
通过监视和内存来详细观察一下虚表指针,虚函数地址以及虚表的关系

再来看看把虚函数重写也叫做覆盖的原理

上述我们观察到
BuyTicket()成员函数进行了重写,但是travel()和ClaimCoupon()成员函数并没有重写,此时观察到Person类维护的有一个虚函数表,Student维护的也有一个虚函数表,BuyTicket()成员函数重写了也就覆盖了可以这么理解,原本的子类是继承父类的成员函数的,那么当前子类的虚函数表也就是父类的虚函数表,但是当子类对父类的虚函数重写后,原本的父类的虚函数就被覆盖为新的重写的虚函数,这个例子的体现就在
_vfptr[0]位置上的BuyTicket()虚函数指针。_vfptr[1]和_vfptr[2]两个虚函数并没有被重写,所以就是继承的父类的虚函数。
我们可以打印虚函数表来验证其真实性
nullptr为结束标记的(vs中)。虚函数表指针是对象前4个或者8个字节#include
using namespace std;
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 _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
virtual void Func4()
{
cout << "Derive::Func4()" << endl;
}
private:
int _d = 2;
};
typedef void(*_vfptr)(); //void(*_vfptr)() --> 函数指针 --> 起别名为_vfptr
void print_virtual_function(_vfptr table[]) //打印虚函数表 table -> 数组名 -> _vfptr*(类型)
{
for (int i = 0; table[i]; ++i) //虚函数表是以nullptr为结束标记的
{
printf("[%d]:%p\n", i, table[i]);
}
cout << endl;
}
int main()
{
Base b;
Derive d;
//只是支持32位机器下
//&b -> Base* -> (int*)&b -> 强制转换为int* -> (*(int*)&b) -> 拿到b对象的前四个字节 -> (_vfptr*)(*(int*)&b) -> 强制转换为_vfptr*(函数指针的指针)
print_virtual_function((_vfptr*)(*(int*)&b));
print_virtual_function((_vfptr*)(*(int*)&d));
//支持32和64位机器下
print_virtual_function((_vfptr*)(*(long long*)&b));
print_virtual_function((_vfptr*)(*(long long*)&d));
//支持32和64位机器下
//&b -> Base* -> (_vfptr**)&b -> Base*强制转换为_vfptr**(函数指针的指针) -> (*(_vfptr**)&b) -> _vfptr*
print_virtual_function((*(_vfptr**)&b));
print_virtual_function((*(_vfptr**)&d));
return 0;
}

我们可以发现Func4()这个函数是在Derive这个子类的虚函数表中的,只不过这个监视窗口进行了修饰而已,其实是有的。可以对打印窗口优化:
typedef void(*_vfptr)(); //void(*_vfptr)() --> 函数指针 --> 起别名为_vfptr
void print_virtual_function(_vfptr table[]) //打印虚函数表 table -> 数组名 -> _vfptr*(类型)
{
for (int i = 0; table[i]; ++i) //虚函数表是以nullptr为结束标记的
{
printf("[%d]:%p->", i, table[i]);
_vfptr f = table[i]; //拿到指针
f(); //调用类中的虚函数
}
cout << endl;
}
虚函数表补充:
visual studio 2019,有的平台中可能存储在静态区(数据段)上面讲解的时单继承中的虚表,总结一下就是子类中的虚函数对父类中对应的虚函数进行重写(覆盖)
单继承中新增虚函数
多继承也就会出现多个虚函数重写的情况,那么父类是如何处理不同虚表中的相同虚函数的重写呢?
#include
using namespace std;
class Base1
{
public:
virtual void func1() { cout << "Base1::func1()" << endl; }
virtual void func2() { cout << "Base1::func2()" << endl; }
};
class Base2
{
public:
virtual void func1() { cout << "Base2::func1()" << endl; }
virtual void func2() { cout << "Base2::func2()" << endl; }
};
class Derive : public Base1, public Base2
{
public:
virtual void func1() { cout << "Derive::func1()" << endl; }
virtual void func3() { cout << "Derive::func3()" << endl; } //子类新增虚函数
};
int main()
{
Derive d;
return 0;
}

可以看到此时子类拥有两张虚表
那么此时就出现了多继承中的两个主要问题了
func3位于哪个虚表中?func1 函数,在两张虚表中的地址不相同?首先来回答第一个问题,子类中新增的虚函数默认添加到第一张虚表中,可以通过打印虚表来验证
typedef void(*VF_T)();
typedef void(*VF_T)();
void PrintVFTable(VF_T table[])
{
//虚函数表是以nullptr为结束标记
int i = 0;
while (table[i])
{
printf("[%d]:%p->", i, table[i]);
VF_T f = table[i];
f(); //调用函数,相当于func()
i++;
}
cout << endl;
}
int main()
{
Derive d;
PrintVFTable(*(VF_T**)&d); //打印第一张虚表
PrintVFTable(*(VF_T**)((char*)&d + sizeof(Base1))); //跳过第一张虚表,打印第二张虚表
return 0;
}

运行代码可以看出,子类中新增的虚函数func3位于第一张虚表中
也可以通过切片的方式,天然的取到第二张虚表的地址
Base2* table2 = &d; //切片
PrintVFTable(*(VF_T**)table2); //打印第二张虚表
再来看看第二个问题的原因,这里会存在多继承中的虚函数冗余调用问题,编译器在调用时,根据不同的地址寻找到同一函数,来解决这一问题
通过反汇编来看看

根据汇编可以看的出来调用p1->func1()和p2->func1()最终都是调用Derive::func1()。需要注意的是p2->func1()函数中第一次jmp跳转到sub指令,这里的ecx寄存器就是存的this指针,通过减少4个字节找到Base1类的
这个过程叫做this指针修正,用于解决冗余虚函数的调用问题,这里会修正后继承的父类
这也就得到了第二个问题的答案
this指针修正的方式调用虚函数,根据不同的地址寻找到同一函数感谢大佬北 海提供的好图!
静态绑定
动态绑定
静态绑定就像函数重载,在编译阶段就确定了不同函数的调用;而动态绑定是虚函数的调用过程,需要 虚表指针+虚表,在程序运行时,根据不同的对象调用不同的函数
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main() {
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
关于多继承中指针偏移问题,下面说法正确的是什么?
由多继承虚函数表的相关知识可以的出,选择C,Base1和Base2是Derive父类,这里Base1先声明,先继承Base1
C++ 多态到这里就介绍结束了,本篇文章对你由帮助的话,期待大佬们的三连,你们的支持是我最大的动力!
文章有写的不足或是错误的地方,欢迎评论或私信指出,我会在第一时间改正