15 面向对象程序设计

目录

0. 重点总结

  • 对于c++面向对象的编程来说,我们必须使用指针和引用来进行面向对象编程,并不是直接用对象
  • 类的5大操作:创建、拷贝、移动、赋值和销毁
  • 面向对象程序设计的核心思想 都是以类为基础的。
  • 接口、实现 和 封装
    • 接口 就是指 用户所能执行的操作
    • 实现 包括 接口的定义、类的成员以及类所需的其他私有函数声明和定义。
    • 封装实现了 接口实现 的分离,它隐藏了实现的细节,类的用户只能使用接口,而不能访问其实现细节
  • 数据抽象、继承、动态绑定(即多态)
    • 数据抽象 实现了 接口 和 实现 的分离。
    • 继承 可以定义相似的类,利用它们之间相似关系建模。
    • 动态绑定 也称作 多态,它可以暂时忽略相似类型的区别,用相同的方式来使用它们。
  • 在C++中,只有当我们使用基类的引用或指针调用一个虚函数时才会发生动态绑定
  • 虚函数在基类中用virtual关键字说明——代表该成员函数时基类允许其派生类修改的;
  • 在派生类中与基类虚函数同名的成员函数前面可以用virtual,也可以不用,不用就是隐式说明这是虚函数,还可以使用override说明该虚函数是派生类改写的父类的虚函数
    • 因为该派生类也可以作为其他类的父类,所以如果派生类中的虚函数是改写了其父类的虚函数,最好还是用override关键字说明一下
  • 因为派生类继承了基类,所以用到基类的地方都可以用派生类代替
  • 存在派生类向基类的类型转换,但不存在基类向派生类的类型转换(基类指针或引用可以绑定到派生类对象,但派生类指针或引用不能绑定到基类对象
  • OOP的核心思想是多态性引用或指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本所在。
  • 虚函数的作用是实现多态性
  • 派生类中的虚函数:
    • 派生类可以覆盖其基类的虚函数,也可以不覆盖,如果不覆盖则直接继承其在基类中的版本。
    • 派生类可以在它覆盖的函数前使用virtual关键字,也可以不用
    • c++11 还允许使用override关键字,让编译器检查这个函数是不是覆盖基类的虚函数。
    • override出现在形参列表后面,或者在const成员函数的const关键字后面,或者在引用成员函数的引用限定符后面
    • 总结:如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数,或者在派生类中用virtual关键字显式声明这是虚函数,还可以用override关键字说明这个派生类的虚函数是改写了其基类的虚函数的,毕竟派生类也可以是其他类的基类
  • 总结:虚函数在基类中无论是否使用都必须提供定义。使用基类的引用或指针调用一个虚成员函数时会执行动态绑定。派生类可以覆盖其基类的虚函数,也可以不覆盖。如果不覆盖则直接继承其在基类中的版本(不覆盖就是不重新定义,直接使用基类定义的虚函数版本,此时该虚函数作为派生类的隐藏成员)
  • 纯虚函数:虚函数在基类中无论是否使用都必须提供定义,但在许多情况下,在基类中不能对虚函数给出有意义的实现。为了让虚函数在基类什么也不做,引进了“纯虚函数”的概念纯虚函数无需定义
    • 在类内的虚函数声明语句分号前书写=0可定义纯虚函数,表示该函数无实际意义。
    • 纯虚函数一般不用定义,若要定义则只能在类外定义函数体
  • 派生类中的成员和友元如果通过对象来访问基类的受保护成员,那么这个对象必须是派生类对象,如果这个对象是基类对象,那么是不能直接访问的这个对象的受保护成员的——因为基类的受保护成员被派生类继承下来了,那么该基类受保护成员就已经成为了派生类的一部分受保护成员,所以派生类的成员和友元才能访问。实际上在派生类继承下来的受保护成员已经和基类的受保护成员已经是独立的个体了,所属类都不同了。
  • 类列表中的访问说明符只影响派生类用户对于基类成员的访问权限,而不影响派生类成员或友元对基类成员的访问权限。
    • 派生类用户和派生类的派生类(继承自派生类的新类)对该基类成员的访问权限受基类中的访问说明符和类列表中的访问说明符的影响;
    • 而派生类的成员或友元对基类成员的访问权限只受基类中的访问说明符的影响
  • 调用的是虚函数才会发生动态绑定,此时基类的引用和指针实际调用的函数版本由动态类型决定;调用的是非虚函数,不会发生动态绑定,实际调用的函数版本由指针的静态类型决定
  • 若派生类成员与基类成员同名,则派生类成员会隐藏基类成员。虽然可通过作用域运算符::来使用隐藏成员,但最好不要在派生类中重用除虚函数之外的定义在基类中的名字。
  • 总结:
      1. 虚函数必须实现,不实现编译器会报错。
      1. 父类和子类都有各自的虚函数版本。由多态方式在运行时动态绑定。
      1. 通过作用域运算符可以强行调用指定的虚函数版本。
      1. 纯虚函数声明如下:virtual void funtion()=0; 纯虚函数无需定义。包含纯虚函数的类是抽象基类,抽象基类不能创建对象,但可以声明指向抽象基类的指针或引用。
      1. 派生类实现了纯虚函数以后,该纯虚函数在派生类中就变成了虚函数,其子类可以再对该函数进行覆盖。
      1. 析构函数通常应该是虚函数,这样就能确保在析构时调用正确的析构函数版本。
  • 基类中有三种成员函数:直接继承函数(让派生类直接继承的公有成员)、虚函数(派生类可以修改的类型相关成员)、不能继承final函数
  • 派生类中的成员函数:直接继承基类的公有成员,修改基类的类型相关成员(虚函数)、自己特有的成员

1. OOP:概述

  • 面向对象程序设计基于三个基本概念:数据抽象、继承和动态绑定。我们在第七章已经介绍了数据抽象的知识,就是那个类,这一章我们要学习剩下的两个:继承和动态绑定。
  • 有的人一开始会觉得有了类不就可以面向对象了吗?只要把实际中的对象抽象成类,好好设计一下,定义好构造函数,拷贝构造函数,重载一些运算,提供一些好的函数工具,注意类类型转换即可。事实证明这种想法是天真的,有了继承和动态绑定,我们在编写面向对象的时候会更加强大。
  • 具体来说,继承和动态绑定对程序的编写有两方面的影响:
    • 一是我们可以更容易地定义与其他类相似但不完全相同的新类
    • 二是在使用这些彼此相似的类编写程序时,我们可以在一定程度上忽略它们的差别。
  • 我们之前学习的类的知识基本是单个类的,现在我们要学如何在已有一个类的基础上方便地编写一个相似的类,并且我们如何使用它们。
  • 面向对象程序设计OOP的核心思想是数据抽象、继承和动态绑定
    • 通过使用数据抽象,我们可以将类的接口与实现分离;
    • 使用继承,可以定义相似的类型并对其相似关系建模;
    • 使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。

继承:父类-基类;子类-派生类。

  • 基类定义派生类们的通用属性和自己的特殊属性,派生类继承基类的通用属性后再定义自己的特殊属性
  • 通过继承联系在一起的类构成一种层次关系。通常在层次关系的根部有一个基类,其他类则直接或间接地从基类继承而来,这些继承得到的类为派生类。
  • 继承时分基类和派生类:
    • 基类负责定义在层次关系中所有类共同拥有的成员
    • 每个派生类定义各自特有的成员。
  • 在C++语言中,基类将类型相关的函数与派生类不做改变直接继承的函数区分对待。对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数(virtual function)。
    • 虚函数可以理解成派生类可以修改的基类的成员函数
  • 基类:在C++语言中,基类会将两种函数区分对待
    • 类型相关的函数(net_price)——虚函数,基类与派生类不同
    • 派生类不做改变直接继承的函数(isbn)
// 基类
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;
    //override表明这个函数是改写基类的虚函数的
};
// 因为Bulk_quote在它的派生列表中使用了public关键字,因此我们完全可以把Bulk_quote的对象当成Quote的对象来使用。

动态绑定:以统一方式使用基类和派生类。只有当我们使用基类的引用或指针调用一个虚函数时才会发生动态绑定*

  • 动态绑定:以统一方式使用基类和派生类。又称运行时绑定,即在运行时选择函数版本(属于基类还是派生类)。
    • 在C++中,只有当我们使用基类的引用或指针调用一个虚函数时才会发生动态绑定
