文章目录

  • 前言
  • OOP:概述
    • 继承
    • 动态绑定
  • 定义基类和派生类
    • 定义基类
      • 成员函数与继承
      • 访问控制与继承
    • 定义派生类
      • 派生类中的虚函数
      • 派生类对象及派生类向基类的类型转换
      • 派生类构造函数
      • 派生类使用基类的成员
      • 继承与静态成员
      • 派生类的声明
      • 被用作基类的类
      • 防止继承的发生
    • 类型转换与继承
      • 静态类型与动态类型
      • 在对象之间不存在类型转换
  • 虚函数
    • 对虚函数的调用可能在运行时才被解析
    • c++的多态性
    • 派生类中的虚函数
    • final和override说明符
    • 回避虚函数的机制
  • 抽象基类
    • 纯虚函数
    • 含有纯虚函数的类是抽象基类
    • 派生类构造函数只初始化它的直接基类
    • 重构
  • 访问控制与继承
    • 受保护的成员 protected
    • 公有、私有和受保护继承
    • 派生类向基类转换的可访问性
    • 友元与继承
    • 改变各个成员的可访问性
    • 默认的继承保护级别
  • 继承中的类作用域
    • 在编译时进行名字查找
    • 名字冲突与继承
    • 名字查找先于类型检查
    • 虚函数与作用域,通过基类调用隐藏的虚函数
  • 构造函数与拷贝控制
    • 虚析构函数
      • 虚析构函数将阻止合成移动操作
    • 合成拷贝控制与继承
      • 派生类中删除的拷贝控制与基类的关系型
    • 派生类的拷贝控制成员
      • 定义派生类的拷贝或移动构造函数
      • 派生类赋值运算符
      • 派生类析构函数
      • 在构造函数和析构函数中调用虚函数
    • 继承的构造函数
      • 继承的构造函数的特点
  • 容器与继承
    • 在容器中放置(智能)指针而非对象
    • 编写Basket类
      • decltype的意义
      • [upper_bound可参考chapter 11](https://blog.csdn.net/weixin_43116900/article/details/105916425)
      • 模拟虚拷贝

前言

面向对象程序设计基于三个基本概念:数据抽象、继承和动态绑定。

继承和动态绑定对程序的编写有两方面的影响:一是我们可以更容易地定义与其他类相似但不完全相同的新类:二是在使用这些彼此相似的类编写程序时, 我们可以在一定程度上忽略掉它们的区别。

OOP:概述

面向对象程序设计的核心思想是数据抽象、继承和动态绑定。通过使用数据抽象,我们可以将类的接口与实现分离;使用继承,可以定义相似的类型并对其相似关系建模;使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。

继承

基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。

在c++语言中,基类将类型相关的函数与派生类不做改变直接继承的函数区分对待。对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数

基类:

class Quote{public:string isbn() const;virtual double net_price(size_t n) const;
};

派生类:

class Bulk_quote : public Quote{public:double net_price(size_t n) const override;
};

因为Bulk_quote在它的派生列表中使用了public关键字,因此我们完全可以把Bulk_quote的对象当成Quote的对象来使用。

派生类必须在其内部对所有重新定义的虚函数进行声明。派生类可以在这样的函数之前加上virtual关键字, 但是并不是非得这么做。C++11新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数, 具体措施是在该函数的形参列表之后增加一个override关键字

动态绑定

通过使用动态绑定,我们能用同一段代码分别处理Quote和Bulk_quote的对象。

double print_total(ostream &os,const Quote &item,size_t n){//根据item的形参对象类型调用Quote::net_price或者Bulk_quote::net_pricedouble ret = item.net_price(n);os<<"ISBN: "<<item.isbn()<<"# sold: "<<n<<"total due: "<<ret<<endl;return ret;
}

因为函数print_total的item形参是基类Quote的一个引用,所以,我们既能使用基类Quote的对象调用该函数,也能使用派生类Bulk_quote的对象调用它。又因为print_total是使用引用类型调用net_price函数的,所以实际传入print_total的对象类型将决定到底执行net_price的哪个版本。

在c++语言中,当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定。

定义基类和派生类

定义基类

基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。

成员函数与继承

在c++语言中,基类必须将它的两种成员函数区分开来:一种是基类希望其派生类进行覆盖的函数;另一种是基类希望派生类直接继承而不要改变的函数。对于前者,基类通常将其定义为虚函数,当我们使用指针或引用来调用虚函数时,该调用将被动态绑定。根据引用或指针所绑定的对象类型不同,该调用可能执行基类的版本,也可能执行某个派生类的版本。

基类通过在其成员函数的声明语句之前加上关键字virtual使得该函数执行动态绑定。任何构造函数之外的非静态函数都可以是虚函数。关键字 virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数

成员函数如果没被声明为虚函数,则其解析过程发生在编译时而非运行时。isbn函数的执行与派生类的细节无关,无论是Quote对象还是Bulk_quote对象,isbn函数的行为都一样。

访问控制与继承

派生类可以继承定义在基类中的成员,但是派生类的成员函数不一定有权访问从基类继承而来的成员。和其他使用基类的代码一样,派生类能访问公有成员,而不能访问私有成员。不过在某些时候基类中还有这样一种成员,基类希望它的派生类有权访问该成员同时禁止其他用户访问。我们用受保护的(protected)访问运算符说明这样的成员。

我们的Quote类希望它的派生类定义各自的net_price函数,因此派生类需要访问Quote的price成员。此时我们将price定义成受保护的。与之相反,派生类访问bookNo成员的方式与其他用户是样的,都是通过调用isbn函数,因此bookNo被定义成私有的,即使是Quote派生出来的类也不能直接访问它。

定义派生类

派生类必须通过使用类派生列表明确指出它是从哪个(哪些)基类继承而来的。类派生列表的形式是:首先是一个冒号,后面紧跟以逗号分隔的基类列表,其中每个基类前面可以有以下三种访问说明符中的一个:public、protected或者private。

访问说明符的作用是控制派生类从基类继承而来的成员是否对派生类的用户可见。

如果一个派生类是公有的,则基类的公有成员也是派生类接口的组成部分。此外,我们能将公有派生类型的对象绑定到基类的引用或指针上。因为我们在派生列表中使用了public,所以Bulk_quote的接口隐式地包含isbn函数,同时在任何需要Quote的引用或指针的地方我们都能使用Bulk_quote的对象。

派生类中的虚函数

派生类经常(但不总是)覆盖它继承的虚函数。如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本。

派生类对象及派生类向基类的类型转换

一个派生类对象包含多个组成部分:一个含有派生类自己定义的(非静态)成员的子对象,以及一个与该派生类继承的基类对应的子对象,如果有多个基类,那么这样的子对象也有多个。

因为在派生类对象中含有与其基类对应的组成部分,所以我们能把派生类的对象当成基类对象来使用,而且我们也能将基类的指针或引用绑定到派生类对象中的基类部分。

Quote item;          //基类对象
Bulk_quote bulk;    //派生类对象
Quote *p = &item;  //p指向Quote对象
p = &bulk;         //p指向bulk的Quote部分
Quote &r = bulk;   //r绑定到bulk的Quote部分

这种转换通常称为派生类到基类的类型转换。和其他类型一样,编译器会隐式地执行派生类到基类的转换。我们可以把派生类对象或者派生类对象的引用用在需要基类引用的地方;同样的,我们也可以把派生类对象的指针用在需要基类指针的地方。

派生类构造函数

尽管在派生类对象中含有从基类继承而来的成员,但是派生类并不能直接初始化这些成员。派生类必须使用基类的构造函数来初始化它的基类部分。

派生类构造函数同样是通过构造函数初始化列表来将实参传递给基类构造函数的。例如:

Bulk_quote::Bulk_quote(const string& book, double p, size_t qty, double disc):Quote(book,p),min_qty(qty),discount(disc){}

除非我们特别指出,否则派生类对象的基类部分会像数据成员一样执行默认初始化。如果想使用其他的基类构造函数,我们需要以类名加圆括号内的实参列表的形式为构造函数提供初始值。

首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。

派生类使用基类的成员

派生类可以访问基类的公有成员和受保护成员。

继承与静态成员

**如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。**不论从基类中派生出来多少个派生类,对于每个静态成员来说都只存在唯一的实例。

静态成员遵循通用的访问控制规则,如果基类中的成员是private的,则派生类无权访问它。假设某静态成员是可访问的,则我们既能通过基类使用它也能通过派生类使用它。

class Base {public:static void statmem() {cout << "这是Base的静态函数statmem()"<< endl;}};
class Derived:public Base {public:void f(const Derived& obj) {Base::statmem();Derived::statmem();obj.statmem();  //通过Derived对象访问statmem();     //通过this对象访问}
};

输出结果:

这是Base的静态函数statmem()
这是Base的静态函数statmem()
这是Base的静态函数statmem()
这是Base的静态函数statmem()

派生类的声明

派生类的声明与其他类差别不大,声明中包含类名但是不包含它的派生列表:

class Bulk_quote : public Quote;  错误,声明不包含派生列表
class Bulk_quote;                 正确

被用作基类的类

如果我们想将某个类用作基类,则该类必须已经定义而非仅仅声明。所以,一个类不能派生它本身

每个类都会继承直接基类的所有成员。对于一个最终类来说,它会继承其直接基类的成员,该直接基类的成员又含有其基类的成员,以此类推,最终的派生类将包含它的直接基类的子对象以及每个间接基类的子对象。

防止继承的发生

防止继承发生的方法,即在类名后跟一个关键字final:

class NoDerived final{};             NoDerived不能作为基类
class Last final:public Base{};     Last不能作为基类

类型转换与继承

通常情况下,如果我们想把引用或指针绑定到一个对象上,则引用或指针的类型应与对象的类型一致,或者对象的类型含有一个可接受的const类型转换规则。存在继承关系的类是一个重要的例外:我们可以将基类的指针或引用绑定到派生类对象上。

我们可以将基类的指针或引用绑定到派生类对象上,这意味着,当使用基类的引用(或指针)时,实际上我们并不清楚该引用(或指针)所绑定对象的真实类型。该对象可能是基类的对象,也可能是派生类的对象。

和内置指针一样,智能指针类也支持派生类向基类的类型转换,这意味着我们可以将一个派生类对象的指针存储在一个基类的智能指针类。

静态类型与动态类型

当我们使用存在继承关系的类型时,必须将一个变量或其他表达式的静态类型与该表达式表示对象的动态类型区分开来。表达式的静态类型在编译时总是已知的,它的变量声明时的类型或表达式生成的类型,动态类型则是变量或表达式表示的内存中的对象的类型。动态类型直到运行时才可知。

如果一个变量非指针也非引用,则它的静态类型和动态类型永远一致。但基类的指针或引用的动态类型可能与其动态类型不一致。

不存在从基类向派生类的隐式类型转换。

在对象之间不存在类型转换

派生类向基类的自动类型转换只对指针或引用类型有效,在派生类类型和基类类型之间不存在这样的转换。

当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分将被忽略掉。

虚函数

当我们使用基类的引用或指针调用一个虚成员函数时会执行动态绑定。

我们必须为每一个虚函数都提供定义,而不管它是否被用到了。

对虚函数的调用可能在运行时才被解析

当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确定应该调用哪个版本的函数。被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那一个。

当我们通过一个具有普通类型(非引用非指针)的表达式调用虚函数时,在编译时就会将调用的版本确定下来。

c++的多态性

OOP的核心思想是多态性(polymorphism)。多态性这个词源自希腊语,其含义是“多种形式”。我们把具有继承关系的多个类型称为多态类型,因为我们能使用这些类型的“多种形式”而无须在意它们的差异。引用或指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本所在

当我们使用基类的引用或指针调用基类中定义的一个函数时,我们并不知递该函数真正作用的对象是什么类型,因为它可能是一个基类的对象也可能是一个派生类的对 象。如果该函数是虚函数,则直到运行时才会决定到底执行哪个版本,判断的依据是引用或指针所绑定的对象的真实类型。

另一方面,对非虚函数的调用在编译时进行绑定。类似的,通过对象进行的函数(虚函数或非虚函数)调用也在编译时绑定。 对象的类型是确定不变的,我们无论如何都不可能令对象的动态类型与静态类型不一致。因此,通过对象进行的函数调用将在编译时绑定到该对象所属类中的函数版本上。

当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。

派生类中的虚函数

当我们在派生类中稷盖了某个虚函数时,可以再一次使用virtual关键字指出该函数的性质。然而这么做并非必须,因为一旦某个函数被声明成虚函数,则在所有派生类中它都是虚函数。

派生类中虚函数的返回类型也必须与基类函数匹配。该规则存在一个例外,当类的虚函数返回类型是类本身的指针或引用时,上述规则无效。也就是说,如果D由B派生得到则基类的虚函数可以返回B*而派生类的对应函数可以返回D*,只不过这样的返回类型要求从D到B的类型转换是可访问的。

基类中的虚函数在派生类中隐含地也是一个虚函数。当派生类覆盖了某个虚函数时,该函数在基类中的形参必须与派生类中的形参严格匹配。

final和override说明符

派生类如果定义了一个函数与基类中虚函数的名字相同但是形参列表不同,这仍然是合法的行为,编译器将认为新定义的这个函数与基类中原有的函数是相互独立的。

在c++11新标准中,我们可以使用override关键字来说明派生类中的虚函数。如果我们使用override标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将会报错。

class B {virtual void fl(int) const;
virtual void f2();
void f3 ();
};class Dl : B {void fl(int) const override; //正确:fl与基类中的fl匹配
void f2(int) override;  //错误:B没有形如f2(int)的函数void f3() override; //错误:f3不是虚函数
void f4 () override; //错误:B没有名为f4的函数
};

我们还能把某个函数指定为final,如果我们已经把函数定义成final了,则之后任何尝试覆盖该函数的操作都将会引发错误。

class D2:B{//从B继承f2()和f3(),覆盖f1(int)void f1(int)const final;  // 不允许后续的其他类覆盖f1(int)
}class D3:D2{void f2(); //正确:覆盖从间接基类B继承而来的f2void f1(int)const; //错误:D2已经将f2声明成final
}

回避虚函数的机制

在某些情况下,我们希望对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的某个特定版本。使用作用域运算符可以实现这一目的。

如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运算符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无限递归。

抽象基类

纯虚函数

和普通的虚函数不一样,一个纯虚函数无须定义。我们通过在函数体的位置(即在声明语句的分号之前)书写 =0 就可以将一个虚函数说明为纯虚函数。其中 =0 只能出现在类内部的虚函数声明语句处。

class Disc_quote : public Quote {public:Disc_quote() = default;Disc_quote(const string& book, double p, size_t qty, double disc):Quote(book, p), min_qty(qty), discount(disc) {}double net_price(size_t n) const = 0;
protected:size_t min_qty=0;  //折扣适用的购买量double discount = 0.0;  //表示折扣的小数值
};

我们也可以为纯虚函数提供定义,不过函数体必须定义在类的外部,也就是说,我们不能在类的内部为一个 =0 的函数提供函数体。

含有纯虚函数的类是抽象基类

含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类。抽象基类负责定义接口,而后续其他类可以覆盖该接口。我们不能(直接)创建一个抽象基类的对象。因为Disc_quote将net_price定义成了纯虚函数,所以我们不能定义Disc_quote的对象,我们可以定义Disc_quote的派生类的对象,前提是这些类覆盖了net_price函数。

Disc_quote的派生类必须给出自己的net_price定义,否则它们仍将是抽象基类。

派生类构造函数只初始化它的直接基类

重构

在Quote的继承体系中增加Disc_quote类是重构(refactoring)的一个典型示例。重构负责重新设计类的体系以便将操作和/或数据从一个类移动到另一个类中。对于面向对象的应用程序来说,重构是一种很普遍的现象。
值得注意的是,即使我们改变了整个继承体系,那些使用了Bulk_quote或Quote 的代码也无须进行任何改动。不过一旦类被重构(或以其他方式被改变),就意味着我们必须重新编译含有这些类的代码了。

访问控制与继承

每个类分别控制自己的成员初始化过程,与之类似,每个类还分别控制着其成员对于派生类来说是否可访问。

受保护的成员 protected

  • 和私有成员类似,受保护的成员对于类的用户来说是不可访问的
  • 和公有成员类似,受保护的成员对千派生类的成员和友元来说是可访问的。
  • 派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权。
class Base{protected:int mem;
};class Sneaky : public Base{friend void clobber(Sneaky&);  //能访问Sneaky::memfriend void clobber(Base&);  //不能访问 Base::mem
};

公有、私有和受保护继承

public protected private
公有继承 public protected 不可见
私有继承 private private 不可见
保护继承 protected protected 不可见

某个类对其继承而来的成员的访问权限受到两个因素影响:一是在基类中该成员的访问说明符,二是在派生类的派生列表中的访问说明符。

派生访问说明符对于派生类的成员(及友元)能否访问其直接基类的成员没什么影响。 对基类成员的访问权限只与基类中的访问说明符有关。

派生访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限。

故,示例如下:

继承代码:

class Base2 {public:void publicFun() {cout << "Base2的publicFun()" << endl;}
private:void privateFun() {cout << "Base2的privateFun()" << endl;}
protected:void protectedFun() {cout << "Base2的protectedFun()" << endl;}};class Public_derv : public Base2 {public:void f1() { cout << "Public_derv调用publicFun()     ";publicFun(); }//void f2() { privateFun(); }  //派生类不能访问private成员void f3() { cout << "Public_derv调用protectedFun()       ";protectedFun(); }
};class Private_derv : private Base2 {public:void f1() { cout << "Private_derv调用publicFun()        ";publicFun(); }//void f2() { privateFun(); }  //派生类不能访问private成员void f3() { cout << "Private_derv调用protectedFun()      ";protectedFun(); }
};class Protected_derv : protected Base2 {public:void f1() { cout << "Protected_derv调用publicFun()      ";publicFun(); }//void f2() { privateFun(); }  //派生类不能访问private成员void f3() { cout << "Protected_derv调用protectedFun()        ";protectedFun(); }
};

测试代码:

 Public_derv public_d;public_d.f1();public_d.f3();public_d.publicFun();Private_derv private_d;private_d.f1();private_d.f3();//private_d.publicFun();  publicFun()在派生类中是private的,不可访问Protected_derv protected_d;protected_d.f1();protected_d.f3();//protected_d.publicFun();  publicFun()在派生类中是protected的,不可访问

输出结果:

Public_derv调用publicFun()              Base2的publicFun()
Public_derv调用protectedFun()           Base2的protectedFun()
Base2的publicFun()
Private_derv调用publicFun()             Base2的publicFun()
Private_derv调用protectedFun()          Base2的protectedFun()
Protected_derv调用publicFun()           Base2的publicFun()
Protected_derv调用protectedFun()                Base2的protectedFun()

派生类向基类转换的可访问性

  • 只有当D公有的继承B时,用户代码才能使用派生类向基类的转换;如果D继承B的方式是受保护的或者私有的,则用户代码不能使用该转换
  • 不论D以什么方式继承B,D的成员函数和友元都能使用派生类向基类的转换;派生类向其直接基类的类型转换对于派生类的成员和友元来说永远是可访问的。
  • 如果D继承B的方式是公有的或者受保护的,则D的派生类的成员和友元可以使用D向B的类型转换,反之,如果D继承B是私有的,则不能使用。

友元与继承

就像友元关系不能传递一样,友元关系同样也不能继承。基类的友元在访问派生类成员时不具有特殊性,类似的,派生类的友元也不能随意访问基类的成员。

对于f2函数,Pal是Base的友元,Pal能够访问Base对象的成员,这种可访问性包括了Base对象内嵌在其派生类对象中的情况。
当一个类将另一个类声明为友元时,这种友元关系只对做出声明的类有效。对于原来那个类来说,其友元的基类或者派生类不具有特殊的访问能力。

class Base {public:friend class Pal;static void statmem(){}
private:void BaseFun() {cout << "这是Base的函数BaseFun()"<< endl;}};class Sneaky : public Base{private:void SneakFun(){cout << "这是Sneak的函数SneakFun()" << endl;}
};class Pal {public:void f1(Base b) { b.BaseFun(); }void f2(Sneaky s) { s.BaseFun(); }  //void f3(Sneaky s) { s.SneakFun(); }  错误,Pal不是Sneaky的友元不能访问SneakFun()
};class D2 : public Pal{public://void f(Base b){b.BaseFun(); }  错误,友元关系不能继承
};

改变各个成员的可访问性

有时我们需要改变派生类继承的某个名字的访问级别,通过使用using声明可以达到这一目的。

class Base3 {public:size_t size()const { return n; }
protected:size_t n;
};
class Derived : private Base3 {public:using Base3::size;
protected:using Base3::n;
};

因为Derived是私有继承,所以继承而来的成员size和n是Derived的私有成员,然而我们使用using声明语句,将size变成了public成员,Derived变成了protected成员,改变了这些成员的可访问性。

默认的继承保护级别

class Base{  };
struct D1 : Base{  };  //默认public继承
class D2 : Base{  };    //默认private继承

继承中的类作用域

每个类定义自己的作用域,在这个作用域内我们定义类的成员。当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内,如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义。

恰恰因为类作用域有这种继承嵌套的关系,所以派生类才能像使用自己的成员一样使用基类的成员。

在编译时进行名字查找

一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。即使静态类型与动态类型可能不一致(当使用基类的引用或指针时会发生这种情况),但是我们能使用哪些成员仍然是由静态类型决定的。

示例:

class D_Quote : public Quote{public:void fun(){}
}D_Quote tmp;
D_Quote *dq = &tmp;
Quote *q = &tmp;dq->fun();  //正确,dq的类型是D_Quote*
q->fun();   //错误,q的类型是Quote*,没有fun()函数

名字冲突与继承

和其他作用域一样,派生类也能重用定义在其直接基类或间接基类中的名字,此时定义在内层作用域(即派生类)的名字将隐藏定义在外层作用域(即基类)的名字。

派生类的成员将隐藏同名的基类成员。

除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。

名字查找先于类型检查

如果派生类的成员与基类的某个成员同名,则派生类将在其作用域内隐藏该基类成员。即使派生类成员和基类成员的形参列表不一致,基类成员也仍然会被隐藏掉。

示例:

class Base{public:void fun();
};class Derived : public Base{public:void fun(int);  //即使派生类成员和基类成员的形参列表不一致,基类成员fun()也仍然会被隐藏掉
};Derived d;
Base b;
d.fun(10);
d.fun();//错误,fun()被隐藏
d.Base::fun(); //正确,调用Base::fun()

虚函数与作用域,通过基类调用隐藏的虚函数

class BaseHide {public:virtual void fcn() { cout << "BaseHide virtual fcn()" << endl; }//虚函数
};class DerivedHide1 : public BaseHide {public:void fcn(int) { cout << "DerivedHide1 fcn(int)" << endl; }  //隐藏BaseHide的fcn()virtual void f2() { cout << "DerivedHide1 virtual f2( )" << endl; }
};class DerivedHide2 : public DerivedHide1 {public:void fcn(int) { cout << "DerivedHide2 fcn(int)" << endl; }void fcn() { cout << "DerivedHide2 fcn()" << endl; }void f2() { cout << "DerivedHide2 f2( )" << endl; }
};

测试代码:

 BaseHide bh;DerivedHide1 dh1;DerivedHide2 dh2;BaseHide *bp1 = &bh, *bp2 = &dh1, *bp3 = &dh2;bp1->fcn();   //虚调用,BaseHide::fcn()bp2->fcn();   //虚调用,BaseHide::fcn()bp3->fcn();  //虚调用,DerivedHide2::fcn()cout << endl;DerivedHide1 *dp1 = &dh1;DerivedHide2 *dp2 = &dh2;//bp2->f2();    错误,BaseHide没有名为f2的成员dp1->f2();    //虚调用,DerivedHide1::f2()dp2->f2();    //虚调用,DerivedHide2::f2()cout << endl;BaseHide *p1= &dh2;DerivedHide1 *p2 = &dh2;DerivedHide2 *p3 = &dh2;//p1->fcn(42);   错误,BaseHide没有fcn(int)p2->fcn(42);   //静态绑定,DerivedHide1::fcn(int)p3->fcn(42);   //静态绑定,DerivedHide2::fcn(int)

结果:

BaseHide virtual fcn()
BaseHide virtual fcn()
DerivedHide2 virtual fcn()DerivedHide1 virtual f2( )
DerivedHide2 virtual f2( )DerivedHide1 fcn(int)
DerivedHide2 fcn(int)

DerivedHide1的fcn函数并没有覆盖BaseHide的虚函数fcn,原因是参数列表不同,将隐藏BaseHide的fcn。
dh1不能调用fcn(),因为被隐藏了,但bp2指针能调用fcn(),因为fcn是虚函数,bp2实际绑定的对象是DerivedHide1类型,而DerivedHide1并没有覆盖不接受实参的fcn(),所以通过bp2进行的调用将在运行时解析为BaseHide定义的版本。

bp2指向派生类对象DerivedHide1,但由于Base类中没有f2(),所以bp2不能调用f2()。

同理,p1不能调用fcn(int),但p2可以调用fcn(int),因为DerivedHide1中有fcn(int),由于fcn(int)是非虚函数,所以不会发生动态绑定,实际调用的函数版本由指针的静态类型决定。

构造函数与拷贝控制

和其他类一样,位于继承体系中的类也需要控制当其对象执行一系列操作时发生什么样的行为,这些操作包括创建、拷贝、移动、赋值和销毁。如果一个类(基类或派生类)没有定义拷贝控制操作,则编译器将为它合成一个版本。

虚析构函数

基类通常应该定义一个虚析构函数,这样我们就能动态分配继承体系中的对象了。

如果我们delete一个Base * 类型的指针,则该指针有可能实际上指向了一个Derived类型的对象, 因此编译器必须清楚它应该执行的的Derived类型的析构函数。和其他函数一样,我们通过在基类中将析构函数定义成虚函数以确保执行正确的析构函数版本。

如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为。

之前我们曾介绍过一条经验准则, 即如果一个类需要析构函数, 那么它也同样需要 贝和赋值操作。 基类的析构函数并不遵循上述准则, 它是一个重要的例外。 一个基类总是需要析构函数, 而且它能将析构函数设定为虚函数。 此时,该析构函数为了成为虚函数而令内容为空,我们显然无法由此推断该基类还需要赋值运算符或拷贝构造函数。

虚析构函数将阻止合成移动操作

基类需要一个虚析构函数这一事实还会对基类和派生类的定义产生另外一个间接的影响:如果一个类定义了析构函数,即使它通过 =default 的形式使用了合成的版本,编译器也不会为这个类合成移动操作。

合成拷贝控制与继承

派生类中删除的拷贝控制与基类的关系型

  • 如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数是被删除的函数或者不可访问,则派生类中对应的成员将是被删除的,原因是编译器不能使用基类成员来执行派生类对象基类部分的构造、赋值或销毁操作。
  • 如果在基类中有一个不可访问或删除掉的析构函数,则派生类中合成的默认和拷贝构造函数将是被删除的,因为编译器无法销毁派生类对象的基类部分。
  • 和过一样,编译器将不会合成一个删除掉的移动操作。 当我们使用=default请求一个移动操作时,如果基类中的对应操作是删除的或不可访问的, 那么派生类中该函数将是被删除的, 原因 是派生类对象的基类部分不可移动。 同样, 如果基类的析构函数是删除的或不可访问的, 则派生类的移动构造函数也将是被删除的。

派生类的拷贝控制成员

派生类构造函数在其初始化阶段中不但要初始化派生类自己的成员,还负责初始化派生类对象的基类部分。因此,派生类的拷贝和移动构造函数在拷贝和移动自有成员的同时,也要拷贝和移动基类部分的成员。类似的,派生类赋值运算符也必须为其基类部分的成员赋值。

和构造函数及赋值运算符不同的是,析构函数只负责销毁派生类自己分配的资源。对象的成员是被隐式销毁的;类似的,派生类对象的基类部分也是自动销毁的。

对象销毁的顺序正好与其创建的顺序相反:派生类析构函数首先执行,然后是基类的析构函数,以此类推, 沿着继承体系的反方向直至最后。

当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象。

定义派生类的拷贝或移动构造函数

在默认情况下,基类默认构造函数初始化派生类对象的基类部分。如果我们想拷贝(或移动)基类部分,则必须在派生类的构造函数初始值列表中显式地使用基类的拷贝(或移动)构造函数。派生类的赋值运算符也必须显式地为其基类部分赋值。

派生类赋值运算符

与拷贝和移动构造函数一样,派生类的赋值运算符也必须显式地为其基类部分赋值。

// Base::operator=(const Base&);  不会被自动调用D &D::operator=(const D &rhs){Base::operator=(rhs); //为其基类部分赋值//按照过去的方式为派生类的成员赋值//酌情处理自赋值及释放已有资源等情况return *this;
}

派生类析构函数

如前所述,在析构函数体执行完成后,对象的成员会被隐式销毁。类似的,对象的基类部分也是隐式销毁的。因此,和构造函数及赋值运算符不同的是,派生类析构函数只负责销毁由派生类自己分配的资源。

对象销毁的顺序正好与其创建的顺序相反:派生类析构函数首先执行,然后是基类的析构函数,以此类推,沿着继承体系的反方向直至最后。

在构造函数和析构函数中调用虚函数

如果构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或析构函数所属类型的虚函数版本。

测试类

class TestBase {public:TestBase() { cout << "这是TestBase的构造函数" << endl;fun();cout << endl; }virtual void fun() { cout << "这是TestBase的虚函数" << endl; }~TestBase() { cout << "这是TestBase的析构函数" << endl; fun();cout << endl;}};class TestDerived:public TestBase {public:TestDerived() { cout << "这是TestDerived的构造函数" << endl;fun();cout << endl;}virtual void fun() { cout << "这是TestDerived的虚函数" << endl; }~TestDerived() { cout << "这是TestDerived的析构函数" << endl;fun();cout << endl;}
};

测试函数:

void testTestBase() {TestBase tb1;TestDerived td1;
}

以上代码,先创建一个TestBase的类,故先执行TestBase的构造函数,然后创建一个TestDerived的类,由于其继承了TestBase,故先执行TestBase的构造函数,然后执行TestDerived的构造函数,最后当testTestBase()函数执行完成后,将td1进行析构,先执行TestDerived的析构函数,然后执行TestBase的析构函数,然后将tb1执行析构,即执行TestBase的析构函数。在构造和析构函数调用的虚函数,其执行与构造函数或析构函数所属类型的虚函数版本。

输出结果:

这是TestBase的构造函数
这是TestBase的虚函数这是TestBase的构造函数
这是TestBase的虚函数这是TestDerived的构造函数
这是TestDerived的虚函数这是TestDerived的析构函数
这是TestDerived的虚函数这是TestBase的析构函数
这是TestBase的虚函数这是TestBase的析构函数
这是TestBase的虚函数

测试函数二:

void testTestBase() {TestDerived td1;TestBase *tb1 = new TestDerived();cout << "调用fun函数:" << endl;tb1->fun();  //此处调用的是派生类的fun函数cout << endl;
}

输出结果:

这是TestBase的构造函数
这是TestBase的虚函数这是TestDerived的构造函数
这是TestDerived的虚函数这是TestBase的构造函数
这是TestBase的虚函数这是TestDerived的构造函数
这是TestDerived的虚函数调用fun函数:
这是TestDerived的虚函数这是TestDerived的析构函数
这是TestDerived的虚函数这是TestBase的析构函数
这是TestBase的虚函数

继承的构造函数

一个类只初始化它的直接基类,出于同样的原因,一个类也只继承其直接基类的构造函数。

类不能继承默认、拷贝和移动构造函数。如果派生类没有直接定义这些构造函数,则编译器将为派生类合成它们。

派生类继承基类构造函数的方式是提供一条注明了(直接)基类名的using声明语句。通常情况下,using声明语句只是令某个名字在当前作用域内可见,而当作用于构造函数时,using声明语句将令编译器产生代码。对于基类的每个构造函数,编译器都生成一个与之对应的派生类构造函数。换句话说,对于基类的每个构造函数,编译器都在派生类中生成一个形参列表完全相同的构造函数。

继承的构造函数的特点

和普通成员的using声明不一样,一个构造函数的using声明不会改变该构造函数的访问级别。例如,不管using声明出现在哪,基类的私有构造函数在派生类中还是一个私有构造函数,受保护的构造函数和公有构造函数也是同样的规则。

当一个基类构造函数含有默认实参时,这些实参并不会被继承。相反,派生类将获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认实参的形参。

如果基类有多个构造函数,则除了两个例外情况,大多数时候派生类会继承所有这些构造函数。第一个例外情况是,如果派生类定义的构造函数于基类的构造函数具有相同的参数列表,则该构造函数将不会被继承。定义在派生类中的构造函数将替换继承而来的构造函数。

第二个例外是默认、拷贝和移动构造函数不会被继承。

测试代码:

class TestBase {public:TestBase(){ }TestBase(int i):id(i) { }void getId() { cout <<"id:"<<id << endl; }private:int id;};class TestDerived:public TestBase {public:using TestBase::TestBase;};//测试函数:
void testTestBase() {TestDerived td1(5);td1.getId();
}

输出:

id:5

若将using TestBase::TestBase;注释掉,则TestDerived td1(5);会报错。

容器与继承

当派生类对象被赋值给基类对象时,其中的派生类部分将被“切掉”,因此,当我们使用容器存放继承体系中的对象时,通常必须采取间接存储的方式。

在容器中放置(智能)指针而非对象

当我们希望在容器中存放具有继承关系的对象时,我们实际上存放的通常是基类的指针。和往常一样,这些指针所指对象的动态类型可能是基类类型,也可能是派生类类型。

测试代码:

class TestBase {public:TestBase(int i):id(i) { }virtual void getId() { cout <<"id:"<<id << endl; }private:int id;
};class TestDerived:public TestBase {public:TestDerived(int i,int j) :TestBase(i),num(j) { }virtual void getId() { TestBase::getId();cout << "num:" << num << endl; }
private:int num;};
//测试函数
void testTestBase() {TestBase tb1(1);TestDerived td1(2, 3);vector<shared_ptr<TestBase>>vec;vec.push_back(make_shared<TestBase>(tb1));vec.push_back(make_shared<TestDerived>(td1));for (auto v:vec) {v->getId();cout << "======" << endl;}
}

输出结果:

id:1
======
id:2
num:3
======

编写Basket类

对于c++面向对象的编程来说,我们必须使用指针和引用来进行面向对象编程。因为指针会增加程序的复杂性,所以我们经常定义一些辅助的类来处理这种复杂情况。

定义一个表示购物篮的类:

decltype的意义

有时我们希望从表达式的类型推断出要定义的变量类型,但是不想用该表达式的值初始化变量(初始化可以用auto)。为了满足这一需求,C++11新标准引入了decltype类型说明符,它的作用是选择并返回操作数的数据类型,在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。

upper_bound可参考chapter 11

Basket类:

class Basket {public:// Basket使用合成的默认构造函数和拷贝控制成员void add_item(const shared_ptr<Quote>&sale){items.insert(sale);}//打印每本书的总价和购物篮中所有书的总价double total_receipt(ostream&)const;
private://该函数用于比较shared_ptr,multiset成员会用它static bool compare(const shared_ptr<Quote>&lhs, const shared_ptr<Quote>&rhs) {return lhs->isbn() < rhs->isbn();}//multiset保存多个报价,按照compare成员排序multiset<shared_ptr<Quote>, decltype(compare)*>items{compare};
};double Basket::total_receipt(ostream&os)const {double sum = 0;for (auto iter = items.cbegin();iter != items.cend();iter=items.upper_bound(*iter)) {sum += print_total(os,**iter,items.count(*iter));}os << "Total Sale: " << sum << endl;return sum;
}

Quote类和QuoteDerived类:

class Quote {public:Quote() = default;Quote(const string &book,double sales_price) :bookNo(book),price(sales_price){}string isbn()const { return bookNo; }//返回给定数量的书籍的销售总额//派生类负责改写并使用不同的折扣计算算法virtual double net_price(size_t n)const { return n*price; }virtual ~Quote() = default;//对析构函数进行动态绑定virtual void debug() {cout << "Quote类的bookNo:" << bookNo << ",price: " << price << endl;}virtual ostream & print(ostream &os) {os << "bookNo: " << bookNo << " price: " << price;return os;}private:string bookNo;  //书籍的ISBN编号
protected:double price = 0.0; //代表普通状态下不打折的价格
};double print_total(ostream &os, Quote &qt,int num) {qt.print(os);os << " number: " << num << endl;return qt.net_price(num);
}class QuoteDerived :public Quote {public:QuoteDerived() = default;QuoteDerived(const string &book, double sales_price,double d):Quote(book, sales_price),discount(d){}virtual double net_price(size_t cnt) const {return cnt*price*discount;}private:double discount;
};

测试函数:

void testBasket(){Basket bsk;bsk.add_item(make_shared<Quote>("123",60));bsk.add_item(make_shared<QuoteDerived>("123", 60,0.5));bsk.add_item(make_shared<QuoteDerived>("345", 100, 0.5));bsk.add_item(make_shared<Quote>("345", 100));bsk.total_receipt(cout);
}

输出结果:

bookNo: 123 price: 60 number: 2
bookNo: 345 price: 100 number: 2
Total Sale: 220

若测试函数改为:

void testBasket(){Basket bsk;bsk.add_item(make_shared<Quote>("123",60));bsk.add_item(make_shared<QuoteDerived>("123", 60,0.5));bsk.add_item(make_shared<Quote>("345", 100));bsk.add_item(make_shared<QuoteDerived>("345", 100, 0.5));bsk.total_receipt(cout);
}

则输出结果为:

bookNo: 123 price: 60 number: 2
bookNo: 345 price: 100 number: 2
Total Sale: 320

显然,结果不是我们预期的结果,我们想要的结果应该是,60+600.5+100+1000.5=240。该程序的结果是由于其iter=items.upper_bound(*iter) 导致的,因为它将所有isbn相同的归为一类,且,按照该类别的第一个进行统一计算价钱。

模拟虚拷贝

在Quote中添加虚函数:

//该虚函数返回当前对象的一份动态分配的拷贝virtual Quote*clone()const & { return new Quote(*this); }virtual Quote*clone()const && { return new Quote(move(*this)); }

在QuoteDerived中添加虚函数:

virtual QuoteDerived*clone()const & { return new QuoteDerived(*this); }virtual QuoteDerived*clone()const && { return new QuoteDerived(move(*this)); }

改写Basket类的add_item:

void add_item(const Quote &sale) {items.insert(shared_ptr<Quote>(sale.clone()));}void add_item(const Quote &&sale) {items.insert(shared_ptr<Quote>(move(sale).clone()));}

此时的测试函数为:

void testBasket(){Basket bsk;bsk.add_item(Quote("123",60) );bsk.add_item(QuoteDerived("123", 60,0.5));bsk.add_item(Quote("345", 100));bsk.add_item(QuoteDerived("345", 100, 0.5));bsk.total_receipt(cout);
}

C++ primer 第15章 面向对象程序设计相关推荐

  1. 第十五章 面向对象程序设计

    第十五章 面向对象程序设计 OOP:概述 面向对象程序设计(object-oriented programming)的核心思想是数据抽象.继承和动态绑定. 继承(inheritance): 通过继承联 ...

  2. java笔记:第6章 面向对象程序设计

    第6章 面向对象程序设计 6.1面向对象技术基础 面向对象三大特征:封装性, 继承性,多态性 封装性:1. 把属性和方法都放在一起  2.实现信息隐藏 类和对象的关系:类是对象的抽象描述.对象是类的实 ...

  3. C++Primer 第15章 OOP

    C++Primer 第15章 OOP #include<iostream> class Base {public:virtual void fun1(int i = 0){std::cou ...

  4. c++primer(第五版) 第十五章 面向对象程序设计习题答案

    纯原创    转载请注明出处:http://blog.csdn.net/axuan_k 略过书上有现成代码的题目 15.1  15.2 15.1 虚成员是基类中的概念 基类中的某些函数希望它的派生类能 ...

  5. 【C++ Primer 第15章】定义派生类拷贝构造函数、赋值运算符

    学习资料 • 派生类的赋值运算符/赋值构造函数也必须处理它的基类成员的赋值 • C++ 基类构造函数带参数的继承方式及派生类的初始化 定义拷贝构造函数 [注意]对派生类进行拷贝构造时,如果想让基类的成 ...

  6. 【C++ Priemr | 15】面向对象程序设计

    类型准换与继承 为了支持c++的多态性,才用了动态绑定和静态绑定. 需要理解四个名词: 对象的静态类型:对象在声明时采用的类型,是在编译期确定的. 对象的动态类型:目前所指对象的类型,是在运行期决定的 ...

  7. 《C++ Primer》第15章 15.4节习题答案

    <C++ Primer>第15章 面向对象程序设计 15.4节 抽象基类 习题答案 练习15.15:定义你自己的Disc_quote和Bulk_quote. [出题思路]本题练习实现不同折 ...

  8. 《C++ Primer》第15章 15.2节习题答案

    <C++ Primer>第15章 面向对象程序设计 本章介绍了面向对象程序设计的两个重要概念:继承和动态绑定,包括: □●继承.基类.派生类的基本概念. □●虚函数和虚基类. □●继承中的 ...

  9. 《C++Primer》14、15章

    第14章 重载运算符与类型转换 14.1 基本概念 只有当操作的含义对于用户来说清晰明了时才使用运算符. 选择作为成员还是非成员? 赋值.下标.调用和成员访问运算符必须是成员. 复合赋值运算符一般是成 ...

最新文章

  1. Module ‘celery‘ has no attribute ‘celery‘问题
  2. 2015 年最受 Linux 爱好者欢迎的软硬件大盘点
  3. javascript面向对象属性函数用法(defineProperty与getOwnPropertyDescriptor)
  4. lua windows下编译
  5. 输入一个正整数求所有素数因子_一起来聊聊素数的两个性质
  6. 花花酱leetcode 题目——搜索专题
  7. nodejs mysql access denied_Node使用Sequlize连接Mysql报错:Access denied for user ‘xxx’@‘localhost’...
  8. View绑定式监听器实现原理
  9. 圆网印花色浆未干引起的印花疵病
  10. Q126:PBRT-V3,VolPathIntegrator(体渲染)流程概述
  11. 正则表达式-找出电话号码
  12. Delphi可视化精讲视频教程-丁鹏-专题视频课程
  13. 计算机网络第七版课后答案(谢希仁版)
  14. 数据抽取的常见理论方法
  15. HTML+CSS(part 1)
  16. Kubeenetes Dashboard admin-kubeconfig
  17. 安徽软件技术职业适应性测试,2019年职业适应性(职业技能)测试纲要
  18. vue 过滤器做字数限制并显示省略号
  19. c语言一元多项式相加
  20. 第五篇:mig读写时序下板实现

热门文章

  1. 小学有学计算机课程,如何进行小学计算机课程有效教学.doc
  2. 【转】gcc/g++ 链接库的编译与链接
  3. 【转】刨根究底字符编码之十五——UTF-32编码方式
  4. 【转】WCF、WebAPI、WCFREST、WebService之间的区别
  5. 使用WebService如何实现分布式事务
  6. 静态html js文件上传,js实现动态添加上传文件页面
  7. 设计模式(六)J2EE 模式
  8. Python 中如何解决 asyncio 文件描述符最大数量限制问题
  9. start()和run()的区别
  10. git 连接gitee时报错 Auth error: Access deined: authorize failure