C++ 继承 | 对象切割、菱形继承、虚继承、对象组合

文章目录

  • 继承
    • 继承的概念
    • 继承方式及权限
    • using改变成员的访问权限
    • 基类与派生类的赋值转换
    • 回避虚函数机制
    • 派生类的默认成员函数
    • 友元与静态成员
  • 多继承
    • 菱形继承
    • 虚继承
  • 组合


继承

继承的概念

继承可以使得子类具有父类的属性和方法或者重新定义、追加属性和方法等。

当创建一个类时,我们可以继承一个已有类的成员和方法,并且在原有的基础上进行提升,这个被继承的类叫做基类,而这个继承后新建的类叫做派生类。基类必须是已经定义而非仅仅声明,因此,一个类不能派生它本身。

继承这种通过生成子类的复用通常被称为 白箱复用(white-box reuse) 。术语 白箱 是相对可视性而言:在继承方式中,父类的内部细节对子类可见。

派生类的作用域嵌套在基类的作用域之内。

class [派生类名] : [继承类型] [基类名]

[继承类型] [基类名] 的组合被称为派生列表,值得注意的是,派生列表仅出现在定义中,而不能出现在声明中:

class A : public B; // ERROR:派生列表不能出现在声明中

正确实现如下:

class Human
{
public:Human(string name = "张三", int age = 18) : _name(name), _age(age){}void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}protected:string _name;int _age;
};class Student : public Human
{
public:Student(string stuNum = "123456") : Human(), _stuNum(stuNum){}void Print() // 将父类的Print函数重定向成自己的Print函数{Human::Print();cout << "stuNum:" << _stuNum << endl;}protected:string _stuNum; // 增加的成员变量
};int main()
{Human h1;Student s1;h1.Print();cout << endl;s1.Print();return 0;
}

基类和派生类都具有他们各自的作用域,那如果出现同名的成员(如上面的 Print函数 ),此时会怎么样呢?这里就要牵扯到一个概念——隐藏。(隐藏而非 重载 ,方法名虽然相同,但处于不同的作用域中。)

隐藏:也叫做重定义,当基类和派生类中出现重名的成员时,派生类就会将基类的同名成员给隐藏起来,然后使用自己的。(但是隐藏并不意味着就无法访问,可以通过声明基类作用域来访问到隐藏成员。)

因此 s1 调用 Print函数 时不会调用父类的,而是调用自己的。
在这里插入图片描述


继承方式及权限

继承的方式和类的访问限定符一样,分为 public(公有继承)private(私有继承)protected(保护继承) 三种。

关于 protected

  1. 对于类的对象来说是不可访问的。
class Human
{
private:int pri;
protected:int pro;
public:int pub;
};

在这里插入图片描述
在这里插入图片描述

  1. 对于派生类的成员(数据成员or成员函数)和基类的友元来说是可以访问的。
    在这里插入图片描述
  2. 子类/子类的友元不能直接访问父类的受保护成员,只能通过子类对象来访问。 如果子类(及其友元)能直接访问父类的受保护成员,那么 protected 提供的访问保护也就太不安全了。
class Human
{
protected:int age;
};class Student : public Human
{int stu_id;friend void get(Human&); // 不能访问Human::agefriend void get(Student&); // 可以访问Student:age
};

在这里插入图片描述

在这里插入图片描述

基类成员的访问说明符/派生类的继承方式

  1. 对基类成员的访问权限只与基类中的访问说明符有关,与派生类的继承方式无关。
class Human
{
private:int pri;
protected:int pro;
public:int pub;friend int f(Human h) { return h.pro; }
};class Student : public Human
{int f1() { return pri; } // ERROR:纵然是public继承也不可以访问private成员int f2() { return pro; } // 正确:protected成员可以被派生类访问
};class Teacher : private Human {int f1() { return pro; } // 正确:protected成员可以被派生类访问,即使继承方式是private
};
  1. 继承方式控制派生类对象(包括派生类的派生类)对于基类成员的访问权限。

