文章目录
15.1 OOP:概述
- 面向对象程序设计的核心思想是
数据抽象
、继承
和动态绑定
(有时也被称为运行时绑定
)- 通过使用数据抽象,我们可以将类的接口与实现分离
- 使用继承,可以定义相似的类型并对其相似关系建模
- 使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象
15.2 定义基类和派生类
- 基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此
- 基类通过在其成员函数的声明语句之前加上关键字
virtual
使得该函数执行动态绑定:- 任何构造函数之外的非静态函数都可以是虚函数
- 关键字
virtual
只能出现在类内部的声明语句之前而不能用于类外部的函数定义 - 如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数
- 如果派生类没有覆盖其基类中的某个虚函数,则该函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本
- 派生类可以在它覆盖的函数前使用
virtual
关键字,但不是非得这么做 - 如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。不论从基类中派生出多少个派生类,对于每个静态成员来说都只存在唯一的实例
- 静态成员遵循通用的访问控制规则
- 表达式的
静态类型
在编译时总是已知的,它是变量声明时的类型或表达式生成的类型;动态类型则是变量或表达式表示的内存中的对象的类型,动态类型直到运行时才可知- 绑定到派生类的基类的指针或引用其动态类型只有在运行时才只知道
- 当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分将被忽略掉
15.3 虚函数
- 当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同
- 基类中的虚函数在派生类中隐含地也是一个虚函数。当派生类覆盖了某个虚函数时,该函数在基类中的形参必须与派生类中的形参严格匹配;否则会产生意想不到的结果。
- 在c++11新标准猴子那个我们可以使用
override
关键字来说明派生类中虚函数,这么做的好处是在使得程序员的意图更加清晰的同时让编译器可以为我们发现一些错误(见例子一) - 我们还能把某个函数指定为
final
,指定为final
的函数不能被覆盖,否则将引发错误(见例子二)final
和override
说明符出现在形参列表(包括任何const或引用修饰符)以及尾置返回类型之后,即放在修饰符最后
- 如果虚函数使用默认参数,则基类和派生类中定义的默认实参最好一致,因为使用的始终是基类中定义的默认实参
//例子一:使用override声明被覆盖的virtual函数
struct B{
virtual void f1(int) const;
virtual void f2();
void f3();
};
struct D1:B{
void f1(int) const override; //正确:f1与基类中的f1匹配
void f2(int) override; //错误:B没有形如f2(int)的函数
void f3() override; //错误:f3不是虚函数
void f4() override; //错误:B没有名为f4的函数
};
//例子二:指定函数为final防止被覆盖
struct D2:B{
//从B继承f2()和f3(),覆盖f1(int)
void f1(int) const final; //不允许后续的其他类覆盖f1(int)
};
struct D3:D2{
void f2(); //正确:覆盖从间接基类B继承而来的f2
void f1(int) const; //错误:D2已经将f2声明成final
};
15.4 抽象基类
- 我们通过在函数体的位置(即在声明语句的分号之前)书写
=0
就可以建个一个虚函数说明为纯虚函数。其中=0
只能出现在类内部的虚函数声明语句。 - 我们也可以为纯虚函数提供定义,不过函数体必须定义在类的外部。也就是说,我们不能在类的内部为一个
=0
的函数提供函数体 - 含有(或未经覆盖直接继承)纯虚函数的类是
抽象基类
。我们不能(直接)创建一个抽象基类的对象。
15.5 访问控制与继承
public | protected | private | |
---|---|---|---|
类成员是否可以访问 | Yes | Yes | Yes |
友元函数是否可以访问 | Yes | Yes | Yes |
子类是否可以访问 | Yes | Yes | No |
类的实例化对象是否可以访问 | Yes | No | No |
public | protected | private | |
---|---|---|---|
public继承 | public | protected | private |
protected继承 | protected | protected | private |
private | private | private | private |
-
访问控制的权限如上表一所示(参考C++ 类访问控制)
-
某个类对继承而来的成员的访问权限受到两个因素影响:一是在基类中该成员的访问说明符,二是在派生类的派生列表中的访问说明符。三种继承方式导致的权限变化见上表二(参考C++ 类访问控制)
-
(*重要)派生类向基类的转换是否可访问由使用该转换的代码决定,同时派生类的派生访问说明符也会有影响。假定
D
继承自B
(见例子一):- 只有当
D
公有地继承B
时,用户代码才能使用派生类向基类的转换;如果D
继承B
的方式是受保护的或者私有的,则用户代码不能使用该转换。 - 不论
D
以什么方式继承B
,D
的成员函数和友元都能使用派生类向基类的转换;派生类向其直接基类的类型转换对于派生类的成员和友元来说永远是可访问的。 - 如果
D
继承B
的方式是公有的或者受保护的,则D
的派生类的成员和友元可以使用D
向B
的类型转换;反之,如果D
继承B
的方式是私有的,则不能使用。
- 只有当
-
使用
class
关键字定义的派生类是私有继承的;而使用struct
关键字定义的派生类是公有继承的。(见例子二)
//例子一:派生类向基类转换的可见性
class Base{};
//下面三条判断的依据是:
// 派生类向其直接基类的类型转换对于派生类的成员和友元来说永远是可访问的
struct Pub_Derv:public Base{
//合法
void memfcn(Base &b){ b=*this;}
};
struct Prot_Derv:protected Base{
//合法
void memfcn(Base &b){ b=*this;}
};
struct Priv_Derv:private Base{
//合法
void memfcn(Base &b){ b=*this;}
};
//下面三条判断的依据是:
// 如果`D`继承`B`的方式是公有的或者受保护的,则`D`的派生类的成员和友元可以使用`D`向`B`的类型转换
struct Derived_from_Public:public Pub_Derv{
//合法
void memfcn(Base &b){ b=*this;}
};
struct Derived_from_Protected:public Prot_Derv{
//合法
void memfcn(Base &b){ b=*this;}
};
struct Derived_from_Private:public Priv_Derv{
//不合法
void memfcn(Base &b){ b=*this;}
};
//下面六条判断的依据是:
// 只有当`D`公有地继承`B`时,用户代码才能使用派生类向基类的转换
int main(){
Pub_Derv d1;
Base* p1=&d1; //正确
Prot_Derv d2;
Base* p2=&d2; //错误
Priv_Derv d3;
Base* p3=&d3; //错误
Derived_from_Public dd1;
Base* p4=&dd1; //正确
Derived_from_Private dd2;
Base* p5=&dd2; //错误
Derived_from_Protected dd3;
Base* p6=&dd3; //错误
};
//例子二
class Base { /* ... */};
struct D1:Base { /* ... */}; //默认public继承
class D2: Base { /* ... */}; //默认private继承
15.6 继承中的类作用域
- 如果派生类(即内层作用域)的成员与基类(即外层作用域)的某个成员同名,则派生类将在其作用域内隐藏此基类成员。即使派生类成员和基类成员的形参列表不一致,基类成员也仍然会被隐藏掉(只要重名就被隐藏)
- (*重要)在c++的继承体系中函数调用的解析过程如下所示(以
p->mem()
为例)(见例子一):- 首先确定
p
的静态类型 - 在
p
的静态类型对应的类中查找mem
。如果找不到,则依次在直接基类中不断查找直至到达继承链的顶端。如果找遍了该类及其基类仍然找不到,则编译器将报错。 - 一旦找到了
mem
,就进行常规的类型检测以确认对于当前找到mem
,本次调用是否合法 - 假设调用合法,编译器将根据调用的是否是虚函数而产生不同的代码:
- 如果
mem
是虚函数且我们是通过引用或指针进行的调用,则编译器产生的代码在运行时确定到底运行该虚函数的哪个版本依据是对象的动态类型 - 反之,如果
mem
不是虚函数或者我们是通过对象(而非引用或指针)进行的调用,则编译器将产生一个常规函数调用
- 如果
- 首先确定
//例子一:继承体系中的函数解析
class Base{
public:
virtual int fcn();
};
class D1:public Base{
public:
//隐藏基类的fcn,这个fcn不是虚函数
//D1继承了Base::fcn()的定义
int fcn(int); //形参列表与Base中的fcn不一致
virtual void f2(); //是一个新的虚函数,在Base中不存在
};
class D2:public D1{
public:
int fcn(int); //是一个非虚函数,隐藏了D1::fcn(int)
int fcn(); //覆盖了Base的虚函数fcn!!!
void f2(); //覆盖了D1的虚函数f2
};
Base bobj; D1 d1obj; D2 d2obj;
Base *bp1=&bobj,*bp2=&d1obj,*bp3=&d2obj;
bp1->fcn(); //虚调用,将在运行时调用Base::fcn
bp2->fcn(); //虚调用,将在运行时调用D1::fcn()
bp3->fcn(); //虚调用,将在运行时调用D2::fcn()
D1 *d1p=&d1obj; D2 *d2p=&d2obj;
bp2->f2(); //错误:Base没有名为f2的成员
d1p->f2(); //虚调用,将在运行时调用D1::f2()
d2p->f2(); //虚调用,将在运行时调用D2::f2()
15.7 构造函数与拷贝控制
- 派生类删除的拷贝控制与基类的关系:基类中某个成员(默认构造函数,拷贝构造函数,拷贝赋值运算符,析构函数,移动操作)被删除,则派生类中对应的成员也将是被删除的。原因是编译器不能使用基类对应的成员来执行派生类中基类部分的构造、赋值、销毁或移动等操作。
- 派生类的拷贝和移动构造函数在拷贝和移动自有成员的同时,也要拷贝和移动基类部分的成员。
- 如果我们想拷贝(或移动)基类部分,则必须在派生类的构造函数初始值列表中显示地使用基类的拷贝(或移动)构造函数(见例子一)
//例子一
class Base { /* ... */};
class D:public Base{
public:
D(const D& d):Base(d) //拷贝基类成员
/* D的成员的初始值 */ { /* ... */ }
D(D&& d):Base(std::move(d) //移动基类成员
/* D的成员的初始值 */ { /* ... */ }
};
15.8 容器与继承
- 当我们希望在容器中存放具有继承关系的对象时,应当在容器中放置(智能)指针而非对象(见例子一)
//例子一
vector<shared_ptr<Quote>> basket;
basket.push_back(make_shared<Quote>("0-201-84470-1",50));
//Bulk_quote是Quote的子类
basket.push_back(make_shared<Bulk_quote>("0-201-54848",50,10,.25));
//实际调用net_price版本指针所指对象的动态类型即Bulk_quote
cout<<basket.back()->net_price(15)<<endl;
15.9 文本查询程序再探
(本节主要是编程实践)