C++Primer(类继承)

一、派生类和基类之间的三种关系

1、派生类对象可以使用基类中不是私有限制的方法

2、 基类的指针可以在不用转换的情况下指向派生类对象。

3、基类的引用变量可以在不用转换的情况下指向派生类的对象


只能够把派生类的对象赋值给基类的引用或者地址赋值给基类的指针,但是反过来的话不行,是单向的。

引用的兼容性可以允许你用派生类的对象去初始化基类的对象,其中的操作就是把派生类对象中数据基类对象的那部分成员用来初始化基类对象的成员。在进行初始化的时候会调用相应的复制构造函数来完成操作。因为在创建派生类对象的时候也会创建一个相应的基类的对象,这种初始化方式,正是用生成派生类对象时的基类对象来初始化的。


二、多态性

多态性一直以来很抽象,但是仔细理解起来感觉只有一句话概括的很准确“一个方法根据调用他对象的不同而展现出不同的行为和功能”。

多态性主要体现在类的继承上面。实现多态性有两点:

1、在派生类中重新定义一个基类中的方法

2、在基类中定义一个虚函数,在派生类中根据自己的需求自己实现。


之前在MFC的C++中的知识碰到过这样一种情况,父类和子类同有一个同名的函数,但是实现不同。把一个派生类的对象的地址赋值给基类的指针,结果调用这个同名的方法的时候,调用的是基类的方法。也就是说,如果父类和子类中都有一个同名的函数的话,如果这个函数没有被定义为虚函数,那么在调用的时候将根据指针类型来选择的方法,但是如果定义为一个虚函数的时候,将根据指针指向的对象来确定相应的方法来调用。


有的时候会将基类的析构函数也声明为虚函数,这是为了在删除子类对象的时候,如果遇到上面那种情况,可以调用子类的析构函数。



三、静态连编和动态连编

在源代码中的函数调用解释为执行特定的函数代码块被称为函数名连编。静态连编发生在程序的编译阶段,动态连编发生在程序的运行阶段。


对于一些基本的数据类型来说,C++禁止不同类型的指针和地址进行赋值,比如我要把一个double的数据的地址赋值给一个int类型的指针,这是错误的,但是在类里面是可以的。

将派生类的引用和指针转换为基类的引用和指针叫做向上强制类型转换,是不用显式调用的,在赋值的时候就会自动调用。反过来就叫做向下的强制类型的转换,向下的强制类型转换是需要显式调用的。隐式的向上强制类型转换需要动态连编的支持,因为他是在程序运行的时候才能够确定指向的到底是基类的对象还是派生类的对象。

编译器对非虚函数使用静态连编,对虚函数使用动态连编。


因为动态连编需要很多准备工作,加大了系统的开销,但是如果没有虚函数的情况下,或者说没有类继承的情况下我们可能根本用不到动态连编,相对来说,静态连编虽然灵活性欠缺但是速度很快。

每个对象其实都有一个隐藏成员,它是一个指针指向一个数组,这个数组里面保存了很多的函数地址。这种数组被称为是虚函数表。虚函数表中存储了为派生类对象声明的所有的虚函数的地址。如果派生类重新定义了虚函数,那么虚函数里面存储的就是我们新定义的函数 地址,如果没有定义,那么虚函数表里面存储的就是基类中原始版本的函数地址。指向虚函数表的指针是每个对象都有一个,虚函数表 是整个类只有一个。


派生类不继承基类的构造函数,也就是说,派生类执行自己的构造函数和继承没什么关系。但是虚构函数还是有必要声明为虚函数的。因为子类对象在创建的时候如果有new的参与,那么把返回的指针赋值给父类类型的指针的话,如果析构函数不是虚的,那么只能够执行父类的析构函数,也就是说,释放掉子类对象中属于基类对象的那部分内存,声明为虚函数之后才会先执行子类的析构函数,再执行父类的析构函数。


最重要的一点就是:只有成员函数才设计到虚函数的问题,友元函数是不涉及到这些的。

基类中的虚函数,在派生类中重新定义的时候,无论参数列表是否和基类的虚函数相同,派生类都会自动屏蔽掉基类的版本,也就是说,派生类对象再调用这个名字的函数的时候,只能调用派生类中新定义的版本,编译器不会生成该函数的两个重载版本。

