1.OPP:概述
面向对象程序设计的核心思想是数据抽象、继承和动态绑定。
通过继承联系在一起的类构成一种层次关系,在层次关系的根部的是基类,基类下面的类是派生类
基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。
虚函数:由基类来声明,但基类希望它的派生类各自重新定义适合自身的版本的函数。
class quote{
public:virtual double net_price(std::size_t n) const; //基类quote声明的虚函数
};
派生类必须通过使用类派生列表明确指出它是从哪个基类继承而来。
class bulk_quote : public quote{ //在基类前写上访问说明符...
};
派生类必须在其内部对所有重新定义的虚函数进行声明。
新标准允许派生类使用关键字override来显式地注明它将使用哪个成员函数来改写基类的虚函数。
double net_price(std::size_t) const override; //派生类重新定义的虚函数
通过动态绑定,我们能用同一段代码分别处理基类和派生类的对象。
在C++语言中,当我们使用基类的引用或指针调用一个虚函数时将发生动态绑定。
double print_total(ostream& os,const quote& item, size_t n)
{auto ret = item.net_price();
}//basic是基类类型,bulk是派生类类型
print_total(cout,basic,20); //调用基类的net_price
print_total(cout,bulk,20); //调用派生类的net_price
函数的执行版本由实参决定,在运行时选择函数的版本,动态绑定又称为运行时绑定。
2.定义基类和派生类
2.1、定义基类
基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。
派生类可以继承其基类的成员。
在C++中,基类必须将它的两种成员函数区分开来:一种是希望派生类进行覆盖的函数,另一种是希望派生类直接继承而不要改变的函数。
基类通过在其成员函数语句前加上关键字virtual,使得该函数执行动态绑定,成为虚函数。
任何构造函数之外的非静态函数都可以是虚函数。
关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。
若基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数。
成员函数如果没被声明为虚函数,则其解析过程发生在编译时而非运行时。
派生类能访问基类的公有成员,而不能访问基类的私有成员。
若基类希望它的派生类有权访问该成员,同时禁止其他用户访问,可使用protected访问运算符来说明该成员。
class quote{ //基类
public:quote() = default; //默认构造函数quote(const std::string& book,double sales_price) : books(book),price(sales_price) {} //接受两个参数的构造函数virtual double net_price(std::size_t n) const {return n*price;} //虚函数virtual ~quote() = default; //虚析构函数
private:std::string books;
protected: //派生类能够访问的成员double price = 0.0;
};
2.2、定义派生类
派生类在每个基类前面可以有三种访问说明符中的一种:public、protected、private。
访问说明符的作用是控制派生类从基类继承而来的成员是否对派生类用户可见。
若一个派生是公有的,则基类的公有成员也是派生类接口的组成部分。(在任何需要基类的引用或指针的地方我们都可以使用派生类的对象)
若派生类没有覆盖基类中的某个虚函数,则派生类会直接继承其在基类中的版本。
新标准允许派生类使用关键字override显式地注明它使用某个成员函数覆盖了它所继承的虚函数。
一个派生类对象包含多个组成部分:一个含有派生类自己定义的非静态成员的子对象,一个与该派生类继承的基类对应的子对象。
由于在派生类对象中含有与其基类对应的组成部分,所以能把派生类的对象当作基类对象来使用,也能将基类的指针或引用绑定到派生类对象中的基类部分。
这种转换称为派生类到基类的类型转换,编译器会隐式地执行派生类到基类的转换。
在派生类对象中含有与其基类对应的组成部分,这一事实是继承的关键所在。
派生类只能通过使用基类的构造函数来初始化它的基类部分,每个类控制他自己的成员的初始化过程。
派生类构造函数是通过构造函数初始化列表来将实参传递给基类构造函数的。
首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。
bulk_quote(const std::string& book,double p,std::size_t qty,double disc) :quote(book,p),qtys(qty),discount(disc) {}
//将实参book,p传给quote的构造函数,剩余的实参依次初始化派生类的成员
派生类的作用域嵌套在基类的作用域之内,派生类可以直接访问基类的公有成员和受保护成员。
若基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。
静态成员遵循通用的访问控制规则,若基类中的成员是private的,则派生类无权访问该成员。
派生类的声明包含类名但不包含它的派生列表。
class bulk_quote : public quote; //错误,派生列表不能出现
class bulk_quote; //正确
派生列表以及与定义有关的其他细节必须与类的主体一起出现。
若我们想将某个类用作基类,则该类必须已经定义而非仅仅声明。
一个类可以是基类,同时也可以是派生类。
class base{...};
class b1 : public base {...};
class b2 : public b1 {...};
//base是b1的直接基类,同时也是b2的间接基类
每个类都会继承直接基类的所有成员。
新标准中提供了一种防止继承发生的方法,即使用关键字final来阻止继承。
class base{...};
class b1 final : base{...}; //b1不能被继承
class b2 : b1{...}; //错误,b1是final的
2.3、类型转换与继承
存在继承关系的类能将基类的指针或引用绑定到派生类的对象上。
当使用基类的引用或指针时,实际上我们并不清楚该引用或指针所绑定的对象的真实类型。
智能指针类也支持派生类向基类的类型转换,这意味着允许将一个派生类对象的指针存储在基类的智能指针内。
当我们使用存在继承关系的类型时,应该区分开一个变量或表达式的静态类型与该表达式表示对象的动态类型。
基类的指针或引用的静态类型可能与其动态类型不一致。
静态类型在编译时就是已知的,动态类型直到运行时才可知,由实参类型决定。
double ret = item.net_price(n);
//item是quote类型的引用,在函数定义时就已经确定了,是静态类型
//由于item是quote类型的引用,可以使用基类版本或派生类版本的net_price,
//具体执行哪个函数由传入实参决定,所以net_price的类型要在运行时才可知,它的类型是动态类型
由于每个派生类对象都包含一个基类部分,基类的引用或指针可以绑定到该基类部分上。
存在派生类向基类的类型转换,不存在从基类向派生类的类型转换。
即使一个基类指针或引用绑定在一个派生类对象上,我们也不能执行从基类向派生类的转换。
bulk_quote bulk;
quote* item = &bulk; //正确,派生类向基类的转换
bulk_quote* bulkp = item; //错误,不存在基类向派生类的转换,哪怕基类中存储的是派生类也不行
若我们已知某个基类向派生类的转换是安全,可通过static_cast来强制覆盖掉编译器的检查工作。
派生类向基类的自动类型转换只对指针或引用类型有效,在派生类类型和基类类型中不存在。
当我们初始化或赋值一个类类型的对象时,实际上在调用它们的构造函数或赋值运算符函数,因为这些成员都接受引用作为参数,所以派生类向基类的转换允许我们给基类的拷贝/移动操作传递一个派生类的对象。
bulk_quote bulk; //派生类对象
quote item(bulk); //使用quote::quote(const quote&)构造函数
item = bulk; //调用quote::operator=(const quote&);
但上述操作会忽略掉bulk_quote部分,只将派生类中的quote部分保存在item中。
3.虚函数
当我们使用基类的引用或指针调用一个虚成员函数时会进行动态绑定,因此必须为每个虚函数都提供定义,而不管它是否被用到。
当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确定应该匹配哪个版本。
通过一个具有普通类型的表达式调用虚函数时,在编译时就会将调用的版本确定下来,对象的动态类型与静态类型相同。
quote base;
base.net_price(20); //调用quote::net_price
一旦某个函数被声明成虚函数,则在所以派生类中它都是虚函数。
派生类的函数若覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致。
派生类中的虚函数的返回类型也必须与基类函数匹配,但当类的虚函数返回类型是类本身的指针或引用时,上述规则则无效。
若派生类定义了一个与基类中虚函数的名字相同但形参列表不同的函数,那该函数与基类中原有的函数是相互独立的。
新标准中我们可以使用override关键字来说明派生类中的虚函数,若该函数没有覆盖已存在的虚函数,则编译器会报错。
struct a{virtual void f1(int) const;
};struct b : public a{void f1(int) const override; //使用override来标记虚函数
};
和其他函数一样,虚函数可以拥有默认实参。
若某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定的。
若虚函数使用默认实参,则基类和派生类中定义的默认实参最好保持一致。
若希望对虚函数的调用不要进行动态绑定,而是强制让其执行虚函数的某个特定版本,可以使用作用域运算符来实现。
double ret = base->quote::net_price(22);
//强制调用quote的net_price版本,不管base实际指向的对象类型是什么
通常情况下,只有成员函数或友元中的代码才需要使用作用域运算符来回避虚函数的机制。
如果一个派生类虚函数需要调用它的基类版本,但没有使用作用域运算符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无限递归。
4.抽象基类
抽象基类是含有不完整的内容的基类,类似于模板,用于生产特定条件的类。
纯虚函数是一种特殊的虚函数,该函数没有具体的定义,由子类来提供实现。
通过在函数体的位置(即声明语句)书写=0就可以将一个虚函数说明为纯虚函数,只能在类内部进行声明。
double net_price(std::size_t) const = 0; //纯虚函数
我们可以为纯虚函数提供定义,不过函数体必须定义在类的外部。
含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类。
抽象基类负责定义接口,而后续的派生类可以覆盖该接口。
由于纯虚函数是不完整的,因此不能直接创建一个抽象基类的对象。
抽象基类的派生类必须给出自己的纯虚函数的定义,否则它们将是抽象基类。
5.访问控制与继承
每个类分别控制着其成员对于派生类来说是否可访问。
派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。
某个类对其继承而来的成员的访问权限受到基类中该成员的访问说明符和在派生类列表中的访问说明符的影响。
派生访问说明符是控制派生类从基类所继承的成员在派生类中的访问权限。
派生访问说明符可以控制继承自派生类的新类的访问权限,这无关于基类的访问权限。
class base{
protected:int prot;
};struct priv : private base{int f1() const { return prot; } //prot继承于base,是priv的私有成员
};struct priv1 : public priv{int use() { return prot; } //错误,prot在priv中是私有的
};
无论派生类以什么方式继承基类,派生类向其直接基类的类型转换对于派生类的成员和友元来说永远是可访问的。
只有当派生类公有地继承基类时,用户代码才能使用派生类向基类的转换。
友元关系不能传递,也不能继承。
派生类的友元不能随意访问基类类的成员,但基类的友元能访问派生类的基类部分。
我们可以通过使用using声明来改变派生类继承的某个名字的访问级别。
using声明语句中名字的访问权限由该using声明语句之前的访问说明符来决定。
class base{
public:std::size_t size() const { return n;}
protected:std::size_t n;
};struct priv : private base{
public:using base::size; //size的访问权限为public
protected:using base::n; //n的访问权限为protected
}
派生类只能为那些它可以访问的名字提供using声明。
默认派生运算符由定义派生类所用的关键字来决定(class是私有的,struct是公有的)。
6.继承中的类作用域
当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。
一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。
class disc : public quote{ //disc是bulk_quote的直接基类
public:std::pair<size_t,double> discont() const { return {quantity,discount};}
};bulk_quote bulk;
bulk_quote* bulkp = &bulk; //直接基类指针
quote* itemp = &bulk; //间接基类指针
bulkp->discont(); //正确,搜索从bulk_quote开始
itemp->discont(); //错误,搜索从quote开始
派生类能逐级向上查找匹配项,而基类不能向下寻找。
派生类也能重用定义在其直接基类或间接基类中的名字,此时外层作用域的名字将被隐藏。
可以通过作用域运算符来使用一个被隐藏的基类成员。
除了覆盖继承而来的虚函数以外,派生类最好不要重用其他定义在基类中的名字。
声明在内层作用域的函数并不会重载声明在外层作用域的函数。
被隐藏的成员或函数将无法调用。
假如基类与派生类的虚函数接受的实参不同,则我们无法通过基类的引用或指针来调用派生类的虚函数。
成员函数无论是否是虚函数都能被重载。
7.构造函数与拷贝控制
7.1、虚析构函数
基类通常应该定义一个虚析构函数,这样就能动态分配继承体系中的对象。
若基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为。
基类的析构函数并不遵循若定义析构函数,则要定义拷贝和赋值操作的经验准则。
若类定义了析构函数,则会阻止合成移动操作。
7.2、合成拷贝控制与继承
基类或派生类的合成拷贝控制成员的行为与其他合成的构造函数、赋值运算符或析构函数类似。
某些定义基类的方式可能导致有的派生类成员成为被删除的函数:
1、若基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符和析构函数是被删除的或不可访问的,则派生类中对应的成员将是被删除的。
2、若基类中有一个不可访问的或删除掉的析构函数,则派生类中合成的默认和拷贝构造函数将被删除的。
上述现象的发生原因在于派生类对象中含有基类部分。
在实际编程过程中,如果在基类中没有默认、拷贝或移动构造函数,则一般情况下派生类也不会定义相关的操作。
基类缺少移动操作会阻止派生类拥有自己的合成移动操作。
7.3、派生类的拷贝控制成员
派生类的析构函数只负责销毁派生类自己分配的资源,它的基类部分会被自动销毁的。
当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动派生类自己的成员和基类部分成员。
在派生类定义拷贝或移动构造函数时,通常使用对应的的基类构造函数初始化对象的基类部分。
派生类的赋值运算符必须显式地为其基类部分赋值。
基类的运算符能正确地处理自赋值的情况。
对象销毁的顺序与其创建的顺序相反。
若构造函数或析构函数调用了某个虚函数,则我们应该执行与调用函数所属类型相对应的虚函数版本。
7.4、继承的构造函数
新标准允许派生类能够重用其直接基类定义的构造函数。
类不能继承默认、拷贝和移动构造函数,若派生类没有直接定义这些构造函数,则编译器会合成。
派生类继承基类构造函数的方式是提供一条注明了基类名的using声明语句。
class bulk_quote : public disc{
public:using disc::disc; //继承disc的构造函数
};
当using作用域构造函数时,对于基类的每个构造函数,编译器都在派生类中生成一个形参列表完全相同的构造函数。
一个构造函数的using声明并不会改变该构造函数的访问级别。
using声明语句不能指定explicit或constexpr。
若基类的构造函数是explicit或constexpr,则继承的构造函数也拥有相同的属性。
当一个基类构造函数含有默认实参时,这些实参并不会被继承,但派生类会获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认实参的形参。
若基类含有几个构造函数,大多数时候派生类会继承所有的构造函数。
8.容器与继承
由于不允许在容器中保存不同类型的元素,当我们使用容器存放继承体系中的对象时,通常必须采取间接存储的方式。
当派生类对象被赋值给基类对象时,其中的派生类部分将被“切掉”,因此容器和存在继承关系的类型无法兼容。
当我们希望在容器中存放具有继承关系的对象时,我们实际上存放的是基类的指针。
9.文本查询程序再探
class queryresult; //未定义,但使用到,先声明
class textquery //保存输入文件
{
public:using line_no = std::vector<std::string>::size_type; //类型别名textquery(std::ifstream&); //构造函数,接受一个文件流为参数queryresult query(const std::string&) const; //
private:std::shared_ptr<std::vector<std::string>> file; //输入文件std::map<std::string, std::shared_ptr<std::set<line_no>>> wm; //用查找的单词做关键字,保存行号的set做值
};textquery::textquery(std::ifstream& is) : file(new std::vector<std::string>)
{std::string text;while (getline(is, text)){file->push_back(text); //保存每一行,file是指针,需解引用int n = file->size() - 1; //当前行号std::istringstream line(text); //分解成单个单词std::string word;while (line >> word){auto& line = wm[word]; //line与wm[word]绑定,line是智能指针if (!line) //若单词不在wm中,返回一个空指针line.reset(new std::set<line_no>); //指针指向新创建的setline->insert(n); //将行号插入set中}}
}queryresult textquery::query(const std::string& sought) const //查找单词
{static std::shared_ptr<std::set<line_no>> nodata(new std::set<line_no>); //空setauto loc = wm.find(sought);if (loc == wm.end())return queryresult(sought, nodata, file); //未找到elsereturn queryresult(sought, loc->second, file);
}class queryresult{friend std::ostream & printis(std::ostream&, const queryresult&); //友元声明
public:typedef std::set<textquery::line_no>::const_iterator line_it;queryresult(std::string s,std::shared_ptr<std::set<textquery::line_no>> p,std::shared_ptr<std::vector<std::string>> f) : //构造函数sought(s), lines(p),file(f) {}line_it begin() const { return lines->cbegin(); }line_it end() const { return lines->cend(); }std::shared_ptr<std::vector<std::string>> get_file() { return file; }
private:std::string sought; //查找的单词std::shared_ptr<std::set<textquery::line_no>> lines; //指向保存单词出现的行号的setstd::shared_ptr<std::vector<std::string>> file; //指向保存文件的vector
};//抽象基类,具体的查询类型从query_base派生,成员都是private的
class query_base {friend class query;
protected:using line_no = textquery::line_no; //类型别名virtual ~query_base() = default; //析构函数
private://返回与当前query匹配的queryresultvirtual queryresult eval(const textquery&) const = 0; //rep是表示查询的一个stringvirtual std::string rep() const = 0;
};class query {//运算符需要访问接受shared_ptr的构造函数,来构造新的query对象,构造函数是私有的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_base的shared_ptr指针作为参数,存储给定的指针query(std::shared_ptr<query_base> query) : q(query) {} std::shared_ptr<query_base> q;
};inline query::query(const std::string& s) : q(new wordquery(s)) {} //分配一个新的wordqueryclass 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); //调用textquery的query成员,在文件中查找}std::string rep() const { return query_word; } //返回查找的单词std::string query_word; //要查找的单词
};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));//将新分配的notquery指针绑定到shared_ptr<query_base>//return 负责将shared_ptr<query_base>类型转换成query(调用query的构造函数)
}//抽象基类,保存操作两个运算对象的查询类型所需的数据
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, "&") {}//从基类binaryquery继承了rep并定义了eval的虚函数queryresult eval(const textquery&) const override;
};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);//含有左侧单词的文本插入到ret_lines指向的set中auto ret_lines = std::make_shared<std::set<line_no>>(left.begin(), left.end()); //含有右侧单词的文本插入到set中ret_lines->insert(right.begin(), right.end());//返回一个queryresult,表示并集return queryresult(rep(), ret_lines, left.get_file());
}queryresult andquery::eval(const textquery& text) const
{auto left = lhs.eval(text), right = rhs.eval(text);//保存left和right交集的setauto ret_lines = std::make_shared<std::set<line_no>>();//该算法将输入序列中共同出现的元素写入目的位置中std::set_intersection(left.begin(), left.begin(), right.begin(), right.end(), std::inserter(*ret_lines, ret_lines->begin()));return queryresult(rep(), ret_lines, left.get_file());
}queryresult notquery::eval(const textquery& text) const
{//包含运算对象出现的行号集合setauto result = query.eval(text);auto ret_lines = std::make_shared<std::set<line_no>>(); //存放的行号auto beg = result.begin(), end = result.end();auto sz = result.get_file()->size();for (size_t n = 0; n != sz; ++n){if (beg == end || *beg != n) //若不在result中,则插入ret_lines->insert(n);else if (beg != end) //获取result的下一行++beg;}return queryresult(rep(), ret_lines, result.get_file());
}