南昌建设企业网站公司,科技软件公司,吉林市今天消息,企业网站制作素材朋友们、伙计们#xff0c;我们又见面了#xff0c;本期来给大家解读一下有关多态的知识点#xff0c;如果看完之后对你有一定的启发#xff0c;那么请留下你的三连#xff0c;祝大家心想事成#xff01; C 语 言 专 栏#xff1a;C语言#xff1a;从入门到精通 数据结… 朋友们、伙计们我们又见面了本期来给大家解读一下有关多态的知识点如果看完之后对你有一定的启发那么请留下你的三连祝大家心想事成 C 语 言 专 栏C语言从入门到精通 数据结构专栏数据结构 个 人 主 页 stackY、 C 专 栏 C Linux 专 栏 Linux 目录
1. 多态的概念
1.1 概念
2. 多态的定义及实现
2.1 多态的构成条件
2.2 虚函数
2.3 虚函数的重写
2.3.1 虚函数重写的两个例外
2.4 C11 override 和 final
2.5 重载、重写(覆盖)、重定义(隐藏)的对比
3. 抽象类
3.1 概念
3.2 接口继承和实现继承
4. 多态的原理
4.1 虚函数表
4.2 多态的原理
4.3 动态绑定与静态绑定
5. 单继承和多继承关系的虚函数表
5.1 单继承中的虚表
5.2 多继承中的虚函数表
6. 总结 1. 多态的概念
1.1 概念 多态的概念通俗来说就是多种形态具体点就是去完成某个行为当不同的对象去完成时会产生出不同的状态。 举个栗子比如买票这个行为当普通人买票时是全价买票学生买票时是半价买票军人买票时是优先买票。 2. 多态的定义及实现
2.1 多态的构成条件 多态是在不同继承关系的类对象去调用同一函数产生了不同的行为。比如Student继承了 Person。Person对象买票全价Student对象买票半价。 那么在继承中要构成多态还有两个条件 1. 必须通过基类的指针或者引用调用虚函数2. 被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写 class Person
{
public:virtual void BuyTicket(){cout 票价-全价 endl;}
};
class Student : public Person
{
public:virtual void BuyTicket(){cout 票价-半价 endl;}
};
void Func(Person p)
{p.BuyTicket();
}int main()
{Person p;Func(p);Student s;Func(s);return 0;
} 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
{
public://void BuyTicket()virtual void BuyTicket(){cout 票价-半价 endl;}
}; 注意 在重写基类虚函数时派生类的虚函数在不加virtual关键字时虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范不建议这样使用。 2.3.1 虚函数重写的两个例外 1. 协变(基类与派生类虚函数返回值类型不同) 派生类重写基类虚函数时与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指 针或者引用派生类虚函数返回派生类对象的指针或者引用时称为协变。 class A
{};
class B :public A
{};class Person
{
public:virtual A* BuyTicket(){cout 票价-全价 endl;return nullptr;}
};
class Student : public Person
{
public://void BuyTicket()virtual B* BuyTicket(){cout 票价-半价 endl;return nullptr;}
}; 2. 析构函数的重写(基类与派生类析构函数的名字不同) 如果基类的析构函数为虚函数此时派生类析构函数只要定义无论是否加virtual关键字 都与基类的析构函数构成重写虽然基类与派生类析构函数名字不同。 虽然函数名不相同看起来违背了重写的规则其实不然这里可以理解为编译器对析构函数的名称做了特殊处理编译后析构函数的名称统一处理成estructor。 class Person
{
public:virtual ~Person(){ cout ~Person() endl; }
};
class Student : public Person
{
public:virtual ~Student() { cout ~Student() endl; }//~Student(){ cout ~Student() endl; }
};int main()
{Person* p new Person;Student* s new Student;delete p;delete s;return 0;
} 2.4 C11 override 和 final 从上面可以看出C对函数重写的要求比较严格但是有些情况下由于疏忽可能会导致函数名字母次序写反而无法构成重写而这种错误在编译期间是不会报出的只有在程序运行时才能发现端倪因此C11提供了override和final两个关键字可以帮助用户检测是否重写。 1. final修饰虚函数表示该虚函数不能再被重写 class Car
{
public:virtual void Drive() final {}
};
class Benz :public Car
{
public:virtual void Drive() { cout Benz-舒适 endl; }
}; 拓展 final修饰类则该类不能被继承 class A final
{
public:int _a;
};
class B final :public A
{
public:int _b;
};
class C : public B
{
public:int _c;
}; 2. override: 检查派生类虚函数是否重写了基类某个虚函数如果没有重写编译报错。 class Car {
public:virtual void Drive() {}
};
class Benz :public Car {
public:virtual void Drive(int i) override { cout Benz-舒适 endl; }
}; 2.5 重载、重写(覆盖)、重定义(隐藏)的对比 3. 抽象类
3.1 概念 在虚函数的后面写上 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 TestCar()
{Car* pBenz new Benz;pBenz-Drive();Car* pBMW new BMW;pBMW-Drive();
} 3.2 接口继承和实现继承 普通函数的继承是一种实现继承派生类继承了基类函数可以使用函数继承的是函数的实现。虚函数的继承是一种接口继承派生类继承的是基类虚函数的接口目的是为了重写达成多态继承的是接口。所以如果不实现多态不要把函数定义成虚函数。 4. 多态的原理 首先先来看一下这道题 // 这里常考一道笔试题sizeof(Base)是多少
class Base
{
public:virtual void Func1(){cout Func1() endl;}
private:int _b 1;
}; 根据之前的判断成员函数在公共代码区所以这个类的大小只有4字节如果我们打印出来看一下的话并不是4字节而是8字节那这是为什么呢我们可以调试来观察一波 通过调试可以观察到在b里面还存在一个__vfptr这个玩意叫做虚函数表到底有什么用呢这就是我们接下来的话题 4.1 虚函数表 通过上面的观察可以发现在Base对象中的这个指针我们叫做虚函数表指针(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;
};通过观察可以发现 1. 派生类对象d中也有一个虚表指针d对象由两部分构成一部分是父类继承下来的成员虚表指针也就是存在部分的另一部分是自己的成员。2. 基类b对象和派生类d对象虚表是不一样的这里我们发现Func1完成了重写所以d的虚表中存的是重写的Derive::Func1所以虚函数的重写也叫作覆盖覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法覆盖是原理层的叫法。3. 另外Func2继承下来后是虚函数所以放进了虚表Func3也继承下来了但是不是虚函数所以不会放进虚表。4. 虚函数表本质是一个存虚函数指针的指针数组一般情况这个数组最后面放了一个nullptr。5. 总结一下派生类的虚表生成a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。6. 这里还有存在一个很容易混淆的问题虚函数存在哪的虚表存在哪的 注意虚表存的是虚函数指针不是虚函数虚函数和普通函数一样的都是存在代码段的只是他的指针又存到了虚表中。另外对象中存的不是虚表存的是虚表指针。那么虚表存在哪的呢实际我们去验证一下会发现vs下是存在代码段的。 4.2 多态的原理 在前面的代码中我们关于买票做了区分在那里Func函数传Person调用Person::BuyTicket全价传Student调用的是Student::BuyTicket半价 1. 观察上图的红色箭头我们看到p是指向基类对象时p-BuyTicket在基类的虚表中找到虚函数是Person::BuyTicket。2. 观察上图的紫色箭头我们看到p是指向派生类对象时p-BuyTicket在派生类的虚表中找到虚函数是Student::BuyTicket。3. 这样就实现出了不同对象去完成同一行为时展现出不同的形态。4. 反过来思考我们要达到多态有两个条件一个是虚函数覆盖一个是对象的指针或引用调用虚函数。5. 再通过下面的汇编代码分析看出满足多态以后的函数调用不是在编译时确定的是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。 4.3 动态绑定与静态绑定 1. 静态绑定又称为前期绑定(早绑定)在程序编译期间确定了程序的行为也称为静态多态比如函数重载。 2. 动态绑定又称后期绑定(晚绑定)是在程序运行期间根据具体拿到的类型确定程序的具体行为调用具体的函数也称为动态多态。 3. 本小节之前(4.2小节)买票的汇编代码很好的解释了什么是静态(编译器)绑定和动态(运行时)绑定 5. 单继承和多继承关系的虚函数表
5.1 单继承中的虚表 //单继承
class Base {
public:virtual void func1() { cout Base::func1 endl; }virtual void func2() { cout Base::func2 endl; }
private:int a;
};
class Derive :public Base {
public:virtual void func1() { cout Derive::func1 endl; }virtual void func3() { cout Derive::func3 endl; }virtual void func4() { cout Derive::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;//1.先取b的地址强转成一个int*的指针//2.再解引用取值就取到了b对象头4bytes的值这个值就是指向虚表的指针//3.再强转成VFPTR*因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。//4.虚表指针传递给PrintVTable进行打印虚表VFPTR * vTableb (VFPTR*)(*(int*)b);PrintVTable(vTableb);VFPTR* vTabled (VFPTR*)(*(int*)d);PrintVTable(vTabled);return 0;
} 思路取出b、d对象的头4bytes就是虚表的指针前面我们说了虚函数表本质是一个存虚函数指针的指针数组这个数组最后面放了一个nullptr。 1.先取b的地址强转成一个int*的指针2.再解引用取值就取到了b对象头4bytes的值这个值就是指向虚表的指针3.再强转成VFPTR*因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。4.虚表指针传递给PrintVTable进行打印虚表5.需要说明的是这个打印虚表的代码经常会崩溃因为编译器有时对虚表的处理不干净虚表最后面没有放nullptr导致越界这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案再编译就好了 5.2 多继承中的虚函数表 //多继承
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*)d sizeof(Base1)));PrintVTable(vTableb2);return 0;
} 观察上图可以看出多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中 6. 总结 1. 什么是多态? 多态分为静态多态和动态多态静态多态在编译时就已经确定好了动态多态在运行时才会确定常见静态多态是函数重载动态多态则是通过父类的指针或者引用调用虚函数对基类虚函数的重写指向谁就调用谁的虚函数以此来实现多种形态。 2. 什么是重载、重写(覆盖)、重定义(隐藏) 重载同一作用域内函数名相同参数不同参数类型/数量/顺序。 重写子类和父类的虚函数名称、返回值协变例外、参数都相同叫做子类重写了父类的虚函数。 重定义子类和父类的函数名相同称子类隐藏了父类的某个函数。 3. 多态的原理 父类和子类之中保存的虚表指针是不一样的通过传入指针或者引用确定去子类还是父类中去寻找虚表指针最后达到调用不同虚函数的目的。 4. inline函数可以是虚函数吗 可以在VS之下如果构成多态编译器会放弃inline的属性这个函数就不是内联函数了因为内联函数会被展开是没有地址的。而虚函数会将其地址放入至虚表之中。 普通调用inline起作用多态调用inline不起作用。 5. 静态成员可以是虚函数吗 不可以编译报错因为静态成员函数没有this指针使用类型::成员函数的调用方式无法访问虚函数表但是它可以指定类域调用所以静态成员函数无法放进虚函数表。 6. 构造函数可以是虚函数吗 不可以编译报错对象中虚表指针是构造函数初始化列表阶段才初始化的。 虚函数多态调用要到虚表中找但是虚表指针还未初始化。 7. 析构函数可以是虚函数吗 最好将析构函数定义成虚函数否则会出现指向子类对象的父类指针调用父类析构函数而不会调用子类虚构函数的情况出现内存泄漏。 8. 对象访问普通函数快还是虚函数更快 首先如果是普通对象是一样快的。如果是指针对象或者是引用对象则调用的普通函数快因为构成多态运行时调用虚函数需要到虚函数表中去查找。 9. 虚函数表是在什么阶段生成的存在哪的 虚函数表是在编译阶段就生成的一般情况下存在代码段(常量区)的。 10. C菱形继承的问题虚继承的原理 菱形继承因为子类对象当中会有两份父类的成员因此会导致数据冗余和二义性的问题。 通过虚基表中的偏移量来找到对应的成员从而解决了数据冗余和二义性的问题。 11. 什么是抽象类抽象类的作用 抽象类不能实例化出对象抽象类强制重写了虚函数 另外抽象类体现出了接口继承关系。 朋友们、伙计们美好的时光总是短暂的我们本期的的分享就到此结束欲知后事如何请听下回分解~最后看完别忘了留下你们弥足珍贵的三连喔感谢大家的支持