类的派生和继承都是很基础,很老生常谈的知识。但是在实际编程过程中,却会发现很多概念自己并没有完全搞清楚。因此在这里结合自己的编程实践,再将这方面的知识整理下。
1.派生类
1)派生类的定义
class 派生类名:继承方式 基类名1,继承方式 基类名1,…继承方式 基类名n
{
派生类成员声明;
};
一个派生类只有一个直接基类的情况,称为单继承。有多个基类的情况,称为多继承。
继承方式的关键字为:public、protected和private。继承方式规定了如何访问从基类继承的成员。
2)派生的过程
经历了3个步骤:
- 吸收基类的成员
吸收了除构造和析构函数之外所有基类的成员。因为每个成员对象都是要实现初始化的,不论是采用显式还是默认构造函数的方式。但是派生类的成员和基类的成员已经不同了,所以构造函数和析构函数肯定是要重写了。 - 改造基类成员
分为2个方面,一个是访问控制的问题,依靠派生类声明时的继承方式来控制。另一个是在派生类中声明和基类成员同名的新成员。 - 添加新的成员
2.访问控制
访问控制可以这样理解,一个基类自身可以有3种访问方式:public、protected、private
private: 只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问.
protected: 可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问
public: 可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问
继承方式也是3种public、protected、private
类的继承方式为公有继承时,基类的公有和保护成员访问属性在派生类中不变,而基类的私有成员不可直接访问。
当类的继承方式为私有继承时,基类中的公有成员和保护成员都以私有成员身份出现在派生类中,而基类的私有成员在派生类中不可直接访问。
保护继承中,基类的公有和保护成员都以保护成员的身份出现在派生类中,而基类的私有成员不可会直接访问。
继承方式和访问方式有9种组合。
访问的优先级可以理解为public<protected<private
组合的结果是访问的权限只能提高不能降低。
3.类型兼容原则
类型兼容原则是指在需要基类对象的地方,都可以用公有派生类来替代。在替代之后,派生类对象就可以作为基类的对象来使用,但只能使用从基类继承得成员。
类型兼容规则的引入,对于基类及其公有派生类的对象,我们可以使用相同的函数同一进行处理(因为当函数的形参为基类的对象时,实参可以是派生类的对象),而没有必要为每一个类设计单独的模块,大大提高程序的效率。
替代有以下的情况
- 派生类的对象可以赋值给基类的对象
- 派生类的对象可以初始化基类的引用
- 派生类的地址可以赋值给指向基类的指针
实例代码
#include "stdafx.h"
#include<iostream>
#include<stdlib.h>
using namespace std;
class B0
{
public:
void display(){ cout<<"B0::display"<<endl;}
};
class B1:public B0
{
public:
void display(){ cout<<"B1::display"<<endl;}
};
class D1:public B1
{
public:
void display(){ cout<<"D1::display"<<endl;}
};
void fun(B0 *ptr)
{
ptr->display();
}
int _tmain(int argc, _TCHAR* argv[])
{
B0 b0; //声明B0类对象
B1 b1;//声明B1类对象
D1 d1;//声明D1类对象
B0 *p;//声明B0类指针
p=&b0; //B0类指针指向B0类对象
fun(p);
p=&b1;//B0类指针指向B1类对象
fun(p);
p=&d1;//B0类指针指向D1类对象
fun(p);
system("pause");
return 0;
}
结果:
尽管指针指向派生类D1的对象,fun函数运行时通过这个指针只能访问到D1类从基类B0继承过来的成员函数display。
4.构造函数
派生类的数据成员由所有基类的数据成员与派生类新增的数据成员共同组成,如果派生类新增成员中包括其他类的对象,那么派生类的数据成员实际上还间接包括了这些对象的数据成员。因此构造派生类时,就要对基类数据成员、新增数据成员和成员对象的数据成员进行初始化。一句话就是无论采用哪有方式,一切数据成员都必须初始化。
派生类构造函数一般语法:
派生类名::派生类名(参数总表):基类名1(参数表1),…基类名n(参数表n)
内嵌对象名1(内嵌对象参数表1)…内嵌对象名m(内嵌对象参数表m)
{
派生类新增成员的初始化语句
}
这里基类名,对象名之间的次序无关紧要,它们各自出现的顺序可以是任意的。在生成派生类时,系统首先会使用这里列出的参数,调用基类和内嵌对象成员的构造函数。
那么什么时候需要声明派生类的构造函数。如果基类声明了带有形参表的构造函数时,派生类就应当声明构造函数,当然,如果基类没有声明构造函数,派生类也可以不声明构造函数,全部采用默认的构造函数。
派生类构造函数执行的一般次序如下:
- 调用基类的构造函数,调用顺序按照它们被继承时声明的顺序(从左向右)
- 调用内嵌对象的构造函数,调用顺序按照它们在类中的声明顺序
- 派生类的构造函数体中的内容。
注意,这些构造函数的执行顺序和派生类构造函数中列出的名称顺序毫无关系。
#include<iostream>
using namspace std;
class B1
{
public:
B1(int i){cout<<"constructing B1"<<i<endl;}//带参构造函数,需要显式初始化
};
class B2
{
public:
B2(int j){cout<<"constructing B2"<<j<endl;}//带参构造函数,需要显式初始化
};
class B3
{
public:
B3(){cout<<"constructing B3*"<<endl;}//无参函数,可以不显式初始化
};
class C:public B2,public B1,public B3
{
public:
C(int a,int b,int c, int d):B1(a),memberB2(d),memberB1(c),B2(b)
private:
B1 memberB1;
B2 memberB2;
B3 memberB3;
};
void main()
{
C obj(1,2,3,4);
}
此例中,基类B3以及B3类成员对象memberB3就不必列出,因为B3类只有默认构造函数。
如果一个基类同时声明了默认构造函数和带有参数的构造函数,那么在派生类构造函数声明中,既可以显式列出基类名和对应的参数,也可以不列出,程序员可以根据实际情况的需要来自行安排。
拷贝构造函数声明在形式上和带参数的构造函数是一样。
例如假设C是B的派生类,那么C的拷贝构造函数写法
C::C(C &c1):B(c1)
{}
c1能给B赋值是利用了类型兼容原则
析构函数在声明上和构造函数一样,析构的顺序则是和构造函数相反。
5.派生类成员的标识和访问
派生类和基类相比,成员的访问属性有以下四种
- 不可访问成员,从基类私有成员继承而来,派生类或是建立派生类的对象模块都没有办法访问到它们,如果派生类继续派生新类,也是无法访问的。
- 私有成员,这里包括从基类继承过来的成员以及新增加的成员,在派生类内部可以访问,但是建立派生类对象的模块无法访问,继续派生,就变成了新的派生类中不可访问的对象。
- 保护成员,可能是新增也可能是从基类继承过来,派生类内部成员可以访问,建立派生类对象的模块无法访问,进一步派生,在新的派生类中可能成为私有成员或者保护成员
- 公有成员,派生类、建立派生类的模块都可以访问,继续派生,可能是新派生类中的私有、保护或者公有成员。
作用域标识符::
基类名::成员名;//数据成员
基类名::成员名(参数表)//函数成员
隐藏规则:
如果存在两个或多个具有包含关系的作用域,外层声明了一个表示符,而内层没有再次声明同名的标识符,那么外层标识符在内层仍然可见;如果内层声明可同名的标识符,则外层标识符在内层就不可见,这时称内层变量隐藏了外层变量。
在类的派生层次中,派生类在内层。如果派生类中声明了与基类成员函数同名的新函数,即使函数的参数表不同,从基类继承的同名函数的所有重载形式也都会被隐藏。
对于多继承情况,首先考虑各个基类之间没有任何得继承关系,也没有共同基类的情况。如果派生类的多个基类拥有同名的成员,同时,派生类又新增这样的同名成员,在这种情况下,派生类成员将隐藏所有基类的同名成员。这时,使用“对象名.成员名”的方式可以唯一标识和访问派生类新增的成员,基类的同名成员也可以使用基类名和作用域分辨符访问。但是,如果派生类没有声明同名成员,就必须通过基类名和作用域分辨符来标识成员了。
下面是实例:
#include<iostream>
using namespace std;
class B1
{
public:
int nV;
void fun(){cout<<"Member of B1"<<endl;}
};
class B2
{
public:
int nV;
void fun(){cout<<"Member of B2"<<endl;}
};
class D1:public B1,publicB2
{
public:
int nV;
void fun(){cout<<"Member of D1"<<endl;}
};
void main()
{
D1 d1;
d1.nV=1;
d1.fun();
d1.B1::nV=2;
d1.B1::fun();
d1.B1::nV=3;
d1.B1::fun();
}
如果上面的例子D1没有声明与B1 B2类同名的成员
那么
d1.nV=1;
d1.fun();//就会出现二义性
上面讨论多继承时,假定了所有基类之间没有继承关系,如果这个条件得不到满足会出现什么情况呢。如果某个派生类的部分或者全部直接基类是从另一个共同的基类派生而来,在这些直接基类中,从上一级基类继承来的成员就拥有相同的名称,因此派生类中也会产生相同的名称,因此在派生类中也会产生相同的名称,对这种类型的同名成员也要使用作用域分辨符来唯一标识,而且必须用直接基类来进行限定。
例子
#include<iostream>
using namespace std;
class B0
{
public:
int nV;
void fun(){cout<<"Menber of B0"<<endl;}
};
class B1:public B0
{
public:
int nV;
};
class B2:public B0
{
public:
int nV;
};
class D1:public B1,public B2
{
public:
int nVd;
void fund(){cout<<"Menber of D1"<<endl;}
};
void main()
{
D1 d1; //声明D1类对象d1
d1.B1::nV=2//使用直接基类
d1.B1::fun();
d1.B2::nV=3; //使用直接基类
d1.B2::fun();
}
6.虚基类
在上面的情况下,派生类对象在内存中就同时拥有成员nV及fun的两份拷贝,对于数据成员来讲,虽然两个nV可以分别通过B1和B2调用B0的构造函数进行初始化,可以存放不同的数值,也可以使用作用域分辨符通过直接基类名限定来分别进行访问。但是在很多情况下,我们只需要一个这样的数据拷贝,同一成员的对分拷贝增加了内存的开销。C++中提供了虚基类技术来解决这个问题。我们可以将共同的基类设置为虚基类,这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。这样就解决了同名成员惟一标识的问题。
其语法为
class 派生类名::virtual 继承方式 基类名
例子
#include <iostream>
using namespace std;
class B0
{
public:
int nV;
void fun{cout<<"Member of B0"<<endl;}
};
class B1:virtual public B0
{
public:
int nV1;
};
class B2:virtual public B0
{
public:
int nV2;
};
class D1:public B1,public B2
{
public:
int nVd;
void fund(){cout<<"Member of D1"<<endl;}
};
void main()
{
D1 d1; //声明D1类对象d1
d1.nV=2; //使用直接基类
d1.fun();
}
虚基类构造函数初始化问题:如果虚基类声明有非默认形式的(即带形参的)构造函数,并且没有声明默认形式的构造函数,事情就比较麻烦了。这时,在整个继承关系中,直接或者间接继承虚基类的所有派生类,都必须在构造函数的成员初始化列表中列出对虚基类的初始化。
比如
D1:D1(int a):B0(a),B1(a),B2(a)//说明构造函数的总参数的数量和基类的数量也没什么关系
{}