C++进阶:继承

2023-11-10

继承

面向对象的三大特性:封装、继承、多态。继承在实际中用的也不是太多,但必须能熟练使用。

1. 继承的概念

继承机制是面向对象思想中复用代码的一种手段,它能够在原有类的特性的基础上进行扩展。由此产生新的类,称为派生类,原有类称作基类。继承体现了面向对象设计的层次结构,体现了由简单到复杂的认知过程,不同于面向过程只有函数复用,继承是类的设计层面上的复用

1.1 继承的定义

对于具有一定联系的一类群体,将其抽象成一类对象后,这些对象必然会具有重复的属性。

比如学校中有学生老师两种群体,他们都是属于人这个大类,他们都有名字、年龄、住址等等共有的属性,但不同的是学生有学号,教师有职工号等不同的属性。

因此就可以将教师和学生所共有的属性,全部放到人这个类中,只将二者独有的属性放入各自的类中,可以避免代码冗余的问题,也更能体现出类的层次设计。

class Son : public Father; //中间用:间隔

原有的类被称为基类或父类,继承父类的类叫做派生类或称子类。继承后父类的成员变量和成员函数都变成了子类的一部分,使得子类可以使用父类的成员,这便是继承的意义。

1.2 继承关系和访问限定

由上图可以看出,子类和父类中间用:间隔,并声明继承关系,继承关系和访问限定符一样有三种:publicprotectedprivate

因为成员变量的访问权限有三种,而类的继承关系也有三种,故组合起来会有九种情况,因此会影响到子类中的成员变量的访问权限,具体如表所示:

父类成员\继承方式 public 继承 protect 继承 private 继承
父类 public 成员 子类 public 成员 子类 protect 成员 子类 private 成员
父类 protected 成员 子类 protected 成员 子类 protect 成员 子类 private 成员
父类 private 成员 子类不可见成员 子类不可见成员 子类不可见成员
  • 父类该成员的访问权限和继承方式中二者最低的权限,就是子类中该成员的访问权限。(类比访问权限,防止权限放大)
  • 任何继承方式下,私有成员在子类中都属于存在但不可访问的成员。若想在子类中访问,则需修改变量在父类中的访问权限为保护。
  • 若不显式地指定继承方式,class类默认的继承方式为私有继承,struct类默认为公有继承,不提倡这种方式。

实际中很少使用除公有继承以外的继承,也不提倡使用其他继承方式,可维护性不强。

继承关系和父类的成员的访问权限只会决定子类从父类继承得的成员的访问权限。不会影响到其他成员,不要混淆。

 

2. 父子对象赋值转换

公有继承下,语法规定,子类对象可以赋值给父类的对象,但明显子类的成员要比父类的成员多,故在赋值时会发生变化,这个变化被形象的称为“切割”或“切片”。

  • 子类对象可以赋值给父类对象、父类对象的指针或引用。子类赋值给父类,是把子类中从父类继承下来的成员变量赋值给父类,或是父类指针指向该部分,或者父类引用该部分。

子类赋值父类的转换,不是被动发生的隐式类型转换,而是编译器语法所支持的行为。

指针的类型决定指针解引用访问的大小,而父类指针解引用就只能访问子类里继承下来的成员。子父类之间的赋值转换,都是把子类中继承下的成员“切割”出来,这个过程就叫做“切片”。

  • **父类对象不可以赋值给子类对象。**父类赋值给子类,会造成越界访问非法空间。所以编译器不允许这种行为。

Student s;
Person p;
//子类赋值给父类,发生赋值兼容
p = s;
Person& rp = s;
Person* pp = &s;
//父类赋值给子类
s = p;//Err
s = (Student)p; //Err
Student* ps = (Student*)&p; //可以但是会越界访问
Student& rs = (Student&)p;
ps->_stuId  = 1;//Err

子类赋值父类的赋值转换,只能发生在公有继承的情况下,因为其他继承方式会影响到成员变量的访问权限,可能会造成权限放大。

 

