文章目录

  • 一、对象数组与对象指针
  • 二、OOP概述
  • 1、继承
  • 2、动态绑定
  • 三、 定义基类和派生类
  • 1、定义基类
  • 2、定义派生类
  • (1)派生类对象及派生类向基类的类型转换
  • (2)派生类构造函数
  • (3)继承与静态成员
  • (4)被用作基类的类
  • (5)防止继承的发生
  • 3、类型转换与继承
  • (1)静态类型与动态类型
  • (2)不存在基类向派生类的隐式转换
  • (3)在对象之间不存在类型转换
  • (4)关键点:
  • 四、虚函数
  • 1、多态原理探究:
  • 2、对虚函数的调用可能在运行时才被解析
  • 3、派生类的虚函数
  • 4、final和override说明符
  • 5、虚函数、默认实参和回避虚函数机制
  • 6、是否类的每个成员函数都可以声明为虚函数?
  • 五、抽象基类
  • 1、纯虚函数
  • 2、含有纯虚函数的类是抽象基类
  • 3、派生类构造函数只初始化它的直接基类
  • 六、访问控制和继承
  • 1、受保护的成员
  • 2、公有,私有和受保护继承
  • 3、派生类向基类转换的可访问性
  • 4、友元与继承
  • 5、改变个别成员的可访问性
  • 6、默认的继承保护级别
  • 七、继承着中的类作用域
  • 1、在编译时进行名字查找
  • 2、名字冲突和继承
  • 3、覆盖重载的函数
  • 4、重载、覆盖和隐藏的区别
  • 八、构造函数与拷贝控制
  • 1、虚析构函数
  • 2、合成拷贝控制与继承
  • 3、派生类的拷贝控制成员
  • (1)定义派生类的拷贝或移动构造函数
  • (2)派生类赋值运算符
  • (3)派生类析构函数
  • (4)在构造函数和析构函数中调用虚函数
  • 4、继承的构造函数
  • 九、容器与继承
  • 十、本章demo:


一、对象数组与对象指针

学习完智能指针后我们就可以用智能指针new一个对象或者进行动态分配:

#include <bits/stdc++.h>
using namespace std;
class Coordinate
{
public:
	Coordinate() = default;
	Coordinate(int x, int y):m_ix(x) ,m_iy(y)
	{ 
		cout << "Coordinate" << endl; 
	}
	~Coordinate() 
	{ 
		cout << "~Coordinate" << endl; 
	}
	inline int print() { return m_ix + m_iy; }
	inline void setvalue(int x1, int y1) { m_ix = x1 ;m_iy = y1 ; } 
private:
	int m_ix = 0;
	int m_iy = 0;
};

int main(int argc, char const *argv[])
{
	//使用智能指针。
	//对象指针:
	unique_ptr<Coordinate> p1(new Coordinate(10,20));
	cout << p1->print() << endl;

	//对象数组:
	unique_ptr<Coordinate []> p2(new Coordinate[3]);
	for (int i = 0; i < 3; ++i)
	{
		p2[i].setvalue(10,20);
		cout << p2[i].print() << endl;
	}

	return 0;
}

二、OOP概述

面向对象程序设计的核心思想是:数据抽象(类),继承和动态绑定。使用继承:可以定义相似的类型并对其相似关系建模。使用动态绑定:可以在一定成都上忽略相似类型的区别,而以统一的方式使用他们的对象。

1、继承

通过继承联系在一起的类构成一种层次关系,根部是一个基类,其他类直接或间接继从基类继承而来。
  继承得到的类叫做派生类。基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。
  某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明为虚函数。

【Note】:
1)派生类必须通过派生类列表(首先是一个冒号,后面紧跟以逗号分隔的基类列表)来指明是那个基类的派生类以及访问权限。
2)派生类必须在其内部对所有重新定义的虚函数进行声明。

什么不能被继承:

  • 构造函数(C++11使用using声明可“继承”);
  • 析构函数;
  • 私有成员函数;
  • 赋值操作符;
  • 友元函数。

2、动态绑定

有时我们需要定义一个函数来处理基类和派生类,比如输出函数,根据实际传入的参数来决定到底输出那个类的数据。函数的运行版本由实参决定,既在运行时选择函数的版本,所以动态绑定有时被称为运行时绑定。
  【Note】:
