第十五章:面向对象程序设计Ⅱ
15、4 抽象基类:
-
定义Disc_quote类保存购买量的值和折扣值。表示特定策略的类分别继承自Disc_quote,派生类定义自己的net_price函数实现各自的折扣。
-
Disc_quote类表示的是一本打折书籍的通用概念,而非某种具体的折扣策略,不希望用户创建一个Disc_quote对象。
-
将Disc_quote::net_price()定义为纯虚函数(pure virtual)告诉用户当前net_price函数没有实际意义。纯虚函数无需定义,通过再声明语句的分号前加=0即可将一个虚函数说明为纯虚函数,=0只能出现在类内部虚函数的声明处。
-
// abstract base class to hold the discount rate and quantity // derived classes will implement pricing strategies using these data class Disc_quote : public Quote { public: // 默认构造函数,对成员默认初始化 Disc_quote() = default; // 虽不能使用这个构造函数,但是派生类可以使用它构建派生类对象的Disc_quote部分 Disc_quote(const std::string& book, double price, std::size_t qty, double disc): Quote(book, price), quantity(qty), discount(disc) { } // 纯虚函数,不希望用户调用 double net_price(std::size_t) const = 0; std::pair<size_t, double> discount_policy() const { return {quantity, discount}; } protected: std::size_t quantity = 0; // purchase size for the discount to apply double discount = 0.0; // fractional discount to apply };
-
-
含有纯虚函数的类是抽象基类:
-
含有(未经覆盖直接继承的)纯虚函数的类是抽象基类(abstract base class)。抽象基类负责定义接口,后续的其他类可以覆盖该接口。不能创建一个抽象基类的对象。
-
Disc_quote discounted; //错误,不能定义Disc_quote的对象
-
-
Disc_quote的派生类必须给出自己的net_price定义(覆盖纯虚函数net_price())否则任然是抽象基类。
-
-
派生类构造函数只初始化它的直接基类:
-
定义Bulk_quote使它继承Disc_quote而非Quote;
-
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; };
-
每个类各自控制其对象的初始化过程,即使Bulk_quote没有自己的数据成员,也需要提供一个四参数的构造函数。
-
构造函数Bulk_quote::Bulk_quote(const string&,double, size_t,double)将其实参传递给Disc_quote的构造函数,随后Disc_quote的构造函数调用Quote的构造函数初始化bulk的bookNo和price成员,Quote构造函数结束后,开始运行Disc_quote的构造函数初始化quantity和discount成员,最后运行Bulk_quote的构造函数。
-
重构(refactoring)负责重新设计类的体系以便于将操作/数据从一个类移动到另一个类中。
-
15、5 访问控制与继承:
-
每个类不仅控制自己成员初始化过程,还控制着其成员对派生类是否可访问。
-
受保护的成员:
-
类使用protected关键字声明它希望与派生类分享但不想被其他公共访问的成员。
-
受保护的成员对于类的用户(对象)来说是不可访问的
-
受保护的成员对于派生类的成员和友元是可访问的。
-
派生类的成员或友元只能通过派生类对象来访问基类的受保护成员,派生类对于一个基类对象中的受保护成员没有访问特权。 只能通过派生类对象访问派生类从基类中继承的成员。
-
class Base{ protected: int prot_mem; // protected成员 }; class Sneaky : public Base{ friend void clobber(Sneaky &); //可以访问Sneaky::prot_mem friend void clobber(Base &); // 非法,不能访问Base::prot_mem int j; // j默认是private的 }; void clobber(Sneaky &s){ s.j = s.prot_mem = 0; // 正确,clobber可访问Sneaky对象的private和protected成员 } void clobber(Base &b){ b.prot_mem = 0; // 错误,clobber不能访问Base的protected成员 }
-
派生类的成员和友元只能访问派生类对象中的基类部分的受保护成员,对普通的基类对象中的成员不具有访问权限。
-
-
公有、私有和受保护继承:
-
类对继承来的成员的访问受两个因素影响:1、基类中成员的访问说明符,2、派生类的派生列表中的访问说明符。
-
class Base{ public: void pub_mem(); // public成员 protected: int prot_mem; // protected成员 private: char priv_mem; // private成员 }; struct Pub_Derv : public Base{ // 派生类能访问protected成员 int f(){return prot_mem;} // 派生类不能访问private成员 char g(){return priv_mem;} }; struct Priv_Derv : private Base{ // private不影响派生类的访问权限 int f1() const {return prot_mem;} };
-
-
派生访问说明符对 派生类的成员(友元)能否访问其直接基类成员没影响,对基类成员的访问权限只与基类中的访问说明符有关。public或private继承的派生类都可访问基类的protected 成员不能访问private成员。
-
派生类的对象对于基类成员的访问权限:如果继承是公有的,成员遵循原有的访问说明符。继承是私有的则变为proivate权限。
-
Pub_Derv d1; // 继承自Base的成员是public的 Priv_Derv d2;// 继承自Base的成员是private的 d1.pub_mem();// 正确,pub_mem在派生类中是public的 d2.pub_mem();// 错误,pub_mem在派生类中是private的
-
-
访问说明符还控制继承自派生类的新类的访问权限。
-
struct Derived_from_Public:public Pub_Derv{ // 正确,在Pub_Derv中仍然是protected的 int use_base() {return prot_mem;} } struct Derived_from_Private : public Priv_Derv{ // 错误,由于Priv_Derv使用private方式继承Base,所以prot_mem是private的了 int use_base(){return prot_mem;} }
-
-
-
派生类向基类转换的可访问性:
- 派生类向基类的转换是否可访问:
- 只有D公有的继承了B,用户才能使用派生类向基类的转换。(派生类赋值给基类的指针或引用)如果是受保护或私有的,则不能转换。
- 无论D如何继承B,D中成员函数和友元都能使用派生类向基类的转换;
- 若D公有或受保护继承B,则D的派生类的成员和友元才能用D向B的类型转换,若是私有的,则不能使用。
- 派生类向基类的转换是否可访问:
-
友元与继承:
-
友元关系不能传递,同样友元关系也不能继承。基类友元访问派生类成员时不具有特殊性,派生类友元也不能随便访问基类成员。
-
class Base{ friend class Pal; } class Pal{ public: int f(Base b){return b.prot_mem;} // Pal是Base的友元 int f2(Sneaky s) {return s.j;} // 错误,Pal不是Sneaky的友元,j是Snealy类的私有成员,Pal无权访问 int f3(Sneaky s) {return s.prot_mem;} // 正确,Pal是Base的友元,可以访问Base对象的成员,包括Base对象内嵌在派生类对象中的情况 }
-
-
Pal是Base的友元,可以访问Base对象的成员,包括Base对象内嵌在派生类对象中的情况.
-
不能继承友元关系;每个类负责控制各自成员的访问权限。
-
-
改变个别成员的可访问性:
-
改变派生类继承的某个名字的访问级别,可通过使用using 声明达到这一目的。
-
class Base{ public: std::size_t size() const {return n;} protected: std::size_t n; }; class Derived : private Base{ public: using Base::size; // 保持对象相关的成员访问级别,用户可以访问size成员 protected: // 派生类可以使用n using Base::n; };
-
使用了using声明语句后改变了成员的可访问性,使得private继承的私有成员可以被Derived的用户使用,派生类也能使用 n。
-
using声明语句中名字的访问权限由using声明语句前的访问说明符决定,出现在private部分,该名字只能被类的成员和友元访问。
-
派生类只能为那些它可以访问的名字提供using声明。
-
-
-
默认的继承保护级别:
-
使用class关键字定义的派生类是私有继承,使用struct定义的派生类是公有继承:
-
class Base{}; struct D1 : Base{}; // 默认public继承 class D2 : Base{}; // 默认private继承
-
-
15、6 继承中的类作用域:
-
派生类的作用域嵌套在其基类的作用域内。若一个名字无法在派生类的作用域内解析,则编译器会继续在外层的基类作用域中寻找该名字的定义。
-
正是由于类作用域有继承嵌套的关系,所以派生类才能像使用自己的成员一样使用基类成员。
-
在编译时进行名字查找:
-
一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的,即使静态成员与动态类型可能不一致,但是可以使用哪些成员仍是由静态类型决定的。
-
// 为派生类Disc_quote添加一个函数 // 继承关系: Quote-> Disc_quote -> Bulk_quote class Disc_quote : public Quote{ public: pair<size_t,double> discount_policy() const{ return {quantity, discount}; } }; Bulk_quote bulk; Bulk_quote* bulkp = &bulk; // 静态类型与动态类型一致 Quote* item = &bulk; // 静态类型与动态类型不一致 bulkp->discount_policy(); // 正确, bulk类型是 Bulk_quote* item->discount_policy(); // 错误, itemP的类型是Quote*
-
尽管bulk中含有一个discount_policy成员,但是其对itemP来说是不可见的,itemP是Quote的指针,意味着对discount_policy的搜索是从Quote开始的,由于Quote不包含discount_policy所以无法通过Quote的对象,引用或指针调用discount_policy.
-
-
名字冲突与继承:
- 派生类中定义的名字将隐藏定义在外层作用域的名字。即其直接基类与间接基类中同名字的便利将被隐藏。派生类的成员将隐藏同名的基类成员。
-
通过作用域运算符类使用隐藏的成员:
-
可以使用作用域运算符使用一个被隐藏的基类成员。
-
struct Derived : Base{ int get_base_mem(){ return Base::mem; } };
-
-
作用域运算符覆盖原有的查找规则,指示编译器从Base类的作用域开始查找mem。
-
除了覆盖继承而来的虚函数外,派生类不要重用其他定义在基类中的名字。
-
-
名字查找优先于类型检查:
-
定义在派生类中的函数不会重载基类中的成员,派生类的成员会隐藏基类中同名的成员。即使派生类成员和基类成员的形参列表不一致,基类成员也会被隐藏。
-
struct Base{ int memfcn(); }; struct Derived : Base{ int memfcn(int); // 隐藏基类中的memfcn函数,不会发生重载 }; Derived d; Base b; b.memfcn(); // 调用Base::memfcn d.memfcn(10); // 调用 Drived::memfcn d.memfcn(); // 错误,Derived中没有参数列表为空的函数memfcn d.Base::memfcn();// 正确,调用Base::memfcn
-
-
编译器在Derived中查到了名为memfcn的成员,则查找过程终止,名字一旦找到就不再继续查找了。查到的memfcn需要int实参,不提供则报错。
-
-
虚函数与作用域:
-
基类与派生类中的虚函数必须有相同的形参列表,否则就无法通过基类的引用或指针调用派生类的虚函数。
-
class Base{ public: virtual int fcn(); // 虚函数 }; class D1 : public Base{ public: int fcn(int); // 隐藏基类的fcn,这个不是虚函数,与Base中的fcn形参列表不一致。是D1自己定义的函数fcn,D1中目前还有从Base继承的函数fcn virtual void f2(); // 新的虚函数,Base中不存在 }; class D2 : public D1{ public: int fcn(int); // 非虚函数,隐藏了D1::fcn(int) int fcn(); // 覆盖了Base的虚函数 void f2(); // 覆盖了D1的虚函数f2 };
-
-
基类调用隐藏的虚函数:
- 基类的指针调用派生类中隐藏的虚函数,当调用虚函数时,编译器产生的代码将在运行时确定使用虚函数的哪个版本,判断依据时动态绑定对象的动态类型。
- 当调用的是非虚函数时,不会发生动态绑定,实际调用的版本由指针的静态类型决定。
-
覆盖重载的函数:
- 一个类仅需覆盖重载集合的一些而非全部函数,如果覆盖基类的每一个版本将很麻烦,一个好的方法是为重载的成员提供一条using声明。
- using声明指定一个名字而不指定形参列表,一条基类成员函数的using声明语句可以将函数的所有重载实例添加到派生类作用域,此时派生类只需要定义特有的函数即可,无需为继承而来的其他函数重新定义。
-
15、7 构造函数与拷贝控制:
15、7、1虚析构函数:
-
基类通常应该定义一个虚析构函数,这样可以动态分配继承体系中的对象。
-
当delete一个动态分配的对象的指针将执行析构函数,继承体系内可能出现指针的静态类型与动态类型不符的现象。
-
class Quote{ public: // 删除一个指向派生类对象的基类指针,需要虚析构函数 virtual ~Quote() = default; // 动态绑定析构函数。 } // 析构函数的虚属性也会被继承。
-
由于一个类需要析构函数,通常也需要拷贝和赋值操作。虚析构函数是个例外。
-
一个类定义了析构函数,即使通过=default使用了合成的版本,编译器也不会为这个类合成移动操作。
-
类定义了拷贝构造函数,则编译器不会为其定义合成的移动操作。
15、7、2合成拷贝控制与继承:
-
基类的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数是被删除的或不可访问(private),则派生类中对应的成员是被删除的。编译器无法使用基类成员指向派生类对象基类部分的构造、赋值或销毁操作。
-
基类中有一个不可访问或删除掉的移动操作,派生类中合成的默认和拷贝构造函数将是被删除的,编译器无法销毁派生类对象的基类部分。
-
基类中移动、析构函数被定义为删除的,则派生类的移动、析构函数也是被删除的。
-
如果基类中没有默认、拷贝或移动构造函数,则一般派生类也不会定义相应操作。除非派生类自定义相应版本的构造函数。
-
移动操作与继承:
-
基类中定义虚析构函数,默认情况下基类通常不含有合成的移动操作,且在派生类中也没有合成的移动操作。
-
当派生类需要执行移动操作时,需要先在基类中定义。
-
class Quote{ public: Quote() = default; Quote(const Quote&) = default; Quote(Quote&&) = default; Quote& operator=(const Quote&) = default; // 拷贝赋值 Quote& operator=(Quote&& ) = default; virtual ~Quote()= default; }
-
15、7、4派生类的拷贝控制成员:
-
派生类的拷贝和移动构造函数在拷贝和移动自有成员的同时,也要拷贝和移动基类部分的成员。
-
派生类定义了拷贝或移动操作时,该操作赋值拷贝或移动包括基类部分成员在内的整个对象。
-
当为派生类定义拷贝或移动构造函数时,通常使用对应的基类构造函数初始化对象的基类部分,如果想拷贝(移动)基类部分,必须在派生类的构造函数初始值列表中显示的使用基类的拷贝(移动)构造函数。
-
class Base{}; class D : public Base{ public: // 使用拷贝或移动构造函数,必须在构造函数初始值列表中显示地调用基类构造函数 D(const D& d):Base(d)/*D的成员的初始值*/{} D(D&& d):Base(std::move(d)))/*D的成员的初始值*/{} } // 这里未提供基类成员初始值,导致基类部分被默认初始化,而非拷贝。 D(const D& d)/*D的成员的初始值*/{}
-
派生类赋值运算符:
-
派生类的赋值运算符也必须显示地为其基类部分赋值。
-
//Base::operator=(const Base&); 基类的赋值运算不会自动调用 D & D::operator=(const D& rhs){ // 显示调用基类赋值运算符为基类部分赋值。 Base::operator=(rhs); return *this; }
-
无论基类的构造函数或赋值运算符是自定义的版本还是合成的版本,派生类的对应操作都能使用它们。
-
-
派生类析构函数:
- 析构函数体执行完后,对象的成员会被隐式销毁,对象的基类部分也是隐式销毁的,派生类只负责销毁由派生类自己分配的资源。
- 构造函数或析构函数调用了某个虚函数,应该执行与构造函数或析构函数所属类型相对应的虚函数版本。
-
继承的构造函数:
-
一个类只初始化他的直接基类,也只继承其直接基类的构造函数。类不能继承默认、拷贝和移动构造函数。如果派生类没有直接定义这些构造函数,编译器将为派生类合成他们。
-
派生类继承基类构造函数的方式是通过using声明,这里的using声明将令编译器产生代码。对于基类中的每一个构造函数,编译器都生成一个形参列表完全相同的构造函数。如果派生类含有自己的数据成员,则这些成员默认初始化。
-
class Bulk_quote : public Disc_quote{ public: using Disc_quote :: Disc_quote; //继承DIsc_quote的构造函数 double net_price(std::size_t) const; }
-
using 声明后编译器生成的构造函数形如:
- derived(params) : base(args){}
- derived是派生类的名字,base是基类的名字,params是构造函数的形参列表,args将派生类构造函数的形参传递给基类的构造函数。
-
继承的构造函数特点:
- 构造函数的using声明不会改变构造函数的访问级别。
- using声明不能指定explicit或constexpr,继承的构造函数与基类这些属性相同。
- 基类构造函数内所含的默认实参不会被派生类继承,派生类会生成多个继承的构造函数,每个构造函数分别省略掉一个含有默认实参的形参。
- 派生类定义的构造函数与基类的构造函数具有相同的参数列表,则该构造函数不会被继承,定义在派生类的构造函数将替换继承来的构造函数。
- 默认、拷贝和移动构造函数不会被继承。这些构造函数按正常规则被合成。继承的构造函数不会被用作用户定义的构造函数来使用,一个类只有继承的构造函数,则也会含有一个合成的默认构造函数。
-
15、8 容器与继承:
-
使用容器存放继承体系中的对象时,必须采用 间接存储 的方式,因为不允许在容器里保存不同类型的元素,所以不能将具有继承关系的多种类型的对象直接存放在容器中。
-
当派生类对象被赋值给基类对象时,其中的派生类部分会被“切掉”,因此容器与存在继承关系的类型无法兼容。
-
在容器中放置(智能)指针而非对象。
-
当在容器中存放具有继承关系的对象时,实际存放的是基类的指针(智能指针)。指针所指的动态类型可能是基类类型也可能是派生类类型。
-
vector<shared_ptr<Qupte>> basket; basket.push_back(make_shared<Quote>("0-201-82470-1",50)); basket.push_back(make_shared<Bulk_quote>("0-201-52470-1",10,.25)); cout << basket.back()->net_price(15) << endl;
-
实际调用net_price()的版本依赖于指针所指对象的动态类型。
-
-
可以将一个派生类的普通指针和智能指针转换为基类的普通指针和智能指针。
-
尽管在形式上有所差别,basket的所有元素的类型都是相同的。