【C++】继承(二)

目录

五、继承和友元

5.1 友元关系不能继承

5.2 解决方法

六、继承与静态成员

6.1 继承体系中的静态成员的概念

6.2 静态成员的访问和使用

七、菱形继承及菱形虚拟继承

7.1 单继承和多继承

7.2 菱形继承

7.3 菱形继承存在的主要问题

7.4 菱形虚拟继承

八、优先使用组合,而不是继承

九、以往的笔试面试题


五、继承和友元

5.1 友元关系不能继承

友元关系不能继承,即基类友元不能访问子类私有和保护成员

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函数可以访问基类的成员(因为它是基类的友元函数),但是友元关系不能继承,所以它不能访问子类的成员。

5.2 解决方法

如果友元函数需要访问子类成员,有什么方法吗

当然有,那就是在子类也声明该友元函数,即可实现上述功能。

【C++】继承(二)_第1张图片

 此时就不会再报错


六、继承与静态成员

6.1 继承体系中的静态成员的概念

  1. 子类可以通过继承父类来获得其静态成员
  2. 静态成员是与类本身相关联的,而不是与类的实例相关联的。
  3. 基类定义了static静态成员,则整个继承体系里面只有一个这样的成员,无论派生出多少个子类,都只有一个static成员实例 。
  4. 静态成员变量存在静态区,属于类,不属于对象,继承只是继承了使用权。

6.2 静态成员的访问和使用

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;
}

七、菱形继承及菱形虚拟继承

7.1 单继承和多继承

继承可以分为单继承和多继承两种形式。单继承是指派生类只有一个直接基类,而多继承是指派生类有多个直接基类。在多继承中,需要小心处理可能出现的二义性问题。

【C++】继承(二)_第2张图片

【C++】继承(二)_第3张图片

7.2 菱形继承

菱形继承是多继承的一种特殊情况。

菱形继承是指通过多层继承关系,最终导致派生类间接继承同一个基类,形成了一个菱形的继承结构。这种继承结构可能会引发一些问题和困扰。

【C++】继承(二)_第4张图片

这也属于菱形继承:

【C++】继承(二)_第5张图片

7.3 菱形继承存在的主要问题

菱形继承存在的主要问题有两个:内存浪费二义性。

内存浪费:因为菱形继承中派生类从多个基类中继承同一个基类,导致该基类在派生类中被重复实例化。例如在菱形继承结构中,顶层基类 Base被实例化了一次,在其派生类 Derived1 中又被实例化了一次,在其派生类 Derived2 中也被实例化了一次,在最底层的派生类 Derived3 中又被实例化了一次。这样就造成了内存浪费。

【C++】继承(二)_第6张图片

二义性:因为派生类同时从不同的基类中继承来自同一个基类的成员,导致访问这些成员时存在二义性。例如在菱形继承结构中,如果派生类 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 。

7.4 菱形虚拟继承

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。

如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。

虚拟继承的格式:派生类继承基类时在继承方式前加上 virtual 关键字

【C++】继承(二)_第7张图片

虚拟继承解决数据冗余和二义性的原理:

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。

【C++】继承(二)_第8张图片注:
        1、A初始化一次,初始化顺序:A  B  C  D
        2、初始化列表出现的顺序不是执行顺序,执行顺序由声明顺序决定。
              继承里面,谁先继承就是谁先声明。

下面是Person关系菱形虚拟继承的原理解释: 

【C++】继承(二)_第9张图片

菱形虚拟继承通过使用虚拟继承的方式,避免了派生类对同一基类的多次实例化,从而解决了内存浪费的问题;同时,通过使派生类只包含一个共享的虚拟基类子对象,消除了对同一成员的二义性,从而解决了二义性的问题。


八、优先使用组合,而不是继承

  • public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
  • 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
  • 优先使用对象组合,而不是类继承 。
  • 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 (父类的公有和保护对子类是透明的)。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响派生类和基类间的依赖关系很强,耦合度高。
  • 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的(组合只能用组合对象的公有成员)。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
  • 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。

九、以往的笔试面试题

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 关系的时候,可以考虑使用组合。通常情况下,推荐优先选择组合而不是继承,因为组合更加灵活,降低了类之间的耦合度。

你可能感兴趣的:(C++,c++)