初识C++ · 继承(2)

目录

前言:

1 继承与友元

2 继承与静态成员

3 菱形继承和菱形虚拟继承

4 继承的总结和反思


前言:

继上文将介绍了继承的概念和定义,以及基类子类的互相赋值,作用域,和默认成员函数的使用,本文介绍最后一点内容:友元,静态成员,以及菱形继承和菱形虚拟继承。


1 继承与友元

继承与友元,即探讨基类的友元和子类是否互通,这里是比较好理解的,基类的友元和子类并不互通,就像你父亲的朋友是你的朋友吗?显然不是,所以调用函数的时候,就调用不了:

class B;

class A
{
public:
	friend void Func(const A& pa, const B& pb);
protected:
	int _a = 1;
};

class B :public A
{
//public:
protected:
	int _b = 2;
};
void Func(const A& pa,const B& pb)
{
	cout << pa._a << endl;
	//cout << pb._b << endl;
}

int main()
{
	A a;
	B b;
	Func(a,b);
	return 0;
}

基类的友元函数不能访问派生类的成员,所以在Func里面我们要访问pb的成员变量就不能通过基类的友元函数来实现,有一个解决办法是在派生类里面加上同样的友元函数声明。

这里也个要注意的地方就是,B是继承A的,所以要先声明一下B,不然函数找不到关于B的定义。


2 继承与静态成员

这里的静态成员介绍与类和对象的介绍是差不多的,在类和对象的时候我们通过静态对象来判断创建了多少个对象,这里其实一样的,看起来静态成员在每个对象实例化的时候都要创建一份,实际上不是,静态成员只有一个:

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 << &(s1._count) << " " << &(s2._count) << endl;
	cout << " 人数 :" << Person::_count << endl;
	Student::_count = 0;
	cout << " 人数 :" << Person::_count << endl;
}

继承的时候同样,static成员还是只有一个,我们就可以把它当作计数的工具,因为走派生类的构造的时候,也会有基类的构造,所以我们在基类的构造里面count++ ,就可以达到计数的目的,不当我们打印出来count的地址,就会发现是一样的,这也证明了count只有一个,因为count是在静态区的,严格来说不在类里面,也可以证明只有一个。


3 菱形继承和菱形虚拟继承

这里其实就是单继承和多继承,单继承是指每个派生类只有一个基类,多继承就是每个子类有多个基类,这其实没有好争议的,比如,黄瓜既是水果也是蔬菜,这就是一种多继承,那么什么是菱形继承呢?

这种就是一种菱形继承,当然,菱形只是一种形式名字,只要是满足这种继承关系都叫做菱形继承,即D继承了B C ,但是B C都继承了A,那么D中有多少份A呢?这个意义就是菱形继承。当时本贾尼博士当了时代的冲锋者,没有人搞这个,本贾尼博士就搞了一个,但是奈何没有经验,就有点坑。

一共有两个坑,数据冗余和二义性


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继承的两个类都继承了Person类,那么我们要访问a的_name的时候就需要知道_name是谁的_name,是teacher的还是Student的?这个时候就需要指定访问了,这个时候可以解决二义性的问题,但是数据冗余的问题还是无法解决。

此时只需要:

class Student : virtual public Person
{
protected:
	int _num; //学号
};
class Teacher : virtual public Person
{
protected:
	int _id; // 职工编号
};

将两个基类的继承方式改为虚拟继承即可改变问题,这时候我们可以访问a._name了,那么当我们用监视窗口调试的时候:

可以看到a._name修改的时候,基类的也都跟着修改了。

说明只有一份Person的数据,这也解决了数据冗余和二义性的问题,但是底层的角度,虚拟继承是怎么样解决菱形继承的问题的呢?

我们就需要通过内存的角度去看了。那么为了方便这里使用几个简单的类去观看:

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._a = 0;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

我们先看普通继承方式-> 通过内存窗口去观察:

这个地址是d的地址,经过两条语句,有两个内存发生变化,这是普通继承下,多份a的原因,所以_a修改两次,就会有两次变化:

那么这是整体的变化,其中58 60是两份不同的a,那么我们再通过虚拟继承看一下,此时代码稍作调整:

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._a = 0;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

 开始调试:

经过了三条a值改变的语句,可以发现同一个地址的值发生了改变,所以这里就是虚拟继承的作为,原来通过监视窗口看的时候,我们不免会以为可能有三份a的值,但是通过内存,我们现在就知道了通过虚拟继承,a只有一份了。

d._b = 3,这个语句执行之后,60部分的内存发生改变,有一个03没错,但是上面的遗传数字是什么意思呢?

不难发现,04上面也有一串数字,这里我们不卖关子了,这串数字是指针的意思,当我们追踪这个指针的时候:

可以看到指针下面有一个28,这其实是偏移量。两个指针被叫做虚基表指针,存储偏移量的表被叫做虚基表,偏移量的作用就是在修改a的时候好找到a,我们不难发现,该指针加上虚基表中的偏移量就可以找到a的地址,这就是祖师爷对菱形继承的解决方法,当然也可以直接存偏移量,但是祖师爷没这样做,我们就顺祖师爷的做法来做就行。

记住了,虚继承是在腰部的位置进行虚拟继承的,也有两个新概念叫做虚基表和虚基表指针。


4 继承的总结和反思

在effective C++中提到,如果面临选择继承还是组合的时候,请优先选择组合,组合就是这样:

class M
{
public:
	A _a;
};

也就是成员变量是其他自定义类型的。

这方面可以概括为,必要继承就继承,必要组合就组合,组合和继承都可以就组合。

这是因为组合的低耦合高内聚,继承的高耦合低内聚。

继承方面来讲,基类和派生类的联系是很紧密的,基类一旦修改保不准派生类就会进行大部分的修改,这就是高耦合,联系是很紧密的,但是从组合方面来说,自定义类的修改对于别的类来说没有影响,因为只是用其中的几个成员变量或者是函数,所以组合的低耦合就是联系并不紧密,所以修改代码的时候不会影响。

那么高内聚就是成员变量之间的联系大,也就是说成员变量基本上都用得到,不存在浪费的情况,有的时候继承,不管三七二十一全都继承了,我不一定用得到,所以对于代码的维护方面来讲的话,组合对于代码的维护是比较容易的,继承就没那么容易了,毕竟关系是十分紧密的,不好修改。

这是关于继承的大部分介绍。


感谢阅读!

相关推荐

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-18 20:58:02       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-18 20:58:02       71 阅读
  3. 在Django里面运行非项目文件

    2024-07-18 20:58:02       58 阅读
  4. Python语言-面向对象

    2024-07-18 20:58:02       69 阅读

热门阅读

  1. 网站流量统计分析工具之plausible.io

    2024-07-18 20:58:02       22 阅读
  2. 设计模式--享元模式

    2024-07-18 20:58:02       21 阅读
  3. ReferenceEquals

    2024-07-18 20:58:02       24 阅读
  4. 2024国家护网面试小结

    2024-07-18 20:58:02       21 阅读
  5. 数据结构第28节 字典树

    2024-07-18 20:58:02       20 阅读
  6. 详解深度学习中的epochs

    2024-07-18 20:58:02       23 阅读
  7. 梧桐数据库: 数据库技术中的重写子查询技术

    2024-07-18 20:58:02       22 阅读
  8. ubuntu 可以直接在图像界面打开命令行吗

    2024-07-18 20:58:02       18 阅读