关于Cstring 总结

前言:串操做是编程中最经常使用也最基本的操做之一. 作为VC程序员,不管是菜鸟或高手都曾用过CString.并且好像实际编程中很难离得开它(虽然它不是标准C++中的库).由于MFC中提供的这个类对 咱们操做字串实在太方便了,CString不只提供各类丰富的操做函数、操做符重载,使咱们使用起串起来更象basic中那样直观;并且它还提供了动态内 存分配,使咱们减小了多少字符串数组越界的隐患。可是,咱们在使用过程当中也体会到CString简直太容易出错了,并且有的不可捉摸。因此有许多高人站过 来,建议抛弃它。
    在此,我我的认为:CString封装得确实很完美,它有许多优势,如“容易使用 ,功能强,动态分配内存,大量进行拷贝时它很能节省内存资源而且执行效率高,与标准C彻底兼容,同时支持多字节与宽字节,因为有异常机制因此使用它安全方 便” 其实,使用过程当中之因此容易出错,那是由于咱们对它了解得还不够,特别是它的实现机制。由于咱们中的大多数人,在工做中并不爱那么深刻地去看关于它的文 档,况且它仍是英文的。 
   因为前几天我在工做中遇到了一个本不是问题但却特别棘手、特别难解决并且莫名惊诧的问题。最后发现是因为CString引起的,后来,没办法,我把整个 CString的实现所有看了一遍,才慌然大悟,并完全弄清了问题的缘由(这个问题,我已在csdn上开贴)。在此,我想把个人一些关于CString的 知识总结一番,以供他(她)人借鉴,也许其中有我理解上的错误,望发现者能通知我,不胜感谢。

1 CString实现的机制.
   CString是经过“引用”来管理串的,“引用”这个词我相信你们并不陌生,象Window内核对象、COM对象等都是经过引用来实现的。而 CString也是经过这样的机制来管理分配的内存块。实际上CString对象只有一个指针成员变量,因此任何CString实例的长度只有4字节.
       即: int len = sizeof(CString);//len等于4
这个指针指向一个相关的引用内存块,如图: CString str("abcd");
                                            ___
     ____________                          |   | 
    |            |                         |   | 
    | 0x04040404 |                         |   |  head部,为引用内存块相关信息
    |____________|                         |   |
         str                               |___|
                                           |'a'| 0x40404040
                                           |'b'|
                                           |'c'|
                                           |'d'|
                                           | 0 |

正由于如此,一个这样的内存块可被多个CString所引用,例以下列代码:
CString str("abcd");
CString a = str;
CString b(str);
CString c;
c = b;
上面代码的结果是:上面四个对象(str,a,b,c)中的成员变量指针有相同的值,都为0x40404040.而这块内存块怎么知道有多少个CString引用它呢?一样,它也会记录一些信息。如被引用数,串长度,分配内存长度。
这块引用内存块的结构定义以下:
struct CStringData
{
  long nRefs;       //表示有多少个CString 引用它. 4
  int nDataLength;  //串实际长度. 4
  int nAllocLength; //总共分配的内存长度(不计这头部的12字节). 4
};
因为有了这些信息,CString就能正确地分配、管理、释放引用内存块。
若是你想在调试程序的时候得到这些信息。能够在Watch窗口键入下列表达式:
(CStringData*)((CStringData*)(this->m_pchData)-1)或
(CStringData*)((CStringData*)(str.m_pchData)-1)//str为指CString实例

正由于采用了这样的好机制,使得CString在大量拷贝时,不只效率高,并且分配内存少。




