目录
一、继承的概念
二、继承的格式
这里我们不要通过背的方式去记,因为这里的访问方式都是按最小的来
顺序是public > protected > private。就比如说,我们public继承,然后基类里面的protected成员,因为protected < public,所以这里的基类在派生类里的访问方式是按protected来进行的。
我们如果不显示的写继承方式,用的是class的,默认按照private方式继承,struct的默认继承方式是public,但建议最好是把继承方式写出来。
三、赋值兼容
赋值兼容其实就是一种赋值,我们以前进行类型转化时,有强制类型转化和隐式类型转化,这些在转化的过程中其实都是通过生成一个中间变量来实现,而这个中间变量具有常性,所以我们在进行类型转化的数据进行引用时要加上一个const。
如果我们没有加const,编译器会给我们报错,因为在类型转化过程中会产生一个具有常性的临时变量,不加const会造成权限提升,就变成一个可以改变的值,不符合常性的特点。所以就会报错,加了const,就变成了不能修改的变量,也相当于具有了常性。
但是我们在继承的时候,我们的就不叫类型转化,而叫做赋值兼容,它与类型转化是有区别的,它只能由子类(派生类)赋值给基类对象,并且它不会产生一个临时变量,而是做一个切片,对属于基类的成员切片出来。
我们可以看到,编译器并没有报错,说明没有权限放大的问题,赋值兼容不产生临时变量,而是直接将属于基类成员的部分分割出去。
为什么基类就不能赋值给派生类呢?
因为这种行为不安全,派生类肯定会拥有基类的全部成员,但是基类不一定有派生类的所有成员,因此如果能够赋值,那么派生类中的某些成员可以没有被赋值到,这时候就会生成一个随机数,如果里面还有指针,那么就会产生野指针,这是非常危险的行为,因此这种基类赋值给派生类的行为被编译器禁止了。
四、派生类的构造函数
在派生类中,我们定义一个构造函数使用初始化列表初始化时,如果没有显示的调用基类的构造函数,编译器就会先去调用基类的默认构造函数,而当我i们需要调用基类的某个特定的构造函数时,我们可以通过初始化列表的方式,来调用我们需要的基类构造函数。
class Base {
public:
Base() {
// Base 类的构造函数
}
};
class Derived : public Base {
public:
Derived() {
// Derived 类的构造函数
}
};
就比如我们创建Derived的对象时,首先调用的是Base(),然后才是Derived()。
五、继承里的静态成员函数
六、菱形继承和虚拟继承
菱形继承是一个十分复杂的继承关系,我们在继承类的时候,要尽可能去避免菱形继承的发生,因为它会带来数据冗余和二义性的问题。
就比如我们继承多个有共同基类的派生类时,最后的派生类会将父类的数据全部拷贝一份,
class Animal {
public:
int age;
};
class Mammal : public Animal {
};
class Bird : public Animal {
};
class Bat : public Mammal, public Bird {
};
比如Bat 类继承自两个基类,这两个基类都有一个共同的基类 Animal
。当我们创建 Bat
类的对象时,Bat
类实际上包含了两个 Animal
类的实例,一个来自 Mammal
,另一个来自 Bird
。这样就造成了 Bat
类中对于 age
变量的冗余,即存在两份相同的 age
变量。
就是说会有多份相同数据的Animal,实际上只需要一份就够了,这就是数据冗余,它会造成内存空间的浪费。
二义性:二义性是指在多继承中,由于派生类从多个基类继承了同名的成员函数或数据成员,导致在调用这些成员时产生歧义,编译器无法确定使用哪个基类的成员。当发生二义性时,编译器会产生错误或警告,因为它无法确定要调用哪个基类的成员。
也就是说会有多个相同类型相同名字的基类成员变量,在调用的时候,编译器就不知道该去用哪个比较好。然后我们对其进行修改,编译器也不知道应该改哪一个。
class A
{
public:
A()
{
_a = 0;
}
int _a;
};
class B : public A
{
public:
B()
{
_b = 1;
}
int _b;
};
class C : public A
{
public:
C()
{
_c = 2;
}
int _c;
};
class D : public B, public C
{
public:
D()
{
_d = 3;
}
int _d;
};
int main()
{
D d;
cout << d._a << endl;
return 0;
}
虽然我们可以通过指定类域去访问
int main()
{
D d;
cout << d.B::_a<< endl;
cout << d.C::_a << endl;
return 0;
}
但是数据冗余的问题依然没有解决
第一行存的是B中的_a,
第二行存的是B中的_b,
第三行存的是C中的_a,
第四行存的是C中的_c,
第五行存的是D中的_d,
这样造成了多份a的存在,虽然这张图看起来没什么,但是假如A类中的成员变量是个数组呢?造成的空间浪费就会变大很多。
所以c++引入了一个虚拟继承。
虚拟继承是在菱形继承的腰部添加virtual
class A
{
public:
A()
{
_a = 0;
}
int _a;
};
class B : virtual public A
{
public:
B()
{
_b = 1;
}
int _b;
};
class C :virtual public A
{
public:
C()
{
_c = 2;
}
int _c;
};
class D : public B, public C
{
public:
D()
{
_d = 3;
}
int _d;
};
虚拟继承可以让我们的B和C有一个公共的A类数据。
我们可以看到A类中的_a被放在了最后,它的地址是0x005EF770,第一行放的是虚基表指针,里面的偏移量是14,转化成10进制是20。它的地址是0x005EF75C。5C转化成10进制就是92,70转化成10进制是112.所以我们很容易看出来,这个偏移量是让B找到_a的。
那么为什么B和C要去找A类对象的成员呢?
因为假如发生这种情况
D d;
B b = d;
C c = d;
我们d赋值给b,要发生切割,就要把d中属于B类对象的部分给切割赋值给b,b中包含有A类的成员,这时候就必须找到A类的成员才能赋值给b。
最后总结
- public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
- 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
- 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称 为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的 内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很 大的影响。派生类和基类间的依赖关系很强,耦合度高。
- 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象 来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复 用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被 封装。
- 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有 些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用 继承,可以用组合,就用组合。