// 通过使用动态绑定,我们能用同一段代码分别处理Quote和Bulk_quote的对象。
double print_total(ostream &os,const Quote &item,size_t n){
    //根据item的形参对象类型调用Quote::net_price或者Bulk_quote::net_price
    double ret = item.net_price(n);
    os<<"ISBN: "<<item.isbn()<<"# sold: "<< n <<"total due: "<<ret<<endl;
    return ret;
}

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

2. 定义基类和派生类

2.1 定义基类

  • 基类一般组成:
    • 虚析构函数:因为派生类可能需要释放其他内存或进行其他如输出信息等操作
    • 通用函数:派生类直接继承
      • 基类希望派生类直接继承而不要改变的函数
    • 虚函数:类型相关函数,派生类可以选择自定义自己的版本
      • 基类希望其派生类进行覆盖的函数
      • 当我们使用指针或引用来调用虚函数时,该调用将被动态绑定。根据引用或指针所绑定的对象类型不同,该调用可能执行基类的版本,也可能执行某个派生类的版本
  • 示例
class Quote
{
public:
    Quote() = default; //强行要求合成默认构造函数
    Quote(const string &book, double sale_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;
    //对析构函数进行动态绑定(派生类可能需要释放其他内存或其他操作),
    //后面会详细介绍,基类应该都要定义一个虚析构函数

private:
    string bookNo;

protected: //让派生类访问,但是不让其他用户访问
    double price = 0; 
}
  • 基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此.
    • 因为派生类可能需要释放其他内存或进行其他如输出信息等操作
  • 成员函数与继承:基类的成员函数分成虚函数和通用函数两种
    • 在c++语言中,基类必须将它的两种成员函数区分开来(基类将成员函数分为两种):
      • 一种是**基类希望其派生类进行覆盖的函数:虚函数,为各自的特殊属性
      • 另一种是基类希望派生类直接继承而不要改变的函数,为通用属性
      • 对于前者,基类通常将其定义为虚函数,当我们使用指针或引用来调用虚函数时,该调用将被动态绑定。根据引用或指针所绑定的对象类型不同,该调用可能执行基类的版本,也可能执行某个派生类的版本
    • 基类通过在其成员函数的声明语句之前加上关键字virtual将该函数声明成虚函数,使得该函数可以执行动态绑定。
    • 任何构造函数之外的非静态函数都可以是虚函数。
    • 关键字 virtual只能出现在类内部的函数声明语句之前而不能用于类外部的函数定义
    • 如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数(或者在派生类中用virtual关键字显式声明这是虚函数,还可以用override关键字说明这个派生类的虚函数是改写基类的虚函数的)。
    • 成员函数如果没被声明为虚函数,则其解析过程发生在编译时而非运行时。调用的则是当前类型对应的成员函数
  • 访问控制与继承:在基类中定义protected受保护成员,使得派生类可以访问但是其他用户不能访问
    • 派生类可以继承定义在基类中的成员,但是派生类的成员函数不一定有权访问从基类继承而来的成员。
    • 派生类能访问基类的公有成员,而不能访问其私有成员
    • 不过在某些时候基类中还有这样一种成员,基类希望它的派生类有权访问该成员同时禁止其他用户访问。我们用受保护的(protected)访问运算符说明这样的成员。(而private私有成员无论是派生类还是其他用户都不能直接访问)

2.2 定义派生类:class son_T:类派生列表{...};

  • 定义派生类
class Bulk_quote:public Quote{
public:
    Bulk_quote() = default;

    Bulk_quote(const std::string&,double,std::size_t,double);

    //覆盖基类的函数版本以实现基于大量购买的折扣政策

    double net_price(std::size_t) const override;
private:
    std::size_t min_qty = 0;
    double discount = 0.0;

};
  • 派生类必须通过使用类派生列表明确指出它是从哪个(哪些)基类继承而来的。
  • 类派生列表的形式是:
    • 首先是一个冒号,后面紧跟以逗号分隔的基类列表,其中每个基类前面可以有以下三种访问说明符中的一个:public、protected或者private。
    • 这里的访问说明符的作用是控制派生类从基类继承而来的成员是否对派生类的用户可见
  • 如果一个派生类是公有的,则基类的公有成员也是派生类接口的组成部分。
  • **我们能将公有派生类型的对象绑定到基类的引用或指针上:即可以发生动态绑定。
    • 因为我们在派生列表中使用了public,所以派生类Bulk_quote的接口隐式地包含基类的isbn函数,同时在任何需要基类Quote的引用或指针的地方我们都能使用Bulk_quote的对象

派生类中的虚函数:可覆盖其基类的虚函数,也可以不覆盖

  • 派生类经常(但不总是)覆盖它继承的虚函数。如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本。
    • 派生类可以覆盖其基类的虚函数,也可以不覆盖,如果不覆盖则直接继承其在基类中的版本
  • 派生类可以在它覆盖的函数前使用virtual关键字表明这是虚函数,也可以不用。C++11 还允许使用override关键字,让编译器检查这个函数是不是覆盖基类的虚函数
  • override出现在形参列表后面,或者在const成员函数的const关键字后面,或者在引用成员函数的引用限定符后面
  • 总结:**如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数,或者在派生类中用virtual关键字显式声明这是虚函数,还可以用override关键字说明这个派生类的虚函数是改写了其基类的虚函数的(毕竟派生类也可以是其他类的基类)

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

  • 派生类对象及派生类向基类的类型转换:总结就是,因为派生类继承了基类,所以用到基类的地方都可以用派生类代替
  • 一个派生类对象,含有两部分:
    • 一部分是派生类自己定义的非静态成员的子对象
    • 一部分是从基类继承来的子对象。如果有多个基类,那么这样的子对象也有多个
    • 总的来说,派生类包含派生类自定义的子对象和基类子对象
  • 因为在派生类对象中含有基类的子对象,所以我们能把派生类对象当成基类对象来使用(注意使用的是基类部分),而且我们也能将基类的指针或引用绑定到派生类对象,准确来说是派生类对象中的基类部分
// 派生类到基类的类型转换(编译器会隐式执行)
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) { }
//首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。

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

  • 派生类构造函数只初始化它的直接基类
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;
};
  • 派生类使用基类的成员:派生类可以访问基类的公有成员和受保护成员,但无权访问基类的私有成员
double Bulk_quote::net_price(size_t cnt) const{
    if(cnt >= min_qty)
        return cnt*(1-discount)*price;
    else
        return cnt* price;
}
  • 派生类的作用域嵌套在基类的作用域之内。因此,对于派生类的一个成员来说,他使用派生类成员的方式与使用基类成员的方式一样

继承与静态成员

  • 如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。
  • 不论从基类中派生出来多少个派生类,对于每个静态成员来说都只存在唯一的实例。
class Base{
public:
    static void statement();
};

class Derived:public Base{
    void f(const Derived&);
};
  • 静态成员遵循通用的访问控制规则,如果基类中的成员是private的,则派生类无权访问它。
  • 假设某静态成员是可访问的,则我们既能通过基类使用它也能通过派生类使用它。
class Base {
public:
    qstatic void statmem() {
        cout << "这是Base的静态函数statmem()"<< endl;
    }

};

class Derived : public Base
{
    void f(const Derived&);

};

void Derived::f(const Derived &obj)
{
    //下面四种访问方式都对
    // (1)直接通过类名访问:基类类名或派生类类名
    Base::statmem();  
    Derived::statmem();
    // (2)通过实例对象访问:基类对象或派生类对象
    obj.statmem();  //通过Derived对象访问
    statmem();   //通过this对象访问
}
/*
输出结果:
	这是Base的静态函数statmem()
	这是Base的静态函数statmem()
	这是Base的静态函数statmem()
这是Base的静态函数statmem()
*/

派生类的声明

  • 派生类的声明:与其他类差别不大,声明中包含类名但是不包含它的派生列表
  • 派生类声明中不准出现派生列表
