目录标题
1 基类与派生类
类的继承,是新的类从已有类那里得到已有的特性。从已有类产生新类的过程就是类的派生。原有的类称为基类或父类,产生的新类称为派生类或子类。
1-1 派生类的定义
在C++中,派生类的一般定义语法为:
class 派生类名:继承方式 基类名1,继承方式 基类名2,...继承方式 基类名n
{
派生类成员声明;
}
一个派生类,可以同时有多个基类,这种情况称为多继承。一个派生类只有一个直接基类的情况,称为单继承。在类族中,直接参与派生出某类的基类称为直接基类,基类的基类甚至更高层的基类称为间接基类。
继承方式规定了如何访问从基类继承的成员。
派生类成员是指除了从基类继承来的所有成员外新增加的数据和函数成员。
1-2 派生类生成过程
- 吸收基类成员
- 改造基类成员
如果派生类声明了一个和某基类成员同名的新成员,派生的新成员就隐藏了外层同名成员。 - 添加新的成员
2 访问控制
类的继承方式有public(公有继承)、protected(保护继承)、private(私有继承)。
2-1 公有继承
当类的继承方式为公有继承时,基类的公有和保护成员的访问属性在派生类中不变,而基类的私有成员不可直接访问。
2-2 私有继承
当类的继承方式为私有继承时,基类的公有和保护成员都以私有成员身份出现在派生类中,而基类的私有成员在派生类中不可直接访问。
2-3 保护继承
保护继承中,基类的公有和保护成员都以保护成员的身份出现在派生类中,而基类的私有成员不可直接访问。
总结:
基类成员属性 | public | protected | private |
---|---|---|---|
公有继承 | public | protected | 不可访问 |
保护继承 | protected | protected | 不可访问 |
私有继承 | private | private | 不可访问 |
即:
无论是派生类的成员还是派生类的对象(即类内和类外)都无法直接访问基类的私有成员。
如果想通过类外访问基类的成员,只有通过公有继承访问基类的公有成员一种方法。
保护继承和私有继承在类外看可以发现都是无法访问,在类内看可以发现公有成员和保护成员都可以访问,私有成员不可直接访问。那么保护和私有继承有什么区别?
直接派生时它们确实是完全相同,但再继续派生时,情况就不一样了,如下
两次保护继承 | |||
---|---|---|---|
基类的基类 | public | protected | private |
基类 | protected | protected | 不可访问 |
派生类 | protected | protected | 不可访问 |
两次私有继承 | |||
---|---|---|---|
基类的基类 | public | protected | private |
基类 | private | private | 不可访问 |
派生类 | 不可访问 | 不可访问 | 不可访问 |
可以看出如果是私有继承,那么派生类再次派生的类就无法访问基类的任何成员,如果是保护继承,派生类再次派生的类就还有可能访问基类的成员。
3 类型兼容规则
类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代。这是多态性的重要基础。
类型兼容规则中所指的替代包括以下情况:
- 派生类的对象可以隐含转换为基类对象
- 派生类的对象可以初始化基类的引用
- 派生类的指针可以隐含转换为基类的指针
在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员。
举例:
class B {...}
class D:public B {...}
B b1,*pb1;
D d1;
//派生类的对象可以隐含转换为基类对象
b1=d1;
//派生类的对象可以初始化基类的引用
B &rb=d1;
//派生类的指针可以隐含转换为基类的指针
pb1=&d1;
4 派生类的构造和析构函数
4-1 构造函数
构造派生类的对象时,就要对基类的成员对象和新增成员对象进行初始化。
派生类构造函数的一般语法形式为:
派生类名::派生类名(参数表):基类名1(基类1初始化参数表),...,
基类名n(基类n初始化参数表),
成员对象名1(成员对象1初始化参数表),...,
成员对象名m(成员对象m初始化参数表),
基本类型成员初始化
{
派生类构造函数的其他初始化操作;
}
class Base1{
public:
Base1(int i){}
};
class Base2{
public:
Base2(int j){}
};
class Derived:public Base1,public Base2{
public:
Derived(int a,int b,int c):Base1(a),Base2(b),i(c){}
private:
int i;
};
在C++11标准中,派生类能够重用其直接基类定义的构造函数。派生类继承基类构造函数的方式是提供一条注明了(直接)基类我的using声明语句。
class Derived:public Base{
public:
using Base::Base;
//相当于Derived(params):Base(args){}
double d;
}
当一个基类构造函数含有默认实参时,派生类将获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认实参的形参。
继承的构造函数不会作为用户定义的构造函数来使用,因此如果一个类只含有继承的构造函数,则它也将拥有一个合成的默认构造函数。
4-2 复制构造函数
如果要为派生类编写复制构造函数,一般需要为基类相应的复制构造函数传递参数。例如:
Derived::Derived(const Derived &v):Base(v){...}
这里还用到了类型兼容规则,用派生类的对象去初始化基类的引用。
4-3 析构函数
如果没有显式地声明过析构函数,编译系统会自动为每个类都生成一个默认的析构函数。自动生成的析构函数的函数体虽然是空的,但并非不做任何事,它会隐含地调用派生类对象成员所在类的析构函数和调用基类的析构函数。
4-4 删除delete构造函数
如果基类中的默认构造函数、复制构造函数、移动构造函数是删除或者不可访问的,则派生类中对应的成员函数将是被删除的,因为编译器无法执行派生类对象中基类部分的构造或赋值操作:
class Base{
public:
Base()=default;
Base(string _info):info(std::move(_info))()
Base(Base &)=delete;//删除复制构造函数
Base(Base &&)=delete;//删除移动构造函数
};
class Derived:public Base{
};
Derived d1;//正确,合成了默认构造函数
Derived d2(d1);//错误,删除了复制构造函数
Derived d3(std::move(d1));//错误,删除了移动构造函数
基类Base中删除复制和移动构造函数后,派生类Derived无法使用复制和移动构造函数,而因为基类中通过default关键字生成了默认构造函数,默认构造函数d1正确进行,构造函数在继承与派生过程中保证了完全的一致性。
4-5 派生类成员的标识与访问
在派生类中,成员可以按访问属性划分为4种:
- 不可访问的成员。这是从基类私有成员继承而来的,派生类或是建立派生类对象的模块都没有办法访问到它们.如果从派生类继续派生新类,也是无法访问的。
- 私有成员。这里可以包括从基类继承过来的成员以及新增加的成员,在派生类内部可以访问,但是建立派生类对象的模块中无法访问,继续派生,就变成了新的派生类中的不可访问成员。
- 保护成员。可能是新增也可能是从基类继承过来的,派生类内部成员可以访问,建立派生类对象的模块无法访问,进一步派生,在新的派生类中可能成为私有成员或者保护成员。
- 公有成员。派生类、建立派生类的模块都可以访问,继续派生,可能是新派生类中的私有、保护或者公有成员。
4-5-1 作用域分辨符
作用域分辨符可以用来限定要访问的成员所在的类的名称,一般的使用形式是:类名::成员名
或类名::成员名(参数表)
对于在不同的作用域声明的标识符,可见性原则是:如果存在两个或多个具有包含关系的作用域,外层声明了一个标识符,而内层没有再次声明同名标识符,那么外层标识符内层仍然可见;如果在内层声明了同名标识符,则外层标识符在内层不可见,这时称内层识符隐藏了外层同名标识符,这种现象称为隐藏规则。
在类的派生层次结构中,基类的成员和派生类新增的成员都具有类作用域。二者的作)范围不同,是相互包含的两个层,派生类在内层。这时,如果派生类声明了一个和某个基类成员同名的新成员,派生类的新成员就隐藏了外层同名成员,直接使用成员名只能访问到生类的成员。如果派生类中声明了与基类成员函数同名的新函数,即使函数的参数表不同从基类继承的同名函数的所有重载形式也都会被隐藏。如果要访问被隐藏的成员,就需使用作用城分辨符和基类名来限定。
如果某派生类的多个基类拥有同名的成员,同时,派生类又新增这样的同名成员,在这种情况下,派生类成员将隐藏所有基类的同名成员。
如果某个派生类的部分或全部直接基类是从另一个共同的基类派生而来,在这些直接基类中,从上一级基类继承来的成员就拥有相同的名称,因此派生类中也就会产生同名现象,对这种类型的同名成员也要使用作用域分辨符来唯一标识,而且必须用直接基类来进行限定。
4-5-2 虚基类
当某类的部分或全部直接基类是从另一个共同基类派生而来时,在这些直接基类中从上一级共同基类继承来的成员就拥有相同的名称。在派生类的对象中,这些同名数据成员在内存中同时拥有多个副本,同一个函数名会有多个映射。我们可以使用作用域分辨符来唯一标识并分别访问它们,也可以**将共同基类设置为虚基类,这时从不同的路径继承过来的同名数据成员在内存中就只有一个,同一个函数名也只有一个映射。**这样就解决了同名成员的唯一标识问题。
虚基类的声明是在派生类的定义过程中进行的,其语法形式为:class 派生类名: virtual 继承方式 基类名
声明了虚基类之后,虚基类的成员在进一步派生过程中和派生类一起维护同一个内存数据。
class Base0{
public:
int var0;
};
class Base1:virtual public Base0{
public:
int var1;
};
class Base2:virtual public Base0{
public:
int var2;
};
class Dervied:public Base1,public Base2{
public:
int var;
};
int main(){
Derived d;
d.var0=2;
return 0;
//如果没有使用虚基类就有三个var0,
//需要使用作用域分辨符,如d.Base0::var0,d.Base1::var0,d.Base2::var0
}
虚基类声明只是在类的派生过程中使用了 virtual 关键字。在程序主函数中,创建了一个派生类的对象 d,通过成员名称就可以访问该类的成员。
比较一下使用作用域分辨符和虚基类技术,前者在派生类中拥有同名成员的多个副本,分别通过直接基类名来唯一标识,可以存放不同的数据、进行不同的操作,后者只维护一份成员副本。相比之下,前者可以容纳更多的数据,而后者使用更为简洁,内存空间更为节省。具体程序设计中,要根据实际问题的需要来选用合适的方法。
4-5-3 虚基类及其派生类构造函数
有人可能会担心:建立 Derived 类对象d时,通过 Derived 类的构造函数的初始化列表,不仅直接调用了虚基类构造雨数 Base0,对从 Base0继承的成员var0进行了初始化,而且还调用了直接基类 Base1和 Base2 的构造函数 Base1()和 Base2(),而 Base1()和Base2()的初始化列表中也都有对基类 Baseo 的初始化。这样,对于从虚基类继承来的成员var0 岂不是初始化了3次?对于这个问题,C++ 编译器有很好的解决办法,我们完全不必担心,可以放心地像这样写程序。下面就来看看 C++ 编译器处理这个问题的策略。为了叙述方便,我们将建立对象时所指定的类称为当时的最远派生类。例如上述程序中,建立对象d时,Derived 就是最远派生类。建立一个对象时,如果这个对象中含有从虚基类继承来的成员,则虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。而且,只有最远派生类的构造函数会调用虚基类的构造函数,该派生类的其他基类(例如,上例中的 Base和 Base2类)对虚基类构造函数的调用都自动被忽略。