2 LPCTSTR 与 GetBuffer(int nMinBufLength) 
这两个函数提供了与标准C的兼容转换。在实际中使用频率很高,但倒是最容易出错的地方。这两个函数实际上返回的都是指针,但它们有何区别呢?以及调用它们后,幕后是作了怎样的处理过程呢?
  (1) LPCTSTR 它的执行过程其实很简单,只是返回引用内存块的串地址。 它是做为操做符重载提供的,
      因此在代码中有时能够隐式转换,而有时却需强制转制。如:
          CString str;
          const char* p = (LPCTSTR)str;
          //假设有这样的一个函数,Test(const char* p);  你就能够这样调用
          Test(str);//这里会隐式转换为LPCTSTR
  (2) GetBuffer(int nMinBufLength) 它相似,也会返回一个指针,不过它有点差异,返回的是LPTSTR
  (3) 这二者到底有何不一样呢?我想告诉你们,其本质上彻底不同,通常说LPCTSTR转换后只应该当常量使用,或者作函数的入参;而GetBuffer (...)取出指针后,能够经过这个指针来修改里面的内容,或者作函数的入参。为何呢?也许常常有这样的代码:
        CString str("abcd");
        char* p = (char*)(const char*)str;
        p[2] = 'z';   
      其实,也许有这样的代码后,你的程序并无错,并且程序也运行得挺好。但它倒是很是危险的。再看
        CString str("abcd");
        CString test = str;
        ....
        char* p = (char*)(const char*)str;
        p[2] = 'z';   
        strcpy(p, "akfjaksjfakfakfakj");//这下完蛋了   
      你知道此时,test中的值是多少吗?答案是"abzd".它也跟着改变了,这不是你所指望发生的。但为何会这样呢?你稍微想一想就会明白,前面说过,因 为CString是指向引用块的,str与test指向同一块地方,当你p[2]='z'后,固然test也会随着改变。因此用它作LPCTSTR作转换 后,你只能去读这块数据,千万别去改变它的内容。
      
      假如我想直接经过指针去修改数据的话,那怎样办呢?就是用GetBuffer(...).看下述代码:
        CString str("abcd");
        CString test = str;
        ....
        char* p = str.GetBuffer(20);
        p[2] = 'z';  //   执行到此,如今test中值却还是"abcd"
        strcpy(p, "akfjaksjfakfakfakj");   //    执行到此,如今test中值仍是"abcd"
      为何会这样?其实GetBuffer(20)调用时,它实际上另外创建了一块新内块存,并分配20字节长度的buffer,而原来的内存块引用计数也相 应减1.  因此执行代码后str与test是指向了两块不一样的地方,因此相安无事。




 (4) 不过这里还有一点注意事项:就是str.GetBuffer(20)后,str的分配长度为20,即指针p它所指向的buffer只有20字节长,给它赋 值时,切不可超过,不然灾难离你不远了;若是指定长度小于原来串长度,如GetBuffer(1),实际上它会分配4个字节长度(即原来串长度);另外, 当调用GetBuffer(...)后并改变其内容,必定要记得调用ReleaseBuffer(),这个函数会根据串内容来更新引用内存块的头部信息。
   (5) 最后还有一注意事项,看下述代码:
      char* p = NULL;
      const char* q = NULL;
      {
          CString str = "abcd";
          q = (LPCTSTR)str;
          p = str.GetBuffer(20);
          AfxMessageBox(q);// 合法的
          strcpy(p, "this is test");//合法的,
      }
      AfxMessageBox(q);// 非法的,可能完蛋
      strcpy(p, "this is test");//非法的,可能完蛋
      这里要说的就是,当返回这些指针后, 若是CString对象生命结束,这些指针也相应无效。



