一.继承的解释
假如你要设计一个学校管理系统,那么你就要设计学生类,教师类,管理者类......,这些类都要设计一些相同成员变量或者方法,比如名字,年龄......。这样会有很多功能重复的代码。
把相同功能的变量和方法写在父类中(或基类),然后让子类(或派生类)继承父类的结构。而对于子类的个性在子类本身中补充,这种方法叫类的继承。
这样设计的结果就是,各个子类既有共性,也有个性(在子类中共性不需要显示定义,直接使用即可;个性需要显示定义)。
需要注意的是,对于普通成员函数,子类会直接调用父类的函数;对于构造函数和析构函数是调用子类自己的函数;对于变量,子类是自己再定义一份,而不是和父类共用一个(不需要显示定义,直接使用);
---------------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------------------------
二.继承的使用
格式:
1.子类名
2.冒号
3.继承方式(如果不写的话默认是private)
4.父类名
补充:一个类可以继承多个类,如需要继承多个类,只需要在4后面加个逗号然后再写另一组3,4:
其他的容易理解,接下来介绍一下继承方式:
我们之前学过类的访问限定符有这几种:
而继承方式也是这几种:
父类中的访问限定符和子类的继承方式之间的不同组合方式决定了子类中的访问限定模式。如下图,父类中的public成员在 以public方式继承的父类的子类中 是public成员。
接下来我介绍一下这张表中可能不清楚的地方
-
在派生类不可见 是指,这个在派生类中存在,但是他既不可以在派生类外访问也不可以在派生类中访问(如果真想访问的话,可以调用派生类继承父类的成员函数来访问修改它,因为对于普通成员函数,子类会直接调用父类的函数,而在父类中,这个成员是可以访问的)
-
protected 和 private在一个类中的功能一致。但是在继承中protected,private与其他访问方式组合产生的效果会不同。如上图,private成员与public继承方式组合是不可见,protected成员与public继承方式组合是protected限定,而我们知道不可见和protected是不同的,这就是不同的效果,可以说,protected为继承而设计,否则只需要private就够了。
-
实际上面的表格我们进行一下总结会发现
(1)基类的私有成员在子类都是不可见。
(2)基类的其他 成员在子类的访问方式为 成员在基类的访问限定符和继承方式之间的最小值(public > protected > private)
补充:使用关键字class定义类时默认的继承方式是private(可以不用显示写),使用struct时默认的继承方式是public(可以不用显式写),不过最好显示的写出继承方式。
---------------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------------------------
三.基类和派生类对象赋值转换
子类对象可以赋值给父类对象,尽管他两不是同一类型的对象。
说到不同类型变量之间的赋值,我们很容易想到的过程是:隐式类型转换生成与左值相同类型的临时对象,然后赋值。
但是这个基类和派生类对象赋值转换比较特殊,拿student赋值给person举例,它的过程如下:
直接截取student中继承了父类的部分,这一部分的类型是person,然后直接赋值给person,中间没有类型转换
那么下面这种情况呢?:
p指向的是x中继承person的部分的空间,所以修改p指向的内容,x的内容也会被修改。
q引用的是x中继承person的部分的空间,所以修改p引用的内容,x的内容也会被修改。
补充:把基类赋值给派生类一般不可以,但不是绝对不可以,这里就不说了。
四.继承中的作用域
-
在继承体系中基类和派生类都有独立的作用域。
-
子类和父类中有同名 成员,子类成员将屏蔽对父类同名成员的直接访问 ,这种情况叫隐藏 , 也叫重定义。(但是可以使用 基类::基类成员 显示访问)
-
需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏 ,而不需要关心参数是否相同(因为对于普通成员函数,子类会直接调用父类的函数所以子类和父类的同名函数不在同一作用域)。
-
**隐藏不是重载,**不是说子类函数的参数匹配不上,父类同名函数能匹配上就调用父类的函数,而是直接报错!
---------------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------------------------
五.派生类的默认成员函数
普通类的成员分为两部分:内置类型成员和自定义类型成员
而子类(派生类)的成员其实可以分为三部分:内置类型成员和自定义类型成员以及父类的成员。是的,在分析对子类成员的处理中,我们应该把子类中继承父类的成员看做一个整体。
(1)默认构造函数
1.如果不在子类中显示写构造函数,那么子类会自动生成默认构造函数。
这个默认构造函数对其他成员的处理和普通类对这些成员的处理方式一致(比如对自定义类型仍旧是调用该自定义类型的构造函数)。而对于继承父类的成员,这个默认构造函数会调用父类的默认构造函数去初始化。
2 .如果显示的写了构造函数,需要注意,c++规定,对于子类中继承父类的成员必须通过父类构造函数初始化(需要知道,成员变量先在初始化列表中定义,然后才进入构造函数内部,定义时的赋值才叫初始化,而在构造函数内部只能叫重新赋值,由于我们把子类继承父类的成员看作一个整体,即一个自定义类型(只能在定义的时候调用构造函数初始化),所以只能在初始化列表中初始化继承父类的那一部分)
当然我上面说的是父类的构造函数需要参数的情况,事实上,为了满足先构造父再构造子,编译器在进入子类构造函数之前自动调用父类的无参构造函数,当然如果你需要传参或者是只有带参构造,就需要像上面说的一样在初始化列表显示调用构造函数。
上图想把a初始化为x(也就是2),但这是不合法的,不能直接初始化继承父类的成员。
所以我们要这样写:
在student的初始化列表中调用person的构造函数去初始化a
3 .在初始化子类成员的时候,一定要先初始化父类成员(俗称先父后子),因为子类成员有可能需要父类成员去初始化。
但是在构造函数的初始化列表中,我们不用先写父类成员的初始化。因为在初始化列表中不是按照初始化列表的顺序而初始化,而是按照在成员在类中的声明顺序进行初始化,而且在子类中默认父类成员是先声明的,所以无论在初始化列表的顺序如何,初始化的顺序都是先初始化父类。
(2)拷贝构造函数
1.如果不在子类中显示写拷贝构造,子类会生成默认的拷贝构造函数。
2.默认的拷贝构造函数对其他成员的处理和普通类对这些成员的处理方式一致(比如对自定义类型仍旧是调用该自定义类型的拷贝构造函数)。而对于继承父类的成员,这个默认的拷贝构造函数会调用父类的拷贝构造函数。
3. 如果自己写的拷贝构造函数,同上面所说的构造函数一样,规定一定要用父类拷贝构造函数去拷贝继承父类的成员,即在初始化列表中调用父类拷贝构造函数。注意不能像上面构造函数一样不去调用拷贝构造,以为会自动调用拷贝构造。确实会自动调用,但不要忘了,拷贝构造也是构造函数,自动调用不传参的话,调用的是父类的无参构造而不是父类的拷贝构造,因为拷贝构造需要参数。
(3)赋值运算符重载
1.如果不在子类中显示写赋值运算符重载,子类会生成默认的赋值运算符重载。
2.默认的赋值运算符重载对其他成员的处理和普通类对这些成员的处理方式一致(比如对自定义类型仍旧是调用该自定义类型的赋值运算符重载)。而对于继承父类的成员,这个默认的赋值运算符重载会调用父类的赋值运算符重载 。
3. 如果自己写的赋值运算符重载,同上面所说的构造函数一样,规定一定要用父类赋值运算符重载去给继承父类的成员赋值,但不用在初始化列表进行了,因为赋值不是只在定义时可以赋值。
注意赋值子类的赋值重载不会自动调用父类的赋值重载,所以一定要显示调用父类的赋值重载。
如上图,需要注意的是,在调用父类的运算符重载时必须指定类域(即 A),否则因为B的赋值重载和A的赋值重载同名,导致A的赋值重载被隐藏,就会一直调用B的赋值重载,构成死循环。
(4)析构函数
1.如果不在子类中显示写析构函数,子类会生成默认的析构函数。
2.默认的析构函数对其他成员的处理和普通类对这些成员的处理方式一致(比如对自定义类型仍旧是调用该自定义类型的析构函数)。而对于继承父类的成员,这个子类默认的析构函数被调用完成后自动会调用父类的析构函数。
3. 如果自己写的析构函数,同上面所说的构造函数一样,规定一定要用父类析构函数去析构父类成员。
4.析构函数也存在赋值运算符所说的死循环问题。因为由于多态原因(现在对这个还不清楚所以不能解释),析构函数的名字会被统一处理成destructor(),所以B把A的析构函数隐藏,因此调用A的析构函数也要指明类域。
**5.不过为了满足先析构子再析构父,编译器在子类析构函数结束后会自动调用父类析构函数,****即就算我们自己写析构函数,也不需要显示的调用父类析构函数,**如果像我上面说的调用了,就会使父类成员析构两次!。
---------------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------------------------
六.继承与有友元
友元不能被继承,父类的友元不是子类的友元,除非在也子类中声明该友元。
---------------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------------------------
七.复杂的菱形继承及菱形虚拟继承
如图,2,3继承了1 , 4又继承了2,3
这样会导致一个问题,1的内容经过2,3继承到4上,4里面会有两份1的内容。但是这样编译时不会报错,因为这两份1的内容处于不同作用域,不会冲突,但是一但使用时就会不知道去使用2类域的,还是3类域的,就会报错。遇到这种情况指定类域就可以了,但是4继承这两个同名的成员是独立的,也就是说如果这个成员是name,那么2和3类域的name可以不同,那么这个人就有两个name,不符合常理。
c++通过虚拟继承来解决这个问题:
虚拟继承从产生这个问题的根源解决问题,用virtual修饰2,3两个类,把virtual放在冒号和继承方式之间(至于virtual如何解决这个问题,现在我还不清)。
上图中应该修饰谁呢?应该修饰2,3,因为问题从他两开始产生。
八.继承和组合
**1.**组合就是直接在子类中定义一个父类对象,而不是继承,通过这个对象来控制继承父类的成员。
**2.**public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
**3.**优先使用对象组合,而不是类继承 。
**4.**继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称 为白箱复用(white-box reuse)。术语"白箱"是相对可视性而言:在继承方式中,基类的 内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很 大的影响。派生类和基类间的依赖关系很强,耦合度高。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象 来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复 用(black-box reuse),因为对象的内部细节是不可见的。对象只以"黑箱"的形式出现。 组合类之间没有很强的依赖关系,耦合度低。
**5.**优先使用对象组合有助于你保持每个类被 封装。 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有 些关系就适合继承那就用继承,另外要实现多态,也必须要继承。
类之间的关系可以用 继承,可以用组合,就用组合。