C++ - 继承【下】
一、继承与友元
友元关系不能继承,也就是说基类友元不能访问派生类私有和保护成员 。
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;
}
二、继承与静态成员
基类定义了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;
}
三、多继承及其菱形继承问题
3.1 继承模型
单继承:一个派生类只有一个 直接 基类时称这个继承关系为单继承
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;
}
这段代码展示了多继承中的菱形继承问题:
Assistant
类同时继承了Student
和Teacher
Student
和Teacher
又都继承自Person
类- 这导致
Assistant
对象中会有两份Person
类的成员(包括_name
),造成数据冗余- 直接访问
_name
会产生二义性,需要通过类域指定访问哪个基类的成员解决这种问题的通常方案是使用虚继承(在继承时添加
virtual
关键字 ),使Person
成为虚基类,确保派生类中只保留一份Person
的成员。
3.2 虚继承
class Person
{
public:string _name; // 姓名/*int _tel;int _age;string _gender;string _address;*/// ...
};// 使用虚继承Person类
class Student : virtual public Person
{
protected:int _num; //学号
};// 使用虚继承Person类
class Teacher : virtual public Person
{
protected:int _id; // 职工编号
};// 教授助理
class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};int main()
{// 使用虚继承,可以解决数据冗余和二义性Assistant a;a._name = "peter";return 0;
}
这段代码通过虚继承(
virtual public
)解决了之前的菱形继承问题:
Student
和Teacher
类都使用虚继承的方式继承Person
类- 这使得
Assistant
类在多重继承时,只会保留一份Person
类的成员- 因此可以直接访问
_name
而不会产生二义性,同时也解决了数据冗余问题
3.3 题目
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;
}
C
3.4 IO库中的菱形虚拟继承
template<class CharT, class Traits = std::char_traits<CharT>>
class basic_ostream : virtual public std::basic_ios<CharT, Traits>
{};template<class CharT, class Traits = std::char_traits<CharT>>
class basic_istream : virtual public std::basic_ios<CharT, Traits>
{};
这段代码展示了 C++ 标准库中输入输出流的部分继承关系:
basic_ostream
(基础输出流类)和basic_istream
(基础输入流类)都通过虚继承的方式继承自basic_ios
类- 虚继承在这里的作用是为了避免多重继承时可能出现的菱形继承问题,特别是当
basic_iostream
(同时支持输入和输出的流类)继承这两个类时 - 模板参数
CharT
表示字符类型,Traits
表示字符特性,默认使用std::char_traits<CharT>
这种设计是 C++ 标准库中解决菱形继承问题的典型应用。
四、继承和组合
对比维度 | 继承(is-a) | 组合(has-a) |
---|---|---|
关系本质 | 派生类是一种基类 | 一个类包含另一个类的对象 |
复用方式 | 白箱复用(基类内部可见) | 黑箱复用(仅通过接口访问) |
耦合度 | 高(基类改,派生类可能要改) | 低(彼此独立,靠接口通信) |
适用场景 | is-a 关系、多态 | has-a 关系、降低耦合 |
1. 继承:is-a(“是” 的关系)
也就是说每个派生类对象都是一个基类对象 。
继承表示 “派生类是一种基类”。比如:
- 狗是一种动物(狗 → 动物)
- 学生是一种人(学生 → 人)
这意味着派生类拥有基类的所有特征和行为,还能在此基础上增加自己的特点。
就像 “狗” 继承了 “动物” 的 “会呼吸、会动” 等属性,同时又新增了 “会汪汪叫” 的特性。
2. 组合:has-a(“有” 的关系)
组合表示 “一个类包含另一个类的对象”。比如:
- 汽车有发动机(汽车 → 包含 → 发动机)
- 电脑有 CPU(电脑 → 包含 → CPU)
这里两个类是 “整体 - 部分” 的关系,整体由部分组成,但部分不属于整体的 “一种”。
比如汽车不会说 “我是一种发动机”,而是 “我包含一个发动机”。
3. 白箱复用 vs 黑箱复用:封装性的区别
这两种关系带来的代码复用方式完全不同,核心差异在 “封装性” 上。
1. 继承:白箱复用(看得见内部)
继承时,派生类可以直接访问基类的 protected 成员(半公开的内部细节),就像 “透明的箱子”—— 基类的内部结构对派生类是可见的。
优点:派生类能直接复用基类的实现,甚至重写基类的方法(比如狗可以重写动物 “叫” 的方法)。
缺点:破坏基类封装,耦合度高。如果基类改了内部实现(比如动物类加了一个新的成员变量),派生类很可能也要跟着改。2. 组合:黑箱复用(看不见内部)
组合时,一个类只通过另一个类的 public 接口来使用它,完全看不到内部实现,就像 “不透明的黑箱子”。
优点:被组合的类封装得很好,彼此依赖弱(耦合度低)。比如汽车和发动机,只要发动机的接口(比如 “启动”“停止” 方法)不变,发动机内部怎么改(比如换成电动的),汽车类都不用动。
缺点:如果需要复用的功能比较复杂,可能需要写更多代码来组合多个类。
4)什么时候用继承?什么时候用组合
优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is-a)那就⽤继承,另外要实现多态,也必须要继承。类之间的关系既适合⽤继承(is-a)也适合组合(has-a),就用组合。
// 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;
}