继承和动态绑定 对程序的编写 有两方面的影响:一是 我们可以更容易地定义与其他类相似 但不完全相同的新类;二是 在使用这些彼此相似的类编写程序时,我们可以在一定程度上 忽略掉它们的区别
在很多程序中都存在着一些相互关联 但是有细微差别的概念。面向对象的程序设计(OOP)适用于这类应用
1、OOP:概述
1、面向对象程序设计的核心思想是 数据抽象、继承 和 动态绑定。通过 使用数据抽象,我们可以将类的接口与实现分离;使用继承,可以定义相似的类型并对其相似关系建模;使用动态绑定,可以在一定程度上 忽略相似类型的区别,而以统一的方式使用它们的对象
1.1 继承
1、通过继承 联系在一起的类构成一种层次关系。通常在层次关系的根部 有一个基类,其他类 则直接或间接地从基类继承而来,这些继承得到的类 称为派生类。基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类 定义各自特有的成员
2、对书的不同定价策略建模,我们首先定义一个名为Quote的类,并将它作为 层次关系中的基类。Quote的对象 表示按原价销售的书籍。Quote派生出另一个名为Bulk_quote的类,它表示 可以打折销售的书籍
这些类 将包含下面的两个成员函数:
- isbn(),返回书籍的ISBN编号。该操作不涉及派生类的特殊性,因此只定义在Quote类中
- net_price(size_t),返回书籍的实际销售价格,前提是 用户购买该书的数量达到一定标准。这个操作显然是类型相关的,Quote 和 Bulk_quote都应该包含该函数
基类 将类型相关的函数 与派生类不做改变直接继承的函数 区分对待。对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类 就将这些函数声明成虚函数。可以将Quote类编写成
class Quote {
public:std::string isbn() const;virtual double net_price(std::size_t n) const;
};
派生类 必须通过 使用类派生列表 明确指出它是从哪个(哪些)基类继承而来的。类派生列表的形式是:首先是一个冒号,后面紧跟以逗号分隔的基类列表,其中每个基类前面可以有 访问说明符:
class Bulk_quote : public Quote { //Bulk_quote继承了Quote
public:double net_price(std::size_t) const override;
};
因为 Bulk_quote 在它的派生列表中 使用了public关键字,因此我们完全可以把 Bulk_quote 的对象 当成Quote的对象来使用
派生类 必须在其内部 对所有重新定义的虚函数进行声明。派生类 可以在这样的函数之前加上virtual关键字,但是并不是非得这么做。C++11新标准 允许派生类显式地注明它将使用 哪个成员函数改写基类的虚函数,具体措施是 在该函数的形参列表之后增加一个override关键字
1.2 动态绑定
1、通过使用动态绑定,能用同一段代码 分别处理 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() //调用Quote::isbn<< " # sold: " << n << "total due: " << ret << endl;return ret;
}
因为函数 print_total 的item形参是 基类Quote的一个引用,既能使用 基类Quote的对象调用该函数,也能 使用派生类 Bulk_quote 的对象调用它;又因为print_total是使用引用类型 调用net_price函数的,实际传入 print_total 的对象类型 将决定到底执行net_price的哪个版本:
//basic的类型是Quote;bulk的类型是Bulk_quote
print_total(cout, basic, 20); //调用Quote的net_price
print_total(cout, bulk, 20); //调用Bulk_quote的net_price
因为在上述过程中 函数的运行版本 由实参决定,即在运行时 选择函数的版本,所以动态绑定 有时又被称为 运行时绑定
当我们使用基类的引用(或指针)调用一个虚函数时 将发生动态绑定
2、定义基类和派生类
2.1 定义基类
1、首先完成 Quote类 的定义:
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; //代表普通状态下不打折的价格
};
新增的部分是在 net_price函数和析构函数之前 增加的virtual关键字 以及 最后的protected访问说明符
基类通常都应该定义一个 虚析构函数,即使 该函数不执行任何实际操作也是如此
2、成员函数与继承:派生类 可以继承其基类的成员,然而 当遇到如net_price这样与类型相关的操作时,派生类 必须对其重新定义。换句话说,派生类 需要对这些操作提供自己的新定义以覆盖(override)从基类继承而来的旧定义
基类 必须将它的两种成员函数区分开来:一种是 基类希望其派生类 进行覆盖的函数:另一种是 基类希望派生类 直接继承而不要改变的函数。对于前者,基类通常将其定义为虚函数(virtual)。当我们使用指针 或 引用调用虚函数时,该调用 将被动态绑定。根据 引用或指针所绑定的对象类型不同,该调用 可能执行基类的版本,也可能 执行某个派生类的版本
基类 通过在其成员函数的声明语句之前 加上关键字virtual 使得该函数执行动态绑定。任何构造函数之外的非静态函数 都可以是虚函数。关键字virtual 只能出现在类内部的声明语句之前 而不能用于类外部的函数定义。如果 基类把一个函数声明成虚函数,则该函数在派生类中 隐式地也是虚函数(之前派生类 可以在这样的函数之前加上virtual关键字,但是并不是非得这么做的原因)
成员函数 如果没被声明为虚函数,则其解析过程发生在 编译时而非运行时。对于isbn成员来说 这正是我们希望看到的结果。isbn函数的执行与派生类的细节无关,在我们的继承层次关系中 只有一个isbn函数,因此 也就不存在 调用isbn()时 到底执行哪个版本的疑问
3、访问控制与继承:派生类 可以继承定义在基类中的成员,但是派生类的成员函数 不一定有权访问 从基类继承而来的成员。和其他使用基类的代码一样,派生类 能访问公有成员,而不能 访问私有成员。不过在某些时候基类中还有这样一种成员,基类 希望它的派生类 有权访问该成员,同时禁止其他用户访问。用受保护的(protected)访问运算符 说明这样的成员
Quote类 希望它的派生类定义 各自的net_price函数,因此派生类 需要访问Quote的price成员。此时我们将price定义成受保护的。与之相反,派生类访问 bookNo成员的方式 与 其他用户是一样的,都是通过调用isbn函数,因此bookNo被定义成 私有的,即使是Quote派生出来的类 也不能直接访问它
4、定义 Quote 类和 print_total 函数
Quote.h
#pragma once
#ifndef QUOTE_H
#define QUOTE_H#include <string>
#include <iostream>class Quote {
public:Quote() = default;Quote(const std::string& book, const double p) : bookNo(book), price(p) {}std::string isbn() const { return bookNo; }virtual double net_price(std::size_t n) const {return n * price;}virtual ~Quote() = default;// 15.11 加入虚函数,virtual不需要再在类外再写一遍virtual void debug() const;
private:std::string bookNo;
protected:double price = 0.0;
};double print_total(std::ostream& os, const Quote& quote, std::size_t n) {double ret = quote.net_price(n); // 通过对象调用类内函数os << "ISBN:" << quote.isbn() << " " << "sold_num:" << n << " " << "total:" << ret << std::endl;return ret;
}void Quote::debug() const {std::cout << "bookNo: " << bookNo << " " << "price: " << price << std::endl;
}#endif
2.2 定义派生类
1、派生类必须通过 使用类派生列表 明确指出它是从哪个(哪些)基类继承而来的。首先是一个冒号,后面 紧跟以逗号分隔的基类列表,其中每个基类前面 可以有以下三种访问说明符中的一个:public、protected 或者 private
Bulk_quote类必须包含一个net_price成员:
class Bulk_quote : public Quote { //Bulk_quote继承自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; //以小数表示的折扣额
};
Bulk_quote类 从它的基类Quote那里继承了isbn函数 和 bookNo、price等数据成员。此外,它还定义了net_price的新版本,同时拥有两个新增加的数据成员 min_qty 和 discount
如果一个派生 是公有的,则基类的公有成员 也是派生类接口的组成部分。此外,我们能 将公有派生类型的对象绑定到基类的引用或指针上。因为 在派生列表中使用了public,所以Bulk_quote的接口 隐式地包含isbn函数,同时在任何需要Quote的引用 或 指针的地方 都能使用Bulk_quote的对象
大多数类 都只继承自一个类,这种形式的继承被称作“单继承”
2、派生类中的虚函数:派生类经常(但不总是)覆盖它继承的虚函数。如果派生类 没有覆盖其基类中的某个虚函数,派生类会直接继承其在基类中的版本
派生类 可以在它覆盖的函数前 使用virtual关键字,但不是非得这么做。C++11新标准 允许派生类 显式地注明它使用某个成员函
数覆盖了它继承的虚函数。具体做法 是在形参列表后面、或者 在const成员函数的const关键字后面、或者在引用成员函数的引用限定符 后面添加一个关键字override
引用限定符 允许成员函数根据对象的左值或右值引用调用具有不同的行为。引用限定符放在成员函数的参数列表之后,使用&表示左值引用,使用&&表示右值引用
class MyClass {
public:void memberFunction() & {// 左值引用版本}void memberFunction() && {// 右值引用版本}
};int main() {MyClass obj1;obj1.memberFunction(); // 调用左值引用版本MyClass().memberFunction(); // 调用右值引用版本return 0;
}
当对象是左值时,编译器会调用左值引用版本,而当对象是右值时,则会调用右值引用版本
obj1是一个左值,临时创建的MyClass对象是一个右值
3、派生类对象 及 派生类向基类的类型转换:一个派生类对象 包含多个组成部分:一个含有派生类自己定义的(非静态)成员的子对象,以及 一个与该派生类继承的基类对应的子对象,如果 有多个基类,那么这样的子对象也有多个。因此,一个Bulk_quote对象 将包含四个数据元素:它从Quote继承而来的 bookNo 和 price 数据成员,以及 Bulk_quote 自己定义的 min_qty 和 discount 成员
因为在派生类对象中含有与其基类对应的组成部分,所以 能把派生类的对象 当成基类对象来使用,而且 也能将基类的指针 或 引用绑定到派生类对象中的基类部分上
Quote item; //基类对象
Bulk_quote bulk; //派生类对象
Quote *p = &item; //p指向Quote对象
p = &bulk; //指向bulk的Quote部分
Quote &r = bulk; //r绑定到bulk的Quote部分
这种转换通常称为 派生类到基类的类型转换。和其他类型转换一样,编译器会隐式地执行派生类到基类的转换
可以 把派生类对象或者派生类对象的引用 用在需要基类引用的地方:同样的,也可以 把派生类对象的指针 用在需要基类指针的地方
在派生类对象中 含有与其基类对应的组成部分,这一事实是继承的关键所在
C++标准 并没有明确规定派生类的对象 在内存中如何分布
4、派生类构造函数:尽管在派生类对象中 含有从基类继承而来的成员,但是派生类 并不能直接初始化这些成员。派生类也必须使用基类的构造函数 来初始化它的基类部分
每个类控制它自己的成员初始化过程
5、派生类对象的基类部分与派生类对象自己的数据成员 都是在构造函数的初始化阶段 执行初始化操作的。派生类构
造函数 同样是通过构造函数初始化列表来将实参传递给基类构造函数的
Bulk_quote(const std::string& book, double p,std::size_t qty, double disc) :Quote(book, p), min_qty(qty), discount(disc) { }//与之前一致
};
该函数 将它的前两个参数(分别表示ISBN和价格)传递给Quote的构造函数,由Quote的构造函数 负责初始化Bulk_quote的基类部分(即bookNo成员 和 price成员)
除非 特别指出,否则派生类对象的基类部分 会像数据成员一样执行默认初始化。如果想 使用其他的基类构造函数,我们需要以类名加圆括号内的实参列表的形式 为构造函数提供初始值
首先 初始化基类的部分,然后按照声明的顺序 依次初始化派生类的成员
6、派生类使用基类的成员:派生类可以访问基类的公有成员和受保护成员
//如果达到了购买书籍的某个最低限量值,就可以享受折扣价格了
double Bulk_quote::net_price(size_t cnt) const
{if (cnt >= min_qty)return cnt * (1 - discount) * price;elsereturn cnt * price;
派生类的作用域嵌套在 基类的作用域之内。对于派生类的一个成员来说,它使用派生类成员(例如 min_qty 和 discount)的方式与使用基类成员(例如price)的方式 没什么不同
7、遵循基类的接口:每个类 负责定义各自的接口。派生类对象 不能直接初始化基类的成员。尽管从语法上来说 可以在派生类构造函数体内 给它的公有或受保护的基类成员赋值,但是最好不要这么做。派生类 应该遵循基类的接口,并且通过调用基类的构造函数 来初始化 那些从基类中继承而来的成员
8、继承与静态成员:如果基类 定义了一个静态成员,则在整个继承体系中只存在 该成员的唯一定义。不论 从基类中派生出来多少个派生类,对于每个静态成员来说都 只存在唯一的实例
class Base {
public:static void statmem();
};
class Derived : public Base {void f(const Derived&);
};
静态成员 遵循通用的访问控制规则,如果基类中的成员是private的,则派生类无权访问它。假设 某静态成员是可访问的,则我们 既能通过基类使用它 也能通过派生类使用它
void Derived::f(const Derived &derived_obj)
{Base::statmem(); //正确:Base定义了statmemDerived::statmem();//正确:Derived继承了statmem//正确:派生类的对象能访问基类的静态成员derived_obj.statmem(); //通过Derived对象访问statmem(); //通过this对象访问
};
9、派生类的声明:派生类声明中包含类名 但是不包含它的派生列表:
class Bulk_quote : public Quote; //错误:派生列表不能出现在这里
class Bulk quote; //正确:声明派生类的正确方式
一条声明语句的目的是 令程序知晓某个名字的存在 以及 该名字表示一个什么样的实体,如一个类、一个函数或一个变量等。派生列表 以及 与定义有关的其他细节必须与类的主体一起出现
10、被用作基类的类:如果 想将某个类用作基类,则该类 必须已经定义而非仅仅声明
class Quote; //声明但未定义
//错误:Quote必须被定义
class Bulk_quote : public Quote { ... };
原因:派生类中包含并且可以使用它从基类继承而来的成员,为了使用这些成员,派生类当然要知道它们是什么
该规定还有一层隐含的意思,即一个类不能派生它本身
一个类是基类,同时 它也可以是一个派生类
class Base { /*...*/ };
class D1 : public Base { /*...*/ };
class D2 : public D1 { /*...*/ };
在这个继承关系中,Base是D1的 直接基类,同时是D2的 间接基类。直接基类 出现在派生列表中,而间接基类 由派生类通过其直接基类继承而来
最终的派生类 将包含它的直接基类的子对象 以及 每个间接基类的子对象
11、防止继承的发生:不希望 其他类继承它,或者 不想考虑它是否适合作为一个基类。C++11新标准 提供了一种防止继承发生的方法,即在类名后跟一个关键字final
class NoDerived final { /* */ }; //NoDerived不能作为基类
class Base { /* */ };
//Last是final的;我们不能继承Last
class Last final : Base { /* */ }; //Last不能作为基类
class Bad : NoDerived { /* */ }; //错误:NoDerived是final的
class Bad2 : Last { /* */ }; //错误:Last是final的
12、
1)定义你自己的 Bulk_quote 类
Bulk_quote.h
#pragma once
#ifndef BULK_QUOTE_H
#define BULK_QUOTE_H#include "Quote.h"class Bulk_quote:public Quote {
public:Bulk_quote() = default;Bulk_quote(const std::string&, double, std::size_t, double); // 不改变的加const,这样可以传入常量double net_price(std::size_t) const override; // 15.11 加入虚函数,override不需要再在类外再写一遍void debug() const override;
private:std::size_t min_num = 0;double discount = 0.0;
};Bulk_quote::Bulk_quote(const std::string& book, double n, std::size_t mn, double dis):Quote(book, n), min_num(mn), discount(dis) {}double Bulk_quote::net_price(std::size_t n) const {if (n >= min_num)return n * (1 - discount) * price;elsereturn n * price;
}void Bulk_quote::debug() const {std::cout << "min_num: " << min_num << " " << "discount: " << discount << std::endl;
}
#endif
2)将 Quote 和 Bulk_quote 的对象传给 print_total 函数
Quote.h
#pragma once
#ifndef QUOTE_H
#define QUOTE_H#include <string>
#include <iostream>class Quote {
public:Quote() = default;Quote(const std::string& book, const double p) : bookNo(book), price(p) {}std::string isbn() const { return bookNo; }virtual double net_price(std::size_t n) const {return n * price;}virtual ~Quote() = default;// 15.11 加入虚函数,virtual不需要再在类外再写一遍virtual void debug() const;
private:std::string bookNo;
protected:double price = 0.0;
};double print_total(std::ostream& os, const Quote& quote, std::size_t n) {double ret = quote.net_price(n); // 通过对象调用类内函数os << "ISBN:" << quote.isbn() << " " << "sold_num:" << n << " " << "total:" << ret << std::endl;return ret;
}void Quote::debug() const {std::cout << "bookNo: " << bookNo << " " << "price: " << price << std::endl;
}#endif
15.6.cpp
#include "Bulk_quote.h"
// 只要包含这一个就两个头文件就包含了using namespace std;int main()
{Quote quo("123", 9.9);print_total(cout, quo, 10);Bulk_quote bquo("124", 9.9, 5, 0.2);print_total(cout, bquo, 10); // 动态绑定return 0;
}
运行结果
3)定义一个类使其实现一种数量受限的折扣策略,具体策略是:当购买书籍的数量不超过一个给定的限量时享受折扣,如果购买量一旦超过了限量,则超出的部分将以原价销售
Limited_quote.h
#pragma once
#ifndef LIMITED_QUOTE_H
#define LIMITED_QUOTE_H#include "Quote.h"class Limited_quote : public Quote{
public:Limited_quote() = default;Limited_quote(const std::string&, double, std::size_t, double);double net_price(std::size_t) const override; // const不加就直接调用基类了,不加override没事,但是加了如果没加const会报错
private:std::size_t n = 0;double discount = 0.0;
};Limited_quote::Limited_quote(const std::string& s, double price, size_t nn, double dis) : Quote(s, price), n(nn),discount(dis) {}double Limited_quote::net_price(std::size_t num) const {if (num <= n) {return num * (1 - discount) * price;}else {return (num - n) * price + n * price * (1 - discount);}
}#endif
15.7.cpp
#include "Limited_quote.h"using namespace std;int main()
{Quote quo("123", 9.9);print_total(cout, quo, 10);Limited_quote bquo("124", 9.9, 5, 0.2);print_total(cout, bquo, 10); // 动态绑定return 0;
}
运行结果
2.3 类型转换与继承
1、通常情况下,如果 想把引用或指针绑定到一个对象上,则引用或指针的类型 应与对象的类型一致,或者 对象的类型含有一
个可接受的const类型转换规则。存在继承关系的类 是一个重要的例外:可以将基类的指针或引用 绑定到派生类对象上。例如,可以用 Quote& 指向一个 Bulk_quote 对象,也可以 把一个 Bulk_quote 对象的地址赋给一个 Quote*
当使用基类的引用(或指针)时,实际上 并不清楚该引用(或指针)所绑定对象的真实类型。该对象 可能是基类的对象,也可能是派生类的对象
和内置指针一样,智能指针类 也支持派生类 向基类的类型转换,这意味着 可以将一个派生类对象的指针 存储在一个基类的智
能指针内
2、静态类型与动态类型:当使用存在继承关系的类型时,必须将 一个变量或其他表达式的静态类型 与 该表达式表示对象的动态类型 区分开来。表达式的静态类型 在编译时总是已知的,它是变量声明时的类型 或 表达式生成的类型;动态类型 则是变量 或 表达式表示的内存中的对象的类型。动态类型 直到运行时才可知
// 当print_total调用net_price时
double ret = item.net_price(n);
知道item的静态类型是 Quote&(函数参数),它的动态类型 则依赖于item绑定的实参,动态类型直到在运行时调用该函数时才会知道。如果我们传递一个 Bulk_quote对象给 print_total,则item的静态类型 将与它的动态类型不一致
如果 表达式既不是引用 也不是指针,则它的动态类型 永远与静态类型一致。例如:Quote类型的变量 永远是 一个Quote对象
3、不存在 从基类向派生类的隐式类型转换:之所以 存在派生类向基类的类型转换 是因为每个派生类对象都包含一个基类部分,而基类的引用或指针 可以绑定到该基类部分上
因为 一个基类的对象可能是派生类对象的一部分,也可能不是,所以 不存在从基类向派生类的自动类型转换:
Quote base;
Bulk_quote* bulkP = &base; //错误:不能将基类转换成派生类
Bulk_quote& bulkRef = base; //错误:不能将基类转换成派生类
如果上述赋值是合法的,则我们有可能会使用bulkP或bulkRef访问base中本不存在的成员
即使 一个基类指针或引用绑定在一个派生类对象上,也不能执行从基类向派生类的转换
Bulk_quote bulk;
Quote *itemP = &bulk; //正确:动态类型是Bulk_quote
Bulk_quote *bulkP = itemP; //错误:不能将基类转换成派生类
编译器 在编译时无法确定某个特定的转换 在运行时是否安全,这是 因为编译器只能通过检查指针 或 引用的静态类型来推断该转换是否合法。如果 在基类中含有一个或多个虚函数,我们可以使用 dynamic_cast 请求一个类型转换,该转换的安全检查 将在运行时执行。同样,如果 已知某个基类向派生类的转换是安全的,则可以使用static_cast来强制覆盖掉编译器的检查工作
// 如果 在基类中含有一个或多个虚函数,我们可以使用 dynamic_cast 请求一个类型转换,该转换的安全检查 将在运行时执行
#include <iostream>class Shape {
public:virtual void draw() {std::cout << "Drawing a shape" << std::endl;}virtual ~Shape() {} // 虚析构函数,确保子类的析构函数被正确调用
};class Circle : public Shape {
public:void draw() override {std::cout << "Drawing a circle" << std::endl;}
};class Rectangle : public Shape {
public:void draw() override {std::cout << "Drawing a rectangle" << std::endl;}
};// 有一个指向Shape的指针,我们想要在运行时确定它指向的是哪种具体的形状(圆形还是矩形)。我们可以使用dynamic_cast进行类型转换
int main() {Shape* shape1 = new Circle();Shape* shape2 = new Rectangle();Circle* circle = dynamic_cast<Circle*>(shape1);Rectangle* rectangle = dynamic_cast<Rectangle*>(shape2);if (circle) {std::cout << "shape1 points to a Circle" << std::endl;} else {std::cout << "shape1 does not point to a Circle" << std::endl;}if (rectangle) {std::cout << "shape2 points to a Rectangle" << std::endl;} else {std::cout << "shape2 does not point to a Rectangle" << std::endl;}delete shape1;delete shape2;return 0;
}
shape1指向一个Circle对象,而shape2指向一个Rectangle对象。通过dynamic_cast,我们尝试将它们转换为Circle和Rectangle。如果转换成功,则相应的指针将指向转换后的对象,否则将返回nullptr
dynamic_cast要求基类必须包含至少一个虚函数(即基类必须是多态的),以便进行运行时类型识别
static_cast是一种静态类型转换,它在编译时执行,不进行运行时的类型检查。如果在已知某个基类向派生类的转换是安全的情况下,可以使用static_cast来进行强制转换
static_cast用于各种显式转换,包括将基类指针或引用转换为派生类指针或引用,以及基本数据类型之间的转换(如int到float)
假设有一个基类Animal,它有两个派生类Dog和Cat,我们知道某个指针指向的Animal对象实际上是一个Dog对象。我们可以使用static_cast将指向Animal的指针转换为指向Dog的指针:
// 如果 已知某个基类向派生类的转换是安全的,则可以使用static_cast来强制覆盖掉编译器的检查工作
#include <iostream>class Animal {
public:virtual void makeSound() {std::cout << "Animal sound" << std::endl;}virtual ~Animal() {} // 虚析构函数,确保子类的析构函数被正确调用
};class Dog : public Animal {
public:void makeSound() override {std::cout << "Bark" << std::endl;}
};class Cat : public Animal {
public:void makeSound() override {std::cout << "Meow" << std::endl;}
};int main() {Animal* animal = new Dog(); // animal指向Dog对象Dog* dog = static_cast<Dog*>(animal); // 将Animal*转换为Dog*dog->makeSound(); // 调用Dog的makeSound函数delete animal;return 0;
}
animal是一个指向Animal对象的指针,但我们知道它实际上指向的是一个Dog对象。通过 static_cast,我们将 animal 转换为 Dog* 指针,然后调用 Dog类的makeSound() 函数。由于我们知道转换是安全的(即animal指向的确实是一个Dog对象),因此使用 static_cast 是合适的
4、在对象之间不存在类型转换:派生类向基类的自动类型转换 只对指针或引用类型有效,在派生类类型 和 基类类型之间不存在这样的转换
当 初始化 或 赋值一个类类型的对象时,实际上是 在调用某个函数。当执行初始化时,我们调用构造函数;当执行赋值操作时,我们 调用赋值运算符。这些成员 通常都包含一个参数,该参数的类型是 类类型的const版本的引用
这些成员 接受引用作为参数,所以派生类向基类的转换 允许我们给基类的拷贝移动操作 传递一个派生类的对象。这些操作 不是虚函数。当我们给基类的构造函数传递一个派生类对象时,实际运行的构造函数是 基类中定义的那个,显然 该构造函数只能处理基类自己的成员。类似的,该运算符 同样只能处理基类自己的成员
例如,书店类使用了 合成版本的拷贝和赋值操作,合成版本 会像其他类一样 逐成员地执行拷贝 或 赋值操作
Bulk_quote bulk; //派生类对象
Quote item(bulk); //使用Quote::Quote(const Quote&) 构造函数
item = bulk; //调用Quote::operator=(const Quote&)
当构造 item 时,运行 Quote的拷贝构造函数。该函数只能处理 bookNo 和 price 两个成员,它负责拷贝 bulk 中 Quote 部分的成员,同时忽略掉 bulk 中 Bulk_quote 部分的成员
类似的,对于 将bulk赋值给item的操作来说,只有bulk中Quote部分的成员被赋值给item
可以说bulk的Bulk_quote部分被切掉(sliced down)了
5、解释第8章第一节中将 ifstream 传递给 Sales_data 的read 函数的程序是如何工作的
istream &read(istream &is, Sales_data &item)
{double price = 0;is >> item.bookNo >> item.units_sold >> price;item.revenue = price * item.units_sold;return is;
}ifstream input(argv[1]);
Sales_data total; // 保存销售总额变量
if (read(input, total))
通常可以将一个派生类对象当作其基类对象来使用
类型 ifstream 继承自 istream 。因此,我们可以像使用 istream 对象一样来使用 ifstream 对象。也就是说,我们是如何使用 cin 的,就可以同样地使用这些类的对象。例如,可以对一个 ifstream 对象调用 getline,也可以使用 >> 从一个 ifstream 对象中读取数据
read 函数是 istream 的成员,但是 ifstream 是 istream 的派生类。因此,istream(基类)通过引用可以绑定到 ifstream(派生类)的对象上
6、存在继承关系的类型之间的转换规则
- 从派生类向基类的类型转换 只对指针或引用类型有效
- 基类向派生类 不存在隐式类型转换
- 和任何其他成员一样,派生类向基类的类型转换 也可能会由于访问受限而变得不可行
尽管 自动类型转换 只对指针或引用类型有效,但是继承体系中的大多数类 仍然(显式或隐式地)定义了 拷贝控制成员。因此,通常能够将一个派生类对象拷贝、移动 或 赋值给一个基类对象。不过需要注意的是,这种操作 只处理派生类对象的基类部分
3、虚函数
1、使用基类的引用 或 指针调用一个虚成员函数时 会执行动态绑定。因为 直到运行时 才能知道到底调用了哪个版本的虚函数,所以 所有虚函数都必须有定义。通常情况下,如果 不使用某个函数,则无须为该函数提供定义。但是 必须为每一个虚函数都提供定义,而不管 它是否被用到了,这是 因为连编译器也无法确定到底会使用哪个虚函数
2、对虚函数的调用 可能在运行时才被解析:被调用的函数 是与绑定到指针或引用上的对象的动态类型 相匹配的那一个
当我们通过一个具有普通类型(非引用非指针)的表达式调用虚函数时,在编译时就会 将调用的版本确定下来。例如,如果我们使用base调用net_price。可以改变base表示的对象的值(即内容),但是不会改变该对象的类型
3、C++的多态性
OOP的核心思想是多态性。我们把具有继承关系的多个类型 称为多态类型,因为我们能使用这些类型的 “多种形式” 而无须在意它们的差异。引用或指针的静态类型与动态类型不同,这一事实正是C++语言支持多态性的根本所在
使用基类的引用 或 指针调用基类中 定义的一个函数时,我们并不知道该函数真正作用的对象是什么类型
对非虚函数的调用 在编译时进行绑定。类似的 通过对象进行的函数(虚函数 或 非虚函数)调用也在编译时绑定。对象的类型是确定不变的,我们无论如何都不可能 令对象的动态类型与静态类型不一致。因此,通过对象进行的函数调用 将在编译时 绑定到该对象所属类中的函数版本上
当且仅当 对通过指针或引用调用虚函数时,才会 在运行时解析该调用,也只有在这种情况下 对象的动态类型才有可能与静态类型不同
4、派生类中的虚函数:在派生类中 覆盖了某个虚函数时,可以再一次使用 virtual关键字指出该函数的性质。然而这么做 并非必须,因为 一旦某个函数被声明成虚函数,则在所有派生类中它都是虚函数
一个派生类的函数 如果覆盖了某个继承而来的虚函数,则它的形参类型 必须与被它覆盖的基类函数完全一致
派生类中虚函数的返回类型 也必须与基类函数匹配。该规则存在一个例外,当类的虚函数返回类型 是类本身的指针或引用时,上述规则无效。也就是说,如果D由B派生得到,则基类的虚函数 可以返回B而派生类的对应函数可以返回D,只不过这样的返
回类型 要求从D到B的类型转换是可访问的
5、final和override说明符:派生类 如果定义了一个函数 与基类中虚函数的名字相同 但是形参列表不同,这仍然是合法的行为。编译器将认为 新定义的这个函数与基类中原有的函数 是相互独立的
在C++11新标准中 可以使用override关键字来说明 派生类中的虚函数。这么做的好处是 在使得程序员的意图更加清晰的同时 让编译器可以为我们发现一些错误,使用override标记了某个函数,但该函数 并没有覆盖已存在的虚函数,此时编译器将报错:
struct B {virtual void f1(int) const;virtual void f2();void f3();
};
struct D1 : B {void f1(int) const override; //正确:f1与基类中的f1匹配void f2(int) override; //错误:B没有形如f2(int)的函数 void f3() override; //错误:f3不是虚函数void f4() override; //错误:B没有名为f4的函数
};
还能把某个函数指定为final,则之后 任何尝试覆盖该函数的操作 都将引发错误
struct D2 : B {//从B继承f2()和f3(),覆盖f1(int)void f1(int) const final; //不允许后续的其他类覆盖f1(int)
};
struct D3 : D2 {void f2(); //正确:覆盖从间接基类B继承而来的f2void f1(int) const; //错误:D2已经将f2声明成final
};
final 和 override 说明符 出现在形参列表(包括任何const或引用修饰符)以及 尾置返回类型之后
6、虚函数与默认实参:虚函数也可以拥有默认实参。如果 通过基类的引用 或 指针调用函数,则使用基类中定义的默认实参,即使 实际运行的是派生类中的函数版本也是如此。此时,传入派生类函数的 将是基类函数定义的默认实参。如果派生类函数依赖不同的实参,则程序结果将与我们的预期不符
7、回避虚函数的机制:希望 对虚函数的调用 不要进行动态绑定,而是强迫其执行虚函数 的某个特定版本。使用作用域运算符 可以实现这一目的
//强行调用 基类中定义的函数版本 而不管baseP的动态类型到底是什么
double undiscounted = baseP->Quote::net_price(42);
强行调用 Quote 的 net_price 函数,而不管baseP实际指向的对象类型到底是什么。该调用 将在编译时完成解析
通常情况下,只有成员函数(或友元)中的代码 才需要使用作用域运算符 来回避虚函数的机制
什么时候 需要回避虚函数的默认机制呢?通常是 当一个派生类的虚函数调用 它覆盖的基类的虚函数版本时。在此情况下,基类的版本 通常完成继承层次中所有类型都要做的共同任务,而派生类中定义的版本 需要执行一些与派生类本身密切相关的操作
如果一个派生类虚函数 需要调用它的基类版本,但是没有使用 作用域运算符,则在运行时 该调用将被解析为 对派生类版本自身的调用,从而导致无限递归
假设我们有一个基类 Base 和一个派生类 Derived。在 Base 中有一个虚函数 virtual void func(),而在 Derived 中也有一个同名的虚函数,但是我们想要在 Derived 中调用 Base 版本的函数。如果我们没有使用作用域运算符,编译器会默认调用派生类自身的版本
#include <iostream>class Base {
public:virtual void func() {std::cout << "Base::func()" << std::endl;}
};class Derived : public Base {
public:// 虚函数,但没有使用作用域运算符virtual void func() {// 在这里调用 Base 版本的 func()func(); // 这里实际上调用的是 Derived::func(),会导致无限递归}
};int main() {Derived d;d.func(); // 此处会导致无限递归,因为 Derived::func() 中的调用被解析为对自身的调用return 0;
}
8、为 Quote 类体系添加一个名为 debug 的虚函数,令其分别显示每个类的数据成员
Quote.h
#pragma once
#ifndef QUOTE_H
#define QUOTE_H#include <string>
#include <iostream>class Quote {
public:Quote() = default;Quote(const std::string& book, const double p) : bookNo(book), price(p) {}std::string isbn() const { return bookNo; }virtual double net_price(std::size_t n) const {return n * price;}virtual ~Quote() = default;// 15.11 加入虚函数,virtual不需要再在类外再写一遍virtual void debug() const;
private:std::string bookNo;
protected:double price = 0.0;
};double print_total(std::ostream& os, const Quote& quote, std::size_t n) {double ret = quote.net_price(n); // 通过对象调用类内函数os << "ISBN:" << quote.isbn() << " " << "sold_num:" << n << " " << "total:" << ret << std::endl;return ret;
}void Quote::debug() const {std::cout << "bookNo: " << bookNo << " " << "price: " << price << std::endl;
}#endif
Bulk_quote.h
#pragma once
#ifndef QUOTE_H
#define QUOTE_H#include <string>
#include <iostream>class Quote {
public:Quote() = default;Quote(const std::string& book, const double p) : bookNo(book), price(p) {}std::string isbn() const { return bookNo; }virtual double net_price(std::size_t n) const {return n * price;}virtual ~Quote() = default;// 15.11 加入虚函数,virtual不需要再在类外再写一遍virtual void debug() const;
private:std::string bookNo;
protected:double price = 0.0;
};double print_total(std::ostream& os, const Quote& quote, std::size_t n) {double ret = quote.net_price(n); // 通过对象调用类内函数os << "ISBN:" << quote.isbn() << " " << "sold_num:" << n << " " << "total:" << ret << std::endl;return ret;
}void Quote::debug() const {std::cout << "bookNo: " << bookNo << " " << "price: " << price << std::endl;
}#endif
15.11.cpp
#include "Bulk_quote.h" using namespace std;int main()
{Quote quo("123", 9.9);print_total(cout, quo, 10);quo.debug();Bulk_quote bquo("124", 9.9, 5, 0.2);print_total(cout, bquo, 10); // 动态绑定bquo.debug();return 0;
}
运行结果
给定下面的类,解释每个 print 函数的机理
class base {
public:string name() { return basename;}virtual void print(ostream &os) { os << basename; }
private:string basename;
};class derived : public base {
public:void print(ostream &os) { print(os); os << " " << i; }
private:int i;
};
派生类 derived 中的 print 函数体中想调用基类 base 中的虚函数 print。然而,在派生类 derived 中的 print 函数体中却忽略了作用域运算符 :: ,这样做的结果是该 print 调用将被解析为对派生类 derived 的 print 函数自身的调用,从而导致无限递归
derived 的成员函数 print 修改为:
void print(ostream &os) { base::print(os); os << " " << i; }
在运行时调用哪个函数:
base bobj; base *bp1 = &bobj; base &br1 = bobj;
derived dobj; base *bp2 = &dobj; base &br2 = dobj;(c)bp1->name(); (d)bp2->name(); (e)br1.print(); (f)br2.print();
(c)调用base中的name函数,不是虚函数,没有动态绑定,所以编译时确定
(d)调用base中的name函数,不是虚函数,没有动态绑定,所以编译时确定
(e)调用base中的print,运行时确定
(f)调用derived中的print,运行时确定
4、抽象基类
1、希望扩展书店程序 并令其支持几种不同的折扣策略。除了购买量 超过一定数量享受折扣外,也可能提供另外一种策略,即购买量不超过某个限额时 可以享受折扣,但是 一旦超过限额就要按原价支付。或者 折扣策略还可能是购买量超过一定数量后 购买的全部书籍都享受折扣,否则全都不打折
上面的每个策略都要求 一个购买量的值 和 一个折扣值。我们可以定义一个新的名为 Disc_quote 的类来支持不同的折扣策略,其中 Disc_quote 负责保存购买量的值 和 折扣值。其他的表示某种特定策略的类(如Bulk_quote)将分别继承自 Disc_quote,每个派生类通过定义自己的 net_price 函数来实现各自的折扣策略
Disc_quote 类与任何特定的折扣策略都无关,因此 Disc_quote 类中的 net_price函数是没有实际含义的
可以在 Disc_quote 类中不定义新的 net_price,此时,Disc_quote 将继承 Quote 中的 net_price 函数
这样的设计 可能导致用户编写出一些无意义的代码。用户可能会创建一个 Disc_quote 对象 并为其提供购买量和折扣值,如果将该对象传给一个像 print_total 这样的函数,则程序将调用Quote版本的net_price。显然,最终计算出的销售价格并 没有考虑我们在创建对象时提供的折扣值,因此上述操作毫无意义
2、纯虚函数:关键问题 并不仅仅是 不知道应该如何定义 net_price,而是 根本就不希望用户创建一个 Disc_quote对象。Disc_quote类 表示的是一本打折书籍的通用概念,而非某种具体的折扣策略
1)抽象基类通常包含纯虚函数,这些函数必须在派生类中实现。这种设计确保了所有派生类具有一致的接口,符合接口隔离原则
2)通过定义抽象基类,可以将通用行为和接口提取到基类中,而特定行为则在派生类中实现。这种方式促进了代码的重用和扩展
3)可以隐藏实现细节,只暴露必要的接口。这有助于简化复杂系统的设计,使其更易于理解和维护
可以将 net_price 定义成 纯虚函数 从而令程序实现我们的设计意图,这样做可以清晰明了地告诉用户当前这个 net_price 函数是没有实际意义的。和 普通的虚函数不一样,一个纯虚函数无须定义。我们通过在函数体的位置(即在声明语句的分号之前)书写=0 就可以将一个虚函数说明为纯虚函数。其中,=0 只能出现在类内部的虚函数声明语句处:
//用于保存折扣值和购买量的类,派生类使用这些数据可以实现不同的价格策略
class Disc_quote : public Quote {
public:Disc_quote() = default;Disc_quote(const std::string& book, double price,std::size_t qty, double disc):Quote(book, price),quantity(qty), discount(disc) { }double net_price(std::size_t) const = 0;
protected:std::size_t quantity = 0; //折扣适用的购买量double discount = 0.0; //表示折扣的小数值
};
和我们之前定义的 Bulk_quote 类一样,Disc_quote 也分别定义了一个默认构造函数 和 一个接受四个参数的构造函数。尽管我们不能直接定义这个类的对象,但是 Disc_quote 的派生类构造函数 将会使用 Disc_quote 的构造函数来构建各个派生类对象的Disc_quote 部分
也可以为纯虚函数提供定义,不过函数体必须定义在类的外部。也就是说,我们不能在类的内部为一个=0的函数提供函数体
3、含有纯虚函数的类是抽象基类:含有(或者未经覆盖直接继承)纯虚函数的类 是抽象基类。抽象基类 负责定义接口,而后续的其他类 可以覆盖该接口。我们不能(直接)创建一个抽象基类的对象。因为 Disc_quote将net_price定义成了 纯虚函数,所以我们不能定义 Disc_quote 的对象。我们可以定义 Disc_quote 的派生类的对象,前提是这些类覆盖了net_price函数:
//Disc_quote声明了纯虚函数,而Bulk_quote将覆盖该函数
Disc_quote discounted; //错误:不能定义Disc_quote的对象
Bulk_quote bulk; //正确:Bulk_quote中没有纯虚函数
Disc_quote 的派生类必须给出自己的net_price定义,否则它们仍将是抽象基类
4、派生类构造函数 只初始化它的直接基类:
重新实现 Bulk_quote,让它继承 Disc_quote 而非直接继承 Quote:
//当同一书籍的销售量超过某个值时启用折扣。折扣的值是一个小于1的正的小数值,以此来降低正常销售价格
class Bulk_quote : public Disc_quote {
public:Bulk_quote() = default;Bulk_quote(const std::string& book, double price,std::size_t qty, double disc):Disc_quote(book, price, qty, disc);//覆盖基类中的函数版本以实现一种新的折扣策略double net_price(std::size t) const override;
};
这个版本的 Bulk_quote 的直接基类是 Disc_quote,间接基类是 Quote。每个 Bulk_quote对象包含三个子对象:一个(空的)Bulk_quote部分、一个Disc_quote子对象 和 一个Quote子对象
每个类 各自控制其对象的初始化过程。因此,即使 Bulk_quote没有自己的数据成员,它也仍然需要 像原来一样提供一个接受四个参数的构造函数。该构造函数 将它的实参传递给 Disc_quote 的构造函数,随后 Disc_quote 的构造函数继续调用 Quote 的构造函数。Quote的构造函数 首先初始化bulk的bookNo和price成员,当 Quote 的构造函数结束后,开始运行Disc_quote的构造函数并初始化quantity 和 discount成员,最后运行 Bulk_quote 的构造函数,该函数无须执行 实际的初始化 或 其他工作
5、重构:在Quote的继承体系中 增加Disc_quote类是重构的一个典型示例。重构 负责重新设计类的体系 以便将操作和/或数据从一个类移动到另一个类中,对于 面向对象的应用程序来说,重构是一种很普遍的现象
即使 我们改变了整个继承体系,那些使用了Bulk_quote 或 Quote 的代码也无须进行任何改动。不过一旦类被重构(或以其他方式被改变)就意味着我们必须重新编译含有这些类的代码了
Disc_quote.h
#pragma once
#ifndef DISC_QUOTE_H
#define DISC_QUOTE_H#include "Quote.h"// 抽象基类
class Disc_quote : public Quote{
public:Disc_quote() = default;Disc_quote(const std::string &s, double p, double dis, std::size_t qua) :Quote(s, p), discount(dis), quantity(qua) {}double net_price(std::size_t) const override = 0;
protected: // 派生类是可以用的double discount = 0.0;std::size_t quantity = 0;
};
#endif
Bulk_quote_15.h
#pragma once
#ifndef BULK_QUOTE_15_H
#define BULK_QUOTE_15_H#include "Disc_quote.h"class Bulk_quote : public Disc_quote {
public:Bulk_quote() = default;Bulk_quote(const std::string &str, double p, double dis, std::size_t qua) :Disc_quote(str, p, dis, qua) {}double net_price(std::size_t) const override;
};double Bulk_quote::net_price(std::size_t n) const {if (n >= quantity)return n * (1 - discount) * price;elsereturn n * price;
}#endif
Limited_quote_16.h
#pragma once
#ifndef LIMITED_QUOTE_16_H
#define LIMITED_QUOTE_16_H#include "Disc_quote.h"class Limited_quote : public Disc_quote {
public:Limited_quote() = default;Limited_quote(const std::string& str, double p, double dis, std::size_t n) :Disc_quote(str, p, dis, n) {}double net_price(size_t) const override;
};double Limited_quote::net_price(std::size_t num) const {if (num <= quantity) {return num * (1 - discount) * price;}else {return (num - quantity) * price + quantity * price * (1 - discount);}
}#endif
15.16.cpp
#include "Bulk_quote_15.h"
#include "Limited_quote_16.h"using namespace std;int main()
{Quote quo("123-1", 5);print_total(cout, quo, 10);Bulk_quote bquo("123-2", 5, 0.2, 2);print_total(cout, bquo, 10);Limited_quote lquo("123-3", 5, 0.2, 2);print_total(cout, lquo, 10);return 0;
}
运行结果
5、访问控制与继承
1、每个类 分别控制自己的成员初始化过程,与之类似,每个类 还分别控制着其成员对于派生类来说 是否可访问
2、受保护的成员:一个类 使用protected关键字 来声明那些它希望与派生类分享但是不想被其他公共访问使用的成员。protected说明符 可以看做是 public 和 private中和后的产物:
- 和私有成员类似,受保护的成员 对于类的用户来说是不可访问的
- 和公有成员类似,受保护的成员 对于派生类的成员 和 友元来说是可访问的
- 派生类的成员或友元 只能通过派生类对象(基对象不行)来访问基类的受保护成员。派生类 对于一个基类对象中的受保护成员没有任何访问特权
class Base {
protected:int prot_mem; //protected成员
class Sneaky : public Base {friend void clobber(Sneaky&); //能访问Sneaky::prot_memfriend void clobber(Base&); //不能访问Base::prot_memint j; //j默认是private
};
//正确:clobber能访问Sneaky对象的(包括继承来的)private和protected成员
void clobber(sneaky &s) { s.j = s.prot_mem = 0; }
//错误:clobber不能访问Base的protected成员
void clobber(Base &b) { b.prot_mem = 0; }
如果派生类(及其友元)能访问基类对象的受保护成员,只要定义一个形如Sneaky的新类 就能非常简单地规避掉 protected提供的访问保护了
在常规情况下,派生类能够访问其直接继承的protected成员,但不能访问其他基类实例的protected成员。然而,如果定义一个能成为基类友元的新类,这个新类可能会访问基类中的受保护成员。例如:
class Sneaky {// 让Sneaky类的成员函数可以访问Base类的受保护和私有成员friend class Base;
public:int getProtectedMember(Base& b) {return b.protected_member;}
};
如果Sneaky成为基类的友元,Sneaky的实例就可以访问该基类的所有成员,包括被标记为protected和private的成员。
这个示例说明了protected成员在设计上的潜在缺陷。如果一个程序员有意或无意地创建了一个新类,并将其作为基类的友元类,那么效果上就绕过了protected的访问保护,从而可能破坏封装性
3、公有、私有和受保护继承:某个类 对其继承而来的成员的访问权限 受到两个因素影响;一是 在基类中该成员的访问说明符,二是 在派生类的派生列表中的访问说明符
class Base {
public:void pub_mem(); //public成员
protected:int prot_mem; //protected成员
private:char priv_mem; //private成员
};
struct Pub_Derv : public Base {//正确:派生类能访问protected成员int f() { return prot_mem; }//错误:private成员对于派生类来说是不可访问的char g() { return priv_mem; }
};
struct Priv_Derv : private Base {//private不影响派生类的访问权限,意味着在这个类中 对应的继承的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的
如果继承是公有的,则成员将遵循 其原有的访问说明符,此时d1可以调用pub_mem。在Priv_Derv中,Base的成员是 私有的,因此类的用户不能调用pub_mem(在Base类中的访问说明符 是public的)
派生访问说明符(类中成员的 决定友元和成员,派生列表中的 决定用户) 还可以控制 继承自派生类的新类的访问权限(派生列表中的没办法 向上控制基类的成员是否可以使用,但是可以控制其派生类的成员(其派生类也可以看成其用户的一种))
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; }
}
Priv_Derv的派生类 无法执行类的访问,对于它们来说,Priv_Derv 继承自Base的所有成员都是私有的
之前还定义了一个名为 Prot_Derv的类,它采用 受保护继承,则Base的 所有公有成员在新定义的类中 都是受保护的。Prot_Derv的用户不能访问pub_mem,但是 Prot_Derv的成员 和 友元可以访问那些继承而来的成员(变成protected的了)
4、派生类向基类转换的可访问性:派生类向基类的转换 是否可访问 由使用该转换的代码决定,同时派生类的派生访问说明符 也会有影响。假定D继承自B:
- 只有当D公有地继承B时,用户代码 才能使用派生类向基类的转换;如果D继承E的方式是 受保护的或者私有的,则用户代码不能使用该转换
- 不论D以什么方式继承B,D的成员函数和友元 都能使用派生类向基类的转换:派生类 向其直接基类的类型转换 对于派生类的成员和友元来说永远是可访问的
- 如果D继承B的方式是公有的或者受保护的,则D的派生类的成员和友元可以使用D向B的类型转换:反之,如果D继承B的方式是私有的,则不能使用(这里的派生类的成员和友元 其实都是用户)
如果基类的公有成员是可访问的,则派生类 向基类的类型转换也是可访问的:反之则不行
5、继承可以是公有的(public)、受保护的(protected)或私有的(private),这三种方式影响着派生类如何访问基类成员,以及外部如何看待派生类和基类之间的关系
公有继承(public)
当一个类以公有方式继承另一个类时,基类的公有成员和受保护成员的访问级别在派生类中保持不变。这意味着:
- 基类的公有成员在派生类中仍然是公有的
- 基类的受保护成员在派生类中仍然是受保护的
- 基类的私有成员在派生类中无法直接访问
公有继承表示派生类是基类的一种特殊形式,符合“是一个(is-a)”关系。这也意味着,可以通过派生类的对象来访问基类的公有成员
受保护继承(protected)
当一个类以受保护方式继承另一个类时,基类的公有和受保护成员在派生类中都成为受保护成员。这种方式的主要区别在于:
- 基类的公有和受保护成员在派生类中都是受保护的
- 基类的私有成员在派生类中无法直接访问
受保护继承不表明一个明确的“是一个(is-a)”关系,但仍然允许派生类访问这些成员
私有继承(private)
私有继承时,基类的公有和受保护成员在派生类中都成为私有成员。这意味着:
- 基类的公有和受保护成员在派生类中都是私有的
- 基类的私有成员在派生类中无法直接访问
私有继承意味着基类的成员是派生类的一个实现细节,而不是一个公开的接口。对外不表明任何形式的“是一个(is-a)”关系
类型转换和访问权限
对于派生类成员函数和友元来说,它们可以使用的派生类向基类的类型转换取决于继承方式:
- 公有继承或受保护继承: 派生类的成员函数和友元可以使用派生类对象向基类类型的转换
- 私有继承: 只有派生类的成员函数和友元可以使用派生类对象向基类类型的转换,外部代码则不能利用这种转换
这种区别反映了不同继承方式背后的设计意图:在公有继承和受保护继承中,派生类与基类之间保持一定程度的“开放”关系
6、类的设计与受保护的成员:不考虑继承的话,一个类有两种不同的用户:普通用户和类的实现者。普通用户 编写的代码使用类的对象,这部分代码 只能访问类的公有(接口)成员;实现者则负责编写类的成员和友元的代码,成员和友元既能访问类的公有部分,也能访问类的私有(实现)部分
第三种用户,即派生类。基类把它希望 派生类能够使用的部分声明成受保护的。普通用户 不能访问受保护的成员,而派生类及其友元 仍旧不能访问私有成员
基类应该 将其接口成员声明为公有的:同时 将属于其实现的部分分成两组:一组可供派生类访问,另一组只能由基类及基类的友元访问。对于 前者应该声明为受保护的,对于 后者应该声明为私有的
7、友元与继承:就像友元关系 不能传递一样,友元关系 同样也不能继承。基类的友元 在访问派生类成员时 不具有特殊性,类似的,派生类的友元 也不能随意访问基类的成员:
class Base {//添加friend声明,其他成员与之前的版本一致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(Base的派生类)的友元//对基类的访问权限由基类本身控制,即使对于派生类的基类部分也是如此,j是Sneaky的privateint f3(Sneaky s) { return s.prot_mem; } //正确:Pal是Base的友元,prot_mem在Base中是protected
};
每个类 负责控制自己的成员的访问权限,Pal是Base的友元,所以Pal 能够访问Base对象的成员,这种可访问性 包括了Base对象内嵌在 其派生类对象中的情况(派生类Sneaky继承了Base的prot_mem对象,所以Pal类可以使用Sneaky类的prot_mem)
当一个类将另一个类 声明为友元时,这种友元关系 只对做出声明的类有效。对于原来那个类来说,其友元的基类 或者 派生类 不具有特殊的访问能力:
//D2对Base的protected和private成员不具有特殊的访问能力
class D2 : public Pal {
public:int mem(Base b){ return b.prot_mem; } //错误:友元关系不能继承
};
不能继承友元关系,每个类 负责控制各自成员的访问权限
8、改变个别成员的可访问性:需要改变 派生类继承的某个名字的访问级别,通过使用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;
};
因为 Derived 使用了私有继承,所以继承而来的成员size和n(在默认情况下)是 Derived的私有成员。然而,我们使用using声明语句 改变了这些成员的可访问性。改变之后,Derived的用户 将可以使用size成员,而Derived的派生类 将能使用n
通过 在类的内部使用using声明语句,我们可以 将该类的直接或间接基类中的任何可访问成员(例如,非私有成员)标记出来。using声明语句中名字的访问权限 由该using声明语句之前的访问说明符 来决定
派生类只能为那些它可以访问的名字提供using声明
9、默认的继承保护级别:默认派生运算符 也由定义派生类所用的关键字来决定。默认情况下,使用class关键字定义的派生类是私有继承的;而使用struct关键字定义的派生类是公有继承的
class Base {/* ... */};
struct D1 : Base { /* ... */ }; //默认public继承
class D2 : Base { /* ... */ }; //默认private继承
使用struct关键字和class关键字定义的类之间 唯一的差别就是 默认成员访问说明符及默认派生访问说明符;除此之外,再无其他不同之处
一个私有派生的类 最好显式地将private声明出来,而不要仅仅依赖于默认的设置
10、判断下面的哪些赋值语句是合法的
Base *p = &d1; //d1 的类型是 Pub_Derv
p = &d2; //d2 的类型是 Priv_Derv
p = &d3; //d3 的类型是 Prot_Derv
p = &dd1; //dd1 的类型是 Derived_from_Public
p = &dd2; //dd2 的类型是 Derived_from_Private
p = &dd3; //dd3 的类型是 Derived_from_Protected
:后面的public / private相当于给:前面的类的每一个成员加了一个限定符,public没影响还是按原来的
只有 d1 和 dd1 才能够赋值。这是因为:只有当派生类公有地继承基类时,用户代码才能 使用派生类向基类的转换。也就是说,如果派生类继承基类的方式是受保护的或者私有的,则用户代码不能使用该转换
void memfcn(Base &b) { b = *this; }
对于每个类,分别判断上面的函数是否合法
只有 Derived_from_Private : private Priv_Derv
这个类的函数不合法
无论派生类以什么方式继承基类,派生类的成员函数 和 友元 都能使用派生类向直接基类的转换;派生类向其直接基类的类型转换对于派生类的成员来说永远是可访问的
如果派生类继承基类的方式 是公有的或者受保护的,则派生类的成员和友元 可以使用派生类向基类的类型转换;反之,如果派生类继承基类的方式 是私有的,则不能使用
11、图形基元(如方格、圆、球、圆锥),将其对应的一组类型 组织成一个继承体系:
Geo.h
#pragma once
#ifndef GEO_H
#define GEO_Hstatic const float PI = 3.14;class Shape {
public:// 不需要构造函数了virtual const char* shape_name() = 0;virtual void resize(float p) = 0;virtual ~Shape() {}; // 析构函数不需要设成纯虚函数
};class Shape_2D : public Shape {
public:Shape_2D() = default;Shape_2D(float x, float y) :x(x), y(y) {}virtual float s() const = 0; // 面积virtual float d() const = 0; // 直径virtual float c() const = 0; // 周长~Shape_2D() override {} // 别忘了析构函数
private:float x = 0.0; // 中心坐标float y = 0.0;
};class Shape_3D : public Shape {
public:Shape_3D() = default;Shape_3D(float x, float y, float z) :x(x), y(y), z(z) {}virtual float v() const = 0; // 体积~Shape_3D() override {}
private:float x = 0.f;float y = 0.f;float z = 0.f;
};class Circle : public Shape_2D {
public:Circle() = default; //通过使用explicit关键字,防止隐式地将float转换为Circle对象explicit Circle(float r) : r(r) {}Circle(float x, float y, float r) : Shape_2D(x, y), r(r) {}float s() const override { return PI * r * r; } // const别忘了float d() const override { return 2 * r; }float c() const override { return 2 * PI * r; }const char* shape_name() override { return "Circle"; }void resize(float p) override { r = r * p; }~Circle() override {}
private:float r = 0.f;
};class Box : public Shape_3D {
public:Box() = default;explicit Box(float len) : half_x(len * 0.5f), half_y(len * 0.5f), half_z(len * 0.5f) {}Box(float x, float y, float z) :Shape_3D(x, y, z), half_x(x * 0.5f), half_y(y * 0.5f), half_z(z * 0.5f) {}float v() const { return 8 * half_x * half_y * half_z; }const char* shape_name() override { return "Box"; }void resize(float p) override {half_x *= 0.5f;half_y *= 0.5f;half_z *= 0.5f;}~Box() override {}
private:// 0.f:这个表示法明确地表示该值是一个float类型,因为.f后缀专门用于表示float常量。// 0.0:默认情况下,这个表示法被解释为一个double类型,因为没有后缀,编译器会将其解释为一个double常量// 使用0.f可以避免从double转换为float的隐式转换float half_x = 0.f;float half_y = 0.f;float half_z = 0.0;
};
#endif
15.21b.cpp
#include "Geo.h"
#include <iostream>using namespace std;int main()
{Circle cc(10);cout << cc.shape_name() << endl;cout << "c: " << cc.c() << endl;cout << "s: " << cc.s() << endl;cout << "d: " << cc.d() << endl;cout << endl;cc.resize(0.5);cout << "resize: " << "c: " << cc.c() << " " << "s: " << cc.s() << " " << "d: " << cc.d() << endl;cout << endl;Box bb(10);cout << bb.shape_name() << endl;cout << "v: " << bb.v() << endl;return 0;
}
6、继承中的类作用域
1、如果一个名字 在派生类的作用域内 无法正确解析, 则编译器将继续在外围的基类作用域中 寻找该名字的定义
派生类的作用域 位于基类作用域之内,也恰恰因为类作用域 有这种继承嵌套关系, 所以派生类才能像使用自己的成员一样使用基类的成员
Bulk_quote bulk;
cout << bulk.isbn();
名字 isbn 的解析将按下述过程进行:
- 因为我们是通过 Bulk_quote 的对象调用 isbn 的, 所以首先在 Bulk_quote 中查找, 这一步没有找到名字 isbn
- 因为 Bulk_quote 是 Disc_quote 的派生类, 所以接下来在 Disc_quote 中查找, 仍然找不到
- 因为 Disc_quote 是 Quote 的派生类, 所以接着在 Quote 中查找, 此时找到了名字 isbn, 所以我们使用的是最终被解析为 Quote 中的 isbn
2、在编译时进行名字查找: 一个对象, 引用或指针的静态类型 决定了该对象的哪些成员是可见的。即使静态类型 与 动态类型可能不一致,能使用哪些成员 仍然是由静态类型决定的(成员函数 与之前的引用和指针的虚函数不同)
class Disc_quote : public Quote {
public:std::pair<size_t, double> discount_policy() const {return {quantity, discount};}// 其他成员与之前的版本一致
};
只能通过 Disc_quote 及其派生类的对象、引用或指针使用 discount_policy:
Bulk_quote bulk;
Bulk_quote *bulkP = &bulk; // 静态类型与动态类型一致
Quote *itemP = &bulk; // 静态类型与动态类型不一致
bulkP->discount_policy(); // 正确:bulkP 的类型是 Bulk_quote*
itemP->discount_policy(); // 错误:itemP 的类型是 Quote*
尽管在 bulk 中确实含有一个名为 discount_policy 的成员,但是该成员对 itemP 是不可见的。itemP 的类型是 Quote 的指针,意味着对 discount_policy 的搜索将从 Quote 开始。显然,Quote 不包含名为 discount_policy 的成员,所以我们无法通过 Quote 的对象、引用或指针调用 discount_policy
在C++中,只有通过基类指针或引用调用虚函数时,才能实现动态绑定,也就是在运行时确定调用的是哪一个派生类的函数实现。这种机制称为多态性。通过这种机制,指针或引用可以指向不同的派生类对象,并调用对应的派生类实现的虚函数,非虚函数总是静态绑定的(即在编译时绑定),不支持多态性
#include <iostream>class Base {
public:virtual void show() { // 基类中的虚函数std::cout << "Base class" << std::endl;}
};class Derived : public Base {
public:void show() override { // 派生类中覆盖虚函数std::cout << "Derived class" << std::endl;}
};int main() {Base *b; // 基类指针Derived d;b = &d;b->show(); // 调用的是派生类的show函数,输出 "Derived class"return 0;
}
在这个例子中,Base 类中的 show 函数被声明为虚函数,因此在 main 函数中,通过基类指针 b 调用 show 函数时,会根据指针实际指向的对象(即 Derived 对象 d)调用 Derived 类中的 show 函数,实现了动态绑定
如果 Base 类中的 show 函数不是虚函数,则 b->show() 会调用 Base 类中的 show 函数,而不是 Derived 类中的 show 函数,输出 “Base class”
3、名字冲突与继承:派生类也能 重用定义在其直接基类或间接基类中的名字,此时在定义内的作用域(即派生类)的名字将隐藏定义在外层作用域(即基类)的名字
重用mem
struct Base {Base() : mem(0) { }
protected:int mem;
};
struct Derived : Base {Derived(int i) : mem(i) { }int get_mem() { return mem; }
protected:int mem;
};
get_mem 中 mem 的解析结果是定义在 Derived 中的名字,下面的代码的输出结果将是 42
Derived d(42);
cout << d.get_mem() << endl; // 打印 42
4、通过作用域运算符来 使用隐藏的成员:
struct Derived : Base {int get_base_mem() { return Base::mem; }// ...
};
作用域运算符将覆盖掉已有的查找规则,并指示编译器从 Base 类的作用域开始查找 mem
除非覆盖派生而来的虚函数之外,派生类最好不要重用定义在基类中的名字
5、关键概念:名字查找与类型
调用 p->mem(),依次执行以下 4 步骤:
- 首先确定 p(或 obj)的静态类型。因为我们调用的是一个成员,所以该类型必须是类类型
- 在 p(或 obj)的静态类型对应的类中查找 mem。如果找不到,则依在直接基类中不断查找 直到继承链的顶端。如果某个类中该类及其基类仍然找不到,则编译器将报错
- 一旦找到了 mem,就进行常规的类型检查 以确认对当前找到的 mem,本次调用是否合法
- 假设调用合法,则 编译器将根据调用的 是否是虚函数 而生成不同的代码:
1)如果mem是虚函数 且我们是通过引用或指针进行的调用,则编译器产生的代码 将在运行时确定 到底运行该虚函数的哪个版本,依据是对象的动态类型
2)反之,如果 mem 不是虚函数 或 我们是通过对象(而非引用或指针)进行的调用,则编译器 将产生 一个常规函数调用
6、名字查找先于类型检查:声明在内层作用域的函数数 并不会重载 声明在外层作用域的函数。因此,定义派生类中的函数 也不会重载其基类的成员
如果派生类(即内层作用域)的成员 与 基类(即外层作用域)的某个成员同名,则派生类将在 其作用域内隐藏该基类成员。即使 派生类成员和基类成员的形参列表不一致,基类成员也仍然会被隐藏掉(虚函数不一样,见8):
struct Base {int memfcn();
};struct Derived : Base {int memfcn(int); // 隐藏基类的 memfcn
};Derived d; Base b;
b.memfcn(); // 调用 Base::memfcn
d.memfcn(10); // 调用 Derived::memfcn
d.memfcn(); // 错误,参数列表为 memfcn 隐藏了
d.Base::memfcn(); // 正确,调用 Base::memfcn
编译器 首先在 Derived 中查找名字 memfcn;因为 Derived 确实定义了一个名为 memfcn 的成员,所以查找过程终止。一旦名字找到,编译器就不再继续查找了。Derived 中的 memfcn 版本需要一个 int 实参,而当前的调用语句没有传递任何实参,所以该调用语句是错误的
7、虚函数与作用域:可以理解 为什么基类与派生类中的虚函数必须有相同的形参列表了。假如基类与派生类的虚函数接受的实参不同,则我们就无法通过基类的引用或指针调用派生类的虚函数了
class Base {
public:virtual int fcn();
};class D1 : public Base {
public:// 隐藏基类的 fcn,这个 fcn 不是虚函数// D1继承了Base::fcn()的定义int fcn(int); // 形参列表与Base中的fcn不一致virtual void f2(); // 新的虚函数,基类中不存在
};class D2 : public D1 {
public:int fcn(int); // 是一个非虚函数,隐藏了 D1::fcn(int)int fcn(); // 覆盖了 Base 的虚函数 fcnvoid 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::fcnD1 *dp2 = &d1obj; D2 *dp2 = &d2obj;
bp2->f2(); // 错误,Base 没有名为 f2 的成员
dp1->f2(); // 虚调用,将在运行时调用 D1::f2()
dp2->f2(); // 虚调用,将在运行时调用 D2::f2()
因为 fcn 是虚函数,所以编译器产生的代码 将在运行时确定使用基类的哪个版本。判断的依据是该指针所绑定对象的真实类型。在 dp2 的例子中,实际绑定的对象是 D1 类型,而 D1 并没有覆盖那个不接受实参的 fcn,所以通过 bp2 进行的调用 将在运行时解析为 Base 定义的版本
接下来的三条调用语句 是通过不同类型的指针进行的(跟之前不同 f2不是虚函数),每个指针 分别指向继承体系中的一个类型。因为 Base 类型没有 f2(),所以第一条语句是非法的,即使 当前的指针碰巧指向了一个派生类对象也无济于事
Base *p1 = &d2obj; D1 *p2 = &d2obj; D2 *p3 = &d2obj;
p1->fcn(42); // 错误:Base 中没有接受一个 int 的 fcn
p2->fcn(42); // 静态绑定,调用 D1::fcn(int)
p3->fcn(42); // 静态绑定,调用 D2::fcn(int)
由于调用的是 非虚函数,所以不会发生动态绑定。实际调用的函数版本 由指针的静态类型决定
8、成员函数 无论是不是虚函数都能被重载。派生类可以 覆盖重载函数的0个或多个实例。有时一个类 仅需要覆盖重载 集合中的一些而非全部函数
一种好的解决方案是 为重载的成员 提供一条 using 声明语句,这样 就无需覆盖 基类中的每一个重载版本了。using 声明语句指定一个名字 而不指定形参列表,所以 一条基类成员函数的 using 声明语句 可以把该函数的所有重载实例 添加到派生类作用域中。此时,派生类只需定义其特有的函数即可,而无需为继承而来的其他函数重新定义
假设我们有一个基类Base,其中有三个重载版本的函数foo。我们创建一个派生类Derived,并希望覆盖其中一个版本,同时继承其他两个版本
class Base {
public:void foo(int x) {std::cout << "Base::foo(int)" << std::endl;}void foo(double x) {std::cout << "Base::foo(double)" << std::endl;}void foo(std::string x) {std::cout << "Base::foo(string)" << std::endl;}
};
class Derived : public Base {
public:// 引入Base中的所有foo重载版本using Base::foo;// 覆盖其中一个重载版本void foo(int x) {std::cout << "Derived::foo(int)" << std::endl;}
};
int main() {Derived d;d.foo(10); // 调用Derived::foo(int)d.foo(3.14); // 调用Base::foo(double)d.foo("hello"); // 调用Base::foo(string)return 0;
}
类内 using 声明的一般规则 同样适用于重载函数的名字;基类函数的每个实例 在派生类中都必须是可访问的。对派生类没有重新定义的 重载版本的访问 实质上是对 using 声明点的调用
基类的函数如果在派生类中通过using声明被引入,那么这些函数在派生类中的可访问性将与它们在基类中的可访问性保持一致。也就是说,如果基类中的函数是public,那么在派生类中它们也是public
派生类没有重新定义的重载版本,仍然可以通过using声明访问到基类的这些重载函数
D1 类需要覆盖它继承而来的 fcn 函数
#include <iostream>
#include <string>class Base {
public:virtual int fcn() {std::cout << "Base::fcn()\n";return 0;}
};class D1 : public Base {
public:int fcn() override { // 新加std::cout << "D1::fcn()\n";return 0;}virtual void f2() { std::cout << "D1::f2()\n"; }
};class D2 : public D1 {
public:int fcn(int); // 见下int fcn() override {std::cout << "D2::fcn()\n";return 0;}void f2() override { std::cout << "D2::f2()\n"; }
};int main() {Base bobj;D1 d1obj;D2 d2obj;Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;bp1->fcn(); //虚调用,将在运行时调用Base::fcnbp2->fcn(); //虚调用,将在运行时调用D1::fcnbp3->fcn(); //虚调用,将在运行时调用D2::fcnreturn 0;
}
为了避免名字隐藏问题,可以使用 using 声明将基类中的重载函数引入派生类(具体见9)
在 D2 类中,声明 int fcn(int);
会隐藏基类中的所有 fcn 重载版本。为了确保 fcn() 仍然可以使用,需要显式地将基类的 fcn() 引入到派生类的作用域中。可以使用 using 声明来实现这一点
#include <iostream>
#include <string>class Base {
public:virtual int fcn() {std::cout << "Base::fcn()\n";return 0;}
};class D1 : public Base {
public:int fcn() override {std::cout << "D1::fcn()\n";return 0;}virtual void f2() {std::cout << "D1::f2()\n";}
};class D2 : public D1 {
public:// 引入D1中的所有fcn重载版本using D1::fcn;// 声明一个新的重载版本的fcnint fcn(int) {std::cout << "D2::fcn(int)\n";return 0;}// 覆盖基类的fcnint fcn() override {std::cout << "D2::fcn()\n";return 0;}void f2() override {std::cout << "D2::f2()\n";}
};
1)引入基类的重载版本:通过在 D2 类中使用 using D1::fcn;,将 D1 类中的所有 fcn 重载版本引入到 D2 类的作用域中。这样可以避免 int fcn(int) 声明隐藏基类中的其他 fcn 函数
2)覆盖基类的函数:int fcn() override 覆盖了 D1 类中的 fcn() 函数,确保通过基类指针调用时,多态机制能够正确调用 D2 类中的 fcn() 实现
3)定义新的重载版本:int fcn(int) 是 D2 类中的一个新的重载版本,用于处理带有 int 参数的调用
2)和 3)就是覆盖和重载的区别
9、派生类中不写using和写using有什么区别
1)不使用 using 声明
重载函数的隐藏:如果派生类定义了与基类同名但参数不同的函数,则基类的所有重载版本都会被隐藏,即使派生类的函数签名与基类的其他重载版本不同。这意味着派生类无法访问基类的其他重载版本
假设我们有一个基类 Base,其中有两个重载版本的函数 foo。派生类 Derived 定义了一个新的重载版本 foo,但没有使用 using 声明
class Base {
public:void foo(int x) {std::cout << "Base::foo(int)" << std::endl;}void foo(double x) {std::cout << "Base::foo(double)" << std::endl;}
};// 派生类不使用 using 声明
class Derived : public Base {
public:void foo(int x) {std::cout << "Derived::foo(int)" << std::endl;}
};
int main() {Derived d;d.foo(10); // 调用Derived::foo(int)// d.foo(3.14); // 编译错误,Base::foo(double)被隐藏return 0;
}
2)使用 using 声明
继承基类的重载函数:派生类可以显式地引入基类的所有重载版本,这样它们不会被隐藏。派生类仍然可以定义新的重载版本,而不影响对基类重载版本的访问
假设我们有一个基类 Base,其中有两个重载版本的函数 foo。派生类 Derived 使用 using 声明引入基类的重载函数,并定义了一个新的重载版本 foo
class Base {
public:void foo(int x) {std::cout << "Base::foo(int)" << std::endl;}void foo(double x) {std::cout << "Base::foo(double)" << std::endl;}
};// 派生类使用 using 声明
class Derived : public Base {
public:using Base::foo; // 引入基类的所有重载版本void foo(int x) {std::cout << "Derived::foo(int)" << std::endl;}
};
int main() {Derived d;d.foo(10); // 调用Derived::foo(int)d.foo(3.14); // 调用Base::foo(double)return 0;
}
通过在派生类 Derived 中使用 using Base::foo;,基类中的所有 foo 重载版本被引入到派生类中。这样,派生类可以定义新的重载版本 foo(int) 而不影响对基类其他重载版本的访问。因此,d.foo(3.14) 能正确调用 Base::foo(double)
总结:
不使用 using 声明:基类的所有重载函数都会被隐藏,派生类无法访问它们
使用 using 声明:基类的所有重载函数被引入到派生类中,派生类可以访问它们,同时可以定义新的重载版本而不影响对基类重载版本的访问
7、构造函数与拷贝控制
1、和其他类一样,位于继承体系中的类 也需要控制当其对象发生一系列操作时发生什么样的行为,这些操作包括初始化、复制、移动、赋值和销毁。如果一个类(基类或派生类)没有定义支持控制操作,编译器将为它合成一个版本。当然,这个合成的版本也可以是成员函数
可以通过将合成的重载版本声明为删除的函数,来明确禁止这些函数的调用。这可以防止隐藏基类中的其他重载版本,同时清晰地表达出不希望这些重载版本被使用的意图
如果 希望 D2 类中不能调用基类中的某些重载版本,可以使用删除函数的语法。删除函数使用 = delete 标记,这样在编译期间就会报错,而不会隐藏基类的其他重载版本
#include <iostream>
#include <string>class Base {
public:virtual int fcn() {std::cout << "Base::fcn()\n";return 0;}
};class D1 : public Base {
public:int fcn() override {std::cout << "D1::fcn()\n";return 0;}
};class D2 : public D1 {
public:// 引入D1中的所有fcn重载版本using D1::fcn;// 声明一个新的重载版本的fcnint fcn(int) {std::cout << "D2::fcn(int)\n";return 0;}// 覆盖基类的fcnint fcn() override {std::cout << "D2::fcn()\n";return 0;}// 将基类中的某些重载版本删除int fcn(double) = delete;void f2() override {std::cout << "D2::f2()\n";}
};
int main() {D2 d2obj;D2 *d2p = &d2obj;d2p->fcn(42); // 调用D2::fcn(int)d2p->fcn(3.14); // 编译错误,D2::fcn(double)被删除return 0;
}
7.1 虚析构函数
1、基类通常应该定义一个虚析构函数,这样 就能动态分配继承体系中的对象了
delete 一个动态分配的对象的指针时 将执行析构函数。如果 该指针指向继承体系中的某个类型,则有可能出现指针的静态类型 与 被删除对象的动态类型不同的情况
通过在基类中将析构函数定义成虚函数 以确保执行正确的析构函数版本:
class Quote {
public:// 如果我们删除的是一个指向派生类对象的基类指针,则需要虚析构函数virtual ~Quote() = default; // 动态绑定析构函数
};
和其他函数一样,析构函数的虚属性也会被继承。只要基类的析构函数是 虚函数, 就能确保当我们delete基类指针时 将运行正确的析构函数版本:
Quote *itemp = new Quote; // 静态类型和动态类型一致
delete itemp; // 调用 Quote 的析构函数
itemp = new Bulk_quote; // 静态类型和动态类型不一致
delete itemp; // 调用 Bulk_quote 的析构函数
如果 一个类需要析构函数,那么它也同样需要 拷贝和赋值操作符。基类的析构函数 并不遵循上述准则,它是个重要的例外
2、虚析构函数将阻止合成移动操作:如果一个类定义了析构函数,即使它通过 =default 的形式使用了合成的版本,编译器也不会为这个类合成移动操作(如果一个类定义了析构函数(无论是否默认),它就不会自动生成移动构造函数和移动赋值操作符)
C++ 提供了一组默认生成的特殊成员函数,如果用户没有显式定义它们,编译器会自动生成它们。这些包括:
默认构造函数
拷贝构造函数
拷贝赋值操作符
移动构造函数
移动赋值操作符
析构函数
然而,一旦用户显式定义了其中的一些函数,编译器不会自动生成与之相关的其他特殊成员函数
如果一个类定义了任何一个析构函数、拷贝构造函数、拷贝赋值操作符,编译器就不会自动生成移动构造函数和移动赋值操作符
显式定义析构函数的类
class MyClass {
public:int* data;MyClass() : data(new int[100]) {}~MyClass() {delete[] data;}// 编译器不会自动生成移动构造函数和移动赋值操作符
};
在这种情况下,由于显式定义了析构函数,编译器不会自动生成移动构造函数和移动赋值操作符。原因是编译器无法确定如何安全地移动资源(如指针data)而不引入资源泄漏或其他问题
解决方案
显式定义移动构造函数和移动赋值操作符:
// 显式定义移动构造函数
MyClass(MyClass&& other) noexcept : data(other.data) {other.data = nullptr;
}// 显式定义移动赋值操作符
MyClass& operator=(MyClass&& other) noexcept {if (this != &other) {delete[] data;data = other.data;other.data = nullptr;}return *this;
}
7.2 合成拷贝控制与继承
1、基类或派生类的合成拷贝控制成员的行为 与其他合成的函数、赋值运算符 或 析构函数一致。它们对类类型的成员依次进行初始化、赋值或销毁操作。此外,这些合成的成员 还负责 使用直接基类中对应的操作 对一个对象的直接基类部分初始化、赋值或销毁的操作
无论基类成员是合成的版本 还是自定义的版本 都没有太大影响,唯一的要求是 相应的成员应该可访问 并且不是一个被删除的函数
对于 派生类的析构函数来说,它除了销毁派生类自己的成员外,还负责销毁派生类的直接基类;该直接基类又销毁它自己的直接基类, 以此类推直至继承链的顶端
Quote 因为定义了析构函数而不能拥有合成的移动操作,因此当我们移动 Quote对象时 实际使用的是合成的拷贝操作。Quote没有移动操作 意味着它的派生类也没有
2、派生类中删除的拷贝控制与基类的关系:基类或派生类也能出于同样的原因 将其合成的默认构造函数 或 任何一个拷贝控制成员定义成删除的函数。某些定义类的方式 也可能导致 有的派生类成员成为被删除的函数:
- 如果某类中的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数 是被删除的 或者 不可访问,则派生类对应的成员也将是被删除的,原因是 编译器不能使用基类成员来执行派生类对象某些部分的构造、赋值或销毁操作
- 如果在基类中有一个不可访问或删除掉的析构函数,则派生类中合成的默认和拷贝构造函数将是被删除的,因为编译器无法销毁派生类对象的基类部分
- 编译器将 不合成一个删除掉的移动操作。当我们使用 =default 请求一个移动操作时,如果基类中的对应操作是删除的 或 不可访问,那么派生类中 该函数将是被删除的,原因是 派生类对象的基类部分不可移动
class B {
public:B();B(const B&) = delete;// 其他成员,不含有移动构造函数
};class D : public B {// 没有声明任何构造函数
};D d; // 正确: D的合成默认构造函数使用B的默认构造函数
D d2(d); // 错误:d 的合成默认构造函数是被删除的
D d3(std::move(d)); // 错误:隐式地使用D的被删除的拷贝构造函数,因为不会定义移动构造函数,就当拷贝构造函数了
定义了 拷贝构造函数,所以编译器将不会为 B 合成一个移动构造函数。既不能移动也不能拷贝 B 的对象。如果派生类希望它自己的对象能够移动和拷贝,则派生类需要自定义相应版本的构造函数。在这个过程中派生类还必须考虑如何移动或拷贝其基类部分的成员。在实际编程过程中,如果基类中没有默认、拷贝或移动构造函数,则一般情况下 派生类也不会定义相应的操作
3、移动操作与继承:
大多数基类都会定义一个虚析构函数。因此在默认情况下,基类通常不含有合成的移动操作,而且在它的派生类中也没有合成的移动操作
基类缺少移动操作 会阻止派生类拥有自己的合成移动操作,需要执行移动操作时 应该首先在基类中进行定义。我们的 Quote 可以使用合成版本,不过前提是 Quote 必须显式地定义这些成员。一旦 Quote 定义了自己的移动操作,那么它必须同时显式地定义拷贝操作
当类使用合成的拷贝和移动操作时,编译器会自动生成这些操作。但是,如果类定义了自己的某些特殊操作(如析构函数、拷贝构造函数、移动构造函数等),编译器就不会自动生成默认的版本。因此,需要显式定义这些操作
1)合成版本的拷贝和移动操作:
如果没有显式定义析构函数、拷贝构造函数、拷贝赋值运算符、移动构造函数和移动赋值运算符,编译器会为类生成默认的版本。这些合成版本的函数按成员逐个进行拷贝和移动
2)显式定义某些成员的影响:
析构函数:如果类定义了析构函数,无论是用户定义的还是合成的,编译器会认为这个类需要特别的资源管理,因此不会生成移动操作的默认版本
拷贝构造函数和拷贝赋值运算符:如果类定义了这两个函数中的任何一个,编译器不会为该类生成默认的移动构造函数和移动赋值运算符
移动构造函数和移动赋值运算符:如果类定义了这两个函数中的任何一个,编译器不会为该类生成默认的拷贝构造函数和拷贝赋值运算符
所以 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; // 显式定义虚析构函数// 其他成员函数与之前的版本一致
};
在上面的例子中,Quote 类显式定义了所有的五个特殊成员函数(构造函数、拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符、析构函数)。这样,编译器不会因为显式定义了其中一个或几个成员函数而忽略生成其他的成员函数
通过上面的定义,就能对 Quote 的对象逐成员地分别进行拷贝、移动、赋值和销毁操作了。而且 除非 Quote 的派生类中含有排斥移动的成员,否则它将自动获得合成的移动操作
4、为什么为 Disc_quote 定义一个默认构造函数?如果去掉该构造函数的话会对 Bulk_quote 的行为产生什么影响
已经定义了一个构造函数,默认构造函数是被删除的,需要显式地定义
如果去掉了Disc_quote的默认构造函数,Bulk_quote的默认构造函数是被删除的
因此,基类的默认构造函数必须显示定义,确保其派生类在执行它的默认构造函数时能调用基类默认构造函数
7.3 派生类的拷贝控制成员
1、派生类构造函数 在初始化阶段中 不仅要初始化派生类自己的成员,还负责初始化派生类对象的基类部分
和构造函数及赋值运算符不同的是,析构函数 只负责销毁派生类自己分配的资源。对象的成员是被隐式销毁的;类似的,派生类对象的基类部分也是自动销毁的
2、定义派生类的拷贝或移动构造函数:当为派生类定义拷贝或移动构造函数时,通常使用对应的基类构造函数初始化对象的基类部分:
class Base { /* ... */ };
class D : public Base {
public:// 基类的默认构造函数初始化对象的基类部分// 要想使用拷贝或移动构造函数,必须在构造函数初始值列表中显式地调用该构造函数D(const D& d) : Base(d) // 拷贝基类成员/* D的成员的初始值 */ { /* ... */ }D(const D&& d) : Base(std::move(d)) // 移动基类成员/* D的成员的初始值 */ { /* ... */ }
};
初始化 Base(d) 将一个 D 对象传递给基类构造函数。尽管从道理上来说,Base 可以包含一个参数类型为 D 的构造函数,但是在实际编程过程中通常不会这么做。相反,Base(d) 一般会匹配 Base 的拷贝构造函数。D 类型的对象 d 将被绑定到 该构造函数的 Base& 形参上。Base 的拷贝构造函数负责将d的基类部分拷贝给要创建的对象。假如我们没有提供基类的初始值的话:
// D这个拷贝构造函数很可能是不正确的定义
// 基类部分被默认初始化,而非拷贝
D(const D&& d) /* 成员初始值,但是没有提供基类初始值 */{ /* ... */ }
这个新构建的对象的配置将非常奇怪:它的 Base 成员被赋予了默认值,而D成员的值则是从其他对象拷贝得来的
默认情况下,基类默认构造函数 初始化派生类对象的基类部分。如果我们想 拷贝(或移动)基类部分,则 必须在派生类的构造函数初始值列表中 显式地使用 基类的拷贝(或移动)构造函数
3、派生类赋值运算符:与拷贝和移动构造函数一样,派生类的赋值运算符 也必须显式地为其基类部分赋值:
// Base::operator=(const Base&)不会被自动调用
D&D::operator=(const D &rhs) {Base::operator=(rhs); // 为基类部分赋值// 按照过去的方法为派生类的成员赋值// 酌情处理自赋值及释放已有资源等情况return *this;
}
基类的运算符(应该可以)正确地处理自赋值的情况。如果赋值命令是正确的,则基类运算符将释放掉其左侧运算对象的基类部分的旧值,然后利用 rhs 为其赋一个新值。随后,我们继续进行其他为派生类成员赋值的工作
无论基类的构造函数或赋值运算符是自定义的版本 还是合成的版本,派生类的对应操作都能使用它们。例如,对于 Base::operator= 的调用将执行 Base 的拷贝赋值运算符,至于该运算符是由 Base 显式定义的还是由编译器合成的无关紧要
4、派生类析构函数:对象的基类部分也是隐式销毁的。因此,和构造函数及赋值运算符不同的是,派生类析构函数只负责销毁派生类自己分配的资源:
class D : public Base {
public:// Base::~Base 被自动调用执行~D() { /* 该处由用户定义清除派生类成员的操作 */ }
};
5、在构造函数和析构函数中调用虚函数:派生类对象的基类部分 将首先被构建。若执行基类的构造函数,该对象的派生类部分是未被初始化的状态
当我们构建一个对象时, 需要把对象的类和构造函数的类看作是同一个:对虚函数的调用绑定 正好符合这种把对象的类和构造函数的类看成同一个的要求:对千析构函数也是同样的道理。 上述的绑定不但对直接调用虚函数有效, 对间接调用也是有效的, 这里的间接调用 是指通过构造函数(或析构函数) 调用另一个函数
当基类构造函数调用虚函数的派生类版本时,这个虚函数可能会访问派生类成员,不然 派生类直接使用基类的虚函数版本就可以了。 然而, 当执行基类构造函数时, 它要用到的派生类成员尚未初始化, 如果我们允许这样的访问, 则程序很可能会崩溃
如果构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或析构函数所属类型相对应的虚函数版本
6、定义 Quote 和 Bulk_quote 的拷贝控制成员,令其与合成的版本行为一致。为这些成员以及其他构造函数添加打印状态的语句,使得我们能够知道正在运行哪个程序
Quote_26.h
#pragma once
#ifndef QUOTE_H
#define QUOTE_H#include <string>
#include <iostream>class Quote {
public:// 构造函数Quote() {std::cout << "Quote默认构造函数" << std::endl;}Quote(const std::string& book, const double p) : bookNo(book), price(p) {std::cout << "Quote两个参数的构造函数" << std::endl;}// 拷贝运算符Quote(const Quote& q) : bookNo(q.bookNo), price(q.price) {std::cout << "Quote拷贝构造函数" << std::endl;}Quote& operator=(const Quote& q) {bookNo = q.bookNo;price = q.price;std::cout << "Quote拷贝赋值运算符" << std::endl;return *this; // 别忘了返回值}// 移动运算符,别忘了加noexceptQuote(Quote&& q) noexcept: bookNo(std::move(q.bookNo)), price(std::move(q.price)) {std::cout << "Quote移动构造函数" << std::endl;}Quote& operator=(Quote&& q) noexcept { // 只要是赋值运算符返回的都是引用bookNo = std::move(q.bookNo);price = std::move(q.price);std::cout << "Quote移动赋值运算符" << std::endl;return *this;}std::string isbn() const { return bookNo; }virtual double net_price(std::size_t n) const {return n * price;}virtual ~Quote() {std::cout << "Quote析构函数" << std::endl;}virtual void debug() const;
private:std::string bookNo;
protected:double price = 0.0;
};double print_total(std::ostream& os, const Quote& quote, std::size_t n) {double ret = quote.net_price(n); // 通过对象调用类内函数os << "ISBN:" << quote.isbn() << " " << "sold_num:" << n << " " << "total:" << ret << std::endl;return ret;
}void Quote::debug() const {std::cout << "bookNo: " << bookNo << " " << "price: " << price << std::endl;
}#endif
Bulk_quote.h
#pragma once
#ifndef BULK_QUOTE_26_H
#define BULK_QUOTE_26_H#include "Quote_26.h"class Bulk_quote :public Quote {
public:// 构造函数Bulk_quote() {std::cout << "Bulk_quote默认构造函数" << std::endl;}Bulk_quote(const std::string&, double, std::size_t, double);// 拷贝运算符Bulk_quote(const Bulk_quote& bq);Bulk_quote& operator=(const Bulk_quote& bq);// 移动运算符Bulk_quote(Bulk_quote&& bq) noexcept; // 没有noexcept就不是一个函数// 如果在一个地方声明了移动构造函数和移动赋值运算符带有 noexcept,而在另一个地方没有带 noexcept,// 这会导致链接器认为它们是不同的函数,从而引起多重定义问题Bulk_quote& operator=(Bulk_quote&& bq) noexcept;double net_price(std::size_t) const override;void debug() const override;virtual ~Bulk_quote() {std::cout << "Bulk_quote析构函数" << std::endl;}
private:std::size_t min_num = 0;double discount = 0.0;
};Bulk_quote::Bulk_quote(const std::string& book, double n, std::size_t mn, double dis) :Quote(book, n), min_num(mn), discount(dis) {std::cout << "Bulk_quote4个参数的构造函数" << std::endl;
}Bulk_quote::Bulk_quote(const Bulk_quote& bq) :Quote(bq), min_num(bq.min_num), discount(bq.discount)
{std::cout << "Bulk_quote拷贝构造函数" << std::endl;
}Bulk_quote& Bulk_quote::operator=(const Bulk_quote& bq) {Quote::operator=(bq);min_num = bq.min_num;discount = bq.discount;std::cout << "Bulk_quote拷贝赋值运算符" << std::endl;return *this;
}Bulk_quote::Bulk_quote(Bulk_quote&& bq) noexcept :Quote(bq), min_num(std::move(bq.min_num)), discount(std::move(bq.discount)) {// Quote(bq)不用加std::move,因为bq本来就是一个左值std::cout << "Bulk_quote移动构造函数" << std::endl;
}Bulk_quote& Bulk_quote::operator=(Bulk_quote&& bq) noexcept {Quote::operator=(bq);min_num = std::move(bq.min_num);discount = std::move(bq.discount);std::cout << "Bulk_quote移动赋值运算符" << std::endl;return *this;
}double Bulk_quote::net_price(std::size_t n) const {if (n >= min_num)return n * (1 - discount) * price;elsereturn n * price;
}void Bulk_quote::debug() const {std::cout << "min_num: " << min_num << " " << "discount: " << discount << std::endl;
}
#endif
15.26.cpp
#include "Bulk_quote_26.h"int main() {std::cout << "------Bulk_quote bulk(\"978 - 7 - 121 - 15535 - 2\", 100, 2, 0.2)------" << std::endl;Bulk_quote bulk("978-7-121-15535-2", 100, 2, 0.2); // 派生类对象std::cout << std::endl;std::cout << "------Bulk_quote bulk1 = bulk------" << std::endl;Bulk_quote bulk1 = bulk;std::cout << std::endl;std::cout << "------Bulk_quote bulk2; bulk2 = bulk------" << std::endl;Bulk_quote bulk2;bulk2 = bulk;std::cout << std::endl;std::cout << "------Bulk_quote bulk3 = std::move(bulk)------" << std::endl;Bulk_quote bulk3 = std::move(bulk);std::cout << std::endl;std::cout << "------Quote quote1 = bulk------" << std::endl;Quote quote1 = bulk;std::cout << std::endl;std::cout << "------Quote quote2 = std::move(bulk)------" << std::endl;Quote quote2 = std::move(bulk);std::cout << std::endl;return 0;
}
1)执行Bulk_quote bulk3 = std::move(bulk)
会先后调用
Quote拷贝构造函数
Bulk_quote移动构造函数
当执行 Bulk_quote bulk3 = std::move(bulk);
时,实际上发生的是通过移动构造函数来初始化 bulk3 对象。通常在这种情况下,应该调用 Quote 的移动构造函数和 Bulk_quote 的移动构造函数。然而,如果 Quote 的移动构造函数未正确声明为 noexcept,编译器可能会退回到使用 Quote 的拷贝构造函数
2)析构函数的调用次数依次是
Quote析构函数
Quote析构函数
Bulk_quote析构函数
Quote析构函数
Bulk_quote析构函数
Quote析构函数
1)对象的析构顺序
bulk 对象的创建和析构:
bulk 在 main 函数结束时析构。
首先调用 Bulk_quote 的析构函数,然后调用 Quote 的析构函数。
bulk1 对象的创建和析构:
bulk1 是通过拷贝构造函数创建的,所以 bulk1 在 main 函数结束时析构。
首先调用 Bulk_quote 的析构函数,然后调用 Quote 的析构函数。
bulk2 对象的创建和析构:
bulk2 是通过默认构造函数创建,然后通过赋值操作符赋值。
bulk2 在 main 函数结束时析构。
首先调用 Bulk_quote 的析构函数,然后调用 Quote 的析构函数。
bulk3 对象的创建和析构:
bulk3 是通过移动构造函数创建的,所以 bulk3 在 main 函数结束时析构。
首先调用 Bulk_quote 的析构函数,然后调用 Quote 的析构函数。
quote1 对象的创建和析构:
quote1 是通过拷贝构造函数创建的,所以 quote1 在 main 函数结束时析构。
仅调用 Quote 的析构函数。
quote2 对象的创建和析构:
quote2 是通过移动构造函数创建的,所以 quote2 在 main 函数结束时析构。
仅调用 Quote 的析构函数。
2)析构函数的调用顺序
quote1 的析构函数调用:
Quote 的析构函数。
quote2 的析构函数调用:
Quote 的析构函数。
bulk1 的析构函数调用:
Bulk_quote 的析构函数。
Quote 的析构函数。
bulk2 的析构函数调用:
Bulk_quote 的析构函数。
Quote 的析构函数。
bulk3 的析构函数调用:
Bulk_quote 的析构函数。
Quote 的析构函数。
bulk 的析构函数调用:
Bulk_quote 的析构函数。
Quote 的析构函数。
7.4 继承的构造函数
1、在 C++11 新标准中,派生类能够重用其直接基类定义的构造函数,这意味着派生类可以直接使用基类的构造函数来初始化自身
构造函数继承允许派生类继承基类的构造函数,这样可以避免在派生类中重新定义所有的构造函数。通过使用 using 关键字,派生类可以引入基类的构造函数,从而使派生类的对象构造更为简便和一致
基类 Base
#include <iostream>
#include <string>class Base {
public:Base() {std::cout << "Base 默认构造函数" << std::endl;}Base(const std::string& str) {std::cout << "Base 带参数的构造函数: " << str << std::endl;}Base(int x, double y) {std::cout << "Base 带两个参数的构造函数: " << x << ", " << y << std::endl;}
};
派生类 Derived
#include <iostream>
#include <string>class Derived : public Base {
public:using Base::Base; // 继承 Base 类的构造函数// 派生类自己的构造函数Derived() {std::cout << "Derived 默认构造函数" << std::endl;}
};
int main() {std::cout << "Creating Base objects:" << std::endl;Base b1;Base b2("Hello");Base b3(42, 3.14);std::cout << "\nCreating Derived objects:" << std::endl;Derived d1; // 调用 Derived 的默认构造函数Derived d2("World"); // 调用 Base 的带参数构造函数Derived d3(7, 8.91); // 调用 Base 的带两个参数的构造函数return 0;
}
输出结果
Creating Base objects:
Base 默认构造函数
Base 带参数的构造函数: Hello
Base 带两个参数的构造函数: 42, 3.14Creating Derived objects:
Derived 默认构造函数
Base 带参数的构造函数: World
Base 带两个参数的构造函数: 7, 8.91
派生类 Derived 的对象构造:
d1 调用了 Derived 的默认构造函数
d2 和 d3 分别调用了 Base 的带参数和带两个参数的构造函数,因为 Derived 使用了 using Base::Base 来继承 Base 的这些构造函数
一个类只初始化 它的直接基类,由于同样的原因,一个类也只继承其直接基类的构造函数。类不能继承默认、拷贝和移动构造函数(不使用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 声明语句将令编译器产生代码。对于基类的每个构造函数,编译器都会生成一个与之对应的派生类构造函数
对于基类的每个构造函数,编译器都在派生类中生成一个形参列表完全相同的构造函数
这些编译器生成的构造函数形如:
derived(parms) : base(args) { }
derived 是派生类的名字,base 是基类的名字,parms 是构造函数的形参列表,args 将派生类构造函数的形参传递给基类的构造函数
Bulk_quote(const std::string& book, double price,std::size_t qty, double disc) :Disc_quote(book, price, qty, disc) { }
如果派生类含有自己的数据成员,则这些成员将被默认初始化
2、以下几种类型的成员函数不会被自动继承:
- 构造函数:
尽管可以使用 using Base::Base; 来继承基类的构造函数,但默认情况下,构造函数不会被自动继承。这是因为派生类通常需要初始化自己的成员,而构造函数是专门用于对象初始化的。 - 析构函数:
派生类不会继承基类的析构函数。派生类会自动生成自己的析构函数,即使它只是在调用基类的析构函数。这是因为析构函数需要按特定顺序释放资源,首先是派生类的资源,然后是基类的资源。 - 拷贝赋值运算符:
拷贝赋值运算符 (operator=) 也不会被自动继承。派生类会自动生成自己的拷贝赋值运算符,但你可以在派生类中显式定义或使用 using 关键字来引入基类的拷贝赋值运算符。 - 移动赋值运算符:
移动构造函数和移动赋值运算符同样不会被自动继承。派生类需要显式定义这些运算符或者使用 using 关键字来继承
3、和普通成员的 using 声明不同,一个构造函数的 using 声明 不会改变该构造函数的访问级别
通过在派生类中使用 using 声明,可以将基类的成员(包括数据成员和成员函数)在派生类中重新声明为不同的访问级别
#include <iostream>class Base {
protected:void func() {std::cout << "Base::func()" << std::endl;}
};class Derived : public Base {
public:using Base::func; // 将 func 重新声明为 public
};int main() {Derived d;d.func(); // 现在可以在派生类对象上调用 func,因为它是 public 访问级别return 0;
}
注意:不能使用 using 声明来提升基类中的 private 成员的访问权限
只能改变 protected 和 public 成员的访问级别
4、一个 using 声明的构造函数不能指定 explicit 或 constexpr。如果基类的构造函数是 explicit 或者 constexpr 则继承的构造函数也拥有相同的属性
explicit 关键字用于构造函数,以防止编译器在需要转换时自动调用该构造函数,一般作用于 只有一个参数的构造函数
#include <iostream>class Base {
public:explicit Base(int x) {std::cout << "Base 带参数的构造函数: " << x << std::endl;}
};class Derived : public Base {
public:using Base::Base; // 继承 Base 的构造函数
};void print(const Base& b) {std::cout << "print(const Base&)" << std::endl;
}int main() {// Derived d = 42; // 错误:由于 explicit,不能进行隐式转换Derived d(42); // 正确:显式调用构造函数print(d);// print(42); // 错误:由于 explicit,不能进行隐式转换print(Base(42)); // 正确:显式调用构造函数return 0;
}
constexpr 用于指示表达式或函数在编译时求值。使用constexpr可以提高程序的效率,因为它允许编译器在编译时执行计算,从而避免在运行时进行重复计算
#include <iostream>class Base {
public:constexpr Base(int x) : value(x) {}constexpr int getValue() const { return value; }
private:int value;
};class Derived : public Base {
public:using Base::Base; // 继承 Base 的构造函数
};int main() {constexpr Base b(42); // 在编译时求值static_assert(b.getValue() == 42, "Value should be 42");constexpr Derived d(24); // 在编译时求值static_assert(d.getValue() == 24, "Value should be 24");std::cout << "Base value: " << b.getValue() << std::endl;std::cout << "Derived value: " << d.getValue() << std::endl;return 0;
}
1)constexpr 变量
constexpr 变量是指在编译时就能确定其值的变量。它类似于 const,但要求其值在编译时确定
2)constexpr 函数
constexpr 函数是指能够在编译时求值的函数。这样的函数必须满足以下条件:
- 函数体内只能包含常量表达式
- 返回类型和参数类型必须是字面值类型
- 函数体不能包含异常抛出
字面值类型:
算术类型:包括整数类型、浮点类型、字符类型等
枚举类型:枚举类型的所有成员都是常量表达式
指针和引用类型:指向字面值类型的指针和引用
数组类型:包含字面值类型的数组
类类型:满足特定条件的类类型
为了使一个类成为字面值类型,必须满足以下条件:
析构函数必须是平凡的(trivial),即不执行任何操作
数据成员必须是字面值类型或引用类型
构造函数必须是常量表达式,即能够在编译时求值
必须有一个constexpr构造函数(即使是默认的)(所有成员变量初始化必须是常量表达式;构造函数体内不能有任何可能导致运行时求值的操作)
struct Point {int x;int y;constexpr Point(int x_val, int y_val) : x(x_val), y(y_val) {}constexpr int getX() const { return x; }constexpr int getY() const { return y; }
};constexpr Point origin() {return Point(0, 0);
}int main() {constexpr Point p = origin();static_assert(p.getX() == 0, "X should be 0");static_assert(p.getY() == 0, "Y should be 0");return 0;
}
constexpr 与 const 的区别
constexpr:
用于指示表达式在编译时求值。
变量、函数、构造函数都可以是 constexpr。
constexpr 变量必须在声明时初始化。
const:
用于指示变量的值不能被修改。
只能用于变量和对象。
const 变量可以在运行时初始化
constexpr int factorial(int n) {return (n <= 1) ? 1 : (n * factorial(n - 1));
}int main() {constexpr int result = factorial(5); // 编译时计算static_assert(result == 120, "Value should be 120");// static_assert 是 C++11 引入的一个编译时断言机制,用于在编译时对表达式进行验证。主要用于在编译时检查常量表达式,并防止程序中的逻辑错误// 这里使用 static_assert 来检查 p.getX() 是否等于 1// 因为 p 是一个 constexpr 对象,p.getX() 也是一个 constexpr 表达式,可以在编译时计算// 如果 p.getX() 的结果不等于 1,编译器会生成错误消息 "X should be 1",并终止编译std::cout << "Factorial: " << result << std::endl;return 0;
}
5、当一个基类构造函数含有默认实参时,这些实参不会被继承。相反,派生类将获得多个继承的构造函数,其中每个构造函数分别省略一个含有默认参数的形参
例如,如果基类有一个接受两个形参的构造函数,其中第二个形参有默认实参,则派生类拷贝获得两个构造函数(两种省略方法:直接去掉和去默认实参):
一个构造函数接受两个形参(没有默认实参)
另一个构造函数仅接受一个形参,它对应基类中最左侧的没有默认值的那个形参(有默认形参的那个被省略了)
6、如果类含有几个构造函数,则两个例外情况。大多数情况下派生类会继承所有构造函数
第一个例外是派生类可以继承一部分构造函数,而其他构造函数定义自己的版本。如果派生类定义的构造函数与基类的构造函数具有相同的形参数列表,则该构造函数不会被继承。定义在派生类中的构造函数将替换派而来的构造函数。
第二个例外是默认、拷贝和移动构造函数不会被继承。这些构造函数按正常规则被合成
继承的构造函数不会被作为用户定义的构造函数被视作使用,因此,如果一个类只有继承的构造函数,则它也将拥有一个合成默认构造函数
即使这个类继承了 基类的构造函数,这些继承来的构造函数 不改变 编译器生成合成 默认构造函数的行为。这个合成的默认构造函数会隐式地调用 Base 的默认构造函数(如果 没有必须显式调用)
#include <iostream>class Base {
public:Base(int x) {std::cout << "Base 带参数的构造函数: " << x << std::endl;}
};class Derived : public Base {
public:using Base::Base; // 继承 Base 的构造函数// 如果没有其他用户定义的构造函数,编译器将合成一个默认构造函数
};int main() {Derived d1(42); // 使用继承的构造函数Derived d2; // 使用合成的默认构造函数return 0;
}
尝试使用无参数的默认构造函数创建 Derived 对象
Derived d2; // 使用合成的默认构造函数
编译器会生成一个合成的默认构造函数,类似于以下代码:
class Derived : public Base {
public:using Base::Base; // 继承 Base 的构造函数// 合成的默认构造函数Derived() : Base() {} // 或者其他适当的基类默认初始化
};
然而,Base 类没有无参数构造函数,因此编译器无法生成合成的默认构造函数,并且编译器会生成错误信息。为了解决这个问题,可以在 Base 类中添加一个无参数构造函数,或者显式定义 Derived 类的默认构造函数
1)为 Base 类添加无参数构造函数:
class Base {
public:Base() {std::cout << "Base 默认构造函数" << std::endl;}Base(int x) {std::cout << "Base 带参数的构造函数: " << x << std::endl;}
};
2)显式定义 Derived 类的默认构造函数:
class Derived : public Base {
public:using Base::Base; // 继承 Base 的构造函数// 显式定义默认构造函数Derived() : Base(0) {std::cout << "Derived 默认构造函数" << std::endl;}
};
可运行的代码
#include <iostream>class Base {
public:Base() {std::cout << "Base 默认构造函数" << std::endl;}Base(int x) {std::cout << "Base 带参数的构造函数: " << x << std::endl;}
};class Derived : public Base {
public:using Base::Base; // 继承 Base 的构造函数// 显式定义默认构造函数//Derived() : Base(0) { Derived() : Base() {// std::cout << "Derived 默认构造函数" << std::endl;//}
};int main() {Derived d1(42); // 使用继承的构造函数Derived d2; // 使用显式定义的默认构造函数return 0;
}
运行结果
#include <iostream>class Base {
public:/*Base() {std::cout << "Base 默认构造函数" << std::endl;}*/Base(int x) {std::cout << "Base 带参数的构造函数: " << x << std::endl;}
};class Derived : public Base {
public:using Base::Base; // 继承 Base 的构造函数// 显式定义默认构造函数Derived() : Base(0) {std::cout << "Derived 默认构造函数" << std::endl;}
};int main() {Derived d1(42); // 使用继承的构造函数Derived d2; // 使用显式定义的默认构造函数return 0;
}
运行结果
8、容器与继承
1、使用容器布置存储体系中的对象时,通常必须采用间接存储的方式。因为不允许容器中保存不同类型的元素,所以我们不能把具有继承关系的多种类型的对象直接存放在容器里
假定我们想定义一个 vector,其保存用户准备用实际的几种书籍。显然我们不能这样用 vector 保存 Bulk_quote 对象。因为我们不能将 Quote 对象转换成 Bulk_quote,所以我们将无法把 Quote 对象放置该 vector 中
也不应该使用 vector 保存 Quote 对象。此时,虽然我们可以把 Bulk_quote 对象放置在容器中,但是这些对象并不是 Bulk_quote 对象了:
vector<Quote> basket;
basket.push_back(Quote("0-201-82470-1", 50));
// 正确,但是只能把对象的 Quote 部分拷贝给 basket,派生类将会被“切掉”
basket.push_back(Bulk_quote("0-201-54848-8", 50, 10, .25));
// 调用 Quote 这个类的版本,打印 750, 即 15 * $50
cout << basket.back().net_price(15) << endl;
2、在容器中放置(智能)指针而非对象
在容器中存放 具有继承关系的对象时,实际上存放的通常是指针(更好的选择是智能指针)
这些指针所指向的动态类型 可能是基类类型,也可能是派生类类型:
vector<shared_ptr<Quote>> basket;
basket.push_back(make_shared<Quote>("0-201-82470-1", 50));
basket.push_back(make_shared<Bulk_quote>("0-201-54848-8", 50, 10, .25));
// 调用 Quote 定义的版本,打印 562.5,即 15 * $50 中价替折扣金额
cout << basket.back()->net_price(15) << endl;
解引用 basket.back() 的返回值以获得运行 net_price 的对象。实际调用的 net_price 版本依赖于指针所指对象的动态类型
正如 可以将一个派生类的普通指针转换成基类指针一样,我们也能把一个派生类的智能指针转换成基类的智能指针
make_shared<Bulk_quote>
返回一个 shared_ptr<Bulk_quote>
对象,当我们调用 push_back 时该对象被转换成 shared_ptr<Quote>
。因此尽管在底层上有所差别,但实际上 basket 的所有元素的类型都是相同的
定义一个存放 Quote 对象的 vector,将 Quote 对象的 shared_ptr / Bulk_quote 的 shared_ptr 传入其中。计算 vector 中所有元素总的 net_price
Quote.h
#pragma once
#ifndef QUOTE_H
#define QUOTE_H#include <string>
#include <iostream>class Quote {
public:Quote() = default;Quote(const std::string& book, const double p) : bookNo(book), price(p) {}std::string isbn() const { return bookNo; }virtual double net_price(std::size_t n) const {return n * price;}virtual ~Quote() = default;// 15.11 加入虚函数,virtual不需要再在类外再写一遍virtual void debug() const;
private:std::string bookNo;
protected:double price = 0.0;
};double print_total(std::ostream& os, const Quote& quote, std::size_t n) {double ret = quote.net_price(n); // 通过对象调用类内函数os << "ISBN:" << quote.isbn() << " " << "sold_num:" << n << " " << "total:" << ret << std::endl;return ret;
}void Quote::debug() const {std::cout << "bookNo: " << bookNo << " " << "price: " << price << std::endl;
}#endif
Disc_quote.h
#pragma once
#ifndef DISC_QUOTE_H
#define DISC_QUOTE_H#include "Quote.h"// 抽象基类
class Disc_quote : public Quote{
public:Disc_quote() = default;Disc_quote(const std::string &s, double p, double dis, std::size_t qua) :Quote(s, p), discount(dis), quantity(qua) {}double net_price(std::size_t) const override = 0;
protected: // 派生类是可以用的double discount = 0.0;std::size_t quantity = 0;
};
#endif
Bulk_quote_15.h
#pragma once
#ifndef BULK_QUOTE_15_H
#define BULK_QUOTE_15_H#include "Disc_quote.h"class Bulk_quote : public Disc_quote {
public:Bulk_quote() = default;Bulk_quote(const std::string &str, double p, double dis, std::size_t qua) :Disc_quote(str, p, dis, qua) {}double net_price(std::size_t) const override;
};double Bulk_quote::net_price(std::size_t n) const {if (n >= quantity)return n * (1 - discount) * price;elsereturn n * price;
}#endif
15.28.cpp
#include "Bulk_quote_15.h"
#include <vector>using namespace std;int main() {Bulk_quote bq("123", 19, 0.2, 10);Bulk_quote bq2("12", 19, 0.1, 10);print_total(cout, bq, 20);print_total(cout, bq2, 20);vector<shared_ptr<Quote>> vec;// 传入Quote对象的shared_ptr,转成quote再放进去就直接是quote了// 当派生类对象被赋值给基类对象时,其中的派生类部分将被“切掉”,因此容器和存在继承关系的类型无法兼容。// 而使用指针时,调用的是net_price版本依赖于指针所指对象的动态类型vec.push_back(make_shared<Quote>(bq)); vec.push_back(make_shared<Quote>(bq2));int total = 0;cout << "没有折扣的价格" << endl;for (auto& p : vec) {total += p->net_price(20);}cout << total << endl;total = 0;cout << "有折扣的价格" << endl;vector<shared_ptr<Quote>> vec2;// 注意比较区别,传入的是Bulk_quote对象的shared_ptrvec2.push_back(make_shared<Bulk_quote>(bq)); vec2.push_back(make_shared<Bulk_quote>(bq2));for (auto& p : vec2) {total += p->net_price(20);}cout << total << endl;return 0;
}
8.1 编写 Basket 类
1、无法直接使用对象进行面向对象编程。 必须使用指针和引用 。 因为指针会增加程序的复杂性, 所以我们经常定义一些辅助的类来处理 复杂情况
定义一个表示购物篮的类:
ptr1.reset(); // shared_ptr 的基本用法:使用 reset 方法重置指针,释放对象
class Basket {
public:// Basket 使用合成的默认构造函数和拷贝控制成员void add_item(const std::shared_ptr<Quote>& sale) {items.insert(sale);}// 打印每个报价的总价和篮子中所有书的总价double total_receipt(std::ostream&) const;private:// 比较书籍的ISBN,multiset 成员会用到它// 自定义比较函数static bool compare(const std::shared_ptr<Quote>& lhs, const std::shared_ptr<Quote>& rhs) {return lhs->isbn() < rhs->isbn();}// multiset保存多个报价, 按照compare成员排序std::multiset<std::shared_ptr<Quote>, decltype(compare)*> items{compare};// decltype(compare)*: 这是一个类型定义,表示 compare 函数的指针类型。decltype(compare) 获取了 compare 的类型// 后面的 * 表示这是一个指向 compare 函数的指针// items{compare}: 这是 multiset 的初始化列表,使用 compare 函数作为自定义比较器
};
multiset 容器可以接受两个模板参数:a 和 b。第一个参数 a 是元素类型,第二个参数 b 是用于比较元素的比较器类型
默认情况下,比较器类型是 std::less<a>
,这意味着元素按升序排序。可以自定义比较器类型来实现不同的排序规则
// 自定义比较器,按降序排序
struct MyCompare {bool operator()(const int& lhs, const int& rhs) const {return lhs > rhs;}
};// 创建一个 multiset,使用自定义比较器按降序排序
std::multiset<int, MyCompare> mset = {3, 1, 4, 1, 5, 9, 2, 6, 5};
因为 shared_ptr 没有定义小于运算符,所以 为了对元素排序我们必须提供自己的比较运算符
2、定义 Basket 的成员:第一个成员是 我们在类的内部定义的 add_item 成员,该成员接受一个指向动态分配的 Quote 的 shared_ptr,然后 将这个 shared_ptr 放置在 multiset 中。第二个成员的名字是 total_receipt,它负责将购物篮的内容逐项打印成清单,然后返回购物篮中所有物品的总价格:
double Basket::total_receipt(std::ostream &os) const {double sum = 0.0; // 保存实时计算出的总价格// iter 指向 ISBN 相同的第一批元素中的第一个// upper_bound 返回一个迭代器,该迭代器指向相同值的若干元素的尾后位置for (auto iter = items.cbegin(); iter != items.cend(); iter = items.upper_bound(*iter)) {// 我们知道在当前的Basket中至少有一个该关键字的元素// 打印该书籍对应的项目sum += print_total(os, **iter, items.count(*iter));}os << "Total Sale: " << sum << endl; // 打印最终的总价格return sum;
}
首先先定义并初始化 iter, 令其指向 multiset 的第一个元素
直接令 iter 指向下一个关键字, 调用 upper_bound 函数可以 令我们跳过与当前关键字相同的所有元素。对于 upper_bound 函数来说,它返回的是一个迭代器,该迭代器指向所有与 iter 关键字相等的元素中 最后一个元素的下一个位置
通过调用 print_total 来打印购物篮中每本书籍的细节:
sum += print_total(os, **iter, items.count(*iter));
解引用 iter 后将得到一个指向准备打印的对象的 shared_ptr。为了得到这个对象,必须解引用 shared_ptr。因此,**iter 是一个 Quote 对象(或者 Quote 的派生类的对象)
使用 multiset 的 count 成员 来统计在 multiset 中有多少元素的键值相同(即 ISBN 相同)
print_total 函数打印并返回给定书籍的总价格
3、隐藏指针:Basket 的用户仍然必须处理动态内存,原因是 add_item 需要接受一个 shared_ptr 参数
Basket bsk;
bsk.add_item(make_shared<Quote>("123", 45));
bsk.add_item(make_shared<Bulk_quote>("345", 45, 3, .15));
重新定义 add_item,使得它接受一个 Quote 对象而非 shared_ptr。将定义两个版本,一个拷贝给定的对象,另一个则采取移动操作
void add_item(const Quote& sale); // 拷贝给定对象
void add_item(Quote&& sale); // 移动给定对象
唯一的问题是 add_item 不知道要分配的类型
new Quote(sale)
这条表达式 将拷贝一个 Quote 类型的对象 并且拷贝 sale 的 Quote 部分。然而,sale 实际指向的是 Bulk_quote 对象,此时,该对象将被切掉一部分
4、模拟虚拷贝:为了避免上述问题,我们给 Quote 类添加一个虚函数,该函数 将申请一份当前对象的拷贝
class Quote {
public:// 该虚函数返回当前对象的一份动态分配的拷贝virtual Quote* clone() const & { return new Quote(*this); }virtual Quote* clone() && { return new Quote(std::move(*this)); }// 其他成员与之前的版本一致
};
class Bulk_quote : public Quote {Bulk_quote* clone() const & { return new Bulk_quote(*this); }Bulk_quote* clone() && { return new Bulk_quote(std::move(*this)); }// 其他成员与之前的版本一致
};
函数后面加上 & 或 && 是用于区分成员函数的引用限定符,这些限定符用于限制函数只能在特定类型的对象上调用
1)&(左值引用限定符):
函数后面的 & 表示这个成员函数只能被左值对象调用。左值是可以取地址的对象,通常是命名变量
2)&&(右值引用限定符):
函数后面的 && 表示这个成员函数只能被右值对象调用。右值通常是临时对象或将要被销毁的对象
#include <iostream>class Test {
public:// 只能被左值对象调用void left_value() & {std::cout << "Called on an lvalue" << std::endl;}// 只能被右值对象调用void right_value() && {std::cout << "Called on an rvalue" << std::endl;}
};int main() {Test t;// 左值对象调用左值限定符成员函数t.left_value(); // 输出 "Called on an lvalue"// 右值对象调用右值限定符成员函数Test().right_value(); // 输出 "Called on an rvalue"// 非法调用,会导致编译错误// t.right_value(); // 错误:没有匹配的函数// 非法调用,会导致编译错误// Test().left_value(); // 错误:没有匹配的函数return 0;
}
拥有 add_item 的拷贝和移动版本,所以 分别定义 clone 的左值和右值版本,每个 clone 函数分配当前类型的一个新对象,其中 const 左值引用将它自己拷贝给新分配的对象,右值引用版本则将自己 移动到新数据中
使用 clone 写出新版本的 add_item:
class Basket {
public:void add_item(const Quote& sale) {items.insert(std::shared_ptr<Quote>(sale.clone()));}void add_item(Quote&& sale) {items.insert(std::shared_ptr<Quote>(std::move(sale).clone()));}// 其他成员与之前的版本一致
};
和 add_item 本身一样,clone 函数也根据作用于左值还是右值 而分为不同的重载版本
尽管 sale 的类型是右值引用类型,但实际上 sale 本身(和任何其他变量一样)是个左值
参数 sale 的类型是 Quote&&,即右值引用。这意味着 add_item 函数可以接受一个右值来进行调用。然而,在函数体内,sale 本身是一个左值。也就是说,当我们在函数体内使用 sale 时,它是一个命名变量,所有命名变量都是左值
因此,我们调用 move 把一个右值引用绑定到 sale 上
items.insert(std::shared_ptr<Quote>(std::move(sale).clone()));
- std::move(sale)
首先,std::move(sale) 将 sale 对象转换为右值引用。右值引用允许对象的资源在不进行拷贝的情况下转移到另一个对象中 - sale.clone()
接下来,clone 函数被调用。由于 sale 已经是右值引用类型,所以调用的是右值引用版本的 clone 函数
这个 clone 函数将创建一个新的 Bulk_quote 对象,并使用 std::move 转移资源。这意味着新对象将继承 sale 对象的所有资源,而不需要拷贝它们
Bulk_quote* clone() && { return new Bulk_quote(std::move(*this)); }
std::shared_ptr<Quote>(new_quote_ptr)
接着,创建一个std::shared_ptr<Quote>
智能指针,管理 clone 返回的新分配的对象。std::shared_ptr 会负责对象的生命周期管理,确保对象在不再使用时自动释放内存
clone 函数也是一个虚函数。sale 的动态类型(通常)决定了到底运行 Quote 的函数还是 Bulk_quote 的函数
把一个 shared_ptr 绑定到这个对象上,因为 shared_ptr 支持派生类向基类的类型转换,所以我们能把 shared_ptr<Quote>
绑定到 Bulk_quote* 上
5、编写你自己的 Basket 类,计算 交易记录的总价格
Quote_30.h
#pragma once
#ifndef QUOTE_30_H
#define QUOTE_30_H#include <string>
#include <iostream>class Quote {
public:Quote() = default;Quote(const std::string& book, const double p) : bookNo(book), price(p) {}std::string isbn() const { return bookNo; }virtual double net_price(std::size_t n) const {return n * price;}virtual Quote *clone() const& {return new Quote(*this); // 调用的是拷贝构造函数(自动生成的 默认的拷贝构造函数)}virtual Quote* clone()&& {return new Quote(std::move(*this)); // 调用移动构造函数}virtual ~Quote() = default;virtual void debug() const;
private:std::string bookNo;
protected:double price = 0.0;
};double print_total(std::ostream& os, const Quote& quote, std::size_t n) {double ret = quote.net_price(n); // 通过对象调用类内函数os << "ISBN:" << quote.isbn() << " " << "sold_num:" << n << " " << "total:" << ret << std::endl;return ret;
}void Quote::debug() const {std::cout << "bookNo: " << bookNo << " " << "price: " << price << std::endl;
}#endif
Disc_quote_30.h
#pragma once
#ifndef DISC_QUOTE_H
#define DISC_QUOTE_H#include "Quote_30.h"// 抽象基类,不变
class Disc_quote : public Quote {
public:Disc_quote() = default;Disc_quote(const std::string& s, double p, double dis, std::size_t qua) :Quote(s, p), discount(dis), quantity(qua) {}double net_price(std::size_t) const override = 0;~Disc_quote() override = default;
protected: double discount = 0.0;std::size_t quantity = 0;
};
#endif
Bulk_quote_30.h
#pragma once
#ifndef BULK_QUOTE_30_H
#define BULK_QUOTE_30_H#include "Disc_quote_30.h"class Bulk_quote : public Disc_quote {
public:Bulk_quote() = default;Bulk_quote(const std::string& str, double p, double dis, std::size_t qua) :Disc_quote(str, p, dis, qua) {}double net_price(std::size_t) const override;virtual Bulk_quote* clone() const& {return new Bulk_quote(*this);}virtual Bulk_quote* clone() && {return new Bulk_quote(std::move(*this));}~Bulk_quote() override = default;
};double Bulk_quote::net_price(std::size_t n) const {if (n >= quantity)return n * (1 - discount) * price;elsereturn n * price;
}#endif
Basket.h
#pragma once
#ifndef BASKET_H
#define BASKET_H#include <set>
#include "Bulk_quote_30.h"class Basket {
public:void addItem(const Quote& q) {ms.insert(std::shared_ptr<Quote>(q.clone()));// q是一个左值,所以调用的是 clone 的 const & 版本,返回一个 Quote* 类型的指针// sale.clone() 返回的 Quote* 被传递给 std::shared_ptr<Quote> 的构造函数,std::shared_ptr 接管了这个指针的所有权,负责在适当的时候销毁它}void addItem(Quote&& q) {ms.insert(std::shared_ptr<Quote>(std::move(q).clone())); // std::move(q)先转右值}double total_rece() const;
private:static bool comp(const std::shared_ptr<Quote> &sq1, const std::shared_ptr<Quote> &sq2) { // 注意参数加const// 使用 左值引用来传递 std::shared_ptr<Quote> 对象// 普通指针可以使用引用传递,在需要传递指针并且不希望复制指针本身时非常有用//void modifyPointer(int*& ptr) {// // 修改指针本身,使其指向另一个地址// static int newVal = 20; // newVal 是一个静态变量,确保它在函数返回后仍然存在// ptr = &newVal;//}//void modifyValue(int* const& ptr) {// // 修改指针所指向的值,通过指针修改其指向的值,但不能修改指针本身// *ptr = 15;//}return sq1->isbn() < sq2->isbn();}std::multiset<std::shared_ptr<Quote>, decltype(comp)*> ms{ comp }; // 不是类型名 是函数名的时候一定用{}不能用(),且第二个参数为bool指针
};double Basket::total_rece() const {double sum = 0.0;for (auto it = ms.begin(); it != ms.end(); it = ms.upper_bound(*it)){sum += print_total(std::cout, **it, ms.count(*it));//std::multiset<int> mset = { 1, 2, 2, 3, 3, 3, 4, 5 }; 统计元素 2 在 multiset 中的出现次数//std::size_t count_2 = mset.count(2);}std::cout << "Total: " << sum << std::endl;return sum;
}#endif
15.30.cpp
#include "Basket.h"using namespace std;int main()
{Basket b;for (int i = 0; i < 6; i++) {b.addItem(Bulk_quote("123", 10.9, 0.2, 3));}for (int i = 0; i < 10; i++) {b.addItem(Bulk_quote("12", 9.9, 0.1, 5));}b.total_rece();return 0;
}
运行结果
9、文本查询程序再探
1、扩展 12.3 节 的文本查询程序,在上一版的程序中,我们可以查询在文件中 某个指定单词的出现情况
系统将支持如下查询形式:
- 单词查询,用于得到匹配某个给定 string 的所有行
- 逻辑非查询,使用 ~ 运算符得到不匹配查询条件的所有行
- 逻辑或查询,使用 | 运算符返回匹配两个条件中任意一个的行
- 逻辑与查询,使用 & 运算符返回匹配全部两个条件的行
- 还希望能够混合使用这些运算符:
fiery & bird | wind
,将使用 C++ 通用的优先级规则 对复杂表达式求值
9.1 面向对象的解决方案
1、可能会认为使用 12.3.2 节的 TextQuery 类来表示单词查询。然后 从该类中派生出其他查询是一种可行的方案
这样的设计实际上存在缺陷:不妨考虑逻辑非查询。单词查询指定一个指向的单词,为了让逻辑非查询 按照查询指向的方式执行,我们将不得不定义逻辑非查询所要查找的单词。一般情况下,我们无法得到这样的单词。相反,一个逻辑非查询中含有一个结果值需要取反的查询和(单词查询或其他任意查询)
应该有几种不同的查询 建模成相互独立的类,这些类共享一个公共基类:
WordQuery // Daddy
NotQuery // ~Alice
OrQuery // hair | Alice
AndQuery // hair & Alice
这些类将只包含两个操作:
- eval:接受一个 TextQuery 对象并返回一个 QueryResult,eval 函数使用给定的 TextQuery 对象查找与之匹配的行
- rep:返回基础查询的 string 表示形式,eval 函数使用 rep 创建一个表示匹配结果的 QueryResult,输出运算符使用 rep 打印查询表达式
2、关键概念:继承与组合
令一个类 公有地继承另一个类时,派生类应当反映与基类的 “是一个(Is A)” 关系。在设计良好的类体系中,公有派生类的对象应该可以用在 任何需要基类对象的地方
类型之间的另一种常见关系是 “有一个(Has A)”关系,具有这种关系的类 暗含成员的意思
Bulk_quote 是 “一种” 按规定价格销售的书籍的报价,只不过 它使用的价格策略不同。书店类都 “有一个” 价格成员和 ISBN 成员
3、抽象基类:在这几种查询之间 并不存在彼此的继承关系,从概念上来说 它们为兄弟。因为所有这些类都共享同一个接口,所以 需要定义一个抽象基类 来表示该接口
Query_base 类将 eval 和 rep 定义成纯虚函数。Query_base 类将把 eval 和 rep 定义成纯虚函数,其他代表某种特定查询类型的类必须覆盖这两个函数
将从 Query_base 直接派生出 WordQuery 和 NotQuery
AndQuery 和 OrQuery 都具有系统中其他类 所不具有的一个特殊属性:它们各自包含两个运算对象。为了对这种属性建模,我们定义另一个名为 BinaryQuery 的抽象类,该抽象类 将用于表示含有两个运算对象的查询。AndQuery 和 OrQuery 继承自 BinaryQuery,而 BinaryQuery 继承自 Query_base
4、将层次关系隐藏于接口类中
必须首先创建查询命令,可以编写下面的代码 来生成之前描述的复合查询 fiery & bird | wind
:
Query q = Query("fiery") & Query("bird") | Query("wind");
其隐含的意思是 用户层代码将不会直接使用这些继承的类。相反,将定义一个名为 Query 的接口类,由它负责隐藏 整个继承体系。Query 类 将保存一个 Query_base 的指针,该指针绑定到 Query_base 的派生类对象上。Query 类与 Query_base 类 提供的操作是相同的:eval 用于求查询的结果,rep 用于生成 查询的 string 版本,同时 Query 也会定义一个重载的输出运算符用于显示查询
用户将通过 Query 对象的操作 间接地创建并处理 Query_base 对象。我们定义 Query 对象有三个重载运算符 以及一个接受 string 参数的 Query 构造函数,这些函数 动态分配一个新的 Query_base 派生类的对象:
- & 运算符生成一个绑定到新的 AndQuery 对象上的 Query 对象
- | 运算符生成一个绑定到新的 OrQuery 对象上的 Query 对象
- ~ 运算符生成一个绑定到新的 NotQuery 对象上的 Query 对象
- 接受 string 参数的 Query 构造函数生成一个新的 WordQuery 对象
5、这些类的工作机理:
很大一部分工作是构建代表用户查询的对象,一旦对象构建完成后,对某一条查询语句的求值(或生成表示形式的)过程基本上就转换为对首指令方向依次对每个对象递归(或显示)的过程(由编译器为我们组织管理)
Query 程序接口类和操作
接口类 | 操作 |
---|---|
TextQuery | 该类给定的文件并构建一个查找图。这个类包含一个 query 操作,它接受一个 string 类型,返回一个 QueryResult 对象;该 QueryResult 对象表示 string 出现的行 |
QueryResult | 该类保存一个 query 操作的结果 |
Query | 是一个接口类,指向 Query_base 派生类的对象 |
Query q(s) | 将 Query 对象 q 绑定到一个存放着 string s 的新 WordQuery 对象上 |
q1 & q2 | 返回一个 Query 对象,该 Query 绑定到一个存放 q1 和 q2 的新的 AndQuery 对象上 |
q1 l q2 | 返回一个 Query 对象,该 Query 绑定到一个存放 q1 和 q2 的新的 OrQuery 对象上 |
~q | 返回一个 Query 对象,该 Query 绑定到一个存放 q 的新的 NotQuery 对象上 |
Query 程序实现类
类 | 操作 |
---|---|
Query_base | 查询类的抽象基类 |
WordQuery | Query_base 的派生类,用于查找一个给定的单词 |
NotQuery | Query_base 的派生类,查询结果是 Query 运算对象没有出现的行的集合 |
BinaryQuery | Query_base 派生出来的另一个抽象基类,表示有两个运算对象的查询 |
OrQuery | BinaryQuery 的派生类,返回它的两个运算对象分别出现的行的并集 |
AndQuery | BinaryQuery 的派生类,返回它的两个运算对象分别出现的行的交集 |
9.2 Query_base 类和 Query 类
1、首先定义 Query_base 类:
// 这是一个抽象类基类,具体的查询类型从中派生,所有成员都是 private 的
class Query_base {friend class Query;
protected:using line_no = TextQuery::line_no; // 用于 eval 函数virtual ~Query_base() = default;
private:// eval 返回与当前 Query 匹配的 QueryResultvirtual QueryResult eval(const TextQuery&) const = 0;// rep 返回表示查询的一个 stringvirtual std::string rep() const = 0;
};
eval 和 rep 都是纯虚函数,因此 Query_base 是一个抽象基类。因为 不希望用户或者派生类直接使用 Query_base,所以它没有 public 成员。所有对 Query_base 的使用 都需要通过 Query 对象,因为 Query 需要调用 Query_base 的虚函数,所以我们将 Query 声明为 Query_base 的友元
2、Query 类:Query 类对外提供接口,同时隐藏了 Query_base 的继承体系。每个 Query 对象都含有一个指向 Query_base 对象的 shared_ptr。因为 Query 是 Query_base 的唯一接口,所以 Query 必须定义 自己的 eval 和 rep 版本
接受一个 string 参数的 Query 构造函数 将创建一个新的 WordQuery 对象,然后将它的 shared_ptr 成员 绑定到这个新创建的对象上。&、| 和 ~ 运算符分别创建 AndQuery、OrQuery 和 NotQuery 对象,这些运算符将返回一个绑定到新创建的对象上的 Query 对象
为了支持这些运算符,Query 还需要另外一个构造函数,它接受指向 Query_base 的 shared_ptr 并且存储给定的指针。将这个构造函数声明为私有的,原因是 不希望一般的用户代码随便 定义 Query_base 对象。因为这个构造函数是私有的,所以我们还需要将三个运算符声明为友元
// 这是一个管理 Query_base 指针的条款类
class Query {// 这些运算符需要访问指向 Query_base 的 shared_ptr 的构造函数,而该函数是私有的friend Query operator~(const Query&);friend Query operator|(const Query&, const Query&);friend Query operator&(const Query&, const Query&);public:Query(const std::string&); // 构建一个新的 WordQuery// 接口函数:调用对应的 Query_base 操作QueryResult eval(const TextQuery& t) const { return q->eval(t); }std::string rep() const { return q->rep(); }private:Query(std::shared_ptr<Query_base> query) : q(query) { }std::shared_ptr<Query_base> q;
};
首先 将创建 Query 对象的运算符声明为友元,之所以这么做是 因为这些运算符需要访问那个私有构造函数
在 Query 的公有接口部分,我们声明了 接受 string 的构造函数,不过没有对其进行定义。因为这个构造函数将要创建一个 WordQuery 对象,所以我们应该首先定义 WordQuery 类
Query 操作使用它的 Query_base 指针 来调用各自的 Query_base 虚函数。实际调用 哪个函数将由 q 所指的对象类型决定,并且 直到运行时 才能最终确定下来
3、Query 的输出运算符
std::ostream& operator<<(std::ostream &os, const Query &query) {// Query::rep 通过它的 Query_base 指针对 rep() 进行虚调用return os << query.rep();
}
运算符函数通过指针调用当前 Query 所指对象的 rep 成员
Query andq = Query(sought1) & Query(sought2);
cout << andq << endl;
输出运算符将调用 andq 的 Query::rep,而 Query::rep 通过它的 Query_base 指针虚调用 Query_base 版本的 rep 函数
4、当一个 Query 类型的对象被拷贝、移动、赋值或销毁时,将分别发生什么
Query 类未定义自己的拷贝/移动控制成员,当进行这些操作时,执行默认语义。而其唯一的数据成员是 Query_base 的 shared_ptr,因此,当拷贝、移动、赋值或销毁一个 Query 对象时,会调用 shared_ptr 的对应控制成员,从而实现多个 Query 对象正确共享一个 Query_base。而 shared_ptr 的控制成员调用 Query_base 的控制成员时,由于指向的可能是 Query_base 的派生类对象,因此可能在类层次中进行相应的拷贝/移动操作,调用 Query_base 的派生类的相应控制成员
当一个 Query_base 类型的对象被拷贝、移动赋值或销毁时,将分别发生什么
Query_base 是一个虚基类,不允许直接声明其对象
当其派生类对象进行这些操作时,会调用 Query_base 的相应控制成员。而 Query_base 没有定义自己的拷贝/移动控制成员,实际上 Query_base 没有任何数据成员,无需定义这些操作。因此,进行这些操作时,执行默认语义,什么也不会发生
9.3 派生类
1、派生类是如何表示一个真实的查询。比如 WordQuery 派生自基类,它的任务就是 保存要查找的单词
其他类分别继承一个或两个运算对象。NotQuery 有一个运算对象,AndQuery 和 OrQuery 有两个。在这些类中,运算对象可以是 Query_base 的任意一个派生类的对象。一个 NotQuery 对象可以被用在 WordQuery、AndQuery、OrQuery 或另一个 NotQuery 中。为了支持这种灵活性,运算对象必须以 Query_base 指针的形式存储,这样 就把该指针绑定到 任何 需要的具体类上
实际上我们的类并不存储 Query_base 指针,而是 直接使用一个 Query 对象。就像用户代码可以通过接口类得到简化一样,我们也可以 使用接口类简化我们自己的类
2、WordQuery 类
一个 WordQuery 查找一个给定的 string,它是在给定的 TextQuery 对象上 实际执行查询的唯一一个操作:
class WordQuery : public Query_base {friend class Query; // Query 使用 WordQuery 构造函数WordQuery(const std::string &s) : query_word(s) { }// 具体类:WordQuery 将定义所有继承而来的纯虚函数QueryResult eval(const TextQuery &t) const{ return t.query(query_word); }std::string rep() const { return query_word; }std::string query_word; // 要查找的单词
};
Query 必须作为 WordQuery 的友元,这样 Query 才能访问 WordQuery 的构造函数
每个表示 具体查询的对象 都必须定义继承而来的纯虚函数 eval 和 rep。在 WordQuery 类的内部定义这两个操作:eval 调用其 TextQuery 参数的 query 成员,由query 成员 在文件中实际进行查找;rep 返回这个 WordQuery 表示的 string(即 query_word)
定义了 WordQuery 类之后,就能定义接受 string 的 Query 构造函数了:
inline Query::Query(const std::string &s) : q(new WordQuery(s)) { }
3、NotQuery 类及~运算符
~
运算符生成一个 NotQuery,其中保存着一个需要对其取反的 Query
class NotQuery : public Query_base {friend Query operator~(const Query &);NotQuery(const Query &q) : query(q) { }// 具体类:NotQuery 将定义所有继承而来的纯虚函数std::string rep() const { return "~(" + query.rep() + ")"; }QueryResult eval(const TextQuery &) const;Query query;
};inline Query operator~(const Query &operand)
{return std::shared_ptr<Query_base>(new NotQuery(operand));
}
因为 NotQuery 的所有成员都是私有的,所以我们一开始就把 ~ 运算符设定为友元。为了 rep 一个 NotQuery。我们需要将 ~ 符号与基础的 Query 连接在一起
在 NotQuery 自己的 rep 成员内对 rep 的调用 最终执行的是一个虚调用:query.rep() 是对 Query 类 rep 成员的虚调用,接着 query::rep 调用 q->rep(),这是一个通过 Query_base 指针进行的虚调用
~运算符动态分配一个新的 NotQuery 对象,其 return 语句隐式地使用 接受一个 shared_ptr<Query_base>
的 Query 构造函数。也就是说,return 语句等价于:
// 分配一个新的 NotQuery 对象
// 将所得的 NotQuery 指针绑定到一个 shared_ptr<Query_base>
shared_ptr<Query_base> tmp(new NotQuery(expr));
return Query(tmp); // 使用接受一个 shared_ptr 的 Query 构造函数
4、BinaryQuery 类:BinaryQuery 类也是一个抽象基类,它保存 操作两个运算对象的查询类型所需的数据:
class BinaryQuery : public Query_base {
protected:BinaryQuery(const Query &l, const Query &r, std::string s) :lhs(l), rhs(r), opSym(s) { }
// 抽象类:BinaryQuery 不定义 evalstd::string rep() const { return "(" + lhs.rep() + " "+ opSym + " "+ rhs.rep() + ")"; }Query lhs, rhs; // 左侧和右侧运算对象std::string opSym; // 运算符名字
};
对 rep 的调用最终是对 lhs 和 rhs 所指向的 Query_base 对象的 rep 函数进行虚调用
BinaryQuery 不定义 eval,而是继承了该纯虚函数。因此,BinaryQuery 也是一个抽象基类,我们不能创建 BinaryQuery 类型的对象
5、AndQuery 类、OrQuery 类及相应的运算符
class AndQuery : public BinaryQuery {friend Query operator&(const Query&, const Query&);AndQuery(const Query &left, const Query &right) :BinaryQuery(left, right, "&") { }// 具体的类: AndQuery 继承了 rep 并且定义了其他纯虚函数QueryResult eval(const TextQuery&) const;
};inline Query operator&(const Query &lhs, const Query &rhs)
{return std::shared_ptr<Query_base>(new AndQuery(lhs, rhs));
}class OrQuery: public BinaryQuery {friend Query operator|(const Query&, const Query&);OrQuery(const Query &left, const Query &right):BinaryQuery(left, right, "|") { }QueryResult eval(const TextQuery&) const;
};inline Query operator|(const Query &lhs, const Query &rhs)
{return std::shared_ptr<Query_base>(new OrQuery(lhs, rhs));
}
各自定义了一个构造函数 通过运算符创建 BinaryQuery 基类部分。它们继承 BinaryQuery 的 rep 函数,但是覆盖了 eval 函数
和 ~ 运算符一样,& 和 | 运算符也返回一个绑定到新分配对象上的 shared_ptr。在这 些运算符中,return 语句负责将 shared_ptr 转换成 Query
6、如果派生类中含有 shared_ptr<Query_base>
类型的成员而非 Query 类型的成员,将需要做出怎样的改变
涉及使用 Query 类型的地方,都要改成 Query_base 指针
7、下面的声明合法吗
BinaryQuery a = Query("fiery") & Query("bird");
AndQuery b = Query("fiery") & Query("bird");
OrQuery c = Query("fiery") & Query("bird");
(a)非法,BinaryQuery为抽象类;
(b)非法,返回的为Query类型,不能转换为AndQuery;
(c)非法,返回的为Query类型,不能转换为AndQuery
9.4 eval 函数
1、eval 函数是我们这个查询系统的核心。每个 eval 函数作用于各自的运算对象,同 时遵循的内在逻辑也有所区别:OrQuery 的 eval 操作返回两个运算对象查询结果的并 集,而 AndQuery 返回交集
NotQuery 的 eval 函数更为复杂些,它需 要返回运算对象没有出现的文本行
需要使用 QueryResult。假设 QueryResult 包含 begin 和 end 成员, 它们允许我们在 QueryResult 保存的行号 set 中进行迭代;另外假设 QueryResult 还包含一个名为 get_file 的成员,它返回一个指向查询文件的 shared_ptr
2、OrQuery::eval:一个 OrQuery 表示的是它的两个运算对象结果的并集,对于每个运算对象来说,我 们通过调用 eval 得到它的查询结果。因为这些运算对象的类型是 Query,所以调用 eval 也就是调用 Query::eval, 而后者实际上是对潜在的 Query_base 对象的 eval 进行虚调用(最终转成对 WordQuery.eval() 的调用)。每次调用完成后,得到的结果是一个 QueryResult,它表示运算对象出现的行号。 我们把这几行号组成一个新 set 中:
// 返回运算对象查询结果 set 的并集
QueryResult
OrQuery::eval(const TextQuery& text) const
{// 通过 Query 成员 lhs 和 rhs 进行的虚调用// 调用 eval 返回每个运算对象的 QueryResultauto right = rhs.eval(text), left = lhs.eval(text);// 将左侧运算对象的行号拷贝到结果 set 中auto ret_lines =make_shared<set<line_no>>(left.begin(), left.end());// 将右侧运算对象的行号插入到结果行号中ret_lines->insert(right.begin(), right.end());// 返回一个新的 QueryResult,它表示 lhs 和 rhs 的并集return QueryResult(rep(), ret_lines, left.get_file());
}
3、AndQuery::eval:AndQuery 的 eval 和 OrQuery 很类似,唯一的区别是它调用了一个标准库算法来 求得两个查询结果集中共有的行:
// 返回运算符对象查询结果 set 的交集
QueryResult
AndQuery::eval(const TextQuery& text) const
{// 通过 Query 运算对象进行的虚调用,以获得运算对象的查询结果 setauto left = lhs.eval(text), right = rhs.eval(text);// 保存 left 和 right 交集的 setauto ret_lines = make_shared<set<line_no>>();// 将两个范围的头尾写入一个目的迭代器中// 本次调用的目的迭代器向 ret_lines 加入元素,传入一个插入迭代器作为目的位置set_intersection(left.begin(), left.end(),right.begin(), right.end(),inserter(*ret_lines, ret_lines->begin()));return QueryResult(rep(), ret_lines, left.get_file());
}
使用标准库算法 set_intersection 来合并两个 set
std::inserter(result, result.begin())
创建一个插入迭代器,指向 result 的起始位置
set_intersection 算法接受五个迭代器。它使用前四个迭代器表示两个输入序列,最后一个参数表示目的位置。该算法将两个输入序列中 共同出现的元素写入到目的位置中
4、NotQuery::eval
// 返回运算对象的结果 set 中不存在的行
QueryResult
NotQuery::eval(const TextQuery& text) const
{// 通过 Query 运算对象对 eval 进行虚调用auto result = query.eval(text);// 开始时结果 set 为空auto ret_lines = make_shared<set<line_no>>();// 我们必须在运算对象结果的所有行中进行迭代auto beg = result.begin(), end = result.end();// 对于输入中的每一行,如果该行不在 result 当中,则将其添加到 ret_linesauto sz = result.get_file()->size();for (size_t n = 0; n != sz; ++n) {// 检查当前行是不是beg(只要不是就一定不在result中)if (beg == end || *beg != n)ret_lines->insert(n); // 如果不在 result 当中,添加这一行else if (beg != end)++beg; // 否则继续获取 result 的下一行 (如果有的话)}return QueryResult(rep(), ret_lines, result.get_file());
}
5、自己实现整个系统
QueryResult.h
#pragma once
#ifndef QUERYRESULT_H
#define QUERYRESULT_H#include <memory>
#include <string>
#include <vector>
#include <set>
#include <iostream>class QueryResult {friend std::ostream& print(std::ostream&, const QueryResult&);
public:typedef std::vector<std::string>::size_type line_no; // size_ttypedef std::set<line_no>::const_iterator line_it;QueryResult(const std::string &s, std::shared_ptr<std::set<line_no>> l, std::shared_ptr<std::vector<std::string>> f) :sought(s), lines(l), file(f) { }// std::shared_ptr 当按值传递 std::shared_ptr 时,会拷贝智能指针本身,并增加引用计数;// 当按引用传递 std::shared_ptr 时,不会拷贝智能指针本身,也不会增加引用计数,只有加const才能顺利引用传入std::set<line_no>::size_type size() const { return lines->size(); }line_it begin() const { return lines->begin(); }line_it end() const { return lines->end(); }std::shared_ptr<std::vector<std::string>> get_file() { return file; }
private:std::string sought; // 这个查找代表的语句std::shared_ptr<std::set<line_no>> lines;std::shared_ptr<std::vector<std::string>> file;
};#endif
QueryResult.h
#pragma once
#ifndef QUERYRESULT_H
#define QUERYRESULT_H#include <memory>
#include <string>
#include <vector>
#include <set>
#include <iostream>class QueryResult {friend std::ostream& print(std::ostream&, const QueryResult&);
public:typedef std::vector<std::string>::size_type line_no; // size_ttypedef std::set<line_no>::const_iterator line_it;QueryResult(const std::string &s, std::shared_ptr<std::set<line_no>> l, std::shared_ptr<std::vector<std::string>> f) :sought(s), lines(l), file(f) { }// std::shared_ptr 当按值传递 std::shared_ptr 时,会拷贝智能指针本身,并增加引用计数;// 当按引用传递 std::shared_ptr 时,不会拷贝智能指针本身,也不会增加引用计数,只有加const才能顺利引用传入std::set<line_no>::size_type size() const { return lines->size(); }line_it begin() const { return lines->begin(); }line_it end() const { return lines->end(); }std::shared_ptr<std::vector<std::string>> get_file() { return file; }
private:std::string sought; // 这个查找代表的语句std::shared_ptr<std::set<line_no>> lines;std::shared_ptr<std::vector<std::string>> file;
};#endif
TextQuery.h
#pragma once
#ifndef TEXTQUERY_H
#define TEXTQUERY_H#include "QueryResult.h"
#include <string>
#include <map>
#include <fstream>class TextQuery {
public:TextQuery(std::ifstream&);QueryResult query(const std::string&);
private:std::shared_ptr<std::vector<std::string>> file;std::map<std::string, std::shared_ptr<std::set<QueryResult::line_no>>> m; // 单词和行键值对// line_no想要使用要不就是全局变量,要不要指明作用域std::string get_word(const std::string& str);
};#endif
TextQuery.cpp
#include "TextQuery.h"
#include "MakePlural.h"
#include <sstream>typedef std::map<std::string, std::shared_ptr<std::set<QueryResult::line_no>>> mType;
typedef mType::mapped_type lType;
// mapped_type 是 std::map 的成员类型,表示 map 的值类型typedef mType::const_iterator mIter;
// 键(first): 总是 const,无论是 const_iterator 还是非 const 迭代器。
// 值(second):
// 对于 const_iterator:second 是 const,不能修改。
// 对于非 const 迭代器:second 是非 const,可以修改TextQuery::TextQuery(std::ifstream& is) : file(new std::vector<std::string>) {std::string text;while (getline(is, text)) {file->push_back(text);int n = file->size() - 1; // 当前在文件中的行号std::istringstream line(text);std::string word;while (line >> word) {word = get_word(word); // 将 word 对应的行号集合的智能指针赋值给 lineslType &lines = m[word];if (!lines)lines.reset(new std::set<QueryResult::line_no>);// 如果 lines 之前是空的(即 nullptr),现在它将指向一个新的空的 std::set<QueryResult::line_no>准备插入lines->insert(n);}}
}std::string TextQuery::get_word(const std::string& str) { // 返回值的引用别忘了std::string res;for (std::string::const_iterator it = str.begin(); it != str.end(); it++) {if (!ispunct(*it))// ispunct 是一个标准库函数,用于检查给定字符是否是标点符号res += tolower(*it);}return res;// 这里的返回值不能用引用,因为res马上就销毁了,只能复制
}QueryResult TextQuery::query(const std::string& sought) { // const要加的话get_word也要是const// 在 const 成员函数中调用非 const 成员函数是不允许的。TextQuery::get_word 需要是一个 const 成员函数,因为在 const 成员函数 TextQuery::query 中调用了它lType noData(new std::set<QueryResult::line_no>);mIter it = m.find(get_word(sought));if (it == m.end()) {return QueryResult(sought, noData, file); // 第一个参数在声明时就需要是const(sought为const)}else {return QueryResult(sought, it->second, file); // 第二个参数在声明时不需要是const,虽然it->second是const(const_iterator成员const)// 即使 std::shared_ptr 本身是 const,你仍然可以将它赋值给另一个 std::shared_ptr,因为赋值操作会增加引用计数,而不会修改 const std::shared_ptr 本身}
}std::ostream& print(std::ostream& os, const QueryResult& qr) {os << qr.sought << " occurs " << qr.lines->size() << " " << make_plural(qr.lines->size(), "time", "s") << std::endl;for (QueryResult::line_it line_i = qr.lines->begin(); line_i != qr.lines->end(); line_i++) {os << "\t(line " << *line_i + 1 << ")" << *(qr.file->begin() + *line_i) << std::endl;}return os;
}
Query.h
#pragma once
#ifndef QUERY_H
#define QUERY_H#include "QueryResult.h"
#include "TextQuery.h"class Query_base {friend class Query;
protected:typedef QueryResult::line_no line_no; virtual ~Query_base() {}
private:virtual QueryResult eval(TextQuery&) const = 0; // 返回查询结果QueryResultvirtual std::string rep() const = 0; // 查询的字符串表达式
};class Query {// 这些运算符需要访问shared_ptr制造器friend Query operator~(const Query&);friend Query operator|(const Query&, const Query&);friend Query operator&(const Query&, const Query&);
public:Query(const std::string&); // 构造新的WordQuery,等WordQuery定义好了再实现// 通过动态指针 调用对应的每个类自己实现的继承自Query_base的函数,所以可以成为统一类QueryResult eval(TextQuery& t) const {return q->eval(t);}std::string rep() const { return q->rep(); }
private:Query(std::shared_ptr<Query_base> query) : q(query) {}std::shared_ptr<Query_base> q;
};inline std::ostream& operator<<(std::ostream& os, const Query& query) {return os << query.rep();
}class WordQuery : public Query_base {friend class Query; // Query使用WordQuery构造函数WordQuery(const std::string &s) : query_word(s) { }QueryResult eval(TextQuery& t) const override { return t.query(query_word); }std::string rep() const override { return query_word; }std::string query_word; // 搜索哪个单词
};inline Query::Query(const std::string &s) : q(new WordQuery(s)) {}class NotQuery : public Query_base {friend Query operator~(const Query&);NotQuery(const Query &q) : query(q) {}std::string rep() const override { return "~(" + query.rep() + ")"; }QueryResult eval(TextQuery&) const override;Query query;
};class BinaryQuery : public Query_base {
protected:BinaryQuery(const Query &l, const Query &r, std::string s) : lhs(l), rhs(r), opName(s) {}std::string rep() const override {return "(" + lhs.rep() + " " + opName + " " + rhs.rep() + ")";}Query lhs, rhs; // 两个操作数std::string opName; // 操作名称
};class AndQuery : public BinaryQuery {friend Query operator&(const Query&, const Query&);AndQuery(const Query &left, const Query &right) : BinaryQuery(left, right, "&") {}QueryResult eval(TextQuery&) const override; // 参数不能自己加const,虚函数覆盖必须完全一致
};class OrQuery : public BinaryQuery {friend Query operator|(const Query&, const Query&);OrQuery(const Query &left, const Query &right) : BinaryQuery(left, right, "|") {}QueryResult eval(TextQuery&) const override;
};inline Query operator&(const Query& lhs, const Query& rhs) {return std::shared_ptr<Query_base>(new AndQuery(lhs, rhs)); // 把虚函数eval正确覆盖后才能用AndQuery构造,之所以能访问AndQuery是因为是友元
}inline Query operator|(const Query& lhs, const Query& rhs) {return std::shared_ptr<Query_base>(new OrQuery(lhs, rhs));
}inline Query operator~(const Query& query) {return std::shared_ptr<Query_base>(new NotQuery(query));
}std::ifstream& open_file(std::ifstream&, const std::string&);TextQuery get_file(int, char**);bool g_word(std::string&);bool g_words(std::string&, std::string&);#endif
Query.cpp
#include "Query.h"
#include <algorithm>QueryResult NotQuery::eval(TextQuery& text) const {QueryResult res = query.eval(text); // 构造NotQuery时(NotQuery(const Query &q):query(q){})参数传进去的query中的q是哪个类型的就是哪个类型(就是WordQuery)std::shared_ptr<std::set<QueryResult::line_no>> res_lines(new std::set<QueryResult::line_no>); // 创建空的QueryResult::line_it beg = res.begin(), end = res.end();// 遍历文件里的每一行,把不在res里面行加入到res_lines中std::vector<std::string>::size_type s = res.get_file()->size(); // 调用的QueryResult中的get_file,返回std::shared_ptr<std::vector<std::string>>for (size_t n = 0; n != s; n++) {if (beg == end || *beg != n) // beg始终指向下一个res里面的行res_lines->insert(n);else if (beg != end) // 相等了就转到下一个++beg;}return QueryResult(rep(), res_lines, res.get_file()); // 第三个参数要加const才行const std::shared_ptr<std::vector<std::string>>或者去掉引用符号
}QueryResult OrQuery::eval(TextQuery& text) const { // 如果text是const下面的eval参数不是const就传不进去QueryResult left = lhs.eval(text), right = rhs.eval(text);std::shared_ptr<std::set<QueryResult::line_no>> res_lines(new std::set<QueryResult::line_no>(left.begin(), left.end()));// 先把左边查询结果在初始化时放进去,后面再加上右边的res_lines->insert(right.begin(), right.end());std::cout << res_lines->size() << std::endl;return QueryResult(rep(), res_lines, left.get_file());
}QueryResult AndQuery::eval(TextQuery& text) const {QueryResult left = lhs.eval(text), right = rhs.eval(text);std::shared_ptr<std::set<QueryResult::line_no>> res_lines(new std::set<QueryResult::line_no>);std::set_intersection(left.begin(), left.end(),right.begin(), right.end(),std::inserter(*res_lines, res_lines->begin()));return QueryResult(rep(), res_lines, left.get_file());
}
get_print.cpp
#include "Query.h"
#include <stdexcept>
#include <fstream>// 都定义在Query.h中
TextQuery get_file(int argc, char** argv) {std::ifstream infile;if (argc == 2)infile.open(argv[1]);if (!infile) {throw std::runtime_error("No input file!");}return TextQuery(infile);
}bool g_word(std::string& s1) {std::cout << "enter a word to search for, or q to quit: ";std::cin >> s1;if (!std::cin || s1 == "q") return false;elsereturn true;
}bool g_words(std::string& s1, std::string& s2) {std::cout << "enter two words to search for, or q to quit: ";std::cin >> s1;if (!std::cin || s1 == "q")return false;std::cin >> s2;return true;
}
15.39.cpp
#include "Query.h"
#include "TextQuery.h"using namespace std;int main(int argc, char **argv)
{TextQuery file = get_file(argc, argv); // get_file通过TextQuery构造函数 搞定了文件的mapwhile (true) {std::string sought1, sought2, sought3;if (!g_words(sought1, sought2)) break;cout << "\nenter third word: ";cin >> sought3;Query q = Query(sought1) & Query(sought2) | Query(sought3); // q是OrQuery,是由AndQuery和WordQuery作为参数const QueryResult results = q.eval(file); // OrQuery的eval,去调AndQuery的eval和WordQuery的eval,AndQuery调两个WordQuery的evalprint(cout, results);}return 0;
}
运行结果
在 OrQuery 的 eval 函数中,如果 rhs 成员返回的是空集将发生什么
OrQuery 的 eval 从 lhs 和 rhs 获取范围来构造 set(或向其插入),而 set 的构造和插入操作可以正确处理空范围,因此无论 lhs 和 rhs 的结果是否为空集,eval 都能得到正确结果
重新实现类,这次使用指向 Query_base 的内置指针而非 shared_ptr
1)智能指针(版本二) 原始指针(版本一)
第一个版本:
使用原始指针和手动引用计数来管理内存
使用 int* uc 来手动管理引用计数,增加了代码的复杂性和内存管理的负担
Query_base *q;
int *uc;
第二个版本:
使用 std::shared_ptr 来管理内存,这样可以更安全和方便地进行内存管理
std::shared_ptr 自动处理引用计数和内存释放,减少了手动管理的错误
std::shared_ptr<Query_base> q;
2)构造函数和赋值操作符
第一个版本:
拷贝构造函数和赋值操作符都需要手动处理引用计数
由于使用原始指针和手动引用计数,析构函数也需要手动管理内存释放
Query(const Query & query) : q(query.q), uc(query.uc) { ++*uc; }
Query& operator=(const Query& query) {++*query.uc;if (--*uc == 0) {delete q;delete uc;}q = query.q;uc = query.uc;return *this;
}
~Query() {if (--*uc == 0) {delete q;delete uc;}
}
第二个版本:
利用 std::shared_ptr,拷贝构造函数和赋值操作符自动处理引用计数和内存管理。
析构函数由 std::shared_ptr 自动管理,无需手动处理。
Query(const std::string &s) : q(new WordQuery(s)) {}
3)友元函数和运算符重载
第一个版本:
使用原始指针时,运算符重载的实现需要手动管理对象的创建和删除
inline Query operator&(const Query &lhs, const Query &rhs) {return new AndQuery(lhs, rhs);
}inline Query operator|(const Query &lhs, const Query &rhs) {return new OrQuery(lhs, rhs);
}inline Query operator~(const Query &operand) {return new NotQuery(operand);
}
第二个版本:
使用 std::shared_ptr 时,运算符重载的实现更加简洁和安全。
inline Query operator&(const Query& lhs, const Query& rhs) {return std::shared_ptr<Query_base>(new AndQuery(lhs, rhs));
}inline Query operator|(const Query& lhs, const Query& rhs) {return std::shared_ptr<Query_base>(new OrQuery(lhs, rhs));
}inline Query operator~(const Query& query) {return std::shared_ptr<Query_base>(new NotQuery(query));
}
第二个版本使用 std::shared_ptr 进行内存管理,这使得代码更安全、简洁且易于维护。std::shared_ptr 自动处理内存分配和释放,减少了手动管理内存的复杂性和错误的可能性
第一个版本虽然实现了相似的功能,但由于使用原始指针和手动引用计数,增加了代码的复杂性和内存管理的风险
因此,推荐使用第二个版本,利用现代 C++ 标准库中的智能指针来管理内存,这样可以提高代码的安全性和可维护性
小结
在C++语言中,动态绑定只作用于虚函数,并且需要通过指针或引用调用
将基类的析构函数定义成虚函数的原因是 为了确保当我们删除一个基类指针,而该指针实际指向 一个派生对象时,程序也能正确运行
术语表
动态绑定:直到运行时才确定到底执行函数的哪一版本。在 C++ 语言中动态绑定的意思是运行时根据对象的动态类型来选择执行虚函数的某一个版本。
动态类型:对象在运行时的类型。引用所引的对象 或者 指针所指对象的动态类型 可能与 该引用或指针的静态类型不同。基类的指针和引用可以指向一个派生类的对象:静态类型是 基类的引用(或指针),而 动态类型是 派生类的引用(或指针)