CString原理介绍

看了很多人写的程序,包括我自己写的一些代码,发现很大的一部分bug是关于MFC类中的CString的错误用法的.出现这种错误的原因主要是对CString的实现机制不是太了解。

    CString是对于原来标准c中字符串类型的一种的包装。因为,通过很长时间的编程,我们发现,很多程序的bug多和字符串有关,典型的有:缓冲溢出、 内存泄漏等。而且这些bug都是致命的,会造成系统的瘫痪。因此c++里就专门的做了一个类用来维护字符串指针。标准c++里的字符串类是string, 在microsoft MFC类库中使用的是CString类。通过字符串类,可以大大的避免c中的关于字符串指针的那些问题。

这里我们简单的看看Microsoft MFC中的CString是如何实现的。当然,要看原理,直接把它的代码拿过来分析是最好的。MFC里的关于CString的类的实现大部分在strcore.cpp中。

    CString就是对一个用来存放字符串的缓冲区和对施加于这个字符串的操作封装。也就是说,CString里需要有一个用来存放字符串的缓冲区,并且有 一个指针指向该缓冲区,该指针就是LPTSTR m_pchData。但是有些字符串操作会增建或减少字符串的长度,因此为了减少频繁的申请内存或者释放内存,CString会先申请一个大的内存块用来 存放字符串。这样,以后当字符串长度增长时,如果增加的总长度不超过预先申请的内存块的长度,就不用再申请内存。当增加后的字符串长度超过预先申请的内存 时,CString先释放原先的内存,然后再重新申请一个更大的内存块。同样的,当字符串长度减少时,也不释放多出来的内存空间。而是等到积累到一定程度 时,才一次性将多余的内存释放。

还有,当使用一个CString对象a来初始化另一个CString对象b时,为了节省空间,新对象b并不分配空间,它所要做的只是将自己的指针指 向对象a的那块内存空间,只有当需要修改对象a或者b中的字符串时,才会为新对象b申请内存空间,这叫做写入复制技术 (CopyBeforeWrite)。

这样,仅仅通过一个指针就不能完整的描述这块内存的具体情况,需要更多的信息来描述。

首先,需要有一个变量来描述当前内存块的总的大小。
其次,需要一个变量来描述当前内存块已经使用的情况。也就是当前字符串的长度
另外,还需要一个变量来描述该内存块被其他CString引用的情况。有一个对象引用该内存块,就将该数值加一。

CString中专门定义了一个结构体来描述这些信息:
struct CStringData
{
 long nRefs;             // reference count
 int nDataLength;        // length of data (including terminator)
 int nAllocLength;       // length of allocation
 // TCHAR data[nAllocLength]

 TCHAR* data()           // TCHAR* to managed data
  { return (TCHAR*)(this+1); }
};

实际使用时,该结构体的所占用的内存块大小是不固定的,在CString内部的内存块头部,放置的是该结构体。从该内存块头部开始的sizeof(CStringData)个BYTE后才是真正的用于存放字符串的内存空间。这种结构的数据结构的申请方法是这样实现的:
pData = (CStringData*) new BYTE[sizeof(CStringData) + (nLen+1)*sizeof(TCHAR)];
pData->nAllocLength = nLen;
其中nLen是用于说明需要一次性申请的内存空间的大小的。

从代码中可以很容易的看出,如果想申请一个256个TCHAR的内存块用于存放字符串,实际申请的大小是: sizeof(CStringData)个BYTE + (nLen+1)个TCHAR

其中前面sizeof(CStringData)个BYTE是用来存放CStringData信息的。后面的nLen+1个TCHAR才是真正用来存放字符串的,多出来的一个用来存放’\0’。

   CString中所有的operations的都是针对这个缓冲区的。比如LPTSTR CString::GetBuffer(int nMinBufLength),它的实现方法是:
