千面之法: 释放 C++ 多态的灵活威力
目录
2.3.1.1:协变(基类与派生类函数的返回值不同,基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或引用时)
1:多态的概念
1.1:概念
通俗来讲,多态就是多种形态,具体一些就是当去完成某个行为的时候,当不同的对象去完成时会产生出不同的状态.
2.多态的定义与实现
2.1:多态的构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为.譬如Student继承了Person,Person对象买票全价,Student对象买票半价.那么在继承中要构成多态还有两个条件1.必须通过基类的指针或者引用调用虚函数2.被调用的函数必须是虚函数,且派生类对基类的虚函数进行重写.
#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; class Adult { public: virtual void BuyTicket() { cout << "买票----全价" << endl; } }; class Child : public Adult { public: virtual void BuyTicket() { cout << "买票----半价" << endl; } }; /* * 多态的条件 * 1.必须要有继承关系 * 2.必须要有虚函数(父类的虚函数和子类的虚函数,要求三同(函数名,参数名,返回值) */ void Func(Adult& a) { a.BuyTicket(); } int main() { Adult a; Child c; Func(a); Func(c); return 0; }


2.2:虚函数
概念:被virtual修饰的类成员函数称为虚函数
class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl;} };
2.3:虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类的虚函数返回值类型,函数名,参数列表完全相同),称子类的虚函数重写了基类的虚函数.

#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; class Adult { public: virtual void BuyTicket() { cout << "买票----全价" << endl; } }; class Child : public Adult { public: virtual void BuyTicket() { cout << "买票----半价" << endl; } }; /* * 多态的条件 * 1.必须要有继承关系 * 2.必须要有虚函数(父类的虚函数和子类的虚函数,要求三同(函数名,参数名,返回值) */ void Func(Adult& a) { a.BuyTicket(); } int main() { Adult a; Child c; Func(a); Func(c); return 0; }2.3.1:虚函数重写的两个例外
2.3.1.1:协变(基类与派生类函数的返回值不同,基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或引用时)
派生类重写类虚函数时,与基类虚函数返回值类型不同,即基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或者引用时.


2.3.1.2:析构函数的重写
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同.虽然函数名不相同,起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理称destructor.

#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; class Person { public: virtual ~Person() { cout << "~Person()" << endl; } }; //公有继承 class Student : public Person { virtual ~Student() { cout << "~Student" << endl; } }; int main() { Person* p1 = new Person; Person* p2 = new Student; delete p1; delete p2; return 0; }2.4:C++11 override和final
C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
2.4.1:final关键字
修饰虚函数,表示该虚函数不能够再被重写
#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; class Car { public: virtual void Drive() final { } }; class Benz: public Car { virtual void Drive() { cout << "Benz" << endl; } }; int main() { return 0; }
#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; //C++11的方法: final修饰的类叫最终类,不能继承 class Car final { public: private: // C++98的方法:父类的构造函数私有 // 子类的构造无法生成和实现,导致子类对象无法实例化 Car() { } }; class Benz :public Car { public }; int main() { Benz b; return 0; }
2.4.2:override关键字
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错.
#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; class Car { public: //virtual void drive() //{ //} }; class Benz : public Car { public: virtual void Drive() override { cout << "virtual void Drive()" << endl; } }; int main() { return 0; }
2.5:重载、重写、 隐藏的对比
重载两个函数在同一个作用域函数名相同/参数不同重写(覆盖)两个函数分别在基类与派生类的作用域.函数名/参数/返回值都必须相同(满足三同)协变除外.两个函数必须是虚函数.重定义(隐藏)两个函数分别在基类与派生类的作用域函数名相同两个基类和派生类的同名函数不构成重写就是重定义.


3.抽象类
3.1:概念
在虚函数的后面加上 = 0,则这个函数被成为纯虚函数.包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能够实例化出对象.派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象.纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承.
#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; class Car { public: virtual void Drive() = 0; }; class Benz : public Car { public: }; class BMW: public Car { public: virtual void Drive() { cout << "BMW()" << endl; } }; int main() { Car* pBenz = new Benz; pBenz->Drive(); Car* pBMW = new BMW; pBMW->Drive(); return 0; }
3.2:接口继承与实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
4:多态的原理
4.1:虚函数表
4.1.1:代码1
#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; class Base { public: virtual void Func1() { cout << "Func1" << endl; } private: int _b = 1; }; int main() { Base b; cout << sizeof(b) << endl; return 0; }

