C++【继承】

简介: C++【继承】

🌇前言

继承 是面向对象三大特性之一(封装、继承、多态),所有的面向对象(OO)语言都具备这三个基本特征,封装相关概念已经在《类和对象》系列中介绍过了,今天主要学习的是 继承,即如何在父类的基础之上,构建出各种功能更加丰富的子类

王阿姨(父类)的两个孩子(子类),在父类的基础之上,衍生出了不同的特性


🏙️正文

1、继承的概念

什么是继承?继承遗产还是继承花呗?答案都不是,先来看看官方解释:继承(inheritance)机制是面向对象程序设计使代码可以复用的重要的手段,它允许程序员在保持原有基类(父类)特性的基础上进行扩展,增加功能,这样产生新的类,称为派生类(子类)

继承相关概念:

  • 被继承对象:父类 / 基类 (base
  • 继承方:子类 / 派生类 (derived

1.1、本质

继承的本质就是 复用代码

假设你现在需要写一个 学校教务系统,单从角色划分上来说,可以简单分为:教职工和学生 这两大类,但如果继续划分的话,还可以分出:校领导、各级院长、辅导员、后勤人员、大一/大二/大三/大四学生等,假设为每种不同的角色都设计一个 struct,那么这个工程量也未免太大了

为了复用代码、提高开发效率,可以从各种角色中选出共同点,组成 基类,比如每个 都有姓名、年龄、性别、联系方式等基本信息,而 教职工学生 的区别就在于 管理与被管理,因此可以在 基类 的基础上加一些特殊信息如教职工号 表示 教职工,加上 学号 表示学生,其他细分角色设计也是如此

这样就可以通过 继承 的方式,复用 基类 的代码,划分出各种 子类

1.2、作用

子类基础父类后,可以享有父类中的所有 公开 / 保护 属性,也就是说,除了 私有 内容外,父类有的,子类全都有

示例:在 父类-车 的基础上,派生出 越野车 和 跑车 这两个 子类

//父类-车
class Car
{
public:
  Car(int speed = int())
    :_speed(speed)
  {}
  int getSpeed()
  {
    return _speed;
  }
private:
  int _speed;
};
//子类-越野车
class SUV : public Car
{
public:
  SUV()
    :Car(100)
  {
    cout << "我是越野车,我的最高速度只有:" << getSpeed() << "km/h" << endl;
  }
};
//子类-跑车
class coupe : public Car
{
public:
  coupe()
    :Car(200)
  {
    cout << "我是跑车,我的最高速度可以到:" << getSpeed() << "km/h" << endl;
  }
};
int main()
{
  SUV s;
  coupe c;
  return 0;
}


可以看到,两个子类都能具备父类中的 公有 / 保护 属性,并且能做到互不干扰

1.3、实际例子

在实际开发中,继承 会经常用到(不然也不会作为 面向对象三大特性 之一了)

比较经典的例子:C++ 中的 IO 流玩的就继承,并且还是菱形继承


2、继承的定义

了解完继承相关概念后,就可以开始学习使用继承了

2.1、格式

继承的格式很简单,格式为 子类 : 继承方式 父类,比如 class a : public b 就表示 a 继承了 b,并且还是 公有继承

注:Java 中的继承符号为 extern,而 C++ 中为 :

2.2、权限

继承有权限的概念,分别为:公有继承(public)、保护继承(protected)、私有继承(private

没错,与 中的访问 限定修饰符 一样,不过这些符号在这里表示 继承权限

简单回顾下各种限定符的用途

  • 公有 public:公开的,任何人都可以访问
  • 保护 protected:保护的,只有当前类和子类可以访问
  • 私有 private:私有的,只允许当前类进行访问

权限大小:公有 > 保护 > 私有

保护 protected 比较特殊,只有在 继承 中才能体现它的价值,否则与 私有 作用一样

访问权限:三种

继承权限:三种

根据排列组合,可以列出以下多种搭配方案

父类成员 / 继承权限 public protected private
父类的 public 成员 外部可见,子类中可见 外部不可见,子类中可见 外部不可见,子类中可见
父类的 protected 成员 外部不可见,子类中可见 外部不可见,子类中可见 外部不可见,子类中可见
父类的 private 成员 都不可见 都不可见 都不可见

注:所谓的外部其实就是子类对象

总结:无论是哪种继承方式,父类中的 private 成员始终不可被 [子类 / 外部] 访问;当外部试图访问父类成员时,依据 min(父类成员权限, 子类继承权限),只有最终权限为 public 时,外部才能访问

如何证明?

之所以说 C++ 的继承机制设计复杂了,是因为 protectedprivate 继承时的效果一样

关于默认继承权限

  • 假设不注明继承权限,class 默认为 privatestruct 默认为 public,最好是注明继承权限

如何强行访问父类中的私有成员?

  • 在父类中设计相应的函数,间接访问私有成员

其实 C++ 中搞这么多种情况(9种)完全没必要,实际使用中,最常见到的组合为 public : publicprotected : public

2.3、使用

如何优雅的使用好 权限?

对于只想自己类中查看的成员,设为 private,对于想共享给子类使用的成员,设为 protected,其他成员都可以设为 public

比如在张三家中,张三家的房子面积允许公开,家庭存款只限家庭成员共享,而个人隐私数据则可以设为私有

class Home
{
public:
  int area = 500; //500 平米的大房子
};
class Father : public Home
{
protected:
  int money = 50000;  //存款五万
private:
  int privateMoney = 100; //私房钱,怎能公开?
};
class Zhangsan : public Father
{
public:
  Zhangsan()
  {
    cout << "我是张三" << endl;
    cout << "我知道我家房子有 " << area << " 平方米" << endl;
    cout << "我也知道我家存款有 " << money << endl;
    cout << "但我不知道我爸爸的私房钱有多少" << endl;
  }
};
class Xiaoming
{
public:
  Xiaoming()
  {
    cout << "我是小明" << endl;
    cout << "我只知道张三家房子有 " << Home().area << " 平方米" << endl;
    cout << "其他情况我一概不知" << endl;
  }
};
int main()
{
  Zhangsan z;
  cout << "================" << endl;
  Xiaoming x;
  return 0;
}


实际使用中,权限 可以很好的保护成员

如何设计一个不能被继承的类?

  • 将父类的构造和析构函数设为私有,这样子类就无法创建父类对象,同时也就无法继承了


3、继承的作用域

子类虽然继承自父类,但两者的作用域是不相同的,假设出现同名函数时,默认会将父类的同名函数隐藏调,进而执行子类的同名函数

隐藏 也叫 重定义,与它类似的概念还有:重写(覆盖)、重载

3.1、隐藏

子类中出现父类的 同名 方法或成员

//父类
class Base
{
public:
  void func() { cout << "Base val: " << val << endl; }
protected:
  int val = 123;
};
//子类
class Derived : public Base
{
public:
  int func() 
  { 
    cout << "Derived val: " << val << endl;
    return 0;
  }
private:
  int val = 668;
};
int main()
{
  Derived d;
  d.func();
  return 0;
}


此时 父子类中的方法和成员均被隐藏,执行的是 子类方法,输出的是子类成员

只修改子类方法名为 funA

int funA() 
{ 
  cout << "Derived val: " << val << endl;
  return 0;
}


发现此时 隐藏 消失,并且结果的是 父类方法 + 父类成员

只修改子类成员为 num

int num = 668


此时 隐藏 也消失,执行结果 子类方法 + 父类成员

综上所述,当子类中的方法出现 隐藏 行为时,优先执行 子类 中的方法;当子类中的成员出现 隐藏 行为时,优先选择当前作用域中的成员(局部优先)

这已经证明了 父子类中的作用域是独立存在的

如何显式的使用父类的方法或成员?

  • 利用域作用限定符 :: 进行访问范围的限制

注意:

  • 只要是命名相同,都构成 隐藏 ,与 返回值、参数 无关
  • 隐藏会干扰调用者的意图,因此在继承中,要尽量避免同名函数的出现

4、基类与派生类对象的赋值转换

在继承中,允许将 子类 对象直接赋值给 父类,但不允许 父类 对象赋值给 子类

  • 这其实很好理解,儿子以后可以当父亲,父亲还可以当儿子吗?

并且这种 赋值 是非常自然的,编译器直接处理,不需要调用 赋值重载 等函数

//父类
class Base
{
protected:
  int val = 123;
};
//子类
class Derived : public Base
{
private:
  int num = 668;
};
int main()
{
  Base b;
  Derived d;
  b = d;
  d = b;  //非法,只允许 子->父
  return 0;
}


子类对象赋值父类对象 时,触发 切片 机制,丝滑的完成 赋值

黄瓜切片变成 黄瓜片,黄瓜片可变不回完整的黄瓜了

4.1、切片

父类对象 看作一个结构体,子类对象 看作结构体Plus 版

子类对象 中多余的部分去除,留下 父类对象 可接收的成员,最后再将 对象 的指向进行改变就完成了 切片


因为整个切片过程是由编译器自己完成的,所以效率很高,并且不会发生 借助临时对象构造再赋值 的情况,具体切片实现原理还后续再进行讲解

注意:切片只在 子类->父类 时发生,因为父类无法满足子类的需求


5、派生类中的默认成员函数

派生类(子类)也是 ,同样会生成 六个默认成员函数(用户未定义的情况下)

不同于单一的 子类 是在 父类 的基础之上创建的,因此它在进行相关操作时,需要为 父类 进行考虑

5.1、隐式调用

子类在继承父类后,构建子类对象时 会自动调用父类的 默认构造函数,子类对象销毁前,还会自动调用父类的 析构函数

class Person
{
public:
  Person() { cout << "Person()" << endl; }
  ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person
{
public:
  Student() { cout << "Student()" << endl; }
  ~Student() { cout << "~Student()" << endl; }
};
int main()
{
  Student s;
  return 0;
}


注意:自动调用是由编译器完成的,前提是父类存在对应的默认成员函数;如果不存在,会报错

5.2、显式调用

因为存在 隐藏 的现象,当父子类中的函数重名时,子类无法再自动调用父类的默认成员函数,此时会引发 浅拷贝 相关问题

class Person
{
public:
  Person() { cout << "Person()" << endl; }
  void operator=(const Person& P) { cout << "Person::operator=()" << endl; }
  ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person
{
public:
  Student() { cout << "Student()" << endl; }
  void operator=(const Student&) { cout << "Student::operator=()" << endl; }
  ~Student() { cout << "~Student()" << endl; }
};
int main()
{
  Student s1;
  cout << "================" << endl;
  Student s2;
  s1 = s2;
  return 0;
}


此时可用通过 域作用限定符 :: 显式调用父类中的函数


总的来说,子类中的默认成员函数调用规则可以概况为以下几点:

  1. 子类的构造函数必须调用父类的构造函数,初始化属于父类的那一部分内容;如果没有默认构造函数,则需要显式调用
  2. 子类的拷贝构造、赋值重载函数必须要显式调用父类的,否则会造成重复析构问题
  3. 父类的析构函数在子类对象销毁后,会自动调用,然后销毁父类的那一部分

注意:

  • 子类对象初始化前,必须先初始化父类那一部分
  • *子类对象销毁后,必须销毁父类那一部分
  • 不能显式的调用父类的析构函数(因为这不符合栈区的规则),父子类析构函数为同名函数 destructor,构成隐藏,如果想要满足我们的析构需求,就需要将其变为虚函数,构成重写

析构函数必须设为 虚函数,这是一个高频面试题,同时也是 多态 中的相关知识


6、继承与友元函数

友元关系不能被继承

场景:友元函数 Print 可以访问父类中的私有成员,但子类继承父类后,友元函数无法访问子类中的私有成员

class Base
{
  friend void Print();
private:
  static const int a = 10;
};
class Derived : public Base
{
private:
  static const int b = 20;
};
void Print()
{
  cout << Base::a << endl;
  cout << Derived::b << endl;
}
int main()
{
  Print();
  return 0;
}


如果想让 Print 函数也能访问子类中的私有成员,则需要 将其也声明为子类的友元函数


总之记住:友元关系不能被继承

这个就像是西欧社会中的一句名言:我的附庸的附庸,不是我的附庸


7、继承与静态成员

静态成员是唯一存在的,无论是否被继承

静态变量为于静态区,不同于普通的堆栈区,静态变量的声明周期很长,通常是程序运行结束后才会被销毁,因此 假设父类中存在一个静态变量,那么子类在继承后,可以共享此变量

可以利用这个特性,写一个统计 创建多少个父类子类对象 的小 demo

class Base
{
  friend void Print();
public:
  Base() { num++; }
  static int num; //静态变量
};
int Base::num = 0;  //初始化静态变量
class Derived : public Base
{
public:
  Derived() { num++; }
};
void Print()
{
  cout << Base::num << endl;
}
int main()
{
  Derived d1;
  Derived d2;
  Derived d3;
  Print();
  return 0;
}


创建了三个子类对象,同时 因为在创建子类对象前,会自动调用父类的默认构造函数,因此最终结果为 6

这也从侧面证明了静态成员是唯一存在的,并且被子类共享


8、菱形继承

单继承:一个子类只能继承一个父类

多继承:一个子类可以继承多个父类(两个及以上)

C++ 支持多继承,即支持一个子类继承多个父类,使其基础信息更为丰富,但凡事都有双面性,多继承 在带来巨大便捷性的同时,也带来了个巨大的坑:菱形继承问题

注:其他面向对象的高级语言为了避免出现此问题,直接规定了不允许出现多继承

8.1、概念

首先 C++ 允许出现多继承的情况,如下图所示

这样看很正常是吧,但如果出现以下这种 重复继承 的情况,就比较麻烦了

此时 普通人X 会纠结于使用哪一个 不用吃饭 的属性!这对于编译器来说,是一件无法处理的事

8.2、现象

将上述概念转化为代码,观察实际现象

注:多继承时,只需要在 父类 之后,添加 , 号,继续增加想要继承的父类

class Person
{
public:
  string _name; //姓名
};
//本科生
class Undergraduate : public Person
{};
//研究生
class Postgraduate : public Person
{};
//毕业生
class Graduate : public Undergraduate, public Postgraduate
{};
int main()
{
  Graduate g1;
  g1._name = "zhangsan";
  return 0;
}


无法编译!

8.3、原因

Undergraduate 中继承了 Person_namePostgraduate 也继承了 Person_name

Graduate 多继承 Undergraduate Postgraduate 后,同时拥有了两个 _name,使用时,无法区分!

通过监视窗口查看信息:

8.4、解决方法

想要解决二义性很简单,通过 :: 限制访问域即可

Graduate g1;
g1.Undergraduate::_name = "zhangsan";
cout << g1.Undergraduate::_name << endl;


但这没有从本质上解决问题!而且还没有解决数据冗余问题

真正的解决方法:虚继承

注:虚继承是专门用来解决 菱形继承 问题的,与多态中的虚函数没有直接关系

虚继承:在菱形继承的腰部继承父类时,加上 virtual 关键字修饰被继承的父类

class Person
{
public:
  string _name; //姓名
};
//本科生
class Undergraduate : virtual public Person
{};
//研究生
class Postgraduate : virtual public Person
{};
//毕业生
class Graduate : public Undergraduate, public Postgraduate
{};
int main()
{
  Graduate g1;
  g1._name = "zhangsan";
  cout << g1._name << endl;
  return 0;
}


此时可以解决 菱形继承数据冗余二义性 问题

虚继承是如何解决菱形继承问题的?

  • 利用 虚基表 将冗余的数据存储起来,此时冗余的数据合并为一份
  • 原来存储 冗余数据 的位置,现在用来存储 虚基表指针

此时无论这个 冗余 的数据存储在何处,都能通过 基地址 + 偏移量 的方式进行访问

虚继承相关知识补充

虚继承底层是如何解决菱形继承问题的?

  • 对于冗余的数据位,改存指针,该指针指向相对距离
  • 对于冗余的成员,合并为一个,放置后面,假设想使用公共的成员(冗余成员),可以通过相对距离(偏移量)进行访问
  • 这样就解决了数据冗余和二义性问题

为何在冗余处存指针?

  • 指针指向空间有预留一个位置,可以用于多态
  • 因此虚继承用的是第二个位置

新建对象进行兼容赋值时,对象指向指针处

  • 该指针(偏移量)指向的目标位置不定
  • 无论最终位置在何处,最终汇编指令都一样(得益于偏移量的设计模式)

虚函数是否会造成空间浪费?

  • 不会,指针大小固定为 4/8 字节

指针所指向的空间(虚基表)是否浪费空间?

  • 可以忽略不计,所有对象共享

假设存在多个共享成员,需要新增指针(偏移量),因为这些成员都是连续的,找到第一个,即可找到其他

  • 即使涉及内存对齐问题,编译器也会根据规则做出调整

为了解决 菱形继承 问题,想出了 虚继承 这种绝妙设计,但在实际使用中,要尽量避免出现 菱形继承 问题


9、补充

继承是面向对象三大特性之一,非常重要,需要对各种特性进行学习

关于多继承时,哪个父类先被初始化的问题

  • 谁先被声明,谁就会先被初始化,与继承顺序无关

除了可以通过继承使用父类中的成员外,还可以通过 组合 的方式进行使用

  • 公有继承:is-a —> 高耦合,可以直接使用父类成员
  • 组合:has-a —> 低耦合,可以间接使用父类成员

实际项目中,更推荐使用 组合 的方式,这样可以做到 解耦,避免因父类的改动而直接影响到子类

当然,使用哪种方式还要取决于具体场景,具体问题具体分析

//父类
class A {};
//继承
class B : public A
{
    //直接继承,直接使用
};
//组合
class C
{
private:
    A _aa;  //创建 A 对象,使用成员及方法
}


可能有的人问 继承 到底有什么用?答案很简单,为后面的 多态 实现铺路,也就是说,多态的实现离不开继承!

关于之前的 适配器 模式,除了可以使用 组合 的方式进行适配外,还可以通过 继承 的方式进行适配

  • queue -> dequelist
  • reverse_iterator -> iterator

在通过后者实现前者时,可以通过 组合,也可以通过 继承


🌆总结

以上就是本次关于 C++【继承】的全部内容了,在本篇文章中,我们重点介绍了继承的相关知识,如什么是继承、如何继承、继承该注意些什么,最后还学习了多继承模式中容易引发的菱形继承问题,探究了其原因及解决方法,关于继承是如何辅助实现多态的,可以期待下篇文章:C++【多态】



相关文章推荐


STL 之 泛型思想


C++【模板进阶】


C++【模板初阶】


STL 之 适配器


C++ STL学习之【优先级队列】


C++ STL学习之【反向迭代器】


C++ STL学习之【容器适配器】
目录
相关文章
|
27天前
|
C++ 开发者
C++学习之继承
通过继承,C++可以实现代码重用、扩展类的功能并支持多态性。理解继承的类型、重写与重载、多重继承及其相关问题,对于掌握C++面向对象编程至关重要。希望本文能为您的C++学习和开发提供实用的指导。
53 16
|
23天前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
46 5
|
3月前
|
编译器 C++ 开发者
【C++】继承
C++中的继承是面向对象编程的核心特性之一,允许派生类继承基类的属性和方法,实现代码复用和类的层次结构。继承有三种类型:公有、私有和受保护继承,每种类型决定了派生类如何访问基类成员。此外,继承还涉及构造函数、析构函数、拷贝构造函数和赋值运算符的调用规则,以及解决多继承带来的二义性和数据冗余问题的虚拟继承。在设计类时,应谨慎选择继承和组合,以降低耦合度并提高代码的可维护性。
41 1
【C++】继承
|
7月前
|
编译器 C++
【C++】详解C++的继承
【C++】详解C++的继承
|
4月前
|
安全 程序员 编译器
【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则
【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则
109 11
|
4月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
71 1
|
4月前
|
C++
C++番外篇——虚拟继承解决数据冗余和二义性的原理
C++番外篇——虚拟继承解决数据冗余和二义性的原理
63 1
|
4月前
|
安全 编译器 程序员
C++的忠实粉丝-继承的热情(1)
C++的忠实粉丝-继承的热情(1)
31 0
|
4月前
|
编译器 C++
C++入门11——详解C++继承(菱形继承与虚拟继承)-2
C++入门11——详解C++继承(菱形继承与虚拟继承)-2
55 0
|
4月前
|
程序员 C++
C++入门11——详解C++继承(菱形继承与虚拟继承)-1
C++入门11——详解C++继承(菱形继承与虚拟继承)-1
57 0