【c++】“谁想继承我的花呗-.-“继承的学习(下)

简介: 【c++】“谁想继承我的花呗-.-“继承的学习(下)

4.继承与友元


我们先说结论:友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。

class Student;
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;
}
void main()
{
  Person p;
  Student s;
  Display(p, s);
}


对于上面的继承关系,person有一个友元函数,这个函数可以访问父类的成员变量,当子类继承父类后,我们用这个函数是会报错的,因为友元关系是不会被继承的:

e996abd31f7d4eb2bf9e10d0ecbc8d99.png

那么该如何解决这种情况呢?我们只需要在子类中也声明一下友元关系即可:

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

d103cc290b4a41fc8b5864ce79b8baa3.png


5.继承与静态成员


基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。

 

class Person
{
public:
  Person() { ++_count; }
protected:
  string _name; // 姓名
public:
  static int _count; // 统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
protected:
  int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
  string _seminarCourse; // 研究科目
};
void TestPerson()
{
  Student s1;
  Student s2;
  Student s3;
  Graduate s4;
  cout << " 人数 :" << Person::_count << endl;
  Student::_count = 0;
  cout << " 人数 :" << Person::_count << endl;
}
int main()
{
  TestPerson();
  return 0;
}


f7650b41f999478aac0736793538b4dc.png


我们可以看到,静态成员变量是所以对象所共有的,无论继承多少次,都只有一个count。下面我们验证一下:

69c9e88a52a64634a32e9182b1fab5da.png

通过上图可以看到父类中的name和子类中的name根本不是一个name,那么count呢?

1af8720f4bce47608f334b7e6743258e.png

通过验证我们也能发现静态成员变量确实只有一个,不管继承了多少次。

下面我们实现一个不能被继承的类:

class A
{
private:
  A()
  {
  }
};
class B :public A
{
};
int main()
{
  B bb;
  return 0;
}


一旦我们将构造函数私有化了那么就不能继承了。


611ecf07b1694205ac05e29447872a10.png


这样的情况是因为我们不想我们的类被继承,那我们本类自己如何调用这个私有化的构造函数呢?

class A
{
public:
  static A CreateObj()
  {
    return A();
  }
private:
  A()
  {
  }
};
class B :public A
{
};
int main()
{
  //B bb;
  A::CreateObj();
  return 0;
}


我们可以实现一个函数让这个函数返回一个A类型的匿名对象即可,由于函数必须由对象去调用我们无法创建一个对象,所以我们将这个函数设为static静态的函数,这样就可以用类名去调用这个函数了。


6.复杂的菱形继承与菱形虚拟继承

我们先看看单继承和多继承的区别:

5336fc07a8c9441995e4a7866f2733cc.png6cea3755f1834affb8d79b4acf169677.png

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

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

菱形继承:菱形继承是多继承的一种特殊情况

097c49180f1548ee99091c6c87601b25.png

菱形继承有数据冗余和二义性的问题,现在我们通过代码调试来观察:

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; // 主修课程
};
void Test()
{
  // 这样会有二义性无法明确知道访问的是哪一个
  Assistant a;
  a._name = "peter";
  // 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
  a.Student::_name = "xxx";
  a.Teacher::_name = "yyy";
}



首先在上面的代码中类assistant继承了student和teacher,所以我们创建了assistant对象后发现访问成员变量name无法访问,编译器报错不明确,如下图:

14474c768ebd42ce8946504f99ef479d.png


这个问题解决起来很简单,就是将我们的变量前面加上域作用限定符,然后指定访问的是哪一个name变量,如下图:

0fd3b1f4a5c5443a8fa4e3276fc67ea2.png


那么数据冗余是什么呢?当一个子类继承两个父类的时候,这两个父类同样是继承另一个父类的,这样就会有很多相同的数据被继承到了子类中,一旦在项目中代码非常多非常复杂那么这种情况是非常浪费空间的,那么这个问题c++的祖师爷是如何解决的呢?这里就引入了虚继承,虚继承可以解决数据冗余和二义性的问题:

0c37c111f55145a79dbf1b4a235408d7.png0715ab5e3c244e86987807620a0f18fc.png

虚继承的语法就是在原先继承方式的前面加上virtual关键字。下面我们通过一个简单的菱形继承模型来看看c++祖师爷是如何解决数据冗余和二义性的问题:

class A
{
public:
  int _a;
};
// class B : public A
class B : virtual public A
{
public:
  int _b;
};
// class C : public A
class C : virtual public A
{
public:
  int _c;
};
class D : public B, public C
{
public:
  int _d;
};
int main()
{
  D d;
  d.B::_a = 1;
  d.C::_a = 2;
  d._b = 3;
  d._c = 4;
  d._d = 5;
  return 0;
}


82c9e20cc5a0448183ba9cce7c95084b.png


我们先看一下不加virtual继承的情况:

909f1b84e2384d5f85046dae0bdfaed9.pngc4c511c1aa5d4200aba51211024e3a8a.png


通过上图我们可以看到B中继承的a在内存第一行第二行就是B中的成员b,C中继承的a在内存中第三行,最后一个红色的是d的值。那么我们再看看虚继承的结果:

38031ec45f9d45698f4e62bd703c8eae.png