1)在c++中,我们使用基类的引用或指针调用一个虚函数时会发生动态绑定。

三、 定义基类和派生类

1、定义基类

基类通常都应该定义一个虚构函数,即使虚构函数不执行任何实际操作也是如此。
  派生类可以继承基类的成员,派生类需要对这些操作提供自己的新定义以覆盖(override)从基类继承而来的就定义。
  任何构造函数之外的非静态函数都可以是虚函数,关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。
  c++是静态类型的语言,其解析过程发生在编译期间,但对于虚函数是在运行期间动态绑定。

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 Quote *clone() const & { return new Quote(*this); }
	virtual Quote *clone()  && { return new Quote(std::move(*this)); }
private:
	string bookNo;
protected:
	double price = 0.0;
};

2、定义派生类

class Bulk_quote : public Quote
{
public:
	Bulk_quote() = default;
	//覆盖基类的函数版本以实现基于大量购买的折扣政策。
	Bulk_quote(const string& book, double p, size_t qty, double disc):
				Quote(book,p), min_qty(qty), discount(disc){}//首先初始化基类,然后初始化派生类的成员。
	double net_price(size_t) const override;
	Bulk_quote *clone() const & { return new Bulk_quote(*this); }
	Bulk_quote *clone()  && { return new Bulk_quote(std::move(*this)); }
private:
	size_t min_qty = 0;//最低购买量。
	double discount = 0.0;//折扣率。
};

派生类必须通过使用派生类列表明确指出它是从哪个基类继承而来的。
  派生类需要对其继承而来的成员函数中需要覆盖的那些进行重新声明。
  派生类经常覆盖它继承的虚函数。如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本。
  派生类可以访问基类的public和protected成员。

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

一个派生类对象包含多个组成部分:一个含有派生类自己定义的(非静态)成员的子对象,以及一个与该派生类继承的基类对应的子对象。在一个对象中,派生类继承基类的部分和派生类的部分不一定是连续存储的。
  因为派生类对象中包含与其基类对应的组成部分,所以我们能把派生类的对象当成基类对象来使用,我们也能将基类的指针或引用绑定到派生类对象的基类部分。这种称为派生类到基类的类型转换,编译器会隐式的执行转换。

Quote item;//基类对象  
bulk_quote bulk;//派生类对象  
Quote *p = &item;//P为一个指针,指向一个Quote类型对象  
p = &bulk;//p指向的是bulk的Quote部分,也就是派生类的基类部分  
Quote &r = bulk;//r绑定到bulk的基类部分

【Note】:
1)派生类内部继承基类的虚函数我们必须定义而不只能声明。
2)继承的关键:派生类中有基类的对应组成部分。

(2)派生类构造函数

虽然派生类从基类继承了部分成员,但是并不能直接初始化这些成员。派生类必须使用基类的构造函数类初始化这些成员。
  【Note】:
1)每个类控制它自己的成员的初始化过程。
2)派生类首先初始化基类的部分,然后按照声明的顺序依次初始化派生类部分。
3)每个类负责定义自己各自的接口,要想与类的对象交互必须使用该类的接口,即使这个对象是派生类的基类部分也是如此。

(3)继承与静态成员

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

(4)被用作基类的类

如果我们想将某个类用作基类,则该类必须已经定义而非仅仅声明。一个类是基类,同时它也可以是一个派生类。
  直接基类:直接继承基类。
  间接基类:继承基类的派生类。
最终的派生类将包含它的直接基类的子对象以及每个间接基类的子对象。

(5)防止继承的发生

有时我们会希望定义这样一种类,我们不希望其他类继承它,或者不想考虑它是否适合作为一个基类。c++11提供了一种防止继承的方法就是在类名后面跟一个关键字final。

3、类型转换与继承

存在继承关系的类:我们可以将基类的指针或引用绑定到派生类上(是类型匹配绑定的一个例外)。含义是当我们使用该指针或引用时,实际我们并不知道绑定对象的真实类型,可能是基类也可能是派生类。智能指针类也支持这样的转换。我们可以在基类的智能指针存储派生类对象。

(1)静态类型与动态类型

