C++ 继承详解

2023-11-05

继承语法

继承的一般语法为:

class 派生类名:[继承方式] 基类名{
    派生类新增加的成员
};

继承方式

继承方式包括 public(公有的)、private(私有的)和 protected(受保护的),此项是可选的,如果不写,那么默认为 private。不同的继承方式会影响基类成员在派生类中的访问权限。

(1)public继承方式

  • 基类中所有 public 成员在派生类中为 public 属性;
  • 基类中所有 protected 成员在派生类中为 protected 属性;
  • 基类中所有 private 成员在派生类中不能使用。

(2)protected继承方式

  • 基类中的所有 public 成员在派生类中为 protected 属性;
  • 基类中的所有 protected 成员在派生类中为 protected 属性;
  • 基类中的所有 private 成员在派生类中不能使用。

(3)private继承方式

  • 基类中的所有 public 成员在派生类中均为 private 属性;
  • 基类中的所有 protected 成员在派生类中均为 private 属性;
  • 基类中的所有 private 成员在派生类中不能使用。

通过上面的分析可以发现:

  1. 基类成员在派生类中的访问权限不得高于继承方式中指定的权限。例如,当继承方式为 protected 时,那么基类成员在派生类中的访问权限最高也为 protected,高于 protected 的会降级为 protected,但低于 protected 不会升级。再如,当继承方式为 public 时,那么基类成员在派生类中的访问权限将保持不变。也就是说,继承方式中的 public、protected、private 是用来指明基类成员在派生类中的最高访问权限的。
  2. 不管继承方式如何,基类中的 private 成员在派生类中始终不能使用(不能在派生类的成员函数中访问或调用)。
  3. 如果希望基类的成员能够被派生类继承并且毫无障碍地使用,那么这些成员只能声明为 public 或 protected;只有那些不希望在派生类中使用的成员才声明为 private。
  4. 如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明为 protected。

注意,我们这里说的是基类的 private 成员不能在派生类中使用,并没有说基类的 private 成员不能被继承。实际上,基类的 private 成员是能够被继承的,并且(成员变量)会占用派生类对象的内存,它只是在派生类中不可见,导致无法使用罢了。private 成员的这种特性,能够很好的对派生类隐藏基类的实现,以体现面向对象的封装性。

private继承特点

如果是private继承,则不会自动将派生类类型转换为基类类型(不会自动转换,但是可以手动显式进行转换),不能隐式转换。private继承就是一种纯粹的实现技术,意味着子类继承了父类,纯粹是看中了父类里面的某些函数实现罢了,这个新的类将不会与父类指针有关系(接口都变private了)。

改变访问权限

使用 using 关键字可以改变基类成员在派生类中的访问权限,例如将 public 改为 private、将 protected 改为 public。

注意:using 只能改变基类中 public 和 protected 成员的访问权限,不能改变 private 成员的访问权限,因为基类中 private 成员在派生类中是不可见的,根本不能使用,所以基类中的 private 成员在派生类中无论如何都不能访问。

using 关键字使用示例:

#include<iostream>
using namespace std;
//基类People
class People {
public:
    void show();
protected:
    char *m_name;
    int m_age;
};
void People::show() {
    cout << m_name << "的年龄是" << m_age << endl;
}
//派生类Student
class Student : public People {
public:
    void learning();
public:
    using People::m_name;  //将protected改为public
    using People::m_age;  //将protected改为public
    float m_score;
private:
    using People::show;  //将public改为private
};
void Student::learning() {
    cout << "我是" << m_name << ",今年" << m_age << "岁,这次考了" << m_score << "分!" << endl;
}
int main() {
    Student stu;
    stu.m_name = "小明";
    stu.m_age = 16;
    stu.m_score = 99.5f;
    stu.show();  //compile error
    stu.learning();
    return 0;
}

代码中首先定义了基类 People,它包含两个 protected 属性的成员变量和一个 public 属性的成员函数。定义 Student 类时采用 public 继承方式,People 类中的成员在 Student 类中的访问权限默认是不变的。

不过,我们使用 using 改变了它们的默认访问权限,如代码第 21~25 行所示,将 show() 函数修改为 private 属性的,是降低访问权限,将 name、age 变量修改为 public 属性的,是提高访问权限。

