目录

---------------------------------------------------------------------------------------------

1.变量声明 Variable declaration 

1.1 变量的作用功能 Function of variables 

1.2 变量的作用域 Life area?? of variables 

1.3 变量作用形态 State of variables 

2.指针 Pointer 

2.1 指针的使用场合 Cases using pointers 

2.2 指针的安全使用 Using pointers safely 

2.3 指针内存回收 Freeing memory of pointers 

3.数据结构类 Data structure classes 

3.1 普通数组与数组类 Traditional array and classes for array 

3.2 区别与应用 Difference and using: Array, List, Map 

3.3 示例 Sample: CTypedPtrArray  

=============================================================================================

1.变量声明 Variable declaration 

    变量的声明方式将直接影响到软件的坚固性和速度。

    The method of variable declaration will affect robustness and speed of software.

1.变量的作用功能 Function of variables 

    整体上讲,变量分为存储服务两种类型。

    前者如CPeople *p=new CPeople(),其特点是与客观对象一一对应,其数量一般较大,而且应与客观对象的生存期相同。

    后者如COODBDatabase Db,其特点是为了完成某个功能出现,数量以能完成功能为限(经常是一个),生存期可以只是本功能的执行时间;不过为了方便,避免多次定义,往往可超过此周期。

    因此,如果一个类需要被声明大量实例,那么不要在其中声明用于服务的成员变量(至少不要声明那些大的),用以下方法来调用服务:

  • 声明static的成员变量,使所有实例共用一个变量;
  • 在成员函数实现中声明所需的服务,不过如果服务是一个需要构造和析构的类,会引起速度下降;
  • 引用外部服务类的指针;

    Variables can be divided mainly into two types: for storage and for service.

    Sample for storage is CPeople *p=new CPeople(), its characteristic is that it is corresponding to a real object, and the amount of this type of variable maybe large, it exists as long as the real object need. 

    Sample for service is COODBDatabase Db, its characteristic is that it exists for only a function, the amount of this type of variable is decided by the function requirement and it lives only during the function; but for convenience and avoiding repeated definition, some can outlive the function.

    So if a class will be instanced for a large number, 

    

1.2 变量的作用域 Life area?? of variables 

全局变量

    大部分程序可以不用任何全局变量完成其功能,因此应当不使用全局变量。但在以下情况下,可方便且安全地使用全局变量:

    ①此变量类型只声明唯一的实例,并且生存期与进程相同:

  • 主窗口或主进程:如extern CFhtHostApp theApp; extern CFhtHostDlg *g_pDlg;其命名不需要很具体;
  • 全程唯一此类型的实例:如CMyCAHost *g_pMyCAHost; COODBDatabase COODBDatabase::m_DefaultOODBDatabase;

如果需要声明两个以上实例的,则按规范以g_开头,且命名要具体,以示区别。

    ②非VC++类的函数,且需要符合其特定参数的,可用全局变量进行传递:

  • Rpc的实现函数,如:

extern "C" boolean RpcSetHost(handle_t IDL_handle,int *pnSize,unsigned char *szCAHost)
    {
    ...
    g_CAConsole.SetHost(pCAHost); 
    ...
    }

  • 线程函数,如:

DWORD WINAPI CRpcServer::ServerThread(LPVOID pParam)
    {
    ...
    g_pServer->StopServer();
    ...
    }

    注意,配合全局函数使用,可提高全局变量的稳定性。

    CADPS系统中的::FhtLoadConfig()函数实际上调用了全局变量CCARegistry::g_CARegistry,使用全局函数屏蔽了全局变量,将此全局变量的应用局限在全局函数内部,因而只需此类的管理员保证此函数中的应用是安全的,对此函数的调用也都是安全的。

    利用全局函数封装全局变量的好处与类的封装相似。在《设计模式》一书中提到的可复用面向对象编程的第一个原则就是:针对接口编程,而不是针对实现编程。这样在需要改变数据结构时,应用层的软件可以完全不改动。

    将无谓的全局变量转换为其他变量,可提高系统稳定性。

