【C++】继承详解

2023-10-26

继承的概念

继承机制是面向对象程序设计使代码复用的重要手段,通过继承机制,可以利用已有的数据类型来定义新的数据类型。所定义的新的数据类型不仅拥有新定义的成员,而且还同时拥有旧的成员。我们称已存在的用来派生新类的类为基类,又称为父类。由已存在的类派生出的新类称为派生类,又称为子类。

语法: class + 派生类名 + : public(继承方式) + 基类名

class Person                                //父类, 基类 
{      
protected:      
  string _name;      
  int _age;      
};      


class Student : public Person               //子类, 派生类
{    
  private:    
  string stu_id;    
};           

继承方式: public , private, protected
在这里插入图片描述

基类和派生类对象赋值转换

class Person
{
protected:
	string _name;
	int _age;
};

class Student : public Person
{
private:
	string _stu_id;
};
void inherit_test1()
{
	Student s1;
	//子类对象可以赋值给父类对象/指针/引用
	Person p1 = s1;          
	Person* p2 = &s1;
	Person& p3 = s1;
	//基类对象指针可以强制转换成派生类对象指针
	Student* ps = (Student*)p2;
}

注意:基类对象不能赋值给派生类对象,但是基类对象的指针可以强制转换派生类对象的指针,但这样很容易产生指针越界问题,所以不推荐使用

继承作用域

1.在继承体系中基类和派生类都有自己独立的作用域
2.若子类中有和父类同名的成员变量,则构成隐藏或者叫重定义。默认调用子类的成员变量,若想要调用父类的需要在前面加上作用域
3.只要函数名相同就够成隐藏

最好不要在继承体系中定义同名的成员

同名成员变量

class Person
{
protected:
	string _name;
	int _age = 10;
};

class Student : public Person
{
public:
	void Print()
	{
		cout << _age << endl;                      //默认调用子类的成员
		cout << Person::_age << endl;              //加上作用域指定调用父类的成员
	} 
private:
	int _age = 20;
	string _stu_id;
};

void inherit_test2()
{
	Student s1;
	s1.Print();
}
int main()
{
	inherit_test2();
	
	return 0;
}

在这里插入图片描述
同名函数

class Person
{
public:
	void Print()                 	           //父类的Print函数
	{
		cout << "Person Print()" << endl;
	}
protected:
	string _name = "peter";
};

class Student : public Person
{
public:
	void Print(int i)                          //子类的Print函数
	{
		cout << "Student Print()" << endl;
	}
private:
	int _age = 20;
	string _stu_id = "123.2.3.2";
};

void inherit_test2()
{
	Student s1;
	s1.Print(1);
	//s1.Print();                            //用法错误,无法匹配到父类的Print
	s1.Person::Print();                      //正确用法
} 
int main()
{
	inherit_test2();
	
	return 0;
}

虽然父类和子类函数的形参有差别,但是并不构成函数重载,因为两个函数并不在同一个作用域内。它们构成隐藏关系,s1默认只能调用到自己类型内的Print函数.想要调用父类的Print函数也要加上作用域

派生类的默认成员函数

1.派生类的构造函数必须调用基类构造函数来初始化基类的成员。若基类没有默认构造函数,必须在派生类构造函数初始化列表阶段显示调用
2.派生类的拷贝构造函数必须调用基类的拷贝构造函数完成基类的拷贝初始化
3.派生类的operator=必须调用基类的operator=来完成基类的赋值
4.派生类的析构函数被调用完成后自动调用基类的析构函数清理基类成员
5.基类先调用构造函数,派生类再调用构造函数。派生类先调用析构函数, 基类再调用析构函数

#include <iostream>

using namespace std;

class Person
{
public:
  //基类的四个默认成员函数
  Person(const char* name = "peter", int age = 18)
    :_name(name)
    ,_age(age)
  {
    cout << "Person(const char* name ,int age)" << endl;
  }