静态类型是在编译时就已知的,它是变量声明时的类型或表达式生成的类型。
  动态类型则是变量或表达式表示的内存中的对象的类型,动态类型直到运行时才可知道。
  但是如果表达式既不是指针也不是引用,则它的动态类型永远和静态类型一致。

(2)不存在基类向派生类的隐式转换

派生类继承基类的所有,但是也可以通过虚函数来重新定义一些成员,在虚函数表中,重新定义就会覆盖(override)掉基类版本,所以基类并不完全是派生类的一部分,所以不存在从基类向派生类的自动类型转换。假如合法我们则可能访问到不存在的成员。
  如果我们要自己转换可以使用dynamic_cast请求类型转换,该转换的安全检查在运行时执行,如果我们知道某个类型是安全的那么可是使用static_cast来转换。

(3)在对象之间不存在类型转换

派生类到基类的类型转换只针对与引用或者指针的类型,其本类型是不支持的,即对象之间不存在类型转换。
  【Note】:
1)当我们用一个派生类的对象给一个基类对象初始化或者赋值时,只有其基类的部分被拷贝、移动或者赋值,它的派生类部分将会被忽略掉。

(4)关键点:

  • 从派生类像基类类型转换只有对指针或者引用类型有效;
  • 基类到派生类不存在隐式类型转换;
  • 派生类到基类的类型转换也可能会由于访问限制而变得不可行。

四、虚函数

当我们使用引用或者指针调用一个虚成员函数时才会执行动态绑定,因为我们知道在程序运行时才知道到底调用了哪个版本的虚函数,所以所有虚函数都必须有定义。OOP的核心思想之一多态性就是通过虚函数体现的。

1、多态原理探究:

多态:同样一个函数在派生类和基类中有不同的形态。

多态性发生的三个条件:继承关系、虚函数覆盖、父类指针或引用指向子类对象。

C++使用virtual关键实现多态,多态是设计模式的基础。多态性使得程序调用的函数是在运行时动态确定的,而不是在编译时静态确定的

【原理如下】:

1. 当类中声明虚函数时,编译器会在类中生成一个虚函数表(v table) 在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证真实反应实际的函数。所以,当我们用父类的指针来操作一个子类的时候,它就像一个地图一样,指明了实际所应该调用的函数

2. 虚函数表是一个存储类成员函数指针的数据结构,虚函数表是由编译器自动生成和维护的。

每个对象都有一个指向虚函数表的指针(vptr指针)。所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表指明了实际所应该调用的函数。

【C++ Primer】面向对象程序设计_派生类


【C++ Primer】面向对象程序设计_虚函数_02


【C++ Primer】面向对象程序设计_面向对象_03


【C++ Primer】面向对象程序设计_派生类_04


  多态的理论基础就是函数指针。

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

基类的引用或指针作为参数时可以传参基类或派生类,当我们使用引用或指针来传参绑定对象然后调用虚函数时实际上是动态绑定,此时我们不知道到底运行时调用的是哪个类型的虚函数,但是我们必须确保每个虚函数在被调用的时候必须可以使用。引用或指针的静态类型与动态类型不同这一事实是c++语言支持多态性的根本所在。

3、派生类的虚函数

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

4、final和override说明符

派生类如果定义了一个与基类虚函数同名函数,但参数列表不相同的话,仍然是合法行为,编译器会认为该函数与基类虚函数是相互独立的,但这往往是把形参列表弄错了的错误,编译器发现不了,所以C++11有一个好东西,在其后加上override表示其要对基类的函数进行覆盖,若未覆盖,编译器报错,我们可以发现自己的错误。final的作用同上。可以跟在类后面说明不许被继承或者跟在虚函数后面说明不许后继的类继承。

5、虚函数、默认实参和回避虚函数机制

如果我们在基类的虚函数中存在默认实参,那么当我们通过引用或指针来调用时使用的是基类的默认实参,即使调用的是派生类的虚函数也是如此。所以在定义默认实参的时候,基类和派生类的默认实参最好一致。
  在某些情况下我们不需要动态绑定,而是强迫执行虚函数的某个特定版本。通过作用域操作符可以实现这个目的。通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数机制。

//返回给定数量的书籍的销售总额。
virtual double net_price(size_t n) const
{
    return n * price;
}

double net_price(size_t) const override;

