首页 > 代码库 > C++ Primer 学习笔记_65_面向对象编程 --概述、定义基类和派生类
C++ Primer 学习笔记_65_面向对象编程 --概述、定义基类和派生类
面向对象编程
--概述、定义基类和派生类
引言:
面向对象编程基于的三个基本概念:数据抽象、继承和动态绑定。
在C++中,用类进行数据抽象,用类派生从一个类继承另一个:派生类继承基类的成员。动态绑定使编译器能够在运行时决定是使用基类中定义的函数还是派生类中定义的函数。
继承和动态绑定在两个方面简化了我们的程序:[继承]能够容易地定义与其他类相似但又不相同的新类,[派生]能够更容易地编写忽略这些相似类型之间区别的程序。
面向对象编程:概述
面向对象编程的关键思想是多态性(polymorphism)。之所以称通过继承而相关联的类型为多态类型,是因为在许多情况下可以互换地使用派生类型或基类型的“许多形态”。正如我们将看到的,在C++中,多态性仅用于通过继承而相关联的类型的引用或指针。
1、继承
派生类(derivedclass)能够继承基类(baseclass)定义的成员,派生类可以无须改变而使用那些与派生类型具体特性不相关的操作,派生类可以重定义那些与派生类型相关的成员函数,将函数特化,考虑派生类型的特性。最后,除了从基类继承的成员之外,派生类还可以定义更多的成员。
我们经常称因继承而相关联的类为构成了一个继承层次。其中有一个类称为根,所以其他类直接或间接继承根类。如:
class Item_base { public: Item_base(const std::string &book = "", double sales_price = 0.0):isbn(book),price(sales_price) {} std::string book() const { return isbn; } virtual double net_price(std::size_t n) const { return n * price; } virtual ~Item_base() {} private: std::string isbn; protected: double price; };
Item_base的派生类将无须改变地继承book函数:派生类不需要重新定义获取ISBN的含义。另一方面,每个派生类需要定义自己的net_price函数版本,以实现适当的折扣价格策略。
在C++中,基类必须指出希望派生类重写哪些函数,定义为virtual的函数是基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。
讨论过这些之后,可以看到我们的类将定义三个(const)成员函数:
1)非虚函数std::stringbook(),返回ISBN。由Item_base定义,Bulk_item 继承。
2)虚函数doublenet_price(size_t) 的两个版本(其中一个已经定义出),返回给定数目的某书的总价。Item_base类和Bulk_item类将定义该函数自己的版本。
2、动态绑定
通过动态绑定,我们能够编写程序使用继承层次中任意类型的对象,无需关心对象的具体类型。使用这些类的程序无须区分函数是在基类还是在派生类中定义的。例如可以编写print_total函数:
void print_total(ostream &os,const Item_base &item,size_t n) { os << "ISBN: " << item.book() << "\t number sold: " << n << "\ttotal price: " << item.net_price(n) << endl; }
【注意:】
第一,虽然这个函数的第二形参是Item_base的引用但可以将Item_base对象或Bulk_item对象传给它。
第二,因为形参是引用且net_price是虚函数,所以对net_price的调用将在运行时确定。调用哪个版本的net_price将依赖于传给print_total的实参。
【小结】
在C++中,通过基类的引用(或指针)调用虚函数时,发生动态绑定。引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指对象的实际类型所定义的。
定义基类和派生类
一、定义基类
像其他类一样,基类也有其接口和实现的数据和函数成员:
class Item_base { public: Item_base(const std::string &book = "", double sales_price = 0.0):isbn(book),price(sales_price) {} std::string book() const { return isbn; } virtual double net_price(std::size_t n) const { return n * price; } //继承层次的根类一般都要定义虚析构函数 virtual ~Item_base() {} private: std::string isbn; protected: double price; };
1、基类成员函数
虚函数:保留字virtual的目的是启用动态绑定。成员默认为非虚函数,对非虚函数的调用在编译时确定,为了指明函数为虚函数,必须加上virtual关键字:
virtual double net_price(std::size_t n) const;
除了构造函数之外,任意非static成员都可以使虚函数。保留字virtual只在类内部的成员函数声明中出现,不能在类定义体外部出现在类定义体上。
【最佳实践】
基类通常应将派生类需要重定义的任意函数定义为虚函数。
2、访问控制和继承
在基类中,public和 private标号具有普通含义:用户代码可以访问类的public成员而不能访问private成员,private成员只能由基类的成员和友元访问。派生类对基类的public和 private成员的访问权限与程序中任意其他部分一样:它可以访问public成员而不能访问private成员。
protected成员可以被派生类对象访问但不能被该类型的普通用户访问。
二、protected成员
可以认为protected访问标号是private和public的混合:
1)像private成员一样,protected成员不能被类的用户访问。
2)像public成员一样,protected成员可被该类的派生类访问。
此外,protected还有另一重要性质:
派生类只能通过派生类对象访问其基类的protected成员,派生类对其基类类型对象protected成员没有特殊访问权限。
void Bulk_item::memfcn(const Bulk_item &d,const Item_base &b) { double ret = price; ret = d.price; ret = b.price; //Error }
【关键概念:类设计与受保护成员】
派生类的提供者通常(但并不总是)需要访问(一般为private的)基类实现,为了允许这种访问而仍然禁止对实现的一般访问,提供了附加的protected访问标号。
定义类充当基类时,将成员设计为public的标准并没有改变:仍然是接口函数应该为 public而数据一般应为private。被继承的类必须决定实现的哪些部分声明为protected而哪些部分声明为private。希望禁止派生类访问的成员应该设为private,提供派生类实现所需操作或数据的成员应设为 protected。换句话说,提供给派生类的接口是protected成员和public成员的组合。
三、派生类
为了定义派生类,使用派生类列表指定基类。派生类列表指定了一个或多个基类以及访问权限:
class ClassName: access-label base-class
其中,以继承单个基类最为常见。然后访问标号[public,private,protected]决定了对继承成员的访问权限。如果想要继承基类的接口,则应该进行public派生。
派生类继承基类的成员并且可以定义自己的附加成员。每个派生类对象包含两个部分:从基类继承的成员和自己定义的成员。一般而言,派生类只(重)定义那些与基类不同或扩展基类行为的方面。
1、定义派生类
从Item_base类派生Bulk_item类,Bulk_item类将继承book、isbn和price成员。Bulk_item类必须重定义net_price函数并定义该操作所需要的数据成员:
class Bulk_item:public Item_base { public: double net_price(std::size_t ) const; private: std::size_t min_qty; double discount; };
每个Bulk_item对象包含四个数据成员:从Item_base继承的isbn和price,自己定义的min_qty和discount。
2、派生类和虚函数
派生类一般会重定义所继承的虚函数,如果派生类没有重定义某个虚函数,则使用基类中定义的版本。
派生类型必须对想要重定义的每个继承成员进行声明。虚函数的声明必须与基类中的定义方式完全匹配,但有一个例外:返回对基类型的引用(或指针)的虚函数。派生类中的虚函数可以返回基类函数所返回类型的派生类的引用(或指针)。
【注释】
一旦函数在基类中声明为虚函数,它就一直为虚函数,派生类无法改变该函数为虚函数这一事实。派生类重定义虚函数时,可以使用virtual保留字,但不是必须这样做[但是建议这么做,可以提醒类的使用者该函数为virtual函数]。
3、派生类对象包含基类对象作为子对象
派生类对象由多个部分组成:派生类本身定义的(非static)成员+由基类(非static)成员组成的子对象。
【注解:C++语言不要求编译器将对象的基类部分和派生部分和派生部分连续排列,因此,图中是关于类如何工作的概念表示而不是物理表示。】
4、派生类中的函数可以使用基类的成员
double Bulk_item::net_price(std::size_t cnt) const { if (cnt >= min_qty) { return cnt * (1 - discount) * price; } return cnt * price; }
因为每个派生类对象都有基类部分,类可以访问其基类的public和protected成员,就好像那些成员是自己的一样[不拿自己当外人O(∩_∩)O哈!]!
5、用作基类的类必须是已经定义的
class Item_base; //仅仅声明了 //Error:Item_base没定义 class Bulk_item : public Item_base {};
每个派生类包含并且可以访问其基类的成员,为了使用这些成员,派生类必须知道它们是什么。这一规则暗示着不可能从类自身派生出一个类。
6、用派生类做基类
class Base { /*....*/ }; class D1: public Base { /*....*/ }; class D2: public D1 { /*....*/ };
每个类继承其基类所有成员。最底层的派生类继承其基类的成员,基类又继承自己的基类的成员,如此沿着继承链依次向上。最底层的派生类对象包含其每个直接基类和间接基类的子对象。
7、派生类的声明
派生类的声明包含类名,而不包含派生类列表。
class Bulk_item : public Item_base; //Error class Bulk_item; //OK
//P479 习题15.6 class Bulk_item:public Item_base { public: double net_price(std::size_t ) const; private: std::size_t min_qty; double discount; }; double Bulk_item::net_price(std::size_t cnt) const { if (cnt >= min_qty) { return cnt * (1 - discount) * price; } return cnt * price; }
//习题15.7 class Item_derivd : public Item_base { public: double net_price(std::size_t ) const; private: std::size_t count; double discount; }; double Item_derivd::net_price(std::size_t cnt) const { if (cnt <= count) { return cnt * (1 - discount) * price; } return cnt * price; }
未完待续...