  Person(const Person& p)
    :_name(p._name)
    ,_age(p._age)
  {
    cout << "Person(const Person& p)" << endl;
  }

  Person& operator=(const Person& p)
  {
    if (this != &p)
    {
      _name = p._name;
    }
    cout << "Person& operator=(const Person& p)" << endl;
    return *this;
  }

  ~Person()
  {
    cout << "~Person" << endl;
  }
protected:
  string _name;
  int _age;
};

class Student : public Person
{
  public:
      //派生类的四个默认成员函数
    
      //当基类没有默认构造函数时,我们必须要在初始化列表处显示调用
      //Student(const char* name = "ken", int age = 20, const char* str_id = "000.0.0.0")
      //:Person(name, age)
      //:_stu_id(str_id)
      //{
      //  cout << "Student(const char* str_id)" << endl;
      //}
      

      //若基类有默认构造函数,可以不写,编译器会自动调用
      Student(const char* str_id = "000.0.0.0")
      :_stu_id(str_id)
      {
        cout << "Student(const char* str_id)" << endl;
      }

      //若基类存在默认构造函数,但是我们又在派生类中显示调用,那么先会调用基类的默认构造函数,再进行显示调用

    Student(const Student& s)
      :Person(s)                        //这里我们直接将Student类对象的引用传给Person的拷贝构造函数
      ,_stu_id(s._stu_id)               //赋值转换的价值在这里实现
     {
        cout << "Student(const Student& s)" << endl;
    }

    Student& operator=(const Student& s)
    {
      if (&s != this)                    //首先得避免自己给自己赋值的现象
      {
        (*this).Person::operator=(s);    //显示调用父类的赋值运算符重载(注意要注明作用域,不然会调用自身陷入死循环,造成栈溢出等问题	)
        _stu_id = s._stu_id;
      }
      cout << "Student& operator=(const Student& s)" << endl;
      return *this;
    }

    ~Student()
    {
      cout << "~Student" << endl;
    }

    void Print()
    {
      cout << "name " << _name << "age " << _age << "stu_id " << _stu_id << endl;
    }

  private:
  string _stu_id;
};


接下来进行三个小测试来观察一下基类和派生类各自的默认构造函数的调用顺序

void test_inherit1()                                                                                                                                            
{                                                                                                                                                               
  Student s1;                                                                                                                                                                                                                                                                                
}                                                                                                                                                               
void test_inherit2()                                                                                                                                            
{                                                                                                                                                               
  Student s1;                                                                                                                                                   
  Student s2(s1);                                                                                                                                               
}                                                                                                                                                               
void test_inherit3()                                                                                                                                            
{                                                                                                                                                               
  Student s1;                                                                                                                                                   
  Student s2;                                                                                                                                                   
  s1 = s2;                                                                                                                                                      
}                                                                                                                                                               
                                                                                                                                                                
int main()                                                                                                                                                      
{      
  test_inherit1();                                                                                                                                                                      
  //test_inherit2();      
  //test_inherit3();      
  return 0;                  
}      

test1运行结果
Person(const char* name ,int age)                         //父类构造
Student(const char* name, int age, const char* stu_id)    //子类构造
~Student                                                  //子类析构
~Person                                                   //父类析构

test2运行结果
Person(const char* name ,int age)                         
Student(const char* name, int age, const char* stu_id) 
Person(const Person& p)                                   //父类拷贝构造
Student(const Student& s)                                 //子类拷贝构造
~Student
~Person
~Student
~Person

test3运行结果
Person(const char* name ,int age)
Student(const char* name, int age, const char* stu_id)
Person(const char* name ,int age)
Student(const char* name, int age, const char* stu_id)
Person& operator=(const Person& p)                         //父类赋值运算符重载
Student& operator=(const Student& s)                       //子类赋值运算符重载
~Student
~Person
~Student


