C++ primer第十五章—面向对象程序设计

15.1 OOP:概述

  1. 面向对象程序设计三个基本概念:数据抽象、继承和动态绑定
  2. 使用数据抽象:可以将类的接口与实现分离。
  3. 使用继承:可以定义相似的类型并对其相似关系建模。
  4. 使用动态绑定:可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。

15.1.1 继承

  1. 公有继承(public)、私有继承(private)、保护继承(protected)是常用的三种继承方式。
  • 公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类所访问。
  • 私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。
  • 保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。
  1. 与类型相关的操作,必须在基类和派生类中包含该函数。
  2. 对于有某些函数,基类希望他的派生类定义适合自身的版本,积累将这些函数声明为虚函数
class Quote{
public:
	std::string isbn() const;
	virtual double net_price(std::size_t n) const;
}
  1. 派生类使用派生类列表,派生类必须在其内部对所有重新定义的虚函数进行声明。在派生类列表使用了public关键字,因此可以把Bulk_quote的对象当成Quote的对象使用。
class Bulk_quote : public Quote{
public:
	double net_price(std::size_t) const override;
}
  1. c++11 新标准允许派生类显式注明它将使用哪个成员函数改写基类的虚函数,具体措施是在该函数的形参列表之后增加一个override(覆盖)关键字。

15.1.2 动态绑定

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

15.2 定义基类和派生类

15.2.1 定义基类

  1. 基类通常都应该定义一个虚构函数,即使虚构函数不执行任何实际操作也是如此。
class Quote{
public:
	Quote() = default;
	Quote(const std::string &book, double sales_price):
		bookNo(book), price(sales_price){ }
	std::string isbn() const { return bookNo; }
	// 派生类负责改写并使用不同的折扣计算方法
	virtual double net_price(std::size_t n) const
		{ return n * price; }
	virtual ~Quote() = default;
private:
	std::string bookNo;   // 书籍的ISBN编号
protected:
	double price = 0.0;
}
  1. 派生类可以继承基类的成员,派生类需要对这些操作提供自己的新定义以覆盖从基类继承而来的就定义,用override进行覆盖。
  2. 基类将两种函数成员区分开,一种是希望派生类进行覆盖的虚函数,一种是派生类直接继承而不用改变的函数。当使用引用或指针调用虚函数时,该调用将被动态绑定。
  3. 任何构造函数之外的非静态函数都可以是虚函数,关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。
  4. 如果基类把一个函数声明为虚函数,则该函数在派生类中隐式地也是虚函数。
  5. c++是静态类型的语言,其解析过程发生在编译期间,但对于虚函数是在运行期间动态绑定。
  6. 派生类能访问公有成员,而不能访问私有成员,可以用protected访问运算符定义派生类有权访问而其他用户禁止访问地成员。

15.2.2 定义派生类

  1. 派生类必须通过使用派生类列表明确指出它是从哪个基类继承而来的。派生类需要对其继承而来的成员函数中需要覆盖的那些进行重新声明。派生类经常覆盖它继承的虚函数。如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本。
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;     // 以小数表示的折扣额
}
  1. 派生类可以访问基类的public和protected成员。声明派生类不需要指名基类。
  2. 如果一个派生是公有的,则基类的公有成员也是派生类接口的组成部分。此外,我们能将公有派生类型的对象绑定到基类的引用和指针上。因为我们在派生列表中使用了public,所以Bulk_quote的接口隐式地包含了isbn函数,同时在任何需要Quote的引用或指针的地方我们都能使用Bulk_quote的对象。
  3. 一个派生类对象包含多个组成部分:一个含有派生类自己定义的(非静态)成员的子对象,以及一个与该派生类继承的基类对应的子对象。
  4. 因为派生类对象中包含与其基类对应的组成部分,所以我们能把派生类的对象当成基类对象来使用,我们也能将基类的指针或引用绑定到派生类对象的基类部分。
    在这里插入图片描述
  5. 这种称为派生类到基类的类型转换,编译器会隐式的执行转换。这种隐式特性意味着我们可以把派生类对象或者派生类对象的引用用在需要基类引用的地方,同样的,我们也可以把派生类对象的指针用在需要基类指针的地方。
  6. 派生类内部继承基类的虚函数我们必须定义而不只能声明
  7. 虽然派生类从基类继承了部分成员,但是并不能直接初始化这些成员。派生类必须使用基类的构造函数类初始化这些成员。派生类首先初始化基类的部分,然后按照声明的顺序依次初始化派生类部分。
    在这里插入图片描述
  8. 如果基类定义了一个静态成员,则在整个继承体系中只存在该成语的唯一定义,不论从基类中派生出多少个派生类,对于每个静态成员来说都只存在唯一的实例。
    在这里插入图片描述
    在这里插入图片描述
  9. 派生类的声明包含类名但是不包含它的派生列表。因为一条声明语句的目的是令程序知晓某个名字的存在以及改名字表示一个什么样的实体,如一个类,一个函数或一个变量。派生列表以及与定义有关的细节必须与类的主体一起出现。
  10. 如果想将一个类用作基类,则该类必须已经定义而非仅仅声明。
    在这里插入图片描述
  11. 防止继承的发生
