第十五章:面向对象程序设计(Ⅰ)
继承与动态绑定带来的优点:
- 1、更容易的定义与其他类相似但不完全相同的新类;
- 2、编写相似程序,可以一定程度忽略他们的区别。
15、1 OOP概述:
-
继承:
-
定义不同定价策略,定义Quote(引用)类作为基类,Quote对象表示按原价销售的书籍;Quote类派生的类 Bulk_quote(批次引用)类表示可以打折销售的书籍。
-
包含的成员函数:
- isbn()函数:返回书籍的ISBN编号,不涉及派生类特殊性,所以定义在quote基类中。
- net_price(size_t):返回书籍实际销售价格,有数量要求所以与类型相关,Quote类和Bulk_quote类都包含。
-
基类希望其派生类各自定义适合自身版本的类,基类就将这些函数声明为虚函数(virtual function)
-
class Quote{ public: // 返回对于特定数量书籍的总价,继承的类必须override并且使用不同的定价方法 virtual double net_price(std::size_t n) const { return n * price; } std::string isbn() const; }
-
-
派生类使用类派生列表明确指出它是从哪个基类继承而来的,形式:首先是一个冒号,后面紧跟以逗号分割的基类列表,其中每个基类前面都可以有访问说明符。
-
// 由于继承过程使用了public关键字,所以可以将bulk_quote对象当成quote对象使用 class Bulk_quote : public Disc_quote { // Bulk_quote inherits from Quote public: // override基类中函数去实现bulk_quote自身的定价策略 double net_price(std::size_t) const override; } // 实现 自己版本的net_price函数。 double Bulk_quote::net_price(size_t cnt) const { if (cnt >= quantity) return cnt * (1 - discount) * price; else return cnt * price; }
-
-
派生类必须对其内部所有重新定义的虚函数声明,派生类可以使用override关键字表明使用哪个成员函数改写基类的虚函数。仅在重新定义的虚函数声明中使用override关键字。
-
-
动态绑定:
-
函数print_total()计算并打印销售给定数量的书籍所得费用。函数返回调用net_price()的结果并将调用isbn()的结果打印输出。
-
// 由绑定到参数item的对象类型决定调用调用Quote::net_price()或Bulk_quote::net_price() double print_total(std::ostream &os, const Quote &item, std::size_t n){ double ret = item.net_price(n); os << "ISBN: " << item.isbn() << " # sold: " << n << " total due: " << ret << endl; return ret; }
-
-
C++中,使用基类的引用(或指针)调用一个虚函数时会发生动态绑定,即函数的运行版本由实参决定,在运行时选择函数的版本。
-
15、2 定义基类和派生类:
-
作为继承关系中根节点的类通常会定义一个虚析构函数。即使该函数不执行任何实际操作。
-
成员函数与继承:
- 对虚函数,派生类需要定义自己的操作来覆盖从基类继承而来的旧定义。
- 基类中的两种函数:
- 希望派生类进行覆盖的虚函数,在成员函数的声明语句前加上关键字vritual 标明是虚函数,当使用指针或引用调用虚函数时,该调用被动态绑定。
- 任何构造函数之外的非静态函数都可以是虚函数。
- 关键字virtual只能出现在类内部的声明语句前而不能出现在类的外部的函数定义。
- 希望派生类直接继承而不改变的函数
- 未被声明为虚函数的成员函数其无论是基类对象还是派生类对象调用行为都一样。
- 希望派生类进行覆盖的虚函数,在成员函数的声明语句前加上关键字vritual 标明是虚函数,当使用指针或引用调用虚函数时,该调用被动态绑定。
-
派生类可以访问从基类那里继承来的公有成员,无权访问继承来的私有成员,但是基类中protected访问运算符控制的成员可以被派生类访问,同时禁止其他用户访问。派生列表中的访问说明符作用是控制派生类从基类继承来的成员是否对派生类的用户可见。
-
定义派生类:
-
派生类bulk_quote需要将继承来的虚函数重新声明,所以必须包含成员函数net_price()。
-
class Bulk_quote : public Disc_quote { // Bulk_quote inherits from Quote public: Bulk_quote() = default; Bulk_quote(const std::string& book, double p, std::size_t qty, double disc) : Disc_quote(book, p, qty, disc) { } // overrides the base version in order to implement the bulk purchase discount policy double net_price(std::size_t) const override; private: std::size_t min_qty = 0; // 折扣策略的最低购买量 double discount = 0.0; // 以小数表示的折扣额 };
-
-
派生类对象及派生类向基类的类型转换:
-
派生类对象不仅包含自己定义的成员的子对象,还有继承自基类的子对象。
-
因为派生类对象中有其基类对应的组成部分,所以可以将派生类对象当成基类对象使用。
-
还能将基类的指针或引用绑定到派生类对象中的基类部分上。称为派生类到基类的类型转换。由编译器隐式进行。
-
Quote item; // 基类对象 Bulk_quote bulk; // 派生类对象 Quote* p = &item; // p指向Quote对象 p = &bulk; // p 指向bulk的Quote部分 Quote &r = bulk; // r绑定到bulk的Quote部分
-
-
派生类构造函数:
-
派生类中含有从基类继承的成员,但是不能直接初始化他们。派生类必须使用 基类的构造函数 来初始化它的基类成员。
-
派生类对象的基类部分与自己数据成员都是在构造函数初始化阶段执行初始化操作的。通过构造函数初始化列表将实参传递给基类构造函数。
-
// 参数book、p传递给Quote的构造函数由Quote的构造函数负责初始化Bulk_quote的基类部分。 Bulk_quote(const std::string& book, double p, std::size_t qty, double disc): Quote(book,p),min_qty(qty), discount(disc){}
-
参数book、p传递给Quote的构造函数由Quote的构造函数负责初始化Bulk_quote的基类部分。当Quote的构造函数体结束后,Bulk_quote对象的基类部分也完成了初始化。然后初始化Bulk_quote自己定义的成员运行Bulk_quote构造函数体。
-
派生类对象的基类部分执行默认初始化。使用其他基类构造函数需要使用类名加圆括号内的实参列表的形式为构造函数提供初始值。这些实参将帮助编译器决定使用哪个构造函数来初始化派生类对象的基类部分。
-
-
派生类使用基类的成员:
- 派生类可以访问基类的公有、受保护成员。
- 派生类的作用域嵌套在基类的作用域内部,派生类成员使用基类成员与使用自己成员一样。
-
继承与静态变量:
- 基类定义的静态成员在整个继承体系中只存在该成员的唯一定义。若静态成员是可访问的,则既能通过基类使用它也能通过派生类使用它。既可以通过类::静态方法调用也能通过继承对象.静态方法调用。
-
派生类的声明:
- 派生类的声明不包含它的派生列表。派生列表及与定义有关的细节必须与类的主体一起出现。
-
基类:
- 将一个类用作基类,该类必须已经定义而非仅仅声明。派生类需要直到其从基类中继承的成员,一个类不能派生他自己。
- 直接基类出现在派生列表中,间接基类由派生类通过其直接类继承而来。
- 最终的派生类将包含它的直接类的子对象及每个间接基类的子对象。
-
防止继承的发生:
-
定义一种不希望其他类继承它的方法,在类名后跟一个关键字final;
-
class NoDerived final{}; // NoDerived不能作为基类
-
-
类型转换与继承:
- 将引用或指针绑定到一个对象上,则引用或指针的类型应与对象的类型一致。或对象的类型含有一个可接受的const类型转换规则。
- 可以将基类指针或引用绑定在派生类对象上:使用基类引用或指针时,并不清楚引用(指针)绑定对象的真实类型。
-
静态类型与动态类型:
-
表达式的静态类型是变量声明时的类型或表达式生成的类型。静态类型在编译时总是已知的。
-
动态类型是变量或表达式表示的内存中的对象的类型。动态类型直到运行时才可知。
-
double ret = item.net_price(n); // item的静态类型是Quote&,动态类型依赖于item绑定的实参,直到运行时调用该函数才知道
-
如果表达式既不是引用也不是指针,则其动态类型永远与静态类型一致。
-
-
不存在基类向派生类的隐式类型转换:
-
派生类对象包含一个基类部分,基类的引用或指针可以绑定到该基类部分上。基类对象既可以作为独立形式存在,也可以作为派生类对象的一部分存在。
-
不能将基类转换成派生类。
-
派生类向基类的转换只对指针或引用有效,在派生类类型和基类类型间不存在这种转换。
-
当初始化或赋值一个类类型对象时,实际上在调用某个函数,函数参数类型是类类型的const版本的引用。因为成员接受引用作为参数,所以派生类向基类转换运行给基类的拷贝/移动操作传递一个派生类对象,这些操作都不是虚函数。那么构造函数与赋值运算符运行的都是基类中定义的那个,不能处理自身的成员。
-
Bulk_quote bulk; // 派生类对象 Quote item(bulk); // 使用Quote::Quote(const Quote&)构造函数 item = bulk; // 调用Quote::operator=(const Quote&)
- 构造item时,运行Quote的拷贝构造函数,只能处理bookNo和price两个成员,忽略掉bulk中min_qty、 discount成员,赋值操作也是如此。
- 忽略Bulk_quote 部分被定义为bulk_quote部分被切掉了(sliced down)
-
用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分被拷贝、移动和赋值,它的派生类部分将被忽略掉。
-
-
总结:
1、从派生类向基类的类型转换只对指针或引用类型有效。
2、基类向派生类不存在隐式类型转换。
3、将派生类对象拷贝、移动或赋值给一个基类对象的操作只处理派生类对象的基类部分。
-
15、3 虚函数:
-
必须为每一个虚函数提供定义。
-
对虚函数的调用可能在运行时才被解析:
- 当通过一个非引用非指针的表达式调用虚函数时,在编译阶段就会将调用的版本确定下来,但是由指针或引用调用虚函数时会发生动态绑定。编译器在运行阶段才会确定是哪一个版本。
- 通过对象进行的函数(虚函数、非虚函数)调用也在编译阶段绑定,对象的类型是确定的。
-
派生类中的虚函数:
- 派生类中覆盖了某个虚函数,可以再使用virtual关键字指出其性质,也可以省略因为一旦一个函数被声明成虚函数,在所有派生类中它都是虚函数。
- 派生类覆盖了继承来的虚函数,其形参类型必须与被他覆盖的基函数完全一致。返回类型也必须与基函数匹配。
-
final和override说明符:
-
使用override关键字说明派生类中的虚函数。如果使用override标记了函数,但函数并没有覆盖已存在的虚函数,则编译器会报错。
-
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{ void f1(int) const final;// 不允许后续其他类覆盖f1(int) }; struct D3:D2{ void f2(); void f1(int) const; // 错误,已声明为final了,不能再覆盖 }
-
-
虚函数与默认实参:
- 虚函数也有默认实参,函数调用使用了默认实参,则实参值由调用的静态类型决定;即是说即是运行的是派生类对象,则传入派生类函数的默认实参值也是基类中定义的默认实参。
- 如果虚函数中使用默认实参,基类和派生类中定义的默认实参最好一致。
-
回避虚函数机制:
-
希望虚函数调用不进行动态绑定而是强迫执行虚函数特定版本,可以使用作用域运算符实现:
-
double undiscounted = baseP->Quote::net_price(42);
-
代码强行调用Quote的net_price函数,不管baseP实际指向的对象类型到底是什么,调用在编译时完成解析。
-
一般成员函数(友元)中代码才需要使用作用域运算符回避虚函数机制。
-
-
派生类的虚函数调用它覆盖的基类的虚函数版本时,需要使用到回避虚函数机制。若不加作用域运算符会导致无限递归。
-
-