类成员变量

    类的成员变量用以存储类的数据、状态、外部数据或服务的地址。以下是一些关于成员变量应用适当和不当的场合:

    ①绝大部分用成员变量进行服务的场合都可以通过其他方法化解,从而使成员变量只有存储功能。

    例如COODBRecord中的CTypedPtrArray <CObArray,COODBField*> m_FieldArray变量,尽管从自然对象中抽象时,每个记录都包含多个Field,而且数据也存储在Field中,但实际上在C++类中,数据是存储在其相应的变量中的,COODBField只在GetFieldValue()函数调用时用一次将数据库中的数据取出,然后用COODBField::DatabaseToClass()转换为C++数据类型,并将数据赋值给C++变量的地址,因而是服务型数据,所以后来将其取消,而在GetFieldValue()调用时生成一组(和列数相同)临时的数据,进行转换服务,将每次Select()操作时所需COODBField的数量从300000(20000Customers时)个减少到15个,将内存占用从107M减少到55M,还节约了运行时间。

    实际上在上例中,只需要一个而非一组COODBField即可完成此工作,不过由于程序易读性比效率更优先,所以在影响不大的情况下(15个和1个内存占用差别不大),并不考虑优化效率的极端手段。

    如果用于服务的数据很小,不适合单独在类的外部声明,则可以改为该服务函数中的一个临时变量。但有些数据的构造和销毁相当花费时间(如CString/CDBVariant等),这时候可考虑将此数据声明为类的static变量。相关内容

    ②绝大部分以上使用场合以外的全局变量,都可以被一个类成员变量以外部服务地址的方式取代。

    在COODBQuery中有一个变量COODBCDMap *m_cpOODBCDMap,此数据存储了全局变量COODBCDMaps g_OODBCDMaps中与此类相对应的列与变量地址的映射信息,由于使用了一个变量来存储此映射信息的地址,并在COODBQuery调用m_cpOODBCDMap=::FhtOODBCDMapOf(aClass.CDMapName())来初始化此变量,而不是直接使用全局变量。因而因而在此后的操作中将不再需要用到全局变量,而只需要判断m_cpOODBCDMap的合法性即可。

类函数变量,全局函数变量

    (略)

1.3 变量作用形态 State of variables 

静态变量

    因为在程序运行时只使用同一个实例,因而可以节约其构造器和销毁器的运行时间,对海量运算节约时间有益。常见的构造器和销毁器缓慢的类包括CString,COle系列类等。要特别注意的是静态变量的声明初始化如static int nCount=0;只进行一次,因此这种应用中要特别注意清除上次的参数数据或避免其影响。

    静态变量的一种常见应用是用来记录上次运行时的数据,不过从经验来看,如果是在类的函数中,应直接声明此数据为类的变量可使应用更加易于理解和控制。

指针变量(见下)

2.指针 Pointer

2.1 指针的使用场合 Cases using pointers 

    在正常情况下,无论从安全性和编程效率、运行效率上比较,指针比普通声明的变量都要低,特别是new和delete的速度相当慢,因此一般不使用指针变量。以下是COODBLib优化前的几个例子:

在Debug版本中选取2E4个CCustomer过程中,需花费:
    563.414ms完成300032次CObject::operator new()操作
    469.955ms完成140000次delete()操作
而全面优化过后,进行此操作共耗时6~7秒,可见指针空间申请和销毁可能占用大量时间。

    但因为对于多态、引用、变量交换等操作指针有其优势,在此类情况下将不得不使用指针;此时要特别注意安全问题和内存回收问题。

2.2 指针的安全使用 Using pointers safely 

    以上三条就是指针的使用要点。

初始化

  • 在指针声明时应习惯性初始化为NULL,如
    CRecordset *pRecordset=NULL;
  • 在类的构造器中应对所有成员指针变量初始化。

