C++灵魂所在之---继承篇
在c++的学习过程之中,那么继承与多态这两个区别于c语言的特性你一定要了解,如果想要学好c++,那么继承与多态必须要了解清楚,尤其是多态,但是要了解多态首先你又必须了解继承先,不过即使这两者都十分了解了,也不敢说已经掌握了c++,因为这只不过是c++之中的冰山一角。(有兴趣的可以了解一下网上说的c++的四种境界)
闲话就说到这,开始正式内容了,关于c++之中的继承,我把在继承内容所学到的内容与大家分享分享。如果有什么不对的地方,欢迎大家提出来!
我学习一个内容的时候,总是喜欢从定义入手,然后根据这个定义再去猜想它有什么功能,然后再去验证我的猜想(当然并非每一个问题都这样,一般都是遇到一些比较重要的概念的时候), 来看一下定义:继承(inheritance)是面对象程序使可以用的最重要的,它允许程序员在保持原有类特性的基础上进行扩展,增加加能。这样产生新的类,称为派生类。继承呈现了面对象程序设计的层次结构,体现了由简单到复杂的认知过程。
换种说法就是所谓“继承”就是在一个已存在的类的基础上建立一个新的类。已存在的类称为“基类(base class)”或“父类(father class)”,新建的类称为“派生类(derived class)”或“子类(son class )”。一个新类从已有的类那里获得其已有特性,这种现象称为类的继承。
来看一张图,你就会了解地更清楚了:
说到了继承,那么就不得不提到派生了,因为这两个概念总是出现在一起,或者说谁也离不开谁,通过继承,一个新建子类从已有的父类那里获得父类的特性。从另一角度说,从已有的类(父类)产生一个新的子类,称为类的派生。类的继承是用已有的类来建立专用类的技术。派生类继承了基类的所有数据成员和成员函数,并可以对成员作必要的增加或调整。一个基类可以派生出多个派生类,每一个派生类又可以作为基类再派生出新的派生类,因此基类和派生类是相对而言的。一代一代地派生下去,就形成类的继承层次结构。相当于一个大的家族,有许多分支,所有的子孙后代都继承了祖辈的基本特征,同时又有区别和发展。与之相仿,类的每一次派生,都继承了其基类的基本特征,同时又根据需要调整和扩充原 有的特征。
关于什么是继承这个概念说完之后,那么就开始下一个内容:继承定义格式,同样来看一张图
public、protected、private这三者又称为访问限定符,用来定义继承关系
再来看一下不同的继承方式下的成员变量访问控制关系的不同:
下面用代码来验证一下:
class base { public: base() { cout << "b()" << endl; } ~base() { cout << "b()" << endl; } void showbase() { cout << "_pri = " << _pri << endl; cout << "_pro = " << _pro << endl; cout << "_pub = " << _pub << endl; } private: int _pri; protected: int _pro; public: int _pub; }; class derived :public base { public: derived() { cout << "d()" << endl; } ~derived() { cout << "d()" << endl; } void showderived() { cout << "_d_pri = " << _d_pri << endl; cout << "_d_pro = " << _d_pro << endl; cout << "_d_pub = " << _d_pub << endl; } private: int _d_pri; protected: int _d_pro; public: int _d_pub; };
首先我们来看一下子类与父类在创建对象的时候有什么关系,首先创建一个基类和一个派生类的对象。
int main() { base b; derived d; return 0; }我们会发现运行上面的程序之后,父类调用自己的构造函数没有什么问题,子类也调用了父类的构造函数,这是为什么呢?其实这点要理解也很简单,我们可以将子类的对象中的内容看做成两部分构成,一部分就是它继承父类的内容,还有一部分就是它自己独有的内容,我们都知道在创建派生类对象的时候,它会去调用构造函数,既然它是由两部分构成的,那么自然它需要两个构造函数来共同构造它,用图来解释一下更直观:
在这里有必要给说明一下以下三条小提示:
1、基类没有缺省构造函数,派生类必须要在初始化列表中显式给出基类名和参数列表。
2、基类没有定义构造函数,则派生类也可以不用定义,全部使用缺省构造函数。
3、基类定义了带有形参表构造函数,派生类就一定定义构造函数
同名隐藏
同样地,还是上面的两个类,我们创建两个对象,一个是父类对象,一个是子类对象,我们可以调用分别里面的
showbase()方法以及showderived()方法,这显然没有任何问题,但是如果我们将这两个方法都改成show()方法,那么这样又会怎么样呢?这就涉及到了同名隐藏的概念!
什么是同名隐藏呢?同名隐藏就是:两个成员函数(包括成员变量)处在不同的作用域之中,但是名字相同(返回值、参数列表可以相同也可以不相同),此时如果你想用派生类的对象去调用基类中的同名方法就无法成功了,不过也有解决的方法,就是在方法前面加上作用域。来看一下在代码:
class base { public: base() { _pri = 0x04; _pro = 0x05; _pub = 0x06; cout << "b()" << endl; } ~base() { cout << "~b()" << endl; } void /*showbase()*/show() { cout << "_pri = " << _pri << endl; cout << "_pro = " << _pro << endl; cout << "_pub = " << _pub << endl; } void show(int a) { cout << a << endl; } private: int _pri; protected: int _pro; public: int _pub; }; class derived :public base { public: derived() { _d_pri = 0x01; _d_pro = 0x02; _d_pub = 0x03; cout << "d()" << endl; } ~derived() { cout << "~d()" << endl; } void /*showderived()*/ show() { cout << "_d_pri = " << _d_pri << endl; cout << "_d_pro = " << _d_pro << endl; cout << "_d_pub = " << _d_pub << endl; } private: int _d_pri; protected: int _d_pro; public: int _d_pub; }; int main() { base b; derived d; b.show(); d.show(); d.show(1); //此处编译期间就会报错 d.base::show(); //可以通过这样的方式来访问同名的函数 return 0; }
基类与派生类类型上的兼容性
这里无非就是两个是否可以相互转化(这里主要讨论指针),相互赋值的关系,派生类可以给基类赋值(可以这样理解基类需要的内容派生类都有,因为派生类继承了基类中的内容),但是反过来基类不能直接给派生类赋值,需要进行强转(前提是在公有继承下)。还是用上面的类来做一下测试:
int main() { base * b; derived * d; d = new derived; b = new base; b = d; //派生类可以给基类赋值 b->showbase(); //调用函数也不会出问题 //d = &b; //报错 “=”: 无法从“base **”转换为“derived *” d = (derived*)&b; //这样编译没有问题,但是尽量不要这样做,可能会出现无法预计的错误 d->showderived(); //如果你用上面这样的方式会带来错误,里面打印了随机值 d->showbase(); return 0; }
关于这部分内容总结一下就是:
1. 子类对象可以赋值给父类对象(切割/切片)
2. 父类对象不能赋值给子类对象
3. 父类的指针/引用可以指向子类对象
4. 子类的指针/引用不能指向父类对象(可以通过强制类型转换完成)
写到这里先来一个小小的总结之后再开始下一部分内容:
1. 基类的private成员在派生类中是不能被访问的,如果基类成员不想在类外直接被访问,但需要 在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
2. public继承是一个接口继承,保持is-a原则,每个父类可用的成员对子类也可用,因为每个子类 对象也都是一个父类对象。
3. protetced/private继承是一个实现继承,基类的部分成员并非完全成为子类接口的一部分, 是 has-a 的关系原则,所以非特殊情况下不会使用这两种继承关系,在绝大多数的场景下使用的 都是公有继承。
4. 不管是哪种继承方式,在派生类内部都可以访问基类的公有成员和保护成员,基类的私有成员存 在但是在子类中不可见(不能访问)。
5. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最 好显示的写出继承方式。
6. 在实际运用中一般使用都是public继承,极少场景下才会使用protetced/private继承.
7.友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员,基类定义了static成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有 一个static成员实例。(静态成员可以被继承)
还有一个问题就是关于继承关系之中的构造函数、析构函数调用过程的问题,我用衣一幅图来表示一下:
关于菱形继承的问题
在继承这块内容的学习过程之中,一定会遇到一个“诡异”的问题就是关于菱形继承的问题,既然说它“诡异”,那么他到底是如何“诡异”的呢?主要是因为它不常见(我只见到输入、输出流在库函数的实现之中用到了菱形继承,有兴趣的可以去了解一下),正是因为这样,导致它显得有些“诡异”。
还是先来看一张图:
说到菱形继承,就不得不说到虚继承的概念,对于虚继承,就是为了解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置为虚基类。这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。
class a{}; //基类 class b:public a{};//子类 class c:public a{}; class d:public b,public c();
如上代码中a,b,c,d就构成了一个菱形继承,如果不用虚基类来实现菱形继承就会导致模糊调用的现象,所谓模糊调用就是说在d的内存中会保留两个基类a的对象,如何解决这个问题,利用虚基类就能很好的解决这个问题,即可改为:
class b:virtual public a{};//子类 class c:virtual public a{};
我们可以进一步了解一下,来看一个代码:
class base { public: int _pub; }; class derived_one :/*virtual*/ public base { public: int _por; }; class derived_two :/*virtual*/ public base { public: int _pri; }; class derived : public derived_one, public derived_two { public: int _num; }; int main() { derived d; cout << sizeof(d) << endl; return 0; }上述代码在不是虚继承的情况下结果为20,这很容易理解,但是如果改为虚继承,那么结果又发生了变化,结果变成了24,这又是为什么呢?我们都知道,如果不引人虚拟继承的概念,那么上面的代码就会有数据二义性的问题产生(里面有两个来自base类的数据成员),但是引入虚拟继承之后,这个二义性问题就可以解决了,还是用图来说明吧:
最后总结一下虚继承:
1. 虚继承解决了在菱形继承体系里面子类对象包含多份父类对象的数据冗余&浪费空间的问题。
2. 虚继承体系看起来好复杂,在实际应用我们通常不会定义如此复杂的继承体系。一般不到万不得 已都不要定义菱形结构的虚继承体系结构,因为使用虚继承解决数据冗余问题也带来了性能上的 损耗。