要区别的一个概念就是重载和重写是不同的。重载是发生在一个类里面,同名函数的两个不同的版本,而重写应该指的就是虚函数在派生类中的重新实现。无论参数列表是否相同,只要是子类中的虚函数名和基类中的相同,那么重新定义子类中的虚函数就会自动屏蔽掉基类中的同名虚函数。


如果基类中的虚函数被重载,那么如果在子类中要需要用到这几种重载的形式应该都进行重写。如果只是重写了一个,在子类中会自动屏蔽掉基类里面同名的虚函数,无论他有几个版本。


四、保护的访问权限

一般来说,基类的数据都会被定义在隐藏的部分中,保护的访问权限下的东西只能够被子类访问,所以将一些方法声明为保护权限,能够防止让子类意外的其他类或对象访问。


五、抽象类

包含纯虚函数的类叫做抽象类,一般来说,抽象类都是用作基类的,并且抽象类不能够创建对象。当我们要想把一个类定义成抽象类的时候,就在某一个虚函数的声明后面加上=0,至于你是否要在这个抽象类的实现文件当中实现这个函数是随意的。


六、类继承和动态内存分配

如果基类当中用到了动态内存分配,比如复制构造函数,赋值运算符的重载,显式定义析构函数等等。如果子类不需要使用动态内存分配,那么在继承下来之后是否还要对子类显式定义这些函数呢。在类对象进行复制的时候,类中的成员复制也是根据他们的数据类型相应的选择合适的复制方式,比如正常的基本数据类型使用规定好了的复制方式,但是一些类的成员就会用到复制函数来完成。在进行子类对象类型之间的复制的时候,因为子类对象的创建也同时创建了基类对象,所以在复制基类对象的部分会自动调用基类的复制函数,如果子类中没有特殊的类成员需要复制,比如动态申请的,那么就可以在子类中不定义复制函数。


上面说的是基类使用了动态内存分配的方式,那么如果子类也使用了动态内存的方式来创建成员的话,就需要为子类也提供复制构造函数还有析构函数,以及重载的赋值运算符。

在子类执行复制构造函数的时候,会自动调用基类的复制构造函数,基类的 复制构造函数可能需要一个基类对象类型的引用变量,所以在子类复制构造函数里面需要用成员初始化列表来将子类对象的引用作为参数传递进去,因为基类的引用是可以指向子类的对象的,然后在基类的复制构造函数中会使用子类对象中属于基类对象的部分,然后给他们赋值。


子类的赋值运算符函数,不单单要负责子类部分数据的赋值,还要负责基类部分数据的赋值,所以在实现子类的赋值运算符函数的时候必须要显式调用基类的赋值运算符函数。


在类的继承中,有关于子类和基类都使用动态内存分配的方法实现有以下三种情况:

子类的析构函数会自动调用基类的析构函数

子类的复制构造函数需要通过初始化成员列表来传递子类对象的引用从而调用相应的基类复制构造函数。

子类的赋值运算符函数需要在函数内部利用作用域标识符的形式来调用基类的赋值运算符函数


以上的三种情况,都是因为子类对象的创建会自动创建基类的对象,在子类对象执行上面的操作的时候,基类对象的部分必须调用它们专属的方法进行处理。


七、类设计回顾

编译器总会为我们自动的生成一些比较重要的函数,比如默认的构造函数。

默认的构造函数包括两个版本,一个是不加任何参数的,第二个是即使有参数,但是参数列表里面的参数都是有默认值的。

基类默认的构造函数在子类的构造函数调用之前会先调用基类的构造函数,如果在子类构造函数的初始化列表中没有给基类传递任何参数,那么将会调用基类的默认的构造函数。


复制构造函数属于一种特殊的构造函数,他以所属类的对象作为参数,在用旧对象初始化新对象的时候,或者在传递参数的时候是按值传递对象,或者在函数的返回值是对象,或者在编译器生成临时的匿名对象的时候,都会调用复制构造函数。