检查 (Check state and parameters)

  • 检查函数传入参数,如
    BOOL SetName(LPCTSTR lpszName)
        {
        if (lpszName==NULL) 
            return FALSE;
        ...
        }
  • 在类成员函数中检查类的指针变量是否可用,即用于外部服务的指针变量是否已经就位。
  • 删除指针内存时检查指针是否为空,配合下面的复位指针,可防止多次删除同一个指针内存。

    一种例外情况是VC的Collection族类(Array/List/Map)使用时不需要判断,因为其GetAt()函数已经进行了判断。

复位指针

在删除指针内存后,应复位指针为0,否则在下次检查指针是否为空时可能出错。如:
    delete m_pRecordset;
    m_pRecordset=NULL;

但在脱离作用域前可不复位,如在函数末尾删除临时指针变量及在销毁器删除成员指针变量。

2.3 指针内存回收 Freeing memory of pointers 

    函数内指针

    如果此指针是临时变量或尽管是类的变量,却只在此函数中才会使用,在函数退出时删除,如果函数退出的情况很复杂,则使用函数销毁器来完成,否则极可能发生漏删现象。参考函数结构规范中的例子。

    替别的函数销毁指针是及其危险且难以维护的事情,如:

    void main()
    {
        char *sz=new char[8];
        memset(sz, 0, 8);
        memcpy(sz, "name", 4);
        PrintfName(sz);
        //...
    }

    void PrintName(char *sz)
    {
        printf("%s", sz);
        delete []sz;
    }

    当上面的两个函数不是同一个程序员维护时,极易发生内存泄漏、重复删除内存等问题。

    在函数中回收内存的要点在于:谁申请的内存谁回收。

    类成员指针

    删除类的内存的要点在于“谁的指针谁回收”,而不是“谁申请的内存谁回收”。因而类只负责回收自己的指针,而不管其父类和支类(CCustomer对于CGroup而言是一个支类)的指针内存是否回收。这使得类的内存回收变得简单,即只需要关心当前类有哪些成员指针变量即可。

    类成员指针变量的内存回收一般在销毁器中进行,少量情况下需要在销毁之前删除内存的,可建立FreeMemory()函数,但需要在销毁器中也调用此函数。注意FreeMemoryAll()和销毁器的要求一般略有不同,FreeMemoryAll()是销毁器的一个子集。COODBQuery是个典型的例子。

    不过具体到每个成员指针变量比较复杂,有几种情况:

    ①指针是外部服务的地址,此指针不需要删除,在VC的CRecordset类应用中可看到:

CDatabase Database;
CRecordset *pRecordset=new CRecordset(&Database);

    可以看出此类中必有一个指针来存储&Database,但此指针肯定不需要删除内存的操作。

    为了能清除的发现此类指针,推荐用m_cp作为其前缀,表明此为Const Pointer。

    ②指针是内部存储数据,则需要删除。内部存储数据有两种,一种是普通指针,由类的某个函数(如构造器)申请内存;另外一种是PtrArray/List/Map的元素,在类的外部申请,通过Add函数加入,但也在类的销毁器中回收。例如:

template <class Class>
COODBQuery <Class>::~COODBQuery()
    {
    if (m_pOODBFields)
        delete []m_pOODBFields;
    m_pOODBFields=NULL;
    FreeMemoryAll();
    }

template <class Class>
void COODBQuery <Class>::FreeMemoryAll()
    {
    //cy: Free all memory.
    for (int i=m_ClassArray.GetSize()-1;i>=0;i--)
        delete m_ClassArray.GetAt(i);
    
    RemoveAll(); //cy: This function will transfer class pointers from ClassArray to ClearedArray.
    }

    可以看到,遵循“谁的指针谁回收”的原则,尽管m_ClassArray.GetAt(i)(实际上是一个从COODBRecord继承的类的指针,如CCustomer)中可能还有指针变量,但其回收工作已经交由Class销毁器完成。这样封装的好处是无论Class的内部结构如何,是否发生变化,只要在Class销毁器中做好内存回收工作即可。

    全局指针,静态指针

    由于全局指针首先是一个全局变量,所以一般情况下会随着程序结束而自动被释放内存,但为了在debug下不会因此而混淆程序中其他未得到回收的内存,建议进行内存回收,其位置在ExitInstance()函数内进行。

    大部分全局指针都是静态指针,所以在命名上没有特殊要求,用g_p作为前缀即可。

