目录
一.继承与友元
二.继承与静态成员函数
三.多继承及其菱形继承问题
4.1 继承模型
4.2虚继承
虚继承下的构造:
4.3多继承中指针偏移问题
四.继承与组合
一.继承与友元
友元关系不能继承,也就是说基类友元不能访问派⽣类私有和保护成员 。
注意:这里编译器有个报错不准的地方,一般像这种报错缺逗号就是没有写前置声明,这里友元声明的形参里有Student,Student在下面,编译器只会向上查找,但上面没有写Student的声明,所以需要前置声明一下编译才能通过
但是可以看到即使加了Student的声明,编译cout << s._stuNum << endl;依旧会报错,这就是友元关系不能继承的原因,基类友元是不能访问派生类私有和保护成员,第一个cout << p._name << endl;编译没报错说明Display成为Person的友元已经生效
要解决这个问题在Student中也加一个友元声明就可以,让Display也成为Student的友元
二.继承与静态成员函数
基类定义了static静态成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个派⽣类,都 只有⼀个static成员实例。
静态成员属于类,不属于对象,也不属于继承体系里的某一个实例静态成员函数归所属类,并不依附 this 指针;继承只是让派生类可以访问、复用这份静态成员,不会多复制一份静态数据,父子共用同一份静态变量。
出现这样的原因是因为_count(静态成员)不是放在对象里面的,而是放在静态区中的;同时继承下来_name是两份,父类和子类互不干扰
三.多继承及其菱形继承问题
4.1 继承模型
单继承:一个派生类只有一个直接基类时称这个继承关系为单继承
多继承:一个派生类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前面,后面继承的基类在后面,派生类成员放到最后面
菱形继承:菱形继承是多继承的一种特殊情况。菱形继承的问题,从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题,在Assistant的对象中Person成员会有两份。支持多继承就一定会有菱形继承,像Java就直接不支持多继承,规避掉了这里的问题,所以事件中也不建议设计初菱形继承这样的模型
class Person { public: string _name; }; class Student : public Person { protected: int _num; }; class Teacher : public Person { protected: int _card; }; // 多继承中间用逗号隔开 class Assistant : public Student, public Teacher { protected: string _majorcourse; }; int main() { // 编译报错:error C2385 :对“_name”的访问不明确 // 编译器不知道走Student这条路径去访问还是Teacher这条路径去访问,产生二义性 Assistant a; //a._name = "peter"; // 需要显⽰指定访问哪个基类的成员可以解决二义性问题,但是数据冗余问题⽆法解决 a.Student::_name = "xxx"; a.Teacher::_name = "yyy"; return 0;如何去解决这种问题呢?
4.2虚继承
让公共基类A 只在最终子类 D 中只保留唯一一份实例,B、C 不再各自内嵌独立 A,而是共享同一个虚基类 A。
这里我们可以看出加上虚继承之后,只有一份_name,不管怎么调用都会改变
virtual放在中间层派生类继承「公共基类」的语句中,public / protected 前面。
在底层的角度简单来说,图中的Person就不会再放到Student和Teacher当中了,一般情况会放在Assistant a;对象头部或尾部,很多情况都会放在尾部(在一个公共区域放一份)
但是归根结底来说,还是不要使用多继承的好,有的兄弟就会说了,既然virtual这么好用,那我每次出现菱形继承的时候加上这个关键字不就好了。有句话说得好,是药三分毒,virtual就是这里的药。virtual会影响底层的存储模型,用的多了之后底层空间就会很乱,虽然编译上可以通过,但是之后会对空间的使用更加难以处理,而且换一个角度来说,Java直接舍弃的语法在C++上会是什么好用的东西吗
虚继承下的构造:
虚基类由最终最底层派生类负责初始化,中间层级的构造函数里对虚基类的初始化列表全部无效、不会执行; 全局保证:虚基类在整个继承体系中只会构造一次。
情况 1:虚基类有无参默认构造
- 最终子类 D 不手动写
A(xxx),编译器自动调用A(); - B、C 里写不写 A 的初始化都无所谓,不会生效。
情况 2:虚基类只有带参构造、无默认构造
必须在最终派生类 D 的初始化列表显式调用 A 的带参构造,否则编译报错; 中间 B、C 无论怎么写都救不了
4.3多继承中指针偏移问题
要想做出这道题,首先我们要明白的是多继承的内存分布:
继承顺序:先
Base1,再Base2,最后自身成员_d。 对象内存排布:
Base1子对象(内含_b1)→ 起始地址 =&dBase2子对象(内含_b2)→ 地址往后偏移一个int大小- 成员
_d→ 再往后偏移一个int
三个指针指向分析:
p3 = &d:Derive*指针,直接指向整个d对象起始地址。p1 = &d:向上转型为Base1*,指向d内部Base1子对象,也就是整个对象的起始地址,因此p1 == p3。p2 = &d:向上转型为Base2*,指向d内部Base2子对象,地址比对象起始位置大一个int,和p1、p3地址不相等。
四.继承与组合
1. 继承:is-a是一种关系
语法:class 子类 : 继承方式 父类
语义:子类是一种父类
// 父类:汽车(Car)—— 定义所有汽车都有的属性和功能 class Car { public: string _colour; // 颜色(所有汽车都有) int _num; // 轮子数(所有汽车都有) // 所有汽车都能开 void Drive() { cout << "汽车能开" << endl; } }; // 子类:宝马(BMW)—— 继承Car,因为宝马是一种汽车 class BMW : public Car { public: // 宝马可以有自己独有的功能(比如自动驾驶) void AutoDrive() { cout << "宝马能自动驾驶" << endl; } }; // 测试代码 int main() { BMW myCar; // 🔥 继承的核心:子类能直接用父类的成员(不用自己定义) myCar._colour = "白色"; // 用父类的_colour myCar.Drive(); // 用父类的Drive() myCar.AutoDrive(); // 用自己独有的AutoDrive() return 0; }底层:子类复用父类代码,会把父类完整子对象嵌入自身内存,支持多态、虚函数重写。
继承的特点(优缺点)
✅ 优点:复用代码超方便
父类写一次的属性 / 功能,所有子类都能直接用,不用重复写(比如所有汽车都有颜色,不用给宝马、奔驰各写一遍_colour)。
❌ 缺点:耦合度极高(父子绑死)
- 耦合度高 = “父类变,子类必受影响”;
- 比如你把父类Car的_colour改名为_color,那么BMW、Benz等所有子类里用_colour的地方都会报错,必须跟着改;
- 而且子类能看到父类的内部细节(比如protected成员),破坏了父类的 “封装性”(就像你能看到别人的隐私)。
2. 组合:has-a拥有关系
语法:类内部包含另一个类的成员变量
语义:A 里面包含一个 B,A 拥有 B
// 零件类:轮胎(Tire)—— 独立的类 class Tire { private: // 🔥 组合的核心:零件的内部细节可以隐藏(private) string _brand; // 轮胎品牌(对外不可见) public: // 零件对外提供的接口(只能通过接口用,看不到内部) void SetBrand(string brand) { _brand = brand; } void Roll() { cout << _brand << "轮胎滚动" << endl; } }; // 整体类:汽车(Car)—— 组合Tire(包含4个轮胎) class Car { private: // 🔥 组合的核心:在类里定义另一个类的对象(包含关系) Tire _tires[4]; // 一辆车有4个轮胎 string _colour; public: // 汽车的功能:通过轮胎的接口使用轮胎(看不到轮胎的内部) void SetTireBrand(int index, string brand) { _tires[index].SetBrand(brand); // 用轮胎的接口SetBrand() } void Drive() { _tires[0].Roll(); // 用轮胎的接口Roll() cout << _colour << "汽车开起来了" << endl; } }; // 测试代码 int main() { Car myCar; myCar._colour = "黑色"; myCar.SetTireBrand(0, "米其林"); // 给第0个轮胎设品牌 myCar.Drive(); // 输出:米其林轮胎滚动 → 黑色汽车开起来了 return 0; }底层:把其他类的实例当作自身成员,单纯复用功能,不存在父子层级。
组合的特点(优缺点)
✅ 优点:耦合度极低(零件和整体解耦)
- 耦合度低 = “零件变,整体不一定变”;
- 比如你把Tire的_brand改名为_tireBrand,只要Tire的接口(SetBrand()、Roll())不变,Car完全不用改;
- 而且Car看不到Tire的内部细节(_brand是 private),保持了封装性(就像你用手机,不用知道手机内部的芯片怎么工作)。
❌ 缺点:复用代码稍麻烦
如果想复用Tire的功能,必须先创建Tire对象,再通过接口调用(不能像继承那样直接用)。
三、总结:怎么选?
- 先看关系:
如果是 “is-a”(A 是 B 的一种)→ 用继承(比如宝马是汽车、学生是人);
如果是 “has-a”(A 包含 B)→ 用组合(比如汽车有轮胎、栈有 vector);
- 优先选组合:
哪怕既像 “is-a” 又像 “has-a”(比如栈和 vector),也优先用组合(耦合低、更安全);
- 只有一种情况必须用继承:
需要实现多态(不同子类有不同行为)时,只能用继承(组合做不到)。