class Bulk_quote : public Quote;  //错误,声明不包含派生列表
class Bulk_quote;  //正确:声明派生类不包括派生列表
  • 为什么要这样规定呢?
    • 声明语句的目的是:让程序知道某个名字的存在以及该名字表示一个什么样的实体,而派生列表没有这个作用,它是定义的一部分,所以,派生列表以及与定义有关的其他细节必须与类的主体一起出现

被用作基类的类:类必须在定义后才能作为基类,仅声明不行

  • 如果我们想将某个类用作基类,则该类必须已经定义而非仅仅声明。所以,一个类不能派生它本身。
    • 因为:派生类中包含并且可以使用它从基类继承而来的成员,为了使用这些成员,派生类当然要知道他们是什么
class Quote;
//错误:Quote必须被定义
class Bulk_quote:public Quote{...}
  • 每个类都会继承直接基类的所有成员。对于一个最终类来说,它会继承其直接基类的成员,该直接基类的成员又含有其基类的成员,以此类推,最终的派生类将包含它的直接基类的子对象以及每个间接基类的子对象
    • 基类可分为直接基类和间接基类,直接基类出现在派生列表中,间接基类通过其直接基类继承而来

2.3 virtual、override和final关键字

覆盖/重写和重载

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

virtual:声明一个函数为虚函数

  • 派生类可以覆盖其基类的虚函数,也可以不覆盖,如果不覆盖则直接继承其在基类中的版本。
  • 派生类可以在它覆盖的函数前使用virtual关键字,也可以不用
  • 如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数,或者在派生类中用virtual关键字显式声明这是虚函数,还可以用override关键字说明这个派生类的虚函数是改写了其基类的虚函数的。毕竟派生类也可以是其他类的基类**
  • 如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数,或者在派生类中用virtual关键字显式声明这是虚函数,还可以用override关键字说明这个派生类的虚函数是改写了其基类的虚函数的。毕竟派生类也可以是其他类的基类**

override:显式注明派生类要覆盖基类的虚函数

  • override显式注明派生类要覆盖基类的虚函数,当某个函数被标记为override时要求该函数是虚函数,且已覆盖基类中的虚函数
  • override和final可出现在形参列表之后(包括任何const和引用修饰符)以及尾置返回类型
    • override出现在形参列表后面,或者在const成员函数的const关键字后面,或者在引用成员函数的引用限定符后面
  • 在c++11新标准中,我们可以使用override关键字来说明派生类中的虚函数。如果我们使用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的函数
};

// 或者
struct B{
    virtual void f1(int) const;
    virtual void f2();
    void f3();
};

struct D1:B{
    void f1(int) const override; //正确
    void f2(int) override; //错误,B没有形如f2(int)的函数
    void f3() override;//错误,f3不是函数
    void f4() override;//错误,B没有f4函数
};

final关键字:防止继承的发生(断绝子孙类/函数)

  • 如果我们定义一个类,并不希望它被继承。或者希望某个函数不被覆盖,则可以把类或者函数指定为final,则之后任何尝试继承该类或覆盖该函数的操作将引发错误。
    • 在类名或者函数名后面跟一个final关键字
  • 防止某个类继承的发生:类名后面跟一个final关键字
    • 我们有时会有这样的需求,定义一个类,不希望它被继承
  • final和override说明符出现在形参列表以及尾置返回类型之后。
class NoDerived final{/*...*/};//不能作为基类
class Bad:NoDerived{/**/};//错误:NoDerived不能被继承

class Base{/*...*/};
class Last final:Base{/*...*/};//last不能作为基类
class Bad2:Last{/**/};//错误:last是final的
  • 防止类中某个函数继承的发生:函数名后面跟一个final关键字
    • 如果把某个函数指定为final,则之后任何尝试覆盖该函数的操作都将会引发错误
class D2:B{
    //从B继承f2()和f3(),覆盖f1(int)
    void f1(int)const final;  // 不允许它的派生类覆盖f1(int)
}

class D3:D2{
    void f2();	//正确:覆盖从间接基类B继承而来的f2
    void f1(int)const; //错误:D2已经将f2声明成final
}

// 或者
struct D2:B{
    void f1(int) const final;//不允许后续的其他类覆盖f1(int)
};

struct D3:D2{
    void f2();  //正确:覆盖间接基类B继承而来的f2
    void f1(int)  const;//错误,D2已经将f2声明为final了
};

  • 所以基类可以有三种函数:直接继承函数、虚函数、不能继承final函数

2.4 类型转换与继承:可以将基类的指针或引用绑定到派生类对象上

  • 可以将基类的指针或引用绑定到派生类对象上;存在派生类向基类的类型转换,但不存在基类向派生类的类型转换(基类指针或引用可以绑定到派生类对象,但派生类指针或引用不能绑定到基类对象)
  • 通常情况下,如果我们想把引用或指针绑定到一个对象上,则引用或指针的类型应与对象的类型一致,或者对象的类型含有一个可接受的const类型转换规则。
    • 存在继承关系的类是一个重要的例外:我们可以将基类的指针或引用绑定到派生类对象上。
  • 我们可以将基类的指针或引用绑定到派生类对象上,这意味着,当我们使用基类的引用或指针时,我们并不清楚它所绑定的对象的真实类型,可能是基类对象,也可能是派生类对象。
    • 使用这个基类的指针或者引用时,实际使用的可能是基类对象也可能是派生类对象
  • 和内置指针一样,智能指针类也支持派生类向基类的类型转换,这意味着我们可以将一个派生类对象的指针存储在一个基类的智能指针类。

静态类型与动态类型

  • 静态类型是指变量声明时的类型,如一个函数的形参声明时是基类类型,这个基类类型就是形参的静态类型;而当调用这个函数时传进来的可以是派生类对象也可以是基类对象:当传进来的是基类对象时,该形参的动态类型还是基类类型;当传进来的是派生类对象时,则该形参的动态类型是派生类类型。总结就是若变量不是指针或引用,则其静态类型与动态类型保持一致。基类指针或引用的静态类型可能与动态类型不一致。
  • 当我们使用存在继承关系的类型时,必须将一个变量或其他表达式的静态类型与该表达式表示对象的动态类型区分开来:
    • 静态类型:是变量声明时的类型或表达式生成的类型,编译时已知
    • 动态类型:是变量或表达式表示的内存中的对象的类型,运行时才可知
  • 如果一个变量非指针也非引用,则它的静态类型和动态类型永远一致。
  • 基类的指针或引用的动态类型可能与其静态类型不一致。
  • 不存在从基类向派生类的隐式类型转换。
    • 之所以存在派生类向基类的转换,是因为每个派生类对象都包含一个基类部分,而基类的引用或指针可以绑定到该派生类对象的该基类部分。
    • 反之,一个基类对象,可能并不是派生类的一部分,所以不存在从基类向派生类的自动类型转换
Bulk_quote bulk;
Quote *itemp = &bulk;//正确
Bulk_quote *bulkp = itemp;//错误:因为编译器并不知道itemp指向的对象是独立存在的基类,还是作为派生类的一部分

在对象之间不存在类型转换:派生类向基类的自动类型转换只对指针或引用类型有效

  • 派生类向基类的自动类型转换只对指针或引用类型有效,在派生类类型和基类类型之间不存在这样的转换。
  • 当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分将被忽略掉。——不建议这么做
    • 基类对象由派生类对象转换而成则只处理派生类的基类部分,派生类不能由基类转换而成
// 示例1
Quote base; // 基类
Bulk_quote bulk; // 派生类
Quote* baseP = &bulk; //派生->基类转换对指针有效
Quote& baseR = bulk; //派生->基类转换对引用有效
Bulk_quote *bulkP = &base; // 错误:基类->派生无效
Bulk_quote &bulkR = base; // 错误:基类->派生无效
base = bulk; //直接派生->基类,只处理bulk的基类部分
bulk = base; // 错误:直接派生->基类无效

