跳到主要内容C++ 继承机制详解(下) | 极客日志C++
C++ 继承机制详解(下)
本文讲解 C++ 继承机制的下半部分。内容包括继承与友元的关系(友元不可继承)、静态成员在继承体系中的共享特性、多继承模型及内存布局、菱形继承产生的数据冗余与二义性问题及其虚继承解决方案、多继承中的指针偏移现象。此外还对比了组合与继承的区别,强调组合是 has-a 关系,继承是 is-a 关系,并指出优先使用组合以降低耦合度,同时保留继承用于多态场景。
FrontendX1 浏览 C++ 继承机制详解(下)
5 继承与友元
友元关系不能被继承。即一个函数是父类的友元函数,但不是子类的友元函数。也就是说父类的友元不能访问子类的私有和保护成员。
using namespace std;
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;
Display(p, s);
return 0;
}
这里出现了许多报错,通常是因为类型定义顺序问题。编译器遇到类型时向上查找,Student 在 Person 之后定义,导致编译错误。可以在 Person 前加上 Student 的前置声明:
class Student;
解决后,剩下的报错涉及友元函数。Display() 是 Person 的友元,但友元关系不能继承,因此 Display() 不是 Student 的友元。解决方法是在 Student 中加一个友元声明:
class Student;
class {
:
;
:
string _name;
};
: Person {
:
;
:
_stuNum;
};
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown 转 HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
- HTML 转 Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online
Person
public
friend void Display(const Person& p, const Student& s)
protected
class
Student
public
public
friend void Display(const Person& p, const Student& s)
protected
int
void Display(const Person& p, const Student& s)
6 继承与静态成员
父类定义了 static 静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个 static 成员实例。而普通成员,假设父类有一个 _name,子类继承下来也有另一个 _name,但是他们两个 _name 不是同一个,各自是各自的。
using namespace std;
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._name << endl;
cout << &s._name << endl;
cout << &p._count << endl;
cout << &s._count << endl;
cout << ++Person::_count << endl;
cout << ++Student::_count << endl;
cout << ++p._count << endl;
cout << ++s._count << endl;
return 0;
}
虽然静态变量可以通过对象访问,但一般不这么做,大多数都是直接指定类域去访问。
7 多继承
7.1 继承模型
- 单继承:一个子类只有一个直接父类时,称为这个继承关系为单继承。
- 多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承。多继承对象在内存中的模型是,先继承的父类在前面,后继承的父类在后面,子类成员放在最后面。
- 菱形继承:菱形继承是多继承的一种特殊情况。从下面的对象成员模型构造可以看出,菱形继承有数据冗余和二义性的问题,在 Assistant 的对象中 Person 成员会有两份。支持多继承就一定会有菱形继承,像 Java 就直接不支持多继承,规避掉了这里的问题,所以实践中我们是不建议设计出菱形继承这样的继承模型的。
7.2 菱形继承的问题
菱形继承是很坑的,有数据冗余(浪费空间)和二义性(不知访问哪个)的问题,现实中不想被打就不要设计出菱形继承(多继承是没问题的,不要搞出菱形继承就行)。
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;
};
上述就是菱形继承,Person 成员在 Assistant 对象中有两份。我们试着访问 Person 的成员 _name:
int main() {
Assistant a;
a._name = "peter";
return 0;
}
我们需要指定访问那个父类成员的成员可以解决二义性的问题,但是数据冗余问题无法解决:
int main() {
Assistant a;
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
return 0;
}
虽然解决了二义性,但数据冗余问题无法解决,Person 有两份。
不仅如此,因为数据冗余,导致菱形继承对象的大小特别大:
int main() {
Assistant a;
cout << sizeof(Assistant) << endl;
return 0;
}
那么现实中有没有人设计出菱形继承呢?还真有,我们简单看一下。
7.3 虚继承
为了解决菱形继承的问题,C++ 引入了虚继承的概念,新增关键字:virtual。
class Person {
public:
string _name;
};
class Student : virtual public Person {
protected:
int _num;
};
class Teacher : virtual public Person {
protected:
int _id;
};
class Assistant : public Student, public Teacher {
protected:
string _majorCourse;
};
注意:因为是 Person 有数据冗余和二义性,所以是 Student 和 Teacher 继承 Person 时是虚继承,加 virtual 关键字。
int main() {
Assistant a;
a._name = "peter";
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
return 0;
}
加了虚继承后,就只有一份 Person 成员了,共用了。既可以直接访问也可以指定类域访问。
这里虽然监视窗口显示的是 3 个 Person,到那实际上他们是共用的。
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> {};
那虚继承对 Student 和 Teacher 有什么影响吗?底层的角度有一些影响,用的角度没有影响。从用的角度来说 Student 和 Teacher 就是一个单继承。virtual 真正影响的是下面的 Assistant。
注:Student 和 Teacher 都要给虚继承,不能只给其中一个虚继承。
那这样算不算菱形继承呢?算的,菱形继承并不是看是否构成菱形,而是看某个类是否被重复继承,是否产生数据冗余和二义性。
那如果我们要加虚继承,该加在哪里呢?B 和 C。因为虚继承是:谁会产生数据冗余和二义性,谁继承它时就要虚继承。在 E 中是 A 有数据冗余二义性,所以 B、C 继承 A 时使用虚继承。
那能不能全部加上虚继承呢?D 和 E 都加上。不要,毕竟是药三分毒。
总结:单继承和多继承可以用,但使用多继承时不要设计出菱形继承。
7.4 多继承中的指针偏移问题
下面说法中正确的是()
A:p1 == p2 == p2
B:p1 < p2 < p3
C:p1 == p3 != p2
D:p1 != p2 != p3
E:p2 == p3 != p1
class Base1 {
public:
int _b1;
};
class Base2 {
public:
int _b2;
};
class Derive : public Base2, public Base1 {
public:
int _d;
};
int main() {
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
要做对这道题,我们要知道下面两个知识点:子类给给父类的对象/指针/引用,会发生切片;子类对象给子类指针,指向的是整个子类对象,子类对象给父类指针,指向的是子类对象中父类的那一部分。多继承对象在内存中的声明是先继承的基类在前面,后面继承的基类在后面,派生类成员在放到最后面。
首先,p3 指向开始肯定是没问题的。对 p2:将子类对象给父类指针,其会指向子类中父类的那一部分,p2 指向 Base2,因为 Base2 先继承,所以 p2 也指向开始。p1 则指向子类对象 Base1 的部分,p1 不可能再指向开始,它发生了偏移。这题选 E。
8 组合与继承
- public 继承是一种 is-a 的关系。也就是说每个子类对象都是一个父类对象。
- 组合是一种 has-a 的关系。假设 B 组合了 A,每个 B 对象都有一个 A 对象。
class Stack {
public:
private:
vector<int> v;
};
class Stack : public vector<int> {};
我们再来看下 is-a 与 has-a:组合是一种 has-a 的关系:栈有一个数组。继承是一种 is-a 的关系:栈是一个数组。
- 继承允许你根据父类的实现定义子类的实现。这种通过生成子类的复用通常称为白箱复用(white-box reuse)。术语'白箱'是相对可视性而言:在继承方式中,父类的内部细节对子类可见。继承一定程度破坏了父类的封装,父类的改变,对子类有很大的影响。子类和父类间的依赖关系很强,耦合度很高。
- 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象类获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以'黑箱'的形式出现。组合之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类的封装。
什么是黑什么是白呢?测试中分为黑盒测试和白盒测试。黑盒测试:指看不见里面的实现,也不需要看见你的实现。比如现在有一个新的 APP,我只需要从用户用的角度去测试。黑盒测试有些地方也叫功能测试。白盒测试:白盒测试要了解其底层实现,从代码运行逻辑角度进行测试。白盒测试明显比黑盒测试更难。
那是耦合度高好还是耦合度低好呢?肯定是耦合度低好。
比如现在有两个模块,模块一有 100 个接口函数,且全部对模块二透明,那模块二就能使用模块一的任意多个函数接口来实现自己的功能。但如果模块一今天把这个函数的参数类型改了,明天吧那个函数的参数个数给改了,因为模块二是依赖模块一的,模块二也只能跟着改。
但如果模块一虽然有 100 个函数接口,但只提供 5 个最关键函数接口给模块二。这时,两模块之间的耦合度就大大降低,只要模块一不改那 5 个函数,其他 95 个函数随便改都不影响模块二。
所以软件工程中提出了一个低耦合、高内聚的概念。高内聚可以认为是一个模块里面关系越紧密越好,没关系的就拿出去。
所以两个类的关系是继承好还是组合好呢?明显是组合更好,因为继承关系下,父类的任何改动都可能会影响子类。
- 优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不是那么绝对,类之间的关系更适合继承(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;
};
车 has-a 轮胎是正常的,但车 is-a 轮胎就是错的。
但 Car 和 BMW/Benz(宝马/奔驰)更符合 is-a 的关系:
class BMW : public Car {
public:
void Drive() {
cout << "好开 - 操控" << endl;
}
};
class Benz : public Car {
public:
void Drive() {
cout << "好坐 - 舒适" << endl;
}
};
只能说宝马/奔驰 is-a 车,不能说宝马/奔驰 has-a 车。
class Stack {
public:
private:
vector<int> v;
};
class Stack : public vector<int> {};
但既可以说:栈有一个数组,也可以说:栈是一个数组,这种情况下优先使用组合。
判断两个类型适合组合还是继承,就用 is-a 和 has-a 来判断。