6、是否类的每个成员函数都可以声明为虚函数?

原则上是可以的,但是不建议使用。因为每声明一个虚函数都会加入到虚函数表中,但是并不是每个虚函数都要调用,所以会影响效率。

五、抽象基类

抽象基类是把某一些具有相似特征的类抽象化,形成一个新的类,比如人。当多个类之间有相似的方法和数据成员时,可以定义一个抽象基类。

1、纯虚函数

一个纯虚函数无需定义,我们通过在函数体的位置后面加上=0 即可。就可以将一个虚函数变为纯虚函数。其中=0只能出现在类内部的虚函数声明语句处。
  我们也可以为虚函数提供定义,不过函数体必须定义在类的外部,也就是说我们不能在类的内部为一个=0的函数提供函数体。

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

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

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

基类quote----->抽象基类disc_quote----->派生类bulk_quote。其中–>表示继承关系。
  那么我们定义bulk_quote的构造函数时只能初始化disc_quote,在由disc_quote的构造函数来初始化quote的成员。
  关键概念重构:
  重构负责重新设计类的体系以便将操作和/或数据从一个类移动到另一个类中,对于面向对象的应用程序来说,重构是一种很普遍的现象。

#include <iostream>
#include <string>

class quote
{
    public:
        quote() = default;
        quote(std::string &book, double p):
            bookNo(book), price(p) { }
        virtual double net_price(std::size_t sz)const     //虚函数
        { return sz * price; }
    
    protected:      //因为派生类也需要有一份这样的成员所以定义为protected
        double price = 0.0;
    
    private:
        std::string bookNo = "";
};

class disc_quote : public quote  //抽象基类,抽象出所有折扣的类
{
    public:
        disc_quote() = default;
        //构造函数初始化quote类时必须调用quote类的构造函数。
        disc_quote(std::string &book, double p, double disc, std::size_t num):   
            quote(book, p), discount(disc), limit_num(num) { }
        double net_price(std::size_t sz)const = 0;

    protected:  //定义成保护的成员,因为要被派生类继承。且每个派生类都有自己独一无二的成员
        double discount = 0.0;           //表示折扣
        std::size_t limit_num = 0;       //表示限定数量
};

class bulk_quote : public disc_quote                        //继承抽象基类,有一种具体的折扣策略
{
    public:
        bulk_quote() = default;
        bulk_quote(std::string &book, double p, double disc, std::size_t num):
            disc_quote(book, p, disc, num) { }
        double net_price(std::size_t sz)const override   //不加const错误,不是同一个函数了
        {
            if(sz > limit_num)
                return (sz-limit_num) * price * discount + limit_num * price;
            else
                return sz * price;
        }

    private:
};

int main()
{
    quote q;
    //disc_quote disc;       //error
}

六、访问控制和继承

1、受保护的成员

一个类使用protected关键字来说明哪些它希望与派生类 分享但那是不想被其他公共访问使用的成员。

  • 和私有成员类似,受保护的成员对于类的用户来说是不可访问的;
  • 和公有成员类似,受保护的成员对于派生类的成员和友元来说是可访问的;
  • 派生类的成员或友元只能通过派生类来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问权限。

【Note】:
1)派生类的成员和友元只能访问派生类对象中的基类部分的受保护成员,对于普通的基类对象中的成员不具有特殊的访问权限。

2、公有,私有和受保护继承

公有继承(public)、私有继承(private)、保护继承(protected)是常用的三种继承方式。
(1)公有继承(public)
  公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类所访问。
(2)私有继承(private)
  私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。
(3)保护继承(protected)
  保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。

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

用户代码才能使用派生类向基类的转换,如果derive继承base的方式是受保护的或者私有的,则用户代码不能使用该转换。 2. 不论derive以什么方式继承base类,derive的成员函数和友元都能使用派生类向基类的转换,派生类向其直接基类的类型转换对于派生类的成员和友元来说是永久可访问的。 3. 如果derive继承base的方式是共有的或者受保护的,则derive的派生类的成员和友元可以使用derive向base的类型转换,反之,如果derive继承base的方式是私有的,则不能使用。

4、友元与继承

