一.保护继承
在保护继承方式中,基类的公有成员和保护成员被派生类继承后变成派生类的保护成员,而基类的私有成员在派生类中不能访问。因为基类的公有成员和保护成员在派生类中都成了保护成员,所以派生类的新增成员可以直接访问基类的公有成员和保护成员,而派生类的对象不能访问它们,因为类的对象是处于类外的,不能访问类的保护成员。对基类的私有成员,派生类的新增成员函数和派生类对象都不能访问。
假设A类是基类,B类是从A类继承的派生类,A类中有保护成员,则对派生类B来说,A类中的保护成员和公有成员的访问权限是一样的。而对A类的对象的使用者来说,A类中的保护成员和私有成员都一样不能访问。可见类中的保护成员可以被派生类访问,但是不能被类的外部对象(包括该类的对象、一般函数、其他类等)访问。我们可以利用保护成员的这个特性,在软件开发中充分考虑数据隐藏和共享的结合,很好的实现代码的复用性和扩展性。
举个简单的例子讨论下保护成员的访问属性。
- class Base
- {
- protected:
- int x; // 基类的保护成员
- };
- int main()
- {
- Base base;
- base.x = 0; // 编译报错
- return 0;
- }
这段代码在编译的时候会报错,错误就出在通过对象base访问保护成员x时,就像上面讲的,对Base类的对象base的使用者来说,Base类中的保护成员x和私有成员的访问特性是一样的,所以对象base不能访问x,这样跟使用私有成员一样通过保护成员实现了数据的隐藏。
- class Base
- {
- protected:
- int x; // 基类的保护成员
- };
- class Child : public Base
- {
- public:
- void InitX();
- };
- void Child::InitX()
- {
- x = 0;
- }
对上面的派生类Child来说,基类Base中的保护成员x和公有成员的访问权限一样,所以Child类的成员函数InitX可以访问Base类的保护成员x。
二.私有继承
在私有继承方式中,基类的公有成员和保护成员被派生类继承后变成派生类的私有成员,而基类的私有成员在派生类中不能访问。派生类的新增成员可以直接访问基类的公有成员和保护成员,但是在类的外部通过派生类的对象不能访问它们。而派生类的成员和派生类的对象都不能访问基类的私有成员。
我们看到不管是保护继承还是私有继承,在派生类中成员的访问特性都是一样的,都是基类的公有和保护成员可以访问,私有成员不能访问。但是派生类作为基类继续派生新类时,两种继承方式就有差别了。例如,A类派生出B类,B类又派生出C类,如果B类是以保护继承方式从A类继承的,则A类的公有成员和保护成员都成为B类的保护成员,再由B类派生出C类时,原来A类的公有成员和保护成员间接继承到C类中,成为C类的保护成员或者私有成员(C类从B类公有继承或保护继承时为前者,私有继承时为后者),所以C类的成员可以间接访问A类的公有成员和保护成员。但是如果B类是以私有继承方式从A类继承的,则A类的公有成员和保护成员都成为B类的私有成员,A类的私有成员不能在B类中访问,B类再派生出C类时,原来A类的所有成员都不能在C类中访问。
由以上分析得出,私有继承使得基类的成员在其派生类后续的派生中不能再被访问,中止了基类成员继续向下派生,这对代码的复用性没有好处,所以一般很少使用私有继承方式。
将之前的例子由公有继承改为私有继承,继而更形象的说明私有继承的特性。
- #include <iostream>
- using namespace std;
- class Base // 基类Base的声明
- {
- public: // 公有成员函数
- void SetTwo(int a, int b) { x=a; y=b; }
- int GetX() { return x; }
- int GetY() { return y; }
- private: // 私有数据成员
- int x;
- int y;
- };
- class Child : private Base // 派生类的声明,继承方式为私有继承
- {
- public: // 新增公有成员函数
- void SetThree(int a, int b, int c) { SetTwo(a, b); z=c; }
- int GetX() { return Base::GetX(); }
- int GetY() { return Base::GetY(); }
- int GetZ() { return z; }
- private: // 新增私有数据成员
- int z;
- };
- int main()
- {
- Child child; // 声明Child类的对象
- child.SetThree(1, 2, 3); // 设置派生类的数据
- cout << "The data of child:"<<endl;
- cout << child.GetX() << "," << child.GetY() << "," << child.GetZ() << endl;
- return 0;
- }
程序运行结果:
The data of child:
1,2,3
Child类从Base类中私有继承,Base类中的公有成员SetTwo()、GetX()和GetY()成为Child类的私有成员,在Child类中可以直接访问它们,例如Child类的成员函数SetThree()中直接调用了Base类的公有成员函数SetTwo()。Base类的私有成员x和y在Child类中不能访问。在外部通过Child类的对象不能访问Base类的任何成员,因为Base类的公有成员成为Child类的私有成员,Base类的私有成员在Child类中不能访问。那么Base类的作为外部接口的公有成员SetTwo()、GetX()和GetY()都被派生类Child隐藏起来,外部不能通过Child类的对象直接调用。
如果我们希望派生类也提供跟基类中一样的外部接口怎么办呢?我们可以在派生类中重新定义重名的成员。上面的Child类就重新定义了公有成员函数GetX()和GetY(),函数体则只有一个调用基类函数的语句,照搬了基类函数的功能。因为派生类中重新定义的成员函数的作用域位于基类中同名函数的作用域范围的内部,根据前面可见性中讲的同名覆盖原则,调用时会调用派生类的函数。通过这种方式可以对继承的函数进行修改和扩展,在软件开发中经常会用到这种方法。
main函数的函数体跟前面例子中的完全相同,但实际上在程序执行的时候是不同的,这里调用的函数GetX()和GetY()都是派生类Child的函数,由于是私有继承,基类Base中的同名函数都不能通过Child类的对象访问。
这个例子跟前面的例子相比,Base类和主函数main的函数体都没有修改,只修改了派生类Child的声明,但Child类的外部接口没有改变。到此可以看到面向对象设计封装性的优越性。我们可以根据需要调整类的内部数据结构,但只要保持外部接口不变,那我们做的类的内部调整对外部就是透明的,不会影响到程序的其他部分。这充分体现了面向对象设计的可维护性和可扩展性。