3.数据结构类 Data structure classes 

3.1 普通数组与数组类 Traditional array and classes for array 

    数组越界是程序内存出错的重要原因,VC的CString和Collections类族就是为了解决这个问题而创建的。学习和掌握其应用对于提高程序的稳定性和可维护性有很大帮助。

    传统数组与VC类的对应如下:

传统数组 VC++类
char[]|char* CString
char*[]|char**|CString[] CStringArray|CStringList
void[]/Bype[]/Word[]/DWord[] CPtrArray/CByteArray/CWordArray/CDWordArray
int[]/long[]/hyper[] CArray <int,int>/CArray <long,long>/CArray <hyper,hyper>
Class[] CArray <Class,Class&>, CTypedPtrArray <CArray|CPtrArray|CObArray,Class|Class*|Class*>或相应的List/Map类,具体场合参考下面的整体比较

    Collections类族对数据安全性作了相当多的处理,因而象下面所示的操作中,完全不需要检查数据合理性:

    CObArray <CPeople,CPeople&> PeopleArray;
    ... //cy: Add or change or delete elements.
    for (int i=0;i<PeopleArray.Size();i++)
        {
        AfxMessageBox(PeopleArray.GetAt(i).Name());
        }

    尽管CString使用安全方便,但其工作速度却有限,对CString的所有操作(包括构造器和销毁器)在1E6次时均达到0.1~1秒左右,因而在一些数据库设计时要注意,特别是利用CString做大量使用的函数的临时变量是不可取的。

3.2 区别与应用 Difference and using: Array,List,Map 

    Collections类族主要包括三种形式及其不同应用:

    Array — CArray, CObArray, CStringArray, CPtrArray, CByteArray, CWordArray, CDWordArray, CTypedPtrArray

    List — CList, CObList, CStringList, CPtrList

    CMap — CMap, CTypedPtrMap, CMapPtrToPtr, CMapPtrToWord, CMapWordToPtr, CMapStringToPtr

整体比较

属性 类族 特点
内存结构 Array 在VC源码CArray::SetSize()中可看到:
TYPE* pNewData = (TYPE*) new BYTE[nNewMax * sizeof(TYPE)];
// copy new data from old
memcpy(pNewData, m_pData, m_nSize * sizeof(TYPE));
// construct remaining elements
ASSERT(nNewSize > m_nSize);
ConstructElements<TYPE>(&pNewData[m_nSize], nNewSize-m_nSize);
// get rid of old stuff (note: no destructors called)
delete[] (BYTE*)m_pData;
m_pData = pNewData;

而在RemoveAt()中则有:
int nMoveCount = m_nSize - (nIndex + nCount);
DestructElements<TYPE>(&m_pData[nIndex], nCount);
if (nMoveCount)
memmove(&m_pData[nIndex], &m_pData[nIndex + nCount],
nMoveCount * sizeof(TYPE));
m_nSize -= nCount;

可见Array采用队列方式存储数据,
因而其内部数据元素是以物理方式顺序排列的,所以检索、顺序执行GetAt()等函数的速度是相当快的。但是由于每次队列长度变化后,数据都要重新申请内存、拷贝内存、释放内存,因而Insert/Add/RemoveAt()的速度都很慢。

如果你使用的数据元素尺寸相当大,而且数组的操作相当复杂,频繁(1E4以上的)使用InsertAt/SetAt/RemoveAt等,应该考虑使用CList来代替。

一个特例是PtrArray/CTypedPtrArray,其内部数据是某个数据地址,而非数据本身,所以其效率更接近CList。

List 在VC源码CList::AddTail()中可看到:
CNode* pNewNode = NewNode(m_pNodeTail, NULL);
pNewNode->data = newElement;
    if (m_pNodeTail != NULL)