友元关系不能传递,友元关系也不能继承。
  基类的友元在访问派生类成员时不具有特殊性,派生类的友元也不能随便访问基类的成员。当一个类将另一个类声明为友元时,这种友元关系只对做出声明的类有效,对于原来的那个类来说,其友元的基类或者派生类不具有特殊的访问能力。

5、改变个别成员的可访问性

通过在类的内部使用using 声明语句,我们可以将该基类的直接或间接基类中的任何可访问成员标记出来,using 声明语句中名字的访问权限由它所在的访问说明符来决定。

  • 如果一条using 声明语句出现在类的private部分,则该名字只能由类的成员和友元来访问。
  • 如果一条using 声明语句出现在类的protect部分,则该名字能由类的成员和友元和派生类来访问。
  • 如果一条using 声明语句出现在类的public部分,则该名字能由类的所有用户访问。

派生类只能为它可以访问的名字提供using 声明。

6、默认的继承保护级别

class默认的成员访问符和默认的派生成员访问符是private。
  struct默认的成员访问符和默认的派生成员访问符是public。
  这也是C和C++的一个不同点。

七、继承着中的类作用域

每个类定义自己的作用域,在这个作用域内我们定义类的成员,当存在继承关系时,派生类的作用域嵌套在其基类作用域之内。如果一个名字在自己的作用域内无法解析,那么就会在基类中查找。

1、在编译时进行名字查找

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

2、名字冲突和继承

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

【Note】:
1)简单来说就是派生类隐藏基类的同名成员。但是即使基类和派生类函数的参数列表不一样,也会被隐藏。 但是我们可以通过作用域符来使用一个被隐藏的基类成员。
2)除了覆盖掉继承而来的虚函数,派生类最好不要重用其他定义在基类的名字。
3)现在可以理解为什么基类和派生类的虚函数必须有相同的列表。调用非虚函数时不会发生动态绑定,实际调用的函数由指针的静态类型来决定。

3、覆盖重载的函数

如果我们在派生类中定义与基类同名的函数,会覆盖基类中所有同名的函数,如果我们仅仅想覆盖基类中的部分函数,我们可以使用using 声明,将基类的同名函数作用域。
  全部添加到派生类作用域中,此时派生类作用域中也有了这些函数,我们在定义我们想覆盖的函数的版本就可以了。

#include <iostream>

class base
{
    public:
        virtual int fun1() { std::cout << "base" << std::endl; return 10;}
};

class D1 : public base
{
    public:
        int fun1(int i) { std::cout << "D1" << std::endl; return i; }
};

class D2 : public D1
{
    public:
        int fun1(int i) { std::cout << "D2" << std::endl; return i; }
};

int main()
{
    base b;
    D1 d1;
    D2 d2;
    base *p1 = &b, *p2 = &d1, *p3 = &d2;
    p1->fun1();
    p2->fun1(1);//p2->fun1(1)报错了,参数不对,绑定的指针是base的,也就是说我们希望动态绑定,但是在D1中int fun1(int i)就把基类的fun1( )虚函数覆盖了,此时编译器解析为静态绑定。
    //p3->fun1(1);
}

4、重载、覆盖和隐藏的区别

重载:是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。

class A{
public:
  void test(int i);
  void test(double i);//overload
  void test(int i, double j);//overload
  void test(double i, int j);//overload
  int test(int i);         //错误,非重载。注意重载不关心函数返回类型。
};

隐藏:是指派生类的函数屏蔽了与其同名的基类函数,注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。

#include <iostream>
using namespace std;

class Base
{
public:
    void fun(double ,int ){ cout << "Base::fun(double ,int )" << endl; }
};

class Derive : public Base
{
public:
    void fun(int ){ cout << "Derive::fun(int )" << endl; }
};

int main()
{
    Derive pd;
    pd.fun(1);//Derive::fun(int )
    pb.fun(0.01, 1);//error C2660: “Derive::fun”: 函数不接受 2 个参数

    system("pause");
    return 0;
}

覆盖():是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。

#include<iostream>
using namespace std;

class Base
{
public:
    virtual void fun(int i){ cout << "Base::fun(int) : " << i << endl;}
};

class Derived : public Base
{
public:
    virtual void fun(int i){ cout << "Derived::fun(int) : " << i << endl;}
};
int main()
{
    Base b;
    Base * pb = new Derived();
    pb->fun(3);//Derived::fun(int)

    system("pause");
    return 0;
}