首先通过CString::GetData()取得CStringData对象的指针。该指针是通过存放字符串的指针m_pchData先后偏移sizeof(CStringData),从而得到了CStringData的地址。
然后根据参数nMinBufLength给定的值重新实例化一个CStringData对象,使得新的对象里的字符串缓冲长度能够满足nMinBufLength。
然后在重新设置一下新的CStringData中的一些描述值。
最后将新CStringData对象里的字符串缓冲直接返回给调用者。

这些过程用C++代码描述就是:
 if (GetData()->nRefs > 1 || nMinBufLength > GetData()->nAllocLength)
 {
  // we have to grow the buffer
  CStringData* pOldData = GetData();
  int nOldLen = GetData()->nDataLength;   // AllocBuffer will tromp it
  if (nMinBufLength < nOldLen)
   nMinBufLength = nOldLen;
  AllocBuffer(nMinBufLength);
  memcpy(m_pchData, pOldData->data(), (nOldLen+1)*sizeof(TCHAR));
  GetData()->nDataLength = nOldLen;
  CString::Release(pOldData);
 }
 ASSERT(GetData()->nRefs <= 1);

 // return a pointer to the character storage for this string
 ASSERT(m_pchData != NULL);
 return m_pchData;

很多时候,我们经常的对大批量的字符串进行互相拷贝修改等,CString 使用了CopyBeforeWrite技术。使用这种方法,当利用一个CString对象a实例化另一个对象b的时候,其实两个对象的数值是完全相同的, 但是如果简单的给两个对象都申请内存的话,对于只有几个、几十个字节的字符串还没有什么,如果是一个几K甚至几M的数据量来说,是一个很大的浪费。
因此CString 在这个时候只是简单的将新对象b的字符串地址m_pchData直接指向另一个对象a的字符串地址m_pchData。所做的额外工作是将对象a的内存应用CStringData:: nRefs加一。
CString::CString(const CString& stringSrc)
{
  m_pchData = stringSrc.m_pchData;
  InterlockedIncrement(&GetData()->nRefs);
}

这样当修改对象a或对象b的字符串内容时,首先检查CStringData:: nRefs的值,如果大于一(等于一,说明只有自己一个应用该内存空间),说明该对象引用了别的对象内存或者自己的内存被别人应用,该对象首先将该应用值 减一,然后将该内存交给其他的对象管理,自己重新申请一块内存,并将原来内存的内容拷贝过来。

其实现的简单代码是:
void CString::CopyBeforeWrite()
{
 if (GetData()->nRefs > 1)
 {
  CStringData* pData = GetData();
  Release();
  AllocBuffer(pData->nDataLength);
memcpy(m_pchData, pData->data(),
  (pData- >nDataLength+1)*sizeof(TCHAR));
 }
}
其中Release 就是用来判断该内存的被引用情况的。
void CString::Release()
{
 if (GetData() != _afxDataNil)
 {
  if (InterlockedDecrement(&GetData()->nRefs) <= 0)
   FreeData(GetData());
 }
}

当多个对象共享同一块内存时,这块内存就属于多个对象,而不在属于原来的申请这块内存的那个对象了。但是,每个对象在其生命结束时,都首先将这块内存的引用减一,然后再判断这个引用值,如果小于等于零时,就将其释放,否则,将之交给另外的正在引用这块内存的对象控制。

CString使用这种数据结构,对于大数据量的字符串操作,可以节省很多频繁申请释放内存的时间,有助于提升系统性能。

通过上面的分析,我们已经对CString的内部机制已经有了一个大致的了解了。总的说来MFC中的CString是比较成功的。但是,由于数据结 构比较复杂(使用CStringData),所以在使用的时候就出现了很多的问题,最典型的一个就是用来描述内存块属性的属性值和实际的值不一致。出现这 个问题的原因就是CString为了方便某些应用,提供了一些operations,这些operation可以直接返回内存块中的字符串的地址值,用户 可以通过对这个地址值指向的地址进行修改,但是,修改后又没有调用相应的operations1使CStringData中的值来保持一致。比如,用户可 以首先通过operations得到字符串地址,然后将一些新的字符增加到这个字符串中,使得字符串的长度增加,但是,由于是直接通过指针修改的,所以描 述该字符串长度的CStringData中的nDataLength却还是原来的长度,因此当通过GetLength获取字符串长度时,返回的必然是不正 确的。

