OOP(面想对象设计)实践————第十五章心得
文章目录
1 简化版本的字符图案
字符图案:一个矩形的可显示的字符列阵。
即:把字符串用矩形边框包围起来。
示例:绿色为输入的字符串
1.1 编码
#include <iostream> #include <string> #include <vector> #include <list> #include <algorithm> #include <cctype> using std::cin; using std::endl; using std::cout; using std::string; using std::vector; using std::max; //分割字符串 vector<string> split(const string& s){ vector<string> ret; typedef string::size_type string_size; string_size i = 0; while(i != s.size()){ //忽略前段的空白:[先前的i,i)中全部字符都是空格 while(i != s.size() && isspace(s[i])){ i++; } //找出下一个单词的终结点 string_size j = i; //[先前的j,j)中的任意字符都不是空格 while(j != s.size() && !isspace(s[j])){ j++; } //找到了一些非空白符 if(i != j){ ret.push_back(s.substr(i, j - i)); i = j; } } return ret; } //找出向量中最长字符串的长度 string::size_type width(const vector<string>& v){ string::size_type maxlen = 0; for (vector<string>::size_type i = 0; i != v.size(); ++i) { maxlen = max(maxlen, v[i].size()); } return maxlen; } vector<string> frame(const vector<string>& v){ vector<string> ret; string::size_type maxlen = width(v); //输出顶部边框 string border(maxlen + 4, '*'); //输出内部的行,每行都用一个星号和一个空格来框起来 ret.push_back(border); for (vector<string>::size_type i = 0; i != v.size(); ++i) { ret.push_back("* "+ v[i] + string(maxlen - v[i].size(), ' ') + " *"); } //输出底部边框 ret.push_back(border); return ret; } //纵向连接 vector<string> vcat(const vector<string>& top, const vector<string>& bottom){ vector<string> ret = top; // for (vector<string>::const_iterator it = bottom.begin(); it != bottom.end() ; ++it) { // ret.push_back(*it); // } //作用同上 ret.insert(ret.end(), bottom.begin(), bottom.end()); return ret; } //横向连接 vector<string> hcat(const vector<string>& left, const vector<string>& right){ vector<string>ret; //在两幅图案之间留空格 string::size_type width1 = width(left) + 1; //用于遍历left和right的索引 vector<string>::size_type i = 0, j = 0; while(i != left.size() || j != right.size()){ string s; //如果左侧图案还有待复制的行,则复制一行 if(i != left.size()){ s = left[i++]; } //填充至适当长度 s += string(width1 - s.size(), ' '); //如果右侧还有代复制的行,则复制一行 if(j != right.size()){ s += right[j++]; } ret.push_back(s); } return ret; } int main(int argc,const char *argv[]){ string s; while(getline(cin, s)){ vector<string> v = split(s); // for (vector<string>::size_type i = 0; i != v.size(); ++i) { // cout << v[i] <<endl; // } vector<string> fra = frame(v); //打印边框 for (vector<string>::const_iterator i = fra.begin(); i != fra.end(); ++i) { cout << *i <<endl; } // vector<string> col, row; // col = vcat(v, fra); // row = hcat(v, fra); // //打印竖着拼图 // for (vector<string>::size_type i = 0; i != col.size(); ++i) { // cout << col[i] << endl; // } //// //打印横向拼图 // for (vector<string>::size_type j = 0; j != row.size(); ++j) { // cout << row[j] <<endl; // } } return 0; }
1.2 缺点
上面的代码有学多缺陷:
- 1 讲一个字符图形描述为vector<string>,每生成一个图形都要对字符进行复制,这造成了时间和空间上的浪费。 如果要连接一个图形的两个副本,那么就要保存三个副本。(原始副本,新生成的图形两边各需要一个副本)
-
无法直到一个给出的图形是如何生成的。有可能是客户给出的,也可能是是某一原始图形进行了一种或几种特定的操作而生成的图形。
2 用oop解决字符图形
2.1 解决的问题
oop解决的问题是:
- 1保存某个图形是如何生成的结构信息(总体设计);
- 2 减少保存数据的副本(细节设计)。
细节设计使用带引用计数的句柄解决,总体设计用继承来解决。
2.2 总体设计
2.2.1 思想
首先生成一幅图形,前面调用了三个函数frame(加框)、hcat(水平)、vcat(垂直),加上输入的字符串,也就是可以设计为四个类。
虽然四个类存在差异,但是都是图形,可以设计一个基类,然后通过派生类来描述不同特征的图形。
为了使派生类通过继承联系起来,可以通过虚拟函数写不需要明确指定的生成图形的代码。
基类 | Pic_base |
---|---|
派生类 | String_Pic |
派生类 | Frame_Pic |
派生类 | HCat_Pic |
派生类 | VCat_Pic |
继承关系对客户并不需要可见,因为任何操作都不是对某个特定的图形进行的,而是通过一幅图形的抽象概念。
在程序中隐藏继承关系并使用关联计数会使客户在使用的中感到更加方便。
我们将定义一个接口类(Picture类),来让用户操作图形。接口类中使用句柄类Ptr(管理指向对象的指针类)来管理内存。
接口类中Ptr类管理着基类Pic_base,但是将绑定到Ptr的对象始终是Pic_base的某个派生类,因此在创建或销毁Pic_base对象时,通过一个Pic_base指针来完成,但是对象是派生类的对象,因此需要为Pic_base提供一个虚拟析构函数。
为了达到隐藏Pic_base和其关联继承的可见性,用户只能通过Picture类间接操作对象而无法直接访问的任何一个其他类。实现的方法是依赖与正常的保护机制。把这些类的public接口设置为空,使编译其保证只与图形之间的任何操作都通过Picture类实现。
因此现在完整的类有6个;
基类 | Pic_base |
---|---|
派生类 | String_Pic |
派生类 | Frame_Pic |
派生类 | HCat_Pic |
派生类 | VCat_Pic |
接口类(使用句柄类) | Picture |
2.2.2 总体设计框架编码
设计结果
//基类 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: //从一个装有string类型对象的容器中获得数据 Picture(const std::vector<std::string>& = std::vector<std::string>()); private: Ptr<Pic_base>p; };
Picture类的构造函数没有被声明为explicit(显示),
因此可以这样定义:执行时,自动将vs对象转换为一个Picture类型对象。
vector<string> vs; Picture p = vs;
但是如果没有,则只能:
vector<string> vs; Picture p(vs);
2.2.3 设计接口
frame、hcat以及vcat三个操作函数不会改变正在作用于Picture类型对象的状态,因为没有必要把它们定义为成员函数。如果把他们那定义为非成员函数,可以允许左右操作数都可以使用自动转换。
例如一个语句写成
hcat(frame(p), p);会比
p.frame(p).hcat(p);更明了,更有助于用户写一个表达式来生成图形。
如果frame是一个成员函数,那么用户将无法把frame(vs)写成vs.frame(),其中vs是Picture对象。因为成员函数左操作数不能是类型转换后的结果。
继续完善接口:
Picture frame(const Picture&); Picture hcat(const Picture&, const Picture&); Picture vcat(const Picture&, const Picture&); std::ostream& operator<<(std::ostream&, const Picture&);
2.3 详细设计
2.3.1 基类(Pic_base类)
下面的编码中:
纯虚拟函数:不用为虚拟函数写出具体的实现,但是这个类不能有相应的对象。可能会有派生类的对象(实现了该纯虚拟函数)。
定义了纯虚拟函数的类为抽象基类,该类只在继承树中用于提供对象的接口。编译器会禁止为这个类生成相应的对象。
class Pic_base { typedef std::vector<std::string>::size_type ht_sz;//高 typedef std::vector<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;//输出内容}; public: virtual ~Pic_base(){} };
2.3.2 派生类
一个函数的纯虚拟特性也会被继承。如果在派生类中定义了全部继承而来的虚拟函数,那么它就是一个具体的类。
例如为了计算Vcat_Pic类型对象,需要将两个用于构成该对象的Picture类型对象的height相加。
如果在派生类中保存Picture类型对象,那么就违背在Picture类中只定义接口而没有具体实现的设计思想。因此在派生类中存储Ptr<Pic_base>类型对象。
写出相应的派生类编码:
除了在String_Pic类中从向量参数v中将底层的字符复制到data数据成员中,其他的地方都只复制类Ptr<Pic_base>对象,即复制了指针,使引用计数器加1。
类中没有定义复制构造函数、复制运算符函数和析构函数,原编译器会自动为我们生成相应的默认函数。原因是Ptr句柄类会负责管理复制、赋值和删除操作。
//派生类 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; }; 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; };
2.4 实现
2.4.1 实现用户接口
这是其中一个操作函数的实现
Picture frame(const Picture& pic){ return new Frame_Pic(pic.p); //等同于 // Pic_base* temp1 = new Frame_Pic(pic.p);//生成一个新Frame_Pic对象 // Picture temp2(temp1);//使用一个Pic_base*指针构造一个Pictrue类型对象 // return temp2;//返回一个Picture类型对象,这将激活Picture类的复制构造函数 }
由于Frame_Pic的构造函数是私有的,因此需要在Frame_Pic类中将该函数声明为友元函数。同时也声明Picture的友元函数,以便访问p成员。另外,由于生成的是一个Frame_Pic类型,但是需要的是Picture类型对象,可以通过Picture的构造函数实现这种类型转换。
class Picture{ friend Picture frame(const Picture&); //构造函数提供类型转换 Picture(Pic_base* ptr):p(ptr){} };
对于String_Pic类的中向量成员,可以通过定义Picture的构造函数实现转换。
//为vector<string>类型构造Picture类型对象 Picture::Picture(const std::vector<std::string>& v):p(new String_Pic(v)){}
同理得:
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 Vcat_Pic(t.p, b.p); }
以及输出运算符函数:
std::ostream& operator<<(std::ostream& os, const 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);//输出当前行,不需要补齐每一行输出 os << std::endl; } return os; }
2.4.2 实现派生类
2.4.2.1 String_Pic
成员函数实现:
Pic_base::ht_sz String_Pic::height() const { return data.size(); } Pic_base::wd_sz String_Pic::width() const { Pic_base::wd_sz n = 0; for (Pic_base::ht_sz i = 0; i != data.size(); ++i) { n = std::max(n, data[i].size()); } return n; } void String_Pic::display(std::ostream &os, Pic_base::ht_sz row, bool do_pad) const { Pic_base::wd_sz start = 0; //如果row没有超出范围,就输出第row行 if(row < height()){ os << data[row]; start = data[row].size(); } //如果有必要,补齐输出各行 if(do_pad){ pad(os, start, width()); } }
do_pad函数实现:
class Pic_base { protected: //静态成员,不属于类的某个对象 //减少定义全局函数或变量 //虽然抽象基类没有相应的对象,但是派生类会继承它的全部成员函数 static void pad(std::ostream& os, wd_sz beg, wd_sz end){ while(beg != end){ os <<" "; ++beg; } } //。。。 }
2.4.2.2 VCat_Pic
成员函数实现
Pic_base::wd_sz VCat_Pic::width() const { return std::max(top->width(), bottom->width()); } Pic_base::ht_sz VCat_Pic::height() const { return top->height() + bottom->height(); } void VCat_Pic::display(std::ostream &os, Pic_base::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, do_pad); w = bottom->width(); } if(do_pad){ pad(os, w, width()); } }
2.4.2.3 HCat_Pic
成员函数实现:
Pic_base::wd_sz HCat_Pic::width() const { return left->width() + right->width(); } Pic_base::ht_sz HCat_Pic::height() const { return std::max(left->height(), right->height()); } void HCat_Pic::display(std::ostream &os, Pic_base::ht_sz row, bool do_pad) const { left->display(os, row, do_pad|| row < right->height()); right->display(os, row, do_pad); }
2.4.2.4 Frame_Pic
成员函数实现
Pic_base::wd_sz Frame_Pic::width() const { return p->width() + 4; } Pic_base::ht_sz Frame_Pic::height() const { return p->height() + 4; } void Frame_Pic::display(std::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 << std::string(width(), '*'); }else if(row == 1 || row == height() - 2){ //在第二行或倒数第二行 os << "*"; pad(os, 1, width() - 1); os << "*"; }else{ //在内部图形 os << "* "; p->display(os, row - 2, true); os << " *"; } } }
2.5 后续问题
//基类 class Pic_base { friend class String_Pic; friend class Frame_Pic; friend class HCat_Pic; friend class Vcat_Pic; friend std::ostream& operator<<(std::ostream& , const Picture& ); typedef std::vector<std::string>::size_type ht_sz;//高 typedef std::vector<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;//输出内容 protected: //静态成员,不属于类的某个对象 //减少定义全局函数或变量 static void pad(std::ostream& os, wd_sz beg, wd_sz end){ while(beg != end){ os <<" "; ++beg; } } public: //虚拟函数 virtual ~Pic_base(){} };
基类的派生类都声明为友元,为什么不可以通过继承来获得访问Pic_base基类成员的权利?
派生类确实可以通过继承获得访问Pic_base类的某些成员,但是仅限于访问保护成员的权利。
那么为什么不像定义pad函数,将这些成员函数定义为Pic_base类的保护成员呢?
因为派生类的成员只能访问自己类对象的基类部分的保护成员,或者是派生的其他类,但是它们不能访问单独的基类对象的保护成员,即不是派生类对象一部分的对象。
例如:类Frame_Pic可以访问Frame_Pic对象Pic_base部分的保护成员,或者派生自Frame_Pic的类成员,但是不能访问单独的Pic_base对象的保护成员。
也就是派生类只能访问类的对象自身拥有的保护成员,但是他们不能访问其他对象的保护成员的权利。
我们设计的派生类只能访问只能访问自己本身所包含的Pic_base部分中的保护成员。但是派生类中Ptr<Pic_base> p指针不能访问派生类的基类中的保护成员,因此只能用友元。
2.6 代码汇总
#include <iostream> #include <vector> #include <string> #include "Picture.h" using std::endl; using std::cin; using std::cout; using std::string; using std::vector; //分割字符串 vector<string> split(const string& s){ vector<string> ret; typedef string::size_type string_size; string_size i = 0; while(i != s.size()){ //忽略前段的空白:[先前的i,i)中全部字符都是空格 while(i != s.size() && isspace(s[i])){ i++; } //找出下一个单词的终结点 string_size j = i; //[先前的j,j)中的任意字符都不是空格 while(j != s.size() && !isspace(s[j])){ j++; } //找到了一些非空白符 if(i != j){ ret.push_back(s.substr(i, j - i)); i = j; } } return ret; } int main(int argc, char** argv){ string str; getline(cin, str); vector<string> vs = split(str); Picture p = vs; Picture q = frame(p); Picture r = hcat(p, q);//横向 Picture s = vcat(q, r);//纵向 cout << frame(hcat(s, vcat(r, q))) << endl; // cout << q << endl; return 0; }
#ifndef ACM_PTR_H #define ACM_PTR_H #include "Refptr.h" #include "Student_info.h" template <class T>T* clone(const T* tp){ return tp->clone(); } template <class T> class Ptr{ public: //在需要时有条件复制对象 void make_unique(){ if(*refptr != 1){ --*refptr; refptr = new size_t(1); p = p?clone(p):0; } } Ptr():p(0),refptr(new size_t(1)){} Ptr(T* t):p(t),refptr(new size_t(1)){} Ptr(const Ptr& h):p(h.p),refptr(h.refptr){++*refptr;} Ptr& operator=(const Ptr&); ~Ptr(); operator bool() const {return p;} T& operator*() const{ if(p) return *p; throw std::runtime_error("unbound Ptr"); } T* operator->() const{ if(p) return p; throw std::runtime_error("unbound Ptr"); } private: T* p; std::size_t * refptr; }; template <class T> Ptr<T>& Ptr<T>::operator=(const Ptr &rhs) { ++*rhs.refptr; if(--*refptr == 0){ delete(refptr); delete(p); } refptr = rhs.refptr; p = rhs.p; return *this; } template <class T> Ptr<T>::~Ptr() { if(--*refptr==0){ delete(refptr); delete(p); } } #endif //ACM_PTR_H
源代码:
#include <iostream> #include <vector> #include <string> #include "Picture.h" using std::endl; using std::cin; using std::cout; using std::string; using std::vector; //分割字符串 vector<string> split(const string& s){ vector<string> ret; typedef string::size_type string_size; string_size i = 0; while(i != s.size()){ //忽略前段的空白:[先前的i,i)中全部字符都是空格 while(i != s.size() && isspace(s[i])){ i++; } //找出下一个单词的终结点 string_size j = i; //[先前的j,j)中的任意字符都不是空格 while(j != s.size() && !isspace(s[j])){ j++; } //找到了一些非空白符 if(i != j){ ret.push_back(s.substr(i, j - i)); i = j; } } return ret; } int main(int argc, char** argv){ string str; getline(cin, str); vector<string> vs = split(str); Picture p = vs; Picture q = frame(p); Picture r = hcat(p, q);//横向 Picture s = vcat(q, r);//纵向 cout << frame(hcat(s, vcat(r, q))) << endl; // cout << q << endl; return 0; }繁星蓝雨 原创文章 507获赞 181访问量 9万+ 关注 私信
- Java面向对象设计最佳实践 - 枚举设计
- 面向对象设计实践指南 读书笔记
- Java面向对象设计最佳实践 - 方法设计(一)
- 面向对象设计模式实践之抽象工厂模式
- LINQ快速开发设计最佳实践(三) LINQ数据访问与业务逻辑层对象模板
- Java面向对象设计最佳实践――内置类设计
- c++primer第十五章面向对象设计小结-15
- 编程实践心得与设计思想
- 朱晔的互联网架构实践心得S1E9:架构评审一百问和设计文档五要素
- 朱晔的互联网架构实践心得S2E3:品味Kubernetes的设计理念
- 编程实践心得与设计思想
- Scala.Actor实践心得与设计思想
- Java面向对象设计最佳实践 - 内置类设计
- Java面向对象设计最佳实践 - 内置类设计 3
- 面向对象与结构化软件设计方法的实践对比(有点意思)
- Scala.Actor实践心得与设计思想
- 软件工程——理论、方法与实践 之 面向对象设计
- 面向对象分析和设计(OOA,OOD,OOP,OOT)
- 设计模式讲解与代码实践(七)——适配器(基于对象)
- 朱晔的互联网架构实践心得S1E8:三十种架构设计模式(下)