电商网站做导购,企业做网站需要租服务器吗,建设银行大连招聘网站,有些网站域名解析错误文章目录 多态的概念多态的定义及实现多态的构成条件虚函数虚函数的重写override 和 final重载、重写#xff08;覆盖#xff09;、重定义#xff08;隐藏#xff09;的对比 抽象类概念接口继承和实现继承 多态的原理虚函数表多态的原理 单继承和多继承关系的虚函数表单继承… 文章目录 多态的概念多态的定义及实现多态的构成条件虚函数虚函数的重写override 和 final重载、重写覆盖、重定义隐藏的对比 抽象类概念接口继承和实现继承 多态的原理虚函数表多态的原理 单继承和多继承关系的虚函数表单继承中的虚函数表多继承中的虚函数表 多态的概念
多态的概念通俗来说就是多种形态具体点就是去完成某个行为当不同的对象去完成时会产生出不同的状态。
在面向对象方法中一般是这样表述多态的向不同的对象发送同一个消息不同的对象在接收时会产生不同的行为即方法。也就是说每个对象可以用自己的方式去响应共同的消息。所谓消息就是调用函数不同的行为就是指不同的实现即实现不同的函数。
在C中多态性表现形式之一是具有不同功能的函数可以用同一个函数名这样就可以实现用一个函数名调用不同的内容的函数。
从系统实现的角度多态分为两类静态多态性和动态多态性。
静态多态性是通过函数重载实现的。由函数重载和运算符重载运算符重载实质上也是函数重载形成的多态性属于静态多态性要求在程序编译时就知道调用函数的全部信息因此在程序编译时系统就能决定要调用的是哪个函数。静态多态性又称编译时的多态性。静态多态性的函数调用速度快、效率高但缺乏灵活性在程序运行前就已决定了执行的函数和方法。
动态多态性的特点是不在编译时确定调用的是哪个函数而是在程序运行过程中才动态地确定操作所针对的对象。它又称运行时的多态性。动态多态性是通过虚函数virtual function实现的。
有关静态多态性的应用函数的重载和运算符重载已经介绍过了在本章中主要介绍动态多态性和虚函数。要研究的问题是当一个基类被继承为不同的派生类时各派生类可以使用与基类成员相同的成员名如果在运行时用同一个成员名调用类对象的成员会调用哪个对象的成员呢
多态的定义及实现
多态的构成条件
多态是在不同继承关系的类对象去调用同一函数产生了不同的行为。比如Student继承了Person。Person对象买票全价Student对象买票半价。
那么在继承中要构成多态有两个条件
必须通过基类的指针或者引用调用虚函数被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写 虚函数
虚函数即被virtual修饰的类成员函数称为虚函数。
class Person {
public:virtual void BuyTicket() { cout 买票-全价 endl;}
};虚函数的重写
虚函数的重写覆盖派生类中有一个跟基类完全相同的虚函数即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同称子类的虚函数重写了基类的虚函数。
class Person {
public:virtual void BuyTicket() { cout 买票-全价 endl; }
};class Student : public Person {
public:virtual void BuyTicket() { cout 买票-半价 endl; }/*注意在重写基类虚函数时派生类的虚函数在不加virtual关键字时虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范不建议这样使用*//*void BuyTicket() { cout 买票-半价 endl; }*/
};void Func(Person p)
{ p.BuyTicket(); }int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
}虚函数重写的两个例外
协变基类与派生类虚函数返回值类型不同
派生类重写基类虚函数时与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用派生类虚函数返回派生类对象的指针或者引用时称为协变。
class A
{};class B : public A
{};class Person {
public:virtual A* f() {return new A;}
};class Student : public Person {
public:virtual B* f() {return new B;}
};析构函数的重写基类与派生类析构函数的名字不同
如果基类的析构函数为虚函数此时派生类析构函数只要定义无论是否加virtual关键字都与基类的析构函数构成重写虽然基类与派生类析构函数名字不同。虽然函数名不相同看起来违背了重写的规则其实不然这里可以理解为编译器对析构函数的名称做了特殊处理编译后析构函数的名称统一处理成destructor。
class Person {
public:virtual ~Person() {cout ~Person() endl;}
};class Student : public Person {
public:virtual ~Student() { cout ~Student() endl; }
};// 只有派生类Student的析构函数重写了Person的析构函数下面的delete对象调用析构函数才能构成多态才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{Person* p1 new Person;Person* p2 new Student;delete p1;delete p2;return 0;
}override 和 final
从上面可以看出C对函数重写的要求比较严格但是有些情况下由于疏忽可能会导致函数名字母次序写反而无法构成重载而这种错误在编译期间是不会报出的只有在程序运行时没有得到预期结果才来debug会得不偿失因此C11提供了override和final两个关键字可以帮助用户检测是否重写。
final修饰虚函数表示该虚函数不能再被重写
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;}
};重载、重写覆盖、重定义隐藏的对比 抽象类
概念
在虚函数的后面写上0则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类也叫接口类抽象类不能实例化出对象。派生类继承后也不能实例化出对象只有重写纯虚函数派生类才能实例化出对象。纯虚函数规范了派生类必须重写另外纯虚函数更体现出了接口继承。
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;}
};void Test()
{Car* pBenz new Benz;pBenz-Drive();Car* pBMW new BMW;pBMW-Drive();
}接口继承和实现继承
普通函数的继承是一种实现继承派生类继承了基类函数可以使用函数继承的是函数的实现。虚函数的继承是一种接口继承派生类继承的是基类虚函数的接口目的是为了重写达成多态继承的是接口。所以如果不实现多态不要把函数定义成虚函数。
多态的原理
虚函数表
// 这里常考一道笔试题sizeof(Base)是多少
class Base
{
public:virtual void Func1(){cout Func1() endl;}private:int _b 1;
};在x86环境下通过观察测试我们发现b对象是8bytes除了_b成员还多一个__vfptr放在对象的前面注意有些平台可能会放到对象的最后面这个跟平台有关对象中的这个指针我们叫做虚函数表指针v代表virtualf代表function。**一个含有虚函数的类中都至少都有一个虚函数表指针因为虚函数的地址要被放到虚函数表中虚函数表也简称虚表。**那么派生类中这个表放了些什么呢我们接着往下分析。
// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
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;}private:int _d 2;
};int main()
{Base b;Derive d;return 0;
}通过观察和测试我们发现了以下几点问题 派生类对象d中也有一个虚表指针d对象由两部分构成一部分是父类继承下来的成员虚表指针也就是存在这一部分的另一部分是自己的成员。 基类b对象和派生类d对象虚表是不一样的这里我们发现Func1完成了重写所以d的虚表中存的是重写的Derive::Func1所以虚函数的重写也叫作覆盖覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法覆盖是原理层的叫法。 另外Func2继承下来后是虚函数所以放进了虚表Func3也继承下来了但是不是虚函数所以不会放进虚表。 虚函数表本质是一个存虚函数指针的指针数组一般情况这个数组最后面放了一个nullptr。 总结一下派生类的虚表生成a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。 这里还有一个很容易混淆的问题虚函数存在哪的虚表存在哪的 答虚函数存在虚表虚表存在对象中。注意上面的回答的错的。但是很多人都是这样认为的。注意虚表存的是虚函数指针不是虚函数虚函数和普通函数一样的都是存在代码段的只是他的指针又存到了虚表中。另外对象中存的不是虚表存的是虚表指针。那么虚表存在哪的呢实际我们去验证一下会发现vs下是存在代码段的。
多态的原理
上面分析了这个半天了那么多态的原理到底是什么还记得这里Func函数传Person调用的Person::BuyTicket传Student调用的是Student::BuyTicket
// 本例类中的 _a 和 _b 无实际意义仅为了在监视窗口展示地更清楚
class Person {
public:virtual void BuyTicket() { cout 买票-全价 endl; }private:int _a 1;
};class Student : public Person {
public:virtual void BuyTicket() { cout 买票-半价 endl; }private:int _b 2;
};void Func(Person p)
{p.BuyTicket();
}int main()
{Person Mike;Func(Mike);Student Johnson;Func(Johnson);return 0;
}观察下图的红色箭头我们看到p是Person对象的引用时p-BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。 观察下图的蓝色箭头我们看到p是Student对象时p-BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。 这样就实现出了不同对象去完成同一行为时展现出不同的形态。 反过来思考我们要达到多态有两个条件一个是虚函数覆盖一个是基类对象的指针或引用调用虚函数。思考一下这是为什么
虚函数覆盖虚函数会放进虚函数表中子类重写虚函数覆盖父类后子类的虚表中存放的是重写后的函数指针。
基类对象的指针或引用调用虚函数派生类对象的指针赋值给基类对象的指针时会发生强制类型转换积累的引用引用派生类时会发生切片。这俩种情况下可以使基类的指针或引用访问派生类的内容仅基类部分当使用其调用重写的虚函数时会去虚表中寻找函数地址如果指针或引用的对象是基类就去基类的虚表找如果指针或引用的对象是派生类就去派生类的虚表找。这样编译器无需分辨具体是哪种类型的指针或引用都只需要做同样的动作去虚表中找函数地址即可。
那么为什么不能通过基类的对象达成多态呢
原因是虚表是不能拷贝的。当我们用一个派生类对象给基类对象赋值或拷贝构造时只会拷贝派生类中的成员不会拷贝派生类的虚表而是自动用基类的虚表来代替。这样基类对象无论如何是不能访问到派生类的虚表的自然也就无法达成多态。从实际应用的角度也不该允许这样的情况发生因为如果拷贝虚表那么基类对象虚表中是父类虚函数还是子类虚函数就不确定了就全乱了。
通过汇编代码分析可以看出满足多态以后的函数调用不是在编译时确定的是运行起来以后到对象的虚表中去找的。不满足多态的函数调用是编译时确认好的。
单继承和多继承关系的虚函数表
需要注意的是在单继承和多继承关系中下面我们去关注的是派生类对象的虚表模型因为基类的虚表模型前面我们已经看过了没什么需要特别研究的。
单继承中的虚函数表
class Base {
public :virtual void func1() { coutBase::func1 endl;}virtual void func2() {coutBase::func2 endl;}private :int a;
};class Derive :public Base {
public :virtual void func1() {coutDerive::func1 endl;}virtual void func3() {coutDerive::func3 endl;}virtual void func4() {coutDerive::func4 endl;}
private :int b;
};观察下图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数也可以认为是他的一个小bug。那么我们如何查看d的虚表呢下面我们使用代码打印出虚表中的函数。
typedef void(*VFPTR) (); // 将函数指针类型重命名void PrintVTable(VFPTR* vTable)
{// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数cout 虚表地址 vTable endl;for (int i 0; vTable[i] ! nullptr; i){printf(第%d个虚函数地址 :0X%x,-, i, vTable[i]);VFPTR f vTable[i];f();}cout endl;
}
int main()
{Base b;Derive d;// 思路取出b、d对象的头4bytes就是虚表的指针前面我们说了虚函数表本质是一个存虚函数指针的指针数组这个数组最后面放了一个nullptr// 1.先取b的地址强转成一个int*的指针// 2.再解引用取值就取到了b对象头4bytes的值这个值就是指向虚表的指针// 3.再强转成VFPTR*因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。// 4.虚表指针传递给PrintVTable进行打印虚表// 5.需要说明的是这个打印虚表的代码经常会崩溃因为编译器有时对虚表的处理不干净虚表最后面没有放nullptr导致越界这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案再编译就好了。VFPTR* vTableb (VFPTR*)(*(int*)b);PrintVTable(vTableb);VFPTR* vTabled (VFPTR*)(*(int*)d);PrintVTable(vTabled);return 0;
}多继承中的虚函数表
class Base1 {
public:virtual void func1() {cout Base1::func1 endl;}virtual void func2() {cout Base1::func2 endl;}
private:int b1;
};class Base2 {
public:virtual void func1() {cout Base2::func1 endl;}virtual void func2() {cout Base2::func2 endl;}
private:int b2;
};class Derive : public Base1, public Base2 {
public:virtual void func1() {cout Derive::func1 endl;}virtual void func3() {cout Derive::func3 endl;}
private:int d1;
};typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{cout 虚表地址 vTable endl;for (int i 0; vTable[i] ! nullptr; i){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*)dsizeof(Base1)));PrintVTable(vTableb2);return 0;
}观察下图可以看出多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
e endl; for (int i 0; vTable[i] ! nullptr; i) { 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*)dsizeof(Base1)));
PrintVTable(vTableb2);return 0;} 观察下图可以看出**多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中**