小憩,味一二 ——08年3月编程手札

来源:互联网 发布:西安软件新城公交 编辑:程序博客网 时间:2024/04/27 19:24
日前笔者在VC6.0开发环境中基于MFC类库开发实现了一个简单的功能,其中在定制派生于CTreeCtrl类时利用到了集合类CPtrArray,编写时遇到若干有趣点,特记录于此。 1开发情境:程序大致功能是初始化时从User.ini文件中读出用户所属部门和姓名显示在树形控件中,当用户在使用程序中展开节点,获取叶子节点用户的基本资料,而基本资料的获取有赖于所属部门和姓名,当决定如何获取用户资料时,利用到了CPtrArray。 User.ini结构中存储的数据形式是: [1] dep=部门 name=姓名 face=头像位图存放地址 onip=在线IP地址 [2] dep=部门 name=姓名 face=头像位图存放地址 onip=在线IP地址字段分别以整数表示,即从1到n,n表示总共用户数,键和键值如上表示。 2实现方法一:方法思路是利用结构体和CPtrArray在初始化时就提取出用户所有资料保存到结构体和CPtrArray中,当需要获取叶子节点用户基本资料时从CPtrArray中导出即可,而不再需要访问User.ini加快处理速度。所定义结构体声明如下: typedef struct _UserInfo {        char  chDep[25];//部室     char  chName[20];//真实姓名        char  chIP[20];//IP        char  chFace[10];//头像 }UserInfo,*PUserInfo; 2.1错误的实现一所定制的类CCustomeTreeCtrl派生于CTreeCtrl类,首先在CustomeTreeCtrl.h中定义CPtrArray类的变量: CPtrArray m_pPtrArray; 在CustomeTreeCtrl.cpp中实现函数如下: void CCustomeTreeCtrl::InitTreeCtrl( ) {        int iIndex=1;        CString strPath=GetModulePath();//获取路径,该函数是自己完成,这里直接调用        CString strPath1=strPath+" //user.ini"; CString strField; CString strUserDep; CString strUserName; CString strUserONIP; CString strUserFace;        while(1)        {               strField.Format("%d",iIndex);               GetPrivateProfileString(strField,"dep","NULL",                              strUserDep.GetBuffer(30),30,strPath1);//获取部门               strUserDep.ReleaseBuffer();               if("NULL"==strUserDep) break;//循环结束               GetPrivateProfileString(strField,"name","NULL",                              strUserName.GetBuffer(30),30,strPath1);//获取姓名               strUserName.ReleaseBuffer();         //显示到树形控件中的节点,这里代码作了节略。               GetPrivateProfileString(strField,"onip","NULL",                              strUserONIP.GetBuffer(30),30,strPath1);//获取IP               strUserONIP.ReleaseBuffer();               GetPrivateProfileString(strField,"face","NULL",               strUserFace.GetBuffer(30),30,strPath1);//获取头像位图存放位置               strUserFace.ReleaseBuffer();               //存储到结构体               UserInfo userInfo;         memset(&userInfo,0,sizeof(UserInfo)); strcpy(userInfo .chDep,strUserDep); strcpy(userInfo .chName,strName);         strcpy(userInfo .chIP,strUserONIP); strcpy(userInfo .chFace,strUserFace); //加到集合类中 m_pPtrArray.Add(&userInfo); iIndex++;        } } 上面这个函数实现了从User.ini中提取所有用户的资料,存在到结构体UserInfo和m_pPtrArray中,下面函数实现从m_pPtrArray提取所需要的用户资料。 void CCustomeTreeCtrl::GetInfoFromArray(UserInfo *pUserInfo) {        //采用传址方式实现 //获取从树控件叶子节点传递进来的部门和姓名值,放在pUserInfo中     //根据部门和姓名从集合类中获取对应的用户其他资料,放在pUserInfo中 int iCount=m_pPtrArray.GetSize();//获取记录总数 for(int i=0;i<iCount;i++){        UserInfo *pUserInfoArray=m_pPtrArray.GetAt(i);     if(!strcmp(pUserInfo->chDep,pUserInfoArray->chDep) &&       !strcmp(pUserInfo->chName,pUserInfoArray->chName) ) {//相等,是该用户        strcpy(pUserInfo->chIP,pUserInfoArray-> chIP); strcpy(pUserInfo->chFace,pUserInfoArray-> chFace); //得到了该用户资料,退出循环 break; } } 这两个函数从逻辑上看并无问题,编译自然也没有任何语法错误,那么错在那呢?因为在测试过程中很快就发现了这个问题,对于这类可重建的错误还是可以通过单步调试发现问题根源。调试发现UserInfo *pUserInfoArray=m_pPtrArray.GetAt(i);所获取的结构体UserInfo中值是乱码,这是为什么呢?也就是说值没有放到集合类pPtrArray中!这里就要对CPtrArray有所了解了,CPtrArray所存储的是指针变量,就是结构体UserInfo的地址。CPtrArray只负责把相应的指针体串联起来形成一个队列。当把结构体UserInfo声明为局部变量,只在循环体存在生命周期,在另一个函数中就已经不存在为其分配的内存地址,也就是栈中本分配给该结构体的地址空间另作它用了,而m_pPtrArray仍然存储这个地址,显然获取的就是乱码了。 2.2错误实现二有了上面的错误,解决办法最直接的就是把结构体UserInfo所生命的变量生命周期延长与对象一般,在CustomeTreeCtrl.h中定义: UserInfo   m_UserInfo; 在函数中InitTreeCtrl( )和GetInfoFromArray(UserInfo *pUserInfo)中是作用范围。但这时出现了想当然的错误。代码显示的结果是无论UserInfo *pUserInfoArray=m_pPtrArray.GetAt(i)中i如何变化,显示的都是同一个用户资料,就是在函数InitTreeCtrl( )中获取到的最后一个用户资料。略思即明了,CPtrArray串联起来的是地址,而这里只声明了一个变量,也就是CPtrArray串联了都是同一个地址,这样这个变量尽管作用域扩大了,但每次都被覆盖重写,然后连到m_pPtrArray。这两错误反应出两个基本问题:编程时轻易会产生的错误就是想当然的逻辑;同时在测试中对于错误实现二而言存在一个测试度的问题,大量而恰如其分的测试是必须有清晰思路的,除了对功能和业务相关性的把握上,还需要对代码实现的技术上有所侧重,这样才能保证测试的高效。 2.3正确的实现一要正确实现,很明显在在CustomeTreeCtrl.h中定义结构体UserInfo为数组。笔者测试时,声明了UserInfo   m_UserInfo[10];结果发现正确显示了。但这个实现是不理想的,首先数组声明的量该多少呢?也就是该预先分配多少个结构体UserInfo的地址空间来存放用户呢?用户的总数目前还是未知的,这个时候一般思路是往大了分配,但这样肯定是浪费了空间。如果先运行一次循环获取用户总数在分配对应空间,这需要再进行一次n的线性循环,也浪费了时间。 2.4正确的实现二既然在栈中分配空间不理想,这个实现就用到了堆中分配具体实现是:这是错误实现一函数InitTreeCtrl( )中代码: //存储到结构体 UserInfo userInfo;         memset(&userInfo,0,sizeof(UserInfo)); strcpy(userInfo .chDep,strUserDep); strcpy(userInfo .chName,strName);         strcpy(userInfo .chIP,strUserONIP); strcpy(userInfo .chFace,strUserFace); //加到集合类中 m_pPtrArray.Add(&userInfo); 把这段代码改成: //存储到结构体 UserInfo *pUserInfo=new UserInfo;//堆中分配         memset(pUserInfo,0,sizeof(UserInfo)); strcpy(pUserInfo->chDep,strUserDep); strcpy(pUserInfo->chName,strName);         strcpy(pUserInfo->chIP,strUserONIP); strcpy(pUserInfo->chFace,strUserFace); //加到集合类中 m_pPtrArray.Add(pUserInfo); 堆中所动态分配的空间,如果不显示删除,即调用delete,其生命周期和应用程序一样。测试结果正常。这样避免了浪费空间和增加一次n的循环。这也体现了堆栈分配空间的区别。但这样同样会造成内存泄露的可能性,同时如果用户数量过大,对内存空间来说是个不小的负担。 3实现方法二:算法无非体现在时间和空间效率上,既然在空间上不满意上面的实现,可着眼于时间效率上,也就是牺牲时间换取空间。这个实现就是在根据叶子节点上部门和姓名去提取用户资料时,还是从User.ini中取,不再内存中分配空间来存储。具体实现如下:在CustomeTreeCtrl.cpp中实现函数如下: void CCustomeTreeCtrl::InitTreeCtrl( ) {        int iIndex=1;        CString strPath=GetModulePath();//获取路径,该函数是自己完成,这里直接调用        CString strPath1=strPath+" //user.ini"; CString strField; CString strUserDep; CString strUserName;        while(1)        {               strField.Format("%d",iIndex);               GetPrivateProfileString(strField,"dep","NULL",                              strUserDep.GetBuffer(30),30,strPath1);//获取部门               strUserDep.ReleaseBuffer();               if("NULL"==strUserDep) break;//循环结束               GetPrivateProfileString(strField,"name","NULL",                              strUserName.GetBuffer(30),30,strPath1);//获取姓名               strUserName.ReleaseBuffer();               //显示到树形控件中的节点,这里代码作了节略。 iIndex++;        } } 上面这个函数实现了从User.ini中仅提取姓名和部门显示在树形控件中节点。 void CCustomeTreeCtrl::GetInfoFromUserIni(UserInfo *pUserInfo) {        BOOL flag=TRUE;        int iIndex=1;        CString strPath=GetModulePath();        CString strPath1=strPath+" //user.ini";        while(flag)        {               CString strField;               strField.Format("%d",iIndex);               CString strUserDep;               CString strUserName;               GetPrivateProfileString(strField,"dep","NULL",                              strUserDep.GetBuffer(30),30,strPath1);               strUserDep.ReleaseBuffer();               if("NULL"==strUserDep) break;               GetPrivateProfileString(strField,"name","NULL",                              strUserName.GetBuffer(30),30,strPath1);               strUserName.ReleaseBuffer(); if((CString)pUserInfo->chDep==strUserDep&& (CString)pUserInfo->chName==strUserName)//判断是否是该用户的资料               {                      CString strUserONIP;                      CString strUserFace;                      GetPrivateProfileString(strField,"onip","NULL",                              strUserONIP.GetBuffer(30),30,strPath1);                      strUserONIP.ReleaseBuffer();                      strcpy(pUserInfo->chIP,strUserONIP);                      GetPrivateProfileString(strField,"face","NULL",                              strUserFace.GetBuffer(30),30,strPath1);                      strUserFace.ReleaseBuffer();                      strcpy(pUserInfo->chFace,strUserFace);                      break;               }               iIndex++;        } } 函数GetInfoFromUserIni(UserInfo *pUserInfo)是从文件User.ini中直接在提取。 4两种方法比较 1)  代码执行行数实现方法二执行代码的行数明显减少了,体现在函数GetInfoFromUserIni(UserInfo *pUserInfo)中if语句中代码只执行一次。而在实现方法一中函数InitTreeCtrl( )却要都执行,也就是说用户的资料选择一个节点只需要提取一个用户的资料,实现方法二是这样,但实现方法一却提取了所有用户资料在那里等待。 2)  空间地址分配实现方法二不需要任何额外的内存,实现方法一则需要n个用户的结构体UserInfo空间。实现方法一是把用户资料先放到内存等待提取,而实现方法二是需要用户资料时再到文件中提取。 3)  循环实现效率两种方法都需要两次n的循环,不同的是循环所提取的资料是从不同地方而来,实现方法一是直接在内存中比较循环,而实现方法二是从文件中提取。 4)  综合比较尽管实现方法二在空间和执行代码行数上减少了不少时间,但从文件中提取数据也牺牲了不少时间。而实现方法一除了只做一次文件数据提取可以在时间上优胜外,空间和代码执行行数上却是极大的缺陷。综上,本次程序编写总结有下面三点值得玩味:测试的度;堆栈的选择;时空效率,牺牲时间或牺牲空间应根据具体而定。            方建生   08.03.10