继承
继承机制
基于目标代码的复用。对事物进行分类,派生类是基类的具体化,把事物(概念)以层次结构表示出来,有利于描述和解决问题。
有利于增量开发 。
单继承
单继承指派生类只有一个基类。
实例
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 class Student { int id; public : char nickname[16 ]; void set_ID (int x) { id = x; } void SetNickName (char * s) { strcpy (nickname, s); } void showInfo () { cout << nickname << " : " << id << endl; } };class Undergraduated_Student : public Student { int dept_no; public : void setDeptNo (int x) { dept_no = x; } void setID (int x) { ... } void showInfo (int x) { cout << dept_no << " : " << nickname << endl;} private : Student::nickname; void setNickName () ; }int main () { Undergraduated_Student us; us.showInfo (); return 0 ; }
构造函数、拷贝构造函数、析构函数不能被继承。这些函数是管理对象的资源的,而派生类往往比基类有更多的资源。
继承方式不影响派生类对基类的访问,影响派生类用户对基类的访问。
访问控制
public、private:访问权限只和基类中的访问权限有关
public
public:class Undergraduated_Student: public Student
原来的 public 是 public,原来的 private 是 private
如果没有特殊需要建议使用 public
private
private:原来所有的都是 private,但是这个 private 是对于 Undergraduate_Student 大对象而言,所以他自己还是可以访问的。
默认的继承方式
protected
如果没有继承的话,protected 和 private 是相同的
派生类可以访问基类中 protected 的属性的成员。
派生类不可以访问基类中的对象 的 protected 的属性。
派生类含有基类的所有成员变量
public 继承
public
protected
不可见
private 继承
private
private
不可见
protected 继承
protected
protected
不可见
继承的初始化
派生类对象的初始化
构造函数的执行次序
基类的构造函数
派生类对象成员类的构造函数(注意!)
派生类的构造函数
析构函数的执行次序(与构造函数执行顺序相反)
派生类的析构函数
派生类对象成员类的析构函数
基类的析构函数
基类构造函数的调用
缺省执行基类默认构造函数
如果要执行基类的非默认构造函数 ,则必须在派生类构造函数的成员初始化表中指出
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 class A { int x; public : A () { x = 0 ; } A (int i) { x = i; } };class B : public A { int y; public : B () { y = 0 ; } B (int i) { y = i; } B (int i, int j) : A (i) { y = j; } B (const B& b) { } }; B b1; B b2 (1 ) ; B b3 (0 , 1 ) ; class B : public A { public : using A::A; };
虚函数
类型相容
类型相容
类型相容是指完全相同的(别名)
一个类型是另一个类型的子类型(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 的版本的对应函数被隐藏
静态绑定是只看形参类型
绑定时间
C++默认静态绑定
前期绑定(Early Binding)(静态绑定)
编译时刻确定调用哪一个方法
依据对象的静态类型
效率高、灵活性差
静态绑定根据形参决定
动态绑定(Late Binding)
晚绑定是指编译器或者解释器在运行前不知道对象的类型,使用晚绑定,无需检查对象的类型,只需要检查对象是否支持特性和方法即可。
c++中晚绑定常常发生在使用 virtual
声明成员函数
运行时刻确定,依据对象的实际类型(动态)
灵活性高、效率低
动态绑定函数也就是虚函数。
直到构造函数返回之后,对象方可正常使用
C++默认的都是静态绑定,Java 默认的都是动态绑定
后期绑定的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 指针
注重效率
默认前期绑定
后期绑定需显式指出 virtual
定义
虚函数是指一个类中你希望重载的成员函数,但你使用一个基类指针或引用指向一个继承类对象的时候,调用一个虚函数时,实际调用的就是继承类的版本。
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 42 43 44 45 46 47 48 49 50 #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 ; }
限制
类的成员函数才可以是虚函数
静态成员函数不能是虚函数
内联成员函数不能是虚函数
构造函数不能是虚函数
析构函数可以(往往)是虚函数
final 和 override
override:希望以虚函数的形式写(编译器报错,防止漏写 virtual 问题)
final:不可以再次重写
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 ) ; }
纯虚函数和抽象类
纯虚函数(Java 中的接口)
声明时在函数原型后面加上 = 0 :virtual int f() = 0;
往往 只给出函数声明,不给出实现:可以给出实现,通过函数外进行定义(但是不好访问,因为查到是 0)
1 2 3 4 int f () = 0 ;int f () { Base::f; }
抽象类
至少包含一个纯虚函数
不能用于创建对象:抽象类类似一个接口,提供一个框架
为派生类提供框架,派生类提供抽象基类的所有成员函数的实现
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 (); }
抽象工厂模式
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 ();
Step3:增加用户跨平台设计的支持,将 Button 抽象出来
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 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 ();
抽象工厂模式的类图
虚析构函数
1 2 3 4 5 6 7 8 9 10 11 12 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;
如果有继承的话,最好使用虚析构函数,在调用析构的函数,会先调用基类的析构函数,所以在析构函数中,只需要析构派生类自己的资源就可以了。
缺省参数
缺省值采用静态绑定方式,效率更高。
绝对不要重新定义继承而来的缺省参数值,以免引起阅读歧义:
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 class A { public : virtual void f (int x = 0 ) = 0 ; };class B : public A { public : virtual void f (int x = 1 ) { cout << x; } };class C : public A { public : virtual void f (int x) { cout << x; } }; A* p_a; B b; p_a = &b; p_a->f (); 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 );
函数的分类
1 2 3 4 5 6 class Shape { public : virtual void draw () const = 0 ; virtual void error (const string& msg) ; int objectID () const ; };
纯虚函数
只有函数接口会被继承
一般虚函数
函数的接口及缺省实现代码都会被继承
子类必须 继承函数接口
可以 继承缺省实现代码
不希望代码被经常修改,微小修改
非虚函数
函数的接口和其实现代码都会被继承
继承类型
public inheritance 公有继承
确定 public inheritance,是真正意义的"IS_A"关系:派生类就是基类,超类中所拥有的性质在子类中仍然成立
Require No more,Promiss No Less(LSP)里氏替换原则:里氏替换原则没有给出实现
不要定义与继承而来的非虚成员函数同名的成员函数
Penguin 问题
软件外包的时候不知道是谁写的,无法修改客户代码
1 2 3 4 class FlyingBird ;class NonFlyingBird ;virtual void fly () { error ("Penguins can't fly!" ); }
契约式设计
每个方法在调用前,应该校验传入参数的正确性,正确才能进行调用,否则是违反契约的。
前置条件:子类方法相比于父类中部被覆盖方法相同或更加宽松(能调用父类一定能调用子类)
后置条件:子类方法相比于父类中部被覆盖方法相同或更加严格(调用子类不出错,调用父类一定不出错)
长方形问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Rectangle { public : void setHeight (int ) ; void setWidth (int ) ; int height () const ; int width () const ; };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 );
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 class Rectangle { public : virtual void setHeight (int ) ; virtual void setWidth (int ) ; int height () const ; int width () const ; };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); }
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 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 没有任何问题
最终的问题:想要满足里氏替换原则,派生类的条件应该比基类更加弱,而这个是更强
不要定义与继承而来的非虚成员函数同名的成员函数
1 2 3 4 5 6 7 8 9 10 class B {class D : public B { public : void mf () ; }; D x; B* pB = &x; pB->mf (); D* pD = &x; pD->mf ();
对于一个对象,在不同的指针的情况下会表示出来不同的行为,这是很麻烦的。
运行时不要出现不一致的
private inheritance 私有继承
特别含义:Implemented-in-term-of 用继承来实现某个功能
需要使用 Base Class 中的 protected 成员,或重载 virtual function
表示不希望一个 Base Class 被 client 使用,否则会使用公有继承
利用一些已经存在的代码,只是在实现中使用到了基类
在设计层面无意义,只用于实现 层面,只是复用实现的方式,接口是被忽略的
实际上是 Has-A 关系
如果两个类的继承是私有的,则不能在派生类外将派生类转换成基类对象。
和组合的联系
尽可能用组合,万不得已用继承
情况一
需要使用 protected 和重载 virtual function
情况二
我们需要尽可能复用,并且减少内存的占用。
如果没有静态成员、虚函数、虚基类,那么一个类是被认为是一个空的类,而方法是定义在代码区。对象不占用空间。
技术上:所有的独立对象必须有一个大小,不然会导致两个指针指向一个地址。
Example:算法的类图,如果使用组合,依据会生成生成一个指针
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);
多继承
定义
定义
1 2 3 class <派生类名>:[<继承方式>] <基类名 1>, [<继承方式>] <基类名 2>,… {<成员表>}
Java 不允许多继承,是因为多继承非常复杂。
继承方式:默认是 private 的继承方式:public、private 、protected
继承方式及访问控制的规定同单继承:重复进行继承
派生类拥有所有基类的所有成员
可以睡的沙发:继承 sofa 和 Bed
setWeight 重名:两个基类有相同部分,我们会拆分基类
之后我们拆分出来 setWeigth()(Base Class Decomposition)
形成菱形结构:还有问题,Weigth 变量依旧在,已然有两个 Weigth 部分
解决方案:虚继承
基类声明顺序(初始化顺序)
基类的声明次序决定:
对基类构造函数/析构函数的调用次序(顶部基类,同层基类按照声明顺序) 上图中就是 ABCD 的顺序
对基类数据成员的存储安排
析构函数正好相反
名冲突
<基类名>::<基类成员名>
成交变量和成员函数的重名问题:在上图中,D 中会有 B 和 C 的 x
问题:每次 setWeight 到底是设置谁的?
虚基类
如果直接基类(如 B、C)有公共的非虚基类 (如 A),则该公共基类中的成员变量在多继承的派生类中有多个副本
如果有一个公共的虚基类 ,则成员变量只有一个副本
类 D 有两个 x 成员,B::x
,C::x
虚继承:保留一个虚指针
虚指针指向 A
可以认为是一个组合关系
合并
1 2 3 4 5 class A ;class B : virtual public A;class C : public virtual A;class D : B, C;
虚基类注意
虚基类的构造函数由最新派生出 的类的构造函数调用
原来是 B 构造一份 A,C 构造一个 A
而现在是由 D 调用 A 的构造函数,在 D 的时候先调用 A 的构造函数,在构造 B 和 C 的时候不再调用 A 的构造函数,而只是存放指针
虚基类的构造函数优先非虚基类的构造函数执行
如果有两个基类,两个类有一个相同名称的虚函数,比如 B 和 C 都有一个同名的虚函数,到底怎么做?不做要求
操作符重载
函数重载:名同,参数不同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Counter { int value; public : Counter () { value = 0 ; } Counter& operator ++() { value ++; return *this ; } Counter operator ++(int ) { Counter temp = *this ; value++; return temp; } }
不可以重载的操作符: .
(成员访问操作符)、.*
(成员指针访问运算符,如下)、::
(域操作符,后面不是变量)、?:
(条件操作符,涉及到跳转,影响理解)、sizeof
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class A { int x; public : A (int i) : x (i) {} void f () {} };int main () { void (A::*p_f)(); p_f = &A::f; A a (1 ) ; (a.*p_f)(); *p_f (); return 0 ; }
永远不要重载 &&
和 ||
:短路运算会造成极大的问题。
注意:=
、()
、[]
、->
不可以作为全局函数重载。
大体上来讲,C++ 一个类本身对这几个运算符就已经有了相应的解释了。
如果将这四种符号进行友元全局重载,则会出现一些冲突
下标和箭头运算符为什么?有保留调用顺序,我们希望能保留原来的顺序,而全局不能要求,而成员函数的 this 就可以解决这个问题
操作符重载的哲理:尽量让事情有效率,但不是过度有效率(返回引用),每次就是返回一个拷贝,而不是引用。
操作符 = 的重载
默认赋值操作符重载函数
逐个成员赋值
对含有对象成员的类,该定义是递归的
赋值操作符的重载不可以被继承 :因为拷贝构造,派生出来的类有一些新的部分
返回引用类型:返回 *this 的引用,支持链式赋值
this 引用应该是非常量引用,返回出来的是作为右值进行计算
a = b = c:不要求非常量引用
(a = b).f():要求非常量引用