下面演示一段代码执行过程
   void Test()
   {
     CString str("abcd");//str指向一引用内存块(引用内存块的引用计数为1,
                           长度为4,分配长度为4)
     CString a;//a指向一初始数据状态,
     a = str;  //a与str指向同一引用内存块(引用内存块的引用计数为2,
                  长度为4,分配长度为4)
     CString b(a);//a、b与str指向同一引用内存块(引用内存块的引用
                   计数为3,长度为4,分配长度为4)
     {
        LPCTSTR temp = (LPCTSTR)a;//temp指向引用内存块的串首地址。
                                  (引用内存块的引用计数为3,长度为4,分配长度为4)
        CString d = a; //a、b、d与str指向同一引用内存块(引用内存块的引用计数为4,                                 长度为4,分配长度为4)
        b = "testa"; //这条语句实际是调用CString::operator=(CString&)函数。
                       b指向一新分配的引用内存块。(新分配的引用内存块的
                       引用计数为1,长度为5,分配长度为5)
                     //同时原引用内存块引用计数减1. a、d与str仍指向原
                      引用内存块(引用内存块的引用计数为3,长度为4,分配长度为4)                      
     }//因为d生命结束,调用析构函数,导至引用计数减1(引用内存
       块的引用计数为2,长度为4,分配长度为4)
     LPTSTR temp = a.GetBuffer(10);//此语句也会致使从新分配新内存块。
                                   temp指向新分配引用内存块的串首地址(新
                                   分配的引用内存块的引用计数为1,长度
                                   为0,分配长度为10)
                                   //同时原引用内存块引用计数减1. 只有str仍
                                     指向原引用内存块(引用内存块的引用计数为1,
                                     长度为4,分配长度为4)                       
     strcpy(temp, "temp");  //a指向的引用内存块的引用计数为1,长度为0,分配长度为10
     a.ReleaseBuffer();//注意:a指向的引用内存块的引用计数为1,长度为4,分配长度为10
   }
   //执行到此,全部的局部变量生命周期都已结束。对象str a b 各自调用本身的析构构
   //函数,所指向的引用内存块也相应减1
   //注意,str a b 所分别指向的引用内存块的计数均为0,这致使所分配的内存块释放
   
    经过观察上面执行过程,咱们会发现CString虽然能够多个对象指向同一引用内块存,可是它们在进行各类拷贝、赋值及改变串内容时,它的处理是很智能并 且很是安全的,彻底作到了互不干涉、互不影响。固然必需要求你的代码使用正确恰当,特别是实际使用中会有更复杂的状况,如作函数参数、引用、及有时需保存 到CStringList当中,若是哪怕有一小块地方使用不当,其结果也会致使发生不可预知的错误

5  FreeExtra()的做用
   看这段代码
   (1)   CString str("test");
   (2)   LPTSTR temp = str.GetBuffer(50);
   (3)   strcpy(temp, "there are 22 character");
   (4)   str.ReleaseBuffer();
   (5)   str.FreeExtra();
   上面代码执行到第(4)行时,你们都知道str指向的引用内存块计数为1,长度为22,分配长度为50. 那么执行str.FreeExtra()时,它会释放所分配的多余的内存。(引用内存块计数为1,长度为22,分配长度为22)

6  Format(...)  与 FormatV(...)
   这条语句在使用中是最容易出错的。由于它最富有技巧性,也至关灵活。在这里,我没打算对它细细分析,实际上sprintf(...)怎么用,它就怎么用。 我只提醒使用时需注意一点:就是它的参数的特殊性,因为编译器在编译时并不能去校验格式串参数与对应的变元的类型及长度。因此你必需要注意,二者必定要对 应上,
   不然就会出错。如:
   CString str;
   int a = 12;
   str.Format("first:%l, second: %s", a, "error");//result?试试



7  LockBuffer() 与 UnlockBuffer()
   顾名思议,这两个函数的做用就是对引用内存块进行加锁及解锁。
   但使用它有什么做用及执行过它后对CString串有什么实质上的影响。其实挺简单,看下面代码:
   (1)   CString str("test");
   (2)   str.LockBuffer();
   (3)   CString temp = str;
   (4)   str.UnlockBuffer();
   (5)   str.LockBuffer();
   (6)   str = "error";
   (7)   str.ReleaseBuffer();
   执行完(3)后,与一般状况下不一样,temp与str并不指向同一引用内存块。你能够在watch窗口用这个表达式(CStringData*)((CStringData*)(str.m_pchData)-1)看看。
   其实在msdn中有说明:
        While in a locked state, the string is protected in two ways: 

           No other string can get a reference to the data in the locked string, even if that string is assigned to the locked string.
           The locked string will never reference another string, even if that other string is copied to the locked string. 