八、构造函数与拷贝控制

1、虚析构函数

  • 继承关系对基类拷贝控制最直接的影响是基类通常应该定义一个虚析构函数。这样我们就能动态分配继承体系中的对象了。因为动态绑定实现多态,如果我们仅仅定义普通版本的析构函数可能释放的并不是我们想要释放的对象。
  • 我们通过在基类中将析构函数定义为虚函数以确保执行正确的析构函数版本。只要基类的析构函数是虚函数,就能确保当我们delete基类指针时将运行正确的析构函数版本。
  • 如果一个类需要析构函数那么它也同样需要拷贝和赋值操作,基类的虚析构函数不遵循这个准则,是个例外。
  • 如果一个类定义了析构函数,即使它通过=default的形式使用了合成版本,编译器也不会为这个类合成移动操作。

综上:如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为。所以虚析构函数能够确保delete基类指针时将运行正确的析构函数版本。

2、合成拷贝控制与继承

基类永远会影响派生类,因为派生类继承基类,如果基类拷贝控制部分操作有问题,那么派生类也会有。比如:基类的拷贝构造定义为删除的,那么派生类也必定为删除的,因为派生类中的拷贝构造必须要调用基类的拷贝构造来初始化基类部分,基类定义为删除的,说明派生类不能构造基类部分,那么派生类也就为删除的了。

3、派生类的拷贝控制成员

派生类构造函数在其初始化阶段中不但要初始化派生类自己的成员,还要负责初始化派生类对象的基类部分,因此,派生类的拷贝和移动构造函数在拷贝和移动自有成员的同时,也要拷贝和移动基类部分的成员,类似的派生类赋值运算符也必须为其基类部分的成员赋值。和构造函数及赋值运算符不同的是,析构函数只负责销毁派生类自己分配的资源。
  【Note】:
1)当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象。

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

在默认情况下,基类默认构造函数初始化派生类对象的基类部分,如果我们向拷贝或移动基类部分,则必须在派生类的构造函数初始值列表显示的使用基类的拷贝或移动构造函数。

(2)派生类赋值运算符

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

(3)派生类析构函数

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

(4)在构造函数和析构函数中调用虚函数

简单理解。这个虚函数可能会访问派生类的成员,毕竟,如果它不需要访问派生类的成员的话,则派生类直接使用基类的虚函数版本就可以了,然而,执行基类构造函数的时候,它要用到派生类成员尚未初始化,如果我们允许这样的访问,我们的程序可能会崩溃。
  【Note】:
1)如果构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或析构函数所属类型相对应的虚函数版本。

4、继承的构造函数

在c++11 新标准中,派生类能够重用直接基类定义的构造函数。派生类继承基类构造函数的方式是提供一条注明了基类名的using声明语句。using声明语句将令编译器产生代码,对于基类的每个构造函数,编译器都生成与之对应的派生类构造函数。

class B
{
public:
    B(const string &s);//构造函数
};
class A:public B{
public:
    using B::B;//继承了基类的构造函数

};

【Note】:
1)通常情况下,using只是令某个名字在当前作用域可见,但是当作用于构造函数时,using声明语句将会使编译器产生代码,派生类继承基类的构造函数,其派生的部分成员将会默认初始化。
2)不管using出现在哪,基类的私有构造函数在派生类中还是一个私有类型的,其访问级别不会被using改变。
3)当一个基类的构造函数有默认实参时,这些实参不会被继承,派生类会获得多个继承的构造函数,每个构造函数都将省略掉一个含有默认实参的形参。

九、容器与继承

当我们使用容器存放继承体系中的对象时,通常必须采用简介存储的方式,因为不允许在容器中保存不同类型的元素,所以我们不能把具有继承关系的多种类型的对象直接存放在容器中。
【Note】:
1)当派生类对象被赋值给基类对象时,其中的派生类部分将被切掉,因此容器和存在继承关系的类型无法兼容。
2)在容器中放置(智能)指针而非对象。

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

