C++面向对象的继承特性:继承特性和其中的权限管控
1.什么是继承
继承是C++
源生支持的一种语法特性,是C++
面向对象的一种支持表现。派生类会“继承”基类的成员变量和成员方法。继承特性可以让派生类“瞬间”拥有基类的权限允许范围内的所有属性和方法。
继承特性本质上是为了代码复用,类在C++
编译器的内部可以理解为结构体,派生类是由基类成员叠加派生类新成员得到的。
继承特性是天然的,天然的意思就是:符合现实世界的本质规律,而不是纯粹人为施加的。继承inheritance
和组合composition
是软件重用的2种有效方式。派生类也叫子类,范围小且具体; 基类也叫父类,范围大且抽象。
C++
类的继承语法如下:
class 派生类名:访问控制 基类名1,访问控制 基类名2,...,访问控制 基类名n
{
// 成员变量和成员方法列表(除了构造和析构之外的成员方法)
};
2.类的继承案例
基类为
person
,派生类为man
,所有权限均为public
。
初步实现:
#include<iostream>
using namespace std;
class person
{
public:
string name;
int age;
person(){
};
person(string Name,int Age):name(Name),age(Age){
};
int get_age(void) const
{
return this->age;
}
void talk(void)
{
cout<<"person talk."<<endl;
}
};
class man:public person
{
};
int main(int argc,char**argv)
{
man tom;
tom.name="tom";
tom.age=15;
int age=tom.get_age();
cout<<age<<endl;
tom.talk();
return 0;
}
输出:
15
person talk.
可以看出尽管man
内啥都没写,但是它具备了person
所有的功能,person
里的成员和方法man都有。但是man
里不止可以只拥有person
有的,
man
也可以有自己的方法:
class man:public person
{
//除了自动继承person的方法,man也可以有自己的方法
public:
void eat(void)
{
cout<<"man eat."<<endl;
}
};
int main(int argc,char**argv)
{
man tom;
tom.talk();
tom.eat();
return 0;
}
输出:
person talk.
man eat.
3.类继承中的权限管控
3.1 C++类中的访问权限管控回顾
public
:类内部可以访问,类的外部可以访问。private
:类内部可以访问,类的外部不能访问。protected
:protected
在类内部,不涉及类的继承时效果跟private
是一样的。类内部可以访问,类的外部不能访问。
3.2 类继承时的三种权限设置对派生类的影响
①
public
继承(公有继承):父类成员在子类中均保持原有访问级别。public
继承,对于父类中public
和protected
成员的权限在子类中其实是没有增强也没有削弱的;但是对于父类中private
成员,其实在子类中是有削弱的。
-
父类的
public
成员,经过public
继承后在子类中是public
的。 -
父类的
private
成员,经过public
继承后在子类中权限是比private
还可怜的。该成员在子类中是存在的,但是它在子类内部不能被直接访问,子类只能通过从父类继承而来的、父类里实现的对该成员进行访问函数来间接访问。 -
父类的
protected
成员,经过public
继承后,在子类中是protected
的。该成员在子类中是子类内部成员可以访问,子类对象外部不可以访问,子类再次去继承产生孙类中他还是protected
的。
②
private
继承(私有继承)
- 父类中的
public
成员和protected
成员经过private
继承就变成了子类中的private
成员。 - 父类中的
private
成员成了子类中比private
还可怜的那种成员,它在子类内部不能被直接访问,子类只能通过从父类继承而来的父类里实现的对该成员进行访问函数来间接访问。
③
protected
继承(保护继承)
- 父类中的
public
成员和protected
成员经过protected
继承就变成了子类中的protected
成员。 - 父类中的
private
成员经过protected
继承成了子类中比private
还可怜的那种成员,它在子类内部不能被直接访问,子类只能通过从父类继承而来的父类里实现的对该成员进行访问函数来间接访问。
注意:如果继承时不写则默认情况下派生类为class
时是private
继承,而派生类为struct
时是public
继承。
有一个方便记的方法,在父类中的权限和继承方法中,哪个最惨,在子类中就是那个权限。
思考:设计类时如何规定成员的访问权限?
- 需要被外界访问的成员直接设置为
public
。- 只能在当前类(父类)中访问的成员设置为
private
。- 只能在当前类(父类)和子类中访问的成员设置为
protected
。
4.继承体系下子类和父类的关系
4.1 子类和父类本质上是2个独立的类
继承只是通过父类来快速构建子类的一种语法技术,继承后得到的子类和父类是独立的2个类;程序中子类的对象和父类的对象也是2个独立的对象,没有任何关系,只是形成对象的模板(类的本身)有部分相同。
这里的继承不像是父亲把财产传给了儿子,父亲传完了父亲就没有了,而像是父亲把自己的手艺传给了儿子,父亲和儿子同时都掌握了该门手艺并且儿子也可以另外学习其他手艺。子类对象中访问的子类从父类继承而来的成员,其实是属于子类自己的,并不是父类(对象)里面的。
4.2 站在子类角度看继承
非继承体系下的类完全是自己“手工”构建的(比如person
类),所有成员根据访问权限不同而分为1个层级(都是自己手工构建的)3大块(3种权限);继承体系下的子类,部分直接从父类继承,部分自己手工打造,所有成员分成2个层级(继承来的,自己写的),每个层级3大块(3种权限)。
继承就是子类在构建自己的过程中使用的一种快速批量成员获取方法而已,这种复制是模板的复制,而不是内存的复制,类是一种数据类型,只会在实例化成对象的时候才占用内存。
4.3 为什么父类private成员在子类中还有
思考:父类的所有成员在子类中都必须有吗?有没有可能父类有一个成员其实子类没必要具备的? 这个取决于实际要解决的问题,例如生活中绝大多数情况下父亲有的儿子都会有,但是也存在特例,也有一些是儿子没办法继承的,比如父亲的智商。
如何做到在继承时让程序员指定父类的哪些成员参与继承,哪些不参与?C++
的设计或者说选择,就是让父类中所有成员无差别全部传给子类,但是传过去之后的访问规则则取决于继承过程中的权限管控。
4.4 为什么父类private成员在子类中还有但不能直接访问
这个规定是其实是人为规定的,因为访问权限这一套本身就是人为的。
如果能直接访问,那就打破了父类private
的含义,破坏了class
的封装性,因为对于父类来说,子类其实就是"外部"了。父类的private
成员在子类中很大可能本来就不需要去访问,所以能直接访问反而有风险,没必要。间接访问合乎一个原则:在哪里定义的成员变量在哪里操作。子类和父类不是一个人写的时尤其要这样,避免问题。这也是一个如何选择的设计哲学问题。
5.继承的优势与不良继承
5.1 为什么会有继承
继承本质上为了代码复用,实际上不考虑代码复用的话完全可以没有继承,每个类单独设计和实现。继承的这种设计思路非常适合用来构建复杂框架体系。用继承来设计类进而构建各层级对象,符合现实中的需要(比如现实生活中描述人的种群:人分为男人和女人,男人又分为老年中年和青年等)。
5.2 何为不良继承
不良继承是继承中的一个BUG,大多数情况下用继承表示实际生活中的层次关系是非常好的,但有的时候会出现一些特例,例如鸵鸟不是鸟问题(鸵鸟从鸟类继承了飞的方法但是实际上鸵鸟不会飞)和圆不是椭圆问题(圆从椭圆继承了长短轴属性然而圆没有长短轴属性)就是不良继承问题。不良继承是天然的,是现实世界和编程的继承特性之间的不完美契合。
5.3 如何解决不良继承
方法是修改继承关系设计。既然圆继承椭圆是一种不良类设计就应该杜绝。去掉继承关系,两个类可以继承自同一个共同的父类,该父类不能执行不对称的setSize()
计算,然后在圆和椭圆这2个子类中分别再设计以区分。
所有不良继承都可以归结为“圆不是椭圆”这一著名具有代表性的问题上。在不良继承中,基类总会有一些额外能力,而派生类却无法满足它。这些额外的能力通常表现为一个或多个成员函数提供的功能。要解决这一问题,要么使基类弱化,要么消除继承关系,需要根据具体情形来选择。
6. 组合介绍以及与继承对比
6.1 什么是组合
组合(composition
)就是在一个class
内使用其他多个class
的对象作为成员。组合也是一种代码复用的方法,本质也是结构体包含。
6.2 继承与组合的特点对比
继承是a kind of(is a)
关系,具有传递性而不具有对称性。组合是a part of(has a)
的关系。
继承是白盒复用,因为类继承允许我们根据自己的实现来覆盖重写父类的实现细节,父类的实现对于子类是可见的。继承的白盒复用特点,一定程度上破坏了类的封装特性,因为这会将父类的实现细节暴露给子类。
组合属于黑盒复用,被包含对象的内部细节对外是不可见的,所以它的封装性相对较好,实现上相互依赖比较小。组合中的被包含类对象会随着包含类对象的创建而创建,消亡而消亡。组合属于黑盒复用,并且可以通过获取其它具有相同类型的对象引用或指针,在运行期间动态的定义组合。这样的缺点就是致使系统中的对象过多。
面向对象的设计原则是优先组合,而后继承。