🌈前言
本篇文章进行C++中类多态(polymorphism)的学习!!! 本文章代码在x64程序中测试,涉及指针是8bytes的,如果为x86,涉及指针则为4bytes
🌸 多态
🌷1、多态的概念
- 多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为时,不同的对象去完成会产生出不同的状态
- 举个栗子1??:比如坐地铁买票时,普通人买票是全价;而学生买票时,是半价买票;军人买票时,是优先买票
- 举个栗子2??:比如过年时,有些软件就会搞一些刷视频邀请人就能获得红包的活动,邀请新人获得的红包可能是0-99r,新人刷视频得到的金额搞一些,老用户少一些,这也是多态的一种
🌸2、多态的定义及实现
🌹2.1、多态的构成条件
- 前言:多态是在不同继承关系的类对象,去调用同一个函数,产生不同的行为。比如:Student继承了Person,则Person对象买票全价,Student对象买票半价
在继承中构成多态的条件有两种:
- 1、必须通过基类的指针或者引用调用虚函数
- 2、被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写(覆盖)
🌺2.2、虚函数
- 虚函数:即被virtual修饰的类成员函数称为虚函数
class Person
{
public:
virtual void BuyTicket() { cout << "买票全价" << endl; }
};
🌻2.3、虚函数的重写
跳转到多态的原理
- 虚函数的重写(覆盖):派生类中有一个与基类完全相同的虚函数,即派生类虚函数与基类虚函数的返回值类型、函数名、参数列表完全相同,称派生类的虚函数重写了基类的虚函数
class Person
{
public:
virtual void BuyTicket() { cout << "买票全价" << endl; }
};
class Student : public Person
{
virtual void BuyTicket() { cout << "买票半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
Func(p);
Student s;
Func(s);
return 0;
}
虚函数重写的二个例外:(1)
- 协变(基类与派生类虚函数的返回值类型不同)
-派生类重写基类虚函数时,与基类虚函数返回值类型不同,即基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类的指针或引用时,称为协变
class A {};
class B : public A {};
class Person
{
public:
virtual A* f() { return new A; }
};
class Student : public Person
{
public:
public:
virtual B* f() override { return new B; }
};
void Test(Person& p)
{
A* ps = p.f();
}
int main()
{
Person p;
Student s;
Test(p);
Test(s);
return 0;
}
2. 析构函数的重写(基类与派生类析构函数的名称不同)
- 如果基类的析构函数为虚函数,则派生类析构函数只需要定义就行了,无论是否添加virtual,都与基类的析构函数构成重写
- 虽然函数名不同,看起来违背了重写规则,但是,这里可以理解为编译器对析构函数做了特殊处理,编译后析构函数的名称统一处理成destructor(析构函数)
class Person
{
public:
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student :public Person
{
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
void Test()
{
Person* p = new Person;
Person* ps = new Student;
delete p;
delete ps;
}
🌼2.4、C++11中override 和 final
🌈前言:
- 从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失
- 因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写
1. final:修饰虚函数,表示该虚函数不能再被重写
class A
{
public:
virtual void Test() final {}
};
class B : public A
{
public:
virtual void Test() {}
};
2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class A
{
public:
virtual void Test(){}
};
class B : public A
{
public:
virtual void Test() override {}
};
🌽2.5、重载、覆盖(重写)、隐藏(重定义)的对比
🌿3、抽象类
🍀3.1、概念
纯虚函数:在虚函数的后面写上=0,即virtual = 0,则这个函数为纯虚函数
- 抽象类:包含纯虚函数的类叫做抽象类(接口类),抽象类不能实例化出对象。
- 只有重写纯虚函数,派生类才能实例化出对象
- 纯虚函数规范了派生类必须重写它,另外纯虚函数更体现除了接口继承
class Base
{
public:
virtual void Show() = 0;
};
class Derive1 : public Base
{
public:
virtual void Show() { cout << "Derive1::Show()" << endl; }
};
class Derive2 : public Base
{
public:
virtual void Show() { cout << "Derive2::Show()" << endl; }
};
void Test()
{
Base* pb1 = new Derive1;
pb1->Show();
Base* pb2 = new Derive2;
pb2->Show();
delete pb1;
delete pb2;
}
🍁3.2、接口继承和实现继承
接口继承和继承的区别:
- 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现
- 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承是接口
- 所以说,如果不实现多态,不要把函数定义成虚函数
🍂4、多态的原理
🍃 4.1、虚函数表
class Test
{
public:
virtual void f1() {}
private:
int _a = 0;
};
int main()
{
Test t;
cout << sizeof(t) << endl;
return 0;
}
- 通过观察我们发现t的对象所占内存是16bytes,除了_a成员,还多一个_vfptr放在成员变量的最前面(主题有些平台可能会放到对象的最后面,这个跟平台有关)
- 对象中的这个指针我们叫做虚函数表指针(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() override
{
cout << "Derive::Func1()" << endl;
}
private:
int _b = 2;
};
void Test(Base* b)
{
b->Func1();
b->Func2();
b->Func3();
}
int main()
{
Base b;
Derive d;
Test(&b);
Test(&d);
return 0;
}
通过调试,我们发现了一下几点问题:
- 派生类对象d中也有一个虚表指针,d对象由二部分构成,一部分是父类继承下来的成员,虚表指针也是继承的一部分,另一部分是派生类自己的成员
- 基类b对象和派生类d对象的虚表是不一样的,这里我们发现Func1完成了重写,所以虚函数的重写也叫做覆盖。覆盖是指虚表中虚函数的覆盖
- Func2继承下来后是虚函数,放进了虚表,Func3也继承下来了,但不是虚函数,没有放进虚表,而是存储在公共代码区中
- 虚表本质是一个存储虚函数指针的数组,一般这个数组最后面放了一个nullptr
- 派生类虚表的生成:1、先将基类中虚表拷贝一份到派生类虚表中 2、如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 3、派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
- 虚函数存在哪?虚表存在哪?很多人说:虚函数存在虚表中,虚表存在对象中,注意这是错误的说法。注意:虚表存的是虚函数指针,虚函数和普通函数一样存在代码段中,只是它的指针又存到了虚表中,另外对象存的不是虚表,存的是虚表指针
- 虚表在vs中是存在代码段中的,Linux存放在可执行文件的只读数据段中(rodata),这与微软的编译器将虚函数表存放在常量段存在一些差别
🍄 4.2、多态的原理
多态的原理:
- 还记得上面的买票例子吗?跳转到前面例子
- Func函数传Person调用的Person::BuyTicket,传Student调用的时Student::BuyTicket
- 观察下图红色箭头可以发现,p指向mike对象时,p->BuyTicket在mike的虚表中找到的是Person::BuyTicket
- 观察下图蓝色箭头可以发现,p指向johnson对象时,p->BuyTicket在johnson虚表中找到的是Student::BuyTicket
- 通过下面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是在运行起来后到对象到找的。不满足多态的函数调用时编译阶段就确认好了
void Func(Person* p)
{
p->BuyTicket();
}
int main()
{
Person mike;
Func(&mike);
mike.BuyTicket();
return 0;
}
void Func(Person* p)
{
...
p->BuyTicket();
001940DE mov eax, dword ptr[p]
001940E1 mov edx, dword ptr[eax]
00B823EE mov eax, dword ptr[edx]
001940EA call eax
00头1940EC cmp esi, esp
}
int main()
{
...
mike.BuyTicket();
00195182 lea ecx, [mike]
00195185 call Person::BuyTicket(01914F6h)
...
}
🍅 4.3、动态绑定与静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态
🍆 5、单继承与多继承关系的虚函数表
🍇 5.1、单继承中的虚表
class Base
{
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a = 0;
};
class Derive : public Base
{
public:
virtual void func1() { cout << "Derive::func1" << endl;}
virtual void func2() { cout << "Derive::func2" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
virtual void func4() { cout << "Derive::func4" << endl; }
private:
int b = 0;
};
typedef void (*VFPTR) ();
void PrintVTable(VFPTR* vTable)
{
cout << "虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
std::printf("第%d个虚函数地址: 0x%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Base b;
Derive d;
VFPTR* vTableb = (VFPTR*)(*(long long*)&b);
PrintVTable(vTableb);
VFPTR* vTabled = (VFPTR*)(*(long long*)&d);
PrintVTable(vTabled);
return 0;
}
🍈 5.2、多重继承中的虚表
这里在x86下测试
class Base1
{
public:
virtual void Fun1() { cout << "Base1::Fun1()" << endl; }
virtual void Fun2() { cout << "Base1::Fun2()" << endl; }
private:
int b1 = 0;
};
class Base2
{
public:
virtual void Fun1() { cout << "Base2::Fun1()" << endl; }
virtual void Fun2() { cout << "Base2::Fun2()" << endl; }
private:
int b2 = 1;
};
class Derive : public Base1, public Base2
{
public:
virtual void Fun1() { cout << "Derive::Fun1()" << endl; }
virtual void Fun3() { cout << "Derive::Fun3()" << endl; }
private:
int d = 2;
};
using VFPTR = void(*) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
std::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;
}
🍈5.3.、菱形继承和菱形虚拟继承
- 实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗,这里就不对其进行讲解了
- C++ 虚函数表解析
- C++ 虚表的内存布局
感谢支持!!!
|