名字遮蔽

如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。所谓遮蔽,就是在派生类中使用该成员(包括在定义派生类时使用,也包括通过派生类对象访问该成员)时,实际上使用的是派生类新增的成员,而不是从基类继承来的。

下面是一个成员函数的名字遮蔽的例子:

#include<iostream>
using namespace std;
//基类People
class People{
public:
    void show();
protected:
    char *m_name;
    int m_age;
};
void People::show(){
    cout<<"嗨,大家好,我叫"<<m_name<<",今年"<<m_age<<"岁"<<endl;
}
//派生类Student
class Student: public People{
public:
    Student(char *name, int age, float score);
public:
    void show();  //遮蔽基类的show()
private:
    float m_score;
};
Student::Student(char *name, int age, float score){
    m_name = name;
    m_age = age;
    m_score = score;
}
void Student::show(){
    cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl;
}
int main(){
    Student stu("小明", 16, 90.5);
    //使用的是派生类新增的成员函数,而不是从基类继承的
    stu.show();
    //使用的是从基类继承来的成员函数
    stu.People::show();
    return 0;
}

运行结果:

小明的年龄是16,成绩是90.5
嗨,大家好,我叫小明,今年16

本例中,基类 People 和派生类 Student 都定义了成员函数 show(),它们的名字一样,会造成遮蔽。第 37 行代码中,stu 是 Student 类的对象,默认使用 Student 类的 show() 函数。

但是,基类 People 中的 show() 函数仍然可以访问,不过要加上类名和域解析符,如第 39 行代码所示。

基类成员函数和派生类成员函数不构成重载。基类成员和派生类成员的名字一样时会造成遮蔽,这句话对于成员变量很好理解,对于成员函数要引起注意,不管函数的参数如何,只要名字一样就会造成遮蔽。换句话说,基类成员函数和派生类成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数,不管它们的参数是否一样

下面的例子很好的说明了这一点:

#include<iostream>
using namespace std;
//基类Base
class Base{
public:
    void func();
    void func(int);
};
void Base::func(){ cout<<"Base::func()"<<endl; }
void Base::func(int a){ cout<<"Base::func(int)"<<endl; }
//派生类Derived
class Derived: public Base{
public:
    void func(char *);
    void func(bool);
};
void Derived::func(char *str){ cout<<"Derived::func(char *)"<<endl; }
void Derived::func(bool is){ cout<<"Derived::func(bool)"<<endl; }
int main(){
    Derived d;
    d.func("c.biancheng.net");
    d.func(true);
    d.func();  //compile error
    d.func(10);  //compile error
    d.Base::func();
    d.Base::func(100);
    return 0;
}

本例中,Base 类的func()、func(int)和 Derived 类的func(char *)、func(bool)四个成员函数的名字相同,参数列表不同,它们看似构成了重载,能够通过对象 d 访问所有的函数,实则不然,Derive 类的 func 遮蔽了 Base 类的 func,导致第 26、27 行代码没有匹配的函数,所以调用失败。

如果说有重载关系,那么也是 Base 类的两个 func 构成重载,而 Derive 类的两个 func 构成另外的重载。

继承时的对象模型

无变量遮蔽

有继承关系时,派生类的内存模型可以看成是基类成员变量和新增成员变量的总和,所有成员函数仍在另外一个区域——代码区,由所有对象共享。请看下面的代码:

#include <cstdio>
using namespace std;
 
class A{
protected:
    char a;
    int b;
public:
    A(char a, int b): a(a), b(b){}
    void display(){
        printf("a=%c, b=%d\n", a, b);
    }
};
 
class B: public A{
private:
    int c;
public:
    B(char a, int b, int c): A(a,b), c(c){ }
    void display(){
        printf("a=%c, b=%d, c=%d\n", a, b, c);
    }
};
 
int main(){
    A obj_a('@', 10);
    B obj_b('@', 23, 95);
    return 0;
}

obj_a 是基类对象,obj_b 是派生类对象。假设obj_a 的起始地址为 0X1000,那么它的内存分布如下图所示:
在这里插入图片描述

