c++编程作风-读书笔记(2)
二、一致性
1、一致性示例,如下程序:
#include "stdafx.h" #include "iostream" class string { public: string() { str_ = new char[80]; len_ = 80; } string(int len) { str_ = new char[len]; len_ = len; } string(char *p) { len_ = strlen(p); str_ = new char[len_]; strcpy(str_, p); } string(string &str); ~string() { delete[] str_; } public: void Assign(char *str) { strcpy(str_, str); len_ = strlen(str); } void Print() { std::cout << str_ << std::endl; } void concat(string &a, string &b); private: char *str_; int len_; }; string::string(string &str) { len_ = str.len_; str_ = new char[len_]; strcpy(str_, str.str_); } void string::concat(string &a, string &b) { len_ = a.len_ + b.len_; str_ = new char[len_]; strcpy(str_, a.str_); strcat(str_, b.str_); } int _tmain(int argc, _TCHAR* argv[]) { char *str = "The wheel that squeaks the loudest\n"; string a(str); string b; string author("Josh Billings\n"); string both; string quote; b.Assign("Is the one that gets the grease\n"); both.concat(a, b); quote.concat(both, author); quote.Print(); return 0; }
不足的地方
a、应该明确定义的状态
比如使用了string x, y(128); x.print();
那现在x的值是不确定的,输出的时候必要要等到’\0’才会停止(所有有构造函数应该使得对象处于明确的定义状态)
b、物理状态的一致性
看两段代码
string(char *p) { len_ = strlen(p); str_ = new char[len_ + 1]; strcpy(str_, p); }
void string::concat(string &a, string &b) { len_ = a.len_ + b.len_; str_ = new char[len_]; strcpy(str_, a.str_); strcat(str_, b.str_); }
在这两个函数中len_所表示的含义有两个,一个是字符串的长度,另外一个是数组的长度;显然,len_的这两种含义都是有意义的,但在所有的构造函数及其他的成员函数中,必须只能有一种(所有的成员必须只有一个明确的定义,用一致的方式来定义对象的状态,这需要识别出类不变性)
c、动态内存的一致性
看这里的三段代码:
string() { str_ = new char[80]; len_ = 80; }
string(int len) { str_ = new char[len]; len_ = len; }
void string::concat(string &a, string &b) { len_ = a.len_ + b.len_; str_ = new char[len_]; strcpy(str_, a.str_); strcat(str_, b.str_); }
可以看到,上面动态分配的内存都是不一致的;要做到一致,有两种选择,一是只能是确保一开始分配的空间足够大,二是对每个字符串值都动态地决定数组的大小以保证安全。
这两种方法都可以用在类中,但只能使用其中的一种,以保持类的一致性,而不应该将这两种方法混合使用。否则,在使用这个类时,不得不去了解在接口中不同操作之间的不用约定(类的接口定义应该是一致的--------避免产生困惑)
d、动态内存的回收
来看字符串连接的一个问题
void string::concat(string &a, string &b) { len_ = a.len_ + b.len_; str_ = new char[len_]; strcpy(str_, a.str_); strcat(str_, b.str_); }
原来的空间显然被泄露掉了(对于每个new操作,都要有相应的delete操作)
看看重新设计的一个string类
#include "stdafx.h" #include "iostream" class string { public: string(); string(const char *pStr); string(string& str); ~string(); const char *content() const; string& operator=(const char *pStr); string& operator=(const string &str); private: char *string_; int length_; }; char *Strdup(const char *pStr) { char *pTemp = new char[strlen(pStr) + 1]; strcpy(pTemp, pStr); return pTemp; } string::string() { string_ = 0; length_ = 0; } string::string(const char *pStr) { string_ = pStr ? Strdup(pStr) : 0; length_ = pStr ? strlen(string_) : 0; } string::string(string& str) { if (str.string_) { string_ = Strdup(str.string_); length_ = strlen(string_); } else { string_ = 0; length_ = 0; } } string::~string() { if (string_) { delete[] string_; } } const char *string::content() const { return string_ ? string_ : 0; } string& string::operator=(const char *pStr) { delete[] string_; string_ = pStr ? Strdup(pStr) : 0; length_ = pStr ? strlen(string_) : 0; return *this; } string& string::operator=(const string &str) { delete[] string_; string_ = str.string_ ? Strdup(str.string_) : 0; length_ = string_ ? strlen(string_) : 0; return *this; } int _tmain(int argc, _TCHAR* argv[]) { string author("zengraoli"); return 0; }
该类使用了动态分配的形式,这样更能有效的避免数组的溢出
但是这里还有一些不足的地方:
a、冗余
length_的引入,一开始是为了表示字符串的长度,但是在这里这个信息却从来没有使用过。在string中,当每次需要字符串长度时,这个值都将在辅助函数Strdup中重新计算,所以这个状态信息是多余的(避免对从不使用的状态信息进行计算和存储)
b、在operator=中存在的一些问题
可以看到在operator=(const string &str)上面,先是delete,虽然不太容易写出像x=x这样的表达式,但是在程序中可能会简接地导致这种复赋值运算的发生(如果a和b碰巧都是引用了同一个string对象,纳闷呢a=b就等价于x=x)。虽然先delete就会导致操作未定义的内存。
所以最好的方式是首先处理自赋值
string& string::operator=(const string &str) { if (&str == this) { return *this; } delete[] string_; string_ = str.string_ ? Strdup(str.string_) : 0; length_ = string_ ? strlen(string_) : 0; return *this; }
对于operator=(const char *pStr)中的delete操作,可能会导致问题的情况是x=x.string(),或者虽然以a=b.string()的形式出现,但a和b的字符指针指向的是同一个地址。在执行x=s的赋值运算时,如常量字符类型指针s的值的等于x.string(),那么将会产生和上面同样的问题。解决的办法是将delete操作符推迟到字符串被复制之后在进行
string& string::operator=(const char *pStr) { char *prevString = string_; string_ = pStr ? Strdup(pStr) : 0; length_ = pStr ? strlen(string_) : 0; delete[] prevString; return *this; }
c、最后一个细节的问题
关于Strdup函数,这个函数的作用是对字符串进行复制并存储在一块新分配的内存中。你会发现上面的代码,在调用该函数的时候都遵循着同样的形式:在每次调用之间,都要进行测试以保证参数指针是非空的;在从函数中返回之后,返回结果都是保存在string_中。因此可以看到简化的地方。
最终得到的修改版本:
#include "stdafx.h" #include "iostream" class string { public: string(); string(const char *pStr); string(string& str); ~string(); const char *content() const; string& operator=(const char *pStr); string& operator=(const string &str); private: void duplicate(const char *pStr); private: char *string_; }; void string::duplicate(const char *pStr) { if (pStr) { string_ = new char[strlen(pStr) + 1]; strcpy(string_, pStr); } else { string_ = 0; } } string::string() { string_ = 0; } string::string(const char *pStr) { duplicate(pStr); } string::string(string& str) { duplicate(str.string_); } string::~string() { if (string_) { delete[] string_; } } const char *string::content() const { return string_ ? string_ : 0; } string& string::operator=(const char *pStr) { char *prevString = string_; duplicate(pStr); delete[] prevString; return *this; } string& string::operator=(const string &str) { if (&str == this) { return *this; } delete[] string_; duplicate(str.string_); return *this; } int _tmain(int argc, _TCHAR* argv[]) { string author("zengraoli"); return 0; }