冰冰学习笔记:继承

欢迎各位大佬光临本文章!!!

还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正。

本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬、帅哥、美女点点支持,您的每一分关心都是我坚持的动力。

我的博客地址:bingbing~bang的博客_CSDN博客https://blog.csdn.net/bingbing_bang?type=blog

我的gitee:冰冰棒 (BingbingSuperEffort) - Gitee.comhttps://gitee.com/BingbingSuperEffort


系列文章推荐

冰冰学习笔记:《反向迭代器的模拟》

冰冰学习笔记:《适配器与仿函数》


目录

系列文章推荐

前言

1.继承的概念

2.继承的定义

3.继承的作用域

4.父类和子类对象的赋值转换

5.子类中的默认成员函数

5.1构造函数和析构函数

5.2拷贝构造和赋值重载

6.继承中友元和静态成员变量

7.单继承与多继承

7.1单继承

7.2多继承

7.3不被继承的类

8.菱形继承

9.继承和组合


前言

        我们常说,C++具备三大特性,封装,继承,多态。封装就是我们前面讲到的类和对象,将所有的实现细节放到一个类中,只对外界提供各种功能接口,从而保护底层的实现。而继承体现的是面向对象程序设计的代码复用手段。模板的复用体现的是函数的复用,而继承体现的就是类的复用。而对于多态,我们将在下一节中讲到。

1.继承的概念

        什么是继承?顾名思义,继承就是将原有对象的信息继承下来,并可以在此基础上进行拓展形成新的对象。而这样产生的类称为派生类或者子类,而原有的类则称为基类或者父类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。举个例子:

        现在我们想创建两个类,一个类用来表示学生,里面包含学生的姓名,年龄,学号,成绩等信息,另一个类则用来表示老师,里面包含了姓名,年龄,学院,职称等信息。此时我们就可以将公共信息,姓名,年龄提取出来,形成一个公共的类Person,学生,老师以及将来可能添加的职工等类都可以以Person类为基础,在此基础上进行信息的添加。此时Student和Teacher类就可以继承Person类,这两个类也称为Person类的子类。

         继承后,子类中都会有一份父类的成员变量和成员函数,每个子类中父类的成员是独立的,彼此不会影响。

2.继承的定义

        那怎么实现一个类的继承呢?如上面的例子,Person类为父类,Student类为子类,我们在创建Student类的时候需要加上继承方式,并指定继承的对象。

        类的继承方式与对象中的访问限定符使用的是同样的关键字,不同的继承方式具有不同的意思,但是使用最多的是公有继承方式。

        这里我们要注意以下几点:

(1)基类的私有成员不管以什么方式继承,子类中都不可见,子类并不是没有继承,父类的私有成员还是存在子类中,只是子类对象无论是在类外还是在类域内都不能访问。其存在的意义可以理解为基类不想让子类继承这些成员。

(2)由于基类中private成员在子类中不可见,因此基类中的成员如果想让子类继承后在类域内可以访问,类外不能访问应该使用protected修饰。因此保护成员限定符是因为继承才出现的,这也是这两个限定符的区别。

(3)最常用的继承方式是public继承,此种继承方式不会改变基类成员的访问限定符的类型。

(4)继承方式可以不显示写出,class类的默认继承方式是私有,struct类的默认继承方式是公有。但是我们应当显示指明继承方式。

(5)protected继承方式会将基类中原有的公有成员变为保护成员。

3.继承的作用域

        我们知道每一个class对象都有自己的作用域,继承中子类和父类也具备独立的作用域。Student继承了Person类的成员变量和成员函数,虽然可以访问他们,但是子类中自己的成员变量和成员函数与父类成员变量和函数并不在同一个作用域。

        我们知道在同一个作用域中不能存在同名的变量,同名但是不同参数的成员函数会形成函数重载。子类中可以存在与父类中变量名相同的变量,原因在于二者是不同的作用域。

         当子类中存在与父类同名的成员时,将自动屏蔽父类中的成员,从而构成隐藏也叫重定义。当函数名一样时就会构成隐藏,即便参数不一样。但是我们尽量不要定义重名的成员。我们如果想访问父类中的同名成员,可以指定类域进行访问。

4.父类和子类对象的赋值转换

        先看下面的代码,是否出现类型转换呢?