复制构造函数一般用处比较多的就是参数的传递函数对象的初始化,编译器在我们使用复制构造函数的时候会为我们定义一个默认的复制构造函数,这个构造函数的功能就是按成员顺序从一个对象复制到另一个对象,如果不使用复制构造函数,编译器只提供声明,但是不提供定义。但是,如果类的成员里面有指针以及静态成员的时候,最好自己显示的定义一个复制构造函数,防止浅拷贝带来的内存问题。


默认的赋值运算符函数其实和复制构造函数的作用类似,只不过,在执行默认的赋值运算符函数的过程中,如果有的成员是特殊的类对象的话,那么他会自动去调用该类的赋值运算符。并且如果成员里面有指针这一类的类型,也需要自己本身来定义一个显式的赋值运算符函数。

编译器不会为我们定义不同类型成员的赋值运算符函数,如果想让不同类型的对象进行赋值的话,就需要定义特殊的符合要求的赋值函数。或者先使用转换函数,然后再使用赋值函数。


构造函数不能被继承的一个原因就是,每一个构造函数都担当着创建和初始化类类对象的工作,类继承的方法都是能够 让子类的对象用父类的方法,然而基类的构造函数似乎在子类中没什么重要的作用。


带有一个特殊参数的构造函数是用来把非类类型的对象转化为类类型的。我们可以定义参数不同的构造函数来实现这一功能。那么要把类类型转化为double这样的类型呢,就需要一种转换函数。转换函数必须是类成员函数,转换函数可以是没有参数的,也可以是由返回值类型没有参数的。即使转换函数不指定目标类型,也同样能够正常转换。

类名 要转换的类型名(){}这就是转换函数的基本格式                                                                                                                                                                      

在参数传递过程中,如果要传递的参数是对象,那么我们尽可能要用引用来作为参数,而不是进行普通的值传递,一个是普通的值传递可能会涉及到复制构造函数的使用还有就是效率问题,如果怕传递的引用肯能会在函数里面更改,可以使用一个const的引用来接受传递进来的参数。


并且在有虚函数的时候,被定义为接受基类引用参数的函数也可以接受派生类的对象参数。


在调用函数的时候,时常会提到到底是返回对象好一点还是返回引用好一点。单指效率来说,还是返回引用好一点。但是有些时候是只能返回对象不能返回引用的。比如,我们要返回一个在函数里面临时创建的一个对象,因为临时创建的对象在函数调用完成之后就会被销毁,所以返回引用是不行的,就只能够返回对象。返回对象就是返回一个临时的对象副本,并且在过程之中还会调用相应的复制构造函数创建出一个临时对象。


八、类继承的注意事项

派生类不能够继承基类的构造函数,所以创建子类对象的时候必须要执行子类的构造函数,并且通过成员初始化列表的形式来嗲用基类的构造函数,让他为我们创建子类对象中的基类部分。


析构函数也是同样不能够继承的,为了防止一些其他的问题发生,基类的析构 函数一般来说都要声明为虚函数。

赋值运算符函数主要用于同类对象的赋值,赋值运算符函数必须是成员函数,换句话说,每一个类都有自己的赋值运算符函数,在有子类和基类的时候,子类对象的赋值运算符函数将会自动调用基类的赋值运算符函数来对子类对象中属于基类的部分进行赋值,对于类中的成员是其他类的对象的时候,在赋值期间也要调用该类对应的赋值运算符函数。如果我们没有显式定义赋值运算符函数,那么编译器只会提供声明但是不会提供定义,一旦我们在程序中用到了类对象的赋值操作的话,编译器将会为我们自动定义一个赋值运算符函数。如果派生类中没有显式定义赋值运算符函数的需求的话,那么在赋值的时候会自动调用基类的赋值运算符函数来进行基类部分的赋值,但是如果子类中存在这种需要,在显式实现赋值运算符函数的过程中,不但要对子类的新成员进行赋值,同时也要显式的调用基类的赋值运算符函数才行。


在子类重新实现了基类当中的虚函数的时候,在调用的时候一定要传递对象的引用而不是值,因为传递值的话,如果重新实现的函数的形参是基类对象的话,那么可能会出现错误。


因为友元函数不是类成员函数,所以不能够继承,但是如果想在子类中访问基类中的友元函数,可以把子类的指针或者引用转为基类的类型,再调用基类的友元函数。


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值