第十五章 面向对象程序设计
三个基本概念: 数据抽象, 继承和动态绑定
15.1 OOP
核心思想是 数据抽象, 继承和动态绑定
- 抽象: 分离类的接口与实现
- 继承: 可以定义相似的类型并对其相似关系建模
- 动态绑定: 可以在一定程度上忽略象类型的区别, 以统一的方式使用他们的对象
继承: 基类 派生类, 虚函数(virtual), 类派生列表(即父类列表, 类后面跟冒号, 多个基类用逗号分割, 基类前面可以有访问说明符) , 可以从多个类继承.
动态绑定: 因为派生类是继承自基类, 在某些时候, 派生类可以当做基类使用, 所以在定义一个函数时, 形参使用基类的情况下, 如果传入的是基类则调用的基类的数据, 传入的派生类对象 则使用的是派生类的方法, 这就是动态绑定.
double print_total(ostream& os, const Quote& item, size_t n) {
double ret = item.net_price(n);
os << "ISBN: " << item.isbn() << " # sold: " << n << " total due: " << ret << endl;
return ret;
}
int main(int argc, char** argv) {
print_total(cout, basic, 20); // basic 基类对象, 则调用 basic.isbn, basic.net_price
print_total(cout, bulk, 20); // bulk 是basic类的派生类对象, 则调用bulk.isbn, bulk.net_price
}
动态绑定 其基本思想就是 派生类可以当成基类使用, 派生类只是扩展了基类属性, 根本思想还是基类. 如 无论是白猫还是黑猫 都是猫, 都具备猫的基本特征.
virtual 和override的作用:
- 基类函数没加virtual,子类有相同函数,实现的是覆盖. 用基类指针调用时,调用到的是基类的函数, 用子类指针调用时,调用到的是子类的函数.各自指针调用各自的函数.没有任何关系.
- 基类函数加了virtual时,实现的是重写: 用基类指针调用基类函数, 值是子类的基类指针或子类指针调用时,调用到的都是子类的函数。
- 函数加上override,强制要求基类必须具有相同的virtual函数,否则会编译报错
class A
{
public:
void foo() { printf("A foo \n"); }
virtual void fun() { printf("A fun \n"); } // 带了virtual 用于派生类覆盖
};
class B : public A
{
public:
void foo() { printf("B foo \n"); } //隐藏:派生类的函数屏蔽了与其同名的基类函数
void fun() { printf("B fun \n"); } //多态、覆盖
};
int main(int argc, char** argv) {
A a;
B b;
A* pa = &a;
B* pb = &b;
A* ppa = &b;
// 多态 体现在 使用父类型的指针如pa,如果实际的赋值可能是派生类型时
// 此时pa在调用virtual的函数时, 指向的将是派生类的函数而非本身的函数
pa->foo(); //运行父类foo
pa->fun(); //运行父类fun
pa = &b;
pa->foo(); //运行父类foo
pb->foo(); //运行B类 foo
pa->fun(); //运行子类B的fun
}
15.2 基类和派生类
15.2.1 定义基类
基类中通常会定义一个虚析构函数, 即使是什么也不干.
有virtual的虚函数, 派生类必须实现自己的定义. 除了静态函数和构造函数之外 都可以是virtual函数.其在派生类中也是virtual的.
15.2.2 定义派生类
类派生列表: 每个基类前面都可以有三种public, protected, private中的一种修饰符. 其含义是: 控制派生类从基类继承而来的成员 是否对派生类的用户可见, 即: 以什么方式继承基类的共有数据成员.
关于派生类修饰符以及类内修饰符的关系可以看下图:
可以理解为基类中的修饰符和派生类列表中的修饰符, 两者合并后的作用总是向更高一级的安全靠.
- 对基类的访问权限只与基类中的访问说明符有关
- 两者公共作用的结果是派生类的对象对基类对象的访问控制以及派生类的继承类
- 只有public继承方式才能使用派生类对象向基类指针/引用 赋值
使用基类指针指向派生类对象, 可以调用派生类方法. 但是并非所有的派生类方法都可以通过基类的指针调用, 基类指针只能调用: 1. 基类中的virtual方法 2. 派生类中重写了基类virtual的方法. 这两者缺一不可. 由此可以理解 派生类对象中含有与其基类对应的组成部分. 所以 使用基类的地方 可以使用派生类.
派生类 只能执行基类的默认构造函数, 其他情况需要派生类显式调用基类构造函数.
继承与静态成员
如果基类中定义了一个静态成员, 则整个继承体系中只存在该成员的唯一定义.
派生类的声明: 派生类的声明跟普通类相同, 不能有继承类的说明.
基类在使用前必须是已经定义的而非仅仅是声明的, 由此一个类不能派生其本身.
防止继承 final: 如果类不想被作为基类, 需要在类名后面添加final
class A final {}; // A 不能被继承
15.2.3 类型转换与继承
派生类的地址可以绑定到基类的指针/智能指针上.
静态类型与动态类型
如果表达式既不是引用也不是指针, 则它的动态类型和静态类型永远一致.
静态类型 在编译时总是已知的, 是变量声明时或表达式生成的类型.
动态类型: 是变量或表达式表示的内存中的对象的类型. (多一层关系)
不存在从基类向派生类的隐式类型转换: 这很好理解, 一只白猫(派生类) 肯定是猫(基类)的一种, 而一只猫(基类)可一定是白猫(派生类);
派生类向基类的自动类型转换 只对指针或引用类型有效.
当使用一个派生类对象为一个基类对象初始化或赋值时, 只有派生类中的基类部分会被拷贝,移动或赋值, 派生类特殊的部分会被忽略掉. 以为基类/派生类在调用自身的构造/赋值函数时只处理其自身的部分.
15.3 虚函数
- 所有虚函数都必须有定义
- 对虚函数的调用可能在运行时才被解析
- 当且仅当对通过指针或引用调用虚函数时, 才会在运行时解析该调用, 也只有在这种情况下对象的动态类型才有可能与静态类型不同
- 一旦某个函数被声明成虚函数, 则在所有的派生类中它都是虚函数.
- 虚函数可以被覆盖, 但是形参和返回类型(返回自身的指针或引用是可以不同)必须与基类相同.
final 和 override
override 在派生类中明确这是虚函数的重定义, 不能覆盖基类(不同名, 同名不同参等) 都会报错.
final: 在函数的后面定义的final, 则任何派生类 都不能覆盖 该方法
虚函数与默认实参
虚函数中的默认实参跟指针/引用的类型有关. 即: 如果使用基类指针调用派生类, 实参也是基类中定义的默认实参.
回避虚函数的机制
如果需要强制使用某个版本的虚函数, 可以使用作用域符号.
class base {
public:
virtual void example() {
// xx
};
};
class sub :public base {
public:
void example() override {
// xx
}
};
int main(int argc, char** argv) {
base A;
sub B;
base* pa = &B;
pa->example(); // 调用派生类 中的example
pa->base::example(); // 强制调用 基类中的example
}
15.4 抽象基类
纯虚函数:
- 无须定义只需声明
- 未使用virtual
- 在声明语句的分号之前 写 =0 即可. =0 只出现在类内部
- 可以为纯虚函数提供定义, 但是定义只能在类外
double net_price(std::size_t) const = 0;
抽象基类:
- 包含纯虚函数的类叫抽象基类
- 不能直接创建一个抽象基类的对象, 其派生类(必须覆盖了纯虚函数)可以
派生类构造函数只初始化它的直接基类
重构: 改变类的继承体系.
15.5 访问控制与继承
protected:
- 受保护的成员对于类的对象来说是不可访问的
- 受保护的成员对于派生类的成员或友元来说是可访问的
- 派生类的成员或友元只能通过派生类来访问基类受保护的成员
- 派生类的成员或友元不能通过基类对象访问基类受保护的成员
以上可以归纳为: 基类中受保护的成员 只对派生类开放, 即只能在派生类内使用, 其余地方均不可见,即使是基类对象.
class s {
protected:
int prot_mem;
public:
int pub_mem;
};
class ss :public s {
friend void ssa(ss&);
friend void ssb(s&);
int j;
};
void ssa(ss& ssr) {
int b = ssr.j;
int s = ssr.prot_mem; // 派生类的友元可以通过 派生类对象访问基类受保护的成员
}
void ssb(s& sr) {
int b1 = sr.pub_mem;
int b = sr.prot_mem; // 派生类的友元 不能通过基类访问受保护的成员
}
类内public,private, protected和派生类列表中的public, private, protected的共同作用 如15.2.2
派生类向基类转换的可访问性:如15.2.2
友元与继承:友元关系不能传递 也不能继承, 友元不能访问私有成员.
改变个别成员的可访问性: using
class Base_p545 {
public:
std::size_t size() const { return n; }
protected:
std::size_t n;
private:
using Base_p545::m;
};
class Derived_p546 :private Base_p545 { // 此处使用private
public:
using Base_p545::size;
public:
using Base_p545::n;
private:
using Base_p545::m; // 私有成员 不可以
};
// 使用using 改变可访问性后, Derived_p546的用户/派生类将可以使用size和n
// 使用using后的成员的访问属性, 受派生类的访问修饰符决定
默认的继承保护级别: 使用class关键字定义的派生类是私有继承的; 使用struct关键字定义的派生类是共有继承的.
15.6 继承中的类作用域
派生类的作用域嵌套在其基类的作用域之内.
在编译时进行名字查找: 从本类开始查找, 直至最后的基类结束.
名字冲突与继承: 内层定义 隐藏 外层定义(即使是形参不同), 类似变量. 可以使用作用域符号调用同名的外层成员.
名字查找先于类型查找:
虚函数与作用域:
覆盖重载的函数: 使用using语句
成员函数无论是否是虚函数都能被重载.
15.7 构造函数与拷贝控制
15.7.1 虚析构函数
基类通常应该定义一个虚析构函数. 虚析构函数内容为空.
如果一个类需要析构函数, 它同样需要拷贝和赋值操作. 而基类的析构函数并不遵循上述准则.
虚析构函数将阻止合成移动操作. 如果一个类定义了析构函数, 即使它通过=default的形式使用了合成的版本, 编译器也不会为这个类合成移动操作.
15.7.2 合成拷贝控制与继承
派生类中删除的拷贝控制与基类的关系:
- 如果基类的默认构造函数,拷贝构造函数, 拷贝赋值运算符或析构函数 是被删除的或者不可访问的, 则派生类中对应的函数将是被删除的. 原因是编译器不能使用基类成员来执行派生类对象中基类部分的构造, 赋值或销毁操作
- 如果在基类中有一个不可访问或删除的析构函数, 则派生类中合成的默认和拷贝构造函数将是删除的, 因为编译器无法销毁派生类对象中的基类部分
- 如果基类中的移动操作时删除的或不可访问的, 派生类中的移动函数将是被删除的. 原因是派生类对象中的基类部分不可移动.
- 如果基类中的析构函数是删除的或不可访问的, 则派生类中的移动构造函数也将是被删除的.
移动操作与继承:
因大多数基类会定义虚析构函数, 基类通常不含有合成的移动操作, 派生类中也没有. 如果确实需要移动操作, 需要首先在基类中定义. 一旦基类显式的定义了移动操作, 则同样需要显式的定义拷贝动作.
15.7.3 派生类的拷贝控制成员
派生类的构造函数在拷贝, 移动和赋值 自有成员的同时, 也要拷贝, 移动和赋值基类部分的成员.
默认情况下, 基类使用默认构造函数初始化自身部分, 如果想拷贝/移动基类部分, 需要派生类显式的使用基类的拷贝/移动函数.
派生类赋值运算符: 基类的赋值运算符 也需要 派生类显式的调用
派生类的析构函数: 析构函数只负责 销毁自身分配的资源. 而基类的析构函数将会被自动执行.
在构造函数和析构函数中调用虚函数: 构造的过程是 先基类后派生类, 析构的过程则相反, 所以在构造函数中调用 派生类的虚函数时, 会发生异常, 在析构中也是如此.
15.26
// Bulk_quote_p541
// 15.26
Bulk_quote_p541(const Bulk_quote_p541& rhs)
:Disc_quote_p541(rhs), min_qty(rhs.min_qty), limit_qty(rhs.limit_qty), discount(rhs.discount) {
std::cout << "Bulk_quote_p541 copy constructor" << std::endl;
};
Bulk_quote_p541(Bulk_quote_p541&& rhs) noexcept:
Disc_quote_p541(std::move(rhs)), min_qty(std::move(rhs.min_qty)),
limit_qty(rhs.limit_qty), discount(std::move(rhs.discount)){
std::cout << "Bulk_quote_p541 Move constructor" << std::endl;
};
Bulk_quote_p541& operator=(const Bulk_quote_p541&& rhs) noexcept {
std::cout << "Bulk_quote_p541 operator move" << std::endl;
if (this != &rhs) {
Disc_quote_p541::operator=(std::move(rhs));
min_qty = std::move(rhs.min_qty);
limit_qty = std::move(rhs.limit_qty);
discount = std::move(rhs.discount);
}
return *this;
};
Bulk_quote_p541& operator=(const Bulk_quote_p541& rhs) {
std::cout << "Bulk_quote_p541 operator=" << std::endl;
min_qty = rhs.min_qty;
limit_qty = rhs.limit_qty;
discount = rhs.discount;
return *this;
};
// Disc_quote_p541
Disc_quote_p541(const Disc_quote_p541& rhs) :Quote_p528(rhs), quantity(rhs.quantity), discount(rhs.discount) {
std::cout << "Disc_quote_p541 copy constructor" << std::endl;
};
Disc_quote_p541(Disc_quote_p541&& rhs) noexcept:
Quote_p528(std::move(rhs)), quantity(std::move(rhs.quantity)), discount(std::move(rhs.discount))
{
std::cout << "Disc_quote_p541 Move constructor" << std::endl;
};
Disc_quote_p541& operator=(const Disc_quote_p541&& rhs) noexcept {
std::cout << "Disc_quote_p541 operator move" << std::endl;
if (this != &rhs) {
Quote_p528::operator=(std::move(rhs));
quantity = std::move(rhs.quantity);
discount = std::move(rhs.discount);
}
return *this;
};
Disc_quote_p541& operator=(const Disc_quote_p541& rhs) {
std::cout << "Disc_quote_p541 operator=" << std::endl;
quantity = rhs.quantity;
discount = rhs.discount;
return *this;
};
// Quote_p528
Quote_p528(const Quote_p528& rhs) :bookNo(rhs.bookNo), price(rhs.price) {
std::cout << "Quote copy constructor" << std::endl; };
Quote_p528(Quote_p528&& rhs) noexcept {
std::cout << "Quote Move constructor" << std::endl;
if (this != &rhs) {
bookNo = std::move(rhs.bookNo);
price = std::move(rhs.price);
}
};
Quote_p528& operator=(const Quote_p528&& rhs) noexcept {
std::cout << "Quote operator move" << std::endl;
if (this != &rhs) {
bookNo = std::move(rhs.bookNo);
price = std::move(rhs.price);
}
return *this;
};
Quote_p528& operator=(const Quote_p528& rhs) {
std::cout << "Quote operator=" << std::endl;
bookNo = rhs.bookNo;
price = rhs.price;
return *this;
};
int main(int argc, char** argv) {
Bulk_quote_p541 b("1", 10, 10, 20, 0.5);
Bulk_quote_p541 b2("2", 20, 20, 40, 0.5);
// Bulk_quote_p541 c(b); (1)
// 输出
// Quote copy constructor
// Disc_quote_p541 copy constructor
// Bulk_quote_p541 copy constructor
// b = b2; (2)
// 输出 Bulk_quote_p541 operator=
// Bulk_quote_p541 d(std::move(b)); (3)
// 输出
// Quote Move constructor
// Disc_quote_p541 Move constructor
// Bulk_quote_p541 Move constructor
// b2 = std::move(b); (4)
// 输出
// Bulk_quote_p541 operator move
// Disc_quote_p541 operator move
// Quote operator move
}
15.7.4 继承的构造函数
类不能继承默认, 拷贝和移动构造函数.
派生类可以使用using 继承基类构造函数, 如:
using Disc_quote::Disc_quote;
编译器会在派生类中为每个基类生成参数列表完全相同的构造函数.
使用using的基类构造函数无论声明在public还是protected, private中, 其原来的报数属性不变.
using声明语句不能指定explicit或constexpr, 如果基类带有explicit/constexpr, 则派生类中继承的也有.
如果基类含有多个构造函数,派生来会继承所有的构造函数(默认形参将会被舍弃). 有两个例外:
- 派生类可以只继承部分构造函数, 其余自定义(覆盖基类构造函数)
- 默认, 拷贝, 移动构造函数不会被继承
15.8 容器与继承
当派生类对象被赋值给基类对象时, 其中的派生类部分将被 "切掉", 因此 容器和存在继承关系的类型无法兼容. 要想解决此问题, 需要在容器中存放指针/智能指针而非对象:
15.8.1 编写Basket类
#include <set>
#include "Quote_p528.h"
class Basket_p559 {
public:
void add_item(const std::shared_ptr<Quote_p528>& sale) { items.insert(sale); }
void add_item(const Quote_p528& sale) {
items.insert(std::shared_ptr<Quote_p528>(sale.clone()));
};
void add_item(Quote_p528&& sale) {
items.insert(std::shared_ptr<Quote_p528>((std::move(sale)).clone()));
};
double total_receipt(std::ostream&) const;
double print_total(std::ostream& os, Quote_p528& q, std::size_t t) const {
auto p = q.net_price(t);
os << "total price: " << p << std::endl;
return p;
};
private:
static bool compare(const std::shared_ptr<Quote_p528>& lhs,
const std::shared_ptr<Quote_p528>& rhs) {
return lhs->isbn() < rhs->isbn();
}
std::multiset <std::shared_ptr<Quote_p528>, decltype (compare)* > items = { {},compare };
};
double Basket_p559::total_receipt(std::ostream& os) const {
double sum = 0.0;
for (auto iter = items.cbegin(); iter != items.cend(); iter = items.upper_bound(*iter)) {
sum += print_total(os, **iter, items.count(*iter));
}
os << "Total Sale: " << sum << std::endl;
return sum;
}
15.9 文本查询程序
// TextQuery.h
#pragma once
#ifndef _TEXTQUERY_H
#define _TEXTQUERY_H
#include<string>
#include<set>
#include<vector>
#include<iostream>
#include<fstream>
#include<sstream>
#include<map>
#include"QueryResult.h"
using namespace std;
// 书上的程序
class TextQuery {
public:
using line_no = vector<string>::size_type;
TextQuery(ifstream&);
QueryResult query(const string&) const;
private:
shared_ptr<vector<string>>file;
map<string, shared_ptr<set<line_no>>>wm;
};
TextQuery::TextQuery(ifstream& is) :file(new vector<string>) {
string text;
while (std::getline(is, text)) {
file->push_back(text);
int n = file->size() - 1;
std::istringstream line(text);
string word;
while (line >> word) {
auto& lines = wm[word];
if (!lines) lines.reset(new set<line_no>);
lines->insert(n);
}
}
};
QueryResult TextQuery::query(const string& sought) const {
static shared_ptr<set<line_no>> nodata(new set<line_no>);
auto loc = wm.find(sought);
if (loc == wm.end()) return QueryResult(sought, nodata, file);
else return QueryResult(sought, loc->second, file);
};
#endif
//QueryResult.h
#pragma once
#ifndef _QUERYRESULT_H
#define _QUERYRESULT_H
#include<string>
#include<set>
#include<vector>
#include<iostream>
#include<fstream>
#include<sstream>
#include<map>
using namespace std;
string make_plural2(size_t ctr, const string& word, const string& ending) {
return (ctr > 1) ? word + ending : word;
}
class QueryResult {
using line_no = vector<string>::size_type;
friend ostream& print(ostream&, const QueryResult&);
friend ostream& print(ostream& os, const QueryResult& qr) {
os << qr.sought << " occurs " << qr.lines->size() << " "
<< make_plural2(qr.lines->size(), "time", "s") << endl;
for (auto num : *qr.lines)
os << "\t ( line " << num + 1 << " )"
<< *(qr.file->begin() + num) << endl;
return os;
};
public:
QueryResult(string s, shared_ptr<set<line_no>>p, shared_ptr<vector<string>>f) :sought(s), lines(p), file(f) {}
set<QueryResult::line_no>::iterator begin() { return (*lines).begin(); }
set<QueryResult::line_no>::iterator end() { return (*lines).end(); }
shared_ptr<vector<string>> get_file() { return file; }
private:
string sought;
shared_ptr<set<line_no>> lines;
shared_ptr<vector<string>> file;
};
#endif
//main.cpp
#include<string>
#include <algorithm>
#include "TextQuery.h"
#include "QueryResult.h"
class Query_base {
friend class Query;
protected:
using line_no = TextQuery::line_no;
virtual ~Query_base() = default;
private:
virtual QueryResult eval(const TextQuery&) const = 0;
virtual std::string rep() const = 0;
};
class Query {
private:
friend Query operator~ (const Query&);
friend Query operator| (const Query&, const Query&);
friend Query operator& (const Query& lhs, const Query& rhs);
friend std::ostream& operator<< (std::ostream& os, const Query& query) {
return os << query.rep();
};
public:
Query(const std::string& s);;
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;
};
class WordQuery : public Query_base {
friend class Query;
WordQuery(const std::string& s) :query_word(s) {};
QueryResult eval(const TextQuery& t) const { return t.query(query_word); };
std::string rep() const { 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 { 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));
}
class BinaryQuery :public Query_base {
protected:
BinaryQuery(const Query& l, const Query& r, std::string s) :lhs(l), rhs(r), opSym(s) {};
std::string rep() const { return "(" + lhs.rep() + " " + opSym + " " + rhs.rep() + ")"; }
Query lhs, rhs;
std::string opSym;
};
class AndQuery :public BinaryQuery {
friend Query operator&(const Query&, const Query&);
AndQuery(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 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));
}
QueryResult OrQuery::eval(const TextQuery& text)const {
auto right = rhs.eval(text), left = lhs.eval(text);
auto ret_lines = make_shared<set<line_no>>(left.begin(), left.end());
ret_lines->insert(right.begin(), right.end());
return QueryResult(rep(), ret_lines, left.get_file());
}
QueryResult AndQuery::eval(const TextQuery& text) const {
auto left = lhs.eval(text), right = rhs.eval(text);
auto ret_lines = make_shared<set<line_no>>();
std::set_intersection(left.begin(), left.end(), right.begin(), right.end(), inserter(*ret_lines, ret_lines->begin()));
return QueryResult(rep(), ret_lines, left.get_file());
}
QueryResult NotQuery::eval(const TextQuery& text) const {
auto result = query.eval(text);
auto ret_lines = make_shared<set<line_no>>();
auto beg = result.begin(), end = result.end();
auto sz = result.get_file()->size();
// 因为是顺序的, 使用找到的result中的值,
// 第一个, 使用所有文件的行号逐次向最大值移动, 只要匹配的就舍弃, 不匹配就加入进去
for (size_t n = 0; n != sz; ++n) {
if (beg == end || *beg != n) ret_lines->insert(n);
else if (beg != end) ++beg;
}
return QueryResult(rep(), ret_lines, result.get_file());
}
using namespace std;
int main(int argc, char** argv) {
// Query q = Query("hlo");
Query q = Query("fiery") & Query("bird"); // | Query("wind");
// Query q = Query("fiery");
cout << q << endl;
ifstream is("E://c++//primer//Project_c++Primer_p567//Debug//p562.txt");
TextQuery t(is);
print(cout,q.eval(t));
}