CPP学习之多态
1. 多态的概念
多态是一种根据本体的身份的不同而调用不同功能的能力。比如买票,成年人全票,学生半票,儿童免票,而且买票主体都是人。
2. 定义及实现
多态要在同一个继承体系内才能实现。
构成多态的两个条件:
- 必须是基类的指针或引用来调用虚函数;
- 被调用的必须是虚函数,且虚函数必须经派生类重写。
示例:
class person
{
public:virtual void func() const //用virtual修饰的类的成员函数都叫虚函数// virtual不能修饰类外的普通函数!!!{cout << "person的虚函数" << endl;}
};class stu : public person
{
public:virtual void func() const{cout << "stu的虚函数" << endl;}
};void func_1(const person& p)
{p.func();
}void test_1()
{func_1(person()); //采用匿名函数(const修饰过的)作为形参func_1(stu()); //此处是切片,即派生类对象赋值给基类对象//同一函数传不同对象会产生不同结果
}
关于虚函数的重写:
-
条件
虚函数重写需要函数的返回值、函数名、形参(形参的所有特征)都相同,简称“三同”。 -
语法:
重写的虚函数在基类上要加上virtual关键字;派生类里重写该虚函数时可以不加virtual关键字,但建议都加上。 -
虚函数重写的两个例外
- 协变(基类和派生类虚函数返回值不同):基类中虚函数返回值是基类指针或者引用,派生类中重写的虚函数的返回值是派生类的指针或引用;
//基类中://virtual person* func() const //协变//{// cout << "person的虚函数" << endl;//}//派生类中//virtual stu* func() const //协变//{// cout << "stu的虚函数" << endl;// return 0;//}
- 析构函数(函数名不同):基类中在析构函数前加virtual关键字,这样派生类的析构函数即是重写了基类的析构函数。(因为类的析构函数都被处理成了destructor这个统一的名字。)
//基类中virtual ~person() { cout << "~person()" << endl; }//派生类中virtual ~stu() { cout << "~stu()" << endl; }//注意:析构函数加上virtual也是函数重写,因为类的析构函数都被处理成了destructor这个统一的名字。//为何统一名字,因为要重写。为何重写?看test_2
在构成多态中,析构函数必须重写!
void test_2()
{person* p = new person; //指向persondelete p;p = new stu; //指向stu,但问题是p的类型还是person*delete p; //为了可以正常delete掉p处的stu空间,需要用到多态(根据指向对象来调用函数,而非当前类型),所以析构函数也要重写 // delete步骤:先析构,再delete
}
- final和override关键字
- final关键字修饰基类虚函数,可以让该虚函数不能被重写;
- override关键字修饰派生类中重写的虚函数,可以检查虚函数是否被重写,如果没有被重写就会报错。(在形参和函数主体之间修饰)
//class Car
//{
//public:
// virtual void Drive() final {}
//};
//class Benz :public Car
//{
//public:
// virtual void Drive() { cout << "Benz-舒适" << endl; } //报错
//};// override修饰派生类虚函数,检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
//class Car
//{
//public:
// virtual void Drive(){}
//};
//class Benz :public Car
//{
//public:
// virtual void Drive() override { cout << "Benz-舒适" << endl; }
//};
//如何设计一个不想被继承的类?
//法一:将该类的构造函数私有化,那要创建该类对象得另造函数create_obj()
//法二:将该类的析构函数私有化,创建对象时通过new来创建,主动在该类中写调用析构函数的函数destroy_obj()
//法三:C++11中在类名后面写final关键字
//class A
//{
//private:
// A() {}
//
//public:
// static A create_obj() //使用静态功能
// {
// return A();
// }
//};
//
//class B : public A
//{
//};
//
//int main()
//{
// A aa = A::create_obj();
//}
- 重载、重写、重定义的区别:
重载需要在同一作用域;在继承体系中,重写必须是虚函数,不是重写那么就是重定义。
3. 抽象类
包含纯虚函数的类叫做抽象类。
纯虚函数:在基类中的虚函数声明后面加上“=0”,不写函数定义。
抽象类不能实例化出对象!而且继承该基类的派生类也不能实例化出对象,只有重写了纯虚函数,派生类才能实例化出对象。
纯虚函数规范了派生类必须将其重写。
纯虚函数也体现了接口继承(从基类中继承了虚函数的壳子,即虚函数声明)。
接口继承与实现继承:
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
4. 原理
要想了解多态原理,我们需要知道什么是虚函数表。
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};
看上面的代码,如果要创建一个Base对象,那这个对象的大小是多少(sizeof)?
在调试过程中我们可知这个对象包含了一个名字为__vfptr的“void**”指针和_b变量,总大小是8byte(x86)。
这个void**指针指向的是虚函数表的地址,虚函数表实际上是一个虚函数指针数组,存放虚函数的地址。
//| 术语 | 含义 |
//| 函数版本 | 指具体哪个类的函数实现被调用 |
//| 函数签名 | 指函数名称、参数列表、返回类型等 |
//| 函数体(实现)| 指函数的实际代码内容 |//| 类型 | 定义 | 什么时候确定 | 作用 |
//| 静态类型 | 编译器在编译阶段看到的类型(由变量声明决定) | 编译期 | 决定默认参数、函数重载、模板匹配 |
//| 动态类型 | 运行时对象实际拥有的类型(由对象创建时的类型决定) | 运行期 | 决定虚函数调用哪个实现 |//如何确定指针的静态类型和动态类型?
//静态类型是声明的时候决定好的;动态类型是指这个指针指向的对象的类型
//如 A* p = new B(); 其中p的静态类型是A*,动态类型是B*//C++ 的多态机制(虚函数)就是靠“静态类型”和“动态类型”的差异实现的:
//虚函数调用(动态绑定):根据动态类型决定调哪个函数。
//默认参数、函数重载、模板匹配等:根据静态类型决定。
//题目:下面输出的结果是什么?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;
//}//分析:
// 1.由p的定义可知,p的静态类型和动态类型都是B*;
// 2.通过p调用test()时,因为A与B是继承关系,且test是A的非静态成员函数,this指针的静态类型仅能是A*,
// 但this指针指向空间保存的是B对象;
// 3.因为this指针指向空间保存的是B对象,所以在调用func函数时,使用的函数版本是B的func,而声明由静态类型决定,所以val==1
// 注:指向派生类的指针本就可以当作指向基类的指针来用(与切片类似),所以在调用test时并没有发生任何指针的隐式类型转换。
// 而且这道题并不构成多态!因为多态调用必须是由外部代码通过“基类指针或引用”第一次发起对虚函数的调用。
//
//结果:输出B->1
//
//总结:
// 主线1:默认参数只看静态类型 -- 编译器写死,永远按指针/引用的声明类型找默认值,和对象实际类型无关。
// 主线2:虚函数只看动态类型 -- 运行时才决定到底执行哪一份函数体,跟指针/引用的声明类型无关。
// 默认参数看指针声明,函数实现看对象真身。
// 虚函数重写的是实现,套用的是父类的壳子,是一种接口继承,所以派生类中可以不写virtual,且参数值找的肯定是父类声明中的缺省值
关于对象的存储:
class person
{
public:virtual void fun_1(){cout << "person::func_1()" << endl;}virtual void fun_2() final //基类自身不需要重写的虚函数{cout << "person::func_2()" << endl;}protected:int _a = 0;
};class stu : public person
{
public:void fun_1(){cout << "stu::func_1()" << endl;}virtual void fun_3() //这个是派生类自己的虚函数,其地址放在虚表最后面{cout << "stu::func_3() 派生类自身的虚函数" << endl;}protected:int _b = 0;
};int main()
{person ps;person ps1;stu st;stu st1;return 0;
}
同类型对象共用虚函数表(同类的对象里面存的虚表指针都指向同一个地方)
从原理层讲,虚函数重写叫做虚函数覆盖,即重写是在派生类的虚函数表上用派生类重写的虚函数的地址覆盖了继承下来的基类被重写的虚函数的地址。
派生类对象的存储内容:
- 首先是base类的虚函数表,因为func1重写了,所以派生类和基类的func1函数指向不同;其次是func2,因为func2为未重写的基类虚函数,所以放在后面;然后是func3,func3是派生类自身虚函数,放在后面;最后是nullptr
- 然后是从base类继承的成员变量;
- 最后是派生类自身的成员变量;
我们可以通过打印来看虚函数表内存储顺序:
//typedef void (*FUNC_PTR)(); // FUNC_PTR 是一个指向“无参数、无返回值”函数的指针类型
// 1. (*FUNC_PTR) 说明FUNC_PTR一个指针; 2.()代表指向一个函数,这个函数没有形参;3.void代表这个函数没有返回值
// 加上typedef之后可以用FUNC_PTR代表 void(*)() : 指向无参无返回值的函数的指针// C++11写法
using FUNC_PTR = void(*)(); //一级指针void print_vftable(FUNC_PTR* table) //FUNC_PTR*就是二级指针了!!!
{for (size_t i = 0; table[i] != nullptr; i++)//注意:虚函数表最后放的是nullptr!!!{printf("[%d]:%p\n", i, table[i]);FUNC_PTR f = table[i]; //取地址直接调用函数f();}printf("\n");
}int main()
{person ps;stu st;//对象内第一个数据就是指向虚表的指针,这个指针是void*,需要强转成步长为4字节的指针(32位),//然后解引用就是虚表内的数据(虚函数地址,16进制,可以用int)int vft_ps = *((int*)&ps);int vft_st = *((int*)&st);print_vftable((FUNC_PTR*)vft_ps);print_vftable((FUNC_PTR*)vft_st);//调用内存和监视窗口就可明白派生类的虚函数地址,依次放在虚表最后面
}
注意:
- 子类赋值给父类对象(切片),不拷贝虚表;若拷贝虚表,那父类对象虚表中的是子类还是父类的虚函数指针就不确定了,就无法构成多态。
- 对象中存的是虚函数表的地址,虚函数表是函数指针数组,数组内指针指向虚函数,虚函数存在代码段内。在某些地方我们可以看到虚函数表存在于只读数据段,但通过测试观察发现虚函数表存储位置更加接近代码段,但这个实验并非充分证明虚函数表就存在于代码段。(具体可以问问AI)。
- 虚函数表和虚基表是两个不同的概念,一个是多态方面的,一个是棱形虚拟继承方面的,不能混淆。
5. 多继承的多态
//多态分类
// 静态的多态(编译时):函数重载
// 动态的多态(运行时):通过继承、虚函数重写所实现的多态//多继承的多态
//多态分类
// 静态的多态(编译时):函数重载
// 动态的多态(运行时):通过继承、虚函数重写所实现的多态//多继承的多态
class base_1
{
public:virtual void func_1(){cout << "base_1::func_1()" << endl;}virtual void func_2(){cout << "base_1::func_2()" << endl;}public:int _a = 0;
};class base_2
{
public:virtual void func_1(){cout << "base_2::func_1()" << endl;}virtual void func_3(){cout << "base_2::func_3()" << endl;}public:int _b = 0;
};class derive : public base_1, public base_2
{
public:void func_1() //重写了base_1和base_2的func_1{cout << "derive::func_1()" << endl;}void func_2(){cout << "derive::func_2()" << endl;}void func_3(){cout << "derive::func_3()" << endl;}virtual void func_4() //自己的虚函数{cout << "derive::func_4()" << endl;}public:int _c = 0;
};int main()
{derive d;cout << sizeof(d) << endl; //共有两份虚表,分别是base_1和base_2int vft1 = *((int*)&d); //取第一份虚表int vft2 = *((int*)((char*)&d + sizeof(base_1))); //取出第二份虚表,改变指针步长后采用sizeof(base_1)进行地址偏移//base_2* ptr2 = &d; //自动切片,将derive的base_2部分的首地址給vft2//int vft2 = *((int*)ptr2);print_vftable((FUNC_PTR*)vft1);print_vftable((FUNC_PTR*)vft2);//打印后可知derive对象自身的虚函数是放在虚表1中的//为什么derive重写了func1,但两个虚表中指向func1的地址却不同?base_1* ptr1 = &d; //与ptrd相同base_2* ptr2 = &d; //与ptrd不同derive* ptrd = &d;//答:调用方式不同,但其实调用的都是同一个函数ptr1->func_1();ptr2->func_1();ptrd->func_1();//从反汇编代码中可以看出ptr1与ptrd调用func_1函数走的路线一样,但ptr2调用时this指针经过了修正,回到了derive对象的首地址//注:ecx寄存器存this指针,寄存器中没有类型概念return 0;
}
虽然虚函数表中第0个虚函数(func1)地址不同,但在上图可以看出其实调用的都是同一个虚函数,具体可以看看反汇编代码。
6. 总结
本文简要介绍了多态的概念、定义、使用方法、注意事项及原理,内容较多,如有错误,请批评指正,谢谢。