class NoDerived final {
...
}
class Last final : Base{
...
}
#include <iostream>
#include <string>
 
// 书店买书打折的例子
class quote
{
    friend std::ostream& operator<<(std::ostream&os, quote &qt);
    friend std::ostream& print_total(std::ostream &os, quote &qt, std::size_t n);
    public:
        quote() = default;
        quote(const std::string &book, double sales_price):
            bookNo(book), price(sales_price) { }
        //赋值运算符返回值不能定义为const quote&   因为可能存在(s1 = s2) = s3; 这种情况若是const就会错误。
        quote& operator=(const quote &qt);
        std::string isbn()const { return bookNo; }
        virtual double net_price(std::size_t n)const   //虚函数
        {
            return n * price;
        }
        virtual ~quote() = default;                    //虚析构函数       
 
    private:
        std::string bookNo;
 
    protected:                                         //派生类可访问的但是禁止其他用户访问
        double price = 0.0;
};
 
class bulk_quote final: public quote                   //派生类列表,final指名此类不希望被其他类继承
{
    public:
        bulk_quote() = default;
        bulk_quote(const std::string &book, double sales_price, std::size_t ms, double dis):
            quote(book, sales_price), min_sold(ms), discount(dis) { }
        double net_price(std::size_t n)const override;
        ~bulk_quote() = default;
 
    private:
        std::size_t min_sold = 0;
        double discount = 0.0;
};
 
class bulk2_quote : public quote
{
    public:
        bulk2_quote() = default;
        bulk2_quote(const std::string &book, double sales_price, std::size_t ms, double dis):
            quote(book, sales_price), max_sold(ms), discount(dis) { }
        double net_price(std::size_t n)const override;
        ~bulk2_quote() = default;
 
    private:
        std::size_t max_sold = 0;
        double discount = 0.0;
};
 
double bulk2_quote::net_price(std::size_t n)const
{
    if(n <= max_sold)
        return n * price * (1-discount);
    else
        return max_sold * price * (1-discount) + (n-max_sold) * price;
}
 
double bulk_quote::net_price(std::size_t n)const
{
    if(n >= min_sold)
        return n * price * (1-discount);
    else
        return n * price;
}
 
std::ostream& print_total(std::ostream &os, quote &qt, std::size_t n)
{
    auto ret = qt.net_price(n);
    os << "Isbn:" << qt.isbn() << " price:" << qt.price 
              << " sold:" << n << " revenue:" << ret << '\n';
    return os;
}
 
std::ostream& operator<<(std::ostream &os, quote &qt)
{
    os << "Isbn:" << qt.isbn() << " price:" << qt.price << '\n';
    return os;
}
 
