前言
面向对象的三大特性:封装继承和多态,这三种特性优者很紧密地联系,但是也有很大的区别。
对于刚接触面度对象编程风格的小白,你可能不了解继承到底是用来做什么的,以及为什么叫做继承,接下来我就带你仔细了解继承的作用以及注意事项。
继承的概念及定义
继承是代码可以复用的重要手段,术语表达继承就是子类继承父类的属性和方法,大白话就是子承父业,子类可以拥有父类除了私有以外的成员和方法,还可以有自己的属性,父类通常也叫做基类,子类有时也叫做派生类。
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程,在之前我们在程序中的复用通常是函数复用,传入不同的参数,函数就可以产生不同的响应,但是在学完继承和多态后,代码的复用就不仅仅局限于函数的复用,继承就是类设计层次的复用。
举一个例子理解一下
class animal { public: void Paint() { cout << age << endl; } protected: int age=10; }; class cat :public animal { protected: int price = 500; };
动物是一个较大的概念,在动物的种类中有猫,小狗等等,他们具有都是动物的特点,也与属于他们自己的特点,继承animal后,cat就是animal的子类,animal就是cat的父类。
如上图所示,如果用子类构造出一个对象,子类也包含了父类的成员和方法。
此时,用父类函数构造出来的子类就也可以使用父类的方法。
继承的意义
现在我们要思考了,为什么我们需要继承呢?向上变得例子来看,继承并没有带来太大的优化,这样你就大错特错了,如果仅仅只有两三个类,在每个类的属性和方法都确定的请款下确实某有必要实现继承,但是在现实中通常是一整个系统程序,往往有很多类且大部分类都是具有相似部分的,如果我们每个类都丹毒重新写,不仅代码臃肿而且工作量大,出bug不易查寻。
有了继承之后,我们可以将部分类中相似的部分提取出来作为父类,在继承父类后添加自己新的属性和方法,这样不仅可以大大减少代码量,还易于维护,代码结构清晰可见。
继承关系及访问限定符
C++中有三种访问限定符,分别是public,private,protected,继承方式也就有了三种,最常用的就是public继承。
如上图所示
私密性public<protected<private。
刚开始时是没有保护这个限定符的,正式出现继承和多态之后才加上的,父类的私有子类不能访问,但父类的保护子类也可以访问。
父类和子类对象之间的转化
子类的对象可以赋值给父类的对象/父类的指针/父类的引用,这里有一种很形象的说法就是切片或者切割,子类的对象继承了父类,所以包含父类的一部分,在赋值给父类对象时,将子类父类的一部分切割给父类对象。
切记!只有子类对象可以赋值给父类。
cat c; animal a = c;//子类对象赋值给父类 animal* a = &c;//赋值子类对象的指针 animal& k = c;//赋值子类对象的引用
赋值切片的过程类似于
继承后的作用域
继承后子类和父类仍然是两个独立的作用域。所以要注意这个问题,如果子类和父类包含有同名函数,这两个函数之间的关系不是重载(重载要求两个函数在同一作用域),而是隐藏,子类会隐藏父类的同名函数。
要注意哈!在父子类中,只要两函数的名称相同就构成隐藏。但是在实际中最好不要在父子类中定义同名函数,不然会容易混淆。
如果构成隐藏后,我们调用该函数就调用的是子类的虚函数,如果我们想要调用父类的虚函数,就要加上作用域(在子类成员函数中,可以使用基类::基类成员,显式访问)。
class person { public: void func1() { cout << " person::func1" << endl; } }; class student : public person { public: void func1() { cout << "func1" << endl; } }; int main() { person p; student s; p.func1(); s.func1(); return 0; }
运行后发现,父类调用该函数使用的父类中的,子类调用就调用子类中的函数,子类将父类的同名函数进行隐藏,这样搞调用时就不会产生歧义。
接下来探索探索继承后,子类的成员函数和弗雷德成员函数之间的关系。
- 1,首先,子类的构造函数必须先调用父类的构造函数初始化子类中所包含父类的一部分,如果父类中没有默认的构造函数,就必须在子类构造函数的初始化列表进行初始化。
- 2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
- 3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
- 4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
- 5. 派生类对象初始化先调用基类构造再调派生类构造。
- 6. 派生类对象析构清理先调用派生类析构再调基类的析构。
通过调试来带领大家观察构造及西狗屎的顺序。
可以发现,在构造子类对象时,再初始化列表首先调用父类的构造函数,在析构时,首先调用子类的析构函数,然后再调用父类的析构函数。
继承与有元
我们知道一个类的有元可以在类外使用类中的变量和方法,那么父类的友元函数能被继承吗?
答案是不能,友元关系不能被继承,基类的有缘不能访问子类的私有和保护。
举一个例子
class A { public: private: int _a=2; }; class B:public A { public: int _b = 0; friend void Printf(); }; void Printf(B b,A a) { cout << b._b; cout << a.a; } int main() { A a; B b; Printf(b,a); return 0; }
在子类中创建一个友元函数,友元函数只可以访问,而不是父类的友元函数,所以父类中的保护和私有我们这个友元函数不能访问。
继承与静态成员
基类定义了static静态成员,整个继承体系里面只有一个这样的成员,无论有多少个子类,静态成员就只有这一个。
class A { public: int _a = 2; static int aa; }; int A::aa=1; class B :public A { public: int _b = 0; }; int main() { A a; B b; A::aa++; cout<<a.aa<<endl; a.aa++; cout << b.aa; return 0; }
由于两个类中的静态变量只有一份,所以我们可以直接用类加作用域直接访问,也可以用对象进行访问。
如图
多继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承。
例如:
多继承:一个子类同时继承多个父类,这种关系称为多继承
有了多继承就会产生一种特殊的情况,那就是菱形继承
这种情况就会产生特殊的影响
假如A中有一个元素,那么BC分别继承了A,然后D再次继承BC,D中就包含了两份这个元素。如果我们去调用D中A的元素(假设为_a)就会出现二义性。
不仅如此,我们D类中包含了两份_a,还会浪费内存。我们可以使用作用域去看。
如何解决数据重复问题呢?
我们可以使用关键字virtual。
使用虚拟继承就可以解决问题
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。
关键字virtual在多态时也会用到,在这里是修饰继承了最上边的父类的子类。在上边的例子中就是修饰B和C。
正如上图,也是菱形继承的一种,我们要在距离被继承两次的父类的下一层子类加virtual,使其变成虚拟继承。
但是还是那句话,我们在实际写程序中尽量不要写菱形继承。
如下图
此时再来观察在内存中是如何解决这件问题的。
借用上边的例子
我们来调试运行看一看。
可以发现,不管我们调用B还是C对象中的_a元素,都是调用的同一个,来看一看底层如何实现的,以及为什么将A类中的_a放在最后的位置。
我们在X86的环境下来观察
可以发现,在B和C对象前都有一个指针,这个指针里存的就是该位置到_a的距离,当我们想要修改A中_a的值时,就会找到该位置距离A对象中_a的长度,然后修改A中_a的值。
将A中数据放在末尾就是为了统一管理,方便每一个继承该父类的子类通过指针找到该位置。
继承和组合的区别:
public是一种is-a的关系,也就是说每个派生类对象都是一个基类对象。
组合是一种has-a的关系,假设B组合了A,每个B对象中都会有一个A对象。
优先使用对象组合,而不是类继承。
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称
为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的
内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很
大的影响。派生类和基类间的依赖关系很强,耦合度高。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象
来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复
用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。
组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被
封装。
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有
些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用
继承,可以用组合,就用组合。
继承的总结和反思
继承大大增加了我们代码的复用性,有人说C++语法复杂,其实就是多继承的问题,比建议设计出多继承,java就吸取了C++的教训,没有引入多继承,如果使用多继承不小心设计出了菱形继承,那会在复杂度和时间上都产生问题。
本文结束,如果有问题或者有疑问就直接评论哦,回复很快。