存在这些问题的operations下面一一介绍。

1. GetBuffer

很多错误用法中最典型的一个就是CString:: GetBuffer ()了.查了MSDN,里面对这个operation的描述是:
 Returns a pointer to the internal character buffer for the CString object. The returned LPTSTR is not const and thus allows direct modification of CString contents。
这段很清楚的说明,对于这个operation返回的字符串指针,我们可以直接修改其中的值:
 CString str1("This is the string 1");――――――――――――――――1
 int nOldLen = str1.GetLength();―――――――――――――――――2
 char* pstr1 = str1.GetBuffer( nOldLen );――――――――――――――3
 strcpy( pstr1, "modified" );――――――――――――――――――――4
 int nNewLen = str1.GetLength();―――――――――――――――――5

通过设置断点,我们来运行并跟踪这段代码可以看出,当运行到三处时,str1的值是”This is the string 1”,并且nOldLen的值是20。当运行到5处时,发现,str1的值变成了”modified”。也就是说,对GetBuffer返回的字符串指 针,我们将它做为参数传递给strcpy,试图来修改这个字符串指针指向的地址,结果是修改成功,并且CString对象str1的值也响应的变成了” modified”。但是,我们接着再调用str1.GetLength()时却意外的发现其返回值仍然是20,但是实际上此时str1中的字符串已经变 成了” modified”,也就是说这个时候返回的值应该是字符串” modified”的长度8!而不是20。现在CString工作已经不正常了!这是怎么回事?

很显然,str1工作不正常是在对通过GetBuffer返回的指针进行一个字符串拷贝之后的。

再看MSDN上的关于这个operation的说明,可以看到里面有这么一段话:
If you use the pointer returned by GetBuffer to change the string contents, you must call ReleaseBuffer before using any other CString member functions.

 原来在对GetBuffer返回的指针使用之后需要调用ReleaseBuffer,这样才能使用其他CString的operations。上 面的代码中,我们在4-5处增建一行代码:str2.ReleaseBuffer(),然后再观察nNewLen,发现这个时候已经是我们想要的值8了。

从CString的机理上也可以看出:GetBuffer返回的是CStringData对象里的字符串缓冲的首地址。根据这个地址,我们对这个地 址里的值进行的修改,改变的只是CStringData里的字符串缓冲中的值, CStringData中的其他用来描述字符串缓冲的属性的值已经不是正确的了。比如此时CStringData:: nDataLength很显然还是原来的值20,但是现在实际上字符串的长度已经是8了。也就是说我们还需要对CStringData中的其他值进行修 改。这也就是需要调用ReleaseBuffer()的原因了。

正如我们所预料的,ReleaseBuffer源代码中显示的正是我们所猜想的:
 CopyBeforeWrite();  // just in case GetBuffer was not called

 if (nNewLength == -1)
  nNewLength = lstrlen(m_pchData); // zero terminated

 ASSERT(nNewLength <= GetData()->nAllocLength);
 GetData()->nDataLength = nNewLength;
 m_pchData[nNewLength] = ´\0´;
其中CopyBeforeWrite是实现写拷贝技术的,这里不管它。

下面的代码就是重新设置CStringData对象中描述字符串长度的那个属性值的。首先取得当前字符串的长度,然后通过GetData()取得CStringData的对象指针,并修改里面的nDataLength成员值。

