感谢各位 点赞 收藏 评论 三连支持
本文章收录于专栏【C++进阶】
❀希望能对大家有所帮助❀
本文章由 风君子吖 原创
回顾
上篇文章,我们学习了继承的相关知识,详细解刨了继承中的各种细节,而本章内容将在继承的基础上学习多态
多态的概念
多态的概念:从字面意思是上来看,就是多种形态。具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态(结果)。
先来举一个日常生活中的例子:我们在路上走着走着,突然手机一响,收到你的好朋友发来的信息,你欣喜地打开跟他的聊天,却发现他竟然给你发了一个拼多多的砍价链接,这时候,你对他回了一句,我没有下载拼多多,然后他却更加开心地回了你一句,新用户砍得更多!
这就是一种多态,明明都是手机用户,为什么新用户能砍的更多,这是不是就是不同的用户进行同样的操作但是却有不同的(状态)结果。
class User { public: virtual void chop() { cout << "只能砍一点点" << endl; } }; class NewUser : public User { public: virtual void chop() { cout << "砍得更多!" << endl; } }; class ReturnUser : public User { public: virtual void chop() { cout << "砍得比较多" << endl; } }; void func(User& user) { user.chop(); } int main() { User us; NewUser nus; ReturnUser rus; func(us); func(nus); func(rus); return 0; }
多态的构成条件
想要实现多态,需要满足以下条件
1. 必须通过基类的指针或者引用调用虚函数
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
虚函数和虚函数重写
在类中被关键字virtual修饰的函数就叫做虚函数,而虚函数在继承体系中是可以构成重写的!
那么什么是虚函数的重写?
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的
返回值类型、函数名字、参数列表完全相同【三同】),则称子类的虚函数重写了基类的虚函数。
这与普通函数的重载不同,函数的重载是需要参数的不同,且在同一作用域,而重写是针对虚函数的。
那么总结下来,虚函数要完成重写需要满足以下条件
1.这个函数得在基类和派生类中是虚函数(在派生类中可以省略掉virtural关键字,可以理解为是从基类继承下来的,但是推荐写上)。
2.函数要满足三同(返回值类型、函数名字、参数列表完全相同)。
可是有两个例外
1.协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指
针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
这种是C++的一种例外情况,但是很少会用到,仅做了解即可。
class A {
public:
virtual A& func()
{
cout << "A::func()" << endl;
}
};
class B : public A
{
virtual B& func()
{
cout << "B::func()" << endl;
}
};
也能构成虚函数的重写
2.析构函数的多态
class A {
public:
virtual A& func()
{
cout << "A::func()" << endl;
}
virtual ~A(){} //析构函数可以构成多态,这是因为底层会将类的析构函数统一命名为destructor
};
class B : public A
{
virtual B& func()
{
cout << "B::func()" << endl;
}
virtual ~B() {}
};
这是因为编译器就会在底层将类的析构函数都统一命名为destructor,所以才能构成多态,毕竟要实现三同!
那么再来看多态的构成条件
1. 必须通过基类的指针或者引用调用虚函数
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
为什么必须是基类的指针或者是引用调用虚函数?
先来看看如果不是指针和引用来调用函数的情况。
注意这里的func函数的参数去掉了&
class User { public: virtual void chop() { cout << "只能砍一点点" << endl; } }; class NewUser : public User { public: virtual void chop() { cout << "砍得更多!" << endl; } }; class ReturnUser : public User { public: virtual void chop() { cout << "砍得比较多" << endl; } }; void func(User user) { user.chop(); } int main() { User us; NewUser nus; ReturnUser rus; func(us); func(nus); func(rus); return 0; }
从结果来看,是没有构成多态的。
那么这是为什么?大家其实可以运用我们之前学习过的继承的知识来解答,因为派生类在转换为基类时,会发生切片 ,这种切片行为如果不是引用或者指针的话,会把派生类的那一部分完完全全地切下来,而如果是引用或者指针,只是限制了你的访问范围。
所以,要想实现多态,是十分严格的,不满足任意一个条件都不行!
多态的原理(重点)
不知道大家对于C++调用函数的方式有没有过了解
对于非类成员函数,C++会在代码区的全局中寻找对应的函数
对于被指定了命名空间的非类成员函数,需要指定命名空间才能调用到这个函数
对于类成员函数,它会根据对象类型在类域中寻找对应的函数!
而多态的方式与以上任何方式都不同,因为多态如果采用以上任意一种方式,都无法实现出多态的效果! 那么多态是如何实现准确调用函数的呢?
先说结论 -- 是通过虚表指针(virtual function pointer)来找到调用的函数!
虚表指针(重点)
先不说虚表指针是干嘛用的,先来看看这东西在哪里,我们再来顺藤摸瓜地了解它是干嘛用的。
class User {
public:
virtual void chop()
{
cout << "只能砍一点点" << endl;
}
int _a;
};
int main()
{
User u;
cout << sizeof(u) << endl;
return 0;
}
你认为User类的大小是多少?(在32位系统下)
为什么是8呢?这里不是只有一个int 类型吗 而int类型是占4个字节
这是因为如果类中有虚函数,那么这个类就会有一个虚表指针,而指针在32位系统下占4个字节,在64位系统下占8个字节。
如果我们调用调试监控,就能发现虚表指针的存在。
而虚表指针,它是一个什么呢? 先说结论 -- 虚表指针是一个函数指针数组,指向的是虚表(virtual function table)。
虚表就是存放虚函数地址的地方。
而虚表指针就是一个指向虚表的函数指针数组,里面存放着这个类的所有虚函数的地址。
而多态,就是通过虚表指针来实现调用对应的函数的。
注意:虚表指针是对象构造的时候生成的,所以构造函数和拷贝构造不能实现多态(条件也不符合),虚表是编译就存在的,虚表存放在代码区
多态的调用(重点)
那么多态的调用函数 跟其他普通函数到底有什么不同之处?
我们举以下例子
class User {
public:
virtual void chop()
{
cout << "只能砍一点点" << endl;
}
int _a;
};
class NewUser : public User {
public:
virtual void chop()
{
cout << "砍得更多!" << endl;
}
};
int main()
{
NewUser nu;
User* ptr1 = ν
ptr1->chop(); //构成多态,是多态的函数调用
User user = nu;
user.chop(); //不构成多态,是普通函数的调用
return 0;
}
光看结果是没有用的,我们进入调试,然后切到反汇编来进行观察这两个函数的调用。(32位系统下,64位系统下稍有区别)
所以结论就是,多态的调用是根据eax寄存器中存放的指针来决定call的函数地址在哪里,这种调用需要程序在运行的过程中将虚表指针指向的对应的虚函数存入eax寄存器中,这种调用方式,我们称为“运行时决议”。
class A { public: virtual void func1() { cout << "A::func()" << endl; } virtual void func2() { cout << "A::func()" << endl; } int _a = 1; }; class B : public A { public: virtual void func1() { cout << "B::func()" << endl; } int _b = 2; }; int main() { A a; B b; A* ptr1 = &b; ptr1->func2(); }
虚表指针的作用
附加知识
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实
现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成
多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
C++11 新增的两个关键字 override 和 final
这是c++11 新增的两个关键字
override
作用:用于识别是否构成了虚函数的重写,如果构成了则无事发生,如果没有构成,则报错!
class A { public: void func1() { cout << "A::func()" << endl; } int _a = 1; }; class B : public A { public: virtual void func1() override { cout << "B::func()" << endl; } int _b = 2; }; 不构成重写
error C3668: “B::func1”: 包含重写说明符“override”的方法没有重写任何基类方法
final
作用:修饰虚函数,表示该虚函数不能再被重写
class A { public: virtual void func1() final //加了final ,该虚函数不可被重写 { cout << "A::func()" << endl; } }; class B : public A { public: virtual void func1() { cout << "B::func()" << endl; } };
error C3248: "A::func1": 声明为 "final" 的函数不能由 "B::func1" 重写
抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口
类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生
类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class A {
public:
virtual void func1() = 0;
int _a = 1;
};
class B : public A
{
public:
virtual void func1()
{
cout << "B::func()" << endl;
}
int _b = 2;
};
int main()
{
//A a; 抽象类无法实例化
B b;
A* prt1 = &b;
prt1->func1();
return 0;
}
单继承调试器的小缺陷
多态有多种情况,而有这样么一种情况,派生类有基类没有的虚函数,那么这个虚函数是否还会进入虚表之中呢? 答案是肯定的,只要是虚函数就一定会进入虚表! 但是在VS调试窗口中是看不见的,可以理解为是调试器的小缺陷。
class A {
public:
virtual void func1()
{
cout << "A::func1()" << endl;
}
virtual void func2()
{
cout << "A::func2()" << endl;
}
int _a = 1;
};
class B : public A
{
public:
virtual void func1()
{
cout << "B::func1()" << endl;
}
virtual void func2()
{
cout << "B::func2()" << endl;
}
virtual void func3()
{
cout << "B::func3()" << endl;
}
int _b = 2;
};
那么怎么证明这个虚函数是在虚表的? 空口无凭,我们直接通过内存观测窗口来证明!
注意:如果观测结果有误,需要重新生成一下解决方案!
从内存窗口来看,是可以看到虚表中存在三个函数指针的,并且在VS编译器下,会在虚表的后面以nullptr作为结束条件。 (其他编译器处理方式不同,有的编译器没有对虚表结尾做处理)
用歪门邪道调用虚函数
既然虚表中存放了虚函数的地址,那么我们就可以用一些歪门邪道来调用虚函数。
class A {
public:
virtual void func1()
{
cout << "A::func1()" << endl;
}
virtual void func2()
{
cout << "A::func2()" << endl;
}
int _a = 1;
};
class B : public A
{
public:
virtual void func1()
{
cout << "B::func1()" << endl;
}
virtual void func2()
{
cout << "B::func2()" << endl;
}
virtual void func3()
{
cout << "B::func3()" << endl;
}
int _b = 2;
};
typedef void(*VFP)(); 重定义void函数指针类型为 VFP
void Print(VFP* VFTable)
{
for (int i = 0; VFTable[i] != nullptr; ++i)
{
printf("第%d个虚函数地址是%p: ",i + 1,VFTable[i]);
VFTable[i]();
}
}
int main()
{
B b;
Print((VFP*)(*(int*)&b));
return 0;
}
需要注意的是,因为在VS编译器下,虚表会以nullptr作为结束标志,其他编译器如果想实现这种效果就不能以 VFTable[i] != nullptr 作为终止条件 ,只能提前知晓他有几个虚函数,然后设置循环几次。
多继承中的虚函数表(重点)
那么对于多继承的多态,会有什么变化呢?
class A { public: virtual void func1() { cout << "A::func1()" << endl; } virtual void func2() { cout << "A::func2()" << endl; } int _a = 1; }; class B { public: virtual void func1() { cout << "B::func1()" << endl; } virtual void func2() { cout << "B::func2()" << endl; } int _b = 2; }; class C : public A,public B { public: virtual void func1() { cout << "C::func()" << endl; } virtual void func3() { cout << "C::func3()" << endl; } int _c = 3; }; typedef void(*VFP)(); void Print(VFP* VFTable) { for (int i = 0; VFTable[i] != nullptr; ++i) { printf("第%d个虚函数地址是%p: ",i + 1,VFTable[i]); VFTable[i](); } } int main() { C c; VFP* vTable1((VFP*)(*(int*)&c)); VFP* vTable2 = (VFP*)(*(int*)((char*)&c + sizeof(A))); Print(vTable1); cout << endl; Print(vTable2); cout << endl; return 0; }
从结果来看,多继承的多态,是会有多个虚表的,取决于你继承了几个类,而派生类的独有虚函数fun3是被存放在最先被继承的基类的虚表中的。
这里有一个疑问,为什么这里两个虚表中都被重写的func1的地址不同,但是都调用到了func1,这个涉及到了比较底层的问题,有一些编译器的行为,由于涉及较偏,这里不作解释,只用知道通过这个地址都能调用到同一个函数即可,如果感兴趣的话,可以通过反汇编来理解。
注意:如果打印不出来,需要重新生成一下解决方案!
对于多继承,它的构造函数我们也了解一下
class A { public: virtual void func1() { cout << "A::func1()" << endl; } A(int a = 1) :_a(a) {} protected: int _a; }; class B { public: virtual void func1() { cout << "B::func1()" << endl; } B(int b = 2) :_b(b) {} protected: int _b; }; class C : public A,public B { public: virtual void func1() { cout << "C::func()" << endl; } C(int a = 1, int b = 2, int c = 3) :A(a) ,B(b) ,_c(c){} protected: int _c; }; int main() { C c; return 0; }
菱形继承、菱形虚拟继承 + 多态(了解)
菱形继承特别是菱形虚拟继承如果还要再加入多态,那就太复杂了,上个文章我就讲了,菱形继承实在不推荐用,会出现比较多的问题。
菱形继承与多继承类似,都会有两个虚表
class A {
public:
virtual void func1()
{
cout << "A::func1()" << endl;
}
virtual void func2()
{
cout << "A::func2()" << endl;
}
int _a = 1;
};
class B : public A
{
public:
virtual void func1()
{
cout << "B::func1()" << endl;
}
virtual void func2()
{
cout << "B::func2()" << endl;
}
int _b = 2;
};
class C : public A
{
public:
virtual void func1()
{
cout << "C::func()" << endl;
}
virtual void func3()
{
cout << "C::func3()" << endl;
}
int _c = 3;
};
class D : public B, public C
{
public:
virtual void func1()
{
cout << "D::func1()" << endl;
}
virtual void func4()
{
cout << "D::func4()" << endl;
}
int _d = 4;
};
int main()
{
D d;
VFP* vTable1((VFP*)(*(int*)&d));
VFP* vTable2 = (VFP*)(*(int*)((char*)&d + sizeof(B)));
Print(vTable1);
cout << endl;
Print(vTable2);
cout << endl;
return 0;
}
注意:如果打印不出来需要重新生成一下解决方案!
菱形虚拟继承+多态(了解,难)
这里只做细微讲解,只看一下结构,和一些注意事项
因为菱形虚拟继承会改变原有的模型结构,这个时候调试窗口的结构已经不太方便我们观察,我们使用内存调试窗口来进行观察。
class A {
public:
virtual void func1()
{
cout << "A::func1()" << endl;
}
int _a = 1;
};
class B : virtual public A
{
virtual void func1()
{
cout << "B::func1()" << endl;
}
virtual void func2()
{
cout << "B::func2()" << endl;
}
int _b = 2;
};
class C : virtual public A
{
virtual void func1()
{
cout << "C::func1()" << endl;
}
virtual void func2()
{
cout << "C::func2()" << endl;
}
int _c = 3;
};
class D : public B, public C
{
public:
virtual void func1()
{
cout << "D::func1()" << endl;
}
virtual void func2()
{
cout << "D::func2()" << endl;
}
virtual void func3()
{
cout << "D::func3()" << endl;
}
int _d = 4;
};
typedef void(*VFP)();
void Print(VFP* VFTable)
{
for (int i = 0; VFTable[i] != nullptr; ++i)
{
printf("第%d个虚函数地址是%p: ",i + 1,VFTable[i]);
VFTable[i]();
}
}
int main()
{
D d;
cout << "sizeof(D):" << sizeof(D) << endl;
cout << "sizeof(B):" << sizeof(B) << endl;
cout << "sizeof(C):" << sizeof(C) << endl;
VFP* vTable1 = ((VFP*)(*(int*)&d));
VFP* vTable2 = (VFP*)(*(int*)((char*)&d + 12));
VFP* vTable3 = (VFP*)(*(int*)((char*)&d + 28));
Print(vTable1);
cout << endl;
Print(vTable2);
cout << endl;
Print(vTable3);
return 0;
}
菱形虚拟继承的构造注意事项
class A { public: virtual void func1() { cout << "A::func1()" << endl; } A(int a = 1) :_a(a){} int _a; }; class B : virtual public A { public: virtual void func1() { cout << "B::func1()" << endl; } B(int a = 1,int b = 2) :A(a) ,_b(b) {} int _b; }; class C : virtual public A { public: virtual void func1() { cout << "C::func1()" << endl; } C(int a = 1, int c = 3) :A(a) , _c(c) {} int _c; }; class D : public B, public C { public: virtual void func1() { cout << "D::func1()" << endl; } D(int a = 5 , int b = 2, int c = 3, int d = 4) :A(a) ,B(b) ,C(c) ,_d(d) {} int _d = 4; }; int main() { D d; cout << d._a << endl; }
实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的
模型,访问基类成员有一定得性能损耗。