8  CString 只是处理串吗?
   不对,CString不仅是能操做串,并且还能处理内存块数据。功能完善吧!看这段代码
    char  p[20];
    for(int loop=0; loop<sizeof(p); loop++)
    {
        p[loop] = 10-loop;
    }
    CString str((LPCTSTR)p, 20);
    char temp[20];
    memcpy(temp, str, str.GetLength());
    str彻底可以转载内存块p到内存块temp中。因此能用CString来处理二进制数据

8  AllocSysString()与SetSysString(BSTR*) 
   这两个函数提供了串与BSTR的转换。使用时须注意一点:当调用AllocSysString()后,须调用它SysFreeString(...) 

9  参数的安全检验
   在MFC中提供了多个宏来进行参数的安全检查,如:ASSERT.  其中在CString中也不例外,有许多这样的参数检验,其实这也说明了代码的安全性高,可有时咱们会发现这很烦,也致使Debug与Release版本 不同,若有时程序Debug通正常,而Release则程序崩溃;而有时恰相反,Debug不行,Release行。其实我我的认为,咱们对 CString的使用过程当中,应力求代码质量高,不能在Debug版本中出现任何断言框,哪怕release运行彷佛
   看起来一切正常。但很不安全。以下代码:
   (1)   CString str("test");
   (2)   str.LockBuffer();
   (3)   LPTSTR temp = str.GetBuffer(10);
   (4)   strcpy(temp, "error");
   (5)   str.ReleaseBuffer();
   (6)   str.ReleaseBuffer();//执行到此时,Debug版本会弹出错框

10 CString的异常处理
   我只想强调一点:只有分配内存时,才有可能致使抛出CMemoryException.
   一样,在msdn中的函数声明中,注有throw( CMemoryException)的函数都有从新分配或调整内存的可能。
 
11 跨模块时的CString.即一个DLL的接口函数中的参数为CString&时,它会发生怎样的现象。解答我遇到的
  问题。个人问题原来已经发贴,地址为:                                  http://www.csdn.net/expert/topic/741/741921.xml?temp=.2283136

  构造一个这样CString对象时,如CString str,你可知道此时的str所指向的引用内存块吗?也许你会认为它指向NULL。其实不对,若是这样的话,CString所采用的引用机制管理内存块就 会有麻烦了,因此CString在构造一个空串的对象时,它会指向一个固定的初始化地址,这块数据的声明以下:
   AFX_STATIC_DATA int _afxInitData[] = {-1,0,0,0};
  简要描述归纳一下:当某个CString对象串置空的话,如Empty(),CString a等,它的成员变量m_pchData就会指向_afxInitData这个变量的地址。当这个CString对象生命周期结束时,正常状况下它会去对所 指向的引用内存块计数减1,若是引用计数为0(即没有任何CString引用时),则释放这块引用内存。而如今的状况是若是CString所指向的引用内 存块是初始化内存块时,则不会释听任何内存。

  说了这么多,这与我遇到的问题有什么关系呢?其实关系大着呢?其真正缘由就是若是exe模块与dll模块有一
  个是static编译链接的话。那么这个CString初始化数据在exe模块与dll模块中有不一样的地址,由于static链接则会在本模块中有一份源 代码的拷贝。另一种状况,若是两个模块都是share链接的,CString的实现代码则在另外一个单独的dll实现,而AFX_STATIC_DATA 指定变量只装一次,因此两个模块中_afxInitData有相同的地址。
  如今问题彻底明白了吧!你能够本身去演示一下。
  __declspec (dllexport) void test(CString& str)
  {
    str = "abdefakdfj";//若是是static链接,而且传入的str为空串的话,这里出错。
  }
 
最后一点想法:写得这里,其实CString中还有许多技巧性的好东东,我并没去解释。如不少重载的操做符、查找等。我认为仍是详细看看msdn,这样会比我讲好多了。我只侧重那些状况下会可能出错。固然,我叙述若有错误,敬请高手指点,不胜感谢!程序员