【C++继承下】继承与友元 / static 菱形继承与虚继承 组合的详解分析

🔥个人主页:爱和冰阔乐

📚专栏传送门:《数据结构与算法》C++

🐶学习方向:C++方向学习爱好者

⭐人生格言:得知坦然 ,失之淡然


🏠博主简介

文章目录


前言

学完继承上后,本文将详细介绍多继承与多继承产生的菱形继承的问题以及通过virtual解决的虚继承,让我们来走进本文感受其中的奥妙!!!

一、实现一个不能被继承的类

1.1 C++98方法

怎么实现一个不能被子类继承的类?在了解完继承上后,我们便知道可以通过访问限定符 private 实现:父类的构造函数私有,子类的构成必须调⽤父类的构造函数,但是父类的构成函数私有化以后,子类看不⻅就不能调⽤了,那么子类就⽆法实例化出对象

但是这个方法表现得不够明显,如果不去定义子类的对象则不会报错

cpp 复制代码
class Base 
{
public:
	void func5() { cout << "Base::func5" << endl; }
protected:
	int a = 1;
private:
	// C++98的⽅法 
	Base()
	{}
};
class Derive :public Base
{
	void func4() { cout << "Derive::func4" << endl; }

};
int main()
{
  //不定义子类对象则不报错
   Base b;
	return 0;
}

1.2 C++11 final 法

以上是C++98的方法,在C++11中新增了一个 final 关键字,final修改基类,派⽣类就不能继承了

cpp 复制代码
// C++11的⽅法 
class Base final
{
public:
	void func5() { cout << "Base::func5" << endl; }
protected:
	int a = 1;
};
class Derive :public Base
{
	void func4() { cout << "Derive::func4" << endl; }

};
int main()
{

	return 0;
}

在C++11中不管有没有定义子类的对象都会报错,现象更明显

二、继承与友元

友元关系不能被继承,也就是说父类友元不能访问子类私有和保护成员,下面我们看一段代码便可以验证该结论

cpp 复制代码
//前置申明
class Student;

class Person
{
public:
	//由于Student的定义在下面,但是编译器是向上查找,我们不能将Student放在上面,因为继承关系,所以我们需要前置申明
	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;
	// 编译报错:: "Student::_stuNum": ⽆法访问 protected 成员 
	// 解决⽅案:Display也变成Student 的友元即可 
	Display(p, s);
	return 0;
}

Display是Person的友元,但不是Student的友元,即友元不可以被继承,那么我们希望友元可以被继承,那么就让Display也变成Student的友元

三、继承与静态成员

父类定义了static静态成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个子类,都只有⼀个static成员实例(即静态成员可以被继承下来,但是继承下来的变量是同一个,这与普通变量继承下来不一样)

我们只需要打印Person对象和Student对象中_name的地址和_count地址便知道静态成员变量继承下来是同一个

cpp 复制代码
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;


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


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

  //注意:只有在公有的情况下,⽗派⽣类指定类域都可以访问静态成员 
   cout << Person::_count << endl;
   cout<<s._count<<endl;
   cout << Student::_count << endl;
	return 0;
}

打印结果:

因此我们便可以得出非静态成员变量继承下来了,但是父类对象和子类对象是各一份,静态成员变量继承下来后,子类和父类共有一份

注意:只有在共有的情况下,父子类指定类域/对象都可以访问静态成员

四、多继承及其菱形继承问题

4.1 继承模型

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

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

菱形继承:菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以看出菱形继承有 数据冗余和⼆义性 的问题,在Assistant的对象中Person成员会有两份。⽀持多继承就⼀定会有菱形继承,像Java就直接不⽀持多继承,规避掉了这⾥的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的(我是个博士生,在学校我既是学生也是讲师,同样我还是一个人)

我们需要注意的是在assistant类中继承了学生(学生中有份person的信息),同时继承了老师(老师中也有份person的信息),意味着assistant中有了两份person的信息,这里就会导致数据冗余(数据浪费)和二义性(访问具有不确定性)

代码演示:

cpp 复制代码
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; // 主修课程 
};

int main()
{
	// 编译报错:error C2385: 对"_name"的访问不明确 
	Assistant a;
	a._name = "peter";
	// 需要显⽰指定访问哪个基类的成员可以解决⼆义性问题,但是数据冗余问题⽆法解决 
	a.Student::_name = "小李";
	a.Teacher::_name = "老李";
	return 0;
}

结果显示,assistant中的_name有两个,一个是从学生中继承过来,一个是从老师继承过来的(二义性),可以通过如上代码进行指定类域进行解决,

注意:多继承一定会产生菱形继承,因为其(儿子)同时继承的两个父类,不能保证两个父类(父亲)之间是否又同时继承于同一个父类(爷爷)

4.2 虚继承

在学习虚继承之前,我们需要明白是 学生 / 老师 / person 中的哪一个导致的数据冗余和二义性?

