面向对象程序设计基于三个基本概念:数据抽象,继承和动态绑定
数据抽象是一种依赖于接口和实现分离的编程技术。继承和动态绑定对程序的编号有两方面的影响:一是我们可以更容易地定义与其它类相似但不完全相同的类;二是在使用这些彼此相似的类编写程序时,我们可以在一定程度上忽略掉它们的区别。
在 c++ 语言中,当我们使用基类的引用或指针调用一个虚函数时将发生动态绑定
定义基类:
1 class Quote { 2 public: 3 Quote() = default; 4 Quote(const std::string &book, double sales_price) : 5 bookNo(book), price(sales_price) {} 6 7 std::string isbn() const { 8 return bookNo; 9 } 10 11 virtual double net_price(std::size_t n) const {//定义成虚函数,运行2时进行动态绑定 12 return n * price; 13 } 14 15 virtual ~Quote() = default;//基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作 16 17 private: 18 std::string bookNo;//书籍的isbn编号 19 20 protected://可被派生类访问 21 double price = 0.0;//代表普通状态下不打折的价格 22 };
注意:基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作
基类通过在其成员函数的声明之前加上关键字 virtual 使得该函数执行动态绑定。任何构造函数之外的非静态函数都可以是虚函数。如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数
派生类可以继承定义在基类中的成员,但是派生类不一定有权访问从基类继承而来的成员。派生类只能访问公有成员和受保护的成员,不能访问私有成员
定义派生类:
1 class Bulk_quote : public Quote { 2 public: 3 Bulk_quote() = default; 4 Bulk_quote(const std::string&, double, std::size_t, double); 5 6 double net_price(std::size_t) const override;//override显式注明该成员函数覆盖它继承的虚函数 7 8 // ~Bulk_quote(); 9 10 private: 11 std::size_t min_qty = 0;//适用折扣政策的最低购买量 12 double discount = 0.0;//以小数表示的折扣额 13 14 };
注意:override 显式注明该成员函数覆盖它继承的虚函数(只能对继承自虚函数的成员使用 override 关键字)
派生类对象及派生类向基类的类型转换:
因为在派生类对象中含有与其基类对应的组成部分,所以我们能把派生类的对象当作基类对象使用,而且我们也能将基类的指针或引用绑定到派生类对象中的基类部分上:
1 Quote item;//基类对象 2 Bulk_quote bulk;//派生类对象 3 4 Quote *p = &item;//p指向Quote对象 5 p = &bulk;//p指向bulk中的Quote部分 6 7 Quote &r = bulk;//r绑定到bulk中的Quote部分
注意:这种转换通常称为派生类到基类的类型转换。和其它类型转换一样,编译器会隐式地执行派生类到基类的转换。这种隐式特性意味着我们可以把派生类对象或者派生类对象的引用用在需要基类引用的地方;同样的,我们可以把派生类对象的指针用在需要基类指针的地方
派生类构造函数:
尽管在派生类对象中含有从基类继承而来的成员,但是派生类并不能直接初始化这些成员。和其它创建了基类对象的代码一样,派生类也必须使用基类的构造函数类初始化它的基类部分
1 Bulk_quote::Bulk_quote(const std::string &book, double p, std::size_t qty, double disc) : 2 Quote(book, p), min_qty(qty), discount(disc) {}
注意:首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员
派生类中基类数据成员如果没有显式构造,则会执行默认初始化
派生类使用基类的成员:
1 double Bulk_quote::net_price(size_t cnt) const { 2 if(cnt >= min_qty) return cnt * (1 - discount) * price; 3 return cnt * price; 4 }
注意:派生类可以访问基类的公有成员和受保护成员
派生类的作用域嵌套在基类的作用域之内。因此,对于派生类的一个成员来说,它使用派生类成员的方式与使用基类成员的方式是一样的
继承与静态成员:
1 #include <iostream> 2 using namespace std; 3 4 class Base{ 5 public: 6 // Base(); 7 // ~Base(); 8 static void statmem(); 9 10 }; 11 12 void Base::statmem() { 13 // 14 } 15 16 class Derived : public Base{ 17 public: 18 // Derived(); 19 // ~Derived(); 20 void f(const Derived&); 21 }; 22 23 void Derived::f(const Derived &derived_obj) { 24 Base::statmem();//正确,Base定义了stamem 25 Derived::statmem();//正确,Derived继承了stamem 26 derived_obj.statmem();//通过Derived对象访问 27 statmem();//通过this对象访问 28 } 29 30 int main(void){ 31 32 }
注意:如果基类定义了一个静态成员,则在整个继承体些中只存在该成员的唯一定义。无论从基类中派生出多少个派生类,对于每个静态成员来说都只存在唯一的实例
静态成员遵循通用的访问控制规制,如果基类中的成员是 private 的,则派生类无权访问它
派生类的声明:
派生类的声明中包含类名但不包含派生列表:
1 class Bulk_quote : public Quote;//错误,派生列表不能出现在这里 2 class Bulk_quote;//正确
被用作基类的类:
如果我们想将某个类用作基类,则该类必须已经定义而非仅仅声明:
1 class Quote; //声明但未定义 2 class Bulk_quote : public Quote {//错误,Quote必须被定义 3 // 4 }
一个类是基类,同时也可以是一个派生类。最终的派生类将包含它的直接基类的子对象以及每个简介基类的子对象
防止继承的发生:
在类名后面跟一个关键字 final 能防止继承发生:
1 class NoDerived final {};//NoDerived不能作为基类 2 class Base {}; 3 4 class Last final : Base {};//Last是final的,我们不能继承Last 5 6 // class Bad : NoDerived {};//错误,NoDerived是final的 7 // class Bad2 : Last {};//错误,Last是final的
类型转换与继承:
可以将基类的指针或引用绑定到派生类对象上,当使用基类的引用或指针时,实际上我们并不清楚该引用或指针所绑定对象的真实类型。该对象可能是基类的对象,也可能是派生类的对象
和内置指针一样,只能指针类也支持派生类想基类的类型转换,这意味着我们可以将一个派生类对象的指针存储在一个基类的智能指针内
静态类型与动态类型:
当使用存在继承关系的类型时,必须将一个变量或其它表达式的静态类型与该表达式表示对象的动态类型区分开来。表达式的静态类型在编译时总是已知的,它是变量声明时类型或表达式生成的类型;动态类型则是变量或表达式的内存中对象的类型。动态类型直到运行时才可知。
如果达式既不是引用也不是指针,则它的动态类型永远与静态类型一致。而基类的指针或引用的静态类型可能与其动态类型不一致。如:
1 Bulk_quote bulk;//派生类对象 2 Quote q* = &bulk;//q的静态类型为Quote,动态类型为Bulk_quote 3 Quote &p = bulk;//p的静态类型为Quote,动态类型为Bulk_quote
不存在从基类向派生类的隐式类型转换:
之所以存在派生类向基类的类型转换是因为每个派生类对象都包含一个基类部分,而基类的引用或指针可以绑定到该基类部分上。但基类不一定包含派生类(派生类可能定义了新的成员),所以如果我们将一个基类的对象向派生类的类型转换,则我们有可能会使用基类中没有的成员,所以不存在从基类向派生类的自动类型转换:
1 Quote base; 2 // Bulk_quote *bulkp = &base;//错误,不能将基类转换成派生类 3 // Bulk_quote &bulkref = base;//错误,不能将基类转换成派生类 4 5 // 即使一个基类指针或引用绑定在一个派生类对象上,也不能执行从基类向派生类的转换 6 Bulk_quote bulk; 7 Quote *itemp = &bulk;//正确,从派生类转换到基类 8 // Bulk_quote *bulkp = itemp;//错误,不能将基类转换成派生类
注意:即使一个基类指针或引用绑定在一个派生类对象上,也不能执行从基类向派生类的转换
我们可以通过 dynamic_cast 或 static_cast 显式地将一个基类对象转换为派生类类型。详见百度百科:https://baike.baidu.com/item/dynamic_cast/4473047?fr=aladdin
在对象之间不存在类型转换:
派生类向基类的自动类型转换只对指针或引用类型有效,在派生类类型和基类类型之间不存在这样的转换。但我们可以通过拷贝构造函数、移动构造函数、拷贝赋值运算符或移动赋值运算符将一个派生类类型转换成基类类型,因为这些拷贝控制成员中通常包含一个本类类型的 const 引用或右值引用:
1 #include <iostream> 2 using namespace std; 3 4 class Quote { 5 public: 6 Quote() = default; 7 Quote(const std::string &book, double sales_price) : 8 bookNo(book), price(sales_price) {} 9 10 std::string isbn() const { 11 return bookNo; 12 } 13 14 virtual double net_price(std::size_t n) const {//定义成虚函数,运行2时进行动态绑定 15 return n * price; 16 } 17 18 virtual ~Quote() = default;//基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作 19 20 private: 21 std::string bookNo;//书籍的isbn编号 22 23 protected://可被派生类访问 24 double price = 0.0;//代表普通状态下不打折的价格 25 }; 26 27 class Bulk_quote : public Quote { 28 public: 29 Bulk_quote() = default; 30 Bulk_quote(const std::string&, double, std::size_t, double); 31 32 double net_price(std::size_t) const override;//override显式注明该成员函数覆盖它继承的虚函数 33 34 // ~Bulk_quote(); 35 36 private: 37 std::size_t min_qty = 0;//适用折扣政策的最低购买量 38 double discount = 0.0;//以小数表示的折扣额 39 40 }; 41 42 Bulk_quote::Bulk_quote(const std::string &book, double p, std::size_t qty, double disc) : 43 Quote(book, p), min_qty(qty), discount(disc) {} 44 45 double Bulk_quote::net_price(size_t cnt) const { 46 if(cnt >= min_qty) return cnt * (1 - discount) * price; 47 return cnt * price; 48 } 49 50 int main(void){ 51 52 Bulk_quote bulk;//派生类对象 53 Quote item(bulk);//使用合成拷贝构造函数Quote::Quote(const Quote&) 54 item = bulk;//使用合成拷贝赋值运算符Quote& Quote::operator=(const Quote&) 55 56 //显然我们不能将基类类型通过拷贝控制成员转换成派生类对象 57 // Quote cnt; 58 // Bulk_quote gg(cnt); 59 60 return 0; 61 }
注意:在上述过程中会忽略 Bulk_quote 中新定义的成员,即 bulk 中只有从基类中继承来的部分被赋值给了 item
显然,我们不能将基类类型通过拷贝控制成员转换成派生类对象