本篇接着补充继承方面的内容,同时本篇的菱形继承尤为重要
5. 继承与友元
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;
}
void main()
{
Person p;
Student s;
Display(p, s);
}
友元关系不能继承,也就是说父类友元不能访问子类私有和保护成员。想要子类也能访问友元,就必须在子类也加上友元
🔥值得注意的是:
class Student;
这一前置声明是为了在 Person
类中声明 Display
函数为友元函数,此函数的参数包含 const Student& s
,由于 Person
类的定义处于 Student
类之前,编译器在处理 Person
类定义时还未看到 Student
类的完整定义,所以使用前置声明来告知编译器 Student
是一个类,这样就能在 Person
类中使用 Student
类型的引用
6.继承与静态成员
class Person
{
public:
static int _count;
string _name;
};
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 << &Person::_count << endl;
cout << &Student::_count << endl;
return 0;
}
直接说结论:父类定义的静态成员可以继承给子类,不过继承的是使用权
普通变量继承到子类的时候,其实是复制一份过去的,所以一样的变量在子类父类其实是一式两份的,但是静态成员不一样,在子类和父类里是同一份
7.菱形继承
我们要知道 C++
的继承方式有几种情况:
🚩单继承:一个子类只有一个直接父类时称这个继承关系为单继承
🚩多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
🚩菱形继承:菱形继承是多继承的一种特殊情况
菱形继承可以说是 C++
刚出继承的时候的一个失误,Java
就没有多继承这一说法,为的就是避免菱形继承的出现
为什么菱形继承会出问题呢?
Assistant
继承了 Teacher
和 Student
本来是没什么问题的,出问题的点就在于 Teacher
和 Student
都继承了 Person
,这就导致 Assistant
里面会有两份 Person
的成员,导致出现数据冗余的问题
✏️假设在 Assistant
中访问 Person
的成员:
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;
};
void Test()
{
Assistant a;
a._name = "peter";
}
这里对象 a
访问 _name
就不知道要找哪个 Person
的 _name
,这样会有二义性无法明确知道访问的是哪一个
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
因此需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
以下是对菱形继承的详细分析及解决方案:
class A
{
public:
int _a;
};
class B : public A
{
public:
int _b;
};
class C : public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
这是个典型的菱形继承的例子,其实这些继承的变量是连续存储的,我们可以调出监视窗口里的内存查看
在内存里确实是在 B
和 C
中存在两个 _a
✏️解决方案:
class A
{
public:
int _a;
};
class B : virtual public A
{
public:
int _b;
};
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
d._a = 0;
return 0;
}
C++规定,把 virtual
关键字加在从共同基类直接派生的类前
virtual
关键字使得在继承体系中,共同的基类在派生类对象中只存在一份共享的子对象
,而不是为每个直接继承的基类都创建一份副本
同样的转到内存中查看:
发现 A
的内存被额外找了一块区域进行存储,变成共享的了,那 B
和 C
中原来存储 A
的地方,根据地址找到对应的值,发现对应回原来的表,其实是与这块共享区域的偏移量
这里是通过了 B
和 C
的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表,虚基表就是寻找基类偏移量的表。虚基表中存的偏移量,通过偏移量可以找到 A
🔥值得注意的是:
- 最右边的图,有具体值的旁边的
00000000
,其实是留给D
类型其它对象的 - 像
d._a = 1
这样子,按照声明顺序就可以直接找到_a
了,不需要用到偏移量。像B* pb = &d;pb->_a = 1;
这样子,因为D
这个子类赋值给B
类这个父类,需要切割,B
并不是按照声明顺序这样能直接找到,所以需要依靠偏移量才能找到B
的_a
- 其实不止是
D
类,像B
类和C
类的内存存储方式、偏移量查找方式和D
类是一样的
总的来说: 比如想要查找 _a
这块共享区域,就根据当前对象的虚基表指针,找到偏移量,然后回到虚基表去找到 _a
这块共享区域
8.继承和组合
public
继承是一种is-a
的关系。也就是说每个派生类对象都是一个基类对象- 组合是一种
has-a
的关系。假设B
组合了A
,每个B
对象中都有一个A
对象 - 优先使用对象组合,而不是类继承
- 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(
white-box reuse
)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高 - 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(
black-box reuse
),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装 - 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合
// Car和BMW Car和Benz构成is-a的关系
class Car {
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
};
class BMW : public Car {
public:
void Drive() { cout << "好开-操控" << endl; }
};
class Benz : public Car {
public:
void Drive() { cout << "好坐-舒适" << endl; }
};
// Tire和Car构成has-a的关系
class Tire {
protected:
string _brand = "Michelin"; // 品牌
size_t _size = 17; // 尺寸
};
class Car {
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
Tire _t; // 轮胎
};