虚函数
一个类只有一个虚函数表。
实现多态的函数
1. 类型相容
类、类型:
类型相容:
类型相容是指完全相同的(别名)
一个类型是另一个类型的子类型(int -> long int)
赋值相容(不会丢失信息):对于类型相同的变量才有
如果类型相同可以直接赋值
子类型可以赋值给父类型
问题:a和b都是类,a、b什么类型时,a = b合法(赋值相容)?B是A的子类型的时候
A a; B b; class B: public A
对象的身份发生变化(a和b都代表栈上对应大小的内存),B类型对象变为了A类型的对象
属于派生类的属性已不存在
将派生类对象赋值给基类对象->对象切片
A a = b
:调用拷贝构造函数
const A &a
:函数必然包含的拷贝构造函数中的参数
B* pb; A* pa = pb; class B: public A
因为是赋值相容的,所以可以指针赋值
这种情况类似Java
B b; A & a=b; class B: public A
:对象身份没有发生变化(还是B)
把派生类对象赋值给基类对象,基类的引用或指针可以引用或指向派生类对象,不严谨的说,可以说让父类指向子类
传参的时候尽量不要拷贝传参(存在对象切片问题),而是使用引用传参。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class A { int x,y; public : void f () ; };class B : public A{ int z; public : void f () ; void g () ; }; A a; B b; a = b; b = a; a.f (); A &r_a = b; A *p_a = &b; B &r_b = a; B *p_b = &a; func1 (A& a){a.f ();}func2 (A *pa){pa->f ();}func1 (b);func2 (&b);
func1(b):为什么是A的呢?
对于B,A的版本的对应函数被隐藏
静态绑定是只看形参类型
2. 绑定时间
C++默认静态绑定
2.1. 前期绑定(Early Binding)(静态绑定)
编译时刻确定调用哪一个方法
依据对象的静态类型
效率高、灵活性差
静态绑定根据形参决定
2.2. 动态绑定(Late Binding)
晚绑定是指编译器或者解释器在运行前不知道对象的类型,使用晚绑定,无需检查对象的类型,只需要检查对象是否支持特性和方法即可。
c++中晚绑定常常发生在使用virtual
声明成员函数
运行时刻确定,依据对象的实际类型(动态)
灵活性高、效率低
动态绑定函数也就是虚函数。
直到构造函数返回之后,对象方可正常使用
C++默认的都是静态绑定,Java默认的都是动态绑定
2.3. 后期绑定的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class A { int x,y; public : virtual f () ; virtual g () ; h (); };class B : public A{ int z; public : f (); h (); }; A a; B b; A *p;
p->f():需要寻找a和b中的f()函数地址
如果不能明确虚函数个数,没有办法索引
虚函数表(索引表,vtable):大小可变
首先构造基类的虚函数表
然后对派生类中的函数,如果查找了,则会覆盖对应函数来生成虚函数表
对象内存空间中含有指针指向虚函数表
(**((char *)p - 4))(p)
:f的函数调用(从虚函数表拿数据),p是参数this
空间上和时间上都付出了代价
空间:存储虚函数表指针和虚函数表
时间:需要通过虚函数表查找对应函数地址,多调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class A { public : A () { f ();} virtual void f () ; void g () ; void h () { f (); g (); } };class B : public A { public : void f () ; void g () ; }; B b; A *p= &b; p->f (); p->g (); p->h ();
尽量不要在构造函数中调用虚函数
此时的虚函数就是和构造函数名空间相同
h()函数是非虚接口
有不同的实现:调用了虚函数和非虚函数
可以替换部分的实现
可以使得非虚函数具有虚函数的特性(让全局函数具有多态:将全局函数做成非虚接口)
1 2 3 4 5 6 7 8 9 10 11 12 13 class A { public : virtual void f () ; void g () ; };class B : public A{ public : void f (B* const this ) { g (); } void g () ; }; B b; A* p = &b; p->f ();
g()是静态绑定
虚函数中调用非虚函数:所有版本是和虚函数一致 的
非虚函数调用虚函数:正常
虚函数要严格查表,非虚函数静态确定,对应p->h()
注意每一个函数在调用的时候都会传入一个const的this指针
2.4. 注重效率
默认前期绑定
后期绑定需显式指出 virtual
3. 定义
虚函数是指一个类中你希望重载的成员函数,但你使用一个基类指针或引用指向一个继承类对象的时候,调用一个虚函数时,实际调用的就是继承类的版本。
1 2 3 4 class A { public : virtual void f () ; };
定义绑定:根据实际引用和指向的对象类型
方法重定义
注意:如基类中被定义为虚成员函数,则派生类中对其重定义的成员函数均为虚函数 ,也就是派生类中的对应函数可以不写虚函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #include <iostream> using namespace std;class Parent { public : char data[20 ]; void Function1 () ; virtual void Function2 () ; } parent;void Parent::Function1 () { printf ("This is parent,function1\n" ); }void Parent::Function2 () { printf ("This is parent,function2\n" ); }class Child :public Parent{ void Function1 () ; void Function2 () ; } child;void Child::Function1 () { printf ("This is child,function1\n" ); }void Child::Function2 () { printf ("This is child,function2\n" ); }int main (int argc, char * argv[]) { Parent *p; if (_getch()=='c' ) p=&child; else p=&parent; p->Function1 (); p->Function2 (); return 0 ; }
4. 虚函数限制
类的成员函数才可以是虚函数:全局函数不可以是虚函数
静态成员函数不能是虚函数:静态的成员函数属于类,并不属于一个对象,所以不能虚函数
内联成员函数不能是虚函数:内联成员函数在编译的时候就已经确定了
构造函数不能是虚函数:
因为创建类的时候是自动调用的,父类的指针无法直接调用,虚函数没有意义
虚函数表是在构造函数中完成的
析构函数可以(往往)是虚函数
如果不是虚函数,不好调用到派生类中的析构函数(delete一个父类指针,如果非虚,不能调用到派生类的析构函数)
5. final,override
1 2 3 4 5 6 7 8 9 10 11 12 13 struct B { virtual void f1 (int ) const ; virtual void f2 () ; void f3 () ; virtual void f5 (int ) final ; };struct D : B { void f1 (int ) const override ; void f2 (int ) override ; void f3 () override ; void f4 () override ; void f5 (int ) ; };
override:希望以虚函数的形式写:编译器报错,防止漏写virtual问题
final:不可以再次重写
6. 纯虚函数和抽象类
6.1. 纯虚函数(Java中的接口)
声明时在函数原型后面加上 = 0 :virtual int f() = 0;
往往 只给出函数声明,不给出实现:可以给出实现,通过函数外进行定义(但是不好访问,因为查到是0)
1 2 3 4 int f () = 0 ;int f () { Base::f; }
6.1.1. 抽象类
至少包含一个纯虚函数
不能用于创建对象:抽象类类似一个接口,提供一个框架
为派生类提供框架,派生类提供抽象基类的所有成员函数的实现
1 2 3 4 class AbstractClass { public : virtual int f () = 0 ; };
1 2 3 4 5 6 7 Figure *a[100 ]; a[0 ] = new Rectangle (); a[1 ] = new Ellipse (); a[2 ] = new Line (); for (int i=0 ; i<num_of_figures; i++){ a[i]->display (); }
6.3. 抽象工厂模式
Step1:提供Windows GUI类库:WinButton
1 2 3 4 WinButton *pb= new WinButton (); pb->SetStyle (); WinLabel *pl = new WinLabel (); pl->SetText ();
step2:增加对Mac的支持:MacButton,MacLabel
1 2 3 4 MacButton *pb= new MacButton (); pb->SetStyle (); MacLabel *pl = new MacLabel (); pl->SetText ();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 Button *pb= new MacButton (); pb->SetStyle (); Label *pl = new MacLabel (); pl->SetText ();class AbstractFactory {public : virtual Button* CreateButton () =0 ; virtual Label* CreateLabel () =0 ; }; class MacFactory : public AbstractFactory {public : MacButton* CreateButton () { return new MacButton; } MacLabel* CreateLabel () { return new MacLabel; } }; class WinFactory : public AbstractFactory {public : WinButton* CreateButton () { return new WinButton; } WinLabel* CreateLabel () { return new WinLabel; } };class Button ; class MacButton : public Button {}; class WinButton : public Button {}; class Label ; class MacLabel : public Label {}; class WinLabel : public Label {}; AbstractFactory* fac;switch (style) {case MAC: fac = new MacFactory; break ;case WIN: fac = new WinFactory; break ; } Button* button = fac->CreateButton (); Label* Label = fac->CreateLabel ();
抽象工厂模式的类图
7. 虚析构函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class B {...};class D : public B{...}; B* p = new D;delete p;class mystring {...}class B {...}class D : public B{ mystring name; ... } B* p = new D;delete p;
如果有继承的话,最好使用虚析构函数,在调用析构的函数,会先 调用基类的析构函数,所以:
在析构函数中,只需要析构派生类自己的资源就可以了
8. 继承的分类
8.1. public inheritance
确定public inheritance,是真正意义的"IS_A"关系:派生类就是基类
Require No more,Promiss No Less(LSP)里氏替换原则:里氏替换原则没有给出实现
不要定义与继承而来的非虚成员函数同名的成员函数
8.1.1. Penguin问题
软件外包的时候不知道是谁写的,无法修改客户代码
1 2 3 4 class FlyingBird ;class NonFlyingBird ;virtual void fly () { error ("Penguins can't fly!" ); }
8.1.2. 如何实现里氏替换原则
Design By contract:每个方法在调用前,应该校验传入参数的正确性,正确才能进行调用,否则是违反契约的。
校验前置条件:对象对自己进行校验
8.1.3. 长方形问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 class Rectangle { public : void setHeight (int ) ; void setWidth (int ) ; int height () const ; int width () const ; };assert (s.width () == s.height ());class Square : public Rectangle { public : void setLength (int ) ; private : void setHeight (int ) ; void setWidth (int ) ; };assert (s.width () == s.height ());Square s (1 ,1 ) ; Rectangle *p = &s; p->setHeight (10 );class Rectangle { public : virtual void setHeight (int ) ; virtual void setWidth (int ) ; int height () const ; int width () const ; };assert (s.width () == s.height ());class Square : public Rectangle { public : void setLength (int ) ; public : void setHeight (int ) ; void setWidth (int ) ; };void Widen (Rectangle& r, int w) { int oldHeight = r.height (); r.setWidth (r.width () + w); assert (r.height () == oldHeight); }class Rectangle { public : virtual void setHeight (int ) ; virtual void setWidth (int ) ; int height () const ; int width () const ;};assert (s.width () == s.height ());class Square : public Rectangle { public : void setLength (int ) ; private : void setHeight (int ) ; void setWidth (int ) ; };assert (s.width () == s.height ());void Widen (Rectangle& r, int w) { int oldHeight = r.height (); r.setWidth (r.width () + w); assert (r.height () == oldHeight); }
访问控制仅仅是检查这个成员时是否属于当前对象声明的静态部分中可访问,和实际运行时时刻无关。
只要通过编译检查,对于public和private没有任何问题
最终的问题:想要满足里氏替换原则,派生类的条件应该比基类更加弱,而这个是更强
8.1.4. 不要定义与继承而来的非虚成员函数同名的成员函数
1 2 3 4 5 6 7 8 9 10 11 12 13 class B { public : void mf () ; };class D : public B { public : void mf () ; }; D x; B* pB = &x; pB->mf (); D* pD = &x; pD->mf ();
对于一个对象,在不同的指针的情况下会表示出来不同的行为,这是很麻烦的。
运行时不要出现不一致的
8.2. private inheritance 私有继承
特别含义:Implemented-in-term-of 用继承来实现某个功能
需要使用Base Class中的protected成员,或重载virtual function
表示不希望一个Base Class被client使用,否则会使用公有继承
利用一些已经存在的代码,只是在实现中使用到了基类
在设计层面无意义,只用于实现 层面,只是复用实现的方式,接口是被忽略的
实际上是Has-A关系
如果两个类的继承是私有的,则不能在派生类外将派生类转换成基类对象。
8.3. 和组合的联系
尽可能用组合,万不得已用继承
8.3.1. 情况一
需要使用protected和重载virtual function
8.3.2. 情况二
我们需要尽可能复用,并且减少内存的占用。
如果没有静态成员、虚函数、虚基类,那么一个类是被认为是一个空的类,而方法是定义在代码区。对象不占用空间。
技术上:所有的独立对象必须有一个大小,不然会导致两个指针指向一个地址。
Example:算法的类图,如果使用组合,依据会生成生成一个指针
8.3.3. Example
目的是为了重定义eat函数
私有继承不能将派生类转换成基类对象
可以强制转换(提供方法)
1 2 3 4 5 6 class CHumanBeing { … };class CStudent :private CHumanBeing { … };void eat (const CHumanBeing & h) { … } CHumanBeing a;CStudent b;eat (a);eat (b);
9. 虚函数的分类
1 2 3 4 5 6 class Shape { public : virtual void draw () const = 0 ; virtual void error (const string& msg) ; int objectID () const ; };
9.1. 纯虚函数
只有函数接口会被继承
9.2. 一般虚函数
函数的接口及缺省实现代码都会被继承
子类必须 继承函数接口
可以 继承缺省实现代码
不希望代码被经常修改,微小修改
9.3. 非虚函数
函数的接口和其实现代码都会被继承
10. 绝对不要重新定义继承而来的缺省参数值!
静态绑定
效率
10.1. Example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class A { public : virtual void f (int x = 0 ) =0 ; };class B : public A{ public : virtual void f (int x = 1 ) { cout << x;} }; A *p_a; B b; p_a = &b; p_a->f ();class C : public A{ public : virtual void f (int x) { cout<< x;} }; A *p_a1; C c; p_a1 = &c; p_a1->f ();
对象中只记录了虚函数入口的位置
默认参数在编译的时候静态绑定(最开始就将x绑定到所有的此类函数中去。)
并没有为每一个版本存储记录一个缺省值:因为如果有的话,需要用额外的空间来存储缺省值。
如果给出参数,则进行压栈(1),而往往有时候没有,这时候将默认参数存储虚函数表中。这会影响语言的效率。(避免寻址)
1 2 3 (**((char *)p_a1 - 4 ))(p_a1)char *q = *((char *)p_a1 - 4 ); (*q)(p_a1, *q+4 );
11. 参考
C++中的virtual(虚函数)的用法