class Person
{
protected:
	string _name; // 姓名
	string _sex;  // 性别
	int _age; // 年龄
};
class Student : public Person
{
public:
	int _No; // 学号
};

void Test()
{
	Student sobj;

	Person pobj = sobj;
	Person* pp = &sobj;
	Person& rp = sobj;

}

        我们将Student类的对象,地址,分别赋值给Person类对象,指针以及使用Person类对象进行引用Student类对象。在我们之前的学习中了解到,如果是类型转换,在引用阶段就会报错,因为引用的并非原对象,而是原对象进行隐式类型转换后形成的临时对象,而临时对象具备常性,因此我们需要使用常引用。但是这里并没有报错,这就说明了这并不是隐式类型转换。

        这里是继承语法的一种特殊情况。C++允许派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用。并把这种方式称为切片或者切割。意思是将子类中父类的那部分内容切下来赋值过去。

        但是我们要注意基类对象不能赋值给子类对象,基类的指针或者引用可以通过强制类型转换的方式赋值给子类的指针和引用,但是存在风险。只有是基类的指针是指向派生类对象时才是安全的。

5.子类中的默认成员函数

        子类也是一个类,虽然它的部分成员是继承的父类中的,因此子类中也应该具备六个默认成员函数,那么子类中的默认成员函数是怎么工作的呢?

5.1构造函数和析构函数

        派生类中生成的默认构造函数对于派生类自己的成员,内置类型不做处理,自定义类型调用自定义类型的默认构造函数,而父类成员会去调用父类的构造函数进行初始化。如果父类中没有默认构造函数,那我们必须在子类中构造函数的初始化列表中显示调用。调用顺序是先调用父类的构造函数,在调用子类的构造函数。

        子类中析构函数对于自己的成员内置类型不做处理,自定义类型会去调用自定义类型的析构函数。而对于继承来的成员,将会调用父类的析构函数。

        但是要注意,子类的析构函数和父类的析构函数将会构成隐藏关系,因此我们如果想在子类中调用需要指定类域名。但是~Person和~Student函数名并不相同,为什么出现隐藏了呢?原因在于由于多态的需要,子类和父类的析构函数编译器会统一处理成destructor(),此时就会构成隐藏。

        调用后将出现下面的状况:

        我们会发现确实是先调用父类的构造在调用子类的构造,但是为什么父类的析构函数调用了这么多次,只有两个对象却调用了4次。

        原因在于子类的析构函数会自己调用父类的析构函数,不用我们显示调用,因为析构函数的调用规则为先析构子类成员,再析构父类成员,如果我们显示调用可能不会遵守这个规则,因此编译器会在调用完子类的析构后会自动调用父类。

5.2拷贝构造和赋值重载

        子类的拷贝构造和构造函数类似,对于子类自己成员,内置类型进行值拷贝。自定义类型调用自定义类型的拷贝构造,而继承的父类成员,必须调用父类的拷贝构造进行初始化。在调用传参的时候我们就可以直接传递子类的对象过去,由于切片的存在,父类的拷贝构造依然可以调用。

        子类的赋值重载对于自己的内置成员做值拷贝,自定义类型调用它的赋值重载进行赋值,继承的父类成员调用父类的赋值重载。与拷贝一样,也是先调用父类赋值重载再调用子类赋值重载。由于父类和子类的赋值重载构成了隐藏,因此我们需要显示指定类域去调用。

6.继承中友元和静态成员变量

        在之前的文章中我们提到过友元关系,友元关系为在类内部用friend声明某个函数为这个类的友元函数,因此这个函数在类外部可以使用类内部被封装的成员。友元的这种行为破坏了封装,我们应该谨慎使用。

        在继承中,友元关系不能被子类继承,及一个函数是父类的友元函数,但并不是子类的友元函数,基类的友元函数依然不能访问子类的私有和保护成员。如果我们想访问,就必须在子类中声明其为子类的友元函数。

        基类定义的静态成员变量,在整个继承体系中只存在一份,子类的操作会影响父类,父类的操作也会影响子类,不同子类之间的操作也会相互影响。例如下面的代码:

class Person
{
public:
	Person() { ++_count; }
protected:
	string _name; // 姓名
public:
	static int _count; // 统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
protected:
	int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
	string _seminarCourse; // 研究科目
};

