继承是面向对象的三大特性之一
类与类之间可以通过继承建立特殊的关系:
哈士奇 <-- | | --> 加菲猫
萨摩耶 <-- 狗 <-- 动物 --> 猫 --> 布偶猫
牧羊犬<-- | | --> 英短
被继承的类称为子类,子类拥有父类的全部特性,并且还能再拓展自己的特性。子类中的成员分为两部分,继承的部分体现了子类与父类的共性,而自己的新增成员体现了子类的个性。当两个类有很多重复内容,此时使用继承可以提高代码复用率,减少重复代码
继承方式
继承方式有public、protected、private三种:
public 将父类的三种不同权限类型的成员原封不动的搬运进子类,不改变权限类型
protected 将父类除了private的类型全部转换成protected搬运进子类
private 将父类除了private的类型全部转换成private搬运进子类
权限特性:
公共权限的数据类内类外都可以访问
保护权限的数据类内可以访问,类外无法访问,子类可以访问,类对象无法访问
私有权限的数据类内可以访问,类外无法访问,子类也无法访问,类对象无法访问
Caution:类自身的所有成员都可以通过自己的对象进行访问,其他类的对象只能访问本类的公有成员,无法访问父类中保护成员和私有成员,继承的权限仅仅局限于类与类之间的访问,不关对象的事
1、单继承语法
class 子类名:继承方法 父类名{ ...... };
Sample:
class Animal{//父类Anminal
public:
int age ,sex;
};
class Cat : public Anmial{//Cat类以public方式继承Animal类的成员
public:
int strength;
};
2、多继承语法
class 子类名 : 继承方法 父类名,继承方法 父类名...{ ...... };
Sample:
class Animal{//父类Animal
public:
int age;
};
class Dragon{//父类Dragon
public:
int age1;
};
class Cat:public Animal, protected Dragon{
//子类Cat同时继承父类Animal和Dragon的成员
public:
int strength;
};
Caution:多继承容易引发父类中出现同名成员,所以实际开发中不建议使用多继承语法
同名成员的处理方式
继承中难免会遇到父类和子类中都有同名的成员,此时想要区分想要访问的成员有一定规则,默认情况下直接访问会被调用的是子类的同名成员
-
子类对象访问子类同名成员,直接访问
-
子类对象访问父类同名成员,加作用域
-
子类父类中拥有同名的函数成员,子类会隐藏父类中同名函数成员,此时需作用域来访问
Sample1:
#include<iostream>
using namespace std;
class Animal{
public:
Animal(int age=100):age(age){}
int age;//同名数据成员
};
class Cat :public Animal{//Cat作为Anmial的子类
public:
Cat(int age=200):age(age){}
int age;//同名数据成员
};
int main() {
Cat c1;
cout << "cat age = " << c1.age << endl;//默认指向子类的同名成员
cout << "animal age = "<<c1.Animal::age << endl;//加作用域来访问父类成员
}
可以看到,通过继承的方式,哪怕父类没有实例化的对象,也可以通过子类来访问父类的成员
Sample2:
#include<iostream>
using namespace std;
class Animal {
public:
void func() {//同名函数成员
cout << "Animal-func()被调用" << endl;
}
void func(int a) {//父类重载的成员函数
cout << "Animal-func(int a)被调用" << endl;
}
};
class Cat :public Animal {//Cat作为Anmial的子类
public:
void func() {//同名函数成员
cout << "Cat-func()被调用" << endl;
}
};
int main() {
Cat c1;
c1.func();
c1.Animal::func();
c1.Animal::func(1);
}
上述代码中,父类中出现函数重载。访问思路可以先从选择子类或父类开始,没加作用域访问子类函数成员,加了作用域访问父类函数成员;再去访问父类中函数时,调用时加入参数访问有参函数,没加参数访问无参函数
Caution:类中的同名静态成员数据和静态成员函数处理方式与上述方法相同,但是多了一种访问方式,可以用类名直接访问静态成员,不需要创建对象
菱形继承与虚基类
继承过程中有可能会出现一种特殊情况——菱形继承
这种继承结构发生于一个同时继承了多个父类的子类,那些父类又正好有一个共同的父类,此时这种继承关系如同一个菱形,被称为菱形继承
二义性
发生菱形继承时,相当于子类继承了父类的父类两次。比如说现有三个类,分别为猫类、龙类、动物类,龙类和动物类同时继承了动物类的成员,此时有一个龙猫类同时继承了龙类与猫类的成员,龙猫类对象使用数据时就会出现二义性
|——>——猫类——>——|
动物类 龙猫类
|——>——龙类——>——|
Sample:
class Animal {//动物类
public:
int age;
};
class Cat:public Animal{};//猫类
class Dragon:public Animal{};//龙类};
class DragonCat :public Cat, public Dragon {};//龙猫类
int main() {
DragonCat p1;
p1.Dragon::age = 18;
p1.Cat::age = 28;
}
龙猫类的数据相当于继承了两次动物类,发生了资源不必要的重复使用,事实上那份数据事实上只需要一份就可以了,但现在DradonCat类中有两份age,并且因为二义性,此时访问p1.age会报错
解决二义性的方法——Virtual关键字
利用虚继承,可以解决菱形继承的问题,此时被继承的最大基类称为虚基类
在继承前加上virtual关键字进行虚继承
class Animal {//虚基类
public:
int age;
};
class Cat:virtual public Animal{};//猫类虚继承
class Dragon:virtual public Animal{};//龙类虚继承
class DragonCat :public Cat, public Dragon {};//龙猫
int main() {
DragonCat p1;
p1.Dragon::age = 10;
p1.Cat::age = 20;
cout << p1.age << endl;
}
从程序输出结果来看,此时p1.age为20,而若将Dragon::和Cat::那两行赋值代码调换顺序,输出结果会变成10,说明age变量只有一个,覆写后显示最后一次赋值的结果。很好的解决了资源重复和二义性的问题
底层逻辑:
发生虚继承后的龙猫类中age只有一份,并且多了两份指针变量——vbptr
v——virtual b——base p——pointer vbptr称为虚基类指针
vbtable称为虚基类表格
每一个虚基类指针指向一个虚基类表格,虚基类表格通过计算内存地址差值作为偏移量,来对指针进行偏移从而找到age的变量所在地址,通过这种方式使得age变量成为唯一确定的变量
illustration :封面 by 紺屋鴉江