C++中的一个更为重要的思想是用户自定义类型可以很容易地当作内建类型使用。通过定义新类型,用户可以为了他们自己的目的来定制语言。这种强大的工具如果被错误的使用,便会十分危险。实际上,设计类库和设计编程语言是相似的,而且应该给予高度的重视。
字符串类
1 class String {
2 public:
3 String(char* p){
4 sz = strlen(p);
5 data = new char[sz + 1];
6 strcpy(data, p);
7 }
8 ~String(){delete[] data;}
9 operator char*() {return data;}
10 private:
11 int sz;
12 char* data;
13 };
差强人意的设计,没有考虑到异常情况的发生,例如内存耗尽。
内存耗尽
内存耗尽什么情况下发生?
用户请求了一个很大的String,而又没有足够的内存空间,就会发生内存耗尽的情况。
这种情况下会发生什么事情?
设计没有考虑,无法预测,但问题是发生在new表达式。
new表达式失败会发生什么?
可能会发生3件事情中的一件:库抛出异常,或者整个程序伴随着一个适当的诊断信息退出,或者new表达式返回0。
抛出异常和new表达式返回0,哪种方法好一些?
判断data是否等于0的做法,似乎可行,但是类内部使用data的时候都要做判断;抛出异常只需在new的时候处理一下,用户也可以通过try捕捉,程序写法上比较简单。
1 class String {
2 public:
3 String(char* p){
4 sz = strlen(p);
5 data = new char[sz + 1];
6 if(data == 0)
7 throw std::bad_alloc();
8 else
9 strcpy(data, p);
10 }
11 ~String(){delete[] data;}
12 operator char*()
13 {
14 return data;
15 }
16 private:
17 int sz;
18 char* data;
19 };
复制引发的内存问题
String类定义中没有复制构造函数和赋值操作符,这样,编译器代表程序员创建他们,并用对类成员的相应复制操作递归地定义它们,因此,复制一个String就相当于复制String的sz和data的成员的值。这就导致了,复制完后,原来的data成员和副本的data成员将指向相同的内存,所以,两个String被释放时,该内存会被释放两次。
最简单的解决办法是通过私有化复制构造函数和赋值操作符来规定不能复制String,赋值操作符不能是虚函数。
复制构造函数和赋值操作符之间的主要区别在于:赋值操作符复制新值进来前必须删除就值。复制的部分用assign函数来完成。
1 class String {
2 public:
3 String(char* p){
4 assign(p, strlen(p));
5 }
6 String(const String& s){
7 assign(s.data, s.sz);
8 }
9 ~String(){delete[] data;}
10 operator char*()
11 {
12 return data;
13 }
14 String& operator=(const String& s){
15 //不能先删除数据然后调用assign,因为把一个String赋给它自身肯定会失败
16 if(this != &s){
17 delete[] data;
18 assign(s.data, s.sz);
19 }
20 return *this;
21 }
22 private:
23 int sz;
24 char* data;
25 void assign(const char* s, unsigned len)
26 {
27 data = new char[len + 1];
28 if(data == 0)
29 throw std::bad_alloc();
30 sz = len;
31 strcpy(data, s);
32 }
33 };
针对用户,适当的隐藏实现是类设计者一个重要的职责,那隐藏实现的作用是什么呢?
隐藏实现
隐藏实现的作用:我们通常把数据隐藏视作是保护类设计者的一种措施,它给我们带来了一定的灵活性,方便以后根据需要修改实现,而且适当的隐藏实现也是帮助防止用户出错的重要方法。
operator char*()所暴露的问题:
- 通过该运算符取得的指针,用户可能会修改data中的内容;
- 释放String时,它所占用的内存也会被释放,这样任何指向String的指针都会失效;
- 通过该运算符释放和重新分配目标Stirng使用的内存来将一个String的赋值,可能会导致任何指向String内部的指针失效。
为了解决这三个问题,作者想以
operator const char*() const{
return data;
}
来解决第1个问题,而无法解决第3个问题,于是放弃这种做法。作者反省到内存管理的工作应该交给用户,更明智的做法是让用户提供将data复制进去的空间,用
1 void make_cstring(char* p, int len) const{
2 if(sz <= len)
3 strcpy(p, data);
4 else
5 throw("Not enough memory supplied");
6 }
缺省构造函数
对于
String s;
String s_arr[20];
的处理,添加缺省构造函数
1 String(): data(new char[1]){
2 sz = 0;
3 *data = '\0';
4 }