C++类间的 “接力棒“ 传递:继承(下)

本篇接着补充继承方面的内容,同时本篇的菱形继承尤为重要

5. 继承与友元

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);
}

友元关系不能继承,也就是说父类友元不能访问子类私有和保护成员。想要子类也能访问友元,就必须在子类也加上友元

🔥值得注意的是:

class Student; 这一前置声明是为了在 Person 类中声明 Display 函数为友元函数,此函数的参数包含 const Student& s,由于 Person 类的定义处于 Student 类之前,编译器在处理 Person 类定义时还未看到 Student 类的完整定义,所以使用前置声明来告知编译器 Student 是一个类,这样就能在 Person 类中使用 Student 类型的引用

6.继承与静态成员

class Person
{
public:
	static int _count;
	string _name;
};

int Person::_count = 0;

class Student : public Person
{
protected:
	int _stuNum;
};

int main()
{
	Person p;
	Student s;

	cout << &p._name << endl;
	cout << &s._name << endl;

	cout << &Person::_count << endl;
	cout << &Student::_count << endl;

	return 0;
}

直接说结论:父类定义的静态成员可以继承给子类,不过继承的是使用权

普通变量继承到子类的时候,其实是复制一份过去的,所以一样的变量在子类父类其实是一式两份的,但是静态成员不一样,在子类和父类里是同一份

在这里插入图片描述

7.菱形继承

我们要知道 C++ 的继承方式有几种情况:

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

菱形继承可以说是 C++ 刚出继承的时候的一个失误,Java 就没有多继承这一说法,为的就是避免菱形继承的出现

为什么菱形继承会出问题呢?

在这里插入图片描述
Assistant 继承了 TeacherStudent 本来是没什么问题的,出问题的点就在于 TeacherStudent 都继承了 Person ,这就导致 Assistant 里面会有两份 Person 的成员,导致出现数据冗余的问题

✏️假设在 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;
};

void Test()
{
	Assistant a;
	a._name = "peter";
}

这里对象 a 访问 _name 就不知道要找哪个 Person_name,这样会有二义性无法明确知道访问的是哪一个

a.Student::_name = "xxx";
a.Teacher::_name = "yyy";

因此需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决

以下是对菱形继承的详细分析及解决方案:

class A
{
public:
	int _a;
};

class B : public A
{
public:
	int _b;
};


class C : 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;
}

这是个典型的菱形继承的例子,其实这些继承的变量是连续存储的,我们可以调出监视窗口里的内存查看

在这里插入图片描述

在内存里确实是在 BC 中存在两个 _a

✏️解决方案:

class A
{
public:
	int _a;
};

class B : virtual public A
{
public:
	int _b;
};

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;
	d._a = 0;
	return 0;
}

C++规定,把 virtual 关键字加在从共同基类直接派生的类前

virtual 关键字使得在继承体系中,共同的基类在派生类对象中只存在一份共享的子对象,而不是为每个直接继承的基类都创建一份副本

同样的转到内存中查看:

在这里插入图片描述

发现 A 的内存被额外找了一块区域进行存储,变成共享的了,那 BC 中原来存储 A 的地方,根据地址找到对应的值,发现对应回原来的表,其实是与这块共享区域的偏移量

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

🔥值得注意的是:

  • 最右边的图,有具体值的旁边的 00000000 ,其实是留给 D 类型其它对象的
  • d._a = 1 这样子,按照声明顺序就可以直接找到 _a 了,不需要用到偏移量。像 B* pb = &d;pb->_a = 1;这样子,因为 D 这个子类赋值给 B 类这个父类,需要切割,B 并不是按照声明顺序这样能直接找到,所以需要依靠偏移量才能找到 B_a
  • 其实不止是 D 类,像 B 类和 C 类的内存存储方式、偏移量查找方式和 D 类是一样的

总的来说: 比如想要查找 _a 这块共享区域,就根据当前对象的虚基表指针,找到偏移量,然后回到虚基表去找到 _a 这块共享区域

8.继承和组合

  • public 继承是一种 is-a 的关系。也就是说每个派生类对象都是一个基类对象
  • 组合是一种 has-a 的关系。假设 B 组合了 A,每个 B 对象中都有一个 A 对象
  • 优先使用对象组合,而不是类继承
  • 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高
  • 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装
  • 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合
// Car和BMW Car和Benz构成is-a的关系
class Car {
protected:
	string _colour = "白色"; // 颜色
	string _num = "陕ABIT00"; // 车牌号
};

class BMW : public Car {
public:
	void Drive() { cout << "好开-操控" << endl; }
};

class Benz : public Car {
public:
	void Drive() { cout << "好坐-舒适" << endl; }
};

// Tire和Car构成has-a的关系

class Tire {
protected:
	string _brand = "Michelin";  // 品牌
	size_t _size = 17;         // 尺寸

};

class Car {
protected:
	string _colour = "白色"; // 颜色
	string _num = "陕ABIT00"; // 车牌号
	Tire _t; // 轮胎
};

希望读者们多多三连支持

小编会继续更新

你们的鼓励就是我前进的动力!

请添加图片描述

评论 72
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值