本节书摘来自异步社区出版社《Imperfect C++中文版》一书中的第2章,第2.10节,作者: 【美】Martin D.Carroll , Margaret A.Ellis,更多章节内容可以访问云栖社区“异步社区”公众号查看。
2.10 练习
C++代码设计与重用
2.10 练习
2.1 给出下面被建议为最小标准接口函数的反例:
a.输入函数;
b.输出函数;
c.用字符串返回外层类类名的函数。
2.2 考虑类WORM_Pool,它和2.4.1节的Pool类很相似,但这一点除外,它在只能写一次但可读多次的内存区域分配内存块。那么,类WORD_Pool是析构函数的反例吗?请说明是或不是的原因。
2.3 假设我们为用户提供一个类Buf,它描述一个缓冲区:
class Buf {
public:
Buf(size_t sz);
//...
};
类Buf的构造函数创建了一个sz个字符大小的缓冲区。假设以后我们的用户想要传递指向Buf的指针,并且他们可以操纵这些指向Buf的指针。这样就有很多指针指向Buf,以至于他们难以决定何时可以安全地删除Buf。
a.为了帮助我们的用户,我们可以创建另外一个类Bufptr,它用来描述一个指向Buf的智能指针:
class Bufptr {
public:
Bufptr(Buf* p);
Buf* operator->() {return rep;}
//...
private:
Buf* rep;
};
Bufptr的构造函数创建了一个指向p的智能指针,operator->返回这个指针。我们也可以在Bur中增加一个指向Buf的引用计数:
class Buf {
private:
friend class Bufptr;
int refcnt;
//...
};
refcnt的值总等于指向Buf的Bufptr的数目,并且,类Bufptr的各种成员函数会维护refptr的值。当没有其他Bufptr对象指向这个Buf的时候,最后一个Bufptr对象的析构函数会删除它所指向的Buf。那么,如果我们把类Buf的析构函数声明为私有函数,我们应该防止哪些在用户端可能会出现的错误呢?把类Buf的析构函数声明为私有函数的缺点又是什么呢?
b.假设我们没有提供类Buf和类Bufptr,而是提供一个单独的类Bufref,它描述一个指向底层缓冲区的引用:
class Bufref {
public:
Bufref(size_t sz);
Bufref(const Bufref& b);
//...
};
它的构造函数将创建一个引用,这个引用指向一块新分配sz个字符的底层缓冲区。拷贝构造函数(具有正规语义)也创建一个新的引用,但它指向b所指向的已经存在的缓冲区。Bufref还提供了一个析构函数,如果已经没有其他的Bufref对象指向这块缓冲区的话,这个析构函数将会删除这块底层缓沖区。请说出类Bufptr相对于类Buf和类Bufptr的优点在什么地方?
c.对于析构函数作为最小标准接口中的函数这个问题,类Buf和类Bufptr是否可以作为令人信服的反例?
2.4 在2.4节里,我们给出了一个类可以作为最小标准接口函数的反例的3个原因,在这个练习里,我们还给出另外两个理论上的但在实际中并不会出现的原因。
a.Complexity_class是一个模拟复杂类型([HU79])的类,假设我们可以创建Complexity_class的实例来模拟复杂类型P和复杂类型NP。那么,针对提议的最小标准接口函数,Complexity_class可以作为哪些函数的反例呢?
b.(*)考虑两个类—TM描述一台图灵机[DW83],Tmset表示图灵机的集合。假设我们可以创建Tmset的实例,用它来表述图灵机的集合,并且这些图灵机在空白磁带开始处将会停止运转。进一步假设存在下面的函数,它将返回tms和{tm}的联合:
Tmset operator+(const Tmset& tms, const TM& tm);
请证明(或说明):Tmset的相等运算符(operator= =)的实现将要求计算一个不可计算的函数(incomputable function)。
2.5 这个练习用于进一步考察浅拷贝和深拷贝。
a.给出一个现实存在的类,它用浅拷贝操作实现它的拷贝构造函数。
b.(*)给出一个现实存在的类,它至少拥有一个指针数据成员,指针的实现不存在内存泄漏,并且通过浅拷贝操作实现它的拷贝构造函数。
c.(**)给出一个现实存在的类,它的浅拷贝操作会破坏某种不变性(invariant,见2.5节),并且这种破坏性是不可以修复的。给出一个现实存在的类,它的深拷贝操作会破坏某种不变性,并且这种破坏性也是不可以修复的。
d.给出一个现实存在的类的实例,它拥有一个强参数,这个强参数用于提供深拷贝和浅拷贝操作。
2.6 在下面的int、double和2.7节中的Rational、Complex4个类型之间,哪些转型是敏感的(sensible):
Rational到int的转型;
int到Rational的转型;
complex到double的转型;
double到Complex的转型。
2.7 C++中的哪些内建的算法转型是敏感的?
2.8 编写一个和C++内建类型int相似的类Int将是相当困难的。
a.编写Int类的接口函数(就是说,给出Int类的声明),切记要提供适当的转型函数。
b.(*)尽管你尽了最大努力,但你设计的类Int的行为和内建int类型的行为有哪些差别?
c.假设你同时需要提供类Char、Short、Long、Float和Double,那么你的Int类需要哪些其他的转型操作呢?Char、Short、Long、Float和Double中的那些类需要具有到Int类的转型操作吗?
2.9 克林法则(Kleen’s theorem,见[DW83])认为一种语言当且仅当它可以表示为正则表达式的时候,才能被有限状态的接受者所接受。
a.(*)假设你的程序库不但提供了一个类FSA(finite state acceptor),用它模拟有限状态的接受者;还提供了另一个类Regex,用Regex来模拟正则表达式。这时,如果要在你的程序库模拟克林法则的话,应该提供什么样的类和函数呢?
b.如果你的解决方法用到了任何隐式转型,并且只有敏感的转型的话,请给出避免多重所有权(multiple ownership)和不必要的转型数目(fanout)的方法。
c.假设你的程序库提供了类Regex,但没有提供类FSA,而你的某些用户使用了一个提供类FSA的程序库;为了使你的用户能够容易地使用a部分所提供的相同功能,应该如何设计你的程序库呢?这个设计的缺点又是什么?
2.10 考虑2.8.3节的Noderef类。
a.当用const来修饰Noderef的时候,如果我们是这样来解释const:既不改变Noderef的值,也不改变底层节点的储存值,那么将会有什么问题发生?
b.我们是否可以这样来设计Noderef:它的接口禁止用户改变底层节点的存储值?如果可以,应该如何设计?
2.11 假设我们希望提供一个函数firstvowel,它返回一个指向给定字符串中的第一个元音字母的指针,这个字符串以null为结束字符;如果这个字符串没有元音字母,则返回0。考虑下列建议的接口:
char* firstvowel(char* s); // 1
const char* firstvowel(char* s); // 2
char* firstvowel(const char* s); // 3
const char* firstvowel(const char* s); // 4
a.对于(1)到(4)的每个接口,如果我们只给用户提供其中的一个接口,请分别说出会发生什么问题?
b.对于在a部分中发现的问题,请给出你的解决方法?
2.11 参考文献和相关资料
Cargill [Car92]、Cline和Lomow[CL95]、Barton和Nackman[BN94]都讨论了一些关于如何设计好的类的话题。
Liskow和Guttag[LG86]讨论了一些关于抽象状态的概念和其他的一些抽象法则的问题。
正规语义(regular semantics)的术语虽然是很新的,但它所描述的原则已经被好的程序员坚持了很多年了。nice类的第一次使用出现在Lee和Stepanov的[LS93]中,它和本文的nice类在意思上略有差异。
在继承面前正确地实现赋值运算符是相当棘手的,更多信息请参阅C++ Roport杂志中的Meyers文档[Mey94c,Mey94a,Mey94b]。
Meyers[Mey92c]也给出了对最小标准接口建议的批判。
这章里的Pool类是由Koenig在[UNI92]里设计的一个类改编而来的。
Doug Lea建议在2.4.5节里的例子使用垃圾收集机制。
浅拷贝和深拷贝操作导致的问题并不仅仅局限于C++语言,在所有的编程语言都会出现。Knight[Knig93]讨论了这种操作在Smalltalk语言中导致的问题。Gorlen,Orlow和Plexico在[GOP90]中给出了一种在C++程序库实现浅拷贝和深拷贝的技术。
Murray在[Mur88]里杜撰了多重所有权问题(multiple ownership problem)的术语。
要想设计一个可以精确模拟C++真实指针行为的C++智能指针是不可能的,Edelson在[Ede92]解释了这个原因。
本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。