欢迎来到博主的专栏——c++编程
博主ID:代码小豪
文章目录
- 继承
- 继承与权限访问
- 基类和派生类
- 基类和派生类的赋值兼容转换
- 基类与派生类的类作用域
- 派生类与基类的构造函数
- 基类与派生类拷贝构造函数
- 继承与静态成员
- final关键字
面向对象编程的核心思想是封装、继承和多态,通过封装,我们可以将类的接口与实现分离,封装在前面已经了解过了,现在来讲讲继承这一特性
继承
通过继承联系在一起的类构成一种层次关系。在层次关系的根部的类称为基类,其他类从基类继承而来,称为派生类。基类负责的定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。
所以在面对对象编程中,设计一个合理的基类以及派生类是一个非常重要的步骤。我举一个例子。
比如,如果我们想要设计出一款游戏,那么首先我们可以考虑设计一个基类entity(实体),这个entity指的是游戏内可以交互的物体,以英雄联盟举例,英雄是一个实体,小兵和野怪是一个实体,防御塔也是一个实体,包括地形也是一个实体。
OK,根据上面所说关于继承的层次关系,基类应该设计成拥有层次中所有类共同拥有的成员(即成员函数和成员变量)。当前构思中设想的派生类有:英雄、小兵、防疫塔等。那么这些派生类拥有的统一特征是什么?
英雄和小兵可以移动,但是防御塔不能,因此移动不是基类的成员,英雄和防御塔都能进行攻击,但是地形不能进行攻击,因此攻击也不是基类的成员
博主这里想到了3个共同特征,(1)坐标,实体会有在地图中的坐标(2)碰撞体积,所有实体具有碰撞体积(3)实体会在游戏当中显示出位置
因此基类entity被博主设计成这样
class entity//基类enity
{
public: void display();
protected: int _x;//x坐标int _y;//y坐标int _Hitbox;//碰撞体积
};
继承与权限访问
现在我们来设计enity的派生类,先来设计玩家操作的英雄类player吧。什么是player具有,但是其他派生类未必具有的操作呢?首先,player会有名字,第二、player会攻击,第三、player会移动,因此,我们可以把player设计成如下情况。
class entity
{public: void display();protected: int _x;//x坐标int _y;//y坐标int _Hitbox;//碰撞体积
};class player :public entity
{
public:void attack();//攻击void move();//移动
private:string _name;
};
前面滔滔不绝讲了这么多,但是博主竟然遗漏的最重要的内容,那就是派生类如何继承基类。以及基类的成员在派生类中的访问权限是什么。
首先,我们要为派生类指出,该派生类是由哪些基类继承而来的。方法如下:
我们在派生类player后面加上一个冒号(:),然后加上继承方式public,然后指出继承的基类是entity。
继承方式分为三种(1)public继承,(2)protected继承,(3)private继承,现在程序员在设计派生类时基本只采用public继承,因此博主只讲public继承的规则
基类的成员权限分为3种,分别是public,protected、private。在public继承下,这三种权限的成员在派生类中的访问权限如下:
基类中的成员权限 | 派生类继承的基类权限 |
---|---|
public | 可以在类域外访问,可以在类域内访问 |
protected | 不可在类域外访问,可以在类域内访问 |
private | 不可在类域外访问,不可在类域内访问 |
例子如下:
class A
{
public:void printA() {cout<<_x<<_y;};
protected:int _x;
private:int _y;
};class B :public A
{
public:void printB(){printA();//可以访问基类的public成员cout << _x;//可以访问基类的protected成员cout << _y;//error不可访问基类的private成员}
};int main()
{B b;b.printA();//可以在类外访问b._x;//error,不可在类外访问b._y;//error,不可在类外访问return 0;
}
由于B public 继承了A,因此在B类域内,可以访问A的public成员和protected成员,不可访问private成员_y。在B类域外,可以访问A的public成员printA(),不可访问A的protected和private成员。
此外,·派生类可以继承多个基类。
class a{};
class b{};
class c :public a, public b {};//c同时继承了a和b
基类和派生类
派生类拥有从基类继承而来的成员,也就是说,基类拥有的成员是所有派生类的共同成员,还记得前面举得关于游戏设计的例子吗?它们的层次关系为
实际上博主觉得这种图更能体现继承的关系
乍一看这个图和数学中的关于集合文恩图有点类似,但实际文恩图越小的圈表示包含的内容越少,而继承则是越小层次的派生类,包含的功能会越多。
基类和派生类的赋值兼容转换
以player和entity为例,entity的内存形式如下:
而player的内存形式如下
一个派生类对象包含两部分,一部分是从基类继承下来的数据,另一部分是派生类自己定义的数据。
基类对象可以用派生类对象进行赋值操作,其效果是得到派生类中基类部分的值。(反之不行)
以enity和player为例,博主为player增加了移动的功能
class entity
{
public: void display() { cout << _x << _y; };
protected: int _x=0;//x坐标int _y=0;//y坐标int _Hitbox = 0;//碰撞体积
};class player :public entity
{
public://void attack();//攻击void move()//移动{_x += 1;_y += 1;}
private:string _name;
};
player调用move1次,x轴和y轴上的坐标就相应的增加1(大概就是往斜上方移动)。
entity entity1;//enity1坐标{0,0}
player player1;//player1坐标{0,0}
player1.move();//enity{0,0},player{1,1}
entity1 = player1;//entity{1,1},player1{1,1}
entity1.display();//1 1
player1和entity并非相同类型,也不符合类之间隐式转换赋值的规则(即赋值对象与赋值重载函数的参数类型一致。),那么为什么派生类可以赋值给基类呢?
c++允许派生类的对象赋值给基类的对象,其基类的值会变成派生类中有关基类部分的值。
基类的指针或引用可以指向派生类。这个过程可以成为切片或切割。意思就是将派生类中有关基类的值进行操作。
为了方便展示接下来的示例,博主将entity中的_x,_y,_Hitbox的权限修改为public,其余代码不变。
class entity{public: void display() { cout << _x << _y; }; int _x=0;//x坐标int _y=0;//y坐标int _Hitbox = 0;//碰撞体积};class player :public entity{public://void attack();//攻击void move()//移动{_x += 1;_y += 1;}private:string _name;};
player player1;//player1坐标{0,0}entity* eptr = &player1;//基类的指针
entity& eref = player1;//基类的引用eptr->_x = 5;//将player1中的_x赋值为5
eref._y = 5;//将player1中的_y赋值为5
eptr->_name="LY";//error,不能操作派生类中除了基类以外的数据
eref->_name="LY";//error,同上
player1.display();//(5,5)
派生类的对象不能用基类赋值,派生类的指针和引用也不能指向基类。
基类与派生类的类作用域
基类与派生类拥有各自的类域,当派生类继承基类时,会在派生类中嵌套在基类的作用域(类似于局部域嵌套在全局域,即优先使用局部域中的标识符)。
如果一个标识符在派生类的作用域中无法被查找到,编译器就会在基类的类域中寻找该标识符的定义。
比如我们定义基类X
class X
{
public:void printX() { cout << _x << _y << _z; }int _x=0;
protected:int _y=0;
private:int _z=0;
};
标识符_x,_y,_z都存在基类X的类域当中,因此定义在基类类域的成员函数print可以访问这些成员。
现在我们用Y来继承基类X。即Y成为X的派生类
class Y:public X
{
public:void printY() { cout << _x << _y << _z; }//error,_z没有访问权限
};
在派生类Y的类域内使用了_x,_y,_z三个标识符,编译器会首先在派生类Y的类域中寻找这三个变量的定义,如果Y的类域中不存在这些标识符的定义,就会到基类的类域中寻找这些变量,而_x,_y,_z定义在基类X的类域当中,因此会访问基类类域中的变量。
由于_x,_y的访问权限分别是publc和protected,因此,我们可以在派生类Y的类域中访问到_x,_y,而_z的权限是private,不能再派生类Y中访问。
因此我们调用Y的printY时,会打印基类部分_x,_y的值(_z不能访问)。
void printY() { cout << _x <<_y; }//打印结果是0 0
由于派生类域和基类类域属于不同的作用域,C++中允许在不同的作用域中声明相同的标识符,比如局部域中的标识符可以和全局与的标识符重名,这个规则在类域当中也适用。
因此我们可以在派生类中定义和基类一致的标识符。
class X
{
public:void printX() { cout << _x << _y << _z; }int _x=0;
protected:int _y=0;
private:int _z=0;
};class Y:public X
{
public:void printY() { cout << _x << _y <<_z; }
private:int _x = 1;int _y = 2;int _z = 3;
};
由于编译器会优先在派生类中查找定义,因此当访问派生类和基类同名的标识符时,会优先访问到派生类的成员,而基类中的同名成员则无法被访问,我们称这一特性为隐藏。
X x;
Y y;
y.printY();//打印结果为1,2,3
x.printX();//打印结果为0,0,0
如果想要在派生类的类域中访问到被隐藏的基类的成员,就要用到类限定符(::)。
class Y:public X
{
public:void printY() { cout << X::_x << X::_y <<_z; }//没有访问X::_Z的权限
private:int _x = 1;int _y = 2;int _z = 3;
};
这样访问的就是基类X的_x,_y成员。
Y y;
y.printY();//打印结果为0,0,3
如果使用相同的成员函数,也会触发隐藏机制,比如:
class X
{
public:void print() { cout << _x << _y << _z; }//同名函数printint _x=0;
protected:int _y=0;
private:int _z=0;
};class Y:public X
{
public:void print() { cout << _x << _y <<_z; }//同名函数print
private:int _x = 1;int _y = 2;int _z = 3;
};
此时如果y对象调用print,输出结果是1,2,3。如果想要调用X类域中的print,就要用到域限定符。
Y y;
y.print();//打印结果为1,2,3
y.X::print();//打印结果为0,0,0
派生类与基类的构造函数
派生类拥有从基类当中继承而来的成员,但是我们并非可以在派生类中初始化所有的基类成员,因为基类当中的private成员在派生类的类域当中无法访问,这也就导致无法在派生类的构造函数中通过直接赋值的方式完成从初始化。
于是c++允许在派生类的构造函数的初始化列表中调用基类的构造函数。以此来完成构造函数的初始化。
class entity
{
public:entity(int x, int y, int Hitbox):_x(x),_y(y),_Hitbox(Hitbox){}
protected: int _x=0;//x坐标int _y=0;//y坐标int _Hitbox = 0;//碰撞体积
};class player :public entity
{
public:player(int x,int y,int Hitbox,string name):entity(x,y,Hitbox),_name(name){}
private:string _name;
};
当构造player时,将x,y,Hitbox实参传递给entity的构造函数,由entity的构造函数负责初始化player的基类部分,即(_x,_y,_Hitbox)成员。
c++规定派生类进行初始化时,会优先初始化基类,再初始化派生类定义的成员,即无论如何,当派生类调用构造函数时,一定会优先构造派生类的基类部分,表现为会优先调用基类相应的构造函数。
基类与派生类拷贝构造函数
以基类entity及其派生类player为例,我们先来看看这两个类的拷贝构造函数是怎样的。
entity(const entity& rhs)
{_x = rhs._x;_y = rhs._y;_Hitbox = rhs._Hitbox;
}
player(const player& player):entity(player),_name(player._name){}
派生类的拷贝构造函数依然可以调用基类的拷贝构造函数完成拷贝,但是有一个奇怪的点,那就是基类的拷贝构造的函数形参是entity类型,但是在派生类player中却传递了一个player类型的参数,为什么这种传参方式是可行呢?
其实这就是前面提到的基类与派生类之间的赋值兼容转换,即基类的引用、指针可以指向派生类。
派生类的赋值重载函数也是同理。
entity& operator=(const entity& entity)
{_x = entity._x;_y = entity._y;_Hitbox = entity._Hitbox;
}
player& operator=(const player& player)
{entity::operator=(player);//调用entity的赋值重载函数_name = player._name;
}
继承与静态成员
如果基类存在一个静态成员,那么对于整个继承体系来说,都存在唯一的一个静态成员,即无论从基类中派生出多少派生类,该静态成员都是唯一的实例。
比如在基类entity中声明一个静态成员变量_num,
class entity
{
protected: int _x=0;//x坐标int _y=0;//y坐标int _Hitbox = 0;//碰撞体积static int _num;
};
在其派生类player中,与entity共用同一个_num。
如果在基类中存在static成员函数,效果也是同理。
class entity
{
public:static void statement() { cout << _num; }
protected: int _x=0;//x坐标int _y=0;//y坐标int _Hitbox = 0;//碰撞体积static int _num;
};
我们可以通过基类访问这个函数,可以通过派生类访问这个函数。
entity::statement();//通过基类访问
entity(1, 1, 1).statement();
player::statement();//通过派生类访问
player(1, 1, 1, "ly").statement();
final关键字
如果我们定义某个类无法被继承,就在类名后面加上关键字final。
class entity final
{
};
此时player将无法作为entity的派生类,发生报错