注意:如果我们想要显示调用基类的析构函数。必须要在前面加上作用域,因为在编译过程中,析构函数的名称会被统一处理成destruct() ,导致基类和派生类的析构函数构成隐藏,以至于不断调用派生类的析构函数导致栈溢出

继承和友元

友元关系不能继承,基类友元不能访问子类私有和保护对象

静态成员变量的继承

在一个继承体系中,静态成员只会有一份,所有子类和父类共用一个静态成员实例

菱形继承和虚拟继承

单继承:一个子类只有一个直接父类时称此继承关系为单继承
多继承:一个子列有多个直接父类时称此关系为多继承
在这里插入图片描述
如图:Student和Teacher是Person的派生类,doctor(博士)同时继承了Student和Teacher。此时在doctor中就包含两份Person的成员变量信息。显然产生了数据冗余和二义性。我们可以通过作用域来调用不同父类的同名成员,解决二义性问题 。但是数据冗余的情况任然存在

在这里插入图片描述
如图所示:我们发现先继承的类成员地址越小,派生类的成员在基类的上面。并且确实存在两个_a,我们可以通过指定作用域来解决二义性问题。
在这里插入图片描述
虚拟继承
为了解决菱形继承中的数据冗余问题,C++提供了虚继承语法。我们可以使用virtual来进行虚继承

class Person
{
public:
	int _a = 1;
};

class Student : virtual public Person   //虚继承
{
public:
	int _b = 2;
};

class Teacher : virtual public Person   //虚继承
{
public:
	int _c = 3;
};

class doctor : public Student, public Teacher
{
public:
	int _d = 4;
};

int main()
{
	doctor d;
	return 0;
}

在这里插入图片描述
在这里插入图片描述
我们发现,变量排列的顺序发生了些许变化,b,c,d的顺序和先前一样,本来的冗余数据_a变成了一份到了最后面。并且d的空间中还有很多非成员变量的数据,这些数据其实是一个个地址

拿出第一个地址输入,我们发现这里有两行数据,第一行数据0,第二行数据为40
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
我们发现在距离&d四十个字节的地方存贮着_a,所以最上面的指针指向的空间的第二个数值存储着其与_a的相对距离,为了验证我们的猜想,我们进行下一组数据的验证

在这里插入图片描述
我们发现第二个值为24
在这里插入图片描述
我们发现_a 和 指针的起始位置相差了六行也就是24个字节,和我们的猜想一致

实验总结:使用虚继承后,冗余数据_a变成了一份并且来到结构的地址最高处。其他成员排列顺序不不变。在VS编译器中每一种类的数据以及冗余数据分开存储,中间由0xcc cc cc cc分隔继承的类开头会有一个指针,指针指向的地方存储了两个数据,第一个两次都为零(暂不讨论),第二个数据存储的是指针和冗余数据_a的相对距离。当使用派生类传递给基类指针或者对象时,编译器可以通过指针指向数据找齐父类的数据,进行切片等操作

其实父类通过开头存储的指针叫做虚机基表指针, 指向的两个表叫做虚基表。虚基表中存储的数据叫做偏移量。通过偏移量可以找到重复继承的部分

多继承是C++语法中较为复杂的,有了多继承就会存在菱形继承,有菱形继承就存在菱形虚拟继承,底层实现非常复杂,不建议使用。像java这些语言就直接不允许多继承

继承和组合

继承:a is b Student is Person 的意思
组合: a has b Student has name 的意思
优先使用 组合
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用
(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。
继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关
系很强,耦合度高。

对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对
象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),
因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,
耦合度低。优先使用对象组合有助于你保持每个类被封装。