// 示例2
Bulk_quote bulk;
Quote item(bulk);
item = bulk;
// 当构造item时,运行Quote的拷贝构造函数。该函数只能处理bookNo和price两个成员,他负责拷贝bulk中Quote部分的成员,同时忽略掉bulk中Bulk_quote部分成员。
// 类似的,对于将bulk赋值给item的操作来说,只有bulk中的Quote部分的成员被赋值给item
// 上述过程忽略了Bulk_quote部分,所以我们可以说bulk的Bulk_quote部分被切掉了
  • 注意:存在继承关系的类型之间的转换规则
    • 要想理解在具有继承关系的类之间发生的类型转换,有三点非常重要:
      • 从派生类向基类的类型转换只对指针或引用类型有效
      • 基类向派生类不存在隐式类型转换
      • 和任何其他成员一样,派生类向基类的类型转换也可能由于访问受限而变得不可行。15.5节详细介绍
    • 尽管自动类型转换只对指针或引用类型有效,但是继承体系中的大多数类仍然定义了拷贝控制成员。因此,我们通常能够将一个派生类对象拷贝、赋值、或移动给一个基类对象。不过需要注意的是,这种操作只处理派生类对象的基类部分

3. 虚函数

  • 总结:
    • 虚函数在基类中无论是否使用都必须提供定义
    • 使用基类的引用或指针调用一个虚成员函数时会执行动态绑定
    • 派生类可以覆盖其基类的虚函数,也可以不覆盖。如果不覆盖则直接继承其在基类中的版本(不覆盖就是不重新定义,此时该虚函数作为派生类的隐藏成员)
  • 当我们使用基类的引用或指针调用一个虚成员函数时会执行动态绑定。
    • 动态绑定只有当我们通过指针或引用调用虚函数时才发生
  • 普通函数不使用时可以只声明不定义,但虚函数无论是否使用都必须提供定义。
    • 普通函数可以只有一个声明,因为我们不会调用它;但是每个虚函数都可能被调用,运行时才确定,所以为了防止运行时无函数调用,索性要求所有的虚函数都要有定义
  • 对虚函数的调用可能在运行时才被解析
    • 当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确定应该调用哪个版本的函数。被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那一个。
    • 当我们通过一个具有普通类型(非引用非指针)的表达式调用虚函数时,在编译时就会将调用的版本确定下来
  • 当且仅当通过指针或引用调用虚函数时,才会在运行时解析该调用;也只有在这种情况下,对象的动态类型才有可能和静态类型不同

C++的多态性:C++支持多态性的根本在于引用或指针的静态类型与动态类型不同

  • OOP的核心思想是多态性。多态性含义是“多种形式”。我们把具有继承关系的多个类型称为多态类型,因为我们能使用这些类型的“多种形式”而无须在意它们的差异。
  • 引用或指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本所在。
  • 当我们使用基类的引用或指针调用基类中定义的一个函数时,我们并不知递该函数真正作用的对象是什么类型,因为它可能是一个基类的对象也可能是一个派生类的对象。如果该函数是虚函数,则直到运行时才会决定到底执行哪个版本,判断的依据是引用或指针所绑定的对象的真实类型。
  • 另一方面,对非虚函数的调用在编译时进行绑定。类似的,通过对象进行的函数(虚函数或非虚函数)调用也在编译时绑定。
    • 对象的类型是确定不变的,我们无论如何都不可能令对象的动态类型与静态类型不一致。因此,通过对象进行的函数调用将在编译时绑定到该对象所属类中的函数版本上。

派生类中的虚函数(一旦虚,一直虚)

  • 派生类可以覆盖其基类的虚函数,也可以不覆盖,如果不覆盖则直接继承其在基类中的版本。
  • 派生类可以在它覆盖的函数前使用virtual关键字,也可以不用
  • C++11 还允许使用override关键字,让编译器检查这个函数是不是覆盖基类的虚函数。
  • 总结:如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数,或者在派生类中用virtual关键字显式声明这是虚函数,还可以用override关键字说明这个派生类的虚函数是改写了其基类的虚函数的。毕竟派生类也可以是其他类的基类
  • 基类中的虚函数在派生类中隐含地也是一个虚函数(因此该虚函数在派生类中可以省略virtual关键字)
    • 因为一旦基类中某个函数被声明成虚函数,则在所有派生类中它都是虚函数
    • 当派生类覆盖了某个虚函数时,则该虚函数在基类和派生类中的形参类型和返回类型必须完全一致
  • 对于派生类中要覆盖的基类的虚函数可以省略virtual关键字的原因:
    • 当我们在派生类中覆盖了某个虚函数时,可以再一次使用virtual关键字指出该函数的性质。然而这么做并非必须,因为一旦某个函数被声明成虚函数,则在所有派生类中它都是虚函数。
  • 若派生类覆盖某个继承而来的虚函数,则该虚函数在基类和派生类中的形参类型和返回类型必须完全一致。该规则存在一个例外:
    • 当类的虚函数返回类型是类本身的指针或引用时,上述规则无效。
    • 也就是说,如果D由B派生得到,则基类B的虚函数可以返回B*,而派生类D的对应函数可以返回D*,只不过这样的返回类型要求从D到B的类型转换是可访问的。(15.5节介绍一个基类的可访问性)
    • 总结:若虚函数返回类型是类本身的指针或引用,且派生类向基类的类型转换是可访问的,则基类返回基类的引用或指针,派生类返回派生类的引用或指针。
  • 总结:基类中的虚函数在派生类中隐含地也是一个虚函数。当派生类覆盖了某个虚函数时,该函数在基类中的形参必须与派生类中的形参严格匹配。

虚函数与默认实参

  • 如果我们通过基类的引用或指针调用函数,则使用基类中定义的默认实参。即使实际运行的是派生类中的函数版本也是如此,此时传入派生类函数的将是基类定义的默认实参。
    • 如果派生类函数依赖不同的实参,则程序结果将与预期不符。
  • 注意:如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。
  • 总结:若调用虚函数时使用默认实参,则不论是派生类或基类,都使用基类定义的默认实参,故基类和派生类中定义的默认实参最好一致。

回避虚函数的机制:避免虚函数调用时进行动态绑定

  • 通过作用域运算符::可指定执行虚函数的特定版本,而避免虚函数调用时进行动态绑定。
  • 在某些情况下,我们希望对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的某个特定版本。使用作用域运算符可以实现这一目的。
// 强行调用基类中定义的函数版本而不管baseP的动态类型是什么
double undiscounted = baseP->Quote::net_price(42);
  • 注意:如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运算符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无限递归。

纯虚函数:虚函数后面加=0

  • 虚函数在基类中无论是否使用都必须提供定义,但在许多情况下,在基类中不能对虚函数给出有意义的实现。
  • 为了让虚函数在基类什么也不做,引进了“纯虚函数”的概念,纯虚函数无需定义。
    • 在类内的虚函数声明语句分号前书写=0可定义纯虚函数,表示该函数无实际意义。
    • 和普通的虚函数不一样,纯虚函数一般不用定义,若要定义则只能在类外定义函数体
  • 通过在函数体的位置(即在声明语句的分号之前)书写 =0(=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;//虚函数后面加=0是纯虚函数
protected:
    size_t min_qty=0;  //折扣适用的购买量
    double discount = 0.0;  //表示折扣的小数值
};
  • 我们也可以为纯虚函数提供定义,不过函数体必须定义在类的外部,也就是说,我们不能在类的内部为一个 =0 的函数提供函数体。
    • 实际上为纯虚函数提供定义是可以但没必要

4. 抽象基类:含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类;不能实例化抽象基类

  • 含有纯虚函数或者未经覆盖直接继承纯虚函数的类是抽象基类
class Base{  // 抽象基类
public:
    virtual int func(int n) const =0;
};
  • 抽象基类负责定义接口,而后续其他类可以覆盖该接口。
  • 我们不能(直接)创建一个抽象基类的对象。
// 因为Disc_quote将net_price定义成了纯虚函数,所以我们不能定义Disc_quote的对象,我们可以定义Disc_quote的派生类的对象,前提是这些类覆盖了net_price函数。
Base base;   // 错误,不能实例化抽象基类
  • 因为抽象基类含有纯虚函数(没有定义),所以我们不能创建一个抽象基类的对象,但可以声明指向抽象基类的指针或引用。
  • 如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象基类
//Bulk_quote继承Disc_quote并覆盖了net_price函数
Bulk_quote bulk; //正确:Bulk_quote中没有纯虚函数
  • 重构:负责重新设计类的继承体系以便将操作或数据从一个类移动到另一个类。
    • 在Quote的继承体系中增加Disc_quote类是重构(refactoring)的一个典型示例。重构负责重新设计类的体系以便将操作和/或数据从一个类移动到另一个类中。对于面向对象的应用程序来说,重构是一种很普遍的现象。
    • 值得注意的是,即使我们改变了整个继承体系,那些使用了Bulk_quote或Quote 的代码也无须进行任何改动。不过一旦类被重构(或以其他方式被改变),就意味着我们必须重新编译含有这些类的代码了。
