目录
五、继承和友元
5.1 友元关系不能继承
5.2 解决方法
六、继承与静态成员
6.1 继承体系中的静态成员的概念
6.2 静态成员的访问和使用
七、菱形继承及菱形虚拟继承
7.1 单继承和多继承
7.2 菱形继承
7.3 菱形继承存在的主要问题
7.4 菱形虚拟继承
八、优先使用组合,而不是继承
九、以往的笔试面试题
友元关系不能继承,即基类友元不能访问子类私有和保护成员
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 Test4()
{
Person p;
Student s;
Display(p, s);
}
在上面的例子进行编译时会报错:
“Student::_stuNum”: 无法访问 protected 成员(在“Student”类中声明)
原因:Display函数可以访问基类的成员(因为它是基类的友元函数),但是友元关系不能继承,所以它不能访问子类的成员。
如果友元函数需要访问子类成员,有什么方法吗
当然有,那就是在子类也声明该友元函数,即可实现上述功能。
此时就不会再报错
- 子类可以通过继承父类来获得其静态成员。
- 静态成员是与类本身相关联的,而不是与类的实例相关联的。
- 基类定义了static静态成员,则整个继承体系里面只有一个这样的成员,无论派生出多少个子类,都只有一个static成员实例 。
- 静态成员变量存在静态区,属于类,不属于对象,继承只是继承了使用权。
1、在定义静态成员时,需要在成员前面加上 static 关键字,表示该成员是静态成员。
class MyClass {
public:
static int _val;
};
2、静态成员不属于类的对象,因此可以在没有创建类的对象的情况下访问。
int main() {
std::cout << MyClass::_val << std::endl; // 直接访问静态成员
return 0;
}
3、静态成员的初始化需要在类外进行,可以在类外使用作用域解析运算符为静态成员赋值。(不用加上static)
int MyClass::_val = 10; // 初始化静态成员
4、当一个类被继承时,它的静态成员可以被派生类直接访问,也可以通过作用域解析运算符::来访问基类的静态成员。(前提是符合继承中的作用域)
class A {
protected:
static int x;
};
int A::x = 10;
class B : public A {
public:
void func() {
// 子类访问父类的静态成员
int y = A::x;
}
};
需要注意的是,如果在子类中定义了一个与父类同名的静态成员,那么该成员将会隐藏父类的同名静态成员,使用该成员时需要标注其所属类。(多态那一篇会详细介绍)
#include
class Base
{
public:
static int var;
};
int Base::var = 0;
class Derived : public Base
{
public:
static int var;
};
int Derived::var = 1;
int main()
{
std::cout << "Base::var = " << Base::var << std::endl; // 输出 Base::var = 0
std::cout << "Derived::var = " << Derived::var << std::endl; // 输出 Derived::var = 1
return 0;
}
继承可以分为单继承和多继承两种形式。单继承是指派生类只有一个直接基类,而多继承是指派生类有多个直接基类。在多继承中,需要小心处理可能出现的二义性问题。
菱形继承是多继承的一种特殊情况。
菱形继承是指通过多层继承关系,最终导致派生类间接继承同一个基类,形成了一个菱形的继承结构。这种继承结构可能会引发一些问题和困扰。
这也属于菱形继承:
菱形继承存在的主要问题有两个:内存浪费和二义性。
内存浪费:因为菱形继承中派生类从多个基类中继承同一个基类,导致该基类在派生类中被重复实例化。例如在菱形继承结构中,顶层基类 Base被实例化了一次,在其派生类 Derived1 中又被实例化了一次,在其派生类 Derived2 中也被实例化了一次,在最底层的派生类 Derived3 中又被实例化了一次。这样就造成了内存浪费。
二义性:因为派生类同时从不同的基类中继承来自同一个基类的成员,导致访问这些成员时存在二义性。例如在菱形继承结构中,如果派生类 Derived3 中同时继承了派生类 Derived2 和派生类 Derived1 中的成员,那么当访问这些成员时就可能会产生歧义。
class A {
public:
int _a = 5;
};
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;
std::cout << "d._a = " << d._a << std::endl; //报错
return 0;
}
调用 d 中的 _a 会出现二义性,不知道是 B 中的 _a 还是 A 中的 _a 。
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。
如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。
虚拟继承的格式:派生类继承基类时在继承方式前加上 virtual 关键字
虚拟继承解决数据冗余和二义性的原理:
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;
return 0;
}
通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。
注:
1、A初始化一次,初始化顺序:A B C D
2、初始化列表出现的顺序不是执行顺序,执行顺序由声明顺序决定。
继承里面,谁先继承就是谁先声明。
下面是Person关系菱形虚拟继承的原理解释:
菱形虚拟继承通过使用虚拟继承的方式,避免了派生类对同一基类的多次实例化,从而解决了内存浪费的问题;同时,通过使派生类只包含一个共享的虚拟基类子对象,消除了对同一成员的二义性,从而解决了二义性的问题。
1. 什么是菱形继承?菱形继承的问题是什么?
菱形继承是指一个类同时继承自两个拥有共同基类的类,从而形成了菱形的继承结构。
例如,类 D 继承自类 B 和类 C,而类 B 和类 C 都继承自类 A,这样就形成了菱形继承结构。
菱形继承的问题主要包括:
- 数据冗余:由于类 D 继承了类 B 和类 C,而类 B 和类 C 又都继承自类 A,因此类 D 中会包含两份来自类 A 的数据,造成数据冗余。
-二义性:当类 D 调用来自类 A 的方法或属性时,由于存在两份来自类 A 的数据,可能会导致二义性,不清楚到底使用哪一份数据。
2. 什么是菱形虚拟继承?如何解决数据冗余和二义性的
菱形虚拟继承是为了解决菱形继承的问题而提出的解决方案。在菱形继承中,如果使用虚拟继承,可以避免数据冗余和二义性的问题。
菱形虚拟继承的特点是,最终派生类只包含唯一一份共同基类的子对象。
使用菱形虚拟继承时,需要在中间的父类(类 B 和类 C)的继承声明前加上 `virtual` 关键字,
例如 'class B : virtual public A'。这样做可以确保最终派生类(类 D)中只包含一份来自共同基类(类 A)的数据。类 B 和类 C生成一个虚基表指针指向一个存放偏移量的虚基表,通过偏移量找到共同基类(类 A)的数据,解决了数据冗余和二义性的问题。
3. 继承和组合的区别?什么时候用继承?什么时候用组合?
继承和组合的区别:
- 继承是一种 is-a 关系,它表示一个类是另一个类的一种类型。通过继承,子类可以获得父类的属性和行为,并且可以添加新的属性和行为。通常情况下,当子类 is-a 父类的时候,可以考虑使用继承。
- 组合是一种 has-a 关系,它表示一个类包含另一个类作为其一部分。通过组合,一个类可以包含另一个类的实例作为自己的成员变量,从而利用该类的功能。通常情况下,当一个类需要使用另一个类的功能,而不是成为它的一种类型时,可以考虑使用组合。
当需要表示 is-a 关系的时候,可以考虑使用继承;当需要表示 has-a 关系的时候,可以考虑使用组合。通常情况下,推荐优先选择组合而不是继承,因为组合更加灵活,降低了类之间的耦合度。