
🔥个人主页:爱和冰阔乐
🐶学习方向:C++方向学习爱好者
⭐人生格言:得知坦然 ,失之淡然

🏠博主简介
文章目录
- 前言
- 一、实现一个不能被继承的类
-
- [1.1 C++98方法](#1.1 C++98方法)
- [1.2 C++11 final 法](#1.2 C++11 final 法)
- 二、继承与友元
- 三、继承与静态成员
- 四、多继承及其菱形继承问题
-
- [4.1 继承模型](#4.1 继承模型)
- [4.2 虚继承](#4.2 虚继承)
- [4.3 指针偏移问题](#4.3 指针偏移问题)
- 五、继承和组合
- 总结
前言
学完继承上后,本文将详细介绍多继承与多继承产生的菱形继承的问题以及通过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
五、继承和组合
- public继承是⼀种is-a的关系。也就是说每个派⽣类对象都是⼀个基类对象(每个stack对象都是一个list对象)
- 组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。(每一个stack对象中都有一个list对象)
cpp
//继承
class stack:public list
{
}
//组合
class stack
{
list lt;
}
- 继承(访问限定为公有和保护)允许你根据父类的实现来定义子类的实现。这种通过⽣成子类的复⽤通常被称为⽩箱复⽤(white-box reuse)。术语"⽩箱"是相对可视性⽽⾔:在继承⽅式中,父类的内部细节对子类可⻅。继承⼀定程度破坏了父类的封装,父类的改变,对子类有很⼤的影响。子类和父类间的依赖关系很强,耦合度⾼
- 对象组合是类继承之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接⼝。这种复⽤⻛格被称为⿊箱复⽤(black-box reuse),因为对象的内部细节(访问限定为私有)是不可⻅的。对象只以"⿊箱"的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使⽤对象组合有助于你保持每个类被封装
什么是黑盒和白盒?
黑盒就是看不到底层的实现,可以理解为封装起来,如写的游戏类代码发给别人玩(.exe为后缀的),看不到底层,那么白盒就是可以看到底层
黑盒测试:不了解底层的实现,从功能角度测试白盒测试:要了解底层实现,从代码运行逻辑角度测试
总结:优先使⽤组合,⽽不是继承。实际尽量多去⽤组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is-a)那就⽤继承,另外要实现多态,也必须要继承。类之间的关系既适合⽤继承(is-a)也适合组合(has-a),就⽤组合
总结
坚持到这里,已经很棒啦,希望读完本文可以帮读者大大更好了解继承!!!如果喜欢本文的可以给博主点点免费的攒攒,你们的支持就是我前进的动力🎆
资源分享:继承源码