系列文章目录
系列文章
15
文章目录
前言
面向对象程序设计基于三个基本概念:数据抽象、继承和动态绑定。
OOP:概述
object-oriented programming 的核心思想是数据抽象、继承和动态绑定。
继承 inheritance
基类 base class
派生类 derived calss
虚函数 virtual function : 派生类各自定义适合自身的版本
类派生列表 class derivation list
class Quote{
public:
std::string isbn() const;
virtual double net_price(std::size_t n) const;
};
class BulkQuote : public Quote {
public:
double net_price(std::size_t) const override;
};
//virtual double net_price(std::size_t) const override; virtual是可选项
动态绑定 dynamic binding
double print_total(ostream &os, const Quote &item, size_t n){
//根据传入item形参的对象调用Quote::net_price
//或者Bulk_quote::net_price
double ret = item.net_price(n);
os << "ISNB: " << item.isbn() //调用Quote::isbn
<< " # sold: " << n << " total due: " << ret << endl;
return ret;
}
print_total(cout, basic, 20); //调用Quote的net_price
print_total(cout, bulk, 20); //调用BulkQuote的net_price
在C++语言中,当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定
定义基类和派生类
//基类
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;
protected:
double price = 0.0;
};
//派生类
class BulkQuote : public Quote /*派生类列表 public/private/protected class1, xxx class2*/{
public:
BulkQuote()=default;
BulkQuote(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; //以小数表示的折扣额
};
BulkQuote::BulkQuote(const std::string& book, double p, std::size_t qty, double disc):
Quote(book, p), min_qty(qty), discount(disc) { }
//首先初始化基类的部分,然后按照生命的顺序依次初始化派生类的成员
double BulkQuote::net_price(size_t cnt) const{
if(cnt >= min_qty)
return cnt * (1-discount) * price;
else
return cnt * price;
//派生类的作用域嵌套在基类的作用域之内
}
基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此
派生类对象及派生类向基类的类型转换
derived-to-base
Quote item; //基类对象
BulkQuote bulk; //派生类对象
Quote *p = &item; //p指向Quote对象
p = &bulk; //p指向bulk的Quote部分
Quote &r = bulk; //r绑定到bulk的Quote部分
这种隐式特性意味着我们可以把派生类对象或者派生类对象的引用用在需要基类引用的地方;同样的,我们也可以把派生类对象的指针用在需要基类指针的地方。
**继承与静态成员 **
//若基类定义了一个静态成员,则基类和派生类中只有一个实例
class Base{
public:
static void statmem();
};
class Derived : public Base{
void f(const Derived&);
};
void Derived::f(const Derived &derived_obj){
Base::statmem();
Derived::statmem();
derived_obj.statmem(); //通过Derived对象访问
statmem(); //通过this对象访问
}
派生类声名:只包含类名 class Derived;
被用作基类的类必须已经定义而非仅仅声名
一个类不能派生它本身
直接基类 direct base 简介基类indirect base
防止继承的发生
class NoDerived final { /* */ }; //不能作为基类
class Last final : public Base {}; //Last不能作为基类
类型转换与继承
派生类可转换成基类,反之不行
静态类型与动态类型
形参的Quote item时静态类型,调用时传入实参,发现item是BulkQuote时发现是动态类型
不存在从基类向派生类的隐式类型转换······ ······在对象之间不存在类型转换
虚函数
对虚函数的调用可能在运行时才被解析
被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那一个。
动态绑定只有当我们通过指针或引用调用虚函数时才会发生。
派生类中的虚函数
形参列表必须完全一样,返回类本身的引用时可不同
final 和 override 说明符
struct B{
virtual void f1(int) const;
virtual void f2();
void f3();
};
struct D1 : B{
void f1(int) const override; //✔ 匹配
void f2(int) override; //❌,B中没有 f2(int)
void f3() override; //❌f3()不是虚函数
void f4() override; //❌B没有f4
};
struct D2 : B{
void f1(int) const final; //不允许后续的其他类覆盖f1(int)
};
struct D3 : D2 {
void f2(); //✔ 覆盖从从间接基类B继承而来的f2
void f1(int) const; //❌D2已经将f2声名成final
};
虚函数与默认实参
虚函数中的默认实参总是用基类版本中的默认值,与动态类型无关
基类和派生类中定义的默认实参最好一致
回避虚函数的机制
以下代码会报错,这与继承中的类作用域有关
class Base{
public:
Base()=default;
Base(int a, int b):bint(a), proint(b){};
Base(Base& _base):
bint(_base.bint), proint(_base.proint) {}
~Base()=default;
int show_bint()const{return bint;}
int show_proint()const{return proint;}
virtual void print_me() final
{cout << "Base is running."<< endl;}
private:
int bint;
protected:
int proint;
};
class Derived: public Base{
private:
int suba;
protected:
int subb;
public:
Derived(/* args */)=default;
Derived(int a, int b, int c, int d):
Base(a, b), suba(c), subb(d) { }
~Derived()=default;
void increase_proint(){++proint;}
virtual void print_me(int inttt)
{
cout << "Drived is running."<< endl;
}
};
void func12(Base& a){
a.print_me(5); //猜想:编译器首先在静态类型中寻找 print_me(int),没找到,报错
//猜想:编译器首先在静态类型中寻找 print_me(),发现是虚函数,然后再看a的动态类型
//发现不是Base,则调用a类型中对应的函数(如果有的话)
}
int main(){
Base father(2,3);
Derived child(1,9,35,90);
child.increase_proint();
cout << child.show_proint() <<endl;
Base &tst = child;
//这里静态类型是Base动态类型是Derived
func12(child); //为什么会报错? child的动态类型应该时子类
return 0;}
抽象基类
纯虚函数=0必须在类内声名,可以且只能在类外部定义
含有(或未经覆盖直接继承)纯虚函数的类是抽象基类 abstract base class
不能直接创建一个抽象基类的对象
派生类构造函数只初始化它的直接基类
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;
protected:
double price = 0.0;
};
class DiscQuote : public Quote{
public:
DiscQuote() = default;
DiscQuote(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;
};
class BulkQuote : public DiscQuote{
public:
BulkQuote() = default;
BulkQuote(const std::string& book, double price, std::size_t qty, double disc) :
DiscQuote(book, price, qty, dics) {}
virtual double net_price(std::size_t) const override;
};
访问控制与继承
访问说明符(public,protected,private):访问控制关键字 public,protected,private在类内无效,仅在类外起作用
protected使得该类的派生类(在派生类内部)可访问
private使得仅能在该类的内部访问
在派生类的派生列表中的访问说明符(派生访问说明符):对派生类类内的访问权限无影响,他的目的是控制派生类用户(即派生类的对象,包括了派生类的派生类在内)对于基类成员的访问权限。派生访问说明符主要是对基类的public部分进行继承后权限控制
派生类向基类转换的可访问性
友元与继承
不能继承友元关系;每个类负责控制各自成员的访问
改变个别成员的可访问性
class Base {
public:
std::size_t size() const {return n;}
protected:
std::size_t n;
};
class Derived : private Base{
public:
using Base::size;
protected:
using Base::n;
};
默认的继承保护级别
class Base { /* ··· */ };
struct D1 : Base { /* ··· */ }; //默认public继承
class D2 : Base { /* ··· */ }; //默认private继承
类继承中的类作用域
派生类的作用域嵌套在其基类的作用域之内
Bulk_quote bulk;
cout << bulk.isbn();
在编译时进行名字找查
静态类型决定了该对象的哪些成员是可见的,或者说静态类型决定在哪个作用域内找查名字
名字冲突与继承
内层作用域(即派生类)中的名字将隐藏定义在外层作用域(即基类)的名字
派生类的成员将隐藏同名的基类成员
普通成员不具有多特特性,而虚函数有,所以定义虚函数还是有必要的
通过作用域运算符来使用隐藏的成员
除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字
名字找查先于类型检查
派生类会隐藏掉基类中同名的成员,与成员是不是函数无关, 派生类不会重载基类中的函数
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
b.Derived::memfcn(3); //❌Derived不是Base的基类
虚函数与作用域
基类与派生类中的虚函数必须具有相同的形参列表。假如基类与派生类的虚函数接受的实参不同,则无法通过基类的引用或指针调用派生类的虚函数。
通过基类调用隐藏的虚函数
覆盖重载函数
使用using声名可以实现。。。
构造函数与拷贝控制
创建、拷贝、移动赋值和销毁
虚析构函数
基类中的析构函数需要为虚函数
class Quote{
public:
//如果我们删除的是一个指向派生类对象的基类指针,则需要虚析构函数
virtual ~Quote() = default; //动态绑定析构函数
};
如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为。
虚析构函数将阻止合成移动操作
合成拷贝控制与继承
派生类中删除的拷贝控制与基类的关系
移动操作与继承
//在基类中定义
class Quote{
public:
Quote() = default; //对成员以此进行默认初始化
Quote(const Quote&) = default; //对成员依次拷贝
Quote(Quote&&) = default; //对成员依次拷贝
Quote& operator=(const Quote&) = default; //拷贝赋值
Quote& operator=(Quote&&) = default; //移动赋值
virtual ~Quote() = default;
//...
};
派生类的拷贝控制成员
当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象
定义派生类的拷贝或移动构造函数
class Base {};
class D : public Base{
public:
// 默认情况下,基类的默认构造函数初始化对象的基类部分
// 要想使用拷贝或移动构造函数,我们必须在构造函数初始值列表中
// 显示地调用该构造函数
D(const D& d): Base(d) //拷贝基类成员
/* D的成员初始值 */ {}
D(D&& d):Base(std::move(d)) //移动基类成员
/* D的成员初始值 */ {}
};
派生类赋值运算符
//Base::operator=(const Base&)不会被自定调用
D &D::operator=(const D &rhs){
Base::operator=(rhs); 为基类部分赋值
//为派生类的成员赋值
//酌情处理自赋值及释放已有资源
return *this;
}
派生类析构函数
class D: public Base{
public:
// Base::~Base被自动调用执行
~D() { /* ... */ } //对象销毁顺序正好与其创建顺序相反
};
在构造函数和析构函数中调用虚函数
啥???
继承的构造函数
容器与继承
当派生类对象被赋值给基类对象时,其中的派生类部分将被“切掉”,因此容器和存在继承关系的类型无法兼容
在容器中放置(智能)指针而非对象
vector<shoared_ptr<Quote>> basket;
basket.push_back(make_shared<Quote>("sdf", 50));
basket.push_back(make_shared<BulkQuote>("jskdf", 89, 9, .9));
Basket类
#include <iostream>
#include <memory>
#include <set>
#include <utility> //std::move
class Quote{
private:
/* data */
public:
Quote(/* args */);
~Quote();
std::string isbn();
virtual Quote* clone() const &{return new Quote(*this);}
virtual Quote* clone() && {return new Quote(std::move(*this));}
};
Quote::Quote(/* args */)
{
}
Quote::~Quote()
{
}
class BulkQuote : public Quote
{
private:
/* data */
public:
BulkQuote(/* args */);
~BulkQuote();
BulkQuote * clone() const &{return new BulkQuote(*this);}
BulkQuote * clone() && {return new BulkQuote(std::move(*this));}
};
BulkQuote::BulkQuote(/* args */)
{
}
BulkQuote::~BulkQuote()
{
}
//表示购物篮的类
class Basket{
public:
//Basket使用合成的默认构造函数和拷贝控制成员
void add_item(const std::shared_ptr<Quote> &sale)
{ items.insert(sale); }
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()) );}
// 打印每本树的总价和购物篮中所有书的总价
double total_receipt(std::ostream &) const;
private:
//该函数用于比较shared_ptr, multiset成员会用到它
static bool compare(const std::shared_ptr<Quote> &lhs,
const std::shared_ptr<Quote> &rhs)
{return lhs->isbn() < rhs->isbn();}
//mutiset保存多个报价,按照compare成员排序
std::multiset<std::shared_ptr<Quote>, decltype(compare)*>
items{compare};
};
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 << std::endl; // 打印最终的总价格
return sum;
}
文本查询程序again
//抽象基类,具体的查询类型从中派生,所有成员都是private的
class Query_base{
friend class Query;
protected:
using line_no = TextQuery::line_no; //用于eval函数
virtual ~Query_base() = default;
private:
//eval返回与当前Query匹配的QueryResult
virtual QueryResult eval(const TextQuery&) const = 0;
//rep是表示查询的一个string
virtual std::string rep() const = 0;
};
//这个是一个管理Query_base继承体系的接口
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(cosnt std::string&); //构建一个新的WordQuery
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;
};
std::ostream& operator<<(std::ostream&os, const Query &query){
//Query::rep通过它的Query_base指针对rep进行了虚调用
return os << query.rep();
}
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; //要找查的单词
};
inline Query::Query(cosnt std::string&s): q(new WordQuery(s)){}
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));
}
class BinaryQuery: public Query_base{
protected:
BinaryQuery(const Query &l, const Query &r, std::string s):
lhs(l), rhs(r), opSym(s) {}
//抽象类:BinaryQuery不定义eval
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, "&") {}
//具体的类: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));
}
QueryResult OrQuery::eval(const TextQuery& text) const{
//通过Query成员lhs和rhs进行的虚调用
//调用eval返回每个运算对象的QueryResult
auto 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());
}
QueryResult AndQuery::eval(const TextQuery& text) const{
//通过Query运算对象进行的虚调用,以获得运算对象的查询结果set
auto left = lhs.eval(text), right = rhs.eval(text);
//保存left和right交集的set
auto ret_lines = make_shared<set<line_no>>();
//将两个范围的交集写入一个目标的迭代器中
//本次调用的目的迭代器向ret添加元素
set_intersection(left.begin(), left.end(),
right.begin(), right.end(),
inserter(*ret_lines, ret_lines->bgin()));
return QueryResult(rep(), ret_lines, left.get_file());
}
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_lines
auto sz = result.get_file()->size();
for (size_t n = 0; n != sz; ++n){
if (beg == end || *beg != n)
ret_lines->insert(n); //如果不在result当中,添加这一行
else
if (beg != end)
++beg; //否则接续获取result的下一行(如果有的话)
}
return QueryResult(rep(), ret_lines, result.get_file());
}
总结
只有指针或引用的静态类型与动态类型可能不相同。
静态类型用于确定编译器在哪个作用域内搜索,名字找查先于类型检查。派生类中的名字会隐藏基类中同名的名字(可通过::显示指出是调用基类的成员还是派生类的成员),因为派生类的作用域嵌套在基类内。函数重载只会发生在同一层作用域
动态类型主要与虚函数相关。首先在静态类型中找查,若没找到则报错,若找到了且不是虚函数,则xxx,若找到后是虚函数再根据动态类型确定调用的虚函数是哪里的(运行时确定使用虚函数的哪个版本)
访问控制
struct 与 class的区别仅为默认访问权限不同calss Derived : Base{}; struct Derived : Base{};
notes:
在继承体系中最好不要出现同名的成员(虚函数除外)