在这里插入图片描述

总结来讲父类成员的访问权限决定了子类是否能访问该成员,而继承方式决定了父类成员在子类中的新权限是怎样的:

  • public:继承自父类的成员在父类中是什么权限,子类中就是什么权限。
  • protected:继承自父类的成员其访问权限都变成 protected
  • private:继承自父类的成员其访问权限都变成 private

派生类向基类转换的可访问性

假设 D 继承自 B

  • 只有当 D 公有地继承 B 时,派生类对象才能使用派生类向基类的转换;如果继承方式是受保护的或者私有的,则不能使用该转换。
  • 不论 D 以什么方式继承 BD 的成员函数和友元都能使用派生类向基类的转换。换言之,派生类向其直接基类的类型转换对于派生类的成员和友元来说永远是可访问的。
  • 如果继承方式是公有的或者受保护的,则D的子类(第二点说的是D本身)的成员和友元可以使用 DB 的类型转换;反之,如果继承方式是私有的,则不能使用。

默认的继承方式

默认情况下:

  • 使用 class 关键字定义的派生类是私有继承的
  • 使用 struct 关键字定义的派生类是公有继承的

using改变成员的访问权限

我们说继承方式决定了派生类的对象及派生类的子类对继承来的成员的访问权限,但这不是绝对的,我们可以通过 using 改变成员的访问权限,但只能改变派生类能访问的成员,即基类中的 protectedpublic 成员。

class Human
{
private:int pri;
protected:int pro;
public:int pub;
};class Teacher : private Human { // 私有继承
private:// 只能被类的成员or友元访问
public:using Human::pri; // 错误:using只能为派生类可访问的成员提供声明using Human::pro; // Teacher的对象、成员、友元、子类都可以访问
protected:using Human::pro; // Teacher的对象、成员、友元可以访问using Human::pub;
};

基类与派生类的赋值转换

我们在 四种强制转换类型中的 dynamic_cast 部分 提到过父类与子类的赋值转换

派生类可以赋值给基类的对象、指针或者引用,这样的赋值也叫做 对象切割

在这里插入图片描述
当把派生类赋值给基类时,可以通过切割掉多出来的成员如 _stuNum 来完成赋值。
但是 基类对象 不可以赋值给 派生类 ,因为他不能凭空多一个 _stuNum 成员出来。

但是 基类的指针却可以通过强制类型转换赋值给派生类对象 , 如:

int main()
{Human h1;Student s1;Human* hPtrs = &s1; // 指向派生类对象Human* hPtrh = &h1; // 指向基类对象// 传统方法Student* pPtr = (Student*)hPtrs; // 没问题Student* pPtr = (Student*)hPtrh; // 有时候没有问题,但是会存在越界风险// 如果父类之中包含虚函数,可以使用dynamic_castStudent* pPtr = dynamic_cast<Student*>(hPtrh);// 如果确认基类向派生类的转换是安全的,可以使用static_castStudent* pPtr = static_cast<Student*>(hPtrs);return 0;
}

总结来讲:

  • 派生类可以赋值给基类的对象、指针或者引用
  • 基类对象不能赋值给派生类对象
  • 基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才是安全的,否则会存在越界的风险。但基类如果是多态类型(父类之中包含虚函数),则可以使用 RTTIdynamic_cast 来实现 指向基类的基类指针派生类对象 的安全转换。

回避虚函数机制

我们说多态是为了实现子类对于同一操作的不同结果,但有时候,派生类需要调用其父类的虚函数版本,而非自己的虚函数版本:

int main()
{Human h1;Student s1;Human* hPtrs = &s1; // 指向派生类对象hPtrs->print(); // 由于hPtrs指向子类对象,因此调用子类的虚函数hPtrs->Human::print(); // 强行调用Human中的虚函数,而不在意hPtrs的动态类型return 0;
}

