ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C++:6.多态

C++:6.多态 一.概念多态(polymorphism)的概念多态分为编译时多态(静态多态)和运⾏时多态(动态多态),编译时多态(静态多态)主要就是我们前⾯讲的函数重载和函数模板他们传不同类型的参数就可以调⽤不同的函数通过参数不同达到多种形态.二.多态的定义及实现2.1多态的构成条件2.1.1实现多态还有两个必须重要条件必须是基类的指针或者引⽤调⽤虚函数被调⽤的函数必须是虚函数并且完成了虚函数重写/覆盖。说明要实现多态效果第⼀必须是基类的指针或引⽤因为只有基类的指针或引⽤才能既指向基类 对象⼜指向派⽣类对象第⼆派⽣类必须对基类的虚函数完成重写/覆盖重写或者覆盖了基类和派⽣类之间才能有不同的函数多态的不同形态效果才能达到。2.1.2虚函数类成员函数前⾯加virtual修饰那么这个成员函数被称为虚函数。注意⾮成员函数不能加virtual修饰。2.1.3虚函数的重写/覆盖虚函数的重写/覆盖派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值 类型、函数名字、参数列表完全相同)称派⽣类的虚函数重写了基类的虚函数。注意在重写基类虚函数时派⽣类的虚函数在不加virtual关键字时虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性)但是该种写法不是很规范不建议这样使⽤不过在考试选择题中经常会故意买这个坑让你判断是否构成多态1 class Person { 2 public: 3 virtual void BuyTicket() { cout 买票-全价 endl; } 4 }; 5 6 class Student : public Person { 7 public: 8 virtual void BuyTicket() { cout 买票-打折 endl; } 9 }; 10 11 void Func(Person* ptr) 12 { 13 // 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket 14 // 但是跟ptr没关系⽽是由ptr指向的对象决定的。 15 ptr-BuyTicket(); 16 } 17 18 int main() 19 { 20 Person ps; 21 Student st; 22 23 Func(ps); 24 Func(st); 25 26 return 0; 27 }2.1.4虚函数重写的⼀些其他问题•协变(了解)派⽣类重写基类虚函数时与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引⽤派⽣类虚函数返回派⽣类对象的指针或者引⽤时称为协变。协变的实际意义并不⼤所以我们 了解⼀下即可。•析构函数的重写基类的析构函数为虚函数此时派⽣类析构函数只要定义⽆论是否加virtual关键字都与基类的析 构函数构成重写虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则实际上编译器对析 构函数的名称做了特殊处理编译后析构函数的名称统⼀处理destructor所以基类的析构函数加了 vialtual修饰派⽣类的析构函数就构成重写。 下⾯的代码我们可以看到如果~A()不加virtual那么delete p2时只调⽤的A的析构函数没有调⽤ B的析构函数就会导致内存泄漏问题因为~B()中在释放资源。2.1.5override 和 final关键字从上⾯可以看出C对虚函数重写的要求⽐较严格但是有些情况下由于疏忽⽐如函数名写错参数 写错等导致⽆法构成重写⽽这种错误在编译期间是不会报出的只有在程序运⾏时没有得到预期结 果才来debug会得不偿失因此C11提供了override可以帮助⽤⼾检测是否重写。如果我们不想让派⽣类重写这个虚函数那么可以⽤final去修饰。class Car { 3 public: 4 virtual void Dirve() 5 {} 6 }; 7 8 class Benz :public Car { 9 public: 10 virtual void Drive() override { cout Benz-舒适 endl; } 11 }; 12 13 int main() 14 { 15 return 0; 16 }1 // error C3248: “Car::Drive”: 声明为“final”的函数⽆法被“Benz::Drive”重写 2 class Car 3 { 4 public: 5 virtual void Drive() final {} 6 }; 7 8 class Benz :public Car 9 { 10 public: 11 virtual void Drive() { cout Benz-舒适 endl; } 12 }; 13 14 int main() 15 { 16 return 0; 17 }2.1.6重载/重写/隐藏的对⽐三.纯虚函数和抽象类在虚函数的后⾯写上 0 则这个函数为纯虚函数纯虚函数不需要定义实现(实现没啥意义因为要被 派⽣类重写但是语法上可以实现)只要声明即可。包含纯虚函数的类叫做抽象类抽象类不能实例 化出对象如果派⽣类继承后不重写纯虚函数那么派⽣类也是抽象类。纯虚函数某种程度上强制了 派⽣类重写虚函数因为不重写实例化不出对象。、四.原理虚函数表指针下⾯编译为32位程序的运⾏结果是什么A. 编译报错 B. 运⾏报错 C. 8 D. 121classBase2{3public:4virtualvoidFunc1()5{6cout Func1() endl;7}8protected:9int_b 1;10char_ch x;11};1213intmain()14{15Base b;16cout sizeof(b) endl;1718return0;19}上⾯题⽬运⾏结果12bytes除了_b和_ch成员还多⼀个__vfptr放在对象的前⾯(注意有些平台可能 会放到对象的最后⾯这个跟平台有关)对象中的这个指针我们叫做虚函数表指针(v代表virtualf代 表function)。⼀个含有虚函数的类中都⾄少都有⼀个虚函数表指针因为⼀个类所有虚函数的地址要 被放到这个类对象的虚函数表中虚函数表也简称虚表。从底层的⻆度Func函数中ptr-BuyTicket()是如何作为ptr指向Person对象调Person::BuyTicket ptr指向Student对象调⽤Student::BuyTicket的呢通过下图我们可以看到满⾜多态条件后底层 不再是编译时通过调⽤对象确定函数的地址⽽是运⾏时到指向的对象的虚表中确定对应的虚函数的 地址这样就实现了指针或引⽤指向基类就调⽤基类的虚函数指向派⽣类就调⽤派⽣类对应的虚函 数。第⼀张图ptr指向的Person对象调⽤的是Person的虚函数第⼆张图ptr指向的Student对 象调⽤的是Student的虚函数•对不满⾜多态条件(指针或者引⽤调⽤虚函数)的函数调⽤是在编译时绑定也就是编译时确定调⽤ 函数的地址叫做静态绑定。•满⾜多态条件的函数调⽤是在运⾏时绑定也就是在运⾏时到指向对象的虚函数表中找到调⽤函数的地址也就做动态绑定。虚函数表•基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共⽤同⼀张虚表不同类型的对象各⾃有独⽴的虚表所以基类和派⽣类有各⾃独⽴的虚表。•派⽣类由两部分构成继承下来的基类和⾃⼰的成员⼀般情况下继承下来的基类中有虚函数表指针⾃⼰就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个就像基类对象的成员和派⽣类对象中的基类对象成员也独⽴的。•派⽣类中重写的基类的虚函数派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函数地址。•派⽣类的虚函数表中包含(1)基类的虚函数地址(2)派⽣类重写的虚函数地址完成覆盖派⽣类⾃⼰的虚函数地址三个部分。•虚函数表本质是⼀个存虚函数指针的指针数组⼀般情况这个数组最后⾯放了⼀个0x00000000标记。(这个C并没有进⾏规定各个编译器⾃⾏定义的vs系列编译器会再后⾯放个0x00000000标记g系列编译不会放)•虚函数存在哪的虚函数和普通函数⼀样的编译好后是⼀段指令都是存在代码段的只是虚函数的地址⼜存到了虚表中。•虚函数表存在哪的这个问题严格说并没有标准答案C标准并没有规定我们写下⾯的代码可以对⽐验证⼀下。vs下是存在代码段(常量区)
返回列表