概念
继承、基类和派生
- 能够从已有的类继承出新的类,原始类成为基类,继承类成为派生类。
- 基类负责定义在层次关系中所有类共同拥有的成员
- 派生类继承了基类原有的特性,但是也派生出了其特有的新特征。
虚函数
- 基类将类型相关的函数与派生类不做改变而直接继承的函数区分对待
- 对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数(virtual)
virtual
- 派生类必须在其内部,对所有重新定义的虚函数进行声明,派生类可以在这样的函数前面加上virtual关键字
- 关键字必须出现在类内部的声明语句之前,而不能用于类外部的函数定义
- 如果一个函数被声明成虚函数,那么该函数在之后的派生类中隐式的也是虚函数
- 但是并不是非得这么做,新标准允许派生类显式的注明,它将使用哪个成员函数来改写基类的虚函数。具体的措施就是在该函数的形参列表之后增加一个override关键字。
动态绑定
- 当我们使用引用或指针调用虚函数时将发生动态绑定
- 大概意思就是:当一个虚函数被动态绑定时,该函数会根据绑定参数的类型不同,来选择执行基类还是派生类的版本
- …
定义基类和派生类
定义基类
- 基类通常都应该定义一个虚析构函数!
- 如果基类把一个函数声明成虚函数,那么该函数在派生类中隐式的也是虚函数
访问控制与继承
- 派生类能访问公有成员,而不能访问私有成员
- protect关键字:受保护的成员,基类希望它的派生类能访问该成员,同时禁止其他用户访问
定义派生类
- 派生类必须通过派生列表明确指出他是从哪个基类继承而来的。
- 类派生列表的形式是,首先一个冒号,后面紧跟以逗号分隔的基类列表
- 其中每个基类前面可以有以下三种访问说明符中的一个public,protected,private
- 举例
class Jicheng : public Jilei{ ... };
- 访问说明符的作用是:控制派生类从基类继承而来的成员是否对派生类的用户可见
- 大多数类都只继承自一个类,这种形式的继承被称作“单继承”
派生类构造函数
- 尽管在派生类对象中含有从基类继承而来的成员,但是派生类并不能直接初始化这些成员。
- 派生类也必须使用基类的构造函数来初始化它的基类部分
- 首先初始化基类的部分,然后按照声明的顺序,依次初始化派生类的成员
eg1:
RatedPlayer::RatedPlayer(unsigned int r, const string &fn, const string &ln, bool ht) : TableTennisPlayer(fn, ln, ht)
{
...
}
//本例中,r为派生类新增,fn,ln,ht为基类继承而来
//派生类RatedPlayer的构造函数RatedPlayer
//其参数fn,ln,ht就需要基类构造函数来初始化
//因此,将fn,ln,ht三个参数,传递给基类构造函数TableTennisPlayer();
eg2:
Bulk_quote(const string &book, double p, std::size_t qty, double disc) : Quote(book, p), min_qty(qty), discount(dis)
{
...
}
//本例中,qty, dis为派生类新增,book, p为基类继承而来
//Bulk_quote为派生类,Quote为基类
//前面两个参数book, p 传递给基类Quote的构造函数,由Quote的构造函数负责构造
//构造完成后,初始化由派生类直接定义的min_qty成员和discount成员
继承与静态成员
- 如果基类定义了一个静态成员,则在整个继承体系中,只存在该成员的唯一定义
- 不论从基类中派生出来多少个派生类,对于每个静态成员来说,都只存在唯一实例
- 静态成员遵循通用的访问控制规则:在基类中的成员是private的,则派生类无权访问;如果是public/protect的,则,我们可以通过基类/派生类使用它
派生类的声明
- 声明中包含类名,但是不包含它的派生列表
class Bulk_quote : public Quote; //错误,派生列表不能出现在这里
class Bulk_quote; //正确!!!
防止继承
- 定义一种类,我们不希望其他类继承它
- 在类名的后面根一个关键字final
class Base{ ... }; //这是一个基类'
class Last final : Base { ... }; //final 就是代表,Last不能作为基类进行继承
class NO final { ... }; //NO不能作为基类
类型转换与继承
动态类型与静态类型
- 表达式的静态类型在编译时总是已知的,它是变量声明时的类型或表达式生成的类型
- 动态类型则是变量或表达式表示的内存中的对象的类型,直到运行时才可知
不能执行基类向派生类的自动类型转换
- 如果转换合法,那么可能会访问基类中不存在的,但是派生类存在的成员
- 即使一个基类指针或引用绑定在一个派生类对象上,我们也不能执行基类向派生类的转换。如下:
- Quote是基类
派生类可以向基类自动类型转换,只对指针或引用有效
- 当一个派生类对象为一个基类对象初始化或赋值时,只有派生类中的基类部分会被拷贝移动或赋值,它的派生类部分将被忽略
虚函数
- 当我们使用基类的引用或指针调用一个虚函数时,会执行动态绑定
- 因为我们直到运行是才能直到到底调用了哪个版本的虚函数
- 所以所有虚函数都必须有定义,不管它是否被用到
- 必须注意:动态绑定只有当我们通过指针或引用调用虚函数时才会发生
关键概念:C++的多态性
- OOP的核心思想是多态性
- 我们把具有继承关系的多个类型称为多态类型,因为我们能使用这些类型的”多种形式“而无需在意他们的差异
- 动态和静态的区别在于,前者根据对象类型决定调用的虚函数版本,后者根据返回类型调用函数版本。
- 静态绑定/联编,是在编译时就确定的,函数的调用类型/版本。但是因为虚函数的存在,使得很难确定。于是就有了动态绑定/联编
- 动态绑定/联编,是在程序运行时,根据对象类型,确定调用的函数版本
- 当不是指针或引用,调用虚函数的时候,都使用静态绑定/联编。即,不因为对象的不同而不同,而只看初始化规定的类型。
- 当使用的是指针或引用,调用虚函数的时候,会使用动态绑定/联编。会根据对象的类型,而调用指定类的虚函数。
- 当且仅当通过对指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同
- 当不是虚函数的时候,则都使用静态
eg: BrassPluss是Brass的派生类
//前提为:ViewAcct为虚函数!!!!!
void fr (Brass & br); // use br.ViewAcct()
void fp (Brass * bp); // use bp.ViewAcct()
void f (Brass b); // use b.ViewAcct()
int main()
{
Brass B("Billy Bee", 123432, 1000.0);
BrassPluss BP("Betty Beep", 232312, 12345.0);
fr(B); // use Brass::ViewAcct()
fr(BP); // use BrassPlus::ViewAcct()
fp(B); // use Brass::ViewAcct()
fp(BP); // use BrassPlus::ViewAcct()
f(B); // use Brass::ViewAcct()
f(BP); // use Brass::ViewAcct()
}
// 不带引用或者指针的,函数参数里里本来是什么,最后调用的就是什么,不会因为参数不同该改变
// 带有引用或指针的,就需要根据参数来决定,最后到底调用的哪个函数
//其实质是,在不同的f()中,其定义的参数类型都是Brass,当静态(即没有引用和指针的时候)的时候,只关注于类型
//当动态时,我们不止关注f(Brass arg)的参数的类型,我们还关注,实参的对象的类型。此时调用的是实参对象对应的虚函数
- 非虚函数
BrassPlus ophelia;
Brass * bp;
bp = &ophelia;
bp->ViewAcct(); //此时调用的是谁的ViewAcct() ???
//如果ViewAcct()不是虚函数,那么,使用的静态联编,调用的就是Brass的版本
//如果是虚函数,动态联编,使用的BrassPlus的版本
虚函数的工作原理
- 在基类中,有一个虚函数表,存放着每个虚函数的地址
- 每当虚函数被覆盖时,就会更新这个虚函数表
- 因此,使用虚函数时,会有一定的成本:
- 额外的存储地址的空间,需要额外创建一个虚函数表,额外的查表操作
派生类中的虚函数
- 当我们在派生类中覆盖了某个虚函数时,可以再一次使用virtual关键字指出该函数的性质
- 但是这么做也并非必须,因为:一旦某函数被声明为虚函数,则在所有派生类中,它都是虚函数
- 如果一个虚函数被派生类覆盖,则覆盖后的虚函数的参数列表和返回值应该和以前的相同,除非:
override
- 上面讲到,派生类的虚函数如果要重写,则要与基类的虚函数名字相同,参数也要相同
- 如果参数不同,也不会报错,编译器会认为这是一个新的函数,会形成一个新的函数,也不会把我们想要覆盖的虚函数覆盖、
- 这就会导致我们没有想到的错误。因为我们想要重写(覆盖)虚函数,但是我们不小心写错了参数。此时虚函数就没有被覆盖,用的还是以前的虚函数。
- 此时就需要override:
- 使用override标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错!
final和override出现在形参列表之后
如果存在后置返回类型,则应在后置返回之后
避免动态绑定,强制执行虚函数的某个特定版本
- 作用域运算符:
double undiscounted = base-> Quote::net_price(42);
//该代码强调用Quote的net_price函数
//而不管base实际指向的对象的类型到底是什么
- 何时会用到:通常是,一个派生类的虚函数调用它覆盖过的基类的虚函数时
派生类构造函数只初始化它的直接基类
- 其实意思就是
- 你的基类或者间接基类初始化时需要什么参数,那么你作为派生类,初始化时就也需要这些参数,即使你的类里没有自己的数据成员
- 因为你作为派生类,是包含有基类的非私有成员的
抽象基类
-
纯虚函数
-
表示当前函数是没有实际意义的
-
double func(std::size_t) = 0;
-
其中,=0只能出现在类内部的虚函数声明语句处
-
含有纯虚函数的类就是抽象基类
-
除非后续派生类覆盖了该纯虚函数,否则它还是抽象基类
-
包含纯虚函数的类,只能用作基类
访问控制与继承
- 对于外部来说,protect成员的行为与private成员相似
- 对于派生类来说,protect成员的行为和public成员相似
- 派生类的成员和友元,只能访问派生类对象中的基类部分的protect成员(其实意思就是,访问基类的受保护成员只能通过派生类对象,此时它就像是public。但是如果通过非派生类对象访问的话,那它就是private的。不能访问)
公有,私有和受保护继承
- class A : public B{…}
- class A : private B{…}
- class A : protect B{…}
- 三种访问说明符,都不影响派生类对于基类的访问
- 这三者的差别就在于,即B在A内是public的,还是private的,还是protect的
默认的继承
- class Base{…}
- struct D1: Base{…} //等价于 class D1:public Base{…}
- class D2: Base{…} //等价于 class D2:private Base{…}
- 这个其实指的就是基类成员的可访问性
- 派生类向基类转换的可访问性
- 假定D继承自B
- 只有当D公有继承B时,用户代码才能使用派生类向基类的转换。否则不能使用该转换
- (解释:当公有继承的时候,用户代码可以直接访问基类的成员函数等,因此可以直接进行转换。否则对于用户代码来说,都是不可访问的)
- 不论什么方式继承B,D的成员函数和友元都能使用派生类向基类的转换
- (解释:对于成员函数和友元来说,基类中的私有成员都是可访问的,因此,对于他们来说,不管什么继承方式,都不会影响他们对于基类成员的访问)
- D以public或protect继承B,则D的派生类的成员和友元可以使用D向B的类型转换
- (解释:同上,派生类可以访问公有或保护继承而来的成员)
- 友元关系不能继承
- using改变个别成员的可访问性
class Base{
public:
std::size_t size() const { return n; }
protected:
std::size_t n;
};
class Derived : private Base{
public:
using Base::size;
protected:
using Base::n;
};
继承中的类作用域
- 每个类定义自己的作用域,在这个作用域内,我们定义类的成员
- 派生类的作用域位于基类作用域之内
- 在编译时,进行名字查找
- 因为下例中,函数不是虚函数,因此不存在动态绑定
名字冲突与继承(当派生类函数与基类函数同名)
- 派生类的成员,将隐藏同名的基类的成员,即使派生类成员和基类成员的形参列表不同
- 同样,我们也可以使用作用域,来使用被隐藏的基类成员。就像上面说的,可以用作用域,强制指定使用被覆盖的虚函数一样
- 作用域指的 类名::a; using
- 如果派生类中的函数成员,与基类的某个函数成员同名
- 则派生类将在其作用域内隐藏该基类成员,即使两者的形参列表不一致
- 看下面两个图
- 但是,如果是派生类中某个函数同基类的虚函数同名,则,只有当函数名和形参都相同时,才会覆盖基类的虚函数
- 看完图后,现在就可以理解,为什么基类和派生类的虚函数要求必须有相同的形参列表了
- 虚函数和非虚函数的覆盖
这里关键要多理解这个f2的错误
构造函数与拷贝控制
虚析构函数
- 如果会有派生类,那么基类的析构函数一定要为虚析构函数