【C++】继承(2):继承与友元,静态成员,多继承黑/白盒复用
目录
一继承与友元
二 继承与静态成员
三 多继承及其菱形继承问题
1 继承模型
2 菱形继承
3 虚继承
4 io库中的菱形继承
四 继承与组合
1 继承和组合
2 示例
3 白盒复用&&黑盒复用
白盒复用(White-box Reuse)
黑盒复用(Black-box Reuse)
五 实现一个不能被继承的类
1 方法
2 例题
前文回顾:【C++】继承(1):深入理解和使用
一继承与友元
友元关系不能继承,也就是说基类友元不能访问派⽣类私有和保护成员
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;
}
int main()
{Person p;Student s;// 编译报错:error C2248: “Student::_stuNum”: ⽆法访问 protected 成员 // 解决⽅案:Display也变成Student 的友元即可 Display(p, s);return 0;
}
注意:这里class student 的前置声明不能省略,不然会报错
解决方案:在class student 中也加入友元函数display
二 继承与静态成员
基类定义了static静态成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个派⽣类,都 只有⼀个static成员实例。
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;// 这⾥的运⾏结果可以看到⾮静态成员_name的地址是不⼀样的 // 说明派⽣类继承下来了,⽗派⽣类对象各有⼀份 cout << &p._name << endl;cout << &s._name << endl;// 这⾥的运⾏结果可以看到静态成员_count的地址是⼀样的 // 说明派⽣类和基类共⽤同⼀份静态成员 cout << &p._count << endl;cout << &s._count << endl;// 公有的情况下,⽗派⽣类指定类域都可以访问静态成员 cout << Person::_count << endl;cout << Student::_count << endl;return 0;
}
非静态成员:非静态成员的继承是父类和子类各一份,地址不一样。
三 多继承及其菱形继承问题
1 继承模型
单继承:一个派生类只有一个直接基类时称这个继承关系为单继承
多继承:一个派生类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前面,后面继承的基类在后面,派生类成员再放到最后面。


示例:

但其实多继承是有问题的,是一个大坑!!!
2 菱形继承
菱形继承:菱形继承是多继承的一种特殊情况。菱形继承的问题,从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题,在Assistant的对象中Person成员会有两份。支持多继承就一定会有菱形继承,像Java就直接不支持多继承,规避掉了这里的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。
多继承不是问题,多继承实现的菱形继承才是问题


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 = "xxx";a.Teacher::_name = "yyy";return 0;
}
我们可以设计出多继承,但是不建议设计出菱形继承,因为菱形虚拟继承以后,⽆论是使⽤还是底层 都会复杂很多。当然有多继承语法⽀持,就⼀定存在会设计出菱形继承,像Java是不⽀持多继承的, 就避开了菱形继承。
不要去玩菱形继承!!!
3 虚继承
虚继承的关键字:virtual virtual是为了解决菱形继承二叉性的问题

很多⼈说C++语法复杂,其实多继承就是⼀个体现。有了多继承,就存在菱形继承,有了菱形继承就有 菱形虚拟继承,底层实现就很复杂,性能也会有⼀些损失,所以最好不要设计出菱形继承。多继承可 以认为是C++的缺陷之⼀,后来的⼀些编程语⾔都没有多继承,如Java。
virtual要放在腰部的位置:
// 使⽤虚继承Person类
class Student : virtual public Person
{
protected:int _num; //学号
};
但是virtual不能多使用:virtual会影响底层的模型,到处都用就会导致底层一团乱

虚继承的底层太过于复杂,我们不讲解
4 io库中的菱形继承
但是在库里面,有实现菱形继承



四 继承与组合
1 继承和组合
继承和组合都是复用
1、public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。是一个
2、组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。有一个
3、继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-boxreuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对派生类可见。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
4、对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-boxreuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
5、优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is-a)那就用继承,另外要实现多态,也必须要继承。类之间的关系既适合用继承(is-a)也适合组合(has-a),就用组合。

软件设计是需要尽量低耦合,高内聚

优先使用组合:因为组合的关联度低(也就是低耦合),高内聚
低耦合的本质:一个地方有改动,不太会影响其他的地方
高耦合就是牵一发而动全身
2 示例
// Tire(轮胎)和Car(⻋)更符合has-a的关系
class Tire {
protected:string _brand = "Michelin"; // 品牌 size_t _size = 17; // 尺⼨
};class Car {
protected:string _colour = "⽩⾊"; // 颜⾊ string _num = "陕ABIT00"; // ⻋牌号 Tire _t1; // 轮胎 Tire _t2; // 轮胎 Tire _t3; // 轮胎 Tire _t4; // 轮胎
};class BMW : public Car {
public:void Drive() { cout << "好开-操控" << endl; }
};// Car和BMW/Benz更符合is-a的关系
class Benz : public Car {
public:void Drive() { cout << "好坐-舒适" << endl; }
};
template<class T>
class vector
{};// stack和vector的关系,既符合is-a,也符合has-a
template<class T>
class stack : public vector<T>
{};template<class T>
class stack
{
public:vector<T> _v;
};int main()
{return 0;
}
3 白盒复用&&黑盒复用
白盒复用(White-box Reuse)
定义:通过继承实现的复用,派生类可以直接访问基类的内部成员(如 protected 成员),相当于 “打开盒子” 看到并使用内部实现。
优点:
- 复用效率高:派生类可以直接继承基类的成员和方法,无需重新实现,减少代码量。
- 便于扩展:派生类可以通过重写基类方法,灵活修改或扩展基类的行为。
- 紧密的逻辑关联:适用于基类与派生类存在明确 “is-a” 关系的场景(如 “学生是一种人”),逻辑清晰。
缺点:
- 耦合度高:派生类与基类强绑定,基类的内部实现修改可能直接影响派生类,破坏封装性。
- 灵活性差:继承关系是静态的,编译时确定,运行时无法动态改变继承的基类。
- 可维护性低:过度使用继承会导致类层次复杂(如菱形继承问题),难以理解和维护。
黑盒复用(Black-box Reuse)
定义:通过组合(或聚合)实现的复用,一个类将另一个类的对象作为成员,仅通过其公开接口访问,无需了解内部实现,相当于 “关闭盒子” 只使用外部功能。
优点:
- 低耦合:被复用的类(成员对象)的内部实现对使用者透明,修改其内部不影响使用者,封装性好。
- 灵活性高:可以在运行时动态替换成员对象(通过接口多态),适应不同场景。
- 避免继承缺陷:无需处理复杂的类层次,规避菱形继承的数据冗余和二义性问题。
- 可维护性强:类之间职责清晰,单个类的修改影响范围小。
缺点:
- 代码量可能增加:需要手动封装成员对象的接口(如转发方法),不像继承那样自动获得基类方法。
- 接口依赖:如果成员对象的接口变化,使用者可能需要调整,依赖于接口的稳定性。
- 不适用于 “is-a” 关系:更适合 “has-a” 关系(如 “汽车有发动机”),若强行用于 “is-a” 场景,逻辑会不自然。
五 实现一个不能被继承的类
1 方法
方法1:C++98方法:基类的构造函数私有,派生类的构造必须调用基类的构造函数,但基类的构造函数私有化后,派生类无法访问,也就不能调用,那么派生类就无法实例化出对象。
方法2:C++11新增了一个final关键字,用final修改基类,派生类就不能继承了。
// C++11的⽅法
class Base final
{
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; }
protected:int b = 2;
};
int main()
{Base b;Derive d;return 0;
}
2 例题
这是一个多继承的问题

p3指向的是整个对象 Base1在Base2前面是因为,先继承的在前面,后继承的在后面
