第十五章 再探字符图形
基类:Pic_base 四个派生类:String_Pic、Frame_Pic、HCat_Pic、VCat_Pic、接口类:Picture。
我们打算隐藏Pic_base和其关联继承的可见性,客户只能通过Picture类间接的操作对象而不能直接的访问任何一个其他类。
//只在执行的时候用到的私有类
class Pic_base{};
class String_Pic:public Pic_base{};
class Frame_Pic: public Pic_base{};
class Vcat_Pic: public Pic_base{};
class Hcat_Pic: public Pic_base{};
//公有接口类与操作
class Picture{
public:
Picture(const std::vector<std::string>&=std::vector<std::string>());
private:
Ptr<Pic_base> p;
};
默认参数的结果是允许我们使用像下面这样的定义:
Picture p; //一个空的Picture类型对象
来生成一个没有任何元素的Picture类型对象。
Picture frame(const Picture&);
Picture hcat(const Picture&,const Picture&);
Picture vcat(const Picture&,const Picture&);
sdt::ostream& operator<<(std::ostream&,const Picture&);
让display函数带三个参数:一个是用来生成输出的流;另一个是要输出的行数,还有一个是一个bool型的变量,表示是否让图形补齐各行的宽度。
class Picture{
//没有公有接口
typedef std::vector<std::string>::size_type ht_sz;
typedef std::string::size_type wd_sz;
virtual wd_sz width() const = 0;
virtual ht_sz height() const = 0;
virtual void display()(std::ostream&,ht_sz,bool) const = 0;
};
对于Pic_base类来说,没有必要获得height和width数据成员。
如果为一个类定义了即使是一个纯虚函数,我们也就隐式的声明了该类没有相对应的对象。我们把这种类叫做抽象基类。因为它们在一个继承树中只是用来提供抽象的接口的基类。它们是完全抽象的:没有自己的对象。一旦为一个类定义了一个纯虚函数,该类就是一个抽象基类,编译器将强行禁止我们为这个类生成相应的对象。
一个函数的纯虚拟特性也可以被继承。如果在一个派生类中定义了所有继承而来的纯虚函数,那么它就是一个具体的类。我们可以为该类生成它的对象。但是,只要在派生类中有一个继承而来的纯虚拟函数没有进行定义,那么该派生类将继承基类的抽象特性,它也是一个抽象类。在这种情况下,派生类本身也是抽象的,我们不能生成该派生类的对象。
class Pic_base{
//没有公共接口
typedef std::vector<std::string>::size_type ht_sz;
typedef std::string::size_type wd_sz;
//这是个抽象基类
virtual wd_sz width() const = 0;
virtual ht_sz height() const = 0;
virtual void display()(std::ostream&,ht_sz,bool) const = 0;
};
class Frame_Pic:public Pic_base{
//没有公共接口
Ptr<Pic_base> p;
Frame_Pic(const Ptr<Pic_base>& pic):p(pic){}
wd_sz width() const;
ht_sz height() const;
void display(std::ostream&,ht_sz,bool) const;
};
class Vcat_pic:public Pic_base{
Ptr<Pic_base> top,bottom;
Vcat_Pic(const Ptr<Pic_base>& t,const Ptr<Pic_base>& b):top(t),bottom(b){ }
wd_sz width() const;
ht_sz height() const;
void display(std::ostream&,ht_sz,bool) const;
};
class Hcat_pic:public Pic_base{
Ptr<Pic_base> left,right;
Hcat_Pic(const Ptr<Pic_base>& l,const Ptr<Pic_base>& r):left(l),right(r){ }
wd_sz width() const;
ht_sz height() const;
void display(std::ostream&,ht_sz,bool) const;
};
class String_Pic:public Pic_base{
std::vector<std::string> data;
String _Pic(const std::vector<std::string>& v):data(v){ }
wd_sz width() const;
ht_sz height() const;
void display(std::ostream&,ht_sz,bool) const;
};
这是整个程序唯一对字符进行复制的地方。在其他的地方,我们只复制Ptr<Pic_base>对象,它实际上只是复制了指针,使引用计数器增加一。
我们来实现接口类与它的操作函数
class Picture{
public:
Picture(const std::vector<std::string>&=std::vector<std::string>());
private:
Ptr<Pic_base> p;
};
Picture frame(const Picture&);
Picture hcat(const Picture&,const Picture&);
Picture vcat(const Picture&,const Picture&);
std::ostream& operator<<(std::ostream&,const Picture&);
Picture frame(const Picture& pic)
{
Pic_base* ret = new Frame_Pic(pic.p);
}
通过一个Pic_base*指针来构造一个Picture类型对象:
class Picture{
Ptr<Pic_base> p;
Picture(Pic_base* ptr):p(ptr){ }
//其他部分同前
};
有了这个Picture类的构造函数以后,我们就能把frame函数写出来:
Picture frame(const Picture& pic)
{
return new Frame_Pic(pic.p);
}
//这个简单的语句与下面的语句作用是相同的
Pic_base* temp1 = new Frame_Pic(pic.p);
//用一个Pic_base*指针构造一个Picture类型对象
Picture temp2(temp1);
//返回一个Picture类型对象,这将激活Picture类的复制构造函数
return temp2;
像frame函数一样,连接函数依赖于Picture类的一个新的构造函数:
Picture hcat(const Picture& l,const Picture& r)
{
return new Hcat_Pic(l.p,r.p);
}
Picture vcat(const Picture& t,const Picture& b)
{
return new Hcat_Pic(t.p,b.p);
}
Picture::Picture(const vector<string>& v):p(new String_Pic(v)){ }
ostream& operator<<(ostream& os,Picture& picture)
{
const Pic_base::ht_sz ht = picture.p->height();
for(Pic_base::ht_sz i = 0;i!= ht;++i){
picture.p->display(os,i,false);
}
return os;
};
在前面我们实现了接口类和其操作,现在来看一看它的派生类。
class String_Pic:public Pic_base{
friend class Picture;
std::vactor<std::string> data;
String_Pic(const std::vecter<std::string>& v):data(v){ }
ht_sz height() const {return data.size();}
wd_sz width() const;
void display(std::ostream&,ht_sz,bool) const;
};
为了得到String_Pic类的width函数,我们先来看看data中的每一个元素,看看哪一个元素是最长的:
Pic_base::wd_sz String_Pic::width() const
{
Pic_base::wd_sz n = 0;
for(Pic_base::hz_sz i = 0;i!=data.size();++i)
n = max(n,data[i].size());
return n;
}
有不要检查一下输出的行是不是超出范围了。
void String_Pic::display(ostream& os,ht_sz,bool do_pad) const
{
wd_sz start = 0;
//如果row没有超出范围,就输出第row行
if(row<height()){
os<<data(row);
start = data[row].size();
}
//如果必要的话补齐输出各行
if(do_pad)
pad(os,start,width());
}
补齐输出结果。因为希望可以从任何一个派生类中访问这个函数,所以我们把这个函数定义为Pic_base类的一个成员函数,并且把它定义成静态的和保护类型的:
class Pic_base{
//其他部分同前
protected:
static void pad(std::ostream& os,wd_sz beg,wd_sz end){
while(beg!=end){
os << " ";
++beg;
}
}
};
令人惊讶的是,我们还可以为一个抽象基类定义一个成员函数。因为我们很难想象,如果一个基类不能有相应的对象,那它为什么还可以拥有成员函数呢?但是请记住,每个派生类的对象都包含有一个基类的部分。在每一个派生类中还继承了基类中定义的所有成员函数。因此,一个基类的函数将在一个派生类型对象的基类部分被调用。在这种特殊的情况下,我们定义的函数是一个静态的函数,所以访问基类成员函数的问题是没有实际意义的。不过我们要认识到在抽象类中既可以定义静态函数,也可以定义成员数据和普通的成员函数,这一点很重要。这些函数将用来访问一个派生类对象中的基类部分。
静态成员(包括静态函数和静态数据)十分好用,因为它们可以让我们尽量少定义全局的函数或者变量。
VCat_Pic类:
class VCat_Pic:public Pic_base
{
friend Picture vcat(const Picture&,const Picture&);
Ptr<Pic_base> top,bottom;
VCat_Pic(const Ptr<Pic_base>& t,const Ptr<Pic_base>& b):top(t),bottom(b){ }
wd_sz width() const {return max(top->width(),bottom->width());}
ht_sz height() const {return top->height()+bottom->height();}
void display(std::ostream&,ht_sz,bool) const;
};
display函数也并不难:
void VCat_Pic::display(ostream& os,ht_sz row,bool do_pad) const
{
wd_sz w = 0;
if(row<top->height()){
//现在处于上面的子图形里
top->display(os,row,do_pad);
w = top->width();
}else if(row<height()>){
//现在处于下面的子图形里
bottom->display(os,row-top->height(),do_pad);
w = bottom->width();
}
if(do_pad)
pad(os,w,width());
}
HCat_Pic类与前面的VCat_Pic类十分相似
class HCat_Pic:public Pic_base
{
friend Picture hcat(const Picture&,const Picture&);
Ptr<Pic_base> left,right;
HCat_Pic(const Ptr<Pic_base>& l,const Ptr<Pic_base>& r):left(l),right(r){ }
wd_sz width() const {return left->width()+right->width());}
ht_sz height() const {return (left->height(),right->height());}
void display(std::ostream&,ht_sz,bool) const;
};
这里提供的display函数比VCat_Pic类中对应的函数要简单的多,因为在这里我们不用去判断行号row是在哪个子图的范围里:
void Hcat_Pic::display(ostream& os,ht_sz row,bool do_pad) const
{
left->display(os,row,do_pad||row<right->height());
right-<display(os,row,do_pad);
}
Frame_Pic类
class Frame_Pic:public Pic_base{
friend Picture frame(const Picture&);
Ptr<Pic_base> p;
Frame_Pic(const Ptr<Pic_base>& pic):p(pic){ }
wd_sz width() const {return p-<width()+4;}
ht_sz height() const {return p-<height()+4;}
void display(std::ostream&,ht_sz,bool) const;
};
display函数很冗长乏味,但是一点也不难
void Frame_Pic::display(ostream& os,ht_sz row,bool do_pad) const
{
if(row>height()){
//超出范围
if(do_pad)
pad(os,0,width());
}else{
if(row==0||row==height()-1){
//在最顶行或者最底行
os << string(width(),"*");
}else if(row == 1||row = height()-2){
//在第二行或者倒数第二行
os << "*";
pad(os,l,width()-1);
os << "*";
}else{
//在内部图形中
os << "*";
p->display(os,row-2,true);
os << "*";
}
}
}
不要忘了友元函数
//对Picture类进行前置声明
class Pictrue;
class Pic_base{
friend std::ostream& operator<<(std::ostream&,const Picture&);
friend class Frame_Pic;
friend class HCat_Pic;
friend class VCat_Pic;
friend class String_Pic;
//没有公共接口
typedef std::vector<std::string>::size_type ht_sz;
typedef std::string::size_type wd_size;
//这是个抽象基类
virtual wd_sz width() const = 0;
virtual ht_sz height() const = 0;
virtual void display(std::ostream&,ht_sz,bool) const = 0;
protected:
static void pad(std::ostream& os,wd_sz,wd_sz);
};
class Picture{
friend std::ostream& operator<<(std::ostream&,const Picture);
friend Picture frame(const Picture&);
friend Picture hcat(const Picture&,const Picture&);
friend Picture vcat(const Picture&,const Pictuer&);
public:
Picture(const std::vector<std::string>& = std::vector<std::string>());
private:
Picture(Pic_base* ptr):p(ptr){ }
Ptr<Pic_base> p;
};
//对Picture类型对象进行操作
Picture frame(const Picture&);
Picture hcat(const Picture&,const Picture&);
Picture vcat(const Picture&,const Picture&);
std::ostream& operator<<(std::ostream&,const Picture&);
派生类只能访问类的对象自身拥有的保护成员,但是它们没有访问其他对象的保护成员的特权。
这个规则也许会令你感到惊讶,但是从逻辑上来讲这是十分简单的:如果语法允许一个派生类访问任何一个对象的保护成员,那么将会破坏这种保护机制。如果在需要访问某个类中的保护成员的时候,我们可以写一个新的类作为该类的派生类,然后在这个新生成的派生类中定义一个成员函数来访问基类中的保护对象。这么做会破坏类的设计者们最初的保护机制。因此,对保护成员的访问权力仅限于对象,而不允许其他的对象访问该对象的保护成员。
前置声明:有时候会有两个类在定义中互相用到了对方的定义,这时候应该哪个定义放在前面呢?为了解决这个问题,你可以先写出下面的语句:
class class-name;
这个语句声明了一个class-name类,但是还没有具体实现它,不过你可以在接下来定义另一个类的时候直接使用class-name类
像这样两个类互相使用互相依赖有可能使程序无法正常工作。例如:
class Yang; //预声明
class Yin{
Yang y;
};
class Yang{
Yin y;
};
这么一来,要实现这种类型就需要无穷大的内存。
Picture类中没有直接包含一个Pic_base类的对象作为数据成员。它包含的是一个Ptr<Pic_base>类的对象成员,而这个成员只是包含了一个Pic_base*类型的指针。对指针的这种使用避免了对象之间无穷的嵌套使用。
对于一个指针或者一个引用,编译器事先并不关心指针类型的实现细节,直到通过该指针或者引用来调用一个函数的时候才会关心指针类的具体内容。因为在operator<<函数的声明中使用const Picture&类型只是作为一个参数类型,所以编译器只需知道Picture是一个类型。而该类型实现细节再定义operator<<之前编译器并不关心。