实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适
合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就
用组合

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【C++】继承详解 的相关文章

  • 将通用列表转换为特定类型

    我有一个包含一些值的列表 Example List testData new List testData Add new List aaa bbb ccc testData Add new List ddd eee fff testData
  • PyQt 与 PySide 比较 [关闭]

    Closed 这个问题是基于意见的 help closed questions 目前不接受答案 我目前在 Linux 上的 Qt 重型 C Python 环境中开发了许多应用程序 并根据需要移植到 PC Mac 我使用嵌入 C 中的 Pyt
  • 多个 Access-Control-Allow-Origin 标头

    作为参考 我使用的是 Visual Studio 2017 和 Windows 10 我有一个 Web api 和带有用户帐户的相应 Web 应用程序 当我尝试登录时遇到一个错误 指出不存在 Access Control Allow Ori
  • 如何在 WPF 中使用 MVVM 从另一个视图打开一个视图

    我是 MVVM 新手 很长一段时间以来我都无法得到这个问题的答案 不知道是问题太难还是我没有解释清楚 我有 MainWindow Xaml 其中包含一个文本块和一个用于从文本块接收数据的按钮 现在当我按下按钮时 它应该打开第二个名为 tab
  • 在 C++03 中返回 `std::auto_ptr` 集合之类的内容的最佳方法是什么?

    std auto ptr不允许存储在STL容器中 例如std vector 但是 有时我需要返回多态对象的集合 因此我无法返回对象向量 由于切片问题 我可以用std tr1 shared ptr并将它们粘贴在vector 但随后我必须付出高
  • 如何使用线性索引在多维数组中设置值

    使用线性索引在 C 多维数组中设置值的最有效方法是什么 例如给定一个数组 int arr2 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 如何使
  • 在哪里设置服务引用上的 CookieContainer?

    例如 当将 WebService 引用添加到 NET 2 0 项目上的 ASMX 服务时 var objService new NameSpace groupservices 那里存在 objService CookieContainer
  • 有没有办法在运行时找到 PackageFamilyName?

    有没有办法在运行时找出 PackageFamilyName 如清单中所示 我查看了 Program Files WindowsApps 但找不到相关的字符串 找不到任何 API 可以让我这样做 欢迎任何其他想法 我想你正在寻找的是包 ID
  • 使用 [NotNull] 作为方法的参数

    考虑这段代码 https github com aspnet Mvc blob master src Microsoft AspNet Mvc Core Builder MvcApplicationBuilderExtensions cs
  • 如何向 Linq 表达式添加排序规则?

    如何实现 IQuariable 的方法如下 var trash from a in ContextBase db Users orderby a FirstName select a ToCollatedList 我想看到的结果 SELEC
  • 如何使用 Identity Server 4 颁发基于 Windows 身份验证的访问令牌

    我的目标是保护 Web API 以便客户端只能使用 IS 基于 Windows 身份验证颁发的访问令牌来访问它 我完成了这个基本示例 http docs identityserver io en release quickstarts 1
  • 实时录制/将音频数据转换为 WAV

    我在音频信号处理方面是新手 目前 我已将设备连接到我的电脑 该电脑从麦克风 播放轨道向我发送音频数据 我已经使用 Steinberg ASIO SDK 2 3 创建了主机应用程序 该应用程序连接到设备并在重复回调中返回原始数据 信号是 24
  • 复合属性

    有没有办法在 C 中制作复合属性以在编译时提供等效的元数据 例如 改变 ClassInterface ClassInterfaceType AutoDual ProgId MyProgId MyMefExport MyProgId publ
  • WebSocket 库 [关闭]

    Closed 这个问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 我想在 Linux 上使用 C 访问 WebSocket API 我见过不同的图书馆 比如libweb
  • 比使用流保存增强随机生成器状态更快的替代方案

    我需要能够保存 加载这个增强随机生成器的状态 boost variate generator
  • C++11内存_顺序_获取和内存_顺序_释放语义?

    http en cppreference com w cpp atomic memory order http en cppreference com w cpp atomic memory order以及其他 C 11 在线参考 将 me
  • rapidjson - 将键更改为另一个值

    这里是rapidjson的hello world 我怎样才能更改密钥 hello to goodbye 并从 json 中获取字符串 我的意思是我想解析 json 更改一些键并获取 json 字符串 就像 goodbye world con
  • C#中从WebApi获取数据

    我有 webapi 和她的方法 HttpPost HttpGet ActionName GetData public MyData GetData FromUri MyData data return datamanager get dat
  • C++ 错误:“_mm_sin_ps”未在此范围内声明

    我正在尝试对将函数应用于数组的不同方法进行基准测试 why is mm sin ps在我的范围内不知道但是 mm sqrt ps is 我怎样才能让它知道 并且编译没有错误 include
  • Socket ReceiveAsync 合并数据包

    我打算通过套接字接收数据包 但由于它们是从发送方以高频率发送的 因此其中许多数据包被打包成一个byte array SocketAsyncEventArgs Buffer然后保存多个数据包 即使它们是单独发送的 使用验证wireshark

