引子
如果让你用C++写一个实用的字符串类,我想下面的方案是很多人最先想到的:
classClxString
{
public:
ClxString();
ClxString(constchar*pszStr);
ClxString(constClxString&str);
ClxString&operator=(constClxString&str);
~ClxString();
private:
char*m_pszStr;
};
ClxString::ClxString(constchar*pszStr)
{
if(pszStr)
{
m_pszStr=newchar[strlen(pszStr)+1];
strcpy(m_pszStr,pszStr);
}
else
{
m_pszStr=newchar[1];
*m_pszStr='/0';
}
}
ClxString::ClxString(constClxString&str)
{
m_pszStr=newchar[strlen(str.m_pszStr)+1];
strcpy(m_pszStr,str.m_pszStr);
}
ClxString&ClxString::operator=(constClxString&str)
{
if(this==&str)
return*this;
delete[]m_pszStr;
m_pszStr=newchar[strlen(str.m_pszStr)+1];
strcpy(m_pszStr,str.m_pszStr);
return*this;
}
ClxString::~ClxString()
{
delete[]m_pszStr;
}
设计分析:
如果有下面的代码
ClxStringstr1=str2=str3=str4=str5=str6="StarLee";
那么,字符串StarLee在内存中就有6个副本,而且每执行一次赋值(=)操作,都会有内存的释放和开辟。这样,内存的使用效率和程序的运行效率都不高。
解决方案:
使用引用计数(Reference Counting)。
引用计数(Reference Counting)
如果字符串的内容相同,就把ClxString类里的指针指向同一块存放字符串值的内存。为每块共享的内存设置一个引用计数。当有新的指针指向该内存块时,计数加一;当有一个字符串销毁时,计数减一;直到计数为零,就表示没有任何指针指向该内存块,再将其销毁掉。
下面就是用引用计数(Reference Counting)设计的解决方案:
classClxString
{
public:
ClxString();
ClxString(constchar*pszStr);
ClxString(constClxString&str);
ClxString&operator=(constClxString&str);
~ClxString();
private:
//这里用一个结构来存放指向共享内存块的指针和该内存块的引用计数
structStringValue
{
intiRefCount;
char*pszStrData;
StringValue(constchar*pszStr);
~StringValue();
};
StringValue*m_pData;
};
//structStringValue
ClxString::StringValue::StringValue(constchar*pszStr)
{
iRefCount=1;
pszStrData=newchar[strlen(pszStr)+1];
strcpy(pszStrData,pszStr);
}
ClxString::StringValue::~StringValue()
{
delete[]pszStrData;
}
//structStringValue
//classClxString
ClxString::ClxString(constchar*pszStr)
{
m_pData=newStringValue(pszStr);
}
ClxString::ClxString(constClxString&str)
{
//这里不必开辟新的内存空间
//只要让指针指向同一块内存
//并把该内存块的引用计数加一
m_pData=str.m_pData;
m_pData->iRefCount++;
}
ClxString&ClxString::operator=(constClxString&str)
{
if(m_pData==str.m_pData)
return*this;
//将引用计数减一
m_pData->iRefCount--;
//引用计数为0,也就是没有任何指针指向该内存块
//销毁内
if(m_pData->iRefCount==0)
deletem_pData;
//这里不必开辟新的内存空间
//只要让指针指向同一块内存
//并把该内存块的引用计数加一
m_pData=str.m_pData;
m_pData->iRefCount++;
return*this;
}
ClxString::~ClxString()
{
//析构时,将引用计数减一
m_pData->iRefCount--;
//引用计数为0,也就是没有任何指针指向该内存块
//销毁内存
if(m_pData->iRefCount==0)
deletem_pData;
}
//classClxString
设计分析:
这个版本的String类用上了引用计数(Reference Counting),内存的使用效率和程序的运行效率都有所提高,那么这就是我们需要的最终版本吗?答案当然是--不!
如果共享字符串被修改,比如给ClxString添加上索引操作符([]),而出现了如下代码:
ClxStringstr1=str2="StarLee";
str1[6]='a';
那么str1和str2都变成了StarLea。这显然不是我们希望看到的!
解决方案:
写入时复制(Copy-On-Write)。
写入时复制(Copy-On-Write)
添加一个布尔变量来标志是否共享字符串。当发生写入操作时,重新开辟一块内存,并将共享的字符串值拷贝到新内存中。
下面就是用写入时复制(Copy-On-Write)设计的解决方案:
classClxString
{
public:
ClxString();
ClxString(constchar*pszStr);
ClxString(constClxString&str);
ClxString&operator=(constClxString&str);
~ClxString();
//索引操作符重载
constchar&operator[](intiIndex)const;
char&operator[](intiIndex);
private:
structStringValue
{
intiRefCount;
char*pszStrData;
//表示是否共享字符串值的变量
boolbShareable;
StringValue(constchar*pszStr);
~StringValue();
};
StringValue*m_pData;
//判断是否共享字符串值
//如果不共享,就开辟新的内存块
ShareStrOrNot(constClxString&str);
};
//structStringValue
ClxString::StringValue::StringValue(constchar*pszStr)
{
iRefCount=1;
//默认共享
bShareable=true;
pszStrData=newchar[strlen(pszStr)+1];
strcpy(pszStrData,pszStr);
}
ClxString::StringValue::~StringValue()
{
delete[]pszStrData;
}
//structStringValue
//classClxString
ClxString::ClxString(constchar*pszStr)
{
m_pData=newStringValue(pszStr);
}
ClxString::ClxString(constClxString&str)
{
ShareStrOrNot(str);
}
ClxString&ClxString::operator=(constClxString&str)
{
if(m_pData==str.m_pData)
return*this;
m_pData->iRefCount--;
if(m_pData->iRefCount==0)
deletem_pData;
ShareStrOrNot(str);
return*this;
}
ClxString::~ClxString()
{
m_pData->iRefCount--;
if(m_pData->iRefCount==0)
deletem_pData;
}
constchar&ClxString::operator[](intiIndex)const
{
returnm_pData->pszStrData[iIndex];
}
char&ClxString::operator[](intiIndex)
{
//有写入操作,开辟新的内存
if(m_pData->iRefCount>1)
{
m_pData->iRefCount--;
m_pData=newStringValue(m_pData->pszStrData);
}
//并设置为不共享字符串值
m_pData->bShareable=false;
returnm_pData->pszStrData[iIndex];
}
ClxString::ShareStrOrNot(constClxString&str)
{
if(str.m_pData->bShareable)
{
m_pData=str.m_pData;
m_pData->iRefCount++;
}
//不共享字符串值,开辟新的内存块
else
{
m_pData=newStringValue(str.m_pData->pszStrData);
}
}
//classClxString
设计分析:
这个版本的ClxString类既使用了引用计数(Reference Counting),也实现了写入时复制(Copy-On-Write),应该时一个很完善的版本了吧?答案依然是--不!
这个版本所实现的根本不是真正的写入时复制(Copy-On-Write)!因为上面的代码根本无法区别下面两行使用了索引操作符([])的代码:
ClxStringstr="StarLee";
str[6]='a';//行1
charc=str[6];//行2
对于代码行1来说是真正的写入操作,应该为字符串重新开辟一块内存还存放字符串值,并且不共享该块内存。
而对于代码行2来说,这其实是一个读操作,而上面设计的ClxString类却也把这行代码当成了写操作。
解决方案:
使用代理(Proxy)
代理(Proxy)
这里的代理(Proxy)其实是设计模式的一种。意图是为其他对象提供一种代理来控制对这个对象的访问,也就是说只有在我们确实需要这个对象时才对它进行创建和初始化。
对于我们这里的ClxString类来说,由于要区分索引操作符([])是读操作还是写操作,可以给字符添加一个代理。我们可以修改operator[],让它返回一个字符的代理,而不是字符本身。然后我们可以看这个代理如何被运用。这样就可以用代理来区分读取和写入操作。我们也可以实现一个真正的写入时拷贝(Copy-On-Write)函数。
下面就是用代理(Proxy)设计的解决方案:
classClxString
{
public:
//字符代理类
classCharProxy
{
public:
CharProxy(ClxString&str,intiIndex);
CharProxy&operator=(constCharProxy&rhs);//写操作
CharProxy&operator=(charc);//写操作
char*operator&();//写操作
operatorchar()const;//读操作
private:
ClxString&m_lxStr;//代理的字符所在的字符串
intm_iIndex;//代理的字符在字符串中的索引
//真正的Copy-On-Write函数
voidCopyOnWrite();
};
ClxString();
ClxString(constchar*pszStr);
ClxString(constClxString&str);
~ClxString();
ClxString&operator=(constClxString&str);
//重载[]操作符,返回字符代理
//将对ClxString的[]操作转接给CharProxy处理
//由CharProxy判断是读取还是写入操作
constCharProxyoperator[](intiIndex)const;
CharProxyoperator[](intiIndex);
private:
structStringValue
{
intiRefCount;
char*pszStrData;
boolbShareable;
StringValue(constchar*pszStr);
~StringValue();
};
StringValue*m_pData;
ShareStrOrNot(constClxString&str);
};
//classCharProxy
ClxString::CharProxy::CharProxy(ClxString&str,intiIndex)
:m_lxStr(str),m_iIndex(iIndex)
{
}
//重载=操作符
//写操作
//用来应对下面的代码
//ClxStringstr1="Star";ClxStringstr2="Lee";str1[1]=str2[2];
ClxString::CharProxy&ClxString::CharProxy::operator=(constCharProxy&rhs)
{
CopyOnWrite();
m_lxStr.m_pData->pszStrData[m_iIndex]=rhs.m_lxStr.m_pData->pszStrData[rhs.m_iIndex];
return*this;
}
//重载=操作符
//写操作
//用来应对下面的代码
//ClxStringstr="StarLee";str[6]='a';
ClxString::CharProxy&ClxString::CharProxy::operator=(charc)
{
CopyOnWrite();
m_lxStr.m_pData->pszStrData[m_iIndex]=c;
return*this;
}
//重载&操作符
//很有可能发生写操作
//用来应对下面的代码
//ClxStringstr="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操作符
//读操作
//用来应对下面的代码
//ClxStringstr="StarLee";cout<<str[6];
ClxString::CharProxy::operatorchar()const
{
returnm_lxStr.m_pData->pszStrData[m_iIndex];
}
voidClxString::CharProxy::CopyOnWrite()
{
if(m_lxStr.m_pData->iRefCount>1)
{
m_lxStr.m_pData->iRefCount--;
m_lxStr.m_pData=newStringValue(m_lxStr.m_pData->pszStrData);
}
}
//classCharProxy
//structStringValue
ClxString::StringValue::StringValue(constchar*pszStr)
{
iRefCount=1;
bShareable=true;
pszStrData=newchar[strlen(pszStr)+1];
strcpy(pszStrData,pszStr);
}
ClxString::StringValue::~StringValue()
{
delete[]pszStrData;
}
//structStringValue
//classClxString
ClxString::ClxString(constchar*pszStr)
{
m_pData=newStringValue(pszStr);
}
ClxString::ClxString(constClxString&str)
{
ShareStrOrNot(str);
}
ClxString&ClxString::operator=(constClxString&str)
{
if(m_pData==str.m_pData)
return*this;
m_pData->iRefCount--;
if(m_pData->iRefCount==0)
deletem_pData;
ShareStrOrNot(str);
return*this;
}
constClxString::CharProxyClxString::operator[](intiIndex)const
{
returnCharProxy(const_cast<ClxString&>(*this),iIndex);
}
ClxString::CharProxyClxString::operator[](intiIndex)
{
returnCharProxy(*this,iIndex);
}
ClxString::~ClxString()
{
m_pData->iRefCount--;
if(m_pData->iRefCount==0)
deletem_pData;
}
ClxString::ShareStrOrNot(constClxString&str)
{
if(str.m_pData->bShareable)
{
m_pData=str.m_pData;
m_pData->iRefCount++;
}
else
{
m_pData=newStringValue(str.m_pData->pszStrData);
}
}
//classClxString
C++ Tips:类的引用成员变量和常量成员变量必须在构造函数的初始化列表里赋值。
总结
从上面几个版本的ClxString类可以看出,对于类的设计并不是一蹴而就的,而是循序渐进、逐步完善的。
分享到:
相关推荐
详细介绍了IOS中ARC的使用方法。一看便懂 。
Paul mckenny Overview of Linux-Kernel Reference Counting.pdf
内存管理:了解ARC(Automatic Reference Counting,自动引用计数)和MRC(Manual Reference Counting,手动引用计数)的工作原理。 Foundation框架:学习Foundation框架中提供的常用类,如NSString、NSArray、...
著名的代理模式例子为引用计数(英语:reference counting)指针对象。 当一个复杂对象的多份副本须存在时,代理模式可以结合享元模式以减少内存用量。典型作法是创建一个复杂对象及多个代理者,每个代理者会引用到...
PHP 使用了引用计数 (reference counting) GC 机制。 每个对象都内含一个引用计数器 refcount,每个 reference 连接到对象,计数器加 1。当 reference 离开生存空间或被设为 NULL,计数器减 1。当某个对象的引用...
Objective-C提供了一系列的内存管理规则,包括MRC(Manual Reference Counting)中的`retain`、`release`和`autorelease`概念,以及在ARC(Automatic Reference Counting)中由编译器自动插入内存管理代码的机制。...
它也记述了像 smart pointers(智能指针),reference counting(引用计数)和 proxy objects(代理对象)这样的重要的 C++ 编程技术。 Effective STL 像 Effective C++ 一样是一本面向指导方针的书,但是它专注于...
它也记述了像 smart pointers(智能指针),reference counting(引用计数)和 proxy objects(代理对象)这样的重要的 C++ 编程技术。 Effective STL 像 Effective C++ 一样是一本面向指导方针的书,但是它专注于...
C++ 对象计数 实例计数
Objective-C 和 Swift 语言的内存管理方式都是基于引用计数「Reference Counting」的,引用计数是一个简单而有效管理对象生命周期的方式。引用计数分为手动引用计数「ARC: AutomaticReference Counting」和自动引用...
引用计数(Reference Counting) 字符串(Strings) 命令行参数(Command Line Arguments) 应用程序布局(Application Layout) 应用程序结构(Application Structure) Windows操作系统(Windows) Linux操作系统(Linux)...
盒子计数维度是通过查看数字如何变化来计算的,因为我们通过应用盒子计数算法使网格更精细。以 Nr 和 r 作为变量的对数与对数图的最佳拟合线的斜率给出了图像的分形维数。使用 Box-Count 或 Box-Counting Dimension ...
伪代码和算法介绍在此:http://www.algorithmist.com/index.php/Counting_sort,本程序是 根据这个伪代码编写的源代码
To reflect that, and to make the Counting Practices Manual (CPM) even more attractive as a reference manual, the Counting Practices Committee (CPC) decided to restructure CPM 4.2 into four parts: ...
利用差分盒计数法计算图像分维数,利用matlab语言编写
IFPUG的标准,FP法软件估计必备。 FUNCTION POINT COUNTING PRACTICES MANUAL ... The Function Point Quick Reference Card presents the Function Point Counting rules in a quick reference format.
实时计数实时计数网络应用
Labview应用技术 循环计数显示(FOR课堂实训)1.docx 学习资料 复习资料 教学资源
通过ARC(Automatic Reference Counting,自动引用计数)来简化内存管理。我们的框架栈则一直基于Cocoa。Objective-C进化支持了块、collection literal和模块,允许现代语言的框架无需深入即可使用。(by gashero)感谢...
ios开发 iOS开发涵盖了许多方面,以下是一些关键的开发技巧和最佳实践: 熟悉Swift或Objective-C:Swift是苹果...内存管理:理解ARC(Automatic Reference Counting)自动引用计数机制,避免内存泄漏和循环引用问题。