如果你在阅读过程中有任何疑问或想要进一步探讨的内容欢迎在评论区畅所欲言我们一起学习、共同成长~ 如果你觉得这篇文章还不错不妨顺手点个赞、加入收藏并分享给更多的朋友噢~前言本文章相关代码及解析均基于 VS2022 环境下的 x64 程序。若要在其他平台运行部分代码可能需调整。1.1.1 多态的概念同一个接口不同对象调用会执行各自专属的实现就是多态。类比买票行为普通人全价学生半价军人优先买票。1.2 多态的构成条件在继承中构成多态需满足 2 个条件必须通过基类的指针 / 引用调用虚函数。被调用的函数必须是虚函数且派生类必须重写基类虚函数。2. 虚函数2.1 虚函数概念虚函数被virtual关键字修饰的类成员函数。class Base { public: virtual void func() {} };2.2 虚函数重写在派生类中定义一个与基类虚函数有完全相同的返回值类型、函数名和参数列表的函数时就称派生类的该函数重写了基类虚函数。2.2.1 示例普通人/学生/军人买票#include iostream using namespace std; class Person { public: virtual void buyTicket() const { cout 普通人全价 endl; } virtual ~Person() default; // 只要是被继承的基类习惯写虚析构 }; class Student :public Person { public: void buyTicket() const override { cout 学生半价 endl; } }; class Soldier :public Person { public: void buyTicket() const override { cout 军人优先买票 endl; } }; int main() { Person ordinaryPerson; Student s1; Soldier s2; ordinaryPerson.buyTicket(); s1.buyTicket(); s2.buyTicket(); return 0; }2.2.2 虚函数重写的两个例外2.2.2.1 协变了解即可基类虚函数返回基类对象的指针/引用而派生类对应虚函数返回派生类对象的指针/引用时即便二者返回值类型不同也构成虚函数重写。class A { public: virtual A* clone() { std::cout A::clone() std::endl; return new A(); // 用 new 操作符动态创建一个 A 类对象并返回该对象的指针 } }; class B : public A { public: // 派生类重写基类的虚函数返回 B 类型的指针构成协变 virtual B* clone() override { std::cout B::clone() std::endl; return new B(); } };class A { public: virtual A getThis() { std::cout A::getThis() std::endl; return *this; } }; class B : public A { public: // 派生类重写基类的虚函数返回 B 类型的引用构成协变 virtual B getThis() override { std::cout B::getThis() std::endl; return *this; } };2.2.2.2 析构函数重写若基类为虚析构那么派生析构无论有无virtual关键字都与基类析构函数构成虚函数重写。基类、派生类析构函数名不同#include iostream using namespace std; class Person { public: virtual ~Person() { cout ~Person() endl; } }; class Student :public Person { public: ~Student() { cout ~Student() endl; } }; int main() { Person* p1 new Student; delete p1; // 基类虚析构父子析构都调用析构先子后父 return 0; }基类虚析构C进阶(01)继承与派生 2.4.2 析构函数的特殊处理与隐藏关系——基类不是虚析构时若用基类指针删除其指向的派生类对象只会调用基类析构不会调用派生析构派生类动态资源无法释放造成内存泄漏。所以稳妥做法是只要是被继承的基类习惯写虚析构 virtual ~类名() default;2.3 C11 override 和 finalC11提供了override 和 final 两个关键字可以帮助用户检测虚函数是否重写。2.3.1 override 显示表明是基类虚函数的重写override关键字用于显式表明派生类该函数是对基类虚函数的重写。如果未能正确重写虚函数编译器就会报错。前面的代码中就使用了 override 。2.3.2 finalfinal关键字用于修饰虚函数或类表明虚函数不能再被重写或者类不能再被继承。class Base { public: virtual void func() final { } }; class Derived : public Base { public: // 错误无法重写 final 函数 // void func() override // { // std::cout Derived::func() std::endl; // } };2.4 重载 vs 重写 vs 重定义类型作用域情况函数特征重载两个函数在同一作用域同函数名参数列表不同重写覆盖两个函数分别在基类和派生类的作用域1返回类型、函数名、参数列表都完全一致协变例外2基类必须是虚函数子类重写自动变虚区别于——派生类和基类定义同名成员构成隐藏重定义隐藏两个函数分别在基类和派生类的作用域同函数名即可3. 抽象类3.1 概念抽象类定义包含纯虚函数的类被称作抽象类也叫接口类。纯虚函数定义在虚函数后写 0此函数即为纯虚函数。抽象类自身不能实例化对象。派生类继承抽象类后重写基类纯虚函数才能实例化对象。示例#include iostream using namespace std; class Car { public: virtual void Drive() 0; // 纯虚函数 virtual ~Car() default; }; class Benz : public Car { public: void Drive() override { cout Benz-舒适 endl; } }; class BMW : public Car { public: void Drive() override { cout BMW-操控 endl; } }; int main() { // Car c; // 编译报错抽象类不能创建对象 // Car* p new Car; // 同样非法 Car* p1 new Benz; Car* p2 new BMW; p1-Drive(); p2-Drive(); delete p1; delete p2; return 0; }3.2 接口继承和实现继承实现继承普通函数的继承属于实现继承派生类继承后直接使用该函数的具体实现。接口继承虚函数继承是接口继承派生类继承的是基类虚函数的接口目的是进行重写以实现多态。若不打算实现多态就不要把函数定义为虚函数。4. 多态的原理普通成员函数加virtual→ 所属类生成虚表、每个对象多一个虚函数指针vptr带来内存开销 动态绑定运行开销。所以实现多态有代价普通成员函数不多态就别写虚函数虚析构不一样虚析构是防御性良好习惯。4.1 多态分类多态分为两种编译时多态静态绑定通过函数重载和运算符重载实现。运行时多态动态绑定通过虚函数和继承实现。4.2 运行时多态的实现原理4.2.1虚函数表vtable每个包含虚函数的类都有一个虚函数表它是一个存储类中所有虚函数地址的表格。当派生类继承基类时先拷贝基类的虚函数表内容。这意味着派生类虚函数表初始时 基类虚函数表。当派生类重写基类虚函数时派生类虚函数表会更新指向派生类的实现。如果派生类新增了虚函数这些虚函数会按照它们在类中声明的顺序被添加到虚函数表的末尾。可以通过强制类型转换访问虚函数表指针并打印虚函数表内容但由于编译器可能未在虚函数表末尾放置明确的终止标志如nullptr这可能导致程序在打印时越界访问内存从而引发崩溃。4.2.2 虚函数指针vptr每个包含虚函数的对象都有一个虚函数指针它指向该对象所属类的虚函数表。当对象被创建时编译器会自动为对象分配一个虚函数指针将其初始化指向该类虚函数表。4.2.3 动态绑定静态绑定早期编译期直接确定函数调用地址。动态绑定后期编译期不定地址运行时判断对象真实类型再调用。流程取对象虚函数指针➡查对应类型虚表➡调用实际类型虚函数基类指针/引用调用被重写的虚函数即触发动态绑定。#include iostream using namespace std; // 基类带虚函数 class Base { public: virtual void func() { cout Base::func endl; } virtual ~Base() default; }; // 子类重写虚函数 class Derive : public Base { public: void func() override { cout Derive::func endl; } }; int main() { Base* p new Derive; p-func(); // 基类指针调用被重写的虚函数触发动态绑定 delete p; return 0; }5.单继承中派生类从一个基类继承。虚函数表的生成和使用相对简单。如 4.2.3 动态绑定 的代码示例。多继承中的虚函数表多继承中1派生类会为每个基类维护一个独立的虚函数表2派生类虚函数表会合并所有基类虚函数表3如果派生类没有重写某个基类虚函数该虚函数的地址会保留在原基类虚函数表中而不会被合并到派生类虚函数表。下面通过动态绑定和函数调用来间接展示多继承中虚函数表的特性 #include iostream class Base1 { public: virtual void func1() { std::cout Base1::func1() std::endl; } virtual void func2() { std::cout Base1::func2() std::endl; } }; class Base2 { public: virtual void func3() { std::cout Base2::func3() std::endl; } virtual void func4() { std::cout Base2::func4() std::endl; } }; // 派生类继承自 Base1 和 Base2 class Derived : public Base1, public Base2 { public: // 重写 Base1 的 func1 void func1() override { std::cout Derived::func1() std::endl; } // 派生类新增虚函数 virtual void func5() { std::cout Derived::func5() std::endl; } }; int main() { Derived derived; // 1 Base1* base1Ptr derived; Base2* base2Ptr derived; std::cout 通过 Base1 指针调用虚函数: std::endl; base1Ptr-func1(); base1Ptr-func2(); std::cout 通过 Base2 指针调用虚函数: std::endl; base2Ptr-func3(); base2Ptr-func4(); // 2 Derived* derivedPtr derived; std::cout 通过 Derived 指针调用新增虚函数: std::endl; derivedPtr-func5(); // 3 std::cout 调用未重写的 Base1::func2: std::endl; base1Ptr-func2(); std::cout 调用未重写的 Base2::func3 和 Base2::func4: std::endl; base2Ptr-func3(); base2Ptr-func4(); return 0; }
C++进阶(02):多态与虚函数
如果你在阅读过程中有任何疑问或想要进一步探讨的内容欢迎在评论区畅所欲言我们一起学习、共同成长~ 如果你觉得这篇文章还不错不妨顺手点个赞、加入收藏并分享给更多的朋友噢~前言本文章相关代码及解析均基于 VS2022 环境下的 x64 程序。若要在其他平台运行部分代码可能需调整。1.1.1 多态的概念同一个接口不同对象调用会执行各自专属的实现就是多态。类比买票行为普通人全价学生半价军人优先买票。1.2 多态的构成条件在继承中构成多态需满足 2 个条件必须通过基类的指针 / 引用调用虚函数。被调用的函数必须是虚函数且派生类必须重写基类虚函数。2. 虚函数2.1 虚函数概念虚函数被virtual关键字修饰的类成员函数。class Base { public: virtual void func() {} };2.2 虚函数重写在派生类中定义一个与基类虚函数有完全相同的返回值类型、函数名和参数列表的函数时就称派生类的该函数重写了基类虚函数。2.2.1 示例普通人/学生/军人买票#include iostream using namespace std; class Person { public: virtual void buyTicket() const { cout 普通人全价 endl; } virtual ~Person() default; // 只要是被继承的基类习惯写虚析构 }; class Student :public Person { public: void buyTicket() const override { cout 学生半价 endl; } }; class Soldier :public Person { public: void buyTicket() const override { cout 军人优先买票 endl; } }; int main() { Person ordinaryPerson; Student s1; Soldier s2; ordinaryPerson.buyTicket(); s1.buyTicket(); s2.buyTicket(); return 0; }2.2.2 虚函数重写的两个例外2.2.2.1 协变了解即可基类虚函数返回基类对象的指针/引用而派生类对应虚函数返回派生类对象的指针/引用时即便二者返回值类型不同也构成虚函数重写。class A { public: virtual A* clone() { std::cout A::clone() std::endl; return new A(); // 用 new 操作符动态创建一个 A 类对象并返回该对象的指针 } }; class B : public A { public: // 派生类重写基类的虚函数返回 B 类型的指针构成协变 virtual B* clone() override { std::cout B::clone() std::endl; return new B(); } };class A { public: virtual A getThis() { std::cout A::getThis() std::endl; return *this; } }; class B : public A { public: // 派生类重写基类的虚函数返回 B 类型的引用构成协变 virtual B getThis() override { std::cout B::getThis() std::endl; return *this; } };2.2.2.2 析构函数重写若基类为虚析构那么派生析构无论有无virtual关键字都与基类析构函数构成虚函数重写。基类、派生类析构函数名不同#include iostream using namespace std; class Person { public: virtual ~Person() { cout ~Person() endl; } }; class Student :public Person { public: ~Student() { cout ~Student() endl; } }; int main() { Person* p1 new Student; delete p1; // 基类虚析构父子析构都调用析构先子后父 return 0; }基类虚析构C进阶(01)继承与派生 2.4.2 析构函数的特殊处理与隐藏关系——基类不是虚析构时若用基类指针删除其指向的派生类对象只会调用基类析构不会调用派生析构派生类动态资源无法释放造成内存泄漏。所以稳妥做法是只要是被继承的基类习惯写虚析构 virtual ~类名() default;2.3 C11 override 和 finalC11提供了override 和 final 两个关键字可以帮助用户检测虚函数是否重写。2.3.1 override 显示表明是基类虚函数的重写override关键字用于显式表明派生类该函数是对基类虚函数的重写。如果未能正确重写虚函数编译器就会报错。前面的代码中就使用了 override 。2.3.2 finalfinal关键字用于修饰虚函数或类表明虚函数不能再被重写或者类不能再被继承。class Base { public: virtual void func() final { } }; class Derived : public Base { public: // 错误无法重写 final 函数 // void func() override // { // std::cout Derived::func() std::endl; // } };2.4 重载 vs 重写 vs 重定义类型作用域情况函数特征重载两个函数在同一作用域同函数名参数列表不同重写覆盖两个函数分别在基类和派生类的作用域1返回类型、函数名、参数列表都完全一致协变例外2基类必须是虚函数子类重写自动变虚区别于——派生类和基类定义同名成员构成隐藏重定义隐藏两个函数分别在基类和派生类的作用域同函数名即可3. 抽象类3.1 概念抽象类定义包含纯虚函数的类被称作抽象类也叫接口类。纯虚函数定义在虚函数后写 0此函数即为纯虚函数。抽象类自身不能实例化对象。派生类继承抽象类后重写基类纯虚函数才能实例化对象。示例#include iostream using namespace std; class Car { public: virtual void Drive() 0; // 纯虚函数 virtual ~Car() default; }; class Benz : public Car { public: void Drive() override { cout Benz-舒适 endl; } }; class BMW : public Car { public: void Drive() override { cout BMW-操控 endl; } }; int main() { // Car c; // 编译报错抽象类不能创建对象 // Car* p new Car; // 同样非法 Car* p1 new Benz; Car* p2 new BMW; p1-Drive(); p2-Drive(); delete p1; delete p2; return 0; }3.2 接口继承和实现继承实现继承普通函数的继承属于实现继承派生类继承后直接使用该函数的具体实现。接口继承虚函数继承是接口继承派生类继承的是基类虚函数的接口目的是进行重写以实现多态。若不打算实现多态就不要把函数定义为虚函数。4. 多态的原理普通成员函数加virtual→ 所属类生成虚表、每个对象多一个虚函数指针vptr带来内存开销 动态绑定运行开销。所以实现多态有代价普通成员函数不多态就别写虚函数虚析构不一样虚析构是防御性良好习惯。4.1 多态分类多态分为两种编译时多态静态绑定通过函数重载和运算符重载实现。运行时多态动态绑定通过虚函数和继承实现。4.2 运行时多态的实现原理4.2.1虚函数表vtable每个包含虚函数的类都有一个虚函数表它是一个存储类中所有虚函数地址的表格。当派生类继承基类时先拷贝基类的虚函数表内容。这意味着派生类虚函数表初始时 基类虚函数表。当派生类重写基类虚函数时派生类虚函数表会更新指向派生类的实现。如果派生类新增了虚函数这些虚函数会按照它们在类中声明的顺序被添加到虚函数表的末尾。可以通过强制类型转换访问虚函数表指针并打印虚函数表内容但由于编译器可能未在虚函数表末尾放置明确的终止标志如nullptr这可能导致程序在打印时越界访问内存从而引发崩溃。4.2.2 虚函数指针vptr每个包含虚函数的对象都有一个虚函数指针它指向该对象所属类的虚函数表。当对象被创建时编译器会自动为对象分配一个虚函数指针将其初始化指向该类虚函数表。4.2.3 动态绑定静态绑定早期编译期直接确定函数调用地址。动态绑定后期编译期不定地址运行时判断对象真实类型再调用。流程取对象虚函数指针➡查对应类型虚表➡调用实际类型虚函数基类指针/引用调用被重写的虚函数即触发动态绑定。#include iostream using namespace std; // 基类带虚函数 class Base { public: virtual void func() { cout Base::func endl; } virtual ~Base() default; }; // 子类重写虚函数 class Derive : public Base { public: void func() override { cout Derive::func endl; } }; int main() { Base* p new Derive; p-func(); // 基类指针调用被重写的虚函数触发动态绑定 delete p; return 0; }5.单继承中派生类从一个基类继承。虚函数表的生成和使用相对简单。如 4.2.3 动态绑定 的代码示例。多继承中的虚函数表多继承中1派生类会为每个基类维护一个独立的虚函数表2派生类虚函数表会合并所有基类虚函数表3如果派生类没有重写某个基类虚函数该虚函数的地址会保留在原基类虚函数表中而不会被合并到派生类虚函数表。下面通过动态绑定和函数调用来间接展示多继承中虚函数表的特性 #include iostream class Base1 { public: virtual void func1() { std::cout Base1::func1() std::endl; } virtual void func2() { std::cout Base1::func2() std::endl; } }; class Base2 { public: virtual void func3() { std::cout Base2::func3() std::endl; } virtual void func4() { std::cout Base2::func4() std::endl; } }; // 派生类继承自 Base1 和 Base2 class Derived : public Base1, public Base2 { public: // 重写 Base1 的 func1 void func1() override { std::cout Derived::func1() std::endl; } // 派生类新增虚函数 virtual void func5() { std::cout Derived::func5() std::endl; } }; int main() { Derived derived; // 1 Base1* base1Ptr derived; Base2* base2Ptr derived; std::cout 通过 Base1 指针调用虚函数: std::endl; base1Ptr-func1(); base1Ptr-func2(); std::cout 通过 Base2 指针调用虚函数: std::endl; base2Ptr-func3(); base2Ptr-func4(); // 2 Derived* derivedPtr derived; std::cout 通过 Derived 指针调用新增虚函数: std::endl; derivedPtr-func5(); // 3 std::cout 调用未重写的 Base1::func2: std::endl; base1Ptr-func2(); std::cout 调用未重写的 Base2::func3 和 Base2::func4: std::endl; base2Ptr-func3(); base2Ptr-func4(); return 0; }