青岛外贸建设网站制作虚拟机网站建设
- 作者: 五速梦信息网
- 时间: 2026年03月21日 09:56
当前位置: 首页 > news >正文
青岛外贸建设网站制作,虚拟机网站建设,微信如何制作公众号,苏州高端网站建设开发1. 多态的概念 多态#xff0c;顾名思义多种形态#xff1b;多态分为编译时多态#xff08;静态多态#xff09;和运行时多态#xff08;动态多态#xff09;#xff0c;静态多态就是就是我们前面讲的函数重载和函数模板#xff0c;可以通过传不同类型#xff0c;然后…1. 多态的概念 多态顾名思义多种形态多态分为编译时多态静态多态和运行时多态动态多态静态多态就是就是我们前面讲的函数重载和函数模板可以通过传不同类型然后在编译期间就确定好使用哪个的称为静态多态动态多态就是一般在程序运行时确定使用指定函数的就称作动态多态。 动态多态举个生活中的例子例如铁路12306中买票的时候当成年且没有特殊情况的人是全价买票学生买票有特定的学生价买票军人买票时可以优先买票而这个过程抽象来说就是传入不同的对象就会完成不同的行为。 2. 多态的定义及使用 2.1 多态的构成条件 多态是一个继承关系下的类对象去调用同一函数产生了不同行为。比如Student继承了Person。Person对象买票全价Student对象优惠买票。 2.1.1 实现多态的两个必要条件 1. 必须是基类的指针或引用调用函数。
- 被调用的函数必须是虚函数virtual修饰的。 解释一下为什么必须是基类的指针或引用调用函数因为只有是基类的指针或引用才能做到既接收基类的地址又接收派生类的地址 第二派生类必须对基类的虚函数进行重写/覆盖派生类对基类的虚函数重写了才能做到传入不同的类执行不同的操作的效果才能达到多态。 3. 虚函数 在继承那一章我们也讲到虚继承虚继承是用来避免菱形继承带来代码的冗余和二义性的问题关键字是virtual虚函数的关键字也是virtual只需要在类成员函数前面加上virtual修饰那么这个成员函数就被称为虚函数。注意如果不是成员函数是不能加virtual修饰的。如下代码所示 class Person { public:virtual void BuyTicket(){cout 买票–全价 endl;} }; 3.1 虚函数的重写 派生类中有一个虚函数跟基类的虚函数完全一致的虚函数称为虚函数的重写。 完全一致指的是函数名、参数列表、返回值类型。注意这里的参数列表指的是参数的类型名字相同没有关系如下代码所示 class Person { public:virtual void BuyTicket(int x){cout 买票–全价 endl;} };class Student : public Person { public:virtual void BuyTicket(int y){cout 买票–半价 endl;} }; 还有一点需要提一下的是如果基类的虚函数加了virtual那么派生类的虚函数可以不加virtual这里我们可以直接按“在继承的时候就把基类的虚函数的属性继承下来了”理解这个不太建议这样子用但在笔试或者考试选择题可能会考。 下面是虚函数、虚函数的重写、实现多态的代码使用 #includeiostream using namespace std;class Person { public:virtual void BuyTicket(int x){cout 买票–全价 endl;} };class Student : public Person { public:virtual void BuyTicket(int y){cout 买票–半价 endl;} };void BuyTicket(Person* ptr) {ptr-BuyTicket(6); }int main() {Person p1;Student s1;BuyTicket(p1);BuyTicket(s1);return 0; }运行结果为 最后再补充一个重重重重要的知识虚函数的重写本质上是重写虚函数的实现下面就有一道相关的练习题。毕竟人教人教不会事教人一脚就懂 3.2 多态场景下的选择题 #includeiostream using namespace std;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; } 首先如果我们对重写的知识理解的不够全面的话那么肯定会选择D 但我们上面说了重写是重写函数的实现函数的参数不会有变化所以我们调用的func应该如下图所示 所以正确的答案是B 最后我们再来细致分析一下p-test()究竟怎么构成多态我们都知道在类里面的成员函数会有一个隐藏的参数就是this指针而test是在A里面的所以完整的函数应该为 virtual void test(A* this)这里就构成重载了基类做this指针的返回值完全符合上面所说的2个多态的构成条件。 3.3 虚函数重写的一些问题 3.3.1 协变 协变派生类重写基类的虚函数的时候返回值不同。即基类虚函数返回基类对象的指针或引用派生类虚函数返回派生类对象的指针或引用这个就成为协变。协变用处不多仅需了解一下即可如下代码所示 #includeiostream using namespace std; //基类 class A {}; //派生类 class B :public A {};class Person { public://基类做返回值virtual A* BuyTicket()//virtual Person* BuyTicket()也可以{cout 买票-全价 endl;return nullptr;} };class Student :public Person { public://派生类做返回值virtual B* BuyTicket()//virtual Student* BuyTicket()也可以{cout 买票-打折 endl;return nullptr;} };void Func(Person* ptr) {ptr-BuyTicket(); }3.3.2 析构函数的重写 基类的析构函数只要定义为虚函数后无论派生类的析构函数写不写virtual都会与基类的析构函数构成重写。 前面继承有提到一嘴就是析构函数最后都会被编译器转换为destructor()那么这时候函数名destructor相同构成隐藏这个是继承那边的。 所以只要基类的析构加了virtual派生类的就构成了重写函数名一样都是destructor、参数一样都没有、返回值类型都没有。 接下来就谈谈为什么要实现析构函数的重写重点 在思考为什么的时候我们就可以通过反推来证明即如果没有实现会发生什么情况我们先看一下下面的代码 #includeiostream using namespace std;class A { public:~A(){cout ~A() endl;} };class B :public A { public:~B(){cout ~B()-delete: _p endl;delete _p;} protected:int* _p new int[10]; };int main() {A* p1 new A;A* p2 new B;delete p1;delete p2;return 0; } 对代码的解析我们用基类A指针实例化了两个对象p1接收基类A的地址、p2接收派生类B的地址如果说我们没有实现析构函数的重写delete p1和delete p2 是直接走基类的析构函数因为没有构成多态也就不会产生不同的行为而只走基类A的析构函数如果派生类B里面有动态申请空间那么就会出现内存泄漏。如下图 但如果我们让基类A的析构函数定义为虚函数那么就构成多态从而会根据传入不同的类型调用不同对象的析构函数。 这里为什么会还会调用一次基类A的析构函数呢前面继承有提到派生类的析构函数里面其实包含了基类的析构函数主要是为了达到先子后父的析构顺序。 最后总结一下C的这些实现其实都是有关联的他们为了处理上面的这种情况而实现了析构函数的重写为了实现析构函数的重写而实现了让析构函数无论函数名为什么最后都会转换成destructor。真的很妙。 3.3.3 override和final关键字 override是用来检查你是否重写错误 final在继承那里也提到过如果想实现一个类不被继承就用final而这里的final是用来实现一个不能被重写的虚函数 3.3.4 重载/重写/隐藏的对比 3.3.5 纯虚函数和抽象类 纯虚函数只要在虚函数后面加个“0”即可纯虚函数不需要定义实现没有很大作用但是在语法上是支持实现的 。而包含纯虚函数的类称作抽象类抽象类不能实例化出对象但是派生类可以继承抽象类如果派生类不重写纯虚函数那么派生类也称为抽象类。纯虚函数其实就是间接强制了派生类要重写纯虚函数。 干说有点难理解举个实际例子狗和猫都是动物狗的叫声是“旺旺”猫的叫声是“喵喵”狗和猫都是动物都有动物的特征所以可以继承动物的属性但是动物是怎么叫的我们不知道而动物就可以称作是抽象类“叫”这一个动作可以用纯虚函数实现猫和狗则是动物的派生类所以需要重写这个纯虚函数狗重写“叫”函数为“旺旺”猫则是“喵喵”。如下代码所示 #includeiostreamclass Animal { public:virtual void talk() 0; }; class Dog : public Animal { public:virtual void talk() {std::cout 汪汪 std::endl;} };class Cat : public Animal { public:virtual void talk() {std::cout (^ω^)喵 std::endl;} }; int main() {Cat cat;Dog dog;cat.talk();dog.talk();return 0; } 运行结果为 4. 多态的原理 首先我们看一道题用一道题引出下面的知识 #includeiostream using namespace std;class Base { public:virtual void Func1(){cout Func1() endl;} protected:int _b 1;char _ch x; };int main() {Base b;cout sizeof(b) endl;return 0; } 我们知道计算类的大小是使用对齐的方式计算的那么计算出来的结果是8可能我们的答案就选择了C实则并不然答案选的是D首先我们通过调试看一下里面究竟多了什么玩意如下图所示 我们发现里面除了成员变量_b和_char还有一个指针_vfptr这个指针我们称作虚函数表指针virtual function pointer; 4.1 虚函数表指针 每个含有虚函数的类中都会有一个虚函数表指针这个指针是指向一块存着虚函数地址的空间虚函数表也称作虚表。所以我们传入不同类去调用该类的虚函数的时候就是通过这个指针去调用不同类中的虚函数的。 4.2 多态的实现 首先我们先来看一串代码 #includeiostream using namespace std; class Person { public:virtual void BuyTicket() {cout 买票-全价 endl;} protected:string _name; };class Student : public Person { public:virtual void BuyTicket() { cout 买票-打折 endl;}protected:int _id; };class Soldier : public Person { public:virtual void BuyTicket() { cout 买票-优先 endl;}protected:string _codename; // 代号 };void Func(Person* ptr) {// 这里可以看到虽然都是Person指针Ptr在调用BuyTicket// 但是跟ptr没关系而是由ptr指向的对象决定的。ptr-BuyTicket(); }int main() {// 其次多态不仅仅发生在派生类对象之间多个派生类继承基类重写虚函数后// 多态也会发生在多个派生类之间。Student st;Soldier sr;Person pr;Func(st);Func(sr);Func(pr);return 0; } 我们可能会好奇Func内的ptr是如何做到传入不同的类对象的地址然后走不同的虚函数执行不同操作的下面就来讲解一下。 如下图 通过上图我们看到满足多态后不再是编译时通过调用对象确定函数的地址而是运行时到指定的对象内找到对象的vfptr虚表然后确定对应的虚函数的地址然后执行这样就实现了指针或引用指向基类就调用基类的虚函数指向派生类就调用派生类的虚函数。 4.3 动态绑定和静态绑定 动态绑定则是满足多态条件的函数调用是在运行时绑定说简单点就是在运行时去虚函数表内找到函数地址然后调用。 静态绑定则是在编译期间确定函数的地址然后调用。 这个可以通过汇编层看一下。 动态绑定看着挺复杂的挺多操作因为他是在运行时先去找到vptr这个指针然后找到虚函数表再通过虚函数表内的地址找到虚函数再调用该函数。 静态绑定编译器直接确定函数地址然后直接调用。 4.4 虚函数表
- 基类对象的虚函数表中存放基类所有虚函数的地址。如果同类型实例化对象的话则虚表共用不同类型虚表各自独立。
- 派生类由两部分构成一部分是派生类自己的成员还有一部分是基类的成员还有基类的虚表指针继承下来的基类中有虚表指针的话就不会自己再生成一个新的虚表指针但继承下来的和虚表指针和基类的虚表指针不是同一个他们的地址不同。
- 如果派生类中有虚函数的重写那派生类虚表中对应的虚函数就会被重写的虚函数覆盖。
- 派生类的虚表中包含基类的虚函数地址派生类重写的虚函数地址还有派生类自己的虚函数地址。
- 虚函数的本质是一个存虚函数指针的指针数组一般情况下这个数组后面会放一个0x00000000的标记 (这个C并没有进⾏规定各个编译器自行定义的vs系列编译器会再后面放个0x00000000 标记g系列编译不会放)
- 虚函数和普通函数一样编译好后是一段指令都是存在代码段的只是虚函数的地址存在虚表中。
- 虚函数表的存在位置是看编译器的C并没有规定下面将来看一下vs的在哪里如下代码所示 #includeiostream using namespace std;class Base { public:virtual void func1() { cout Base::func1 endl; }virtual void func2() { cout Base::func2 endl; }void func5() { cout Base::func5 endl; } protected:int a 1; };class Derive : public Base { public:// 重写基类的func1virtual void func1() { cout Derive::func1 endl; }virtual void func3() { cout Derive::func1 endl; }void func4() { cout Derive::func4 endl; }protected:int b 2; };int main() {int i 0;static int j 1;int* p1 new int;const char* p2 xxxxxxxx;printf(栈:%p\n, i);printf(静态区:%p\n, j);printf(堆:%p\n, p1);printf(常量区:%p\n, p2);Base b;Derive d;Base* p3 b;Derive* p4 d;printf(Person虚表地址:%p\n, (int)p3);printf(Student虚表地址:%p\n, (int)p4);printf(虚函数地址:%p\n, Base::func1);printf(普通函数地址:%p\n, Base::func5);return 0; } 运行结果为 由此可见虚表的地址与常量区的地址很接近那就说明虚表是存在常量区的。 5. 额外补充的知识 首先我们先来看一个代码。 #includeiostream using namespace std;class A { public: void test(float a) {cout a; } }; class B :public A { public: void test(int b) {cout b; } }; void main() {A* a new A;B* b new B;a b; a-test(1.1); } 按照我们的思路a赋值给b之后那么A的test的隐式指针this就应该转换成B类的this应该调用的是cout出b的值为1。 但是编译器不是这么做的编译器虽然通过指针的指向访问成员变量但是不能通过指针的指向来访问成员函数而是通过指针的类型来访问成员函数。也就是说无论a的指针指向了谁都只能访问到A类内的成员函数。引用也是一样的 派生类有几个父类且那几个父类都有虚函数的话那么派生类就会有几张虚表。而如果在派生类中增加虚函数的话只会放在第一张虚表的最后。
- 上一篇: 青岛团购网站建设如何免费申请网站域名
- 下一篇: 青岛外贸网站建设哪家好网站建设是学哪个学科
相关文章
-
青岛团购网站建设如何免费申请网站域名
青岛团购网站建设如何免费申请网站域名
- 技术栈
- 2026年03月21日
-
青岛手机建站模板南平如何做百度的网站
青岛手机建站模板南平如何做百度的网站
- 技术栈
- 2026年03月21日
-
青岛手机端建站模板wordpress淘宝客插件破解版
青岛手机端建站模板wordpress淘宝客插件破解版
- 技术栈
- 2026年03月21日
-
青岛外贸网站建设哪家好网站建设是学哪个学科
青岛外贸网站建设哪家好网站建设是学哪个学科
- 技术栈
- 2026年03月21日
-
青岛网络推广建站资源网站快速优化排名
青岛网络推广建站资源网站快速优化排名
- 技术栈
- 2026年03月21日
-
青岛网站关键字优化成都品牌形象设计
青岛网站关键字优化成都品牌形象设计
- 技术栈
- 2026年03月21日






