第五章 面向对象的编程风格(定义一个抽象基类)
2015-05-23 10:41
253 查看
5.4定义一个抽象基类
本节将重新定义前一节的num_sequence class.我要为所有数列设计出共享的抽象基类,然后继承它。
这该如何做到呢? 定义抽象基类的第一个步骤就是找出所有子类共通的操作行为。
举个列子,所有数列类的共通操作行为是什么呢?这些操作行为代表的是num_sequence这个基类的公有接口(public interface)。
以下是我的第一次尝试:
class num_sequesce{
public:
//elem(pos):返回位置上的元素 int elem(int pos)
//gen_elems(pos):产生直到位置的所有元素void gen_elems(int pos)
//what_am_i():返回确认的数列类型const char* what_am_i()const
//print(os):将所有元素写入ostream& print(ostream &os=cout)const
//check_integrity(pos)检查是否为有效位置bool _integrity(int pos)
//max_elems():返回支持的最大位置值static int max_elems()
int elem(int pos);
void gen_elems(int pos);
const *char what_am_i()const;
ostream& print(ostream &os=cout)const;
bool check_integrity(int pos);
static int max_elems();//不需要实例,直接调用
//...
elem()会返回用户指定的位置上的元素值。
max_elems()会返回最大的元素个数。
check_integrity()会判断传入的是否为有效位置
print()会输出元素值
gen_elems()会产生数列元素
what_am_i()会返回一个字符串,代表数列的名称。
设计抽象基类的下一步,便是设法找出哪些操作行为与类型相关(type_dependent)——也就是说,由哪些操作行为必须根据不同的派生类而有不同的实现方式,这些操作行为应该成为整个类继承体系中的虚函数。
以本例而言,每个数列都必须提供它们自己的gen_elems()实现,但check_integrity()就不会因为类型不同而有任何差异——它必须能够判断pos位置是否有效而其判断方式并不因数列类型而有所不同。同样的,max_elems()也不会因类型不同而有任何的差异——所有数列类型都可以存储着同样的元素。
并非每一个函数都能如此轻易的做出区分。what_am_i()也许不会和类型相关——这和我们的继承体系的实现有关。对elem()和print()来说,情况也是如此,此刻我们先假设它们都和类型相关。稍后我们将看到另一种截然不同的设计,将它们转换为和类型无关的函数。注意,static member function无法被声明为虚拟函数。
抽象基类设计的第三步,便是试着找出每个操作行为的访问层级。如果某个操作行为应该让一般程序皆能访问,我们应该将它声明为public.例如elem(),max_elems(),what_am_i()但如果某个操作行为在基类之外不需要被用到,我们就将它声明为private.即使是该基类的派生类型,亦无法访问基类中的private member.本例的所有操作行为都必须给派生类用,所以我们不能把它们声明为private.
第三种访问层级,是所谓的protected.这种层级的操作行为可以让派生类访问,却不允许一般程序使用。例如check_integrity() 和gen_elems()都是派生类必须调用的,却不是一般程序会用到的。以下便是重新修正之后的
num_sequence class的定义:
class num_sequence{
public:
virtual ~num_sequence(){};
virtual int elem(int pos)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;
protected:
virtual void gen_elems(int pos)const=0;
bool check_integrity(int pos)const=0;
const static int _max_elems=1024;
};//对数据成员分类的一般规律:1、public\private\protected 2、对virtual进行分类 3、构造继承函数。
每个虚函数,要么得有其定义,要么可设为纯虚函数——如果对于该类而言,这个虚函数并无实际意义的话。
例如:gen_elems()之于num_sequence class.将虚函数赋值为0,意思是另它成为一个纯虚函数:
virtual void gen_elems(int pos)=0;
任何类声明如果有一个或多个纯虚函数,那么,由于其接口的不完整性(纯虚函数没有函数的定义,是谓不完整),程序无法为它产生任何对象。这种类只能作为派生类的子对象(subject)使用,而且前提是这些派生类必须为所有的虚函数提供确切的定义num_sequence class应该声明什么样的data member呢?这个问题并不存在任何必须坚持或者快速判断的标准。本例中,num_sequence并未声明任何data
member,因为它只是为数列继承体系提供的一个接口;派生类必须自行设计自身的data member.
那么constructor和destructor又当如何?由于此类并没有任何non_static data member需要进行初始化操作,所以其constructor亦无存在价值。不过,我会为它设计destructor。是的,根据一般规则,凡基类定义有一个或多个虚函数,应该将其destructor声明为virtual.像这样:
class num_sequence{
public:
virtual~destructor();
//...
}
为什么呢?考虑已下程序片段:
num_sequence *ps=new Fibonacci(12);
//....使用数列
delete ps;
ps是基类num_sequence的指针,但它实际是指向派生类fbonacci对象。当Delete表达式被应用于该指针,destructor会先应用于指针所指的对象上,于是将此对象占用的内存空间归还给程序的空闲(free store)。还记得吗?non_virtual函数在编译时便已经完成了解析(resolved)根据该对象被调用的类型来判断。
于是,本例中,通过ps调用的destructor一定是fibonacci的destructor.正确的情况应该是根据实际对象的类型选择调用哪一个destructor。而解析操作应该在运行时进行。为了促进正确的行为的发生,我们必须将destructor声明为virtual。
但是我并不建议在我们这个基类将其destructor声明为pure virtual——虽然它其实不具有任何实质意义的实现内容。对这类destructor而言,最好是提供空白定义,像下面这样2:
inline num_sequence::~num_sequence(){}
为求完整,下面列出num_sequence的output运算符和check_integrity()函数的实现:
bool num_sequence::
check_integrity(int pos)const
{
if(pos<=0||pos>_max_elems)
{
cerr<<"!!invalid position:"<<pos<<"cannot honor request\n";
return false;
}
return true;
}
ostream& operator<<(ostream &os,const num_sequence &ns)
{return ns.print(os);}
虽然上述内容使我们完成了抽象基类num_sequence的整个定义,但是类本身并不完全,它仅仅是为其派生类提供了一个接口而已。每个派生类还必须提供适合的实现细节,以补足基类num_sequence的不足。
本节将重新定义前一节的num_sequence class.我要为所有数列设计出共享的抽象基类,然后继承它。
这该如何做到呢? 定义抽象基类的第一个步骤就是找出所有子类共通的操作行为。
举个列子,所有数列类的共通操作行为是什么呢?这些操作行为代表的是num_sequence这个基类的公有接口(public interface)。
以下是我的第一次尝试:
class num_sequesce{
public:
//elem(pos):返回位置上的元素 int elem(int pos)
//gen_elems(pos):产生直到位置的所有元素void gen_elems(int pos)
//what_am_i():返回确认的数列类型const char* what_am_i()const
//print(os):将所有元素写入ostream& print(ostream &os=cout)const
//check_integrity(pos)检查是否为有效位置bool _integrity(int pos)
//max_elems():返回支持的最大位置值static int max_elems()
int elem(int pos);
void gen_elems(int pos);
const *char what_am_i()const;
ostream& print(ostream &os=cout)const;
bool check_integrity(int pos);
static int max_elems();//不需要实例,直接调用
//...
elem()会返回用户指定的位置上的元素值。
max_elems()会返回最大的元素个数。
check_integrity()会判断传入的是否为有效位置
print()会输出元素值
gen_elems()会产生数列元素
what_am_i()会返回一个字符串,代表数列的名称。
设计抽象基类的下一步,便是设法找出哪些操作行为与类型相关(type_dependent)——也就是说,由哪些操作行为必须根据不同的派生类而有不同的实现方式,这些操作行为应该成为整个类继承体系中的虚函数。
以本例而言,每个数列都必须提供它们自己的gen_elems()实现,但check_integrity()就不会因为类型不同而有任何差异——它必须能够判断pos位置是否有效而其判断方式并不因数列类型而有所不同。同样的,max_elems()也不会因类型不同而有任何的差异——所有数列类型都可以存储着同样的元素。
并非每一个函数都能如此轻易的做出区分。what_am_i()也许不会和类型相关——这和我们的继承体系的实现有关。对elem()和print()来说,情况也是如此,此刻我们先假设它们都和类型相关。稍后我们将看到另一种截然不同的设计,将它们转换为和类型无关的函数。注意,static member function无法被声明为虚拟函数。
抽象基类设计的第三步,便是试着找出每个操作行为的访问层级。如果某个操作行为应该让一般程序皆能访问,我们应该将它声明为public.例如elem(),max_elems(),what_am_i()但如果某个操作行为在基类之外不需要被用到,我们就将它声明为private.即使是该基类的派生类型,亦无法访问基类中的private member.本例的所有操作行为都必须给派生类用,所以我们不能把它们声明为private.
第三种访问层级,是所谓的protected.这种层级的操作行为可以让派生类访问,却不允许一般程序使用。例如check_integrity() 和gen_elems()都是派生类必须调用的,却不是一般程序会用到的。以下便是重新修正之后的
num_sequence class的定义:
class num_sequence{
public:
virtual ~num_sequence(){};
virtual int elem(int pos)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;
protected:
virtual void gen_elems(int pos)const=0;
bool check_integrity(int pos)const=0;
const static int _max_elems=1024;
};//对数据成员分类的一般规律:1、public\private\protected 2、对virtual进行分类 3、构造继承函数。
每个虚函数,要么得有其定义,要么可设为纯虚函数——如果对于该类而言,这个虚函数并无实际意义的话。
例如:gen_elems()之于num_sequence class.将虚函数赋值为0,意思是另它成为一个纯虚函数:
virtual void gen_elems(int pos)=0;
任何类声明如果有一个或多个纯虚函数,那么,由于其接口的不完整性(纯虚函数没有函数的定义,是谓不完整),程序无法为它产生任何对象。这种类只能作为派生类的子对象(subject)使用,而且前提是这些派生类必须为所有的虚函数提供确切的定义num_sequence class应该声明什么样的data member呢?这个问题并不存在任何必须坚持或者快速判断的标准。本例中,num_sequence并未声明任何data
member,因为它只是为数列继承体系提供的一个接口;派生类必须自行设计自身的data member.
那么constructor和destructor又当如何?由于此类并没有任何non_static data member需要进行初始化操作,所以其constructor亦无存在价值。不过,我会为它设计destructor。是的,根据一般规则,凡基类定义有一个或多个虚函数,应该将其destructor声明为virtual.像这样:
class num_sequence{
public:
virtual~destructor();
//...
}
为什么呢?考虑已下程序片段:
num_sequence *ps=new Fibonacci(12);
//....使用数列
delete ps;
ps是基类num_sequence的指针,但它实际是指向派生类fbonacci对象。当Delete表达式被应用于该指针,destructor会先应用于指针所指的对象上,于是将此对象占用的内存空间归还给程序的空闲(free store)。还记得吗?non_virtual函数在编译时便已经完成了解析(resolved)根据该对象被调用的类型来判断。
于是,本例中,通过ps调用的destructor一定是fibonacci的destructor.正确的情况应该是根据实际对象的类型选择调用哪一个destructor。而解析操作应该在运行时进行。为了促进正确的行为的发生,我们必须将destructor声明为virtual。
但是我并不建议在我们这个基类将其destructor声明为pure virtual——虽然它其实不具有任何实质意义的实现内容。对这类destructor而言,最好是提供空白定义,像下面这样2:
inline num_sequence::~num_sequence(){}
为求完整,下面列出num_sequence的output运算符和check_integrity()函数的实现:
bool num_sequence::
check_integrity(int pos)const
{
if(pos<=0||pos>_max_elems)
{
cerr<<"!!invalid position:"<<pos<<"cannot honor request\n";
return false;
}
return true;
}
ostream& operator<<(ostream &os,const num_sequence &ns)
{return ns.print(os);}
虽然上述内容使我们完成了抽象基类num_sequence的整个定义,但是类本身并不完全,它仅仅是为其派生类提供了一个接口而已。每个派生类还必须提供适合的实现细节,以补足基类num_sequence的不足。
相关文章推荐
- 第五章 面向对象的编程风格(定义一个派生类)
- 第五章 面向对象的编程风格(面向对象编程概念)
- 第五章 面向对象的编程风格(面向对象的编程思维)
- 第五章 面向对象的编程风格(不带继承的多态)
- 第二章 面向对象的编程风格(如何编写函数)
- VC编程技术点滴(二)定义一个简单的对话框
- Java编程:定义一个int型的一维数组,包含10个元素,分别赋一些随机整数,然后求出所有元素的最大值,最小值,平均值,和值,并输出出来。
- 第二章 面向对象的编程风格(使用静态局部变量)
- 第二章 面向对象的编程风格(提供重载函数)
- 点评阿里JAVA手册之编程规约(命名风格、常量定义、代码风格、控制语句、注释规约)
- Problem Description 有一个长度为n(n<=100)的数列,该数列定义为从2开始的递增有序偶数,现在要求你按照顺序每m个数求出一个平均值,如果最后不足m个,则以实际数量求平均值。编程输出该平均值序列。 Input 输入数据有多组,每组占一行,包含两个正整数n和m,n和m的含义
- 【C/C++学院】0817-递归汉诺塔 双层递归 /CPP结构体 /面向过程与面向对象的编程模式/类的常识共用体实现一个类的特征/QT应用于类以及类的常识
- 简单编程(十四)定义一个方法能够判断并返回两个整数的最大值,并调用自己的方法测试是否正确。
- 第四章 基于对象的编程风格(如何实现一个class)
- js编程思路--给网站定义一个全局的js对象,放到window对象中
- 定义一个计算机类:属性:品牌;价格;颜色; 方法:(1)编程的功能 (2)上网的功能 实例化两个对象:“lenovo”;“hasee”
- 冰编程对派出类的功能进行验证。。 轿车类等 派生出自行车类 停车等方法。然后从该类为基类 最大速度。重量等字段以及开车 要求具有速度 定义一个车辆类
- 第二章 面向对象的编程风格(调用函数)
- 黑马程序员—编程实现:猫和狗都会叫,但猫是喵喵的叫,狗是汪汪的叫?定义一个动物类,在动物类(animal)中有一个叫的抽象方法。 写两个子类,一个猫一个狗, * 继承自动物类,并实现相应的抽象方法。
- PHP中将对数据库的操作,封装成一个工具类以及学会使用面向对象的方式进行编程