我们发现虚继承中的内存地址和我们刚刚看到的完全不一样,虚继承里面存放的竟然是指针,B类中有一个指针和3这个值,C类中有一个指针和4这个值,那么这能代表什么呢?我们再开一个内存来看看刚刚里面存放的地址到底是什么东西:

b968af23a51046beaf61415312b4d63c.png我们发现这个地址里面的开头都是0,这是什么意思呢?

f5df7df12516469ca8c6e69593517abc.png

我们通过那个0下面的值发现,16进制转化为十进制分别是20和12,然后我们试着在第一个地址加上这个值发现,上面的地址加上这个值正好指向了A:

ab1a9bfe55e54eea9b21f71f03c29ac2.png

也就是说虚继承解决数据冗余和二义性的本质是通过偏移量找到A让其这三个类中的a变量都指向父类的那个变量的地址。

总结:

这里是通过了BC的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A

 

下面我们做一道多继承的题:

e6175d68647f465993ab62bb929afc53.png

对于上面这道题选哪个选项呢?我们来解释一下:


首先D类指针p开了一个D的空间,然后进入D的构造函数,在D的构造函数中我们发现初始化列表对三个类都进行了初始化,而初始化的顺序是谁先继承谁就先初始化,A先被B继承所以先去调用A的构造函数初始化,随后打印class A,接着B类对象初始化,在B类初始化列表中我们发现又初始化一次A,那么A会成功初始化吗?答案是不会因为我们虚继承了,三份A只用初始化一次A,所以这次直接打印class B,然后初始化C,C与B同理打印class C,走完D的初始化列表后进入构造函数打印class D所以答案选A。最重要的是要知道虚继承后只有一份A走了构造函数。


下面我们再看一下组合和继承的区别:

4e398f5b5edb4eb6961775af93637b5e.png


组合的耦合度低,将类C改了类D不会受很大的影响,而继承一旦A改了B继承的很多成员都会随之改变。


总结



1. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。

2. 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。

3. 继承和组合

public继承是一种 is-a的关系。也就是说每个派生类对象都是一个基类对象。

组合是一种 has-a的关系。假设B组合了A,每个B对象中都有一个A对象。

优先使用对象组合,而不是类继承 。

继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称

为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的

内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很

大的影响。派生类和基类间的依赖关系很强,耦合度高。

对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象

来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复

用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。

组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被

封装。

实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有

些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用

继承,可以用组合,就用组合。



目录
相关文章
|
3月前
|
算法 C语言 C++
C++语言学习指南:从新手到高手,一文带你领略系统编程的巅峰技艺!
【8月更文挑战第22天】C++由Bjarne Stroustrup于1985年创立,凭借卓越性能与灵活性,在系统编程、游戏开发等领域占据重要地位。它继承了C语言的高效性,并引入面向对象编程,使代码更模块化易管理。C++支持基本语法如变量声明与控制结构;通过`iostream`库实现输入输出;利用类与对象实现面向对象编程;提供模板增强代码复用性;具备异常处理机制确保程序健壮性;C++11引入现代化特性简化编程;标准模板库(STL)支持高效编程;多线程支持利用多核优势。虽然学习曲线陡峭,但掌握后可开启高性能编程大门。随着新标准如C++20的发展,C++持续演进,提供更多开发可能性。
75 0
|
14天前
|
编译器 C语言 C++
配置C++的学习环境
【10月更文挑战第18天】如果想要学习C++语言,那就需要配置必要的环境和相关的软件,才可以帮助自己更好的掌握语法知识。 一、本地环境设置 如果您想要设置 C++ 语言环境,您需要确保电脑上有以下两款可用的软件,文本编辑器和 C++ 编译器。 二、文本编辑器 通过编辑器创建的文件通常称为源文件,源文件包含程序源代码。 C++ 程序的源文件通常使用扩展名 .cpp、.cp 或 .c。 在开始编程之前,请确保您有一个文本编辑器,且有足够的经验来编写一个计算机程序,然后把它保存在一个文件中,编译并执行它。 Visual Studio Code:虽然它是一个通用的文本编辑器,但它有很多插
|
1月前
|
安全 程序员 编译器
【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则
【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则
77 11
|
1月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
51 1
|
1月前
|
C++
C++番外篇——虚拟继承解决数据冗余和二义性的原理
C++番外篇——虚拟继承解决数据冗余和二义性的原理
39 1
|
1月前
|
Java 编译器 C++
c++学习,和友元函数
本文讨论了C++中的友元函数、继承规则、运算符重载以及内存管理的重要性,并提到了指针在C++中的强大功能和使用时需要注意的问题。
17 1
|
28天前
|
安全 编译器 程序员
C++的忠实粉丝-继承的热情(1)
C++的忠实粉丝-继承的热情(1)
16 0
|
1月前
|
编译器 C++
C++入门11——详解C++继承(菱形继承与虚拟继承)-2
C++入门11——详解C++继承(菱形继承与虚拟继承)-2
27 0
|
1月前
|
程序员 C++
C++入门11——详解C++继承(菱形继承与虚拟继承)-1
C++入门11——详解C++继承(菱形继承与虚拟继承)-1
32 0
|
2月前
|
C++
C++(二十)继承
本文介绍了C++中的继承特性,包括公有、保护和私有继承,并解释了虚继承的作用。通过示例展示了派生类如何从基类继承属性和方法,并保持自身的独特性。此外,还详细说明了派生类构造函数的语法格式及构造顺序,提供了具体的代码示例帮助理解。