继承是类设计层面上的复用。是在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。
注意:1,基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。
3. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
4. 在实际运用中一般使用public继承,很少使用protetced/private继承。
我们之前说过,同类型的对象可以复制,不同类型的对象通过隐式类型转换可以赋值,那么有二个问题:
1,(在公有继承)子类对象可以赋值给父类对象/指针/引用吗?
2,是通过隐式类型转换赋值的吗?
接下来我们写一段代码来说明这个问题。
class Person
{
protected:
string _name;
string _Sex;
int age;
};
class Student :public Person {
public:
int _No;
};
int main()
{
Student s;
Person p = s;
Person* pp = &s;
Person& ppp = s;
return 0;
}
1, 观察上述代码,我们发现子类对象可以赋值给父类对象,并且子类到父类没有进行强制类型转换,因为上述代码Person &rp=s;可以编译通过,如果是隐式类型转换,隐式类型转换中间会产生一个临时变量,临时变量具有常性,它传给Person &rp是权限的放大,发生错误。
2,子类对象可以赋值给父类对象是一个特殊支持,语法天然支持这个行为,这个过程形象的叫切片。
3,因为继承的关系,一个子类可以看成一个特殊的父类,子类对象赋值给父类对象时,子类对象将自己内部父类的成员切割出去,拷贝给父类。
那么反过来父类对象可以赋值给子类对象吗?
答案是不可以,但是父类的指针和引用可以通过强制类型转换赋值给子类的指针和引用,但是这种转化可能会不安全。
子类中的成员有两部分,一类是自己的,一类是从父类中继承的,针对这两部分,我们分别有以下的做法。
=======================================================
假设需要显示的写析构。
假设我们定义了3个student对象,看上面的运行结果,会出现出现一个问题,父类调用的析构函数数目*2,原因就是显式的调用了父类的析构函数,所以不需要显式调用父类的析构函数,因为它会在每个子类析构函数的后面,自动调用父类析构函数。为什么会自动调用,子类继承父类,父类先构造,子类后构造,子类就先析构,父类后析构,如果自己写,不能保证这样的析构顺序。
=======================================================
=======================================================
假设要自己写赋值。
友元关系不能被继承。
一个子类只有一个直接父类时称这个继承关系为单继承。
一个子类有两个或以上直接父类时称这个继承关系为多继承。
菱形继承是多继承的一种特殊情况。
菱形继承的问题:菱形继承有数据冗余和二义性的问题。
比如下图:
Assistant中有两份person的数据,造成数据冗余,并且也无法分辨到底访问的是哪个peson的成员,具有二义性。(指定作用域可以解决二义性)
我们在下图腰部位置加入virtual关键字,就可以解决数据冗余和二义性。
class Person
{
public:
string _name;
};
class Student : virtual public Person
{
protected:
int _num;
};
class Teacher : virtual public Person
{
protected:
int _id;
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse;
};
接下来我们写一个菱形继承
1,不加virtual
我们观察D对象的内存结构,我们发现A在内存中有两份,数据冗余。
2,加virtual
我们再次观察D对象的内存结构,D对象中将A整体放到了对象组成的最下
面,这个A同时属于B和C,解决了数据冗余。但是第一行和第三行是什么?
我们进一步观察发现,第一行和第三行是一个指针,分别指向一张表,这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的是偏移量。通过偏移量
可以找到公共的A,比如第一个表中偏移量为20,B这个指针加上20,就能找到A。
那么为什么要找A?
如果是遇到切片这种场景,B 中有自己的一部分,还有从A中继承的一部分,现在需要B b=d,因为A不在B里,是在一个公共的区域,D d将自己属于B b的那一部分拷贝给B b,还要找到A,怎么找,通过这个指针+偏移量去找,然后拷给B。
注意:公共的A不一定是在最后,和编译器有关。
它不仅会影响D的对象模型,也会影响B,C的对象模型,B,C的对象模型和 D类似。
菱形虚继承虽然解决了数据冗余和二义性,但是付出了一些的代价,复杂度变高,访问效率也变低,但是是可接受的范围。
能不用菱形继承就不用。
1,C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
2. 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。(学生是人,玫瑰花是植物)
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
(车有轮胎,头有眼睛)
一些类型,既可以是is-a,又是has-a,比如stack是一个vector,stack有vector,这种情况,优先使用组合。为什么优先使用组合?
1,软件工程提倡低耦合。
2,继承通过生成派生类的复用通常被称为白箱复用。在继承方式中,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
3,组合是类继承之外的另一种复用选择。这种复用风格被称为黑箱复用,因为对象的内部细节是不可见的,组合类之间没有很强的依赖关系,耦合度低。
4,组合的耦合度低,代码维护性好。不过继承也有用,有些关系就适合继承,比如多态。