《王道》C++面向对象编程之继承
1 继承定义
继承就是一个类继承了另一个类的属性和方法,这个新的类包含了上一个类的属性和方法,被称为子类或者派生类,被继承的类称为父类或者基类。
在C++语言中,一个派生类可以从一个基类中派生,也可以从多个基类中派生。前者称为单继承,后者称为多继承。
继承的定义格式如下:
继承的特点:
1)子类拥有父类的所有属性和方法(除了构造函数、析构函数和赋值运算符重载函数);
2)子类可以拥有父类没有的属性和方法;
3)子类是一种特殊的父类,可以用子类来代替父类;
4)子类对象可以当做父类对象使用。
2 继承中的访问控制
我们在一个类中可以对成员变量和成员函数进行访问控制,通过C++提供的三种权限修饰符实现。在子类继承父类时,C++提供的三种权限修饰符也可以在继承的时候使用,分别为公有继承(public),保护继承(protected)和私有继承(private)。这三种不同的继承方式会改变子类对父类属性和方法的访问。
表1 不同派生方式派生类对基类成员的访问属性
注意:父类中的private成员依然存在于子类中,但是却无法访问到。不论何种方式继承父类,子类都无法直接使用父类中的private成员。
派生类对基类成员的访问形式主要有以下两种:
1)内部访问:由派生类中新增的成员函数对基类继承来的成员的访问;
2)外部访问:在派生类外部,通过派生类的对象对从基类继承来的成员的访问。
*公有继承
父类的public成员成为子类的public成员,可以被该子类中的函数(内部访问)及其友元函数访问,除此之外,也可由该子类的对象(外部访问)访问。
父类的private成员仍旧是父类的private成员,子类中的函数(内部访问)及其友元函数和对象(外部访问)不可以访问这些成员。
父类的protected成员成为子类的protected成员,可以被该子类中的函数(内部访问)及其友元函数访问,除此之外,不可由该子类的对象(外部访问)访问。
注意:一定要区分派生类的对象和派生类的成员函数对基类的访问是不同的。
表2 公有继承的访问规则
*私有继承
私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被派生类的对象所访问。
表3 私有继承的访问规则
*保护继承
表4 保护继承的访问规则
*父类如何设置子类的访问?
1)需要被外界访问的成员设置为public。
2)只能在当前类中访问设置为private。
3)只能在当前类和子类中访问,设置为protected
3 继承中的类型兼容性原则
3.1 继承中的同名成员
当在继承中,如果父类的成员和子类的成员属性名称相同,我们可以通过作用域操作符来显式的使用父类的成员,如果我们不使用作用域操作符,默认使用的是子类的成员属性。
示例:
# include<iostream> using namespace std; class PP { public: int i; }; class CC:public PP { public: int i; public: void test() { /* 使用父类的同名成员 */ PP::i = 10; /* 使用子类的同名成员 */ i = 100; } void print() { cout << "父类:" << PP::i << "," << "子类:" << i << endl; } }; int main() { CC cc; cc.test(); cc.print(); return 0; }
3.2 类型兼容性原则
类型兼容性原则是指在需要父类对象的所有地方,都可以用公有继承的子类对象来替代。通过公有继承,子类获得了父类除构造和析构之外的所有属性和方法,这样子类就具有了父类的所有功能,凡是父类可以解决的问题,子类也一定可以解决。
类型兼容性原则可以替代的情况:
1)子类对象可以当做父类对象来使用;
2)子类对象可以直接赋值给父类对象;
3)子类对象可以直接初始化父类对象;
4)父类指针可以直接指向子类对象;
5)父类引用可以直接引用子类对象。
示例:
# include<iostream> using namespace std; /* 创建父类 */ class MyParent { protected: char * name; public: MyParent() { name = "HelloWorld"; } void print() { cout << "name = " << name << endl; } }; /* 创建子类 */ class MyChild:public MyParent { protected: int i; public: MyChild() { i = 100; name = "I am Child"; } }; void main() { /* 定义子类对象 */ MyChild c; /* 用子类对象当做父类对象使用 */ c.print(); /* 用子类对象初始化父类对象 */ MyParent p1 = c; p1.print(); /* 父类指针直接指向子类对象 */ MyParent * p2 = &c; p2->print(); /* 父类对象直接引用子类对象 */ MyParent& p3 = c; p3.print(); }
4 继承时的二义性及其解决方法
4.1 多基继承
*产生原因
在派生类中对基类成员的访问应当具有唯一性.但在多基继承时,如果多个基类中存在同名成员的情况,造成编译器无从判断具体要访问哪个基类中的成员,则称为对基类成员访问的二义性。
如下图所示,在子类中就存在父类A、B的两份show(),在调用的时候就会出现二义性问题。
示例:
#include<iostream> using namespace std; class Base1 { public: void fun(){cout<<"base1 "<<endl;}; }; class Base2 { public: void fun(){cout<<"base2 "<<endl;}; }; class Derived:public Base1,public Base2{}; int main() { Derived obj; obj.fun(); //产生歧义 return 0; }
编译时,会提示:
当派生类Derived访问fun()函数时,无法确定访问的是Base1的还是Base2的,将出现二义性错误。
*解决方法
法1:
若两个基类中具有同名的数据成员或成员函数,应使用成员名限定来消除二义性,如:
obj.Base1::fun(); //指明访问base1的fun函数
法2:
更好的方法是在类Derived中也定义一个同名函数fun(),根据需要调用Base1或Base2的函数fun(),从而实现对基类同名函数的隐藏,如:
class Derived:public Base1,public Base2{ public: void fun(){ Base1::fun(); } };
4.2 菱形继承
*产生原因
当一个派生类从多个基类派生时,而这些基类又有一个共同的基类,当对这个共同的基类中说明的成员进行访问时,可能出现二义性问题。
例如:
Base为Derived11和Derived12的基类,而Derived2又继承Derived11和Derived12,当Derived2访问Base的data时,会出现二义性错误。
示例:
class Base { public: int data; }; class Derived11:public Base{}; class Derived12:public Base{}; class Derived2:public Derived11,public Derived12 {}; int main() { Derived2 obj; obj.data=1; //产生二义性 return 0; }
*解决方法
法1:作用域运算符
Obj.Derived11::a,指明访问哪一个基类的data.但是由于派生类的直接基类有一个共同的基类,所以 obj.Base::a是错误的。
法2:使用虚基类
产生二义性的最主要的原因就是base在派生类Derived2中产生了2个对象,从而导致了对基类Base的成员data访问的不一致性。要解决这个问题,只需使用关键字virtual将这个公共基类Base声明为虚基类。