> 技术文档 > 【C++】——继承(下)

【C++】——继承(下)


【C++】——继承(下)

  • 5 继承与友元
  • 6 继承与静态成员
  • 7 多继承
    • 7.1 继承模型
    • 7.2 菱形继承的问题
    • 7.3 虚继承
    • 7.4 多继承中的指针偏移问题
  • 8 组合与继承

5 继承与友元

  友元关系不能被继承。即一个函数是父类的友元函数,但不是子类的友元函数。也就是说父类的友元不能访问子类的私有和保护成员

class Person{public :friend void Display(const Person& p, const Student& s);protected:string _name; // 姓名};class Student : public Person{protected :int _stuNum; // 学号};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);return 0;}

【C++】——继承(下)

  这里出现了许多报错,但没关系,我们先看框出来的这一条
  这条报错说缺少\",\",一般出现这种报错,而我们检查出并没有\",\"的相关问题时,通常都是 类型出问题了,一般是我们没有定义某个类型但我们直接去使用就会出现这种报错
  编译器遇到一个类型、变量、函数时都只会向上查找,这是为了提高编译速度
  出现报错的原因是friend void Display(const Person& p, const Student& s);中我们使用了 Student Student Student 类型,它的定义在下面。但 Student Student Student Person Person Person 的继承,又不可能将其放在 Person Person Person 的前面,我们可以在 Person Person Person 前加上 Student Student Student前置声明

class Student;

  
  解决这个问题后报错就少很多啦,我们再看看剩下的报错

在这里插入图片描述

  这里就是友元函数的问题啦。 Display() Display() Display() Person Person Person 的友元,但友元关系不能继承下来,因此 Display() Display() Display() 不是 Student Student Student 的友元。解决方法也很简单, S t u d e n t Student Student 中加一个有元声明就好

//前置声明class Student;class Person{public :friend void Display(const Person& p, const Student& s);protected:string _name; // 姓名};class Student : public Person{public:friend void Display(const Person& p, const Student& s);protected :int _stuNum; // 学号};void Display(const Person& p, const Student& s){cout << p._name << endl;cout << s._stuNum << endl;}

  
  

6 继承与静态成员

  父类定义了 s t a t i c static static 静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个 s t a t i c static static 成员实例。
  而普通成员,假设父类有一个_name,子类继承下来也有另一个_name,但是他们两个_name不是同一个,各自是各自的。

我们来演示一下:

class Person{public :string _name;static int _count;};int Person::_count = 0;class Student : public Person{protected :int _stuNum;};int main(){Person p;Student s;// 这⾥的运⾏结果可以看到⾮静态成员_name的地址是不⼀样的// 说明派⽣类继承下来了,⽗派⽣类对象各有⼀份cout << &p._name << endl;cout << &s._name << endl;// 这⾥的运⾏结果可以看到静态成员_count的地址是⼀样的// 说明派⽣类和基类共⽤同⼀份静态成员cout << &p._count << endl;cout << &s._count << endl;// 公有的情况下,⽗类子类指定类域都可以访问静态成员cout << ++Person::_count << endl;cout << ++Student::_count << endl;// 也可以通过对象进行访问cout << ++p._count << endl;cout << ++s._count << endl;return 0;}

运行结果:

在这里插入图片描述

  虽然静态变量可以通过对象访问,但一般不这么做,大多数都是直接指定类域去访问

  
  

7 多继承

7.1 继承模型

  • 单继承一个子类只有一个直接父类时,称为这个继承关系为单继承

在这里插入图片描述

单继承

  

  • 多继承一个子类有两个或以上直接父类时称这个继承关系为多继承。多继承对象在内存中的模型是,先继承的父类在前面,后继承的父类在后面,子类成员放在最后面

在这里插入图片描述

多继承

  

  • 菱形继承:菱形继承是多继承的一种特殊情况。菱形继承的问题,从下面的对象成员模型构造,可以看出菱形继承有数据冗余二义性的问题,在 A s s i s t a n t Assistant Assistant 的对象中 P e r s o n Person Person 成员会有两份。支持多继承就一定会有菱形继承,像 J a v a Java Java 就直接不支持多继承,规避掉了这里的问题,所以实践中我们是不建议设计出菱形继承这样的继承模型的

在这里插入图片描述

菱形继承

  

7.2 菱形继承的问题

