引子
如果让你用C++写一个实用的字符串类,我想下面的方案是很多人最先想到的:
{
public :
ClxString();
ClxString( const char * pszStr);
ClxString( const ClxString & str);
ClxString & operator = ( const ClxString & str);
~ ClxString();
private :
char * m_pszStr;
};
ClxString::ClxString( const char * pszStr)
{
if (pszStr)
{
m_pszStr = new char [strlen(pszStr) + 1 ];
strcpy(m_pszStr, pszStr);
}
else
{
m_pszStr = new char [ 1 ];
* m_pszStr = '/0' ;
}
}
ClxString::ClxString( const ClxString & str)
{
m_pszStr = new char [strlen(str.m_pszStr) + 1 ];
strcpy(m_pszStr, str.m_pszStr);
}
ClxString & ClxString:: operator = ( const ClxString & str)
{
if ( this == & str)
return * this ;
delete []m_pszStr;
m_pszStr = new char [strlen(str.m_pszStr) + 1 ];
strcpy(m_pszStr, str.m_pszStr);
return * this ;
}
ClxString:: ~ ClxString()
{
delete []m_pszStr;
}
设计分析:
如果有下面的代码
那么,字符串StarLee在内存中就有6个副本,而且每执行一次赋值(=)操作,都会有内存的释放和开辟。这样,内存的使用效率和程序的运行效率都不高。
解决方案:
使用引用计数(Reference Counting)。
引用计数(Reference Counting)
如果字符串的内容相同,就把ClxString类里的指针指向同一块存放字符串值的内存。为每块共享的内存设置一个引用计数。当有新的指针指向该内存块时,计数加一;当有一个字符串销毁时,计数减一;直到计数为零,就表示没有任何指针指向该内存块,再将其销毁掉。
下面就是用引用计数(Reference Counting)设计的解决方案:
{
public :
ClxString();
ClxString( const char * pszStr);
ClxString( const ClxString & str);
ClxString & operator = ( const ClxString & str);
~ ClxString();
private :
// 这里用一个结构来存放指向共享内存块的指针和该内存块的引用计数
struct StringValue
{
int iRefCount;
char * pszStrData;
StringValue( const char * pszStr);
~ StringValue();
};
StringValue * m_pData;
};
// struct StringValue
ClxString::StringValue::StringValue( const char * pszStr)
{
iRefCount = 1 ;
pszStrData = new char [strlen(pszStr) + 1 ];
strcpy(pszStrData, pszStr);
}
ClxString::StringValue:: ~ StringValue()
{
delete []pszStrData;
}
// struct StringValue
// class ClxString
ClxString::ClxString( const char * pszStr)
{
m_pData = new StringValue(pszStr);
}
ClxString::ClxString( const ClxString & str)
{
// 这里不必开辟新的内存空间
// 只要让指针指向同一块内存
// 并把该内存块的引用计数加一
m_pData = str.m_pData;
m_pData -> iRefCount ++ ;
}
ClxString & ClxString:: operator = ( const ClxString & str)
{
if (m_pData == str.m_pData)
return * this ;
// 将引用计数减一
m_pData -> iRefCount -- ;
// 引用计数为0,也就是没有任何指针指向该内存块
// 销毁内
if (m_pData -> iRefCount == 0 )
delete m_pData;
// 这里不必开辟新的内存空间
// 只要让指针指向同一块内存
// 并把该内存块的引用计数加一
m_pData = str.m_pData;
m_pData -> iRefCount ++ ;
return * this ;
}
ClxString:: ~ ClxString()
{
// 析构时,将引用计数减一
m_pData -> iRefCount -- ;
// 引用计数为0,也就是没有任何指针指向该内存块
// 销毁内存
if (m_pData -> iRefCount == 0 )
delete m_pData;
}
// class ClxString
设计分析:
这个版本的String类用上了引用计数(Reference Counting),内存的使用效率和程序的运行效率都有所提高,那么这就是我们需要的最终版本吗?答案当然是--不!
如果共享字符串被修改,比如给ClxString添加上索引操作符([]),而出现了如下代码:
str1[ 6 ] = ' a ' ;
那么str1和str2都变成了StarLea。这显然不是我们希望看到的!
解决方案:
写入时复制(Copy-On-Write)。
写入时复制(Copy-On-Write)
添加一个布尔变量来标志是否共享字符串。当发生写入操作时,重新开辟一块内存,并将共享的字符串值拷贝到新内存中。
下面就是用写入时复制(Copy-On-Write)设计的解决方案:
{
public :
ClxString();
ClxString( const char * pszStr);
ClxString( const ClxString & str);
ClxString & operator = ( const ClxString & str);
~ ClxString();
// 索引操作符重载
const char & operator []( int iIndex) const ;
char & operator []( int iIndex);
private :
struct StringValue
{
int iRefCount;
char * pszStrData;
// 表示是否共享字符串值的变量
bool bShareable;
StringValue( const char * pszStr);
~ StringValue();
};
StringValue * m_pData;
// 判断是否共享字符串值
// 如果不共享,就开辟新的内存块
ShareStrOrNot( const ClxString & str);
};
// struct StringValue
ClxString::StringValue::StringValue( const char * pszStr)
{
iRefCount = 1 ;
// 默认共享
bShareable = true ;
pszStrData = new char [strlen(pszStr) + 1 ];
strcpy(pszStrData, pszStr);
}
ClxString::StringValue:: ~ StringValue()
{
delete []pszStrData;
}
// struct StringValue
// class ClxString
ClxString::ClxString( const char * pszStr)
{
m_pData = new StringValue(pszStr);
}
ClxString::ClxString( const ClxString & str)
{
ShareStrOrNot(str);
}
ClxString & ClxString:: operator = ( const ClxString & str)
{
if (m_pData == str.m_pData)
return * this ;
m_pData -> iRefCount -- ;
if (m_pData -> iRefCount == 0 )
delete m_pData;
ShareStrOrNot(str);
return * this ;
}
ClxString:: ~ ClxString()
{
m_pData -> iRefCount -- ;
if (m_pData -> iRefCount == 0 )
delete m_pData;
}
const char & ClxString:: operator []( int iIndex) const
{
return m_pData -> pszStrData[iIndex];
}
char & ClxString:: operator []( int iIndex)
{
// 有写入操作,开辟新的内存
if (m_pData -> iRefCount > 1 )
{
m_pData -> iRefCount -- ;
m_pData = new StringValue(m_pData -> pszStrData);
}
// 并设置为不共享字符串值
m_pData -> bShareable = false ;
return m_pData -> pszStrData[iIndex];
}
ClxString::ShareStrOrNot( const ClxString & str)
{
if (str.m_pData -> bShareable)
{
m_pData = str.m_pData;
m_pData -> iRefCount ++ ;
}
// 不共享字符串值,开辟新的内存块
else
{
m_pData = new StringValue(str.m_pData -> pszStrData);
}
}
// class ClxString
设计分析:
这个版本的ClxString类既使用了引用计数(Reference Counting),也实现了写入时复制(Copy-On-Write),应该时一个很完善的版本了吧?答案依然是--不!
这个版本所实现的根本不是真正的写入时复制(Copy-On-Write)!因为上面的代码根本无法区别下面两行使用了索引操作符([])的代码:
str[ 6 ] = ' a ' ; // 行1
char c = str[ 6 ]; // 行2
对于代码行1来说是真正的写入操作,应该为字符串重新开辟一块内存还存放字符串值,并且不共享该块内存。
而对于代码行2来说,这其实是一个读操作,而上面设计的ClxString类却也把这行代码当成了写操作。
解决方案:
使用代理(Proxy)
代理(Proxy)
这里的代理(Proxy)其实是设计模式的一种。意图是为其他对象提供一种代理来控制对这个对象的访问,也就是说只有在我们确实需要这个对象时才对它进行创建和初始化。
对于我们这里的ClxString类来说,由于要区分索引操作符([])是读操作还是写操作,可以给字符添加一个代理。我们可以修改operator[],让它返回一个字符的代理,而不是字符本身。然后我们可以看这个代理如何被运用。这样就可以用代理来区分读取和写入操作。我们也可以实现一个真正的写入时拷贝(Copy-On-Write)函数。
下面就是用代理(Proxy)设计的解决方案:
{
public :
// 字符代理类
class CharProxy
{
public :
CharProxy(ClxString & str, int iIndex);
CharProxy & operator = ( const CharProxy & rhs); // 写操作
CharProxy & operator = ( char c); // 写操作
char * operator & (); // 写操作
operator char () const ; // 读操作
private :
ClxString & m_lxStr; // 代理的字符所在的字符串
int m_iIndex; // 代理的字符在字符串中的索引
// 真正的Copy-On-Write函数
void CopyOnWrite();
};
ClxString();
ClxString( const char * pszStr);
ClxString( const ClxString & str);
~ ClxString();
ClxString & operator = ( const ClxString & str);
// 重载[]操作符,返回字符代理
// 将对ClxString的[]操作转接给CharProxy处理
// 由CharProxy判断是读取还是写入操作
const CharProxy operator []( int iIndex) const ;
CharProxy operator []( int iIndex);
private :
struct StringValue
{
int iRefCount;
char * pszStrData;
bool bShareable;
StringValue( const char * pszStr);
~ StringValue();
};
StringValue * m_pData;
ShareStrOrNot( const ClxString & str);
};
// class CharProxy
ClxString::CharProxy::CharProxy(ClxString & str, int iIndex)
: m_lxStr(str), m_iIndex(iIndex)
{
}
// 重载=操作符
// 写操作
// 用来应对下面的代码
// ClxString str1 = "Star"; ClxString str2 = "Lee"; str1[1] = str2[2];
ClxString::CharProxy & ClxString::CharProxy:: operator = ( const CharProxy & rhs)
{
CopyOnWrite();
m_lxStr.m_pData -> pszStrData[m_iIndex] = rhs.m_lxStr.m_pData -> pszStrData[rhs.m_iIndex];
return * this ;
}
// 重载=操作符
// 写操作
// 用来应对下面的代码
// ClxString str = "StarLee"; str[6] = 'a';
ClxString::CharProxy & ClxString::CharProxy:: operator = ( char c)
{
CopyOnWrite();
m_lxStr.m_pData -> pszStrData[m_iIndex] = c;
return * this ;
}
// 重载&操作符
// 很有可能发生写操作
// 用来应对下面的代码
// ClxString str = "StarLee"; char *p = &str[6]; *p = 'a';
char * ClxString::CharProxy:: operator & ()
{
CopyOnWrite();
// 这里必须设置为不共享
// 因为有指针指向字符串内部的字符,有随时改写字符的权力
m_lxStr.m_pData -> bShareable = false ;
return & (m_lxStr.m_pData -> pszStrData[m_iIndex]);
}
// 重载了char操作符
// 读操作
// 用来应对下面的代码
// ClxString str = "StarLee"; cout << str[6];
ClxString::CharProxy:: operator char () const
{
return m_lxStr.m_pData -> pszStrData[m_iIndex];
}
void ClxString::CharProxy::CopyOnWrite()
{
if (m_lxStr.m_pData -> iRefCount > 1 )
{
m_lxStr.m_pData -> iRefCount -- ;
m_lxStr.m_pData = new StringValue(m_lxStr.m_pData -> pszStrData);
}
}
// class CharProxy
// struct StringValue
ClxString::StringValue::StringValue( const char * pszStr)
{
iRefCount = 1 ;
bShareable = true ;
pszStrData = new char [strlen(pszStr) + 1 ];
strcpy(pszStrData, pszStr);
}
ClxString::StringValue:: ~ StringValue()
{
delete []pszStrData;
}
// struct StringValue
// class ClxString
ClxString::ClxString( const char * pszStr)
{
m_pData = new StringValue(pszStr);
}
ClxString::ClxString( const ClxString & str)
{
ShareStrOrNot(str);
}
ClxString & ClxString:: operator = ( const ClxString & str)
{
if (m_pData == str.m_pData)
return * this ;
m_pData -> iRefCount -- ;
if (m_pData -> iRefCount == 0 )
delete m_pData;
ShareStrOrNot(str);
return * this ;
}
const ClxString::CharProxy ClxString:: operator []( int iIndex) const
{
return CharProxy(const_cast < ClxString &> ( * this ), iIndex);
}
ClxString::CharProxy ClxString:: operator []( int iIndex)
{
return CharProxy( * this , iIndex);
}
ClxString:: ~ ClxString()
{
m_pData -> iRefCount -- ;
if (m_pData -> iRefCount == 0 )
delete m_pData;
}
ClxString::ShareStrOrNot( const ClxString & str)
{
if (str.m_pData -> bShareable)
{
m_pData = str.m_pData;
m_pData -> iRefCount ++ ;
}
else
{
m_pData = new StringValue(str.m_pData -> pszStrData);
}
}
// class ClxString
C++ Tips:类的引用成员变量和常量成员变量必须在构造函数的初始化列表里赋值。
总结
从上面几个版本的ClxString类可以看出,对于类的设计并不是一蹴而就的,而是循序渐进、逐步完善的。