1、面向对象程序设计基于三个基本概念:数据抽象、继承和动态绑定。
- 数据抽象:可以将类的接口与实现分离。
- 继承:可以定义相似的类型并对其相似关系建模。
- 动态绑定:可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。
继承
- 通过继承联系在一起的类构成一种层次关系。通常在层次关系的根部有一个基类,其他类则是直接或间接地从基类继承而来,这些继承得到的类称为派生类。
- 基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。
实例
- Quote的对象表示按原价销售的书籍。Quote派生出另一个名为Bulk_quote的类,它表示可以打折销售的书籍。这些类包含下面的两个成员函数:
1)isbn(),返回书籍的ISBN编号。该操作不涉及派生类的特殊性,因此只定义在Quote类中。
2)net_price(size_t),返回书籍的实际销售价格,前提是用户购买该书的数量达到一定标准。这个操作显然是类型相关的,Quote和Bulk_quote都应该包含该函数。 - 对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数。
class Quote {
public:
std::string isbn() const;
virtual double net_price(std::size_t n) const;
};
- 派生类必需通过使用派生类列表明确指出它是从哪个基类继承而来的。
class Bulk_quote : public Quote {
public:
double net_price(std::size_t) const override;
};
动态绑定
- 通过使用动态绑定,我们能用同一段代码分别处理Quote和Bulk_quote的对象。
//计算并打印销售给定数量的某种书籍所得的费用
double print_total(ostream &os, const Quote &item, size_t n) {
double ret = item.net_price(n);
os << "ISBN: " << item.isbn() << "# sold: " << n << "total due: " << net << endl;
return net;
}
//basic 的类型是Quote;bulk的类型是Bulk_quote
print_total(cout, basic, 20); //调用Quote的net_price
print_total(cout, bulk, 20); //调用Bulk_quote的net_price
- 上述过程中函数的运行版本由实参决定,即在运行时选择函数的版本,所以动态绑定有时又被称为运行时绑定。
- 在C++语言中,当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定。
2、定义基类和派生类
定义基类
class Quote {
public:
Quote() = default;
Quote(const std::string &s, double p) : bookNo(s), price(p) {}
std::string isbn() const {return bookNo;}
//返回给定数量的书籍的销售总额
//派生类负责改写并使用不同的折扣计算算法
virtual double net_price(std::size_t n) const {return n * price;}
virtual ~Quote() = default; //对析构函数进行动态绑定
private:
std::string bookNo;
protected:
double price = 0.0;
};
- 在C++语言中,基类必须将它的两种成员函数区分开来:一种是基类希望其派生类进行覆盖的函数;一种是基类希望派生类直接继承而不要改变的函数。
- 派生类可以访问基类的公有成员,而不能访问私有成员。不过在某些时候基类中还有这样一种成员,基类希望它的派生类有权访问该成员,同时禁止其他用户访问。我们用受保护的(protected)访问运算符说明这样的成员。
- 基类通常都应该定义一个虚析构函数,即使函数不执行任何实际操作也是如此。
定义派生类
class Bulk_quote : public Quote {
public:
Bulk_quote() = default;
Bulk_quote(const std::string&, double, std::size_t, double);
double net_price(std::size_t) const override;
private:
std::size_t min_qty = 0; //使用折扣政策的最低购买量
double discount = 0.0; //折扣额
};
- 派生类到基类的类型转换:可以将基类的指针或引用绑定到派生类对象。
- 在派生类对象中含有与其基类对应的组成部分,这一事实是继承的关键所在。
派生类构造函数
Bulk_quote(const std::string &book, double p, std::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;
}
}
- 每个类负责定义各自的接口。要想与类的对象交互必须使用该类的接口,即使这个对象是派生类的基类部分也是如此。
继承与静态成员
- 如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。不论从基类中派生出多少个派生类,对于每个静态成员来说都只存在唯一的实例。
- 静态成员遵循通用的访问控制规则,如果基类中的成员是private的,则派生类无权访问它。假设静态成员是可访问的,则我们既能通过基类使用它也能通过派生类使用它。
防止继承的发生
- C++新标准提供了一种防止继承发生的方法,即在类名后跟一个关键字final。
class NoDerived final {};
3、类型转换与继承
- 和内置指针一样,智能指针类也支持派生类向基类的类型转换,这意味着我们可以将一个派生类对象的指针存储在一个基类的智能指针内。
- 表达式的静态类型在编译时总是已知的,它是变量声明时的类型或表达式生成的类型;动态类型则是变量或表达式表示的内存中的对象的类型。动态类型知道运行时才可知。
- item的静态类型是Quote&,而在此例中它的动态类型则是Bulk_quote。如果表达式既不是引用也是不指针,则它的动态类型永远与静态类型一致。
- 不存在从基类向派生类的隐式类型转换。
- 在对象之间不存在类型转换。
- 当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分将被忽略掉。
4、虚函数
- 当我们使用基类的引用或指针调用一个虚成员函数时会执行动态绑定。因为我们知道运行时才能知道到底调用了哪个版本的虚函数,所以所有虚函数都必须有定义。
- 当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。
- 基类中的虚函数在派生类中隐含地也是一个虚函数。当派生类覆盖了某个虚函数时,该函数在基类中的形参必须与派生类中的形参严格匹配。当类的虚函数返回类型是类本省的指针或引用时,上述规则无效。
1)如果D由B派生得到,则基类的虚函数可以返回B而派生类的对应函数可以返回D,只不过这样的返回类型要求从D到B的类型转换是可访问的。
final和override说明符
- 如果我们使用override标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错。
- 我们还能把某个函数指定为final,如果我们已经把函数定义成final了,则之后任何尝试覆盖该函数的操作都将引发错误。
- 在某些情况下,我们希望对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的某个特定版本。使用作用域运算符可以实现这一目的:
double undiscounted = baseP->Quote::net_price(42);