引子
如果让你用C++写一个实用的字符串类,我想下面的方案是很多人最先想到的:
class
ClxString
{
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;
}
设计分析:
如果有下面的代码
ClxString str1
=
str2
=
str3
=
str4
=
str5
=
str6
=
"
StarLee
"
;
那么,字符串StarLee在内存中就有6个副本,而且每执行一次赋值(=)操作,都会有内存的释放和开辟。这样,内存的使用效率和程序的运行效率都不高。
解决方案:
使用引用计数(Reference Counting)。
引用计数(Reference Counting)
如果字符串的内容相同,就把ClxString类里的指针指向同一块存放字符串值的内存。为每块共享的内存设置一个引用计数。当有新的指针指向该内存块时,计数加一;当有一个字符串销毁时,计数减一;直到计数为零,就表示没有任何指针指向该内存块,再将其销毁掉。
下面就是用引用计数(Reference Counting)设计的解决方案:
class
ClxString
{
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添加上索引操作符([]),而出现了如下代码:
ClxString str1
=
str2
=
"
StarLee
"
;
str1[
6
]
=
'
a
'
;
那么str1和str2都变成了StarLea。这显然不是我们希望看到的!
解决方案:
写入时复制(Copy-On-Write)。
写入时复制(Copy-On-Write)
添加一个布尔变量来标志是否共享字符串。当发生写入操作时,重新开辟一块内存,并将共享的字符串值拷贝到新内存中。
下面就是用写入时复制(Copy-On-Write)设计的解决方案:
class
ClxString
{
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)!因为上面的代码根本无法区别下面两行使用了索引操作符([])的代码:
ClxString str
=
"
StarLee
"
;
str[
6
]
=
'
a
'
;
//
行1
char
c
=
str[
6
];
//
行2
对于代码行1来说是真正的写入操作,应该为字符串重新开辟一块内存还存放字符串值,并且不共享该块内存。
而对于代码行2来说,这其实是一个读操作,而上面设计的ClxString类却也把这行代码当成了写操作。
解决方案:
使用代理(Proxy)
代理(Proxy)
这里的代理(Proxy)其实是设计模式的一种。意图是为其他对象提供一种代理来控制对这个对象的访问,也就是说只有在我们确实需要这个对象时才对它进行创建和初始化。
对于我们这里的ClxString类来说,由于要区分索引操作符([])是读操作还是写操作,可以给字符添加一个代理。我们可以修改operator[],让它返回一个字符的代理,而不是字符本身。然后我们可以看这个代理如何被运用。这样就可以用代理来区分读取和写入操作。我们也可以实现一个真正的写入时拷贝(Copy-On-Write)函数。
下面就是用代理(Proxy)设计的解决方案:
class
ClxString
{
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类可以看出,对于类的设计并不是一蹴而就的,而是循序渐进、逐步完善的。