3. 继承的作用域

定义出一个类就划分出一个作用域,故父子类都有独立的作用域,子类继承父类后可能就会发生重名现象。当父子类出现同名成员时,会发生隐藏现象,即子类成员会屏蔽从父类继承来的同名成员,这种情况叫隐藏或重定义。

  • 成员函数和成员变量一样只需名称相同即可构成隐藏。隐藏后,可以通过类名::的方式访问继承来的同名成员。
  • 实际上在使用继承时,不要定义重名变量。
3.1 成员变量隐藏
class Person {
protected:
	string _name = "人名";
	int _num = 111; //身份证号
};
class Student : public Person {
public:
	void Print() {
		cout << "人名->" << _name  << endl;
		cout << "身份证号->" << Person::_num << endl; //访问父类同名成员,必须加域名限定
		cout << "学号->" << _num << endl;
	}
protected:
	int _num = 999; //学号
};
3.2 成员函数隐藏
class A {
public:
	void Func() {
		cout << "A::Func()" << endl;
	}
};
class B : public A {
public:
	void Func(int i) {
		A::Func(); //必须加上类名限定
		cout << "Func(int i)->" << i << endl;
	}
};
b.A::Func(); //必须加上类名限定
b.Func(1);
  • 父类和子类的同名成员函数构成隐藏,但不会构成函数重载,因为从定义上说继承得的函数仍然属于父类作用域。
  • 调用子类中和父类重名函数时,必须显式加上类名限定。

虽说继承来的成员函数是子类的一部分,但编译器优先查找子类,找到后便不会再查找,故调用父类成员函数时要加上类域和访问限定符。

继承得的函数可通过子类直接调用,但并不代表它就是子类的函数,因此不会构成重载,因为该函数并没有在子类中。继承只是一种关系链,告诉编译器两个类具有特殊的继承关系。

总结
  • 成员变量构成隐藏时,会导致访问不明确,所以编译不通过
  • 成员函数构成隐藏时,默认访问子类中的成员函数,若想访问父类中的必须加父类域名限定。

 

4. 子类的默认成员函数

一般类里有6个默认成员函数,也就是不主动编写,编译器会默认生成的构造函数、析构函数、拷贝构造、赋值重载以及取地址重载。在子类中,默认的成员函数会有哪些变化呢?

  • 构造子类对象时,子类构造函数先会调用父类的构造函数初始化父类那部分成员,再对子类成员初始化。

    • 如果父类没有默认的构造函数,则必须在子类构造函数初始化列表中显式调用。
  • 拷贝构造子类对象时,拷贝构造同样需要调用父类的拷贝构造初始化父类那部分成员,再拷贝构造子类成员。

    • 显式调用父类拷贝构造时,直接传子类对象过去,自动切片即可。
  • 使用子类对象赋值时,赋值重载同样要显式调用父类的赋值重载,再对子类成员赋值。

    • 显式调用父类赋值重载时,需指定父类域,也直接传子类对象,自动切片。