class Quote; // 间接基类
class Disc_quote; // 直接基类,抽象基类
class Bulk_quote; // 派生类
aDisc_quote disc; // 错误,不能创建抽象基类的对象
Bulk_quote bulk;

5. 访问控制与继承

  • 我们通常认为一个类有两种不同的用户:普通用户 和 类的实现者。其中:
    • 普通用户编写的代码使用类的对象,这部分代码只能访问类的公有(接口)成员
    • 实现者则负责编写类的成员和友元的代码,成员和友元既能访问类的公有部分,也能访问类的私有部分
    • 如果进一步考虑继承的话就会出现第三种用户,即派生类
      • 派生类可以访问基类的公有(public)成员和受保护(protected)成员,但不能访问基类的私有(private)成员
  • 继承相关点:
    • 大多数类都只继承自一个类,这种形式的继承叫做“单继承”。这一章主要讲的是单继承。
    • 一个派生类的对象中,包含继承自基类的部分和派生类自定义的部分。正因为派生类含有基类部分,所以可以进行派生类到基类的类型转换,这种转换是隐式的。
    • 不存在从基类向派生类的隐式类型转换。
    • 派生类向基类的自动类型转换只对指针或引用有效,对象之间不存在类型转换。
    • 如果基类定义了静态成员,则不论派生出多少个派生类,每个静态成员都只存在唯一实例。
    • 防止一个类被继承可以使用关键字final,这是C++11新标准中提供的。
    • 继承中的虚函数与纯虚函数。
  • 每个类分别控制自己的成员初始化过程,与之类似,每个类还分别控制着其成员对于派生类来说是否可访问。
    • 每个类分别控制其成员的初始化过程和对派生类的访问权限

访问说明符

  • 在C++中通过使用访问说明符public、protected、private来对类的成员进行访问控制,控制成员对于普通用户或派生类来说是否可访问:
    • public:定义为public的成员对普通用户、类的实现者、派生类都是可访问的。
      • public通常用于定义类的外部接口。
      • public成员在整个程序内可见。
    • protected:定义protected成员的目的是让派生类可以访问而禁止其他用户访问。所以类的实现者和派生类可以访问,而普通用户不能访问。
      • 使用protected关键字来声明那些他希望与派生类分享,但是不想被其他公共访问使用的成员。protected说明符可以看做是public和private中和后的产物
      • protected成员只对该类成员函数和友元函数、派生类成员函数和友元函数可见,类用户不可访问。
    • private:定义为private的成员只能被类的实现者(成员和友元)访问。
      • private部分通常用于封装(即隐藏)类的实现细节。
      • private成员只对该类成员函数和友元函数可见,类用户不可访问。
  • 类的普通用户(即普通函数)如何使用一个类的public成员
    • 实例化一个类对象出来,然后调用对象的成员变量和成员函数

受保护的成员protected

  • 和私有成员类似,受保护的成员对于类的用户来说是不可访问的
class Base {
    Base() {
        a=10;
    }
protected: 
    int a;
};

int main() {
    Base A;
    A.a=12; //错误
    return 0;
}
  • 和公有成员类似,受保护的成员对于派生类的成员和友元来说是可访问的。
class Base {
    Base() {
        a=10;
    }
protected: 
    int a;
};

class Derived:public Base {
    void test() {
        a=11;
    }
};
  • 派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权。即派生类的成员和友元不能越过派生类直接通过基类对象访问其受保护成员
    • 这句话有点拗口,意思是:派生类中的成员和友元如果通过对象来访问基类的受保护成员,那么这个对象必须是派生类对象,如果这个对象是基类对象,那么是不能直接访问的这个对象的受保护成员的——因为基类的受保护成员被派生类继承下来了,那么该基类受保护成员就已经成为了派生类的一部分受访问成员,所以派生类的成员和友元才能访问。实际上在派生类继承下来的受保护成员已经和基类的受保护成员已经是独立的个体了,所属类都不同了。
// 示例1
class Base
{
protected:
    int prot_mem;
};

class Sneaky : public Base
{
    //注意这是友元函数,不是成员函数
    friend void clobber(Sneaky&);   //能访问Sneaky::prot_mem
    firend void clobber(Base&); //不能访问 Base::prot_mem
    int j; //j默认是private
};

//正确:派生类的友元函数可以通过派生类对象访问基类的protected成员
void clobber (Sneaky &s) {s.j = s.prot_mem = 0;}
//错误:派生类的友元函数不能通过基类对象访问基类的protected成员
void clobber(Base &b) {b.prot_mem = 0;} 

// 示例2:
class Base {
    Base() {
        a=10;
    }
    protected: int a;
};

class Derived:public Base {
    //可以通过编译
    void test1() {
        this->a=22;
    }
    //不可以通过编译
    Base b;
    void test2() {
        b.a=22;
    }
};
//test1()通过派生类对象访问受保护成员可以通过编译,test2()通过基类对象访问就不行
  • 规定:派生类的成员和友元只能访问派生类对象中继承下来的的基类部分的受保护成员;对于普通的基类对象中的成员不具有特殊的访问权限。
    • 为什么要这么规定呢?为什么第二个函数Base的对象不让它访问基类的protected,我们这么想,如果合法,那有人设计了一个Base类,里面有个protected的成员,我们可以简单地定义它的派生,然后就可以通过Base对象去访问Base的protected了,这显然非常不安全,于是就禁止了
  • 派生类可以访问基类的受保护成员,但是只能通过两种合法的方式:
    • 在派生类内中直接访问
    • 通过派生类对象访问

公有、私有和受保护继承:类派生列表中

  • 在类派生列表中用到了访问说明符public、protected和private,它们分别表示不同的继承方式
  • 继承关系与成员访问符
    • 第一行表示在父类中的属性,表格中表示不同继承方式下的派生类继承下来的成员的属性变化
public成员 protected成员 private成员
public继承 public protected 不可见
protected继承 protected protected 不可见
private继承 private private 不可见
  • public继承:如果继承是公有的,则成员将遵循其原有的访问说明符。父类中的public、protected和private属性在子类中不发生改变。
  • private继承:比private级别高的访问权限会变成private。
    • 即父类中的三种访问属性在子类中都会变成private。
  • protected继承:比protected级别高的访问权限会变成protected。
    • 即父类中的public属性在子类中变为protected,父类中的protected和private属性在子类中不变。
  • 如果我们在派生列表中不使用访问说明符,则struct关键字默认的是公有继承,class关键字默认的是私有继承。不过建议在继承时最好显式地将访问说明符写出来。
class A : public B { /*  */ };     // 公有继承
class A : private B { /*  */ };    // 私有继承
class A : protected B { /*  */ };  // 受保护继承
  • 某个类对其继承而来的成员的访问权限受到两个因素影响:
    • 一是在基类中该成员的访问说明符
    • 二是在派生类的派生列表中的访问说明符
  • 派生访问说明符对于派生类的成员(及友元)能否访问其直接基类的成员没什么影响。 对基类成员的访问权限只与基类中的访问说明符有关。
  • 派生访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限
  • 总结:类列表中的访问说明符只影响派生类用户对于基类成员的访问权限,而不影响派生类成员或友元对基类成员的访问权限。
    • 派生类用户和派生类的派生类(继承自派生类的新类)对该基类成员的访问权限受基类中的访问说明符和类列表中的访问说明符的影响;
    • 而派生类的成员或友元对基类成员的访问权限只受基类中的访问说明符的影响
class Base{
public:
    void pub_mem();//public成员
protected:
    void 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;}
};

//派生访问说明符只影响用户的访问权限
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{
    //正确:Base::prot_mem在Pub_Derv中仍然是protected的
    int use_base(){return prot_mem;}
};