派生类的默认成员函数

之前有写过 类的默认六个成员函数

class Human
{
public:Human(){cout << "Human 构造函数" << endl;}~Human(){cout << "Human 析构函数" << endl;}protected:string _name;int _age;
};class Student : public Human
{
public:Student(){cout << "Student 构造函数" << _name << endl;}~Student(){//~Human(); 不需要手动调用父类的析构函数,编译器会在子类析构函数结束后自动调用。cout << "Student 析构函数" << endl;}
protected:string _stuNum;
};int main()
{Student s1;return 0;
}

在这里插入图片描述
可以看到,调用派生类的默认成员函数时都会调用基类的默认构造函数。

  • 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
  • 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  • 派生类的 operator= 必须要调用基类的 operator= 完成基类的复制。
  • 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。

在派生类的析构函数中,基类的析构函数会被隐藏,为了实现多态,它们都会被编译器重命名为 destructor


友元与静态成员

友元

友元关系是不会继承的(友元关系不具有传递性),可以这样理解,你长辈的朋友并不是你的朋友。

  • 基类的友元能访问基类的私有/保护成员,但不能访问子类的私有/保护成员。(当然基类本身也无法访问子类的私有成员。)
  • 子类访问父类友元的私有成员就更不用想了:
    1. 一是友元关系并不对称,A 是 B 的友元,B 不一定是 A 的友元,也就是说父类本身都不一定能访问父类友元的私有成员(父类不一定是其友元的友元),何况子类;
    2. 二是就算父类是其友元的友元,但友元关系不具有传递性,子类不一定是父类友元的友元。
  • 子类的友元无法访问父类的私有/保护成员。

静态成员

无论继承了多少次,派生了多少子类,静态成员在这整个继承体系中有且只有一个。静态成员不再单独属于某一个类亦或者是某一个对象,而是属于这一整个继承体系。


多继承

如果一个子类同时继承两个或以上的父类时,此时就是多继承。

多继承虽然能很好的继承多个父类的特性,达到复用代码的效果,但是他也有着很多的隐患,例如菱形继承的问题,这也就是为什么后期的一些语言如 java 把多继承去掉的原因。


菱形继承

class Human
{
public:int _age;
};class Student : public Human
{
public:int _stuNum;
};class Teacher : public Human
{
public:int _teaNum;
};

这里有着人类、学生类、老师类。在学校中,还存在着同时具有老师和学生这两个属性的人,也就是助教。所以我们可以让他同时继承 teacher类student类

在这里插入图片描述

class Assistant : public Teacher, public Student
{
};

按照道理来说,各个类的大小应该是这样的。Human 类4个字节,TeacherStudent 都是8个字节,而 Assistant 是12个字节。但是实际上 Assistant 却是16字节。
在这里插入图片描述
这就是菱形继承的 数据冗余二义性 问题的体现。

这里的 TeacherStudent 都从 Human 中继承了相同的成员 _age 。但是 Assistant 再从 TeacherStudent 继承时,就分别把这两个 _age 都给继承了过来。

在这里插入图片描述
这就是数据冗余问题。

倘若我们想要给那个 _age 赋值:

在这里插入图片描述
在这里插入图片描述

因为里面存在两个一样的 _age ,因此需要指定作用域:
在这里插入图片描述
在这里插入图片描述

这也就是二义性问题。


虚继承

想解决二义性很简单,当多个类继承同一个类时,就在继承这个类时,为其添加一个虚拟继承的属性。

class Student : virtual public Human
{
public:int _stuNum;
};class Teacher : virtual public Human
{
public:int _teaNum;
};

在这里插入图片描述

这时就可以看到,它只继承了一次。

接下来看看大小:
在这里插入图片描述
按照道理来说,a 应该是 12字节ts 应该是 8字节 啊?这里就牵扯到了C++的对象模型,先推荐一篇博客: C++中的虚函数(表)实现机制以及用C语言对其进行的模拟实现

这里多出来的 8个字节,其实是两个虚基表指针(vbptr)。同理,st 多出来的 4字节,是 一个vbptr

因为这里 Human 中的 _ageteacherstudent 共有的,所以为了能够方便处理,在内存中分布的时候,就会把这个共有成员 _age 放到对象组成的最末尾的位置。然后在建立一个虚基表,这个表记录了各个虚继承的类在找到这个共有的元素时,在内存中偏移量的大小,而虚基表指针则指向了各自的偏移量。

这里打个比方:
在这里插入图片描述
通过这个偏移量,他们能够找到自己的 _age 的位置。

为什么需要这个偏移量呢?

int main()
{Assistant a;Teacher t = a; Student s = a;return 0;
}

如上,当把对象 a 赋值给 ts 的时候,因为他们互相没有对方的 _stuNum_teaNum,所以他们需要进行对象的切割,但是又因为 _age 存放在对象的最尾部,所以只有知道了自己的偏移量,才能够成功的在切割了没有的元素时,还能找到自己的 _age


组合

那除了继承还有什么好的代码复用方式吗?那答案肯定是有的,就是组合。组合就是将多个类组合在一起,实现代码复用。

继承和组合又有什么区别呢?

  • 继承是一种 is a 的关系,基类是一个大类,而派生类则是这个大类中细分出来的一个子类,但是他们本质上其实是一种东西。正如:学生也是人,所以他可以很好的继承人的所有属性,并增加学生独有的属性。
  • 组合是一种 has a 的关系,就是一种包含关系。对象a对象b 中的一部分,对象b 包含 对象a

组合这种通过对方开放接口来实现的复用被称为 黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以 黑箱 的形式出现。

class Study
{
public:void ToStudy(){cout << "Study" << endl;}
};class Student : public Human
{
public:Study _s;int _stuNum;
};

这里的 Student类 中包含了一个 Study类 ,学习是学生日常生活中不可缺少的一部分。

比较组合和继承:

  • 组合的依赖关系弱,耦合度低。保证了代码具有良好的封装性和可维护性。 在组合中,几个类的关联不大,我只需要用到你那部分的某个功能,我并不需要了解你的实现细节,只需要你开放对应的接口即可,并且如果我要修改,只修改那一部分功能即可。
  • 继承的依赖关系就非常的强,耦合度非常高。 因为你要想在子类中修改和增加某些功能,就必须要了解父类的某些细节,并且有时候甚至会修改到父类,父类的内部细节在子类中也一览无余,严重的破坏了封装性。并且一旦基类发生变化时,牵一发而动全身,所有的派生类都会有影响,这样的代码维护性会非常的差。一个可用的解决方法就是只继承抽象类,因为抽象类通常提供较少的实现。

但是大部分场景下,如果继承和组合都可以选择,那么 优先使用对象组合,而不是类继承

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/443761.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

博弈论 | 博弈论简谈、常见的博弈定律、巴什博弈

文章目录博弈论什么是博弈论&#xff1f;博弈的前提博弈的要素博弈的分类非合作博弈——有限两人博弈囚徒困境合作博弈——无限多人博弈囚徒困境常见的博弈定律零和博弈重复博弈智猪博弈斗鸡博弈猎鹿博弈蜈蚣博弈酒吧博弈枪手博弈警匪博弈海盗分金巴什博弈博弈论 什么是博弈论…

MySQL命令(二)| 表的增删查改、聚合函数(复合函数)、联合查询

文章目录新增 (Create)全列插入指定列插入查询 (Retrieve)全列查询指定列查询条件查询关系元素运算符模糊查询分页查询去重&#xff1a;DISTINCT别名&#xff1a;AS升序 or 降序更新 (Update)删除 (Delete)分组&#xff08;GROUP BY&#xff09;联合查询内连接&#xff08;inne…

MySQL | 数据库的六种约束、表的关系、三大范式

文章目录数据库约束NOT NULL&#xff08;非空约束&#xff09;UNIQUE&#xff08;唯一约束&#xff09;DEFAULT&#xff08;缺省约束&#xff09;PRIMARY KEY&#xff08;主键约束&#xff09;AUTO_INCREMENT 自增FOREIGN KEY&#xff08;外键约束&#xff09;CHECK&#xff08…

哈希 :哈希冲突、负载因子、哈希函数、哈希表、哈希桶

文章目录哈希哈希&#xff08;散列&#xff09;函数常见的哈希函数字符串哈希函数哈希冲突闭散列&#xff08;开放地址法&#xff09;开散列&#xff08;链地址法/拉链法&#xff09;负载因子以及增容对于闭散列对于开散列结构具体实现哈希表&#xff08;闭散列&#xff09;创建…

C++ 泛型编程(一):模板基础:函数模板、类模板、模板推演成函数的机制、模板实例化、模板匹配规则

文章目录泛型编程函数模板函数模板实例化隐式实例化显式实例化函数模板的匹配规则类模板类模板的实例化泛型编程 泛型编程旨在削减重复工作&#xff0c;如&#xff1a; 将一个函数多次重载不如将他写成泛型。 void Swap(int& left, int& right) {int temp left;lef…

你真的了解静态变量、常量的存储位置吗?

文章目录引言C对内存的划分如何落实在Linux上自由存储区和堆之间的问题栈常量区静态存储区静态局部变量静态局部变量、静态全局变量、全局变量的异同macOS系统的测试结果总结引言 在动态内存的博客中&#xff0c;我提到&#xff1a; 在Linux 内存管理的博客中&#xff0c;我提…

C++ 泛型编程(二):非类型模板参数,模板特化,模板的分离编译

文章目录非类型模板参数函数模板的特化类模板的特化全特化偏特化部分参数特化参数修饰特化模板分离编译解决方法非类型模板参数 模板的参数分为两种&#xff1a; 类型参数&#xff1a; 则是我们通常使用的方式&#xff0c;就是在模板的参数列表中在 class 后面加上参数的类型…

数据结构 | B树、B+树、B*树

文章目录搜索结构B树B树的插入B树的遍历B树的性能B树B树的插入B树的遍历B*树B*树的插入总结搜索结构 如果我们有大量的数据需要永久存储&#xff0c;就需要存储到硬盘之中。但是硬盘的访问速度远远小于内存&#xff0c;并且由于数据量过大&#xff0c;无法一次性加载到内存中。…

MySQL 索引 :哈希索引、B+树索引、全文索引

文章目录索引引言常见的索引哈希索引自适应哈希索引B树索引聚集索引非聚集索引使用方法联合索引最左前缀匹配规则覆盖索引全文索引使用方法索引 引言 为什么需要索引&#xff1f; 倘若不使用索引&#xff0c;查找数据时&#xff0c;MySQL必须遍历整个表。而表越大&#xff0c;…

服装店怎么引流和吸引顾客 服装店铺收银系统来配合

实体店的同城引流和经营是实体经济的一个重要的一环&#xff0c;今天我们来分享服装行业的实体店铺怎么引流和吸引、留住顾客&#xff0c;并实现复购。大家点个收藏&#xff0c;不然划走就再也找不到了&#xff0c;另外可以点个关注&#xff0c;下次有新的更好的招&#xff0c;…

MySQL 锁的相关知识 | lock与latch、锁的类型、简谈MVCC、锁算法、死锁、锁升级

文章目录lock与latch锁的类型MVCC一致性非锁定读&#xff08;快照读&#xff09;一致性锁定读&#xff08;当前读&#xff09;锁算法死锁锁升级lock与latch 在了解数据库锁之前&#xff0c;首先就要区分开 lock 和 latch。在数据库中&#xff0c;lock 和 latch 虽然都是锁&…

MySQL 存储引擎 | MyISAM 与 InnoDB

文章目录概念innodb引擎的4大特性索引结构InnoDBMyISAM区别表级锁和行级锁概念 MyISAM 是 MySQL 的默认数据库引擎&#xff08;5.5版之前&#xff09;&#xff0c;但因为不支持事务处理而被 InnoDB 替代。 然而事物都是有两面性的&#xff0c;InnoDB 支持事务处理也会带来一些…

MySQL 事务 | ACID、四种隔离级别、并发带来的隔离问题、事务的使用与实现

文章目录事务ACID并发带来的隔离问题幻读&#xff08;虚读&#xff09;不可重复读脏读丢失更新隔离级别Read Uncommitted (读未提交)Read Committed (读已提交)Repeatable Read (可重复读)Serializable (可串行化)事务的使用事务的实现Redoundo事务 事务指逻辑上的一组操作。 …

MySQL 备份与主从复制

文章目录备份主从复制主从复制的作用备份 根据备份方法的不同&#xff0c;备份可划分为以下几种类型&#xff1a; 热备(Hot Backup) &#xff1a; 热备指的是在数据库运行的时候直接备份&#xff0c;并且对正在运行的数据库毫无影响&#xff0c;这种方法在 MySQL 官方手册中又…

C++ 流的操作 | 初识IO类、文件流、string流的使用

文章目录前言IO头文件iostreamfstreamsstream流的使用不能拷贝或对 IO对象 赋值条件状态与 iostate 类型输出缓冲区文件流fstream类型文件模式文件光标函数tellg() / tellp()seekg() / seekp()向文件存储内容/读取文件内容string流istringstreamostringstream前言 我们在使用 …

C++ 右值引用 | 左值、右值、move、移动语义、引用限定符

文章目录C11为什么引入右值&#xff1f;区分左值引用、右值引用move移动语义移动构造函数移动赋值运算符合成的移动操作小结引用限定符规定this是左值or右值引用限定符与重载C11为什么引入右值&#xff1f; C11引入了一个扩展内存的方法——移动而非拷贝&#xff0c;移动较之拷…

且谈关于最近软件测试的面试

前段时间有新的产品需要招人&#xff0c;安排和参加了好几次面试&#xff0c;下面就谈谈具体的面试问题&#xff0c;在面试他人的同时也面试自己。 面试问题是参与面试同事各自设计的&#xff0c;我也不清楚其他同事的题目&#xff0c;就谈谈自己设计的其中2道题。 过去面试总是…

C++ 多态 | 虚函数、抽象类、虚函数表

文章目录多态虚函数重写重定义&#xff08;参数不同&#xff09;协变&#xff08;返回值不同&#xff09;析构函数重写&#xff08;函数名不同&#xff09;final和override重载、重写、重定义抽象类多态的原理虚函数常见问题解析虚函数表多态 一种事物&#xff0c;多种形态。换…

C++ 运算符重载(一) | 输入/输出,相等/不等,复合赋值,下标,自增/自减,成员访问运算符

文章目录输出运算符<<输入运算符>>相等/不等运算符复合赋值运算符下标运算符自增/自减运算符成员访问运算符输出运算符<< 通常情况下&#xff0c;输出运算符的第一个形参是一个 非常量ostream对象的引用 。之所以 ostream 是非常量是因为向流写入内容会改变…

C++ 重载函数调用运算符 | 再探lambda,函数对象,可调用对象

文章目录重载函数调用运算符lambdalambda等价于函数对象lambda等价于类标准库函数对象可调用对象与function可调用对象function函数重载与function重载函数调用运算符 函数调用运算符必须是成员函数。 一个类可以定义多个不同版本的调用运算符&#xff0c;互相之间应该在参数数…