  菱形继承是很坑的,有数据冗余(浪费空间)和二义性(不知访问哪个)的问题,现实中不想被打就不要设计出菱形继承(多继承是没问题的,不要搞出菱形继承就行)。

  我们通过代码来看一下菱形继承存在的问题

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; // 主修课程};

  
  上述就是菱形继承, Person Person Person 成员在 Assistant Assistant Assistant 对象中有两份。我们试着访问 Person Person Person 的成员 _ name name name

int main(){Assistant a;a._name = \"peter\";return 0;}

在这里插入图片描述

  
  我们需要指定访问那个父类成员的成员可以解决二义性的问题,但是数据冗余问题无法解决

int main(){Assistant a;a.Student::_name = \"xxx\";a.Teacher::_name = \"yyy\";return 0;}

【C++】——继承(下)

虽然解决了二义性,但数据冗余问题无法解决,Person 有两份

  
  不仅如此,因为数据冗余,导致菱形继承对象的大小特别大

int main(){Assistant a;cout << sizeof(Assistant) << endl;return 0;}

在这里插入图片描述

  


  那么现实中有没有人设计出菱形继承呢?还真有,我们简单看一下

在这里插入图片描述

在这里插入图片描述

  虽然但是,别学他
  

7.3 虚继承

  为了解决菱形继承的问题,C++ 引入了虚继承的概念,新增关键字: v i r t u a l virtual 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; // 主修课程};

  注意:因为是Person有数据冗余和二义性,所以是 Student 和 Teacher 继承 Person 时是虚继承,加 virtual 关键字
  

int main(){Assistant a;a._name = \"peter\";a.Student::_name = \"xxx\";a.Teacher::_name = \"yyy\";return 0;}

  加了虚继承后,就只有一份 P e r s o n Person Person 成员了,共用了。既可以直接访问也可以指定类域访问。

在这里插入图片描述

  这里虽然监视窗口显示的是 3 个 Person Person Person,到那实际上他们是共用的
  


  其实库中的菱形继承也是用虚继承来解决的

template<class CharT, class Traits = std::char_traits<CharT>>class basic_ostream : virtual public std::basic_ios<CharT, Traits>{};template<class CharT, class Traits = std::char_traits<CharT>>class basic_istream : virtual public std::basic_ios<CharT, Traits>{};

  那虚继承对 Student Student Student Teacher Teacher Teacher 有什么影响吗?
  底层的角度有一些影响,用的角度没有影响。从用的角度来说 Student Student Student Teacher Teacher Teacher 就是一个单继承。 virtual virtual virtual 真正影响的是下面的 Assistant Assistant Assistant

Student Student Student Teacher Teacher Teacher 都要给虚继承,不能只给其中一个虚继承


在这里插入图片描述

  那这样算不算菱形继承呢?
  算的,菱形继承并不是看是否构成菱形,而是看某个类是否被重复继承,是否产生数据冗余和二义性

  那如果我们要加虚继承,该加在哪里呢?
   B B B C C C。因为虚继承是:谁会产生数据冗余和二义性,谁继承它时就要虚继承。在 E E E 中是 A A A 有数据冗余二义性,所以 B B B C C C 继承 A A A 时使用虚继承

  那能不能全部加上虚继承呢? D D D E E E 都加上
  不要,毕竟是药三分毒。

  总结:单继承和多继承可以用,但使用多继承时不要设计出菱形继承

  
  

7.4 多继承中的指针偏移问题

  • 下面说法中正确的是()
    A:p1 == p2 == p2    B:p1 < p2 < p3    C:p1 == p3 != p2    D:p1 != p2 != p3    E:p2 == p3 != p1
class Base1 { public: int _b1; };class Base2 { public: int _b2; };class Derive : public Base2, public Base1 { public: int _d; };int main(){Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;}

要做对这道题,我们要知道下面两个知识点:

  • 子类给给父类的对象/指针/引用,会发生切片;子类对象给子类指针,指向的是整个子类对象,子类对象给父类指针,指向的是子类对象中父类的那一部分
  • 多继承对象在内存中的声明是先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯

在这里插入图片描述

  首先, p 3 p3 p3 指向开始肯定是没问题的。对 p 2 p2 p2将子类对象给父类指针,其会指向子类中父类的那一部分 p 2 p2 p2 指向 B a s e 2 Base2 Base2,因为 B a s e 2 Base2 Base2 先继承,所以 p 2 p2 p2指向开始 p 1 p1 p1 则指向子类对象 B a s e 1 Base1 Base1 的部分, p 1 p1 p1 不可能再指向开始,它发生了偏移。这题选 E

  
  

8 组合与继承

  • p u b l i c public public 继承是一种 i s is is- a a a 的关系。也就是说每个子类对象都一个父类对象
  • 组合 是一种 h a s has has- a a a 的关系。假设 B B B 组合了 A A A每个 B 对象都一个 A 对象

  什么意思呢?我举个例子大家就明白了

//组合class Stack{public://成员函数private:vector<int> v;};//继承class Stack : public vector<int>{};

我们再来看下 i s is is- a a a h a s has has- a a a

  • 组合是一种 h a s has has- a a a 的关系:栈一个数组
  • 继承是一种 i s is is- a a a 的关系:栈一个数组

  


  • 继承允许你根据父类的实现定义子类的实现。这种通过生成子类的复用通常称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言在继承方式中,父类的内部细节对子类可见。继承一定程度破坏了父类的封装,父类的改变,对子类有很大的影响。子类和父类间的依赖关系很强,耦合度很高
  • 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象类获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合之间没有很强的依赖关系,耦合度低优先使用对象组合有助于你保持每个类的封装
      

什么是黑什么是白呢?
测试中分为黑盒测试和白盒测试

  • 黑盒测试:指看不见里面的实现,也不需要看见你的实现。比如现在有一个新的 APP,我只需要从用户用的角度去测试。黑盒测试有些地方也叫功能测试
  • 白盒测试:白盒测试要了解其底层实现,从代码运行逻辑角度进行测试。
      
    白盒测试明显比黑盒测试更难

  那是耦合度高好还是耦合度低好呢?肯定是耦合度低好

  比如现在有两个模块,模块一有100个接口函数,且全部对模块二透明,那模块二就能使用模块一的任意多个函数接口来实现自己的功能。但如果模块一今天把这个函数的参数类型改了,明天吧那个函数的参数个数给改了,因为模块二是依赖模块一的,模块二也只能跟着改。
  但如果模块一虽然有100个函数接口,但只提供5个最关键函数接口给模块二。这时,两模块之间的耦合度就大大降低,只要模块一不改那5个函数,其他95个函数随便改都不影响模块二。

  所以软件工程中提出了一个低耦合、高内聚的概念。高内聚可以认为是一个模块里面关系越紧密越好, 没关系的就拿出去

  所以两个类的关系是继承好还是组合好呢?明显是组合更好,因为继承关系下,父类的任何改动都可能会影响子类
  


  • 优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不是那么绝对,类之间的关系更适合继承( i s is is- a a a)那就用继承,另外要实现多态,也必须要继承。类之间的关系既适合用继承( i s is is- a a a)也适合组合( h a s has has- a a a),就用组合。

  
比如: Tire Tire Tire(轮胎) 和 Car Car Car(车) 更符合 has has has- a a 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; // 轮胎};

  车 has has has- a a a 轮胎是正常的,但车 is is is- a a a 轮胎就是错的

  但 Car Car Car BMW BMW BMW / Benz Benz Benz(宝马/奔驰)更符合 is is is- a a a 的关系

class BMW : public Car {public:void Drive() { cout << \"好开-操控\" << endl; }};// Car和BMW/Benz更符合is-a的关系class Benz : public Car {public:void Drive() { cout << \"好坐-舒适\" << endl; }};

  只能说宝马/奔驰 is is is- a a a 车,不能说宝马/奔驰 has has has- a a a
  

//组合class Stack{public://成员函数private:vector<int> v;};//继承class Stack : public vector<int>{};

  但既可以说:栈有一个数组,也可以说:栈是一个数组,这种情况下优先使用组合

  判断两个类型适合组合还是继承,就用 i s is is- a a a h a s has has- a a a 来判断

  
  
  
  


  好啦,本期关于继承的知识就介绍到这里啦,希望本期博客能对你有所帮助。同时,如果有错误的地方请多多指正,让我们在 C++ 的学习路上一起进步!