一步步深入进面向对象编程风格去。
不带继承的多态
5.3节我看了三遍,也没看出他讲了什么东西,反正diss一遍自己写的代码。
我矮子里拔将军,找出了这些知识点来给大家讲述。
static_cast是一个特殊的转换记号,可将整型变量转换为对应的枚举型变量。(如可用于检验某整数是不是代表某一有效数列)
用法举例:
class num_sequence{
public:
//...
static ns_type nstype(int num)
{
return num<=0 || num>=num_seq ? unset : static_cast<ns_type>(num);//三目运算符(用于判断的三目运算符)别看错了!
}
代码说明:①ns_type是enum型变量(enum ns_type{//...};
),②ns_unset是无效值(整型变量)③static_cast<枚举类型变量>(整型变量);
这个用法记住!
定义一个抽象基类
为什么要定义一个抽象基类?
- 因为我想着为一些类设计出共享的抽象基类,这是一个需求
分三个步骤:
- 第一,找出所有子类(一些类)共通的操作行为
- 第二,找出哪些操作行为(基类成员函数)必须根据不同的派生类而有不同的实现方式
- 第三,找出每个操作行为(基类成员函数)的访问层级
定义抽象基类第一步
所有子类共通的操作行为,这些行为代表的是基类的公有接口。举个例子:
class num_sequence{//基类num_sequence
public:
int elem(int pos);//返回存储数列的vector的pos位置上的元素
void gen_elems(int pos);//存储数列的vector后产生直到pos位置的所有元素
const char* what_am_i()const;//返回确切的数列类型(字符型数组/字符串)
ostream& print(ostream &os=cout)const;//将某数列的所有元素写进
//os,准备输出数列所有的元素
bool check_integrity(int pos);//检查pos是否在数列中为有效位置
//即输入的pos是不是合法的输入位置
static int max_elems();//返回存储数列的vector支持的最大位置值
不难看出,这些函数在派生类(数列类,比如Fibonacci类,square类,triangular类)会用到,这些函数(行为)就是派生类(子类)共通的操作行为。
定义抽象基类第二步
找出哪些操作行为(基类成员函数)必须根据不同的派生类而有不同的实现方式。
如果某操作行为(基类成员函数)必须根据不同的派生类而有不同的实现方式,这个操作行为就要成为整个类继承体系的虚函数,即该成员函数(至少在基类中)的声明前加virtual。
- 比如上面例子中的gen_elems()成员函数,他的作用是让存储某个数列的vector后产生直到pos位置的所有元素。每个数列类(派生类)都要提供自己的gen_elems()的实现。所以gem_elems()在基类主体的public里必须声明成虚函数。
- check_integrity()和max_elems()成员函数不用在基类主体的public里必须声明成虚函数。为什么?因为这两个基类成员函数的作用分别是判断pos位置是否输入的合法和求人为规定存放最多多少个数列元素。这些作用对于哪个数列类(派生类)都是一个作用,不会因为数列类主体不同而有所不同。 那就不用给他们在基类主体里声明成虚函数。
- 注意,C++语法规定静态成员函数无法被声明为虚函数
定义抽象基类第三步
- 找出每个操作行为的访问层级。
①某个操作行为(类成员函数)能让一般程序(在该类主体和该类的派生类如果有派生类的话外)访问这个成员函数,那么我们将这个成员函数的声明/定义放进基类的public:里。
②某个操作行为(类成员函数)在基类之外不需要被用到,自然不用在基类之外访问这个成员函数。我们就把这个成员函数的声明/定义放进基类的private:里
③某个操作行为(类成员函数)在该类的派生类(如果有的话)主体/对象可以被访问,却不许一般程序访问。 那么我们就把这个成员函数的声明/定义放进基类的protected:里。
给一个例子,结合着上面的例子(我搬下来给你看)和我说的这三条来看,明朗不少能让你。
class num_sequence{//基类num_sequence重构前
public:
int elem(int pos);//返回存储数列的vector的pos位置上的元素
void gen_elems(int pos);//存储数列的vector后产生直到pos位置的所有元素
const char* what_am_i()const;//返回确切的数列类型(字符型数组/字符串)
ostream& print(ostream &os=cout)const;//将某数列的所有元素写进
//os,准备输出数列所有的元素
bool check_integrity(int pos);//检查pos是否在数列中为有效位置
//即输入的pos是不是合法的输入位置
static int max_elems();//返回存储数列的vector支持的最大位置值
//重构基类num_sequence的定义
clss num_sequence{
public:
virtual ~num_sequence(){};
//正是因为
virtual int elem(int pos)const=0;
//返回存储数列的vector的pos位置上的元素
//这个成员函数在派生类的类主体里或许会用到(访问),在基类和其派生类类主体外也可能用到,
//而且每个派生类主体内的同名成员函数定义(实现)也不一样,
//就把这个基类成员函数声明开头打上虚函数并放进public里
virtual const char* what_am_i()const=0;
//返回确切的数列类型(字符型数组/字符串)
//这个成员函数在派生类的类主体里或许会用到(访问),在基类和其派生类类主体外也可能用到,
//而且每个派生类主体内的同名成员函数定义(实现)也不一样,
//就把这个基类成员函数声明开头打上虚函数并放进public里
static int max_elems(){return _max_elems;}
//返回存储数列的vector支持的最大位置值,
//这个静态成员函数在派生类的类主体里或许会用到(访问),在基类和其派生类类主体外也可能用到,
//但每个派生类主体内的同名成员函数定义(实现)一样,
//就把这个基类静态成员函数声明开头放进public里(C++语法规定静态成员函数无法被声明为虚函数)
virtual ostream& print(ostream &os=cout)const=0;
//将某数列的所有元素写进os,准备输出数列所有的元素
//这个成员函数在派生类的类主体里或许会用到(访问),在基类和其派生类类主体外也可能用到,
//而且每个派生类主体内的同名成员函数定义(实现)也不一样,
//就把这个基类成员函数声明开头打上虚函数并放进public里
protected:
virtual void gen_elems(int pos)const=0;
//存储数列的vector后产生直到pos位置的所有元素,只会在该类(基类)的派生类类主体/类对象中用到,一般程序不会用到这个。
//而且每个派生类主体内的同名成员函数定义(实现)也不一样,
//就把这个基类成员函数声明开头打上虚函数并放进protected里
bool check_integrity(int pos)const;
//检查pos是否在数列中为有效位置(输入的pos是不是合法的输入位置),
//只会在该类(基类)的派生类类主体/类对象中用到,一般程序不会用到这个。
//但每个派生类主体内的同名成员函数定义(实现)一样,
//就把这个基类成员函数声明放进protected里
const static int _max_elems=1024;
//派生类和基类都用到这个数据成员了。一般程序不会用,所以把该数据成员放进protected里。
这里你可能会疑惑声明为virtual的类成员函数,最后缀了一个=0,怎么回事?
这样的类成员函数就是纯虚函数
纯虚函数
- 纯虚函数没有定义,代表这个类成员函数(纯虚函数)在声明它为纯虚函数的类中无实质意义。这也是纯虚函数存在的作用,为什么要用到纯虚函数,的解释(不想定义只想声明基类的虚函数,但要定义其派生类同名(虚)成员函数,zou这么个意思,基类的纯虚函数只是做一个“统一知晓这个成员函数的作用(顾名思义)”的角色罢了。)
- 将虚函数赋值为0,就是让这个虚函数变成了纯虚函数。
例如:virtual void gen_elems(int pos)=0;
- 任何类如果声明有一个(或多个)纯虚函数,声明纯虚函数所在的接口(public/private/protected)就不完整了。也就不能定义/声明这个类的类对象。这种类只能作为该类的派生类的子对象(subobject)使用(把该类(基类)当其派生类的子对象使用就是一般用作声明/定义派生类的成员函数时有
基类名 形参名,
这样的形式。这就是把声明有纯虚函数的基类当其派生类的子对象用了。,但前提是这些派生类必须为所有虚函数提供确切的定义
其他说明
本例的num_sequence类(基类)不声明任何数据成员(数列),因为这个基类只是为“数列继承体系”提供一个接口。这个基类的派生类必须有自身的数据成员。自然,这个基类没有任何的非静态数据成员进行初始化操作,也就不需要给这个基类设计构造函数。
但是还要给这个基类设计析构函数。为什么呢?
- 根据一般规则,凡是基类定义一个/多个虚函数,应该要将基类的析构函数声明为虚函数,(为什么有这样的规则,看下这篇文章——C++:基类析构函数为什么要定义为虚函数——ENSHADOWER的博客)
如:
class num_sequence{
public:
virtual ~num_sequence(){};
//...
再举个例子,如果(注意,如果,就是可能怎么怎么着,所以咱们写代码才要有顾及如果的情况发生)有以下代码块:
num_sequence *ps=new Fibonnacci(12);
//利用指针使用Fibonacci数列
delete ps;
- ps是基类num_sequence的指针,指向派生类Fibonacci的类对象。delete表达式作用时,指针所指的类对象的类析构函数应用于指针所指的类对象身上。
- 于是指针将此类对象占用的内存空间归还给程序的空闲空间。
- 我们不难发现,通过ps类指针调用的析构函数肯定是它指向的类对象(Fibonacci这个派生类的类对象)的析构函数,而非基类num_sequence的析构函数。
- 所以,根据实际类对象的所属类选择调用哪个所属类的析构函数。这是个解析操作。在程序运行时进行,所以为了这样正确的行为发生,我们必须将基类的析构函数声明为虚函数。就是这样:
class num_sequence{
public:
virtual ~num_sequence(){};
//...
然而不建议把这个基类的析构函数声明为纯虚函数(为什么请看LIPPMAN96A的第五章引言部分),所以给这个析构函数(虚函数)提供空白定义即可。如下
inline num_sequence::~num_sequence(){}
至此抽象基类就定义完了。
完整抽象基类的定义
给出完整的抽象基类的定义(有的东西没学,放到下几节来讲)
class num_sequence {
public:
typedef vector<unsigned int>::iterator iterator;
virtual ~num_sequence(){};
virtual num_sequence *clone() const = 0;
virtual unsigned int elem( int pos ) const = 0;
virtual bool is_elem( unsigned int ) const=0;
virtual int pos_elem( unsigned int ) const=0;
virtual const char* what_am_i() const = 0;
static int max_elems(){ return _max_elems; }
virtual ostream& print( ostream &os = cout ) const = 0;
virtual bool operator ==( const num_sequence& ) const=0;
virtual bool operator !=( const num_sequence& ) const=0;
//想要让工作num_sequence operator+( const num_sequence& )函数,
//则需要移除所有的纯虚函数。
// virtual num_sequence operator+( const num_sequence& ) const=0;
virtual iterator begin() const = 0;
virtual iterator end() const = 0;
virtual int length() const = 0;
virtual int beg_pos() const = 0;
virtual void set_position( int pos ) = 0;
virtual void set_length( int pos ) = 0;
virtual const vector<unsigned int>* sequence() const=0;
protected:
// static const int _max_elems = 1024;
//编译器不支持const static类型(已测试确实如此,codeblocks17.12)
enum { _max_elems = 1024 };
virtual void gen_elems( int pos ) const = 0;
bool check_integrity( int pos, int size ) const;
};