随机推荐

  • 雪花算法生成ID

    雪花算法生成ID Snowflake 雪花算法是由Twitter开源的分布式ID生成算法 以划分命名空间的方式将64 bit位分割成多个部分 每个部分代表不同的含义 而Java中64bit的整数是Long类型 所以在Java中 SnowFl
  • [网络安全]sqli-labs Less-2 解题详析

    网络安全 Less 2 GET Error based Intiger based 基于错误的GET整型注入 判断注入类型 判断注入点个数 查库名 查表名 查users表的列名 查字段 注意 总结 往期回顾 网络安全 sqli labs L
  • TensorRT简介

    一 什么是TensorRT 一般的深度学习项目 训练时为了加快速度 会使用多 GPU 分布式训练 但在部署推理时 为了降低成本 往往使用单个 GPU 机器甚至嵌入式平台 比如 NVIDIA Jetson 进行部署 部署端也要有与训练时相同的
  • 【设计模式】工厂方法模式(C#)

    设计模式 工厂方法模式 1 概述 针对简单工厂中的缺点 使用工厂方法模式就可以完美的解决 完全遵循开闭原则 定义一个用于创建对象的接口 让子类决定实例化哪个产品类对象 工厂方法使一个产品类的实例化延迟到其工厂的子类 工厂方法模式的主要角色
  • 解决gensim fasttext官方案例报错TypeError: Either one of corpus_file or corpus_iterable value must be provide

    完整报错为 TypeError Either one of corpus file or corpus iterable value must be provided 解决方法 将官方案例中传递参数时指定的sentences 删除即可 比如
  • Recyclerview源码深入探索:Adapter的增删改再也不迷路

    作者 maxcion 看到标题说的是三级缓存 有的地方说是四级缓存 请你不要迷惑 到底是三还是四 这就像图片加载这个场景有人说是三级缓存有人说是二级缓存 说三级缓存是把通过网络请求图片这个环节也认为是一层缓存 你认为这个环节应该不应该属于缓
  • 基于Qt、C++的毕业设计课设数学绘图工具(平面图、图表、立体图绘制-附下载链接)

    基于Qt C 的毕业设计课设数学绘图工具 平面图 图表 立体图绘制 介绍 这是我的毕业设计 基于Qt Creator 4 11 1 c 语言 效果图如下 点我下载项目源码 含打包软件 使用说明 1 二维函数绘制 开始界面 函数设置 输入界面
  • MySQL免安装配置教程(win10)

    一 下载安装包 1 1 下载zip包 打开官网地址下载zip安装包 这里下载的版本是5 7 可自行选择 对应下载网址 https downloads mysql com archives community 根据自己电脑进行选择对应安装包
  • 周志华《机器学习》笔记(第4章) 决策树

    第四章 决策树 1 总述 决策树基于树结构进行决策 叶结点对应于决策结果 其他每个结点对应于一个属性测试 每个结点包含的样本集合根据属性测试的结果被划分到子结点中 最终目的是产生一个泛化能力强 能够处理未知样本的决策树 基本流程遵循简单而直
  • XSS学习

    目录 什么是XSS 概念 理解 XSS分类 存储型XSS 反射型XSS 原理 攻击过程 DOM型 攻击过程 DOM行XSS与反射型XSS区别 存储型XSS与反射型XSS区别 DVWA实验 反射型XSS low等级 JavaScript弹窗函
  • 2013年第四届C/C++ A组蓝桥杯省赛真题解析

    目录 第一题 高斯日记 题目描述 思路分析 AC代码 第二题 排它平方数 题目描述 思路分析 AC代码 第三题 振兴中华 题目描述 思路分析 AC代码 第四题 颠倒的价牌 题目描述 思路分析 AC代码 第五题 前缀判断 题目描述 思路分析
  • 将文件从本机上传到虚拟机中Linux系统中的几种方法

    一 使用FileZilla上传文件 1 启动虚拟机 打开Linux终端 输入ifconfig命令查看IP地址 IP地址为192 168 59 6 2 打开FileZilla 输入IP地址 用户名 密码 端口号 点击快速连接 连接成功后 左边
  • 测试工程师(初&中)面试题+知识点

    说明 记录下个人开始转行自学 gt 开始求职期间主要的学习内容 涵盖了 计算机基础 测试基础 自动化测试等 初中级测试 20年夏更新 需要掌握的大部分内容 巩固基础与按知识点自查时可选择性参考 一 面试题 1 请分别介绍一下单元测试 集成测
  • 芯片的SD/MMC控制器以及SD卡介绍

    1 MMC SD卡 eMMC介绍 1 1 三者关联 1 最早出现的是MMC卡 卡片式结构 按照MMC协议设计 相较于NandFlash芯片来说 MMC卡有2个优势 第一是卡片化 便于拆装 第二是统一了协议接口 兼容性好 2 后来出现SD卡
  • 数据库---mysql 之 常用命令行命令

    1 展示当前所有的数据库 show databases mysql gt show databases Database information schema jzq test mtx 1 mysql performance schema
  • 使用特网云云主机的最显着原因之一

    云计算的快速发展主要是由于移动设备的数量不断增加 云不仅对企业有用 对普通人也有用 我们无需在 PC 上安装程序即可运行程序 通过 Internet 存储和访问内容 无需物理服务器即可开发和测试程序 等等 云计算本质上是我们解决当今企业面临
  • 序列化的简介

    序列化 序列化的介绍 1 1 定义 序列化是将对象状态转换为可保持或传输的格式的过程 与序列化相对的是反序列化 它将流转换为对 象 这两个过程结合起来 可以轻松地存储和传输数据 1 2 序列化的目的 通过序列化以字节流的形式使对象在网络中进
  • 为什么别选计算机专业?

    在知乎看到一个这样的问题 为什么别选计算机专业 这个话题有 800 万人次浏览 以下是一位匿名用户的高赞回答 内容可能比较主观化 仅代表原作者个人观点 如果有不同意见欢迎留言区交流啊 不明白现在鼓吹计算机是什么意思 985计算机毕业 刷Le
  • 广东海洋大学数学与计算机学院校友会,2020年广东海洋大学数学与计算机学院全日制硕士研究生入学考试复试及录取工作方案...

    为规范我校全日制硕士研究生复试工作 保障研究生入学质量 依据教育部有关文件及广东省研究生招生录取工作会议精神 结合学校今年硕士研究生招生工作的实际情况 特制定本工作方案 一 工作原则 研究生复试工作要坚持公开 公平 公正和科学选拔的原则 德
  • 【C++】继承详解

    文章目录 继承的概念 基类和派生类对象赋值转换 继承作用域 派生类的默认成员函数 继承和友元 静态成员变量的继承 菱形继承和虚拟继承 继承和组合 继承的概念 继承机制是面向对象程序设计使代码复用的重要手段 通过继承机制 可以利用已有的数据类