通过测试可以发现b对象是8字节.除了_b成员,还多了一个_vfptr放在对象的前面(有些平台可能会放到对象的最后面,这个跟平台有关系),对象中的这个指针叫做虚函数指针表(v代表virtual,f代表function).一个含有虚函数的类中至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表.
4.1.2:代码2
#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; class Base { public: virtual void Func1() { cout << "Base:Func1()" << endl; } virtual void Func2() { cout << "Base:Func2()" << endl; } virtual void Func3() { cout << "Base:Func3()" << endl; } private: int _b = 1; }; class Derive :public Base { public: virtual void Func1() { cout << "Derive:Func1()" << endl; } virtual void Func2() { cout << "Derive:Func2()" << endl; } private: int _d = 2; }; int main() { Base b; Derive d; return 0; }
4.2:多态的原理
#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; 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 Mike; Func(Mike); Student Johnson; Func(Johnson); return 0; }


4.3:动态绑定与静态绑定

5:单继承与多继承关系的虚函数表
5.1:单继承中的虚函数表
#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; class Base { public: virtual void Func1() { cout << "Base::Func1()" << endl; } virtual void Func2() { cout << "Base::Func2()" << endl; } private: int _a = 1; }; 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 = 2; }; //打印对象虚基表,对象虚基表本质是一个函数指针数组 typedef void(*Vfptr)(); void PrintVfptr(Vfptr * vft) { for(size_t i = 0; i < 4; i++) { cout << vft[i] << "->"; vft[i](); } } int main() { Base b; Derive d; Vfptr* ptr = (Vfptr*)(*(int*)(&d)); PrintVfptr(ptr); return 0; }


5.2:多继承中的虚函数表
#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; //定义函数指针 typedef void (*Vfptr)(); 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 << "Derive:func1()" << endl; } virtual void func3() { cout << "Derive:func3()" << endl; } private: int _d1; }; class Derive : public Base1, public Base2 { virtual void func1() { cout << "Derive:func1()" << endl; } virtual void func3() { cout << "Derive:func3()" << endl; } private: int _d1; }; void PrintTable(Vfptr Vtable[]) { cout << "虚表地址>" << Vtable << endl; for (size_t i = 0; Vtable[i] != nullptr; i++) { cout << "第" << i << "个虚函数地址: 0X" << Vtable[i] << endl; Vtable[i](); } cout << endl; } int main() { Derive d; /* * (*(int*)(&d))强制类型转换为指针类型并且解引用,那么每次在访问的时候只访问四个字节的数据 * 取出d对象的头4个字节,就是虚表的指针,虚表的本质是存了一个虚函数的指针数组,这个数组最后面放了一个Nullptr */ Vfptr* vTableb1 = (Vfptr*)(*(int*)&d); PrintTable(vTableb1); /* * 强转成char *,char*类型的指针每次解引用跳过一个字节, */ Vfptr* vTableb2 = (Vfptr*)(*(int*)((char*)&d + sizeof(Base1))); PrintTable(vTableb2); return 0; } 
6:多态相关的问题
什么是多态.
- 通俗来说,就是多种形态,具体一些就是去完成某个行为,当不同的对象去完成的时候会产生出不同的状态.
什么是重载、重写(覆盖)、重定义(隐藏)
- 重载
(1):两个函数在同一个作用域.
(2):函数名相同/参数不同.
- 重写(覆盖)
(1):两个函数分别在基类与派生类的作用域.
(2):要满足三同(函数名/参数/返回值都必须相同) PS:协变除外.
(3):两个函数都必须是虚函数.
- 重定义(隐藏)
(1):两个函数分别在基类和派生类的作用域
(2):函数名相同
(3):两个基类和派生类的同名函数(不构成重写就是重定义).
inline(内联函数)可以是虚函数吗
- 可以,不过编译器如果忽略inline属性,那么这个函数就不再是inline函数,而是会把虚函数表放到虚函数中.
静态成员函数可以是虚函数吗
- 不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表
构造函数可以是虚函数吗
- 不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的.
析构函数可以是虚函数吗
- 可以,并且最好把基类的析构函数定义成虚函数.
什么场景下析构函数是虚函数
- 如果基类的析构函数为虚函数,此时派生类的析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写.
对象是访问普通函数快还是虚函数快
- 首先如果是普通对象,是一样快的.、
- 如果是指针对象或者引用对象,则调用普通函数快一些,因为构成了多态,运行时调用虚函数需要到虚函数表中去查找.
虚函数表是在什么阶段生成的
- 虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的.
C++菱形继承的问题,虚继承的原理
- 菱形继承造成了数据冗余和二义性
