【c++】继承详解(菱形 / 虚拟继承)

文章目录

  • 继承的概念 && 定义
    • 概念
    • 定义
      • 定义格式:
      • 访问限定符 && 继承关系
      • 总结:
  • 基类派生类 赋值转换
  • 继承中的作用域
  • 派生类的默认成员函数
  • 继承与友元
  • 继承 与 静态函数
  • 菱形继承 与 菱形虚拟继承
    • 单继承
    • 多继承
    • 菱形继承
    • 虚拟继承

继承的概念 && 定义

概念

继承 (inheritance) 机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称 派生类

继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。

继承的主要功能如下:

  1. 代码复用 子类可以继承父类的属性和方法,避免重复编写相同的代码。

  2. 继承属性和行为 子类可以获得父类的属性和方法,从而具备相同的行为和特性。

  3. 扩展功能 子类可以在继承基础上添加新的属性和方法,以实现特定的功能需求。

  4. 代码组织和层次化 通过继承,可以将类组织成层次结构,便于理解和管理。


定义

定义格式:

【c++】继承详解(菱形 / 虚拟继承)_第1张图片


访问限定符 && 继承关系

其中,访问限定符:用于定义类中成员(属性和方法)的可访问性。

分为以下几类:

  1. 公有(public)公有成员可以在任何位置被访问,包括类的内部和外部。
  2. 私有(private)私有成员只能在所属类的内部被访问,对外部是不可见的。
  3. 受保护(protected)受保护成员可以在所属类的内部、子类中和同一包中被访问,对外部是不可见的。
  4. 默认(package-private)默认成员没有显式的访问修饰符,只能在同一包中被访问,对外部和子类是不可见的。

继承方式 :指的是一个类从另一个类继承属性和方法时的行为。

  1. 公有继承(public inheritance)子类从父类公有成员继承时,这些成员在子类中的访问权限保持不变。公有成员在子类中仍然是公有的。
  2. 私有继承(private inheritance)子类从父类私有成员继承时,这些成员在子类中会变为私有成员,对外部和子类都不可见。
  3. 受保护继承(protected inheritance)子类从父类受保护成员继承时,这些成员在子类中会变为受保护成员,对外部是不可见的,但在子类中可以访问。

可以理解为下图

类成员 / 继承方式 public继承 protected继承 private继承
基类的public成员 派生类的public成员 派生类的protected成员 派生类的private成员
基类的protected成员 派生类的protected成员 派生类的protected成员 派生类的private成员
基类的private成员 派生类中不可见 派生类中不可见 派生类中不可见

总结:

  1. 基类private成员在派生类中都是完全不可见的(不论继承方式)。不可见是指基类的私有成员被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
    2. 基类private成员在派生类中不能被访问,如果基类成员不想在类外直接被访问,但需要在 派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
  2. 由上面的表格可以看出:基类的私有成员在子类都是不可见 。基类的其他成员在子类的访问方式 == min(成员在基类的访问限定符,继承方式),public > protected > private
  3. 使用关键字 class时默认的继承方式private,使用struct时默认的继承方式public,但最好显式的写出继承方式。
  4. 在实际运用中一般使用public继承,很少使用也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都 只能在派生类的类 里面使用,扩展维护性不强。

基类派生类 赋值转换

  1. 派生类对象 可以赋值给 基类的对象 / 指针 / 引用
  2. 基类对象不能赋值给派生类对象。
  3. 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。

继承中的作用域

  1. 在继承体系中基类和派生类都有 独立的作用域
  2. 子类和父类中有同名成员子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏 ,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  4. 实际编写代码中,继承体系里面最好不要定义同名成员
// Student的_num和Person的_num构成隐藏关系
// 可以看出这样代码虽然能跑,但是非常容易混淆
class Person
{
protected :
	string _name = "李田所"; // 姓名
	int _num = 1145141919810; // 身份证号
};

class Student : public Person
{
public:
	void Print()
	{
		cout << " 姓名:" << _name << endl;
		cout << " 身份证号:" << Person::_num << endl;
		cout << " 学号:" << _num << endl;
	}
protected:
	int _num = 123456; // 学号
};

void Test()
{
	Student s1;
	s1.Print();
};

// B中的fun和A中的fun不是构成重载,因为不是在同一作用域
// B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
class A
{
public:
	void fun()
	{
	cout << "func()" << endl;
	}
};

class B : public A
{
public:
	void fun(int i)
	{
		A::fun();
		cout << "func(int i)->" <<i<<endl;
	}
};

void Test()
{
	B b;
	b.fun(10);
};

派生类的默认成员函数

  1. 派生类的构造函数必须调用基类的构造函数初始化基类 的那部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造 完成基类的拷贝初始化
  3. 派生类的operator=必须要调用基类的operator=完成基类的复制
  4. 派生类的析构函数 会在被调用完成后自动调用 基类的析构函数 清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
  5. 派生类对象初始化 先调用基类构造再调派生类构造。
  6. 派生类对象析构清理 先调用派生类析构再调基类的析构。
  7. 在后续一些场景中析构函数需要构成重写,重写的条件之一是函数名相同。编译器会对析构函数名进行特殊处理,处理destrutor(),父类析构函数不加 virtual 的情况下,子类析构函数和父类析构函数构成隐藏关系。

