15.2 定义基类和派生类
1、派生类如果没有覆盖基类中的某个虚函数,则该虚函数的行为类似于其他普通成员,派生类会直接继承其在基类中的版本
2、每个类控制它自己的成员初始化过程,派生类中的基类成员必须使用基类的构造函数进行初始化
3、我们可以将基类的指针或引用绑定到派生类对象上来实现多态,但不存在基类向派生类的隐式类型转换,
4、对象之间不存在类型转换
- 派生类和基类类型之间不存在类型转换
- 当使用派生类初始化/赋值一个基类类型对象时,实际上时调用了基类的构造函数或拷贝函数,虽然传递了派生类对象,但因为调用的基类的构造函数/拷贝函数,因此只能处理基类自己的成员
- 当我们使用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分将被忽略掉
15.3 虚函数
1、当派生类覆盖了某个虚函数时,该函数在基类中的形参必须和派生类中的形参一致(虚函数的返回类型可以是类本身的指针或引用)
2、虚函数也可以拥有默认实参,如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定
3、回避虚函数机制
通常当一个派生类的虚函数调用它覆盖的基类的虚函数的版本时,需要使用作用域运算符回避虚函数机制
//强行调用基类中定义的函数版本
double undiscounted = baseP->Quote::net_price(42);
15.4 抽象基类
1、含有纯虚函数的类是抽象基类,抽象基类负责定义接口给派生类覆盖,抽象基类不能用来创建对象
15.5 访问控制与继承
1、派生类的成员和友元只能通过派生类对象访问派生类中基类部分的受保护成员
class Base {
protected:
int prot_mem;
};
class Sneaky : public Base {
friend void clobber(Sneaky&); //能访问Sneaky::port_mem
friend void clobber(Base&); //不能访问Base::prot_mem
int j;
};
//right
void clobber(Sneaky &s) {
s.j = s.prot_mem = 0;
}
//error
void clobber(Base &b) {
b.prot_mem = 0;
}
2、公有、私有和受保护继承
- 派生访问说明符对于派生类的成员(及友元)能否访问其直接基类的成员没有任何影响,对基类成员的访问权限只和基类中的访问说明符有关。派生类可以访问基类中public、protected成员,基类友元可以访问基类中的所有成员
- 派生访问说明符的目的是控制派生类用户(包括派生类的派生类)对于基类的访问权限,访问权限见下表
public继承 | protected继承 | private继承 | |
---|---|---|---|
public成员 | public | protected | 隔离 |
protected成员 | protected | protected | 隔离 |
private成员 | private | private | 隔离 |
3、通过using声明改变派生类继承的个别成员的访问级别
- 派生类只能为那些它可以访问的直接或间接基类中的任何可访问成员提供using声明
- using声明语句中名字的访问权限由该using声明语句之前的访问说明符来决定
4、默认继承保护级别
- 默认情况下,使用class关键字定义的派生类默认是私有继承的,使用struct关机子定义的派生类默认是公有继承的
- 类似的使用class定义的类定义在第一个访问说明符之前的成员是private的,使用structure定义的类定义在第一个访问说明符之前的成员是public的,即唯一的区别就是默认访问权限
15.6 继承中的类作用域
1、在C++中名称的查找是由内而外的,先派生类后基类,派生类的作用域嵌套在基类中,如果一个名字在派生类的作用域找不到,编译器会继续在基类作用域中查找
2、一个对象、引用或指针的静态类型(定义的类型)决定了该对象的哪些成员是可访问的
3、派生类的成员将隐藏同名的基类成员,派生类可通过作用域运算符来使用隐藏的成员
4、除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字
5、函数调用的解析过程(假定调用p->mem())
- 确定p的静态类型
- 在p的静态类型对应的类中查找mem,如果找不到依次在直接基类中查找直至到达继承链的顶端,如果任然找不到则报错
- 一旦找到mem,进行常规类型检查以确认对于当前找到的mem,调用是否合法
- 假设调用合法,编译器将根据调用的是否是虚函数而产生不同的代码。如果mem是虚函数且通过指针或引用进行调用,则依据对象的动态类型确认虚函数版本;如果不是虚函数或者不是通过引用或指针进行调用,则编译器产生一个常规函数调用
6、C++中名字查找优先于类型检查
- 派生类中的成员会隐藏掉基类中的同名成员,即使形参列表不一致
- 派生类与基类中的虚函数必须有相同的形参列表
15.7 构造函数与拷贝控制
1、基类或派生类的合成拷贝控制成员的行为与其他合成的构造函数、赋值运算符或析构函数类似,它们对类本身对成员依次进行初始化、赋值或销毁的操作。此外这些合成的成员还负责使用直接基类中对应的操作对一个对象的直接基类部分进行初始化、赋值或销毁的操作。
2、派生类中删除的拷贝控制与基类的关系
- 如果基类中的默认构造函数、拷贝构造函数、拷贝赋值函数或析构函数是被删除的或不可访问的,则派生类中对应的成员将是删除的。原因是编译器不能使用基类成员来执行派生类对象基类部分的构造、赋值或销毁操作
- 如果在基类中有一个不可访问的或删除掉的析构函数,则派生类中合成的默认和拷贝否早函数将是被删除的,因为编译器无法销毁派生类中的基类部分
- 如果基类部分中的移动操作时被删除的或不可访问的,则派生类中的移动操作时被删除的,因为派生类中的基类部分无法移动
- 如果基类中的析构函数时被删除的或不可访问的,则派生类中的移动构造函数也是被删除的
3、移动操作与继承
- 因为基类缺少移动操作会阻止派生类拥有自己的合成移动操作,因此在派生类需要执行移动操作时应该首先在基类中定义
- 在当前类中如果定义了拷贝构造函数,编译器不会为当前类合成一个移动构造函数
class Quote {
public:
Quote() = default;
Quote(const Quote&) = default;
Quote(Quote&&) = default;
Quote& operator=(const Quote&) = default;
Quote& opreator=(Quote&&) = default;
virtual ~Quote() = default;
}
4、派生类的拷贝控制成员和赋值运算符
- 当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动基类部分成员在内的整个成员,需要在派生类的初始值列表中显示的使用基类的拷贝(或移动)构造函数
- 派生类的赋值运算符与拷贝和移动构造函数一样,也必须显示的为其基类部分赋值
class Base {};
class D : public Base {
pubic:
//默认情况下,基类的默认构造函数初始化对象的基类部分
//要想使用拷贝或移动构造函数,必须在构造函数初始化列表中显示的调用该构造函数
D(const D& d) : Base(d) //拷贝基类成员
{}
D(D&& d) : Base(std::move(d)) //移动基类成员
{}
}
//Base::operator=(const Base&) 不会被自动调用
D& D::opreator=(const D& rhs) {
Base::operator=(rhs); //为基类部分赋值
//按照过去的方式为派生类成员赋值
//酌情处理字符值及已有资源释放
return *this;
}
5、派生类的析构函数
- 派生类的析构函数只负责销毁派生类自己分配的资源
- 派生类成员是在析构函数体执行完后被隐式销毁的,基类部分也是被隐式销毁的,其中派生类的析构函数先执行,然后是基类的析构函数
6、继承的构造函数