一
C++中的一个更为重要的思想是用户自定义类型可以很容易地当作内建类型使用。通过定义新类型,用户可以为了他们自己的目的来定制语言。这种强大的工具如果被错误的使用,便会十分危险。实际上,设计类库和设计编程语言是相似的,而且应该给予高度的重视。
注:原地址在http://archive.cnblogs.com/a/1692743/ 本文对原内容的错误进行了修改,并增加了新的内容
从一个String类的设计来看“库设计就是语言设计”
字符串类
class String {
public:
String(char* p){
sz = strlen(p);
data = new char[sz + 1];
strcpy(data, p);
}
~String(){delete[] data;}
operator char*() {return data;}
private:
int sz;
char* data;
};
差强人意的设计,没有考虑到异常情况的发生,例如内存耗尽。
内存耗尽
内存耗尽什么情况下发生?
用户请求了一个很大的String,而又没有足够的内存空间,就会发生内存耗尽的情况。
这种情况下会发生什么事情?
上面的设计没有考虑,无法预测,但问题是发生在new表达式。
new表达式失败会发生什么?
可能会发生3件事情中的一件:库抛出异常,或者整个程序伴随着一个适当的诊断信息退出,或者new表达式返回0。
抛出异常和new表达式返回0,哪种方法好一些?
判断data是否等于0的做法,似乎可行,但是类内部使用data的时候都要做判断;抛出异常只需在new的时候处理一下,用户也可以通过try捕捉,程序写法上比较简单。
class String {
public:
String(char* p){
sz = strlen(p);
data = new char[sz + 1];
if(data == 0)
throw std::bad_alloc();
else
strcpy(data, p);
}
~String(){delete[] data;}
operator char*()
{ return data;
}
private:
int sz;
char* data;
};
对于这个设计,客户程序通过写类似下面的语句来检测错误
try
{
String s(p);
//与s有关的操作...
}
catch(std::bad_alloc)
{
//处理内存耗尽
}
复制引发的内存问题
String类定义中没有复制构造函数和赋值操作符,这样,编译器代表程序员创建他们,并用对类成员的相应复制操作递归地定义它们,因此,复制一个String就相当于复制String的sz和data的成员的值。这就导致了,复制完后,原来的data成员和副本的data成员将指向相同的内存,所以,两个String被释放时,该内存会被释放两次。
最简单的解决办法是通过私有化复制构造函数和赋值操作符来规定不能复制String。
复制构造函数和赋值操作符之间的主要区别在于:赋值操作符复制新值进来前必须删除就值,其余部分相同。复制的部分用assign函数来完成。
class String {
public:
String(char* p){
assign(p, strlen(p));
}
String(const String& s){
assign(s.data, s.sz);
}
~String(){delete[] data;}
operator char*() {
return data;
}
String& operator=(const String& s){ //不能先删除数据然后调用assign,因为把一个String赋给它自身肯定会失败
if(this != &s){
delete[] data;
assign(s.data, s.sz);
}
return *this;
}
private:
int sz;
char* data;
void assign(const char* s, unsigned len)
{
data = new char[len + 1];
if(data == 0)
throw std::bad_alloc();
sz = len;
strcpy(data, s);
}
};
针对用户,适当的隐藏实现是类设计者一个重要的职责,那隐藏实现的作用是什么呢?
隐藏实现
隐藏实现的作用:我们通常把数据隐藏视作是保护类设计者的一种措施,它给我们带来了一定的灵活性,方便以后根据需要修改实现,而且适当的隐藏实现也是帮助防止用户出错的重要方法。
operator char*()所暴露的问题:
- 通过该运算符取得的指针,用户可能会修改data中的内容;
- 释放String时,它所占用的内存也会被释放,这样任何指向String的指针都会失效;
- 通过该运算符释放和重新分配目标Stirng使用的内存来将一个String的赋值,可能会导致任何指向String内部的指针失效。
为了解决这三个问题,作者想以
operator const char*() const
{
return data;
}
来解决第1个问题,而无法解决第3个问题,于是放弃这种做法。作者反省到内存管理的工作应该交给用户,所以更明智的做法是让用户提供将data复制进去的空间,用
void make_cstring(char* p, int len) const{
if(sz <= len)
strcpy(p, data);
else
throw("Not enough memory supplied");
}
来实现。其实也可以make_cstring中分配内存并复制data到这个空间中,但由于用户往往会忘记非自己显式获得的资源,所以作者抛弃了这种设计
缺省构造函数
对于
String s;
String s_arr[20];
这些要求缺省构造函数的处理,添加缺省构造函数
String(): data(new char[1])
{
sz = 0;
*data = '\0';
}
赋值操作符
String& String::operator+=(const String& s)
{
char* odata = data;
assign(odata, sz+s.sz+1);
strcat(data, s.data);
delete [] odata;
return *this;
}
注意必须防止一个String与它自己连接
连接操作符
String operator+(const String& op1, const String& op2)
{
String ret(op1);
ret += op2;
return ret;
}
这个连接操作符接受如下操作
String s("hello ");
char *p = "world";
String q = s + p;
String w = s + q;
String t = p + p;
String x = q + q;
q = p + s
q = q + s;
二
关于库设计的一些思想:
设计好一个好程序库的要求之一就是彻底隔离接口的实现
构造函数这种形式将用户看待对象的方式与对象的实际表示方式解耦了,也可以说构造函数隐藏了对象创建对象的细节,同样析构函数也隐藏了销毁对象的细节