class Student : public Person {
public:
    Student(const char* name = "Peter", int stuId = 1)
        : Person(name);   //显式调用父类构造函数
        , _stuId(stuId);
    {}
    Student(const Student& s) 
        : Person(s) //显式调用父类拷贝构造
        , _stuId(s._stuId)
	{}
    Student& operator=(Student s) {
        cout << "operator=(Student s)" << endl;
        if (this != &s) {
            Person::operator=(s); //显式调用父类赋值重载
            _stuId = s._stuId;
        }
        return *this;
    }
  • 子类的析构函数会在析构完子类后,自动调用父类的析构函数,故不允许在子类析构中显式调用父类的析构函数
~Student() {
    //Person::~Person();//Err - 不允许主动调用父类析构
    cout << "~Student()" << endl;
}

子类对象初始化时,会先调用父类的构造函数,再调用子类的构造函数。析构反之,会先析构子类再析构父类,以满足栈的后进先出的特性。

析构函数的名称会被统一处理成deconstructor,故父子类的析构会构成隐藏。所以子类调用父类的析构同样要加类域限定,但子类析构不允许主动调用父类析构,因为析构要满足栈帧后进先出的特性,故无法主动调用,只能由编译器自动调用。

总结

只有析构需要特殊注意不可主动调用,其他默认成员函数都可以显式调用对应的父类的默认成员函数即可,传参时直接传子类对象利用自动切片完成效果。

 

5. 继承和友元

友元关系无法继承,也就是说父类中的友元,无法访问子类的成员。

class Person {
	friend void Display(const Person& p, const Student& s);
protected:
	string _name;
};
class Student : public Person {
private:
	int _stuId;
};
void Display(const Person& p, const Student& s) {
	cout << "Person::_name" << p._name << endl;
	cout << "Student::_stuId" << s._stuId << endl; //无法访问子类成员
}

 

6. 继承和静态成员

继承同样会把父类中的静态成员继承下来,且操作的是同一个变量。也就是说,不会因为是静态成员就不继承,也不会影响其静态的特性。

class A {
public:
	A() {
		_si += 1;
	}
protected:
	static int _si;
};
int A::_si = 0;
class B : public A {
public:
	B() {
		_si += 1;
	}
};
class C : public A {
public:
	C() {
		_si += 1;
	}
};
cout << _si << endl; //5

 

7. 菱形继承和虚拟继承

7.1 单继承和多继承

单继承:一个子类只有一个直接父类,这样的继承关系为单继承。

多继承:一个子类有两个及以上的直接父类,这样的继承关系为多继承。

7.2 菱形继承

菱形继承是当继承关系呈现出一种菱形的状态,是多继承的一种特殊情况。

菱形继承会产生的问题是:数据冗余和二义性。

  1. 从两个父类中都继承了相同的成员 _name,造成了数据冗余。
  2. 二义性是指,直接访问父类继承的成员时不确定是哪个父类,比如 _name 不确定是 Student 类还是 Teacher 类中的。

数据冗余和二义性都指的是最顶层的“爷类”中的成员,直接父类中的成员并不会产生问题。

7.3 菱形虚拟继承

实际开发中一般不会使用到多继承,一定要避免的是菱形继承。对于其二义性的问题,只需要再访问变量时加上父类域限定即可。而对于数据冗余的问题,C++花了很大力气解决,解决方法就是虚拟继承。

class Assistant : public Student, public Teacher {
protected:
	string _majorCourse;//专业课
};
Assistant a;
a._name = "zhangsan"; //Err - _name的访问不明确	
a.Student::_name = "张同学";
a.Teacher::_name = "张老师";

当然,我们可以通过上述代码的方式指定 _name 的值,此举解决了二义性的问题。但事实上 Assistant 类中应该只有一个 _name,此时应使用虚拟继承。

数据冗余和二义性的根源在于中间的两个父类都继承了它们的父类,他们的父类就叫做虚基类。因此在这两个父类的位置使用虚继承

class Student : virtual public Person {
public:
	int _stdId;//学生号
};
class Teacher : virtual public Person  {
public:
	int _teaId;//职工号
};

通过内存窗口探究虚拟继承和未采用虚拟继承的区别:

  • 普通菱形继承,从父类B中继承的成员放在一块,父类C中继承的成员放在一块,D单独的成员放在一块。可见,数据_a确实存有两份。
  • 采用虚拟继承后,父类B和父类C中继承来的“冗余”成员_a被单独放在一块空间中,父类BC中独有的成员也被分别放在其他空间中,但在该成员的上方分别还存有对应的内存指针。
  • 检查两个指针所指向的位置,发现存的值都是0,但其下分别还有一个4字节的空间,存有一定大小的值。而两个值相减就是当前指针位置距公共成员_a的偏移量。以此来定位继承得来的公共成员_a

菱形继承关系中最上层的类叫做虚基类,而内存位置中映射相对距离的表叫做虚基表。

表中的相对距离应该是使用虚继承的那两个父类所划分的内存空间距离公共成员的距离。

C++作为“第一个吃螃蟹的人”,支持面向对象的早期语言,走了一些弯路,踩了一些坑,语法设计比较复杂。

多种的继承方式和多继承就是典型的例子。有了多继承就会有菱形继承,所以说多继承是C++的缺陷之一,之后的面向对象语言如Java直接抛弃了多继承。除此之外,C++还有一个缺陷是没有垃圾回收机制。

8. 继承和组合