m_pNodeTail->pNext = pNewNode;

因而List采用链表方式存储数据,因而当链表数据有所变动时,只做了一下指向变动,所以即使数据元素非常多单个数据元素也很大,执行Insert/Add/Remove的速度都很快,但是因为没有统一的Index,因而如果要找到某个元素只有遍历整个链表。

整体上说,List的使用比较繁琐,特别为小尺寸数据设计List更是得不偿失的,这也是为什么有CWordArray而没有CWordList的原因,因而在大多数情况下应该有限考虑是否可以使用Array来存储数据。

Map Map采用杂凑(Hashing)表方式来存储和检索数据,其内容包括Key和Value的双列表及其之间的映射关系表。例如在OODBLib用到的::FhtRegister <Class>()函数,在内部使用了Key=Class名称/Value=Class与数据库列的映射,从而在使用Class时只需要知道类的名字即可找到与之对应的数据库映射方法。

在下面的Characteristics表中可看到,Key的数值不可重复,即只能是唯一的(例如在上例中同一个程序中不可能有两个相同名字的类的声明),而Value可重复(不同名字的类可有完全相同的变量和函数)。

尽管杂凑表使用了高效的检索方式,大量使用CMap::LookUp()函数还是很耗时的,400 000次LookUp()执行时间大约是1秒,而同样次数的CObArray::GetAt()函数只需要0.3秒。

应用技巧 Array ①如果在你所使用的场合中,出现下列情况之一:a.数据元素的尺寸很大b.经常需要执行Add函数(例如数据库在执行Select语句时) 则需要事先执行SetSize(int nSize,int nGrowBy)函数,事先确定大致尺寸(之后仍可增加)及每次增加时一次申请的内存数,从而避免频繁执行内存申请、复制和删除工作。
②所有的命名中含Ptr的类都不进行删除内存的工作,需要自己完成。
③如果在循环中涉及RemoveAt()函数,循环一定要从尾部开始,否则可能漏掉元素,例:
CTypedPtrArray <CObArray,CPeople*> PeopleArray;
for (int i=PeopleArray.GetSize()-1;i>=0;i--)
    {
    if (PeopleArray.GetAt(i)->Name()=="Tom")
        {
        delete PeopleArray.GetAt(i);
        PeopleArray.RemoveAt(i); //cy: 如果从i=0循环,则删除i后会漏掉对i-1的检查。
        }
    }
List (暂缺)
Map ①由于LookUp的执行速度比较慢,如果必须进行映射,可在第一此LookUp时将其地址获得并记录,此后可直接使用。参考OODBLib/COODBQuery::COODBQuery(),注意
m_cpOODBCDMap=::FhtOODBCDMapOf(aClass.CDMapName());
语句完成了上述操作。

    下面是MSDN中Collections: Choosing a Collection Class章节的一些性能列表及注释。

Collection Shape Features

Shape Ordered? Indexed? Insert an element Search for specified element Duplicate elements?
List Yes No Fast Slow Yes
Array Yes By int Slow Slow Yes
Map No By key Fast Fast No (keys)
Yes (values)

Characteristics of MFC Collection Classes
Class Uses C++ templates Can be serialized Can be dumped Is type-safe
CArray Yes Yes 1 Yes 1 No
CByteArray No Yes Yes Yes 3
CDWordArray No Yes Yes Yes 3
CList Yes Yes 1 Yes 1 No
CMap Yes Yes 1 Yes 1 No
CMapPtrToPtr No No Yes No
CMapPtrToWord No No Yes No
CMapStringToOb No Yes Yes No
CMapStringToPtr No No Yes No
CMapStringToString No Yes Yes Yes 3
CMapWordToOb No Yes Yes No
CMapWordToPtr No No Yes No
CObArray No Yes Yes No
CObList No Yes Yes No
CPtrArray No No Yes No
CPtrList No No Yes No
CStringArray No Yes Yes Yes 3
CStringList No Yes Yes Yes 3
CTypedPtrArray Yes Depends 2 Yes Yes
CTypedPtrList Yes Depends 2 Yes Yes
CTypedPtrMap Yes Depends 2 Yes Yes
CUIntArray No No Yes Yes 3
CWordArray No Yes Yes Yes 3

