一、基本概念
从一个类派生出另一个类的格式如下:
1 2 3 4 5 6 7 8 9 10
| class A { public: int n1; }; class B:public A { public: int n2; };
|
此时类A派生出了类B,类A称为基类,类B称为派生类,派生类对象也是基类对象。
在公有派生的情况下,存在着三条赋值兼容规则(这个和C++三种传递方式(值传递、指针传递和引用传递)相对应):
- 派生类对象可以赋值给基类对象;
- 派生类对象可以用来初始化基类引用;
- 派生类对象的地址(或派生类的指针)可以赋值给基类的指针。
注意,这三条规则仅限于公有派生,如果是私有派生和保护派生则不成立。
这个规则很好理解。基类没有派生类的全部成员,但派生类有基类的全部成员,因此派生类>基类,基类对象当然可以接收派生类的值、址和引用了,但是反过来却不行。
下面这个例子(续上面的程序)展示了这三条规则:
1 2 3 4 5 6 7
| A a; B b; a = b; A &r = b; A *pa = &b; B *pb = &b; pa = pb;
|
如果这三条反过来是不成立的。一般来讲,基类的指针不能赋值给派生类的指针。但是通过强制类型转换,也可以将基类指针强制转换成派生类指针,然后再赋值给派生类指针。只是这样做是有风险的。比如下面这个例子(续上面的程序):
1 2 3 4 5 6
| A *pa = &b; B *pb = &a; B *pb = (B*) &a; pa -> n1 = 1; pa -> n2 = 2; pb -> n1 = 3;
|
另外,还有多层次的派生。下面这个例子展示了多重派生:
1 2 3 4 5 6 7 8 9 10 11 12
| class A { int n1; }; class B:public A { int n2; }; class C:public B { int n3; };
|
在本例中,类A派生类B,类B派生类C;类A是类B的直接基类,类B是类C的直接基类;类A是类C的间接基类。
二、覆盖
派生类和基类可能有同名成员变量或同名成员函数(或二者兼有),这种现象叫做覆盖。看看下面我的程序是如何解决这个问题的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| #include<iostream> using namespace std;
class A { public: int n; void test() { cout<<"class A!"<<endl; } };
class B:public A { public: int n; void test() { cout<<"class B!"<<endl; } void func(); };
void B::func() { n = 1; A::n = 2; cout<<n<<" "<<A::n<<endl; test(); A::test(); }
int main() { B b; b.func(); return 0; }
|
程序输出结果:
对于多重派生来说也是一样的,对于重名的成员变量或成员函数,你不写类名::,那么系统默认是属于本类的。为防止困惑,遇到重名的还是最好都写上类名::吧。
三、保护(protected)
访问范围说明符protected,是用来修饰成员变量或成员函数的。protected一般用来修饰基类的成员变量或成员函数,这样派生类也可以去访问基类的成员变量或成员函数了,但是在类定义外部是访问不了的(三种范围说明符的关系如下图,可能不太严谨)。
因此,保护成员的可被访问范围(自己的和我派生的,都能访问,其他类不能访问)大于私有成员(只能自己类内部访问,派生的也不行),而小于公有成员(谁都能访问)。
举一个下面程序的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| #include<iostream> using namespace std;
class A{ private: int pri; public: int pub; protected: int pro; };
class B:public A { void test() { pri = 1; pub = 2; pro = 3; } };
int main() { B b; b.pri = 1; b.pub = 2; b.pro = 3; return 0; }
|
注意,在基类中,一般都将需要隐藏的成员说明为保护成员而非私有成员。
四、派生类的构造函数&析构函数
派生类对象创建时,也是要调用构造函数进行初始化的,这个过程跟封闭类对象比较类似,但又不尽相同。在派生类内部定义构造函数的格式如下:
1 2 3 4
| 构造函数名(形参表):基类名(基类构造函数实参表) { 函数体; }
|
构造函数和析构函数在基类对象和派生类对象里有不同的生存期,下面我们用一个程序来说明它们是怎么运作的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| #include<iostream> using namespace std;
class A{ private: int n1, n2; public: A(int _n1, int _n2):n1(_n1), n2(_n2) { cout<<"A被建造!"<<endl; } ~A() { cout<<"A被拆毁!"<<endl; } void Print() { cout<<n1<<","<<n2<<endl; } };
class B:public A{ private: int m1, m2; public: B(int _n1, int _n2, int _m1, int _m2):A(_n1, _n2), m1(_m1), m2(_m2) { cout<<"B被建造!"<<endl; } ~B() { cout<<"B被拆毁!"<<endl; } void Print() { cout<<m1<<","<<m2<<endl; } };
int main() { B b(1, 2, 3, 4); b.B::Print(); b.A::Print(); return 0; }
|
输出结果如下:
1 2 3 4 5 6
| A被建造! B被建造! 3,4 1,2 B被拆毁! A被拆毁!
|
由输出结果可以看到,在派生类对象构建时,先执行基类的构造函数,再执行派生类的构造函数;在派生类对象消亡时恰好相反,先执行派生类的析构函数,再执行基类的析构函数。这个步骤与封闭类对象的创建和消亡恰好相反。
对于多层次的派生结构来说,派生类对象生成时,会从最顶层的基类开始逐层往下执行所有基类的构造函数,最后再执行自身的构造函数;当派生类对象消亡时,会先执行自身的析构函数,然后从底向上依次执行各个基类的析构函数。
假如在上面程序main函数里加上A a(5, 6);,那么出现的情况是:整个程序A被建造了两次(这个正常),A被销毁了两次,而不是一次。如果是多层次派生结构,又定义了许多对象,那么中间的构造和析构函数就会被调用很多次了。
五、私有派生和保护派生
上面四节说的都是公有派生的大前提下实现的,因为公有派生是最常用的。实际上还有私有派生和保护派生,这两个反而不常用。下表说明了不同派生方式导致派生类的可访问范围的不同:
| 基类成员 |
公有派生 |
私有派生 |
保护派生 |
| 私有成员 |
不可访问 |
不可访问 |
不可访问 |
| 保护成员 |
保护 |
私有 |
保护 |
| 公有成员 |
公有 |
私有 |
保护 |
| 下面我们用程序来说明私有派生的访问范围(一看就懂): |
|
|
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| #include<iostream> using namespace std;
class A{ private: int pri; public: int pub; protected: int pro; };
class B:private A{ void test() { pri = 1; pub = 2; pro = 3; } };
class C:public B{ void test() { pri = 1; pub = 2; pro = 3; } };
int main() { B b; b.pri = 1; b.pub = 2; b.pro = 3; return 0; }
|
下面我们用程序来说明保护派生的访问范围(一看就懂):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| #include<iostream> using namespace std;
class A{ private: int pri; public: int pub; protected: int pro; };
class B:protected A{ void test() { pri = 1; pub = 2; pro = 3; } };
class C:public B{ void test() { pri = 1; pub = 2; pro = 3; } };
int main() { B b; b.pri = 1; b.pub = 2; b.pro = 3; return 0; }
|
无论是什么派生方式,私有成员都只允许自己访问,其他类是不可访问的。公有派生的特点是,基类该是什么成员,派生类也该是什么成员,原有访问范围不变。保护派生的特点是,无论基类是什么成员(除了私有成员),到了派生类就都是保护成员。私有派生的特点是,无论基类是什么成员(除了私有成员),到了派生类就都是私有成员。
一般情况下,都应该使用公有派生。