int main()
{
    quote item("herry pory", 128.0);             //基类
    quote *p = &item;
    bulk_quote bulk("hello boy", 114, 8, 0.5);   //销售方式1
    bulk2_quote bulk2("hello boy2", 114, 8, 0.5);//销售方式2
    p = &bulk;
    quote &q = bulk;
    print_total(std::cout, item, 10);
    print_total(std::cout, bulk, 10);
    print_total(std::cout, bulk2,10);
 
    return 0;
}

15.2.3 类型转换与继承

  1. 存在继承关系的类:我们可以将基类的指针或引用绑定到派生类上(是类型匹配绑定的一个例外)。含义是当我们使用该指针或引用时,实际我们并不知道绑定对象的真实类型,可能是基类也可能是派生类。
  2. 智能指针类也支持这样的转换。我们可以在基类的智能指针存储派生类对象
  3. 静态类型是在编译时就已知的,它是变量声明时的类型或表达式生成的类型。动态类型则是变量或表达式表示的内存中的对象的类型,动态类型直到运行时才可知。比如上面的类中,print_total( )传递的参数我们也不知道是基类还是派生类,这样print_total里面的net_price也就不知道是哪个版本,直到动态运行时才能知道。但是如果表达式既不是指针也不是引用,则它的动态类型永远和静态类型一致
  4. 派生类继承基类的所有,但是也可以通过虚函数来重新定义一些成员,在虚函数表中,重新定义就会覆盖(override)掉基类版本,所以基类并不完全是派生类的一部分,所以不存在从基类向派生类的自动类型转换。假如合法我们则可能访问到不存在的成员。而之所以存在派生类向基类的转换是因为每个派生类对象都包含一个基类部分,而基类的引用或指针可以绑定到该基类的一部分存在。
    在这里插入图片描述
  5. 如果我们要自己转换可以使用dynamic_cast请求类型转换,该转换的安全检查在运行时执行,如果我们知道某个类型是安全的那么可是使用static_cast来转换。
  6. 在拷贝赋值运算符和拷贝构造函数传值的时候传的也是引用,所以派生类也可以通过这两个来初始化或赋值基类,但是拷贝过去的只是基类存在的对象,其余部分被切割掉了。当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝,移动或赋值,它的派生类部分将会被忽略掉。
    在这里插入图片描述
  7. 存在继承关系的类型之间的转换规则
  • 从派生类向基类的类型转换只针对指针或引用有效。
  • 基类向派生类不存在隐式类型转换
  • 和任何其他成员一样,派生类向基类的类型转换也可能会由于访问受限而变得不可行。

15.3 虚函数

  1. 我们必须定义而不只能声明虚函数。引用或指针的静态类型与动态类型不同这一事实是c++语言支持多态性的根本所在
  2. 一个函数在基类中被声明为虚函数,那么在派生类override覆盖基类的这个函数时可以不加virtual关键字,因为一个函数一旦被声明为虚函数时,那么它在所有派生类中也是虚函数。
  3. 一个派生类的函数如果覆盖了基类的虚函数,那么这个函数的参数和返回类型必须和虚函数一致。当派生类覆盖基类的函数形参或者返回类型与基类的虚函数不一致时,这个函数被编译器认为是派生类重新自己定义的新的函数,而不被认为是覆盖掉基类的虚函数。如果我们在实际变成中发生这种错误是很难发现的,在测试时是在派生类的函数中添加了override关键字发现的,这也是c++11所添加它的原因。
  4. 但存在一个例外,就是返回类型如果是类本身的指针或引用时,这个规则无效。就是基类可以返回base或base&,派生类可以返回dervie或derive&
  5. c++11新标准中我们可以使用override(覆盖)关键字来说明派生类中的虚函数。这么作的好处是使得程序员的意图更加清晰的同时让编译器可以为我们发现一些错误,后者在编程时间中显的更加重要,如果我们使用override标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错。
  6. final可以跟在类后面说明不许被继承或者跟在虚函数后面说明不许后继的类继承。
  7. final和override说明符出现在形参列表(包括任何const或引用修饰符)以及尾置返回类型之后。
    在这里插入图片描述
    在这里插入图片描述
  8. 如果我们在基类的虚函数中存在默认实参,那么当我们通过引用或指针来调用时使用的是基类的默认实参,即使调用的是派生类的虚函数也是如此。所以在定义默认实参的时候,基类和派生类的默认实参最好一致。
  9. 在某些情况下我们不需要动态绑定,而是强迫执行虚函数的某个特定版本。通过作用域操作符可以实现这个目的。通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数机制。
  10. 有种情况,基类的虚函数是抽象出来所有继承层次都要完成的操作,派生类只是在基类的虚函数操作中在添加一些额外的操作,那么我们就可以在通过作用域操作符。在派生类的虚函数中调用基类版本,但是必须要通过作用域操作符,不然就会引起无限递归。(课后题15.11 为上面定义的quote类体系增加一个名为debug的虚函数,令其分别显示每个类的数据成员。写时开始定义每个类的debug函数然后输出数据成员,但是派生类是不能直接输出基类的private成员的,虽然它继承了。只能通过作用域操作符来调用基类的debug函数然后在输出派生类新增加的数据成员。)