struct Derived_from_Private:public Priv_Derv{
    //错误:Base::prot_mem在Priv_Derv中是private的
    int use_base(){return prot_mem;}
};
  • 继承相关示例代码
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
    • 规定1:只有当D公有的继承B时,用户代码才能使用派生类向基类的转换;如果D继承B的方式是受保护的或者私有的,则用户代码不能使用该转换
    • 规定2:不论D以什么方式继承B,D的成员函数和友元都能使用派生类向基类的转换;派生类向其直接基类的类型转换对于派生类的成员和友元来说永远是可访问的。
    • 规定2:如果D继承B的方式是公有的或者受保护的,则D的派生类的成员和友元可以使用D向B的类型转换,反之,如果D继承B是私有的,则不能使用。
    • 注意:对于代码中的某个节点来说,如果基类的公有成员是可访问的,则派生类向基类的类型转换也是可以访问的;反之不行

友元与继承:友元关系不能传递和继承

  • 就像友元关系不能传递一样,友元关系同样也不能继承。基类的友元在访问派生类成员时不具有特殊性,类似的,派生类的友元也不能随意访问基类的成员。
class Base{
   //添加友元类,其他跟之前一样    
   friend class Pal;//Pal访问Base的派生类时不具备特殊性
};

class Pal{
public:
    int f(Base b) {return b.prot_mem;}//正确:Pal是Base的友元
    int f2(Sneaky s){return s.j;}//错误:Pal不是Sneaky的友元
    //对于基类的访问权限由基类本身控制,即使对于派生类的基类部分也是如此
    int f3(Sneaky s){return s.prot_mem;};//正确:class Sneaky : public Base,访问的是基类的部分,能不能访问基类说了算,而Pal是Base的友元
};

// 对于f2函数,Pal是Base的友元,Pal能够访问Base对象的成员,这种可访问性包括了Base对象内嵌在其派生类对象中的情况。
  • 当一个类将另一个类声明为友元时,这种友元关系只对做出声明的类有效。对于原来那个类来说,其友元的基类或者派生类不具有特殊的访问能力
class D2: public Pal{
public:
    int mem(Base b){
        return b.prot_mem;//错误:友元关系不能继承
    }
}

改变个别成员的可访问性

  • 派生类只能为可访问成员提供using 类名::成员以修改直接或间接基类成员的访问权限,该权限由using 类名::成员前的访问说明符决定。
  • 有时我们需要改变派生类继承的某个名字的访问级别,通过使用using声明可以达到这一目的
// 示例1
class Base{
public:
    std::size_t size() const {return n;}
protected:
    std::size_t n;
};

class Derived:private Base{
Public:
    //保持对象尺寸相关的成员访问级别
    using Base::size;
protected:
    using Base::n;
};
// 因为Derived是私有继承,所以继承而来的成员size和n是Derived的私有成员,
// 然而我们使用using声明语句,将size变成了public成员,Derived变成了protected成员,改变了这些成员的可访问性。

// 示例2
class People{
protected:
    string name;
};

class Student : public People{
public:
    using People::name;  // 将继承来的name成员的访问权限改为public
};

int main()
{
    Student me;
    me.name = "SongLee";     // 可以访问name了
    cout << me.name << endl;  
    return 0;
}
  • 派生类只能为那些它可以访问的名字提供using声明,也就是不能为private提供using
  • 通过在类的内部使用using声明语句,我们可以将该类的直接或间接基类中的任何可访问成员(非私有成员)标记出来,改变其访问权限

默认的继承保护级别:class为private。struct为public

  • class和struct唯一的区别是,class的默认成员访问说明符和默认派生访问说明符是private,而struct的是public。
class Base{  };
struct D1 : Base{  };  //默认public继承
class D2 : Base{  };  //默认private继承

6. 继承中的类作用域

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

在编译时进行名字查找

  • 在编译时进行名字查找:派生类的作用域位于基类作用域之内。查找派生类成员名字时,若在派生类中未找到则会前往基类查找
  • 一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。即使静态类型与动态类型可能不一致(当使用基类的引用或指针时会发生这种情况),但是我们能使用哪些成员仍然是由静态类型决定的
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()函数

名字冲突与继承**

  • 名字冲突与继承:**若派生类成员与基类成员同名,则派生类成员会隐藏基类成员。
    • 虽然可通过作用域运算符::来使用隐藏成员,但最好不要在派生类中重用除虚函数之外的定义在基类中的名字.
  • 6.4.1节:声明在内层作用域的函数并不会重载声明再外层作用域的函数。因此,定义派生类中的函数也不会重载其基类中的成员。
  • 如果派生类(即内层作用域)的成员与基类(即外层作用域)的某个成员同名,则派生类将在其作用域内隐藏该基类成员。即使派生类成员和基类成员的形参列表不一致,基类成员也仍然会被隐藏掉。
  • 和其他作用域一样,派生类也能重用定义在其直接基类或间接基类中的名字,此时定义在内层作用域(即派生类)的名字将隐藏定义在外层作用域(即基类)的名字。
  • 派生类的成员将隐藏同名的基类成员。不会进行重载,直接隐藏
  • 除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字
struct Base
{
    Base() : mem(0) {} //构造函数
protected:
    int mem;
};
struct Derived : Base
{
    Derived(int i) : mem(i){}  //用i初始化Derived::mem
                                     //Base::mem进行默认初始化
    int get_mem() {return mem;}   //返回Derived::mem

protected:
    int mem; //跟基类的重名,隐藏基类中的mem
};
Derived d(42);
cout << d.get_mem() << endl; //打印42
  • 通过作用域运算符来使用隐藏的成员
//我们当然也可以强行打印0,只要把派生类中的函数这样定义就好了
struct Derived: Base
{
   int get_base_mem() {return Base::mem;}
   //…
}
Derived d(42);
cout << d.get_mem() << endl; //打印0
// 作用域运算符将覆盖掉原有的查找规则,并指示编译器从Base类的作用域开始查找mem
  • 名字查找先于类型检查
class Base{
public:
	void fun();
};

class Derived : public Base{
public:
	void fun(int);  //隐藏基类的fun。即使派生类成员和基类成员的形参列表不一致,基类成员fun()也仍然会被隐藏掉
};

Derived d;
Base b;
b.fun();   //调用Base::fun
d.fun(10); //调用Derived::fun
d.fun();//错误,参数列表为空的fun()被隐藏
d.Base::fun(); //正确,调用Base::fun()

名字查找与继承

  • 理解函数调用的解析过程对于理解c++继承至关重要,假定我们调用p->mem()或者(obj.mem()),则依次执行以下4个步骤:
      1. 首先确定p(obj)的静态类型。因为调用的是一个成员,所以该类型必须是类类型
      1. 在p(obj)的静态类型对应的类中查找mem。如果找不到,则依次在直接基类中不断查找直达继承链的顶端。如果找遍了该类及其基类仍然找不到,则编译器报错
      1. 一旦找到了mem,就进行常规的类型检查,以确认对于当前找到的mem,本次调用是否合法
      1. 假设调用合法,则编译器将根据调用的是否是虚函数而产生不同的代码:
    • 如果mem是虚函数且我们是通过引用或指针进行的调用,则编译器产生的代码将在运行时确定到底运行该虚函数的哪个版本,依据是对象的动态类型
    • 反之,如果mem不是虚函数或者我们是通过对象进行的调用,则编译器将产生一个常规函数调用
  • 总结调用p->mem()执行的步骤:确定p的静态类型;在静态类型对应的类中查找mem;找到后,对mem进行类型检查确保调用合法;合法后,若mem是虚函数且通过引用或指针进行调用则会在运行时依据动态类型确定虚函数版本,若mem不是虚函数或通过对象进行调用则会产生一个常规函数调用

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

  • 之前说过要求基类与派生类中的虚函数要有相同的形参列表,因为加入基类和派生类的虚函数接受的实参不同,那我们就没办法通过基类的引用或指针调用派生类的虚函数了
  • 示例1
class Base
{
public:
    virtual int fcn();
};

class D1 : public Base 
{
public:
   //隐藏了基类的fcn,下面这个fcn不是虚函数
   //D1继承了Base::fcn()的定义
    int fcn(int); //形参列表与Base中的fcn不一致
   
    virtual void f2();  //是一个新的虚函数,在Base中不存在
 };

class D2 : public D1 
{
public:
    int fcn(int); //是一个非虚函数,隐藏了D1::fcn(int)
    int fcn();  //覆盖了Base的虚函数fcn
    void f2();  //覆盖了D1的虚函数f2
 };