  • 继承是面向对象的特征之一,是一种语法机制。形象化解释类继承的关系,就是每个派生类对象都是一个基类对象(is-a),比如学生是一种人,宝马是一款车等。
  • 组合的定义是在新类里面创建原有类的对象,重复利用已有类的功能。在B类中创建了A类对象,就称B组合了A,比如头上有眼睛,车上有轮胎(has-a)。
//继承
class B : public A {
    ;
};
//组合
class B {
    A _a;
};

继承和组合都是一种复用的方式,完全符合“是一个”的关系,就用继承,完全符合“有一个”的关系,就用组合,当两种都有时,优先选用组合的方式。

不要轻易的使用继承,不能单纯为了复用代码而继承。过多的使用继承会破坏代码的可维护性和封装性,修改父类时,会影响到继承它的子类,从而增加程序的维护难度和成本。

具体解释

使用继承复用时,父类的内部细节对子类是可见的,故称继承为一种白盒复用。而对象组合要求被组合的对象具有健全的接口,因为对象的内部细节是不可见的,这种复用风格被称为黑箱复用。

  • 子类通过继承揭示了父类的实现细节,所以继承在一定程度上破坏了封装性 。子类的实现与父类有着紧密的依赖关系,以至于修改一方的实现可能会导致另一方发生变化,这种较强的依赖关系限制了灵活性。
  • 对象组合是通过创建其他对象进行复用的。组合因为对象只能通过接口访问,所以并不破坏封装性,更进一步,因为对象的实现是基于接口写的,所以实现上存在较弱的依赖关系。

对象组合比继承更满足高内聚、低耦合的设计要求。故优先采用组合而不是继承。

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

C++进阶:继承 的相关文章

随机推荐

  • 设计模式:Adapter(适配器)--类对象结构型模式

    一 介绍 Adapter 适配器 也叫做包装器 wrapp 在许多时候横夺得类不能够被复用 仅仅是因为将一个类的接口与专业应用得接口不匹配 当一些类的接口互不兼容 这时候就需要适配器 它能将一个类的接口转化为客户希望的另一个接口 Adapt
  • freenas共享指定网络密码不正确_华为手机网络邻居连接(windows10)

    一 打开控制面板 微表键 R 输入control 程序和功能 找不到的可以把查看方式改为大图标 启用或关闭Windows功能 勾选SMB 1 0 CIFS 文件共享支持 需要重启电脑 二 开始菜单 设置 网络和Internet 属性 查看你
  • antd Table组件rowSelection方法的一些坑:table组件多选时,会选中上一页或者下一页的表格同行数据

  • GMT格林威治时间&标准时&北京时间

    格林威治时间 Greenwich Mean Time 英文简写GMT 是国际标准时间 也被称为世界时 生活中最常听 最常用的是北京时间 或东八区时间 的叫法 而世界时区的划分和地理位置相关 全球共分为24个时区 每个时区内又有相应的标准时
  • Java的集中注释及用法,快捷键

    Java一共有三种注释方式 1 单行注释 注释内容 快捷键 注释 ctrl 取消注释 ctrl Eclipse和Idea应该是一样的特殊情况Idea可能不适用 但是Eclipse一定行 2 多行注释 注释内容 快捷键 注释 ctrl shi
  • 用虚拟机玩游戏的方法!! 开3D加速!

