面向对象vs基于对象
面向对象(Object-Oriented) 和 基于对象(Object-Based) 其实是两个不同的概念。笔者在阅读《Essential C++》时发现作者把基于对象的编程风格
和面向对象的编程风格
列为并列的两章来讲解。基于对象主要讲解了类class
的设计;而面向对象章节则主讲面向对象三大特性。以下我会逐步辨析《Essential C++》中的num_sequence
类的实现来帮助读者领悟面向对象思维。
以下援引百度百科关于对于这两种方式的论述:
基于对象(Object-Based),和面向对象(Object-Oriented)不是一个概念,不提供抽象、继承、重载等有关面向对象语言的功能。
基于对象的编程语言没有提供象抽象、继承、重载等有关面向对象语言的许多功能。而是把其它语言所创建的复杂对象统一起来,从而形成一个非常强大的对象系统,以供使用。
面向对象的三大特性
封装(Package)、继承(Inherit)、多态(Polymorphism) 是OO思想最明显的体现。
我们现在打造一个num_seq
类(数列类)作为基类,就有如下图关系:
num_seq
代表数列类,Fibonacci
等等都是特殊的数列,可以认为他们之间有继承-派生关系。
class num_seq//抽象基类
{
public:
int elem(int pos);//返回下标pos的元素值
void gen_elems(int pos);//生成直到pos的元素
string what_am_i()const;//返回数列类型
ostream &print(ostream &os = cout) const;//输出数列内容
bool check_integrity(int pos);//检查pos合法性
static unsigned int max_elems();//返回数列元素上限
};
封装
这个抽象基类目前还非常不完善。我们还需要确定各个成员的访问权限,也即是执行封装操作。
问:为什么我们需要封装操作?直接开放访问权限的话,我们可以省去许多接口函数的编写,来让我们的操作更加灵活。
·
答:放弃封装操作确实可以提供更高的灵活性。但这样做不利于拓展和维护。比如下面的一个容器类:
class Array
{
public:
int size();//返回大小
int getElem(int pos);
int insert(int val, int pos);//插入
//···省略更多操作
//到底需要吗?
protected:
int* _elems;
int _size;
};
如果直接开放访问权限的话,将可以从外部不经过接口就改变成员的值。这样就会有许多不可预料的情况发生。比如因为没有同步修改_elems
和_size
而导致_size
不再与_elems
指向数组大小相等。
正是因为封装,我们把对protected
成员的可能发生的操作限定在了成员函数和友元这几种操作方式。保证私有和保护成员不会遭到意料之外的修改。
所以经过封装之后的代码是:
class num_seq
{
public:
int elem(int pos);
string what_am_i()const;//不加上const,const修饰的对象就无法使用这个函数,所以请尽量加上。
ostream& print(ostream &os = cout) const;
static unsigned int max_elems()const;
~num_seq(){}//空白定义析构函数,因为基类并没有需要析构的数据成员
protected://我们希望子类对象可以访问这些成员
void gen_elems(int pos);
bool check_integrity(int pos)const;
const static unsigned int _max_elems = 1024;
};
bool num_seq::check_integrity(int pos)const
{
if(pos<=0||pos>_max_elem)
{
cerr << "Invaild position:" << pos << endl;
return false;
}
return true;
}
check_integrity()
和_max_elems
似乎与数列种类是什么没有关系,都是适用的,所以不妨在基类就实现它,直接由子类继承。
这个基类还是不太完善,因为我们还需要继续判定各个函数是否需要根据数列种类不同而重写。
继承
抽象基类已经提供了基本的接口,我们把不同的数列认为是其不同的子类,继承基类之后,需要重写一些基类函数,来让他契合子类的特性。这里以Fibonacci
子类的实现为例子:
// 为了方便,我直接把函数定义写在了类内
//该类的对象是Fibonacci数列的一个子列
class Fibonacci:public num_seq//继承
{
public:
Fibonacci(int len=1,int beg_pos=1)
:_length(len),_beg_pos(beg_pos)
{
if(beg_pos+len>_elems.size())
gen_elems(beg_pos + len);
}
int elem(int pos)
{
if(!check_integrity(pos))
return 0;
if(pos>_elems.size())
Fibonacci::gen_elems(pos);//不加Fibonacci::也可以,不过我希望在这里静态绑定它。
return _elems[pos - 1];
}
string what_am_i()const
{
return "Fibonacci";
}
ostream &print(ostream &os = cout) const
{
for (int i = _beg_pos; i < _beg_pos+_length; ++i)
{
os << _elems[i] << ' ';
}
return os;
}
static unsigned int max_elems()
{
return _max_elems;
}
protected:
void gen_elems(int pos)//斐波拉契数列生成方式
{
if(_elems.empty())
{
_elems.push_back(1);
_elems.push_back(1);
}
if(_elems.size()<=pos)
{
int ix = _elems.size();
int n2 = _elems[ix - 2];
int n1 = _elems[ix - 1];
for (; ix < pos; ++ix)
{
int new_one = n1 + n2;
_elems.push_back(new_one);
n2 = n1;
n1 = new_one;
}
}
}
//bool check_integrity(int pos)const;//不需要这两行
//const static unsigned int _max_elems = 1024;
int _length;
int _beg_pos;
static vector<int> _elems;
};
vector<int> Fibonacci::_elems;//别忘了这行
注意到我们并不需要重写check_integrity(int pos)const
和const static unsigned int _max_elems
,可以直接从基类继承过来。
这里采用了静态存储斐波拉契数列的实体,所有对象共用一份实体,减少了浪费。
静态对象成员必须在类外初始化,别忘了追加上面代码的最后一行。
到现在,你已经可以在主函数内测试一下了,也可以继续追加其他数列的派生类。但目前为止,还是有一些问题没有解决。
多态
多态(Polymorphism) 包含静态多态和动态多态。顾名思义,多态就是多种形态。我们在重载时就实现了一种多态(同一个函数名、运算符具有多种形态)。重载是一种静态多态,因为具体调用哪一个函数在编译时就可以确定了,这也可以称之为静态绑定。看下面这个函数:
//显示数列名称以及前三个元素
void func(num_seq& ns)
{
cout << ns.what_am_i() << ':'
<< ns.elem(1) << ' '
<< ns.elem(2) << ' '
<< ns.elem(3) << endl;
}
这个函数的神奇之处在哪里?是num_seq&
类型的形参也可以接受其子类对象作为实参。这样的话好处多多。这个函数可以接受其所有的子类的对象,以后根据实际需要追加了新的子类,这个函数未经修改也可以很好的发挥功能。
我们的基类在这里起到了框架的作用。非常方便程序以后拓展更多内容。
但如果你亲自试验过传入子类对象,你就会发现这个函数是无法工作的。他会告诉你这一系列函数没有定义。
这是因为函数名被动态绑定至父类,而父类里面并没有给出实现。
要让他使用子类内定义的同名函数,就要对父类作如下修改:加上关键字virtual
,因为我不打算在基类中给予定义,所以将其声明为纯虚函数。
class num_seq
{
public:
virtual int elem(int pos) = 0;//纯虚函数
virtual string what_am_i()const = 0;
virtual ostream &print(ostream &os = cout) const = 0;
static unsigned int max_elems() { return _max_elems; }
virtual ~num_seq(){}//虚析构
protected:
virtual void gen_elems(int pos) = 0;
bool check_integrity(int pos)const;
const static unsigned int _max_elems = 1024;
};
一旦有一个纯虚函数,这样的类将被认为是抽象类,因为成员函数没有实现,所以不能声明对象实体。
对析构函数使用virtual
,可以避免一些问题,我现在做一个简单的例子:
//演示案例,为构造析构都做了标记
class A
{
private:
string s;
public:
A() : s("") { cout << "Construct A" << endl; }
~A() { cout << "Destruct A" << endl; }//要不要加上virtual?
};
class B: public A
{
private:
string s1;
public:
B():s1(""){ cout << "Construct B" << endl; }
~B(){ cout << "Destruct B" << endl; }
};
int main()
{
A *p = new B;//这是允许的
delete p;
return 0;
}
运行一下:
发现并没有运行子类的析构。这会是个问题。如果采用虚析构:
如果采用了多重继承的话,这样的问题会变得极为复杂。所以一般不推荐多重继承,多重继承还需要虚继承来避免多次继承。这里不做论述。
总结
面向对象基于三大特性,过程语言比如C语言虽然能模拟一些对象结构,但要实现这三大特性非常困难。
鉴于本人水平有限,所以有错误欢迎提出,敬请谅解。