但是,现在的问题是,我们虽然知道了错误的原因,知道了当修改了GetBuffer返回的指针所指向的值之后需要调用ReleaseBuffer才 能使用CString的其他operations时,我们就能避免不在犯这个错误了。答案是否定的。这就像虽然每一个懂一点编程知识的人都知道通过new 申请的内存在使用完以后需要通过delete来释放一样,道理虽然很简单,但是,最后实际的结果还是有由于忘记调用delete而出现了内存泄漏。
实 际工作中,常常是对GetBuffer返回的值进行了修改,但是最后却忘记调用ReleaseBuffer来释放。而且,由于这个错误不象new和 delete人人都知道的并重视的,因此也没有一个检查机制来专门检查,所以最终程序中由于忘记调用ReleaseBuffer而引起的错误被带到了发行 版本中。

要避免这个错误,方法很多。但是最简单也是最有效的就是避免这种用法。很多时候,我们并不需要这种用法,我们完全可以通过其他的安全方法来实现。
比如上面的代码,我们完全可以这样写:
 CString str1("This is the string 1");
 int nOldLen = str1.GetLength();
 str1 = "modified";
 int nNewLen = str1.GetLength();

但是有时候确实需要,比如:
我们需要将一个CString对象中的字符串进行一些转换,这个转换是通过调用一个dll里的函数Translate来完成的,但是要命的是,不知道什么原因,这个函数的参数使用的是char*型的:
DWORD Translate( char* pSrc, char *pDest, int nSrcLen, int nDestLen );
这个时候我们可能就需要这个方法了:
CString strDest;
Int nDestLen = 100;
DWORD dwRet = Translate( _strSrc.GetBuffer( _strSrc.GetLength() ),
 strDest.GetBuffer(nDestLen),
 _strSrc.GetLength(), nDestlen );
_strSrc.ReleaseBuffer();
strDest.ReleaseBuffer();
if ( SUCCESSCALL(dwRet)  )
{
}
if ( FAILEDCALL(dwRet) )
{
}

的确,这种情况是存在的,但是,我还是建议尽量避免这种用法,如果确实需要使用,请不要使用一个专门的指针来保存GetBuffer返回的值,因为 这样常常会让我们忘记调用ReleaseBuffer。就像上面的代码,我们可以在调用GetBuffer之后马上就调用ReleaseBuffer来调 整CString对象。


2. LPCTSTR

关于LPCTSTR的错误常常发生在初学者身上。
例如在调用函数
DWORD Translate( char* pSrc, char *pDest, int nSrcLen, int nDestLen );
时,初学者常常使用的方法就是:
int nLen = _strSrc.GetLength();
DWORD dwRet = Translate( (char*)(LPCTSTR)_strSrc),
 (char*)(LPCTSTR)_strSrc),
 nLen,
 nLen);
if ( SUCCESSCALL(dwRet)  )
{
}
if ( FAILEDCALL(dwRet) )
{
}

他原本的初衷是将转换后的字符串仍然放在_strSrc中,但是,当调用完Translate以后之后再使用_strSrc时,却发现_strSrc已经工作不正常了。检查代码却又找不到问题到底出在哪里。

其实这个问题和第一个问题是一样的。CString类已经将LPCTST重载了。在CString中LPCTST实际上已经是一个 operation了。对LPCTST的调用实际上和GetBuffer是类似的,直接返回CStringData对象中的字符串缓冲的首地址。
其C++代码实现是:
_AFX_INLINE CString::operator LPCTSTR() const
 { return m_pchData; }

因此在使用完以后同样需要调用ReleaseBuffer()。
但是,这个谁又能看出来呢?

其实这个问题的本质原因出在类型转换上。LPCTSTR返回的是一个const char*类型,因此使用这个指针来调用Translate编译是不能通过的。对于一个初学者,或者一个有很长编程经验的人都会再通过强行类型转换将 const char*转换为char*。最终造成了CString工作不正常,并且这样也很容易造成缓冲溢出。

通过上面对于CString机制和一些容易出现的使用错误的描述,可以使我们更好的使用CString。

 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/benny5609/archive/2007/12/09/1926126.aspx

转载于:https://www.cnblogs.com/liwqiang/archive/2010/02/23/1672067.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/276181.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何从零开始开发一个 Chrome 插件?

