2. C++ Polymorphism
C++的多态性
编译期多态:依靠template模板和overloading重载,以不同的模板参数具现化导致调用不同的函数
运行时多态:依靠vptr虚函数机制,运行时通过虚表指针与虚函数表去确定该类虚函的真正实现
> 相较于运行时多态,实现编译期多态的类之间并不需要成为一个继承体系,它们之间可以没有什么关系,但约束是它们都有相同的隐式接口。
> 虚函数其实是用了动态联编,即程序在编译阶段并不能确切知道将要调用的函数,只有在程序运行时才能确定将要调用的函数(new出的对象才是动态绑定……对于栈上的对象来说,其要调用的函数是确定的,所以是静态绑定):(* p->vptr[n] )()
编译期多态:模板与重载
模板 实现了一套代码供多种类型使用的能力(泛型),常见于容器类、可自定义数据类型的类
模板是怎么实现的? 如果传入的T不支持定义的操作,会不会编译不过?
- 模板会实例化成不同的代码,进行泛型替换后再编译执行(会产生代码膨胀)
- 只有到实例化一个模板时,编译器才会生成代码( 会实例化模板的所有成员,而不是用到哪个成员才实例化哪个成员)
- 因此类型相关的错误可能在链接时才报告
- 模板也支持自定义类型,但是如果该类型有些操作不支持(如没有重载运算符),那么有可能会出错
- 类模板的成员函数就是一个普通的成员,而不是模板
模板的特化
对于某个特定的类型,需要对模板进行特殊化,即特殊的处理,而不走原模板
// 原模板类template <class T, P>class A<T, P> {...};// 对齐进行int,double全特化template <>class A<int, double>{int data1;double data2;};// 原模板方法template <class T>T mymax(const T t1, const T t2){return t1 < t2 ? t2 : t1;}// 重载掉这个方法,特化处理const char* mymax(const char* t1,const char* t2){return (strcmp(t1,t2) < 0) ? t2 : t1;}const char* p = mymax(p1,p2);
这里就是声明了一个重载函数,对char* 类型特化处理,因为用模板的判断会出错
模板偏特化
模板的偏特化是指根据需要模板的某些但不是全部的参数进行特化
template <class T2>class A<int, T2> { ... };
严格的来说,函数模板并不支持偏特化,但由于可以对函数进行重载,所以可以达到类似于类模板偏特化的效果。
template <class T2>void f<int, T2>(){} // 会报错// 重载实现template <class T2>void f(){} // 注意:这里没有"模板实参"
类模板的匹配规则:最优化的优于次特化的,即模板参数最精确匹配的具有最高的优先权
函数模板的匹配规则:非模板函数具有最高的优先权。如果不存在匹配的非模板函数的话,那么最匹配的和最特化的函数具有高优先权
运行时多态:继承与重写
继承
默认继承方式:class默认为private继承,struct默认为public继承
public继承:变量/函数修饰符不变,private成员直接不继承,不能访问
protected继承:public变为protected,private成员直接不继承,不能访问
private继承:public、protected变为private,private成员直接不继承,不能访问
怎么写好一个继承类:
要调用父类的构造函数 / 析构函数
class C:C(int i, string n): A(i) // 选择父类构造调用
- 子类由于要初始化父类成员,因此必须要调用父类的构造函数,如果没有显示调用,则系统必会调用父类的无参构造函数
- 同样,对于析构函数来说,也必须要调用父类的析构函数(逆序);对于拷贝构造、赋值构造函数来说,它们也必须调用对应的父类构造函数……如果父类没有相应构造函数,则出现error
父类声明虚析构函数
A* a = new C(); // 父类指针指向子类对象delete a; // 如果父类不是虚析构函数,那么就只会调用A的析构函数!C* c = new C(); // 正常创建delete c; // 必定能先C后A成功析构,无论A是否是虚析构
因此,父类声明虚析构函数是用来保证其子类能成功调用到自身析构函数的
构造函数不能是虚函数,为什么呢?
时机、意义、实现
1. 从存储空间角度看,虚函数对应一个指向vtable虚函数表的指针,可是这个指向vtable的指针其实是存储在对象的内存空间的。如果构造函数是虚的,就需要通过vtable来调用,而此时对象还没有实例化,vtable还没有建立,怎么找虚函数呢?所以构造函数不能是虚函数。
2. 从使用角度看,虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
3. 从实现上看,vtable在构造函数调用后才建立,因而构造函数不可能成为虚函数。从实际意义看,在调用构造函数时还不能确定对象的真实类型,而且构造函数的作用是提供初始化,在对象的生命周期只执行一次,不是对象的动态行为,也没有必要成为虚函数。
重写函数 与 隐藏函数 的区别
- 函数隐藏是 子类函数名与父类相同,则父类所有重载函数都被隐藏
- 重写函数是 子类函数签名要与父类相同,且父类是virtual函数,同样所有重载函数都被覆盖掉。重写可以省略override标记
调用父类的重载运算符函数
重载继承法:调用父类C &operator=(const C &c){A::operator=(c);}
private虚函数
主要作用在,如果虚函数是private,那么根据继承方式的不同(public/protected/private),该函数的存在形式可能会不同(可被重写/已不存在),那么其意义就会改变…… 当我希望该虚函数不能被外部调用、被私有继承后不存在时,就可以声明为private虚函数
但若继承下来后进行重写时,是连其访问修饰符都可以修改的(如改为public)
给C++抽象类用的-纯虚函数
这块其实是面向对象语言的知识
纯虚函数:virtual void Function()= 0;
- 抽象类:至少有一个纯虚函数的类+不能实例化+要求子类必须实现纯虚函数(除子抽象类外)
- 为了强调一个类是抽象类,可将该类的构造函数声明为protected(不可实例化)
- 抽象类不能用作参数类型、函数返回类型或显式转换的类型
构造函数中可以调用虚函数吗?
可以,但是没有动态绑定的效果,因为虚函数表还没有建立,
父类构造函数中调用的仍然是父类版本的函数,子类中调用的仍然是子类版本的函数
关于上转型
将子类转父类
C* c = new C();A* a = (A*) c;
a调用的依然是子类的函数,因为vptr不会变
多重继承
如class D: public A, private B, protected C
常用于接口类、功能组件类、复合对象等
构造函数
可以调用多个父类的构造函数。D(): B(), C(), A()
这些构造函数的调用顺序 和上述出现顺序无关,而是和声明子类时父类出现的顺序相同。也就是 A B C
命名冲突
父类可能会出现同名的成员,你需要在 成员名字前面加上类名和域解析符::,以显式地指明到底使用哪个类的成员,消除二义性(如A::Func())
菱形继承需要经过虚继承来实现(否则base class会经由每一条路径被复制,编译器会产生二义性)
class File{};
class InputFile: virtual public File{};
class OutputFile: virtual public File{};
class IOFile: public InputFile, public OutputFile{};
但总是用virtual也是不合适的,它有代价:
- 虚继承类的对象会更大一些;
- 虚继承类的成员访问会更慢一些;
- 虚继承类的初始化更反直觉一些。继承层级的最底层(most derived class)负责虚基类的初始化,而且负责整个继承链上所有虚基类的初始化。
基于这些复杂性,Scott Meyers对于多继承的建议是:
- 如果能不使用多继承,就不用他;
- 如果一定要多继承,尽量不在里面放数据,也就避免了虚基类初始化的问题。
- 如需了解更多,参考Effective C++
关于虚继承的指针
pA==pC 而 pB!=pC
为什么呢? 因为C继承自A和B,A和B各有一个虚表指针vptr,所以C和A首地址相同,和B首地址不同