1. To serialize, you must explicitly call the collection object’s Serialize function; to dump, you must explicitly call its Dump function. You cannot use the form ar << collObj to serialize or the form dmp << collObj to dump.

2. Serializability depends on the underlying collection type. For example, if a typed pointer array is based on CObArray, it is serializable; if based on CPtrArray, it is not serializable. In general, the “Ptr” classes cannot be serialized.

3. If marked Yes in this column, a nontemplate collection class is type-safe provided you use it as intended. For example, if you store bytes in a CByteArray, the array is type-safe. But if you use it to store characters, its type safety is less certain.

Collections类族函数调用的效率都相当高,如果在需要极端考虑效率时可参考此表

3.3 示例 Sample: CTypedPtrArray 

    CTypedPtrArray

    前面提到,在使用CArray时将会遇到在Add/InsetAt/Remove时的效率问题,而CList的检索效率又比较低,VC的Collections中的Ptr系列类可以中和两者的优点。

    Ptr系列类包括CPtrArrayCTypedPtrArray,前者可以存储任意指针,而后者则可以对指针的类型进行进行检查,保证其为统一类型,在取出时也不需要进行类型转换,比较方便。大部分情况下我们只在一个队列中存储一种对象,因而下面我们以CTypedPtrArray为例演示其使用方法。

//cy: People.h
class CPeople : public CObject
    {
    DECLARE_SERIAL (CPeople) //cy: This will enable Serialize().
    public:
        CString m_strName;
        long m_lAge;

        CPeople(LPCTSTR lpszName, long lAge);
        ~CPeople();

        virtual void Serialize(CArchive &ar);
    }

//cy: People.cpp
IMPLEMENT_SERIAL(CPeople,CObject,1) //cy: Seriaize CPeople as a CObject with class version 1.

void CPeople::Serialize(CArchive &ar)
    {
    if (ar.IsStoring()) //cy: Save.
        ar<<m_strName<<m_lAge;
    else
        ar>>m_strName>>m_lAge;
    }

    //cy: XView.cpp
    #include "AfxTempl.h" //cy: CTypedPtrArray defined here.

    {
    CPeople *pTom,*pJerry,*pPeople;
    CTypedPtrArray <CObArray,CPeople*> Peoples;
    
    pPeople=new CPeople("Tom",13");
    Peoples.Add(pPeople); //cy: Now the array is {Tom}.
    pPeople=new CPeople("Jerry",12); //cy: You can use same pointer variable again.
    Peoples.Add(pPeople); //cy: Now the array is {Tom, Jerry}.
    CPeople Mike("Mike",11);
    Peoples.Add(&Mike); //cy: Wrong! Address of a normal data will be invalid when it is out of effect field. Now the array is temporarily {Tom, Jerry, Mike}.

    CArchive ar; 
    //... Code to create ar as save mode.
    Peoples.Serialize(ar); //cy: Data of Tom/Jerry/Mike will be save into the the file.
    
    pTom=m_Peoples.GetAt(0);
    pJerry=m_Peoples.GetAt(1);
    pMike=m_Peoples.GetAt(2);
    
    Peoples.RemoveAt(0); //cy: Tom's CLASS POINTER is not in the array now. Now the array is {Jerry,Mike}.
    AfxMessgaeBox(pTom->m_strName); //cy: Right. Thought pointer of Tom is not in the array now, it's memory is not freed.

    //cy: PtrArray will not delete memory you allocated, delete them yourself.
    for (int i=0;i<Peoples.GetSize();i++) 
        delete Peoples.GetAt(i); //cy: When you delete Mike, memory error will occurs.
    delete pTom; //cy: pTom is not in the array now, you need to delete it.
    }