10. 继承(Inheritance)
派生类必须使用类派生列表明确指出它是从哪个基类继承而来的。因为每个派生类对象都是属于基类的,并且一个基类可以由多个派生类,基类代表的对象比任意派生类代表的对象多。For example, the base class Vehicle represents all vehicles, including cars, trucks, boats, airplanes, bicycles and so on.By contrast, derived-class Car represents a smaller, more specific subset of all vehicles.
继承关系形成类的层级结构(class hierarchies),一个基类与派生类之间存在层级关系。虽然类可以独立存在,但一旦它们被使用在继承关系中,它们就成为其他类的附属。一个类要么成为基类- -向其他类提供成员,要么成为派生类- -从其他类继承成员,或者两者兼而有之。
10.1 基类和派生类
基类往往更加具有一般性,派生类往往更加具体。单继承(single inheritance),一个类是从一个基类派生出来的。多重继承(multiple inheritance),一个派生类同时从两个或者多个基类继承而来。
例如,在上图中,CommunityMember是Employee\Student\Alumnus的直接基类。是图中其他类的间接基类。
10.1.1 继承的基本概念
现在重新考虑上图中的继承层次,继承开始于基类Shape,为了指明TwoDimensionalShape是由Shape派生而来,TwoDimensionalShape类的定义可以写为:
class TwoDimensionalShape : public Shape
这是public继承的示例,也是最常用的一种形式。还有private和protected继承。对于所有形式的继承,基类的private成员不能由基类的派生类直接访问,但是这些基类的private成员仍然是继承的(即,他们仍然是派生类的一部分)。对于public继承,当基类变成派生类的一部分,除private外的成员仍然保持原始的成员访问权限。(例如,基类的public成员仍然是派生类的Public成员,protected成员仍然是派生类的protected成员)。通过继承基类的成员函数,派生类可以操控基类的private成员(如果这些继承的成员函数提供了基类的这种功能)。
继承关系并不适宜于所有的类的关系。一些情况下,复合(composition)关系更加适宜。类似地处理基类对象和派生类对象是可能的;它们的共性表现在基类成员身上。
10.1.2 基类和派生类的类型转换
公有(public继承)派生类对象可以被当作基类的对象使用,反之不可以。因为:
1.派生类的对象可以隐含转换为基类对象
2.派生类的对象可以初始化基类的引用
3.派生类的指针可以隐含转换为基类的指针
通过基类对象名、指针只能使用从基类继承的成员。
10.2 引用和指针
复合类型是指基于其他类型定义的类型,引用和指针属于其中的两种两种类型。与声明变量相比,定义复合类型的变量要复杂一些。一条声明语句由一个基本数据类型(base type)和紧随其后的一个声明符列表组成。每个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。
目前为止,我们接触到的声明语句中,声明符就是变量名,此时变量的类型也就是声明的基本数据类型。还可以有更复杂的声明符,它基于基本数据类型得到更复杂的类型,并把它指定给变量。
10.2.1 引用
引用是引用变量的简称,是C++新增的复合类型。引用是以定义的变量的别名。引用的主要用途是用作函数的形参和返回值。被引用的变量的生命周期一定要比引用长,这是使用引用的基本原则。
引用就是为类的对象起了另外一个名字,引用类型引用另外一种类型。通过将声明符写成&d的形式来定义引用类型,d为声明的变量名:
int ival=1024;
int& refVal=ival;
int& refVal;//错误,引用必须初始化
一般在初始化变量时,初始值会被拷贝到新建的对象中。然而在定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化,初始化后不可改变。
通过pass-by-reference,调用者赋予被调用函数直接访问调用者数据,并修改该数据的能力。
Pass-by-reference会削弱安全性,被调用函数可能会损坏调用者的数据。Pass-by-reference可以消除拷贝大量数据的传值开销。
带&的是引用型参数,它是地址传递,其实参会随着形参的改变而改变;不带&的参数是一般参数,是值传递,其实参不会随着形参的改变而改变。所以,结构改变,并且需要传回这种改变的要用引用型参数,否则用一般参数。
1.可以在一行中同时定义被引用变量和引用变量。
2.引用在定义时要进行初始化。
int x,&number=x;
or
int x;
int &number=x;
3.对引用的操作就是对被引用的变量的操作。
4.引用类型变量的初始值不能是常数。
int &ref=90;//错误!
5.可以使用动态分配的内存空间初始化一个引用变量。
6.C++指针与引用符号应该靠近类型而非名字
float* p;//不应该是float *p
int& x//不应该是int &x
在函数调用中,只需提及变量的名字(例如,number)就可以通过引用来传递它。在被调用函数的函数体内,引用参数实际上指的是调用函数的原始变量,原始变量可以通过被调用函数直接修改。函数原型和头文件必须一致。
在引用类型前面加上const,const 引用的目的是,禁止通过修改引用值来改变被引用的对象。
10.2.2 引用的本质
引用的本质是指针常量,传递的是变量的地址。传引用的代码更加简洁,传引用不必使用二级指针。
10.2.3 指针
指针是指向另外一种类型的复合类型。与引用相似,指针也实现了对其他对象的间接访问。指针本身就是一个对象,允许对指针赋值和拷贝,在指针的生命周期内它可以先后指向几个不同的对象。指针无需在定义时赋初值。在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。
空指针:空指针不指向任何对象,在试图使用一个指针之前代码应该首先检查它是否为空。
int *p1=nullptr;//等价于 int *p1=0;
int *p2=0;//直接将p2初始化为字面量0
//需要首先#include cstdlib
int *p3=NULL;
得到空指针最直接的办法就是使用字面值nullptr来初始化指针。nullptr是一种特殊的字面值,它可以被转换为任意其他的指针类型。
10.2.4 引用和 const
如果引用的数据对象类型不匹配,当引用为const时,C++将创建临时变量,让引用指向临时变量。
如果函数的实参不是左值或与const引用形参的类型不匹配,那么C++将创建正确类型的匿名变量,将实参的值传递给匿名变量,并让形参来引用该变量。
1.使用const可以避免无意中修改数据的编程错误
2.使用const可以使函数能够处理const和非const实参,否则只能接收非const实参
3.使用const,函数可以正确生成并使用临时变量
10.2.5 引用用于函数的返回值
传统的函数返回机制与值传递类似。函数的返回值被拷贝到一个临时位置(寄存器或栈),然后调用者程序再使用这个值。返回引用的语法:
返回的数据类型& 函数名(形参列表){
}
1.如果返回局部变量的引用,此引用本质是野指针。
2.可以返回函数的引用形参、类的成员、全局变量、静态变量。
10.3 基类和派生类的关系
使用继承,而不是复制粘贴
10.3.1 基类和派生类的构造函数和析构函数
派生类的构造函数必须调用基类的构造函数
析构函数和友元函数,不能从基类继承而来。
当派生类的构造函数调用基类的构造函数时,传向基类构造函数的实参必须与基类的构造函数指定的参数的个数和类型一致,否则会出现编译错误。
在派生类的头文件中,使用#include包含基类。
之前的章节介绍了public和protected访问修饰符。一个基类的公共成员可以在它的体内和任何地方访问,即程序对此类的对象有一个句柄(名字,引用或指针)或者此类的派生类。基类的private成员只能在体内或者friend类访问。
使用protected访问修饰符提供了一种介于public和private之间的访问等级。将基类的原本的private成员改为protected的,则派生类就可以直接访问基类的private成员。基类的protected成员可以在基类的体内访问,也可以被基类的成员和类的friend访问,还有派生类的成员和派生类的friend也可以访问。
使用继承可以使得代码更容易维护。
10.4 派生类中的构造函数和析构函数
构造函数:先调用基类的构造函数,再调用派生类的构造函数;基类先,派生类后
析构函数:先调用派生类的析构函数,再调用基类的析构函数。派生类先,基类后
当程序创建派生类对象时,派生类构造函数会立即调用基类构造函数,基类构造函数的主体将执行,派生类的成员初始值设定项将执行,最后派生类构造函数的主体将执行。如果层次结构包含两个以上的级别,则此过程将向上级联。
当派生类对象被销毁时,程序将调用该对象的析构函数。这开始了析构函数调用的链(或级联),其中派生类析构函数以及直接和间接基类的析构函数以及类成员的执行顺序与构造函数的执行顺序相反。当调用派生类对象的析构函数时,析构函数将执行其任务,然后调用层次结构中下一个基类的析构函数。此过程将重复进行,直到调用层次结构顶部的最后一个基类的析构函数。然后,从内存中删除该对象。
假设我们创建一个派生类的对象,其中基类和派生类都包含(通过组合)其他类的对象。创建该派生类的对象时,首先执行基类成员对象的构造函数,然后执行基类构造函数主体,然后执行派生类成员对象的构造函数,然后执行派生类的构造函数主体。派生类对象的析构函数的调用顺序与其相应构造函数的调用顺序相反。
默认情况下,基类构造函数、析构函数和重载赋值运算符不由派生类继承。但是,派生类构造函数、析构函数和重载赋值运算符可以调用基类版本。
10.4.1 继承基类的构造函数
子类为完成基类初始化,在 C++11 之前,需要在初始化列表调用基类的构造函数,从而完成构造函数的传递。
如果派生类没有构造函数,在这种情况下,C++会为派生类生成默认构造函数,并隐式的调用基类的默认构造函数。
有时,派生类的构造函数只是指定与基类的构造函数相同的参数,并简单地将构造函数参数传递给基类的构造函数。C++11 允许您指定派生类应继承基类的构造函数。为此,需要显式包含 using 声明
-
C++11规定
-
可用using语句继承基类构造函数。
-
但是只能初始化从基类继承的成员。
- 派生类新增成员可以通过类内初始值进行初始化。
-
语法形式:
-
using BaseClass::BaseClass;//继承基类的所有构造函数
- 如果派生类有自己新增的成员,且需要通过构造函数初始化,则派生类要自定义构造函数。可以在派生类构造函数中调用基类构造函数。
除了少数例外(下面列出),对于基类中的每个构造函数,编译器都会生成一个派生类构造函数,该构造函数调用相应的基类构造函数。每个生成的构造函数都与派生类同名。
When you inherit constructors:
1.每个生成的构造函数都具有与其对应的基类构造函数相同的访问说明符(public、protected 或 private)。
2.copy 和 move 构造函数不会被继承。
3.如果通过在其原型中放置 = delete 来删除基类中的构造函数,则派生类中的相应构造函数也会被删除。
4.如果派生类未显式定义构造函数,编译器仍会在派生类中生成默认构造函数。
5.如果在派生类中显式定义的构造函数具有相同的参数列表,则不会继承给定的基类构造函数。
6.基类构造函数的默认参数不会被继承。相反,编译器在派生类中生成重载构造函数。例如,如果基类声明构造函数
BaseClass(int = 0, double = 0.0);
编译器生成以下两个不带默认参数的派生类构造函数
DerivedClass();
DerivedClass(int);
DerivedClass(int, double);
它们都调用指定默认参数的 BaseClass 构造函数。
如果不继承基类的构造函数:
1.派生类的新增成员:派生类定义构造函数初始化
2.继承来的成员:调用基类构造函数进行初始化
3.派生类的构造函数需要向基类的构造函数传递参数
单继承时构造函数的定义语法:
派生类名::派生类名(基类所需的形参,本类成员所需的形参):基类名(参数表),本类成员初始化列表
{
//其他初始化操作;
};
多继承且有对象成员时派生类的构造函数的定义:
派生类名::派生类名(形参表):
基类名1(参数),基类名2(参数),...,基类名n(参数),本类成员(含对象成员)初始化列表
{
//其他初始化操作;
};
构造函数的执行顺序:
1.调用基类的构造函数,基类构造函数的调用顺序按照它们被继承时声明的顺序(自左向右)
2.对初始化列表中的成员进行初始化。顺序按照它们在类中定义的顺序;对象成员初始化时自动调用其所属类的构造函数
3.执行派生类的构造函数体中的内容
10.4.2 继承中的默认构造函数
若基类的构造函数未被显式的调用,基类的默认构造函数就会被调用。
例如:
Circle(){
radius=1.0;
}//等价于下列的函数形式
Cirlce():Shape{} {
radius=1.0;
}//Shape为Circle的基类
所以,要考虑给基类提供默认构造函数。
10.4.3 派生类的复制构造函数
从基类继承而来的成员的复制构造由基类的复制构造函数完成,派生类新增的成员的复制构造由派生类的复制构造函数完成。
若派生类没有声明复制构造函数:编译器会在需要时生成一个隐含的复制构造函数,先调用基类的复制构造函数,再为派生类新增的成员执行复制。
若派生类定义复制构造函数:1.一般要为基类的复制构造函数传递参数 2.复制构造函数只能接收一个参数,既用来初始化派生类定义的成员,也将被传递给基类的复制构造函数 3.基类的复制构造函数形参类型是基类对象的引用,实参可以是派生类对象的引用。
10.4.4 派生类的析构函数
析构函数不被继承,派生类如果需要,要自行声明析构函数
声明方法与无继承关系时类的析构函数相同
不需要显式的调用基类的析构函数,系统会自动的隐式调用
先执行派生类析构函数的函数体,再调用基类的析构函数
10.4.5 访问从基类继承的成员(继承中的名字隐藏)
当派生类与基类中有相同成员时:
1.若未特别限定,则通过派生类对象使用的是派生类中的同名成员
2.如要通过派生类对象访问基类中被隐藏的同名成员,应使用基类名和作用域操作符(::)来限定。
内部作用域的名字隐藏外部作用域的(同名)名字
而派生类视为内部作用域,基类视为外部作用域。
可以避免某些潜在的危险行为
10.5 重定义函数
- Redefining Functions (重定义函数)
Shape::toString() 被Circle继承,因此可以通过Circle对象调用该函数。
std::cout << circle.toString();
但基类的toString()无法输出派生类对象信息
可以重定义派生类的toString()以描述派生类对象
#include <string>
std::string Shape::toString() {
using namespace std::string_literals;
return "Shape color "s + color
+ ((filled) ? " filled"s : " not filled"s);
}
string Circle::toString() {
return "Circle color " + color +
" filled " + ((filled) ? "true" : "false");
}
string Rectangle::toString() {
return "This is a rectangle object";
}
- Redefine (重定义)
- Redefine v.s. Overload (重定义与重载)
3.1. Overload Functions (重载函数)
3.1.1. more than one function with the same name (多个函数名字相同)
3.1.2. But different in at least one of the signatures: (但至少一个特征不同)
(1) parameter type (参数类型)
(2) parameter number (参数数量)
(3) parameter sequence (参数顺序)
3.2. Redefine Functions (重定义函数)
3.2.1. The functions have the same signature (函数特征相同)
(1) Name (同名)
(2) Parameters (including type, number and sequence) (同参数:类型,数量和顺序)
(3) Return type (返回值类型)
3.2.2. Defined in base class and derived class, respectively (在基类和派生类中分别定义)
10.6 public, protected and private Inheritance(访问控制属性)
我们一般使用public继承,使用protected继承比较少见。
当使用public继续生成派生类,基类中的public成员变成了派生类的public成员,基类的protected成员变成了派生类的protected成员。基类的Private成员不可以由派生类直接访问,但是可以通过基类的public和protected成员函数访问。
当使用基类派生派生类,使用public\protected\private都有可能。
public继承:
基类的public和protected成员,访问属性在派生类中保持不变;基类的private成员,派生类的成员函数不可以直接访问。派生类中的成员函数可以直接访问基类的public和protected成员,但是不能直接访问基类的private成员。通过派生类的对象,只能访问public成员。
private继承:是最严格的一种继承
基类的public和protected成员都以private的身份出现在派生类中;基类的private成员,派生类的成员函数不可以直接访问。派生类中的成员函数可以直接访问基类中的public和protected成员,但是不能直接访问基类的private成员。通过派生类的对象,不能访问从基类继承的任何成员。
protected继承:
基类的public和protected成员都以protected的身份出现在派生类中;基类的private成员,不可以直接访问。派生类中的成员函数,可以直接访问基类中的public和protected成员,但是不能直接访问基类的private成员。通过派生类的对象,不能访问从基类继承的任何成员。
10.7 虚基类
当派生类从多个基类继承,而这些基类又有共同基类,则在访问此共同基类的成员时,将产生冗余,并有可能因冗余带来不一致性。
虚基类的声明:以virtual说明基类继承方式。例如:
class B1:virtual public B
虚基类主要用来解决多继承时可能发生的对同一基类继承多次而产生的二义性问题;为最远的派生类提供唯一的基类成员,而不重复产生多次复制。
在第一级继承时就要将共同基类设计为虚基类
标签:10,继承,成员,基类,引用,派生类,构造函数 From: https://www.cnblogs.com/yyyylllll/p/18390173