C++进阶——继承(2)
ʕ • ᴥ • ʔ
づ♡ど
🎉 欢迎点赞支持🎉
个人主页:励志不掉头发的内向程序员;
专栏主页:C++语言;
文章目录
前言
一、继承与友元
二、继承与静态成员
三、多继承及其菱形继承问题
(1)继承模型
(2)虚继承
(3)IO库中的菱形虚拟继承
四、继承和组合
总结
前言
上一章节我们讲解了一部分继承,告诉了我们继承是什么,以及继承的模板和默认成员函数在继承中的行为。本章节我们接着上一章节来讲讲我们继承的友元静态成员以及我们继承的模型。本章节也蛮烧脑的,大家来和我一起看看吧。
一、继承与友元
友元关系不能继承,也就是说父类友元不能访问子类私有和保护成员。
class Student;class Person
{
public:friend void Display(const Person& p, const Student& s);
protected:string _name = "*****";
};class Student : public Person
{
protected:int _stuNum = 1234;
};void Display(const Person& p, const Student& s)
{cout << p._name << endl;cout << s._stuNum << endl;
}int main()
{Person p;Student s;Display(p, s);
}
此时我们可以看到,我们Display可以调用 Person 的保护/私有,但是却不能调用 Student 的保护/私有,这就说明了我们友元函数是没有办法继承的。
解决办法也很简单,在我们 Student 中也写一个友元函数即可。
class Student : public Person
{friend void Display(const Person& p, const Student& s);
protected:int _stuNum = 1234;
};
此时我们程序就能够运行了。
二、继承与静态成员
父类定义了 static 静态成员,则整个继承体系里面只有一个这样的成员,无论派生出多少个子类,都只有一个 static 成员实例。
我们普通对象继承下来,它的内部拥有从父类那里继承下来的成员,也有自己的成员,但是本质上从父类继承下来的成员也是属于独有的成员,有自己的空间。
但是静态成员不一样,静态成员可以被继承下来。它只会有一个实例。
class Person
{
public:string _name;static int _count;
};int Person::_count = 0;class Student : public Person
{
protected:int _stuNum;
};
在这个代码中 _count 就是静态成员,无论继承了多少次,只有一个实例,使用的都是同一个静态类型。
我们尝试着把它们的地址打印出来。
int main()
{Person p;Student s;cout << &p._name << endl;cout << &s._name << endl;cout << "------------------------------" << endl;cout << &p._count << endl;cout << &s._count << endl;return 0;
}
我们发现,从父类继承下来的普通成员变量 _name 和父类是不同的空间,但是继承下来的静态成员变量 _count 却是和父类的空间是相同的。
三、多继承及其菱形继承问题
(1)继承模型
继承模型一般就分为三种:
单继承:一个子类只有一个直接父类时称这个继承关系为单继承。
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的父类在前面,后继承的父类在后面,子类成员放到最后面。
菱形继承:菱形继承是多继承的一种特殊情况。菱形继承的问题,从下面的对象模型构造,可以看出菱形继承有数据冗余和二义性的问题,在 Assistant 的对象中 Person 成员会有两份。支持多继承就一定会有菱形继承,在实践中我们不建议设计出菱形继承这样的模型。
从壮大自己的角度来看多继承看上去很合理,而且从现实的角度,一个对象有多种特征也是很常见的,就比如我们的圣女果,又可以当蔬菜又可以是水果,这样的例子在我们现实中还有很多。但是从语法的角度来看却并不是如此。
class Person
{
public:string _name; // 姓名
};class Student : public Person
{
protected:int _num; //学号
};class Teacher : public Person
{
protected:int _id; // 职⼯编号
};class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};
我们这里的 Assistant 就是图上的菱形继承,我们从下面的图中就会发现端倪。
此时就会产生数据冗余和二义性的问题,我们最下面的 Assistant 中就会同时记录它的两个父类中不同的 Person 的信息,因为它的父类都继承了它父类的父类的信息。此时就会产生冗余,然后等到访问时我们编译器不知道该访问我们哪个父类的 _name 时就会产生二义性。
int main()
{Assistant a;a._name = "peter";return 0;
}
当然,想要解决二义性可以依靠指定类域来解决。
int main()
{Assistant a;// 需要显⽰指定访问哪个基类的成员可以解决⼆义性问题,但是数据冗余问题⽆法解决a.Student::_name = "xxx";a.Teacher::_name = "yyy";cout << a.Student::_name << endl;cout << a.Teacher::_name << endl;return 0;
}
虽然这样可以,但是我们并不建议这样做,这样会导致数据冗余的情况暂且不说,它会让我们的代码很混乱,如果我们要调用一个成员函数,到底是调用哪个才行,调用 Student 的 _name 还是 Teacher 的?不管是哪个都显得不合理。而且还浪费资源。为了避免出来这样的菱形继承,我们最好就不要出现多继承,使用单继承就能够很好的避免。像 Java 就不允许出现多继承。
(2)虚继承
虽说我们不建议使用菱形继承,但是如果是在没有办法,不得不写菱形继承,此时我们也有办法解决我们数据冗余和二义性的问题,那就是虚继承,它的底层十分复杂,而且在性能上也有损失。
class Person
{
public:string _name; // 姓名
};// 使⽤虚继承Person类
class Student : virtual public Person
{
protected:int _num; //学号
};// 使⽤虚继承Person类
class Teacher : virtual public Person
{
protected:int _id; // 职⼯编号
};// 教授助理
class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};
我们在要继承的父类的继承方式前面加入一个 virtual(虚拟的)字符,于是这个继承就是虚继承了。
一般要变成虚继承的类是会造成继承的子类会数据冗余和二义性的类,就比如我们 Student 和 Teacher 要继承给我们的 Assistant 时,我们的 Assistant 就会产生数据冗余和二义性,所以就要让我们 Student 和 Teacher 变成虚继承。再比如像这样子的菱形继承。
我们 E 会产生数据冗余和二义性是由父类 B 和 C 造成的,所以就是在 B 和 C 继承时变成虚继承。
int main()
{// 使⽤虚继承,可以解决数据冗余和⼆义性Assistant a;a._name = "peter";a.Student::_name = "xxx";a.Teacher::_name = "yyy";return 0;
}
有了虚继承,我们的冗余的数据就会合并,最终效果就是虽然还是有两个 _name,但是其实它们时公用的 _name。
修改其中一个,其他的都会变。此时就解决了问题。
当然,如果只写了一边虚继承,另外一边没写就会报错。
(3)IO库中的菱形虚拟继承
我们的 IO 流就是 C++ 中最经典的菱形继承。
它把基础信息都放在 ios_base 中,然后再用 ios 继承,然后再用 istream 和 ostream 继承 ios。此时设计师想让创建一个既有 istream 特征的类,又有 ostream 特征的类,于是我们 iostream 便是菱形继承了。它在这里也使用了虚继承。
四、继承和组合
- public 继承是一种 is-a(是什么) 的关系。也就是说每个派生类对象都是一个基类对象。
- 组合是一种 has-a(有什么) 的关系。假设 B 组合了 A,每个 B 对象中都有一个 A 对象。
// 继承
class stack : public list<int>
{ };// 组合
class stack
{
private:list<int> _it;
};
继承和组合其实在逻辑上是很相似的。继承就是一个继承另外一个,组合是一个是由另外一个组成。
它们之间主要的区别在于结构逻辑:
- 继承允许你根据父类的实现来定义子类的实现。这种通过生成子类的复用通常称为白箱复用(white-box reuse)。术语 "白箱" 是相对可视性而言;在继承方式中,父类的内部细节对子类可见。继承一定程度破坏了父类的封装,父类的改变,对子类有很大的影响。子类和父类间的依赖关系很强,耦合性很高。
- 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(blank-box reuse),因为对象的内部细节是不可见的。对象只以 "黑箱" 的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保存每个类被封装。
所谓的耦合就是代码与代码之间的关系。假如有两个人写代码,一个人写的代码就是依靠另外一个人写的各种函数和类。此时这个人如果把函数名改了、类名改了,另外一个人也就不得不去改他的代码,一个人改,另外一个也得跟着改,说明代码的耦合度很高,代码之间关联性很强。此时他们就想了一个办法,他们把很多个代码隐藏了,就只开放几个函数给其他人使用。这样只有不去改这几个函数,其他函数随便更改都无所谓,这样我们的耦合度就很低了,代码之间关联性很弱。所以我们编程有个规则:高内聚,低耦合。
所以我们的代码优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承,另外要实现多态,也得使用继承。类之间的关系既适合用继承也适合用组合,就用组合。
// Tire(轮胎)和Car(⻋)更符合has-a的关系
class Tire
{
protected:string _brand = "Michelin"; // 品牌size_t _size = 17; // 尺⼨
};class Car
{
protected:string _colour = "⽩⾊"; // 颜⾊string _num = "陕ABIT00"; // ⻋牌号Tire _t1; // 轮胎Tire _t2; // 轮胎Tire _t3; // 轮胎Tire _t4; // 轮胎
};// Car和BMW/Benz更符合is-a的关系
class BMW : public Car
{
public:void Drive() { cout << "好开-操控" << endl; }
};class Benz : public Car
{
public:void Drive() { cout << "好坐-舒适" << endl; }
};
总结
以上便是继承的全部内容,我们在这里对虚继承只是简单的了解,因为我们没必要去写出菱形继承,但是如果大家想要知道它的原理,可以等博主后面更新,下一章节我们就来了解了解多态,它是在继承的基础上实现的,所以大家一定要搞明白继承的内容,我们下一章节再见。
🎇坚持到这里已经很厉害啦,辛苦啦🎇
ʕ • ᴥ • ʔ
づ♡ど