void Test()
{
	Person p;
	cout << p._count << endl;
	Student s1;
	cout << s1._count << endl;
	s1._count=100;
	Student s2;
	cout << s2._count << endl;
	Student s3;
	cout << s3._count << endl;
	Graduate s4;
	cout << s4._count << endl;

}

        每创建一个子类或者父类对象count都会自增,s1中修改count,其他对象也会更改。

7.单继承与多继承

        继承体系分为单继承和多继承,而多继承又会构成更为复杂的菱形继承。

7.1单继承

        一个子类只有一个直接的父类时称这种继承关系为单继承。

7.2多继承

         一个子类有两个或以上直接父类时称这种继承关系为多继承。

7.3不被继承的类

        当我们定义的类不想被其他类继承时我们有两种方案可以做到,一种是将父类的构造函数私有化,那么子类在实例化对象的时候无法调用父类的构造函数初始化父类成员,就会报错,从而避免类的继承。另一种方式是C++11引进的方式,我们可以使用关键字final进行修饰,被修饰的类就称为最终类,在子类继承时就会出错。

        还有下面的经典题目,p1,p3虽然地址相同,但是并不代表指向的对像一样,p1指向的是Derive类中Base1类成员,p3指向的是Derive类整体。又由于栈是从高地址向低地址延申,b1先入栈,然后b2在入栈,这将导致p2地址将大于p1,p3。

8.菱形继承

        正是因为有多继承的存在,就会造成更加复杂的菱形继承问题。 菱形继承是多继承的一种特殊情况。菱形继承就是子类具备多个直接父类,而这些父类又是从一个共同的父类继承下来的,这就导致了在子类中存在数据冗余和二义性的问题。

        例如Assistant为Student和Teacher的子类,而Teacher和Student类又是从Person中继承而来的,Person类中的成员_name在Student类中有一份,在Teacher类中同样具备一份,当Assistant类继承这两个类后,Assistant类中就具备两份Person类中的数据,这就导致了数据冗余的问题,而对Assistant类中_name成员进行赋值时,编译器无法确定是对Student类继承下来的_name还是对Teacher类中继承下来的_name进行赋值,出现二义性的问题。

那如何解决这种问题呢?

        对于二义性问题的解决,我们可以使用指定类域的方式进行调用,指定_name为Student类的成员初始化,而不是Teacher类中的成员_name。

        但是这种方式只是解决了成员访问的问题,数据冗余问题还是没有解决,多出来的数据根本用不到,如果Person类中的成员非常大,那就造成了极大的空间浪费。

        基于这种情况,C++引用了新的关键字来进行解决,virtual关键字在继承中表示虚拟继承,在Student和Teacher类继承Person类时采用virtual关键字修饰,使其变为虚拟继承,这样Person类中的成员就只会出现一次,无论我们指定那个类域进行访问,都是对同一变量的修改。

        那么virtual关键字修饰后,Assistant类,Student类和Teacher类究竟发生了什么变化呢? 

        我们打开内存窗口可以很明显的看到,virtual修饰后的类存储方式发生了变化,继承的子类也发生了相应的变化。这些类中将父类Person的变量提取出来形成公共变量统一放在了最下面(每个编译器可能放的位置不同),Student类成员中的Person类成员多了一个指针,该指针指向了一张表,这个表就称为虚基表,其中虚基表中存储的是与公共变量_p的偏移量。这样即便发生切片行为,也可以通过这个指针找到对应的偏移量,从而访问到公共成员将其切片出去。

        而对于Assistant类对象,其内部将会有一个指向Teacher成员的虚基表和一个指向Student类成员的虚基表,通过这两个虚基表在进行Student类和Teacher类切片时找到_p成员。

        简单来说,使用虚继承后,Assistant类对象就成了下面的存储方式:

9.继承和组合

        继承和组合都是代码复用的一种手段,但是二者并不相同,继承是一种is-a的关系,B是A的子类,B就必然是A的一种,例如菊花就是植物的一种,菊花就是植物的子类,它继承了植物的共有特征。

        组合是一种has-a的关系,B组合了A,就意味着B中一定有A对象,例如汽车和车轮。

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

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

        当两种情况都符合时,我们应该优先使用组合这种低耦合的方式,但是这不意味着继承就没有用处,实现多态就必须使用继承。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bingbing~bang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值