    曾经用过一些不同虚拟机结果大多都不支持3D驱动 所以导致虚拟机是不能够玩游戏的 今天给大家带来的是用虚拟机玩游戏的方法 虚拟机软件 VMWare虚拟机 版本最好是在6 0以上 硬件要求 CPU2 4以上 内存在1G以上 512的朋友可以试试
  • dedecms搜索功能实现

    index htm
  • @validate校验中的@NotEmpty、@NotBlank、@NotNull区别

    1 引入的包 jakarta validation api 2 0 2 jar 2 直接看源码上的注释 NotEmpty The annotated element must not be code null nor empty p Sup
  • coverity分析端软件环境搭建

    在某linux机器的 share2 coverity 目录下安装使用coverity说明 从官网下载安装文件 cov analysis linux64 2019 06 sh 放置于 share2 coverity 目录下 我是由于机器安装软
  • arch linux 防火墙,Arch linux操作系统之Firewalld防火墙安装使用

    本篇文章将介绍一下Arch下安装配置firewall防火墙 Centos7 X系列 FirewallD默认直接开启的 root Centos6 chkconfig list grep iptables centos6 x系列 iptable
  • c++day4

    仿照string类 完成myString 类 include
  • 【译】用 Rust 实现 csv 解析-part7

    Rust and CSV parsing 译文 用 Rust 实现 csv 解析 part7 原文链接 https blog burntsushi net csv 原文作者 BurntSushi 译文来自 https github com
  • 【华为OD机试真题】网上商城优惠活动(python版)100%通过率 超详细代码注释 代码解读

    华为OD机试真题 2022 2023 真题目录 点这里 华为OD机试真题 信号发射和接收 试读 点这里 华为OD机试真题 租车骑绿道 试读 点这里 网上商城优惠活动 时间限制 1s 空间限制 50MB 限定语言 不限 题目描述 背景 某网上
  • C++的++

    include stdafx h int main int x y m n x 0 x x printf x 0 x x的值是 d n x x 0 y 0 y x printf x 0 y 0 y x的值是 d n y x 0 x x pr
  • Zotero使用指南02:配合Word

    转载Zotero使用指南02 配合Word Zotero是一款广受好评的文献管理软件 其具有四大优点 首先是开源免费 源代码托管在Github平台 其次 其软件界面基于浏览器内核 支持Windows MacOS Linux多种操作系统 此外
  • Threejs基础代码段(二)Tweenjs补间动画

    一 Tweenjs是什么 tween js是一款可生成平滑动画效果的js动画库 只需要告诉tween你想修改什么值 以及动画结束时它的最终值是什么 动画花费多少时间等信息 tween引擎就可以计算从开始动画点到结束动画点之间值 来产生平滑的
  • sqlmap tamper脚本_注入工具 -- sqlmap(注入参数)

    任何关系走到最后 不过相识一场 有心者有所累 无心者无所谓 情出自愿 事过无悔 不负遇见 不谈亏欠 一 指定数据库类型 dbms http 192 168 1 121 sqli Less 1 id 1 name 2 dbms mysql d
  • 前后端部署具体详解(Go+Vue+Nginx)

    与你相识 博主介绍 本人是普通大学生一枚 每天钻研计算机技能 CSDN主要分享一些技术内容 因我常常去寻找资料 不经常能找到合适的 精品的 全面的内容 导致我花费了大量的时间 所以会将摸索的内容全面细致记录下来 另外 我更多关于管理 生活的
  • AcWing 172. 立体推箱子 BFS+状态表示

    题 代码参考了书上的 判断是否合法的函数写的好精简 这题理解了 就能很好的理解BFS 状态表示的理解 lie 0 立着 lie 1 横着躺着 lie 2 竖着躺着 j 0123分别表示左右上下 nextx i j 代表lie i时x往j方向
  • C++进阶:继承

    继承 面向对象的三大特性 封装 继承 多态 继承在实际中用的也不是太多 但必须能熟练使用 1 继承的概念 继承机制是面向对象思想中复用代码的一种手段 它能够在原有类的特性的基础上进行扩展 由此产生新的类 称为派生类 原有类称作基类 继承体现