虽然变量 a 仅占用一个字节的内存,但由于内存对齐的需要,编译器会添加 3 个无用的字节(图中灰色部分),保证地址是 4 的倍数。后面的讲解中将忽略内存对齐,假设 a 的长度为4个字节。

假设 obj_b 的起始地址为 0X1100,那么它的内存分布如下图所示:
在这里插入图片描述

可以发现,基类的成员变量排在前面,派生类的排在后面。

下面再由 B 类派生出一个 C 类:

class C: public B{
private:
    int d;
public:
    C(char a, int b, int c, int d): B(a,b,c), d(d){ }
};
 
C obj_c('@', 45, 1009, 39);

假设 obj_c 的起始地址为 0X1200,那么它的内存分布如下图所示:
在这里插入图片描述

成员变量按照派生的层级依次排列,新增成员变量始终在最后。

有变量遮蔽

更改上面的C类:

class C: public B{
private:
    int b;  //遮蔽A类的变量
    int c;  //遮蔽B类的变量
    int d;  //新增变量
public:
    C(char a, int b, int c, int d): B(a,b,c), b(b), c(c), d(d){ }
    void display(){
        printf("A::a=%c, A::b=%d, B::c=%d\n", a, A::b, B::c);
        printf("C::b=%d, C::c=%d, C::d=%d\n", b, c, d);
    }
};
 
C obj_c('@', 23, 95, 2000);

假设 obj_c 的起始地址为 0X1300,那么它的内存分布如下图所示:
在这里插入图片描述

当基类A、B的成员变量被遮蔽,仍然会留在派生类对象 obj_c 的内存中,C 类新增的成员变量始终排在基类A、B的后面。

总结:派生类的对象模型中,会包含所有基类的成员变量。这种设计方案的优点是访问效率高,能够在派生类对象中直接访问基类变量,无需经过好几层间接计算。

final关键字

C++中,final关键字用于修饰类时,有以下作用:

  1. 禁止继承:将类标记为final,意味着无法继承。
  2. 禁止重写方法:当方法被标记为final时,在子类中无法重写该方法。