// 强制调用基类中定义的函数版本而不管baseP的动态类型到底是什么
double undiscounted = baseP->Quote::net::price(42);

15.4 抽象基类

  1. 可以新定义一个类disc_quote来继承quote基类,它含有两个数据成员,一个是discount折扣,一个是num数量,我们可以让本来继承quote的派生类来继承新定义的disc_quote类,这样就不用每个派生类都定义一遍属于自己的discount和num成员。
  2. 我们可以将disc_quote中的net_price定义为一个纯虚函数来实现。这样可以清晰的告诉用户当前这个net_price没有实际意义的。和普通虚函数不一样。一个纯虚函数无需定义,我们通过在函数体的位置后面加上=0 即可。就可以将一个虚函数变为纯虚函数。其中=0只能出现在类内部的虚函数声明语句处。注意:我们也可以为虚函数提供定义,不过函数体必须定义在类的外部,也就是说我们不能在类的内部为一个=0的函数提供函数体
  3. 含有(未经覆盖直接继承)纯虚函数的类是抽象基类,抽象基类负责定义接口,而后续的其他类可以覆盖接口,我们不能直接创建一个抽象基类的对象。我们可以定义抽象基类的派生类对象,前提是覆盖了纯虚函数的接口。
    在这里插入图片描述
    在这里插入图片描述
  4. 派生类构造函数只初始化它的直接基类。那么我们定义bulk_quote的构造函数时只能初始化disc_quote,在由disc_quote的构造函数来初始化quote的成员。
    在这里插入图片描述
    在这里插入图片描述
  5. 重构负责重新设计类的体系以便将操作和/或数据从一个类移动到另一个类中,对于面向对象的应用程序来说,重构是一种很普遍的现象。
#include <iostream>
#include <string>
 
// 练习,定义自己的disc_quote和bulk_quote
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;
        disc_quote(std::string &book, double p, double disc, std::size_t num):   //构造函数初始化quote类时必须调用quote类的构造函数
            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. 注意:包含纯虚函数的类是抽象基类,我们不能实例化抽象基类,如果派生类没有覆盖掉纯虚函数,那么派生类也是抽象类,也不能实例化,也就是说如果我们在派生类中实例化抽象基类,必须要覆盖纯虚函数。

15.5 访问控制与继承

15.5.1 受保护的成员

  1. 一个类使用protected关键字来说明哪些它希望与派生类 分享但那是不想被其他公共访问使用的成员。
  • 和私有成员类似,受保护的成员对于类的用户来说是不可访问的。
  • 和公有成员类似,受保护的成员对于派生类的成员和友元来说是可访问的。
  1. 派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问权限。
    在这里插入图片描述

15.5.2 公有、私有和受保护继承

  1. 某个类继承基类的成员访问权限受到两方面限制,一方面是基类内部该成员的访问权限,另一方面是派生类的继承方式。
  2. 公有继承(public)、私有继承(private)、保护继承(protected)是常用的三种继承方式。
  • 公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类所访问。
  • 私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。
  • 保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。
  1. 派生类访问说明符目的是控制派生类用户对基类的访问权限。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

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

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

