二、一致性
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;
}