1. 概述
- OOP核心思想:数据抽象、继承、动态绑定
- C++核心思想:封装、继承、多态(与OOP一一对应)
2. 定义基类和派生类
基类通过在成员函数声明前加上virtual关键字使得该函数执行动态绑定,派生类需要对这些基类的成员函数重新定义以覆盖其旧定义,方法是在该函数的参数列表后面、或const关键字后面、或者引用限定符后面加上override关键字。 ——《C++ Primer》
-
基类
如果一个类被用做基类,则该类必须已经定义而非声明;class Quote{ public: Quote()=default; Quote(const string book, double p): bookNo(book),price(p){} string isbn() const{return bookNo;} virtual double net_price(size_t n) const{return n*price;} virtual ~Quote()=default; private: string bookNo; protected: double price=0.0; };
-
派生类
class Bulk_quote : public Quote{ //派生列表 public: Bulk_quote()=default; Bulk_quate(const string&, double, size_t, double); double net_price(size_t)const override; private: size_t min_qty=0; double discount=0.0; }; //派生类构造函数 Bulk_quote::Bulk_quote(const string& book, double p, size_t qty, double disc): Quote(book,p),min_qty(qty),discount(disc){} double Bulk_quote::net_price(size_t cnt) const{ if(cnt>=min_qty) return cnt*(1-discount)*price; else return cnt*price; }
派生类构造函数
- 派生类的初始化:尽管在派生类对象中含有从基类中继承来的成员,但是派生类不能直接初始化这些成员,必须使用基类的构造函数来初始化派生类的基类部分。
派生类向基类的类型转换(只对引用和指针有效)
- 基类的引用和指针可以绑定在派生类对象上;也叫做派生类向基类的类型转换;
Quote q; Bulk_quote b; Quote *p=&q; p=&b; Quote &r=b;
可以将基类的指针或引用绑定到派生类对象上有一层极为重要的含义:当使用基类的指针或引用时,实际上我们并不清楚该指针或引用所绑定对象的真实类型,有可能是基类对象,也有可能是派生类对象。——《C++ Primer》
派生列表
-
类的声明中不应包含派生列表,但是定义中要包含(如果有);
class Bulk_quote:public Quote; //错误 class Bulk_quote;//正确
防止继承的发生
-
final
关键字,跟在类名后面,表示该类不能作为基类;//NoDerived类和Last类都不能作为其他类的基类 class NoDerived final {/*...*/}; class Last final : Base {/*...*/};
3. 类型转换
静态类型与动态类型
-
静态类型:编译时已知,是变量声明时的类型或者表达式生成的类型;
-
动态类型:运行时可知,是变量或表达式表示的内存中的对象类型;
double print_total(ostream &os, const Quote &item, size_t n) { double ret = item.net_price(n); os<<item.isbn()<<ret<<endl; return ret; }
-
在
print_total
函数中,item
的静态类型是Quote&
,这是在编译时已知的;但是其动态类型在运行时才知道,如果传入print_total
的实参是Quote
对象,则item
的动态类型也是Quote
,如果传入的实参是Bulk_quote
对象,则item
的动态类型是Bulk_quote
; -
如果表达式既不是引用也不是指针,则其静态类型和动态类型永远一致;
不存在基类向派生类的隐式类型转换
- 派生类的引用或指针不能绑定在基类上,即不存在基类向派生类的隐式类型转换;
Quote base; Bulk_quote* b=&base;//错误 Bulk_quote& r=base;//错误
- dynamic_cast可以申请一个从基类向派生类的类型转换;
对象之间不存在类型转换
- 只能用来拷贝,不会发生类型转换
Bulk_quote bulk; Quote item(bulk);//调用Quote(const Quote&)构造函数,拷贝bulk中的Quote部分成员; item=bulk;//调用Quote::operator=(const Quote&),只有bulk中的Quote部分成员被赋值给item,bulk_quote部分被切掉了
总结:
- 从派生类向基类的类型转换只对指针和引用类型有效;
- 基类向派生类不存在隐式类型转换;
4. 虚函数
C++的多态性
- 动态绑定只有在通过指针或引用调用虚函数的时候才会发生;
- 指针或引用调用非虚函数在编译的时候绑定,不会发生动态绑定;
- 当使用普通类型(对象)调用虚函数在编译的时候绑定,不会发生动态绑定;
派生类中的虚函数
-
override
关键字标记派生类中的虚函数,用来覆盖基类中的对应虚函数; -
派生类中用于覆盖的虚函数必须和基类中要被覆盖的虚函数有相同的返回类型和形参列表;
-
final
关键字,表示不允许派生类对其进行覆盖;struct D1{ void f1(int) const final; }; struct D2:D1{ void f1(int) const;//错误,f1不能被覆盖 };
回避虚函数的机制
-
如果使用基类指针或引用调用虚函数,则会发生动态绑定,如果我们不希望其发生动态绑定,而是强迫其执行虚函数的某个特定版本,则使用作用域运算符:
double print_total(ostream &os, const Quote &item, size_t n) { double ret = item.Quote::net_price(n); //强制执行基类的虚函数版本 os<<item.isbn()<<ret<<endl; return ret; }
-
如果派生类的虚函数需要调用其基类版本,则必须使用作用域运算符,否则在运行该调用的时候将被解析为对派生类版本自身的调用,从而导致无限递归;
5. 抽象基类
纯虚函数
- 纯虚函数:通过在函数体的位置(声明的分号前)书写
=0
就可以将一个虚函数说明为纯虚函数;
class Disc_quote:public Quote{
public:
Disc_quote()=default;
Disc_quote(const string& book, double p, size_t qty, double disc):
Quote(book, p), quantity(qty),discount(disc){}
double net_price(size_t) const =0;//声明net_price为纯虚函数
protected:
size_t quantity=0;
double discount=0.0;
};
- 纯虚函数由其派生类来重写;
含有纯虚函数的类是抽象基类
- 抽象基类负责定义接口,不能直接实例化,不能创建一个抽象基类的对象。抽象基类只能作为其他类的基类;
- 重写了纯虚函数的派生类可以被实例化,如果派生类没有重写纯虚函数,则该派生类仍然是一个抽象基类;
抽象基类的作用:接口和实现分离
- 之前的Bulk_quote派生类实现了一个打折策略,但还可能要设计其他的打折策略,因此需要一个通用的接口,这个接口就是抽象基类,它不能被实例化,只能由其派生类去实现里面具体的功能。接口和实现分离,提高了程序的安全性。
重写Bulk_quote类:
class Bulk_quote : public Disc_quote{
Bulk_quote()=default;
Bulk_quote(const string& book, double p, size_t qty, double disc):
Disc_quote(book, p, qty, disc){}
//重写纯虚函数,实现新的折扣策略
double net_price(size_t) const override;
};
6. 访问控制和继承
- 一个类对其继承而来的成员的访问权限受两个因素影响:
- 基类中的访问说明符;
- 派生类的派生列表中的访问说明符;派生列表中的访问说明符表示继承来的基类成员在派生类中的表示,即,如果访问说明符是public,则该派生类从基类继承的protected成员在该派生类中仍然是protected,从基类继承的public成员仍然是public的;如果访问说明符是private,则该派生类从基类继承的public成员和protected成员都是private的。