_name 是 Person 类的成员,当通过 Assistant 对象访问 _name 时(如 a._name),编译器无法确定应该访问哪条路径继承而来的 _name,因此我们可以确定是Person导致的二义性,在学生和老师中都保存了_name,产生了数据冗余

那么有没有一种方法可以解决?这里我们C++祖师爷便提出来虚继承来解决上述问题,通过在学生和老师中添加virtual关键字来实现:

cpp 复制代码
class Student : virtual public Person
class Teacher : virtual public Person

我们发现调试结果显示,不再出现二义性,并且,_name是同一份即共用(都是老李),而不再是前面显示的是两份(一个是老李,一个小李)

思考下,在下面这个继承关系中,应该在哪加virtual?

我们需要知道,在哪加virtual是看谁会产生如丧问题,便在继承它的子类中加,在 E中,显而易见,A会有两份,那么肯定是在继承A的子类中加,即B和C中加

总结:我们可以设计出多继承,但是不建议设计出菱形继承,有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂,性能也会有⼀些损失,所以最好不要设计出菱形继承。多继承可

以认为是C++的缺陷之⼀,因此不到万不得已不要设计出菱形继承!!!

4.3 指针偏移问题

下面说法正确的是:C

cpp 复制代码
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };

int main()
{
  Derive d;
  Base1* p1 = &d;
  Base2* p2 = &d;
  Derive* p3 = &d;

   return 0;
}

分析:底层是Derive多继承于Base1和Base2,那么在创建Derive对象 d 时,其底存放布局是先父类(先继承的在前)再是子类

p3:p3 是指向整个 Derive 对象的指针,它存储的地址是整个对象的起始地址,也就是 0x1000(和 Base1 部分的起始地址一致)

p1:p1 是指向 Base1 类型的指针,当它指向 Derive 对象 d 时,它只能 "看到" d 中属于 Base1 的部分(即 _b1)

p2:p2 是指向 Base2 类型的指针,它只能 "看到" d 中属于 Base2 的部分(即 _b2)。由于 Base2 的部分在 d 中排在 Base1 之后,它的起始地址是 Base1 部分的地址加上 Base1 的大小(4 字节),也就是 0x1000 + 4 = 0x1004

总结:因此p1等于p3不等于p2

五、继承和组合

  1. public继承是⼀种is-a的关系。也就是说每个派⽣类对象都是⼀个基类对象(每个stack对象都是一个list对象)
  2. 组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。(每一个stack对象中都有一个list对象)
cpp 复制代码
//继承
class stack:public list
{
}

//组合
class stack
{
  list lt;
}
  1. 继承(访问限定为公有和保护)允许你根据父类的实现来定义子类的实现。这种通过⽣成子类的复⽤通常被称为⽩箱复⽤(white-box reuse)。术语"⽩箱"是相对可视性⽽⾔:在继承⽅式中,父类的内部细节对子类可⻅。继承⼀定程度破坏了父类的封装,父类的改变,对子类有很⼤的影响。子类和父类间的依赖关系很强,耦合度⾼
  2. 对象组合是类继承之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接⼝。这种复⽤⻛格被称为⿊箱复⽤(black-box reuse),因为对象的内部细节(访问限定为私有)是不可⻅的。对象只以"⿊箱"的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使⽤对象组合有助于你保持每个类被封装

什么是黑盒和白盒?

黑盒就是看不到底层的实现,可以理解为封装起来,如写的游戏类代码发给别人玩(.exe为后缀的),看不到底层,那么白盒就是可以看到底层
黑盒测试:不了解底层的实现,从功能角度测试

白盒测试:要了解底层实现,从代码运行逻辑角度测试

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

总结

坚持到这里,已经很棒啦,希望读完本文可以帮读者大大更好了解继承!!!如果喜欢本文的可以给博主点点免费的攒攒,你们的支持就是我前进的动力🎆

资源分享:继承源码

相关推荐
草莓熊Lotso6 小时前
《C++ Stack 与 Queue 完全使用指南:基础操作 + 经典场景 + 实战习题》
开发语言·c++·算法
敲上瘾6 小时前
单序列和双序列问题——动态规划
c++·算法·动态规划
ajassi20006 小时前
开源 C++ QT QML 开发(二十二)多媒体--ffmpeg编码和录像
c++·qt·开源
Simon_He8 小时前
最强流式渲染,没有之一
前端·面试·ai编程
小糖学代码9 小时前
Linux:11.线程概念与控制
linux·服务器·c语言·开发语言·c++
Larry_Yanan12 小时前
QML学习笔记(四十)QML的ApplicationWindow和StackView
c++·笔记·qt·学习·ui
Kratzdisteln14 小时前
【C语言】Dev-C++如何编译C语言程序?从安装到运行一步到位
c语言·c++
怪兽201415 小时前
什么是 Redis?
java·数据库·redis·缓存·面试
Dream it possible!15 小时前
LeetCode 面试经典 150_栈_有效的括号(52_20_C++_简单)(栈+哈希表)
c++·leetcode·面试··哈希表