class test final{
};

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

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

  • 如何在C(Linux)中的while循环中准确地睡眠?

    在 C 代码 Linux 操作系统 中 我需要在 while 循环内准确地休眠 比如说 10000 微秒 1000 次 我尝试过usleep nanosleep select pselect和其他一些方法 但没有成功 一旦大约 50 次 它
  • JNI 将 Char* 2D 数组传递给 JAVA 代码

    我想从 C 代码通过 JNI 层传递以下指针数组 char result MAXTEST MAXRESPONSE 12 12 8 3 29 70 5 2 42 42 在java代码中我写了以下声明 public static native
  • 如何填充 ToolStripComboBox?

    我发现它很难将数据绑定到ToolStripComboBox 好像没有这个ValueMember and DisplayMember特性 怎么绑定呢 访问toolstripcombobox中包装的组合框并访问其ValueMember Disp
  • 函数参数的默认参数是否被视为该参数的初始值设定项?

    假设我有这样的函数声明 static const int R 0 static const int I 0 void f const int r R void g int i I 根据 dcl fct default 1 如果在参数声明中指
  • 从同一个类中的另一个构造函数调用构造函数

    我有一个带有两个构造函数的类 C 这是代码片段 public class FooBar public FooBar string s constructor 1 some functionality public FooBar int i
  • 查看 NuGet 包依赖关系层次结构

    有没有一种方法 文本或图形 来查看 NuGet 包之间的依赖关系层次结构 如果您使用的是新的 csproj 您可以在此处获取所有依赖项 在项目构建后 项目目录 obj project assets json
  • 使用可变参数包类型扩展的 C++ 函数调用者包装器

    我绑定了一些 API 并且绑定了一些函数签名 如下所示 static bool WrapperFunction JSContext cx unsigned argc JS Value vp 我尝试将对象和函数包装在 SpiderMonkey
  • unordered_map 中字符串的 C++ 哈希函数

    看起来 C 标准库中没有字符串的哈希函数 这是真的 在任何 c 编译器上使用字符串作为 unordered map 中的键的工作示例是什么 C STL提供模板专业化 http en cppreference com w cpp string
  • 对 std::vector 进行排序但忽略某个数字

    我有一个std vector
  • File.AppendText 尝试写入错误的位置

    我有一个 C 控制台应用程序 它作为 Windows 任务计划程序中的计划任务运行 此控制台应用程序写入日志文件 该日志文件在调试模式下运行时会创建并写入应用程序文件夹本身内的文件 但是 当它在任务计划程序中运行时 它会抛出一个错误 指出访
  • 启动时的 Excel 加载项

    我正在使用 Visual C 创建 Microsoft Excel 的加载项 当我第一次创建解决方案时 它包含一个名为 ThisAddIn Startup 的函数 我在这个函数中添加了以下代码 private void ThisAddIn
  • IQueryable 单元或集成测试

    我有一个 Web api 并且公开了一个端点 如下所示 api 假期 name name 这是 Web api 的控制器 get 方法 public IQueryable
  • 在视口中查找 WPF 控件

    Updated 这可能是一个简单或复杂的问题 但在 wpf 中 我有一个列表框 我用一个填充数据模板从列表中 有没有办法找出特定的数据模板项位于视口中 即我已滚动到其位置并且可以查看 目前我连接到了 listbox ScrollChange
  • 为什么这个二维指针表示法有效,而另一个则无效[重复]

    这个问题在这里已经有答案了 这里我编写了一段代码来打印 3x3 矩阵的对角线值之和 这里我必须将矩阵传递给函数 矩阵被传递给指针数组 代码可以工作 但问题是我必须编写参数的方式如下 int mat 3 以下导致程序崩溃 int mat 3
  • WPF DataGridTemplateColumn 组合框更新所有行

    我有这个 XAML 它从 ItemSource 是枚举的组合框中选择一个值 我使用的教程是 http www c sharpcorner com uploadfile dpatra combobox in datagrid in wpf h
  • 使 Guid 属性成为线程安全的

    我的一个类有一个 Guid 类型的属性 该属性可以由多个线程同时读写 我的印象是对 Guid 的读取和写入不是原子的 因此我应该锁定它们 我选择这样做 public Guid TestKey get lock testKeyLock ret
  • OpenGL:仅获取模板缓冲区而没有深度缓冲区?

    我想获取一个模板缓冲区 但如果可能的话 不要承受附加深度缓冲区的开销 因为我不会使用它 我发现的大多数资源表明 虽然模板缓冲区是可选的 例如 排除它以利于获得更高的深度缓冲区精度 但我还没有看到任何请求并成功获取仅 8 位模板缓冲区的代码
  • 使用 C 在 OS X 中获取其他进程的 argv

    我想获得其他进程的argv 例如ps 我使用的是在 Intel 或 PowerPC 上运行的 Mac OS X 10 4 11 首先 我阅读了 ps 和 man kvm 的代码 然后编写了一些 C 代码 include
  • 灵气序列解析问题

    我在使用 Spirit Qi 2 4 编写解析器时遇到一些问题 我有一系列键值对以以下格式解析
  • 如何将十六进制字符串转换为无符号长整型?

    我有以下十六进制值 CString str str T FFF000 如何将其转换为unsigned long 您可以使用strtol作用于常规 C 字符串的函数 它使用指定的基数将字符串转换为 long long l strtol str