15.5.4 友元与继承

  1. 友元关系不能传递,友元关系也不能继承。
  2. 基类的友元在访问派生类成员时不具有特殊性,派生类的友元也不能随便访问基类的成员。
    在这里插入图片描述
    在这里插入图片描述

15.5.5 改变个别成员的可访问性

  1. 通过在类的内部使用using 声明语句,我们可以将该基类的直接或间接基类中的任何可访问成员标记出来,using 声明语句中名字的访问权限由它所在的访问说明符来决定。
  • 如果一条using 声明语句出现在类的private部分,则该名字只能由类的成员和友元来访问。
  • 如果一条using 声明语句出现在类的protect部分,则该名字能由类的成员和友元和派生类来访问。
  • 如果一条using 声明语句出现在类的public部分,则该名字能由类的所有用户访问。
  1. 注意:派生类只能为它可以访问的名字提供using 声明。
class Base {
public:
	std::size_t size() const { return n; }
protected:
	std::size_t n;
};
class Derived : private Base {  // 注意:private继承
public:
	// 保持对象尺寸相关的成员的访问权限
	using Base::size;
protected:
	using Base::n;
};

15.5.6 默认的继承保护级别

  1. class定义的派生类默认是private继承
  2. struct 定义的派生类默认是 public 继承

15.6 继承中的类作用域

  1. 每个类定义自己的作用域,在这个作用域内我们定义类的成员,当存在继承关系时,派生类的作用域嵌套在其基类作用域之内。如果一个名字在自己的作用域内无法解析,那么就会在基类中查找。
  2. 在编译时进行名字查找,一个对象,引用或指针的静态类型,决定了该对象的哪些成员是可见的。即使静态类型与动态类型可能不一致(当使用基类的引用或指针时会发生这种情况),但是我们能使用哪些成员仍然是由静态类型决定的。
    在这里插入图片描述
    在这里插入图片描述
  3. 派生类的成员将隐藏同名的基类成员。可以使用作用域运算符来使用一个被隐藏的基类成员。
    在这里插入图片描述
  4. 不同作用域不能进行函数重载,只能进行函数覆盖。
    在这里插入图片描述
  5. 必须是基类的虚函数。派生类为实现多态覆盖掉基类的虚函数。必须是虚函数
  6. 两种函数隐藏的情况:
  • 派生类的函数与基类的函数完全相同函数名和参数列表都相同只是基类的函数没有使用virtual关键字此时基类的函数将被隐藏而不是覆盖
  • 派生类的函数与基类的函数同名但参数列表不同在这种情况下不管基类的函数声明是否有virtual关键字基类的函数都将被隐藏注意这种情况与函数重载的区别重载发生在同一个类中。
  1. 覆盖就是覆盖掉了,访问不了了,隐藏是必须通过域操作符才能访问,一般是虚函数被重写是覆盖,其他的是隐藏。
    在这里插入图片描述
    在这里插入图片描述
  2. 派生类可以覆盖重载函数的0个或多个实例,如果派生类希望所有的重载版本对于它来说式可见的,那么它就需要覆盖所有的版本,或者一个也不覆盖。有时一个类仅需要覆盖重载集合中的一些而不是全部函数,可以使用using声明,这样派生类只需要定义其特有的函数就可以了。

15.7 构造函数与拷贝控制

15.7.1 虚析构函数

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

15.7.2 合成拷贝控制与继承

  1. 简单的记忆就是基类永远会影响派生类,因为派生类继承基类,如果基类拷贝控制部分操作有问题,那么派生类也会有。比如:基类的拷贝构造定义为删除的,那么派生类也必定为删除的,因为派生类中的拷贝构造必须要调用基类的拷贝构造来初始化基类部分,基类定义为删除的,说明派生类不能构造基类部分,那么派生类也就为删除的了。
  2. 对于派生类的析构函数来说,他除了销毁派生类自己的成员外,还负责销毁派生类的直接基类,该直接基类又销毁它自己的直接基类,以此类推直至继承链的顶端。
  3. Quote因为定义了析构函数而不能拥有合成的移动操作,因此当我们移动Quote对象时实际上使用的是合成拷贝操作。
