C++之多态总结
声明:本人所有测试代码环境都为vs2017
本文目录
- 一.多态的引入
- 二.多态的概念
- 三.多态的实现条件
- 四.虚函数的重写
- 五.C++11里的 override 和 final
- 六.抽象类
- 七.多态的本质原理
- 7.1 基类和派生类虚函数表的构建过程
- 7.2 虚函数的调用原理
- 7.3 多继承中虚函数表的构建过程
一.多态的引入
在继承中我们已经学到:派生类的对象可以赋值给基类的的对象/指针/和引用。那么有没有考虑过在子类重写父类的函数?
重写:在子类中定义与父类中原型相同的函数,函数重写只发生在基类与派生类之间。
这只是简要概括一下重写,关于重写的概念在下面还会重点提到。
我们先来实现一个场景:当你去售票网站购票时,可以以三个可供选择的身份购票,依次是普通成人、学生、和军人。如果是普通成人,则购买全价票,如果是学生,则购买半票,如果是军人,则购买免费票。
有了继承的基础,我们很容易想到,将普通人定义成基类,学生类和军人类来继承它。基类(普通成人类)定义一个卖票的方法,在学生类和军人类中也同时定义一个购票方法,再定义一个调用卖票方法的接口,通过将不同类型的对象传进去,来调用各个类对应的不同的方法。但是有人会问,为什么不直接通过每个对象来调用各自的卖票方法?这样当然可以,但是我们在这里就是要体现一个接口的概念,通过一个接口调用各自对象对应的不同的方法。
//普通成人类
class Person
{
public:Person(const string& name, const string& gender, int age):_name(name),_gender(gender),_age(age){}void BuyTicket(){cout << "全价票" << endl;}
protected:string _name;//姓名string _gender;//性别int _age;//年龄
};
//学生类
class Student :public Person
{
public:Student(const string& name, const string& gender, int age, int number):Person(name,gender,age),_number(number){}void BuyTicket(){cout << "半价票" << endl;}
private:int _number;//学号
};//军人类
class Soilder :public Person
{
public:Soilder(const string& name,const string& gender,int age,const string& hornor):Person(name, gender, age),_hornor(hornor){}void BuyTicket(){cout << "免费票" << endl;}
private:string _hornor;//获得的荣誉
};//调用方法
void TestBuyTicket(Person& p)
{p.BuyTicket();
}//测试函数
void Test1()
{Person p("白领", "男", 36);Student st("小静", "女", 17, 001);Soilder so("战狼", "男", 30, "一等功");/*p.BuyTicket();st.BuyTicket();so.BuyTicket(); 直接调,可以实现,但没有意义,不存在一个接口的概念*/TestBuyTicket(p);TestBuyTicket(st);TestBuyTicket(so);
}
这个方法的打印结果是:
全价票
全价票
全价票
并没有达到我们想要的效果,原因是因为在派生类对基类进行函数重写后,赋值兼容性原则遇上函数重写。对于重写的函数都是执行基类的成员函数。那么我们来看看解决方法。
解决方法一
定义三个面向不同人群时使用的不同方法,这个本质就和上面提到的那个直接一样,体现不了接口唯一性,而且当你面对的不同的类数量庞大的时候,就会变得十分复杂
void TestBuyTicket(Person& p)
{p.BuyTicket();
}void TestBuyTicket(Student& p)
{p.BuyTicket();
}void TestBuyTicket(Soilder& p)
{p.BuyTicket();
}
解决方法二
三种不同情况的卖票,一个卖票方法,对应不同人群的不同应对方式,这就是多态的加入
我把具体代码实现留在下面。
二.多态的概念
同一事物在不同场景下表现出来的不同形态。
三.多态的实现条件
- 基类中必须要有虚函数(被virtual修饰的成员函数),派生类必须对基类的虚函数进行重写。
- 关于虚函数的调用:必须通过基类的指针(引用)来调用虚函数。
多态的体现:代码运行时,根据基类指针(引用)指向的不同类的对象,调用对应类的虚函数。
那么这个时候,上面的解决方案二就可以完整地写出来了:
class Person
{
public:Person(const string& name, const string& gender, int age):_name(name),_gender(gender),_age(age){}virtual void BuyTicket()//virtual的加入{cout << "全价票" << endl;}
protected:string _name;string _gender;int _age;
};//学生类
class Student :public Person
{
public:Student(const string& name, const string& gender, int age, int number):Person(name,gender,age),_number(number){}virtual void BuyTicket()//virtual的加入,派生类不+virtual关键字时,也可以构成重写成功,但写法不规范{cout << "半价票" << endl;}
private:int _number;//学号
};//军人类
class Soilder :public Person
{
public:Soilder(const string& name,const string& gender,int age,const string& hornor):Person(name, gender, age),_hornor(hornor){}virtual void BuyTicket()//virtual的加入{cout << "免费票" << endl;}
private:string _hornor;
};void TestBuyTicket(Person& p)
{p.BuyTicket();
}void Test1()
{Person p("白领", "男", 36);Student st("小静", "女", 17, 001);Soilder so("战狼", "男", 30, "一等功");TestBuyTicket(p);TestBuyTicket(st);TestBuyTicket(so);
}
四.虚函数的重写
关于重写的要点:
1.一个成员函数在基类中,一个成员函数在派生类中。
2.构成重写的函数,其函数原型(返回值类型 函数名(参数列表))必须相同
#### 重写的两个例外
- 协变
基类中虚函数返回基类的引用(指针),派生类中虚函数返回派生类的引用(指针)。
注意,只要是返回基类所返回的基类类型就可以。
class A
{};
class B :public A
{};class Base
{
public:virtual void TestFunc1(){cout << "Base::TestFunc1()" << endl;}virtual void TestFunc2(int)//参数列表{cout << "Base::TestFunc2()" << endl;}void TestFunc3()//无virtual{cout << "Base::TestFunc3()" << endl;}virtual void TestFunc4(){cout << "Base::TestFunc4()" << endl;}virtual Base* TestFunc5(){cout << "Base::TestFunc5()" << endl;return this;}//返回的是A 基类 类型virtual A& TestFunc6(A& a){cout << "Base::TestFunc6()" << endl;return a;}
};class Derived : public Base
{
public:virtual void TestFunc1(){cout << "Derived::TestFunc1()" << endl;}virtual void TestFunc2(){cout << "Derived::TestFunc2()" << endl;}virtual void TestFunc3(){cout << "Derived::TestFunc3()" << endl;}void TestFunc4(){cout << "Derived::TestFunc4()" << endl;}virtual Derived* TestFunc5(){cout << "Derived::TestFunc5()" << endl;return this;}virtual B& TestFunc6(A& a){cout << "Derived::TestFunc6()" << endl;return *(new B);}
};void TestVirtualFunc(Base* pb)
{pb->TestFunc1();pb->TestFunc2(10);//参数列表都不同pb->TestFunc3();//无virtualpb->TestFunc4();pb->TestFunc5();A a;pb->TestFunc6(a);
}void Test2()
{Base b;Derived d;TestVirtualFunc(&b);TestVirtualFunc(&d);
}
- 析构函数
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写
class Base1
{
public:Base1(int b):_b(b){cout << "Base1::Base1()" << endl;}virtual ~Base1(){cout << "Base1::~Base1()" << endl;}
protected:int _b;
};
class Derived1 : public Base1
{
public:Derived1(int b): Base1(b), _p(new int[10]){cout << "Derived1::Derived1(int)" << endl;}~Derived1(){delete[] _p;cout << "Derived1::~Derived1()" << endl;}protected:int* _p;
};
//只有派生类Derived的析构函数重写了Base的析构函数,下面的delete对象调用析构函数,
//才能构成多态,才能保证pb1和pb2指向的对象正确的调用析构函数。
void TestDestroy()
{Base1* pb1 = new Base1(10);Base1* pb2 = new Derived1(10);delete pb1;delete pb2;}
五.C++11里的 override 和 final
- final:修饰虚函数,表示该虚函数不能再被继承注意,C++98里不可使用
class Base2
{
public:virtual void TestBase1()final{cout << "Base2::TestBase1()" << endl;}int _b;
};
class Derived2 :public Base2
{
public:virtual void TestBase1(){cout << "Derived2::TestBase1()" << endl;}};
- override: 修饰派生类虚函数检测该虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class Base2
{
public:virtual void TestBase1(){cout << "Base2::TestBase1()" << endl;}int _b;
};
class Derived2 :public Base2
{
public:virtual void TestBase1()override{}
};
void TestOverVirtualFun(Base2* pb)
{pb->TestBase1();
}
void Test3()
{Base2 b;b.TestBase1();Derived2 d;d.TestBase1();TestOverVirtualFun(&b);TestOverVirtualFun(&d);
}
六.抽象类
- 首先要弄懂纯虚函数的概念:在虚函数的后面写上 = 0 ,则这个函数为纯虚函数。
- 包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能定义对象,但可以定义指针(引用)
- 派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
- 纯虚函数规范了派生类必须重写,另外纯虚函数更体现接口继承。
来段代码示例:
class Place
{virtual void Describe() = 0;//纯虚函数
};class BeiJing :public Place
{
public:void Describe(){cout << "TianAnMen" << endl;//重写虚函数}
};class ShangHai :public Place
{
public:void Describe(){cout << "DongFangMIingZhu" << endl;//重写虚函数}
};
void Test4()
{BeiJing bei;bei.Describe();BeiJing* pbei = new BeiJing;pbei->Describe();//Place p;//报错,抽象类不能定义对象Place* ppbei = new BeiJing;//可以,定义基类指针或引用ppbei->Describe(); //输出TianAnMenShangHai sh;sh.Describe();ShangHai* psh = new ShangHai;psh->Describe();Place* ppsh = new ShangHai;ppsh->Describe(); //输出DongFangMIingZhu
}
七.多态的本质原理
7.1 基类和派生类虚函数表的构建过程
1.首先先看一个包含虚函数的类大小是多少,就是代码中包含两个虚函数的Base3
class Base3
{
public:virtual void Test1(){cout << "Base3::Test1()" << endl;}virtual void Test2(){cout << "Base3::Test2()" << endl;}int _b;
};
void Test5()
{cout << sizeof(Base3) << endl;//8
}
如果是没有虚函数的情况下,Base3的大小应该是4。实际打印结果为:8,所以可以得出结论:如果一个类中包含有虚函数(不管多少个),类的大小会多4个字节。那么这4个字节到底是什么时候多出来的?经过调试,我得出了结论:
- 当一个类包含有虚函数时,将会有一个虚表指针指向一个虚函数表,该表用来保存类中所有虚函数的地址;
- 如果类没有显式定义构造函数,则编译器会给该类生成一个默认的构造函数,该函数会将虚表指针放在对象的前4个字节里;
如果类显式定义了构造函数,则编译器会对该构造函数进行改写,让它依旧能将虚表指针放在对象的前4个字节里。
2.基类虚函数表构建过程
现在已经知道编译器会通过虚表指针找到虚函数表,那么当一个类中包含多个虚函数时,这个虚函数表是怎么构建的?
class Base4
{
public:virtual void Test1(){cout << "Base4::Test1()" << endl;}virtual void Test2(){cout << "Base4::Test2()" << endl;}virtual void Test3(){cout << "Base4::Test3()" << endl;}int _b;
};
通过监视上述代码中Base4类的对象的虚函数表,可以看到Test1()-Test3()是按顺序置于虚函数表中的。可以得出基类虚函数表构建过程是:将虚函数按照其在类中的声明次序依次增加到虚表中。
3.派生类虚函数表构建过程
代码:定义一个派生类Derived4继承Base4,并且在该派生类中再多增加两个虚函数Test4()和Test5()
class Derived4 :public Base4
{
public:virtual void Test5(){cout << "Derived::Test5()" << endl;}virtual void Test1(){cout << "Derived::Test1()" << endl;}virtual void Test3(){cout << "Derived::Test3()" << endl;}virtual void Test4(){cout << "Derived::Test4()" << endl;}int _d;
};
先说构建过程:
- 将基类虚表中的内容拷贝一份放到子类虚表中
- 如果派生类重写了基类某个虚函数,派生类用自己的虚函数地址替换相同偏移量地址的基类虚函数入口地址
- 如果派生类新增加的虚函数,则按照它们的声明次序一一添加到虚表的后面
证明:由于通过监视窗口不能直接看到派生类新增加的虚函数 ,那么就通过记录虚函数表的地址,在内存窗口中查看。
发现虚函数表一共占了五个4字节空间,通过对比,发现前三个依次与基类的三个虚函数地址相匹配,那么就只需要验证后两个地址是不是派生类新增加的两个虚函数的地址。
方法一:创建一个类对象,通过该对象调用一次派生类新增加的虚函数,调试转到反汇编,看call指令的函数地址是否与那后两个虚函数地址匹配。
int main()
{Derived4 d;d._d = 1;//验证法一:d.Test5();d.Test4();
}
方法二:由于代码中的虚函数的函数内容都是打印本函数的函数名,所以虚函数被调用就可以看到有哪个虚函数存在。那么现在就通过虚函数表来调用所有的虚函数,可以直接将表中包含的虚函数打印出来。
//1.从对象前4个字节中获取表格地址vfptr
//2.从vfptr指向的空间中获取虚函数的入口地址
//3.调用该虚函数
#include
typedef void(*PVFT)();//定义一个函数指针//打印函数
void PrintTable(Base4& b,const string& information)
{cout << information << endl;PVFT* pVFT = (PVFT*)(*(int*)&b);while (*pVFT){(*pVFT)();//通过函数指针调用该函数pVFT++;}cout << endl;
}int main()
{Base4 b1;b1._b = 1;Derived4 d;d._d = 1;//验证法二:PrintTable(b1, "Base4 :VFT table");PrintTable(d, "Derived4 :VFT table");
}
打印结果就是将虚函数表中包括的虚函数都打印出来了,其中包括基类的三个虚函数Test1()–Test3(),以及派生类新增的虚函数Test4()和Test5(),结果截图我就不放了,可以证明上面的派生类虚函数表构建过程。
7.2 虚函数的调用原理
class Base5
{
public:virtual void Test1(){cout << "Base5::Test1()" << endl;}virtual void Test2(){cout << "Base5::Test2()" << endl;}virtual void Test3(){cout << "Base5::Test3()" << endl;}void Test4(){cout << "Base5::Test4()" << endl;}int _b;
};class Derived5 :public Base4
{
public:virtual void Test1(){cout << "Derived5::Test1()" << endl;}virtual void Test2(){cout << "Derived5::Test2()" << endl;}virtual void Test3(){cout << "Derived5::Test3()" << endl;}int _d;
};void TestVritual(Base5* pb)
{pb->Test1();pb->Test2();pb->Test3();pb->Test4();
}
void Test7()
{Base5 b;Derived5 d;TestVritual(&b);TestVritual((Base5*)&d);//将派生类强转为基类
}
虚函数的调用:通过基类的指针或者引用调用虚函数
7.3 多继承中虚函数表的构建过程
class B1
{
public:virtual void TestFunc1(){cout << "B1::TestFunc1()" << endl;}virtual void TestFunc2(){cout << "B1::TestFunc2()" << endl;}int _b1;
};// 8
class B2
{
public:virtual void TestFunc3(){cout << "B2::TestFunc3()" << endl;}virtual void TestFunc4(){cout << "B2::TestFunc4()" << endl;}int _b2;
};// 20 24
class D : public B1, public B2
{
public:virtual void TestFunc1(){cout << "D::TestFunc1()" << endl;}virtual void TestFunc4(){cout << "D::TestFunc4()" << endl;}virtual void TestFunc5(){cout << "D::TestFunc5()" << endl;}int _d;
};typedef void(*PVFT)();void PrintVFT1(B1& b, const string& str)
{cout << "D重写B1基类的虚表" << endl;PVFT* pVFT = (PVFT*)(*(int*)&b);while (*pVFT){(*pVFT)();++pVFT;}cout << endl;
}void PrintVFT2(B2& b, const string& str)
{cout << str << endl;PVFT* pVFT = (PVFT*)(*(int*)&b);while (*pVFT){(*pVFT)();++pVFT;}cout << endl;
}void Test8()
{cout << sizeof(D) << endl;D d;d._b1 = 1;d._b2 = 2;d._d = 3;PrintVFT1(d, "D重写B1基类的虚表");PrintVFT2(d, "D重写B2基类的虚表");
}
- 与单继承中派生类虚表的构建过程相同
- 对于派生类新增加的函数,按照其声明次序增加到第一个虚表最后
关于多态的总结暂时先到这里。
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