随机推荐

  • EM算法及其改进算法

    EM算法及其改进算法 搬运 EM算法笔记一 讲了基础的EM算法 EM算法笔记二 讲述了EM的改进算法 讲得很清晰 EM算法笔记三 不同应用场景的阐述
  • JavaScript实现跳跃游戏的贪婪方法的算法

    JavaScript实现跳跃游戏的贪婪方法的算法 跳跃游戏是一种常见的游戏类型 其中玩家需要控制角色进行跳跃操作以避开障碍物并达到目标 在这篇文章中 我们将使用JavaScript实现跳跃游戏的贪婪算法 该算法能够根据当前情况做出最优的跳跃
  • SpringFramework核心技术一(IOC:使用ICO容器)

    使用容器 这ApplicationContext是高级工厂的接口 能够维护不同bean及其依赖项的注册表 使用该方法T getBean String name Class requiredType 可以检索bean的实例 一 在Applic
  • 稀疏贝叶斯学习【Sparse bayesian learning】

    参考文献 An Empirical Bayesian Strategy for Solving the Simultaneous Sparse Approximation Problem 传统图像恢复 例如用Gaussian 噪声模型 TV
  • 基于ROS环境的相机标定教程

    一 参考资料 ROS学习 利用电脑相机标定 二 安装usb cam驱动包 usb cam ROS Wiki GitHub ros drivers usb cam A ROS Driver for V4L USB Cameras usb ca
  • 五、Android开发基础知识

    android系统一共分为四层 application java应用程序 Framework java框架或系统服务 Library 本地框架或本地服务又称为Native Android Runtime java运行环境 Linux Ker
  • app逆向篇之常用命令及刷机

    前言 之前刷机的时候记录的 刚好今天发一下 这篇仅用来记录学习及实践过程中的一些知识点 如有错误或不足之处 望大佬们不吝指教 ADB命令 1 连接设备 adb tcpip 5555 指定连接端口 adb connect 192 168 12
  • Eclipse下载与安装

    一 下载Eclipse 1 下载链接 https www eclipse org downloads 2 点击Download Packages 不要点击Download 64 bit 因为我没点这个 所以不知道 点击进去之后会有很多版本的
  • 基于深度学习的IQA论文整理

    文章目录 经典的CORNIA 经典的BRISQUE 用CNN进行质量评价的典型文章 最近的一些新颖的方法 经典的CORNIA Unsupervised Feature Learning Framework for No reference
  • 微软 Windows Server 2016 简体中文 MSDN 官方原版 ISO 镜像下载

    Windows Server 2016 它可以理解为服务器版的 Windows 10 宣告整个核心架构定型稳定 Windows Server 2016 是微软推出的第六个 Windows server 版本 也是 Windows 10 的服
  • 完美解决微信小程序使用复选框van-checkbox无法选中

    由于小程序使用了vant ui框架 导致checkbox点击无法选中问题
  • Elasticsearch之相关性评分

    一 概念 1 相关性 确切地说 应该加限定词 应该称作 已匹配到的内容的相关性 通俗地讲 就是已匹配到的内容跟要搜索的词 或句子 像不像 其中 已匹配到的内容 是指那些匹配了部分的搜索词的内容 或者完全和搜索词一模一样 这样就算匹配 而这个
  • 浅析DNS-RCODE(针对DNS应答码/状态码进行分析)

    1 前言 本文针对Header section format的RCODE进行分析 此部分对应Wireshark中打开的DNS数据包Domian Name System部分Flags的RCODE 1 1 参考资料 1 1 1 RFC1035
  • OA 权限树搭建时的技巧

    1
  • CGAL模运算在C/C++中的应用

    CGAL模运算在C C 中的应用 CGAL Computational Geometry Algorithms Library 是一个用于计算几何算法的强大C 库 其中包含了许多模块 包括模运算 Modular Arithmetic 模块
  • Linux 系统修改时区,nodejs 进行时间戳转换时,相差8小时

    做一个简单记录 1 查看系统时间 date date Y m d H M S 按照格式显示当前日期 结果如下 图中是修改后的时间 CST 表示的是美国 澳大利亚 古巴或中国的标准时间 2 查看时区 date Z 我系统之前查出来的是WAT
  • c语言用递归求整数阶乘n!

    include
  • CB Loss:基于有效样本的类别不平衡损失

    点击上方 AI公园 关注公众号 选择加 星标 或 置顶 因公众号更改了推送规则 记得读完点 在看 下次AI公园的新文章就能及时出现在您的订阅列表中 作者 Sik Ho Tsang 编译 ronghuaiyang 导读 使用每个类的有效样本数
  • react入坑学习(一)const 的用法

    解构赋值 const 的用法 const 概念 const 几种用法 用法一 用法二 const 的用法 在公司初次学习react 记录一点学习成果 const 概念 const 用于声明一个或多个常量 声明时必须进行初始化 且初始化后值不
  • C++ 继承详解

    C 继承 继承语法 继承方式 private继承特点 改变访问权限 名字遮蔽 继承时的对象模型 无变量遮蔽 有变量遮蔽 final关键字 继承语法 继承的一般语法为 class 派生类名 继承方式 基类名 派生类新增加的成员 继承方式 继承