目录
7.1基类与派生类
7.1.1 继承关系举例
如图7.1所示,反映了交通工具的派生关系,最高层是抽象程度最高的,是最具有普遍和一般意义的概念,下层具有上层的特性,同时加入了自己的新特性,最下层是最为具体的。在这个层次结构中,由上到下是一个具体化、特殊化过程;由下到上是一个抽象化过程。上下层之间可以看作基类与派生类的关系。
新的类从已有类那里得到已有的特性叫做类的继承;
从已有类产生新类的过程就是类的派生。
7.1.2派生类的定义
class 派生类名:继承方式 基类名1,继承方式 基类名2,... ,继承方式 基类名n
{
派生类成员声明;
}
继承方式缺省默认为私有继承(private)
//Base1,Bas2是已经定义的类
class Derived: public Base1,private Base2
{
public:
Derived();
~Derived();
};
1.一个派生类可以同时有多个基类,这种情况称为多继承,只有一个基类的情况,称为单继承。
2.在类族中,直接派生出来的基类称为直接基类,基类的基类甚至更高层的基类称为间接基类。
交通工具->汽车->卡车,则汽车类是卡车类的直接基类,交通工具类是卡车类的间接基类。
3.派生类成员是指除了从基类继承来的所有成员外,新增加的数据和函数成员。
7.1.3派生类生成过程
1.吸收基类成员
派生类继承基类中除构造和析构函数之外的所有非静态成员。
2.改造基类成员
如果派生类声明了一个和某基类成员同名的新成员,派生的新成员就隐藏了外层同名成员。这时在派生类中或者通过派生类的对象,直接使用成员名就只能访问到派生类中声明的同名成员,这称作同名隐藏。
3.添加新的成员
根据实际情况的需要,给派生类添加适当的数据和函数成员,来实现必要的新增功能。
7.2访问控制
从基类继承的成员,其访问属性由继承方式控制。
类的继承方式有public(公有)、protected(保护)、private(私有)三种。
7.2.1 公有继承
1.当类的继承方式为公有继承时,基类的公有和保护成员的访问属性在派生类中不变(仍作为派生类的公有成员和保护成员,派生类其他成员和对象可以直接访问),而基类的私有成员不可直接访问。
7.2.2 私有继承
1.当类的继承方式为私有继承时,基类中的公有成员和保护成员都以私有成员身份出现在派生类中(作为派生类的私有成员,派生类的其他成员可以直接访问他们,但类族外部通过派生类的对象无法直接访问他们),而基类的私有成员在派生类中不可直接访问。
7.2.3 保护继承
1.保护继承中,基类的公有和保护成员都以保护成员的身份出现在派生类中,而基类的私有成员不可直接访问。这样,派生类的其它成员就可以直接访问从基类继承来的公有和保护成员,但在类外部通过派生类的对象无法直接访问他们。无论是派生类的成员还是派生类的对象都无法直接访问基类的私有成员。
例:假定某一个类A有保护数据成员x,我们来讨论成员x的访问特征。
//基类A的定义为:
class A {
protected:
int x;
};
//情况一,如果主函数为:
int main() {
A a;
a.x=5; //错误
}
//情况二
class B:public A {
public:
void function();
};
void B::function() {
x=5; //在派生类B的成员函数function内部,完全可以访问基类的保护成员
}
注意:如果B是A的派生类,B的成员函数只能通过B的对象访问A中定义的protected成员,而不能通过A的对象访问A的protected成员。
7.3 类型兼容规则(向上转型)
1.类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来代替。或者说,一个公有派生类的对象在使用上可以被当作基类的对象,反之则不可。具体表现在:
派生类的对象可以隐含转换为基类对象。
派生类的对象可以初始化基类的引用。
派生类的指针可以隐含转换为基类的指针。
2.在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员。
7.4 派生类的构造和析构函数
7.4.1 构造函数
1。派生类的成员对象是由所有基类的成员对象与派生类新增的成员对象共同组成。因此,构造派生类的对象时,就要对基类的成员对象和新增成员对象进行初始化。基类构造函数并没有继承下来,要完成这些工作,就必须给派生类添加新的构造函数。
派生类构造函数一般语法形式:
派生类名::派生类名(参数表):基类名1(基类1初始化参数表),...,基类名n(基类n初始化参数表),成员对象名1(成员对象1初始化参数表),..,成员对象名m(成员对象m初始化参数表),基本类型成员初始化
{
派生类构造函数的其它初始化操作;
}
如果对基类初始化时,需要调用基类的带有形参表的构造函数时,派生类就必须声明构造函数,提供一个将参数传递给基类构造函数的途径,保证在基类进行初始化时能够获得必要的数据。
2.派生类构造函数执行的一般顺序如下:
(1)调用基类构造函数,调用顺序按照他们被继承时声明的顺序。
(2)对派生类新增的数据成员初始化,初始化顺序按照他们在类中声明的顺序。
(3)执行派生类的构造函数体中的内容。
3.派生类构造函数举例
//7.4.cpp
#include<iostream>
using namespace std;
class Base1 { //基类Base1,构造函数有参数
public:Base1(int i) {cout<<"Constructing Base1 "<<i<<endl;}
};
class Base2 { //基类Base2,构造函数有参数
public:Base2(int j) {cout<<"Constructing Base2 "<<j<<endl;}
};
class Base3 { //基类Base3,构造函数无参数
public:Base3() {cout<<"constructing Base3 *"<<endl;}
};
class Derived:public Base2,public Base1,public Base3 {
//派生类Dervied,注意基类名顺序
public:
Derived(int a,int b,int c,int d):Base1(a),member2(d),member1(c),Base2(b)
{}
private:
Base1 member1;
Base2 member2;
Base3 member3;
};
int main() {
Derived obj(1,2,3,4);
return 0;
}
结果:Constructing Base2 2
Constructing Base1 1
Constructing Base3 *
Constructing Base1 3
Constructing Base2 4
Constructing Base3 *
4.在C++11标准中,派生类能够重用其直接基类定义的构造函数。一个类只初始化它的直接基类,同样,一个类也只继承其直接基类的构造函数。派生类继承基类构造函数的方式是提供一条注明了基类名的using声明语句。下面例子定义了一个Derived类,令其继承Base类的构造函数:
class Derived: public Base {
public:
using Base::Base; //继承Base的构造函数
double d;
}
7.4.2 复制构造函数
如果要为派生类编写复制构造函数,一般要为基类相应的复制构造函数传递参数。
例如,假设Derived类是Base类的派生类,Derived类的复制构造函数形式如下:
Derived::Derived(const Derived &v):Base(v) {...}
7.4.3 析构函数
在派生过程中,基类的析构函数也不能继承下来,如果需要析构,就要在派生类中声明新的析构函数。派生类析构函数的声明方法与没有继承关系的类中析构函数的声明方法完全相同,只要在函数体中负责把派生类新增的非对象成员的清理工作做好就够了,系统会自己调用基类及对象成员的析构函数来对基类及对象成员进行清理。但他的执行次序和构造函数正好严格相反。
//7.4.cpp
#include<iostream>
using namespace std;
class Base1 { //基类Base1,构造函数有参数
public:
Base1(int i) {
cout<<"Constructing Base1 "<<i<<endl;
}
~Base1() {
cout<<"Destructing Base1"<<endl;
}
};
class Base2 { //基类Base2,构造函数有参数
public:
Base2(int j) {
cout<<"Constructing Base2 "<<j<<endl;
}
~Base2() {
cout<<"Destructing Base2"<<endl;
}
};
class Base3 { //基类Base3,构造函数无参数
public:
Base3() {
cout<<"Constructing Base3 *"<<endl;
}
~Base3() {
cout<<"Destructing Base3"<<endl;
}
};
class Derived:public Base2,public Base1,public Base3 {
//派生类Dervied,注意基类名顺序
public:
Derived(int a,int b,int c,int d):Base1(a),member2(d),member1(c),Base2(b) {
}
private:
Base1 member1;
Base2 member2;
Base3 member3;
};
int main() {
Derived obj(1,2,3,4);
return 0;
}
结果:Constructing Base2 2
Constructing Base1 1
Constructing Base3 *
Constructing Base1 3
Constructing Base2 4
Constructing Base3 *
Destructing Base3
Destructing Base2
Destructing Base1
Destructing Base3
Destructing Base1
Destructing Base2
7.4.4 删除delete构造函数
通过delete来实现禁止默认构造函数或删除复制构造函数以阻止复制的做法,在基类中删除掉的构造函数,在派生类中也对应是删除状态。
class Base {
public:
Base()=default;
Base(String_info):info(std::move(_info)) {}
Base(Base &)=delete;
Base(Base &&)=delete;
private:
string info;
};
class Derived:public Base {
};
Derived d1; //正确,合成了默认构造函数
Derived d2(d1); //错误,删除了复制构造函数
Derived d3(std::move(d1)); //错误,删除了移动构造函数
7.5 派生类成员的标识与访问
7.5.1 作用域分辨符
1.符号:“::”,它可以用来限定要访问的成员所在的类的名称,一般的使用形式是:
类名::成员名 //数据成员
类名::成员名(参数表) //函数成员
如果派生类中声明了与基类成员函数同名的新函数,即使函数的参数表不同,从基类继承的同名函数的所有重载形式也都会被隐藏。如果要访问被隐藏的成员,就需要使用作用域分辨符和基类名来限定。
细节:如果子类中定义的函数与父类的函数同名但具有不同的参数表,不属于函数重载,这时子类中的函数将隐藏父类中的同名函数,调用父类中的同名函数必须使用父类名称来限定。只有在相同的作用域中定义的函数才可以重载。
2.如果某个派生类的部分或全部直接基类是从另一个共同的基类派生而来的,在这些直接基类中,从上一级基类继承来的成员就拥有相同的名称,因此派生类中也就会产生同名的现象,对这种类型的同名成员也要使用作用域分辨符来唯一标识,而且必须用直接基类来进行限定。
例如:
//7_7.cpp
#include<iostream>
using namespace std;
class Base0 { //定义基类Base0
public:
int var0;
void fun0() {
cout<<"Member of Base0"<<endl;
}
};
class Base1:public Base0 { //定义派生类Base1
public:
int var1;
};
class Base2:public Base0 { //定义派生类Base2
public:
int var2;
};
class Derived:public Base1,public Base2 {
public:
int var;
void fun() {
cout<<"Member of Derived"<<endl;
}
};
int main() {
Derived d;
d.Base1::var0=2;
d.Base1::fun0();
d.Base2::var0=2;
d.Base2::fun0();
return 0;
}
程序运行结果为:
Member of Base0
Member of Base0
7.5.2 虚基类
1.为了解决同名成员的唯一标识问题。我们可以使用作用域分辨符来唯一标识并分别访问他们,也可以将共同基类设置为虚基类,这时从不同的路径继承过来的同名数据成员在内存中就只有一个,同一个函数名也只有一个映射。
2.虚基类的声明是在派生类的定义过程中进行的,其语法形式为:
class 派生类名:virtual 继承方式 基类名
3.虚基类举例:
使用了虚基类之后,在派生类Derived中只有唯一的数据成员var0。在建立Derived类对象的模块中,直接使用“对象名.成员名”方式就可以唯一标识和访问这些成员。
//7_8.cpp
#include<iostream>
using namespace std;
class Base0 { //定义基类Base0
public:
int var0;
void fun0() {
cout<<"Member of Base0"<<endl;
}
};
class Base1:virtual public Base0 { //定义派生类Base1
public:
int var1;
};
class Base2:virtual public Base0 { //定义派生类Base2
public:
int var2;
};
class Derived:public Base1,public Base2 {
public:
int var;
void fun() {
cout<<"Member of Derived"<<endl;
}
};
int main() {
Derived d;
d.var0=2; //直接访问虚基类的数据成员
d.fun0(); //直接访问虚基类的函数成员
return 0;
}
运行结果:
Member of Base0
7.5.3 虚基类及其派生类的构造函数
例7_8中,程序中的所有类使用的都是编译器自动生成的默认构造函数,如果虚基类声明有默认形式的(即带形参的)构造函数,并且没有声明默认形式的构造函数,这时若在整个继承关系中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中列出对虚基类的初始化。
#include<iostream>
using namespace std;
class Base0 { //定义基类Base0
public:
Base0(int var):var0(var) {}
int var0;
void fun0() {
cout<<"Member of Base0"<<endl;
}
};
class Base1:virtual public Base0 { //定义派生类Base1
public:
Base1(int var):Base0(var) {}
int var1;
};
class Base2:virtual public Base0 { //定义派生类Base2
public:
Base2(int var):Base0(var) {}
int var2;
};
class Derived:public Base1,public Base2 {
public:
Derived(int var):Base0(var),Base1(var),Base2(var) {};
int var;
void fun() {
cout<<"Member of Derived"<<endl;
}
};
int main() {
Derived d(1);
d.var0=2; //直接访问虚基类的数据成员
d.fun0(); //直接访问虚基类的函数成员
return 0;
}
运行结果:
Member of Base0
建立对象时所指定的类称为当时的最远派生类。例如上例中,建立对象d时,Derived就是最远派生类。建立一个对象时,如果这个对象中含有从虚基类继承来的成员,则虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。而且,只有最远派生类的构造函数会调用虚基类的构造函数,该派生类的其他基类(例如Base1和Base2类)对虚基类构造函数的调用都自动被忽略。