类的基本思想是数据抽象
和封装
。数据抽象是一种依赖于接口
和实现
分离的编程技术。
面向对象程序设计的核心思想是数据抽象
、继承
和动态绑定
。
定义基类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #ifndef BULK_QUOTE_H #define BULK_QUOTE_H #include <string> using std::string;
class Quote { public: Quote() = default; Quote(const std::string &book, double sales_price): bookNo(book), price(sales_price) { } string isbn() const; double getPrice() const; virtual double net_price(size_t n) const; virtual ~Quote() = default; private: string bookNo; double price = 0.0; }; #endif
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include "Quote.h" string Quote::isbn() const { return bookNo; }
double Quote::getPrice() const { return price; }
double Quote::net_price(size_t n) const { return n * getPrice(); }
|
在C++语言中基类将它的两种成员函数区分开:一种是基类希望其派生类进行覆盖的函数,另一种是基类希望派生类直接继承而不要改变的函数。对于前者基类通常将其定义为虚函数(virtual)
。当我们使用基类的引用或指针调用一个虚函数时,将发生动态绑定,根据引用或绑定的对象类型不同,该调用可能执行基类的版本,也可能执行某个派生类的版本。
定义派生类
派生类必须通过使用类派生列表(class derivation list)
明确指出他是从哪个(哪些)基类继承而来的。
1 2 3 4 5 6 7 8 9 10 11
| #include "Quote.h" class Bulk_quote : public Quote { public: Bulk_quote() = default; Bulk_quote(const string &n, double p, size_t m, double d): Quote(n, p), min_qty(m), discount(d) { } double net_price(size_t) const override; private: size_t min_qty = 0; double discount = 0.0; };
|
C++11允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数:在该函数的形参列表之后加一个override
关键字。
1 2 3 4 5 6 7 8
| #include "Bulk_quote.h" double Bulk_quote::net_price(size_t n) const { if (n > min_qty) return (n - min_qty) * getPrice() * discount + min_qty * getPrice(); return n * getPrice(); }
|
成员函数通过一个名为this
的隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this。
net_price()函数参数列表之后的const
关键字的作用是修改隐式this指针的类型。默认情况下,this的类型是指向类类型非常量版本的常量指针。例如在Bulk_quote成员函数中,this的类型是Bulk_quote *const
。默认情况下我们不能把this绑定到一个常量对象上。这也使得我们不能在一个常量对象上调用普通的成员函数。
紧跟在参数列表后面的const
表示this是一个指向常量的指针。这样使用const的成员函数称作常量成员函数
。常量成员函数不能改变调用它的对象的内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include "Bulk_quote.h" #include "Quote.h" #include <iostream> using std::ostream; using std::cout; using std::endl;
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: " << ret << endl; return ret; } int main() { Quote basic("241024", 12.5); Bulk_quote bulk("241025", 12.5, 10, 0.8); print_total(cout, basic, 20); print_total(cout, bulk, 20); return 0; }
|
编译
1 2 3 4 5 6 7 8 9 10 11 12 13
| CC = g++ SRC = main.cpp Quote.cpp Bulk_quote.cpp OBJ = main.o Quote.o Bulk_quote.o EXEC = main
$(EXEC) : $(OBJ) $(CC) -std=c++11 $(OBJ) -o $@ rm -f $(OBJ) $(OBJ) : Quote.h Bulk_quote.h $(CC) -std=c++11 -c $(SRC)
clean: rm -f main
|
防止继承的发生
C++11新标准提供了一种防止继承发生的方法,即在类名后跟一个关键字final:
1
| class NoDerived final { ... };
|
类型转换与继承
通常情况下,把引用或指针绑定到一个对象上,则引用或指针的类型应与对象的类型一致。存在继承关系的类是一个重要的例外:可以将基类的引用或指针绑定到派生类对象上。使用基类的引用或指针时,实际上我们并不清楚该引用或指针所绑定类型的真实类型。
派生类中的虚函数
当我们在派生类中覆盖了某个虚函数时,可以再一次使用virtual关键字指出该函数的性质。然而这么做并非必须,因为一旦某个函数被声明成虚函数,则在所有派生类中它都是虚函数。
纯虚函数
想要定义一个Disc_quote类来支持不同的折扣策略,Disc_quote负责保存购买量的值和折扣值。其他表示某种特定策略的类将分别继承自Disc_quote,Disc_quote类中的net_price函数显然没有实际含义,因此可以将net_price定义成纯虚(pure virtual)
函数。和普通的虚函数不一样,一个纯虚函数无须定义。通过在函数体的位置书写=0
就可以将一个虚函数说明为纯虚函数。
1 2 3 4 5 6 7 8 9 10 11 12 13
| #include "Quote.h"
class Disc_quote : public Quote { public: Disc_quote() = default; Disc_quote(const string &book, double price, size_t qty, double disc): Quote(book, price), quantity(qty), discount(disc) { } double net_price(size_t) const = 0; protected: size_t quantity = 0; double discount = 0.0; };
|
我们也可以为纯虚函数提供定义,不过函数体必须定义在类的外部。
含有纯虚函数的类是抽象基类(abstract base class)
。抽象基类负责定义接口,而后续的其他类可以覆盖该接口。我们不能直接创建一个抽象基类的对象。
重新实现Bulk_quote
让Bulk_quote继承Disc_quote而非直接继承Quote:
1 2 3 4 5 6 7 8 9 10
| #include "Disc_quote.h"
class Bulk_quote : public Disc_quote { public: Bulk_quote() = default; Bulk_quote(const string &book, double price, size_t qty, double disc): Disc_quote(book, price, qty, disc) { } double net_price(size_t) const override; };
|
Bulk_quote.cpp不需要改变,也不需要实现Disc_quote.cpp文件。
reference
《C++ Primer 第五版》