class B{
public:
	B();
	B(const B&) = default;
	// 其他成员,不含有移动构造函数
};
class D : public B {
	// 没有声明任何构造函数
};
D d;                     // 正确:D的合成默认构造函数使用B的默认构造函数
D d2(d);                 // 错误:D的合成构造函数是被删除的
D d3(std::move(d));      // 错误:隐式地使用D的被删除的拷贝构造函数
  1. 当我们确实需要执行移动操作时,应该首先在基类中定义,我们的Quote可以使用合成的版本,但是必须显式地定义这些成员。一旦Quote定义了自己的移动操作,就必须同时显示地定义拷贝操作。
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.3 派生类的拷贝控制成员

  1. 派生类构造函数在其初始化阶段中不但要初始化派生类自己的成员,还要负责初始化派生类对象的基类部分,因此,派生类的拷贝和移动构造函数在拷贝和移动自有成员的同时,也要拷贝和移动基类部分的成员,类似的派生类赋值运算符也必须为其基类部分的成员赋值。
  2. 和构造函数及赋值运算符不同的是,析构函数只负责销毁派生类自己分配的资源。
    在这里插入图片描述
  3. 派生类赋值运算符也必须显式调用。
    在这里插入图片描述
  4. 析构函数负责销毁由派生类自己分配的资源。基类的析构函数被自动调用。对象销毁顺序正好相反,派生类析构函数首先执行,然后是基类的析构函数,以此类推。
    在这里插入图片描述
  5. 如果构造函数或析构函数调用了某个虚函数,则我们会执行与构造函数或析构函数所属类型相对应的虚函数版本。

15.7.4 继承的构造函数

  1. 在c++11 新标准中,派生类能够重用直接基类定义的构造函数。派生类继承基类构造函数的方式是提供一条注明了基类名的using声明语句。using声明语句将令编译器产生代码,对于基类的每个构造函数,编译器都生成与之对应的派生类构造函数。
    在这里插入图片描述
  2. 如果派生类含有自己的数据成员,则这些成员将被默认初始化。
Bulk_quote(const std::string& book, double price, std::size_t qty, double disc):
	Disc_quote(book, price, qty, disc) {}
  1. 和普通成员的using声明不一样,一个构造函数的using声明不会改变该构造函数的访问级别,不管using声明出现在哪,基类的私有构造函数在派生类中还是一个私有构造函数;受保护的构造函数和公有构造函数也是同样的规则。同时,一个using不能指定explicit或constexpr,与基类构造函数含有相同的属性。
  2. 当基类构造函数含有默认实参,这些实参不会被继承,派生类将获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认形参的实参。
  3. 派生类会继承基类的所有构造函数,但是派生类可以定义自己的版本,如果有相同的参数列表,则该构造函数就不会被继承,定义在派生类中的构造函数将替换继承而来的构造函数。
  4. 默认、拷贝和移动构造函数不会被继承。如果一个类只含有继承的构造函数,则它也将拥有一个合成的默认构造函数。

15.8 容器与继承

15.8.1 编写Basket类

  1. 当我们使用容器存放继承体系中的对象时,通常必须采用简介存储的方式,因为不允许在容器中保存不同类型的元素,所以我们不能把具有继承关系的多种类型的对象直接存放在容器中。
  2. 注意:当派生类对象被赋值给基类对象时,其中的派生类部分将被切掉,因此容器和存在继承关系的类型无法兼容。
  3. 在容器中放置(智能)指针而非对象,当我们希望在容器中存放具有继承关系的对象时,我们实际上存放的通常是基类的指针,更好的是存放智能指针,和往常一样,这些指针所指对象的动态类型可能是基类类型,也可能是派生类类型。
  4. 正如我们可以把派生类的指针转换为基类的指针一样,我们也可以把派生类的智能指针转换为基类的智能指针。
std::vector<std::shared<quote>>basket;

15.9 文本查询程序再探

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值