什么是浏览器插件&#xff1f;简单来说浏览器插件&#xff0c;是浏览器上的一种工具&#xff0c;可以提供一些浏览器没有的功能&#xff0c;帮你做一些有趣的事情。开发者可以根据自己的喜欢&#xff0c;去实现一些功能。插件基于Web技术&#xff08;html、css、js&#xff09;…

mysql 重复字段查询及排除重复值

转载链接&#xff1a;http://blog.sina.com.cn/s/blog_3edc5e2e010131ys.html mysql 重复字段查询及排除重复值 SELECT a.id,a.title FROM dede_archives a left join dede_taglist t on t.taga.title WHERE t.typeid$id and t.arcrank>-1 and a.typeid28 group by t.tag; …

swiper移入暂停_react中swiper注意事项及鼠标划入停止轮播

首先是实例化swiper这里有一个注意点&#xff0c;就是实例化的时机如果你的swiper内容是写死的&#xff0c;可以在componentDidMount中实例化&#xff0c;但是如果你的内容是通过接口异步请求过来的&#xff0c;就必须在componentDidUpdate里实例化&#xff0c;因为如果在 comp…

轉Excel的一種簡單方法

写了这么久的程序﹐越来越喜欢那种简单的解决方法﹐这段时间在做一个报表系统﹐其中有需要转Excel﹐而且要求兼容openoffice﹐遂利用asp语法,asp.net的控件封装特性以及excel 2003的xml试算清格式做了一个看起来比较"清爽"的excel转档方案。一.开始原理很简单﹐excel…

详解MySQL中EXPLAIN解释命令

转载链接&#xff1a;http://database.51cto.com/art/200912/168453.htm explain显示了mysql如何使用索引来处理select语句以及连接表。可以帮助选择更好的索引和写出更优化的查询语句。 使用方法&#xff0c;在select语句前加上explain就可以了&#xff1a; 如&#xff1a;…

Shell编程基础

我们可以使用任意一种文字编辑器&#xff0c;比如gedit、kedit、emacs、vi等来编写shell脚本&#xff0c;它必须以如下行开始&#xff08;必须放在文件的第一行&#xff09;&#xff1a; # !/bin/sh ...注意&#xff1a;最好使用“!/bin/bash”而不是“!/bin/sh”&#xff0c;…

总结:自学前端的高效学习路线

提到前端&#xff0c;大多数人都会想到薪资高。也正因为如此&#xff0c;很多人想要从事前端开发这个岗位&#xff0c;也由此衍生出来一个问题&#xff1a;为什么前端工程师供不应求&#xff0c;但还是有很多学前端的人找不到工作&#xff1f;其实行业不是缺前端工程师&#xf…

机器人出魔切还是三相_英雄联盟:辅助也要去上单,机器人布里茨玩法介绍

英雄联盟&#xff1a;辅助也要去上单&#xff0c;机器人布里茨玩法介绍出装方面我们都知道他的被动是可以将法力值化为机的护盾的&#xff0c;而这样的话装备就可以选择魔切&#xff0c;然后再出一个鞋子&#xff0c;为什么不先出三项呢&#xff1f;三项的性价比是比较高的&…

vmware创建虚拟机不识别网卡

今天在给虚拟机添加网卡的时候&#xff0c;出现了虚拟机不识别新加的网卡&#xff0c;很纳闷&#xff0c;连的一样的端口组&#xff0c;为什么新加的网卡识别不了呢 然后查看pci设备&#xff0c;发现网卡的驱动为 AMD 79C970 PCnet32- LANCE 然后都vc上查看&#xff0c;果真驱动…

转:26个Jquery使用小技巧(jQuery tips, tricks solutions)

26个Jquery使用小技巧(jQuery tips, tricks & solutions) 前段时间发布了Jquery类库1.4版本&#xff0c;使用者也越来越多&#xff0c;为了方便大家对Jquery的使用&#xff0c;下面列出了一些Jquery使用技巧。比如有禁止右键点击、隐藏搜索文本框文字、在新窗口中打开链接…

