类继承
1. 派生类
派生类构造
- 派生类构造函数必须使用基类构造函数
- 基类应在进入派生类构造函数之前被创建, C++使用初始化列表语法来完成
- 如果不调用基类构造函数, 程序将使用基类的默认构造函数
- 创建派生类对象时首先调用基类构造函数然后再调用派生类构造函数, 而派生类对象过期时程序首先调用派生类析构函数, 然后在自动调用基类析构函数
使用派生类
- 要使用派生类, 程序必须要能够访问基类声明
派生类与基类的特殊关系
- 派生类对象可以使用基类的公有方法
- 派生类可以访问基类的非私有成员
说明: 访问时需要用基类名称和作用域解析符来指定访问的基类的成员(数据或方法), 如果基类成员名称和派生类新添加的成员没有冲突可以直接使用成员名称访问
- 基类指针或引用可以在不进行显式类型转换的情况下指向或引用派生类对象
SubClass subClassObject;
Baseclass* pb = &subClassObject;
Baseclass& rb = subClassObject;
2. 继承
继承方式和关系
- C++有三种继承关系: private, protected, public
- 继承通常建立is-a关系
- 公有继承不建立has-a关系
- 公有继承不能建立is-like-a关系
- 公有继承不建立is-implemented-as-a(作为...来实现)关系
- 公有继承不建立uses-a关系
多态公有继承
- 通过将基类方法声明为虚函数实现多态
- 关键字virtual只用于声明方法原型, 不能用于定义时(除非声明就是定义)
- 如果在派生类中重新定义基类的方法, 则将其声明为虚方法
- 基类的析构函数通常声明为虚的
3. 静态联编和动态联编
将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编, 在C++中可以在编译过程完成这种联编, 即静态联编, 又称早期联编(绑定)
由于虚函数和多态的引入, 使用哪一个函数在编译期无法确定, 所以程序在运行时选择正确的虚方法的代码,这种称为动态联编, 又称晚期联编(绑定)
- C++默认使用静态联编, 因为其效率更高
虚函数工作原理
- 给对象添加一个隐藏成员--虚函数指针
- 虚函数指针指向虚函数表(保存函数地址的数组)
基类对象包含一个指针, 该指针指向基类中所有虚函数的地址表, 派生类对象包含一个指向独立地址表的指针, 如果派生类提供了虚函数的新定义, 该虚函数表将保存新的地址; 如果派生类没有重新定义虚函数, 该vtbl将保存函数原始版本地址; 如果派生类定义了新的虚函数, 则将该函数的地址添加到vtbl中
- 注意事项:
- 构造函数不能为虚函数
- 析构函数应当是虚函数, 除非不作为基类
- 友元函数不能是虚函数, 因为友元不是类成员, 而只有成员函数才能是虚函数
- 重新定义将隐藏方法(这不是重载)
class Base {
public:
virtual void show(int a) const {
cout << "In base" << endl;
}
};
class Sub : public Base {
public:
virtual void show() const {
cout << "In sub" << endl;
}
};
int main() {
Sub s;
s.show(1); // invalid
s.show(); // valid
return 0;
}
4. 抽象基类
一个类包含纯虚函数时, 该类为抽象类, 不能创建抽象类的对象
纯虚函数
virtual void show(int x) = 0;
- 在原型中使用=0指出该函数是一个纯虚函数, 在类中不可以定义该函数, 但是在实现文件中定义
- ABC(抽象类)要求具体派生类覆盖其纯虚函数, 迫使派生类遵循ABC设置的接口规则
5. 继承和动态内存分配
派生类不使用new
- 这种情况派生类不必要定义显式析构函数, 拷贝构造函数和赋值运算符
派生类使用new
这种情况必须为派生类定义显式析构函数, 拷贝构造函数和赋值运算符
拷贝构造函数调用基类拷贝构造函数来处理从基类继承的数据
hasDMA::hasDMA(const hasDMA & hs) : baseDMA(hs) {}
赋值运算符通过显式调用基类运算符完成赋值
hasDMA::operator=(const hasDMA & hs) {
...
baseDMA::operator=(hs);
...
}
- 采用动态内存分配和友元的基类继承, 采用强制类型转换以匹配原型时选择正确的函数
os << (const baseDMA &) hs;
6. 什么不能被继承
- 构造函数不能被继承
- 析构函数也不能被继承
- 赋值运算符不能被继承
- 友元函数不能继承