// D1的fcn函数并没有覆盖Base的虚函数fcn,原因它们的形参列表不同。
// 实际上,D1的fcn将隐藏Base的fcn。
// 此时拥有了两个名为fcn的函数:一个是D1从Base继承而来的虚函数fcn;另一个是D1自己定义的接受一个int参数的非虚函数fcn
  • 通过基类调用隐藏的虚函数
    • 注意:如果派生类中没有覆盖基类中的虚函数,则运行时解析为基类定义的版本
Base bobj;
D1 d1obj;
D2 d2obj;

Base *bp1 = &bobj, *bp2 = & d1obj, *bp3 = & d2obj;
bp1->fcn(); //虚调用,将在运行时调用Base::fcn
bp2->fcn(); //虚调用,将在运行时调用Base::fcn
bp3->fcn(); //虚调用,将在运行时调用D2::fcn

D1 *d1p = &d1obj; 
D2 *d2p = &d2obj'
bp2->f2(); //错误:Base没有名为f2的成员
d1p->f2(); //虚调用,将在运行时调用D1::f2()
d2p->f2(); //虚调用,将在运行时调用D2::f2()
  • 示例2
class Base
{
public:
    int fcn(int) //与类中下面的虚函数同名,必须要形参不同才能重载
    {
        cout << "Base不虚" << endl; 
    }
    virtual int fcn()
    {
        cout << "Base虚" << endl;
    }        
};

class D1 : public Base 
{
public:
    int fcn(int) //并不是虚函数,因为参数和基类不同
    {
        cout << "D1不虚" << endl;
    }

    int fcn() //这才是虚函数,虽然没写override,更好的习惯是写上,这里为了迷惑你们就不写了
    {
        cout << "D1虚" << endl;
    }
};


int main()
{
    Base bobj; 
    D1 d1obj;
    Base *bp1 = &bobj; 
    Base *bp2 = &d1obj;
    D1 *bp3 = &d1obj;
//调用的是虚函数,动态绑定
    bp1->fcn(); //Base虚
    bp2->fcn(); //D1虚
    bp3->fcn(); //D1虚
//调用的是非虚函数,静态绑定
    bp1->fcn(1); //Base不虚
    bp2->fcn(1); //Base不虚
    bp3->fcn(1); //D1不虚
    system("pause"); //提交的时候不要这句
    return 0;
}
  • 示例3
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;
//基类BaseHide的引用或指针只能访问派生类中基类BaseHide的部分
bp1->fcn();   //虚调用,BaseHide::fcn()
bp2->fcn();	 //虚调用,BaseHid::fcn(),这是由于没有覆盖基类的虚函数,直接继承的基类版本
bp3->fcn();	 //虚调用,DerivedHide2::fcn(),覆盖了间接基类的虚函数版本

cout << endl;

DerivedHide1 *dp1 = &dh1;  //指向自己类型
DerivedHide2 *dp2 = &dh2; //指向自己类型
//bp2指向派生类对象DerivedHide1,但由于Base类中没有f2(),所以bp2不能调用f2()。
//bp2->f2();    错误,BaseHide没有名为f2的成员,因为DerivedHide1中继承下来的基类部分没有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)是非虚函数,所以不会发生动态绑定,实际调用的函数版本由指针的静态类型决定。
*/

7. 构造函数与拷贝控制

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

7.1 虚析构函数:基类通常应该定义一个虚析构函数

  • 虚析构函数:基类通常应该定义一个虚析构函数。只要基类的析构函数是虚函数,则整个继承体系的析构函数都会是虚函数(虚函数特性,只要虚,一直虚)
  • 基类通常应该定义一个虚析构函数,这样我们就能动态分配继承体系中的对象了。
  • 如果我们delete一个父类Quote*类型的指针,则该指针有可能实际上指向了一个派生类Bulk_quote类型的对象, 因此编译器必须清楚它应该执行的Derived类型的析构函数。和其他函数一样,我们通过在基类中将析构函数定义成虚函数以确保执行正确的析构函数版本——只要基类的析构函数是虚函数,则整个继承体系的析构函数都会是虚函数:
class Quote
{
public:
    virtual ~Quote() = default; //这样一来,后面继承它的类的析构函数都是虚的了
};
  • 只要基类的析构函数是虚函数,就能确保我们当我们delete基类指针时将运行正确的析构函数版本:
Quote *itemP = new Quote; //静态类型与动态类型一致
delete itemP; //调用Quote的析构函数
itemP = new Bulk_quote; //静态类型与动态类型不一致
delete itemP; // 调用Bulk_quote的析构函数
  • 如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为。
  • 之前我们曾介绍过一条经验准则, 即如果一个类需要析构函数, 那么它也同样需要拷贝和赋值操作。 基类的析构函数并不遵循上述准则, 它是一个重要的例外。
    • 一个基类总是需要析构函数, 而且它能将析构函数设定为虚函数。 此时,该析构函数为了成为虚函数而令内容为空,我们显然无法由此推断该基类还需要赋值运算符或拷贝构造函数。
  • 虚析构函数将阻止合成移动操作:如果一个类定义了析构函数,即使是=default合成的,编译器也不会为这个类合成移动操作

7.2 合成拷贝控制与继承

  • 基类或派生类的合成拷贝控制成员的行为与其它合成的构造函数、赋值运算符或析构函数类似:它们对类本身的成员一次进行初始化、赋值或销毁操作。此外,这些合成的成员还负责适用直接基类中对应的操作对一个对象的直接基类部分进行初始化、赋值或销毁的操作
  • 对于派生类的析构函数来说,它除了销毁派生类自己的成员外,还负责销毁派生类的直接基类;该直接基类又销毁它自己的直接基类,以此类推直到继承链的顶端。

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

  • 下面的情况会导致派生类成员成为被删除的函数:
      1. 如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数是delete(被删除的函数)或者private(不可访问),则派生类中对应的成员将是被删除的,原因是编译器不能使用基类成员来执行派生类对象基类部分的构造、赋值或销毁操作。
      1. 如果在基类中有一个private或delete的析构函数,则派生类中合成的默认和拷贝构造函数将是delete的(被删除的),因为编译器无法销毁派生类对象的基类部分。
      1. 和过去一样,编译器将不会合成一个delete的移动操作。 当我们使用=default请求一个移动操作时,如果基类中的对应操作是delete(删除的)或private(不可访问)的, 那么派生类中该函数将是delete(被删除)的, 原因是派生类对象的基类部分不可移动。 同样, 如果基类的析构函数是删除的或不可访问的, 则派生类的移动构造函数也将是被删除的
class B
{
public:
    B(); //默认构造函数声明
    B(const B&) = delete; //定义为删除的拷贝构造函数
    //既然定义了拷贝构造函数,就不会合成移动操作了
};
class D : public B
{
    //没有声明任何东西,只是单纯继承了B
};
D d;   //正确:D的合成默认构造函数调用B的默认构造函数,自己反正也没成员
D d2(d); //错误:因为B的拷贝构造函数是delete的,所以D的也是delete,无法被调用
D d3(std::move(d));//错误,没有移动构造函数,所以会隐式地调用D的合成拷贝构造函数,但是D的合成拷贝构造函数是删除的
  • 基类B含有一个可访问的默认构造函数和一个显式删除的拷贝构造函数。因为我们定义了拷贝构造函数,所以编译器不会为B合成一个移动构造函数。因此我们既不能移动也不能拷贝B的对象。如果B的派生类希望它自己的对象能被移动和拷贝,则派生类需要自定义相应版本的构造函数。当然,在这一过程中派生类还必须考虑如何移动或拷贝其基类部分的成员。在实际编程过程中,如果在基类中如果没有默认、拷贝或移动构造函数,则一般情况下派生类也不会定义相应的操作

移动操作与继承

  • 大多数基类都会定义一个虚析构函数,因此在默认情况下,基类通常不会有合成的移动操作,而且在它的派生类中也没有合成的移动操作。
  • 因为基类缺少移动操作会阻止派生类拥有自己的合成移动操作,所以当我们确实需要执行移动操作时应该首先在基类中进行定义,一旦基类定义了自己的移动操作,那么他必须同时显式的定义拷贝操作。例如:
class Quote{
public:
    Quote() = default; //默认构造:对成员依次进行默认初始化
    Quote(const Quote&) = default; //拷贝构造:对成员依次拷贝
    Quote(Quote&&) = default; //移动构造:对成员依次拷贝
    Quote& operator=(const Quote&) = default; //拷贝赋值
    Quote& operator=(Quote&&) = default; //移动赋值
    virtual ~Quote() = default; //虚析构函数
    //其他成员与之前的版本一致
}
// 通过上面的定义,我们就能对Quote对象逐成员地分别进行拷贝、移动、赋值和销毁操作。
// 而且除非Quote的派生类含有排斥移动的成员,否则它将自动获得合成的移动操作。

7.3 派生类的拷贝控制成员

  • 派生类的拷贝控制成员:
    • 当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象。
    • 不同的是,派生类的析构函数只负责销毁派生类自己分配的资源。
    • 要拷贝或移动基类部分,就必须在派生类对应的构造函数初始值列表中显式调用基类的拷贝或移动构造函数。同样派生类的赋值运算符也必须显式地为其基类部分赋值。
    • 不同的是,派生类析构函数只负责销毁由派生类自己分配的资源,基类部分自动销毁。
  • 派生类构造函数在其初始化阶段中不但要初始化派生类自己的成员,还负责初始化派生类对象的基类部分
  • 派生类的拷贝和移动构造函数在拷贝和移动自有成员的同时,也要拷贝和移动基类部分的成员
  • 派生类赋值运算符也必须为其基类部分的成员赋值
  • 和构造函数及赋值运算符不同的是,派生类的析构函数只负责销毁派生类自己分配的资源。对象的成员是被隐式销毁的;类似的,派生类对象的基类部分也是自动销毁的。
  • 对象销毁的顺序正好与其创建的顺序相反:派生类析构函数首先执行,然后是基类的析构函数,以此类推, 沿着继承体系的反方向直至最后。(创建时,在派生类的构造函数中,先调用基类的构造函数初始化基类部分,再初始化派生类其他部分。销毁时,先执行派生类析构函数,再执行基类的析构函数)
  • 总结:当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象。

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

  • 在默认情况下,基类默认构造函数初始化派生类对象的基类部分。
    • 如果我们想拷贝(或移动)基类部分,则必须在派生类的构造函数初始值列表中显式地使用基类的拷贝(或移动)构造函数
    • 派生类的赋值运算符也必须显式地为其基类部分赋值。
  • 当为派生类定义拷贝或移动构造函数时,我们通常使用对应的基类构造函数初始化对象的基类部分
class Base{/**/};
class D:public Base{
public:
   //默认情况下,基类的默认构造函数初始化对象的基类部分
   //我们要拷贝或移动基类部分,就必须在派生类对应的构造函数初始值列表中显式调用基类的拷贝或移动构造函数
    D(const D& d):Base(d){/**/}  //调用基类的拷贝构造函数来拷贝基类部分
   //这儿比较特殊的是Base(d)会去匹配Base的拷贝构造函数,虽然人家其实接受B类型
    D(D&& d):Base(std::move(d)){/**/} //调用基类的移动构造函数来移动基类部分
};
/*
初始值Base(d)将一个D对象传递给基类构造函数。
尽管从道理上来说,Base可以包含一个参数类型为D的构造函数,但是在实际编程过程中通常不会这么做。
相反,Base(d)一般会匹配Base的拷贝构造函数,D类型的对象d将被绑定到该构造函数的Base&形参上,
Base的拷贝构造函数负责将d的基类部分拷贝给要创建的对象。
*/

派生类赋值运算符

  • 与拷贝和移动构造函数一样,派生类的赋值运算符也必须显式地为其基类部分赋值
// Base::operator=(const Base&);  不会被自动调用

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

派生类析构函数

  • 在析构函数体执行完成后,对象的成员会被隐式销毁。类似的,对象的基类部分也是隐式销毁的。
    • 因此,和构造函数及赋值运算符不同的是,派生类析构函数只负责销毁由派生类自己分配的资源
class D:public Base{
public:
    ~D(){/*该处由用户定义清楚派生类成员的操作*/} //Base::~Base()会自动被调用执行
};
  • 对象销毁的顺序正好与其创建的顺序相反:派生类析构函数首先执行,然后是基类的析构函数,以此类推,沿着继承体系的反方向直至最后

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

  • 如果构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或析构函数所属类型相对应的虚函数版本。
// 测试类
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的虚函数
*/

7.4 继承的虚构函数:默认构造函数、拷贝构造函数、移动构造函数不能被继承

  • 一个类只初始化它的直接基类,出于同样的原因,一个类也只继承其直接基类的构造函数。
  • 类不能继承默认、拷贝和移动构造函数。如果派生类没有直接定义这些构造函数,则编译器将为派生类合成它们。
  • 在C++11新标准中,派生类能够重用其直接基类定义的构造函数,相当于是派生类偷懒调用直接基类的构造函数了(只有构造函数能这么干,其他的拷贝移动什么的不能继承,默认构造函数也不行)
  • 派生类继承基类构造函数的方式是提供一条注明了(直接)基类名的using声明语句。通常情况下,using声明语句只是令某个名字在当前作用域内可见,而当作用于构造函数时,using声明语句将令编译器产生代码。
    • 对于基类的每个构造函数,编译器都生成一个与之对应的派生类构造函数。换句话说,对于基类的每个构造函数,编译器都在派生类中生成一个形参列表完全相同的构造函数。如下:
class Bulk_quote:public Disc_quote{
public:
    using Disc_quote::Disc_quote;//继承Disc_quote的构造函数
    double net_price(std::size_t) const;
};
  • 继承的构造函数的特点
    • 和普通成员的using声明不一样,一个构造函数的using声明不会改变该构造函数的访问级别。例如,不管using声明出现在哪,基类的私有构造函数在派生类中还是一个私有构造函数,受保护的构造函数和公有构造函数也是同样的规则。
    • 若基类的构造函数是explicit或constexpr,则继承的构造函数也是explicit或constexpr
    • 当一个基类构造函数含有默认实参时,这些实参并不会被继承。相反,派生类将获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认实参的形参。
    • 如果基类有多个构造函数,则除了两个例外情况,大多数时候派生类会继承所有这些构造函数:
      • 第一个例外情况是,如果派生类定义的构造函数与基类的构造函数具有相同的参数列表,那么基类的这个构造函数将不会被继承,将使用派生类自己定义的。定义在派生类中的构造函数将替换继承而来的构造函数。
      • 第二个例外是默认、拷贝和移动构造函数不会被继承。
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();
}
//若将using TestBase::TestBase;注释掉,则TestDerived td1(5);会报错。

// 输出:id:5

8. 容器与继承

  • 当派生类对象被赋值给基类对象时,其中的派生类部分将被“切掉”,因此,当我们使用容器存放继承体系中的对象时,通常必须采取间接存储的方式
vector<Quote> basket;
basket.push_back(Bulk_quote("a", 50, 10, 0.25));
//basket的元素是Quote对象,因此当我们向其中添加Bulk_quote对象时,派生部分会被忽略
  • 我们不能把具有继承关系的多种类型的对象直接存放在容器中
  • 容器中只能存放同一类型的元素。基类与派生类虽然存在继承关系,但不是同一类型
    • 若需要在容器中存放具有继承关系的对象,则应存放指向基类对象的指针。
  • 在容器中放置(智能)指针而非对象
    • 当我们希望在容器中存放具有继承关系的对象时,我们实际上存放的通常是基类的指针(更好是智能指针)。和往常一样,这些指针所指对象的动态类型可能是基类类型,也可能是派生类类型:
vector<shared_ptr<Quote>> basket;
basket.push_back(make_shared<Quote>("a", 50));
basket.push_back(make_shared<Bulk_quote>("b", 50, 10, 0.25));
cout << basket.back()->net_price(15) << endl; //打印折扣后的价格,这回派生类对象是完整的
  • 注意:我们以前能把派生类的指针转换成基类指针;那我们也能把派生类的智能指针转换为基类的智能指针
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
======
*/
posted @ 2021-05-31 13:43  夏目的猫咪老师  阅读(65)  评论(0编辑  收藏  举报