周末包邮送书和小红包中奖名单公布

大家好&#xff0c;我是若川。周末送福利&#xff0c;给大家送红包、包邮送新书&#xff01;这篇文章中&#xff0c;准备了3本自选前端新书&#xff0c;10个2元小红包&#xff0c;在看抽10人每人5元红包&#xff0c;2月28日晚8点开奖。现将名单公布如下&#xff1a;在看抽奖&am…

Ubuntu 命令行修改网络配置方法

转载链接&#xff1a;http://www.jb51.net/article/15807.htm Ubuntu 命令行修改网络配置方法 /etc/network/interfaces 打开后里面可设置DHCP或手动设置静态ip。前面auto eth0&#xff0c;让网卡开机自动挂载. 1. 以DHCP方式配置网卡 编辑文件/etc/network/interfaces: sudo v…

python treeview底部加个按钮_Python爬取京东商品信息(GUI版本)

前言本文的文字及图片来源于网络,仅供学习、交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理。作者&#xff1a;DYblog转载&#xff1a;https://www.cnblogs.com/dy8888/p/13257918.htmlPS&#xff1a;如有需要Python学习资料的小伙伴可以加点击…

Linux下编译安装Mysql简单步骤

常规方式编译安装MySQL时&#xff0c;适合用第一条最正宗的MySQL产品线5.2及以前版本&#xff1a;所谓常规方式编译安装MySQL就是延续早期MySQL的3部曲安装方式&#xff0c;即./configure;make;make install&#xff0c;下面是老男孩在早期的企业生产场景下操作过的具体命令及参…

Vue.js 3.0 响应式 API 比 2.x 好在哪儿?

Hello&#xff0c;各位小伙伴&#xff0c;接下来的一段时间里&#xff0c;我会把我的课程《Vue.js 3.0 核心源码解析》中问题的答案陆续在我的公众号发布&#xff0c;由于课程的问题大多数都是开放性的问题&#xff0c;所以我的答案也不一定是标准的&#xff0c;仅供你参考喔。…

招聘.NET程序员

人才难找啊&#xff0c;顺便发个招聘启事。 西安瀚博科技有限公司招聘.NET程序员&#xff0c;有工作经验者优先 如有意向&#xff0c;请发邮件到 slzhanghiweb.cn 转载于:https://www.cnblogs.com/shengli/archive/2010/03/08/1680861.html

xml解析类

转载链接&#xff1a;http://zyan.cc/post/253 今天在PHP4环境下重新写一个接口程序&#xff0c;需要大量分析解析XML&#xff0c;PHP的xml_parse_into_struct()函数不能直接生成便于使用的数组&#xff0c;而SimpleXML扩展在PHP5中才支持&#xff0c;于是逛逛搜索引擎&#x…

jmeter学习指南之聚合报告

jmeter视频地址&#xff1a;https://edu.51cto.com/course/14305.html 上一篇文章中我们讲了Jmeter结果分析最常用的一个Listener查看结果树&#xff0c;今天接着讲另一个最常用的listener--聚合报告Aggregate Report。我们先来看看聚合报告中的主要名称的含意&#xff1a;Labe…

敏捷开发概述

敏捷方法强调适应性而非预见性。 目前列入敏捷方法的有&#xff1a; 軟件開發節奏&#xff0c;Software Development Rhythms 敏捷數據庫技術&#xff0c;AD/Agile Database Techniques 敏捷建模&#xff0c;AM/Agile Modeling 自適應軟件開發&#xff0c;ASD/Adaptive Softwar…

2021 整理的最全学习资源,送给每一个努力着的人

时间来到了 2021 年&#xff0c;新的一年有新的期待&#xff0c;而我亦有新的祝福如果说在过去的一年&#xff0c;经历太多&#xff0c;心酸、迷茫、焦虑、幸福、喜悦那么在 2021 年&#xff0c;希望你可以去过一种遇见自己的生活&#xff0c;恬淡、热情&#xff0c;喜欢自己而…