class Basket
{
public:
	Basket() = default;
	~Basket() = default;
	void add_item(const shared_ptr<Quote> &sale)
	{
		items.insert(sale);
	}
	void add_item(const Quote &sale)
	{
		items.insert(shared_ptr<Quote>(sale.clone()));
	}
	void add_item(Quote &&sale)
	{
		items.insert(shared_ptr<Quote>(std::move(sale).clone()));
	}
	double total_receipt(ostream &) const;
private:
	//定义static成员函数。
	static bool compare(const shared_ptr<Quote> &lhs, 
						const shared_ptr<Quote> &rhs)
	{
		return lhs->isbn() < rhs->isbn();
	}	
	//定义了一个指向Quote对象的shared_ptr的multiset,自定义比较运算符。
	multiset<shared_ptr<Quote>,decltype(compare)*> items{compare};
};

十、本章demo:

#include <bits/stdc++.h>
using namespace std;
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 Quote *clone() const & { return new Quote(*this); }
	virtual Quote *clone()  && { return new Quote(std::move(*this)); }
private:
	string bookNo;
protected:
	double price = 0.0;
};

double print_total(ostream &os, const Quote &item, size_t n)
{
	double ret = item.net_price(n);//通过引用调用虚函数,动态绑定。
	os << "ISBN: " << item.isbn() << " # sold: " << n << " total due : " << ret << endl;
	return ret;
}

class Bulk_quote : public Quote
{
public:
	Bulk_quote() = default;
	//覆盖基类的函数版本以实现基于大量购买的折扣政策。
	Bulk_quote(const string& book, double p, size_t qty, double disc):
				Quote(book,p), min_qty(qty), discount(disc){}//首先初始化基类,然后初始化派生类的成员。
	double net_price(size_t) const override;
	Bulk_quote *clone() const & { return new Bulk_quote(*this); }
	Bulk_quote *clone()  && { return new Bulk_quote(std::move(*this)); }
private:
	size_t min_qty = 0;//最低购买量。
	double discount = 0.0;//折扣率。
};

class Basket
{
public:
	Basket() = default;
	~Basket() = default;
	void add_item(const shared_ptr<Quote> &sale)
	{
		items.insert(sale);
	}
	void add_item(const Quote &sale)
	{
		items.insert(shared_ptr<Quote>(sale.clone()));
	}
	void add_item(Quote &&sale)
	{
		items.insert(shared_ptr<Quote>(std::move(sale).clone()));
	}
	double total_receipt(ostream &) const;
private:
	//定义static成员函数。
	static bool compare(const shared_ptr<Quote> &lhs, 
						const shared_ptr<Quote> &rhs)
	{
		return lhs->isbn() < rhs->isbn();
	}	
	//定义了一个指向Quote对象的shared_ptr的multiset,自定义比较运算符。
	multiset<shared_ptr<Quote>,decltype(compare)*> items{compare};
};

double Basket::total_receipt(ostream &os) const
{
	double sum = 0.0;
	//iter指向ISBN相同的第一批元素的第一个,upper_bound返回这批元素中的最后一个。
	for (auto iter = items.cbegin();
			  iter != items.cend();
			  iter = items.upper_bound(*iter))
	{
		//*iter是shared_ptr,所以要对其解引用得到Quote对象;items.count()计算同名的书籍有所少个。
		sum += print_total(os, **iter, items.count(*iter));
	}
	os << "Total Sale: " << sum << endl;
	return sum;
}

double Bulk_quote::net_price(size_t cnt) const
{
	if (cnt >= min_qty)
	{
		return cnt * discount * price;
	}
	else
	{
		return cnt * price;
	}
}

int main(int argc, char const *argv[])
{
	Basket bsk;
	bsk.add_item(make_shared<Quote>("AVR", 60.0));
	bsk.add_item(make_shared<Quote>("FPGA", 70.0));
	bsk.add_item(make_shared<Bulk_quote>("DSP", 50.0, 4, 0.8));
	bsk.add_item(make_shared<Bulk_quote>("DSP", 50.0, 4, 0.8));
	bsk.add_item(make_shared<Bulk_quote>("DSP", 50.0, 4, 0.8));
	bsk.add_item(make_shared<Bulk_quote>("DSP", 50.0, 4, 0.8));
	bsk.add_item(make_shared<Bulk_quote>("DSP", 50.0, 4, 0.8));
	bsk.total_receipt(cout);
	system("pause");
	return 0;
}