继承与友元

基类友元不能被继承。,尽管派生类可以访问基类的公有和保护成员,但对于基类中的友元函数,派生类并没有同样的访问权限。

class Base {
protected:
    int num;
public:
    Base(int n) : num(n) {}

    friend void Display(const Base& obj); // 友元函数
};

class Derived : public Base {
public:
    Derived(int n) : Base(n) {}
};

void Display(const Base& obj) {
    std::cout << "Number: " << obj.num << std::endl;
}

int m ain() {
    Derived d(42);
    Display(d); // 尝试在派生类中调用友元函数
    return 0;
}

在上面的代码中,我们尝试通过 Display(d)在派生类 Derived中调用友元函数。然而,这会导致编译错误,提示找不到匹配的函数。因为 友元函数 Display 不会被派生类继承它只能访问声明它为友元的类的私有成员


继承 与 静态函数

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。

因为 静态成员属于类而不是类的实例,它在内存中只有一个副本。无论派生多少个子类,这些子类都共享同一个静态成员实例。

class Base {
public:
    static int count;
};

int Base::count = 0; // 初始化静态成员

class Derived : public Base {
public:
    Derived() {
        count++; // 在派生类中访问和修改静态成员
    }
};

int main() {
    Derived d1;
    Derived d2;
    Derived d3;

    std::cout << "Count: " << Base::count << std::endl; // 输出静态成员的值
    return 0;
}

在上面的代码中:由于静态成员在整个继承体系中只有一个副本,因此输出的结果将是所有派生类实例共享的 count 的值。


菱形继承 与 菱形虚拟继承

单继承

单继承 一个子类只有一个直接父类 时称这个继承关系为单继承。

【c++】继承详解(菱形 / 虚拟继承)_第2张图片


多继承

多继承 一个子类有两个或以上直接父类 时称这个继承关系为多继承。

【c++】继承详解(菱形 / 虚拟继承)_第3张图片


菱形继承

菱形继承 一个子类有两个或以上直接父类 时称这个继承关系为多继承。

【c++】继承详解(菱形 / 虚拟继承)_第4张图片

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有 数据冗余 二义性 的问题。 在Assistant的对象中Person成员会有两份。

【c++】继承详解(菱形 / 虚拟继承)_第5张图片

将上面的模型图 实现为代码:

class Person
{
public:
	string _name; // 姓名
};

class Student : public Person
{
protected:
	int _stu_id; //学号
};

class Teacher : public Person
{
protected:
	int _emp_id; // 职工编号
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

对于上面的类,当我们使用

Assistant a ;
a._name = "阿杰"; // _name不明确,编译错误

这种访问方式的话,程序会因为二义性(_name不明确),导致编译错误。

我们可以指定 该成员变量的父类解决该问题:

a.Student::_name = "阿伟";
a.Teacher::_name = "淑慧";

但依然没有解决数据冗余的问题,为了解决该问题,我们引用虚拟继承的概念:


虚拟继承

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

如上面的继承关系,在 StudentTeacher 的继承 Person 时使用虚拟继承,即可解决问题。另外需要注意:虚拟继承不要在其他地方去使用。

class Person
{
public :
	string _name ; // 姓名
};

class Student : virtual public Person
{
protected :
	int _stu_num ; //学号
};

class Teacher : virtual public Person
{
protected :
	int _e_id ; // 职工编号
};

class Assistant : public Student, public Teacher
{
protected :
	string _majorCourse; // 主修课程
};

void Test ()
{
	Assistant a;
	a._name = "阿伟";
}

上面的代码执行后,不会出错,程序会将Assistant a的_name 变量 改变。


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

虚拟继承的原理是通过 在派生类对象中只保留一份共享基类子对象 的方式来消除二义性和数据冗余

具体来说,虚拟继承使得虚基类子对象被单独存储,并通过指针访问,而不是嵌入到每个派生类对象中,因此不会在每个派生类之间产生冗余数据。

当多个类从同一个虚基类派生时,它们共享同一个虚基类子对象。这个虚基类子对象只被构造一次,而不是被每个派生类分别构造,因此可以消除数据冗余和二义性。

虚拟继承中的 虚基类子对象的构造顺序 比较特殊,需要按照 “最远派生类优先”(Most Derived Class First)的顺序进行构造,也就是从最后一个派生类开始构造虚基类子对象,依次向上构造

需要注意的是,虚拟继承有一些性能上的开销,因为每次访问虚基类时需要间接寻址。此外,虚拟继承可能会导致代码的可读性降低,因为虚基类的存在并不直观。因此,在使用虚拟继承时需要权衡其优缺点,并根据具体情况决定是否使用。

你可能感兴趣的:(C++进阶,c++,java,开发语言)