面向对象编程(OOP) 核心思想在于 数据抽象 继承 和 动态绑定
也叫 封装 继承 和 多态
通过数据抽象 可以将类的接口和实现分离
通过继承 可以定义相似的类型并对其相似关系建模
通过动态绑定 可以一定程度上忽略相似类别的区别 以统一的方式使用它们的对象
继承
继承中 一般基类负责定义所有类共有的成员 而每个派生类定义各自特有的成员
通过使用virtual
关键字 可以定义虚函数 派生类必须对所有的虚函数进行声明
所有的虚函数都必须有定义 因为编译器不知道是否会在动态绑定时使用
一个函数在基类中被声明为虚函数 则在所有派生类中都为虚函数 无需再次声明virtual
关键字
基类通常应该定义一个虚析构函数 因为动态绑定可能会导致实际类型与指针类型不符
定义了虚析构函数将会阻止编译器为其合成移动操作
派生类必须通过类派生列表来明确指出从哪个(些)基类继承而来
class b_quote : public Quote{
public:
b_quote() =default;
b_quote(string &book,double p, double disc): Quote(book,p),discount(disc){ }
double price() override;
private:
double discount;
};
可以通过override
关键字显式地注明使用哪个成员函数改写基类的虚函数 以防止形参列表不一致而导致未覆盖虚函数而重新定义了一个新函数的错误
protected
访问运算符 表示该成员只能被类本身,友元和派生类访问
如果要被继承 那么基类必须先被定义而不仅仅是声明
派生类和其他创建基类的方法一样 必须通过基类的构造函数来初始化它的基类部分
初始化顺序是 先初始化基类成员 再按定义顺序初始化派生类内的成员
静态成员无论继承多少次 仍只有一个
通过final
关键字 可以禁止该类被继承 但该类本身仍可以继承别的基类
class base final: public Quote{ };
也可以通过final
关键字修饰函数 使得该函数不可以被继承类覆盖
友元关系不可被继承
派生类的派生列表中的访问说明符并不影响其是否能访问直接基类的成员(只与基类中的访问权限有关)
其作用是控制派生类用户(包括派生类的派生类)对于基类成员的访问权限 即表示派生类从基类继承过来的成员的访问权限
公有继承 则遵循原有访问说明符 私有和保护继承则是改为对应说明符权限
class base{
public:
int a;
protected:
int b;
};
class b1: public base{}; //b1中 a为public成员 b为protected成员
class b2: private base{}; //b2中 a和b为private成员
通过在类的内部使用using语句可以改变成员的访问权限
class b3: private base{
public:
using base:: a;
protected:
using base:: b;
}; // a变为了public成员 b变为了Protected成员
默认情况下使用class
定义的派生类是私有继承 使用struct
定义的是公有继承
派生类的作用域位于基类的作用域之内 即先在派生类中解析 无法完成的情况下再向外层基类作用域中寻找解析
通过using
语句 可以在派生类内部继承基类的每个构造函数
对于基类的每个构造函数 编译器都在派生类中生成一个形参列表完全相同的派生类的构造函数
using Quote:: Quote; // 对应就会有 b_quote(args): Quote(args) { }
使用这些构造函数时 如果派生类含有自己特有的数据成员 这些成员将被默认初始化
使用容器存放继承体系中的对象时 应该通过指针进行间接储存
否则会因为类型转化而导致报错(基类向派生类转化)或者信息丢失(派生类向基类的转换)
动态绑定
C++中无法直接使用对象进行面向对象编程 反而得使用指针和引用来间接操控
基类通过声明虚函数来使得该函数执行动态绑定
动态绑定可以根据传入的参数 在运行时选择调用的类成员
double print(const Quote & item) {
cout<<item.price();
}
b_quote a;
Quote b;
print(a);
print(b);
// 运行时传入 b_quote和Quote对象都可以 并且会自动选择对应版本函数
如果调用虚函数时使用了默认实参 则使用的是本次调用的静态类型的默认实参
如通过基类对象引用调用函数 实际传入的为派生类对象 那么使用的是基类的默认实参 尽管调用的是派生类的函数
可以通过作用域运算符强迫执行指定的虚函数版本 而不是动态绑定
一般用于某个派生类中的虚函数调用被它覆盖的虚函数时 因为否则将调用自身无限递归
即使动态绑定了 我们仍旧只能调用其静态类型的对象所包含的成员
如我们将基类引用绑定到派生类对象上 我们不能调用那些在派生类中新添加的成员
派生类中都包含它的基类部分 因此 派生类可以隐式转换为基类
但基类不能隐式转换为派生类
注意 派生类向基类的隐式转换只对指针和引用有效
对于代码中给定的部分而言 如果此时基类的公有成员是可访问的 则派生类向基类的转换也是可访问的 反之则不行
我们可以使用派生类对象为基类对象初始化或赋值
这是因为本质上我们调用的构造函数或者拷贝函数接受引用输入 因此可以隐式转换
使用派生类对象为基类对象初始化或赋值时 只有该派生类中的基类部分会被拷贝、移动或赋值 它的派生类部分会被忽略
我们将具有继承关系的多种类型称为多态类型, 因为我们可以使用这些类型的"多种形式"而无需在意它们之间的差异
引用或指针的静态类型和动态类型不一致(即动态绑定)是C++支持多态性的根本所在
封装
通过在声明函数本来函数体的位置书写 = 0 就可以将一个虚函数声明为纯虚函数 这种方式仅能出现在类的内部声明虚函数时
double price(int) = 0;
含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类 负责定义接口 也不能创建抽象基类的对象
定义纯虚函数的真正目的是为了定义抽象类
纯虚函数通常没有定义体,但也完全可以拥有, 甚至可以显示调用。