CArray的类成员函数和使用方法详解

CArray基础

C++并不支持动态数组,MFC提供了一个CArray类来实现动态数组的功能。有效的使用CArray类,可以提高程序的效率。MFC提供了一套模板库,来实现一些比较常见的数据结构如Array,List,Map。CArray即为其中的一个,用来实现动态数组的功能。

一、CArray类的构造函数

CArray是从CObject派生,有两个模板参数,第一个参数就是CArray类数组元素的变量类型,后一个是函数调用时的参数类型。有一个类 class Object,要定义一个Object的动态数组,那么可以用以下两种方法:

CArray Var1;

CArray Var2;

Var2的效率要高。举例说明如下:

 CArray m_Array;

该语句定义一个CArray数组对象,模板类CArray有两个参数,第一个参数为数组元素的类型,该例中是CPoint,即m_Array是 CPoint数组;第二个参数为引用类型,一般有两种选择,一种选择与第一个参数类型相同,它意味着数组对象作为参数传递时,传递的是数组对象。第二种选择是第一个参数类型的引用,它意味着数组对象作为参数传递时,传递的是数组对象的指针。因此,尤其对于较复杂的数组结构类型,推荐使用引用传递,节约内存同时加快程序运行速度,正如本例使用的是CPoint&。

二、CArray类成员函数

1. 属性
       GetSize()获得此数组中的元素数
       GetUpperBound()返回最大的有效索引值
       SetSize()设置包含在此数组中的元素数

2. 操作
       FreeExtra()释放大于当前上界的未使用的内存 
       RemoveAll()从此数组移去所有元素

3. 元素访问 
       GetAt()返回在给定索引上的值
       SetAt()设定一个给定索引的值;数组不允许扩展
       ElementAt()返回一个对数组中元素指针的临时参考
       GetData()允许对数组中的元素访问。可以为NULL

4. 扩展数组
      SetAtGrow()为一个给定索引设置值;如果必要,扩展数组
      Add()在数组的末尾添加元素;如果必要,扩展数组 
      Append()在数组上附加另一个数组;如果必要,扩展数组
      Copy()把另一个数组拷贝到数组上;如果必要,扩展数组

5. 插入/移去
       InsertAt()在指定的索引上插入一个元素(或另一个数组中的所有元素)
       RemoveAt()在指定的索引上移去一个元素

6. 运算符
       [ ]在特定索引上设置或获取元素

三、CArray类使用举例
       1. 一条良好的使用原则:

在使用一个数组之前,使用SetSize建立它的大小和为它分配内存。如果不使用SetSize,则为数组添加元素就会引起频繁地重新分配和拷贝。频繁地重新分配和拷贝不但没有效率,而且导致内存碎片。

2. Add()和SetSize()的使用举例

CArray m_Array; m_Array.SetSize(10,10); CPoint pt1(10,10); m_Array.Add(pt1); CPoint pt2(10,50); m_Array.Add(pt2); CPoint pt3(10,100); m_Array.Add(pt3); int size=m_Array.GetSize();

SetSize()函数设定数组的大小,该函数有两个参数,

(1) 第一个参数设定数组的大小;

(2) 第二个参数设定数组增长时内存分配的大小,缺省值是-1,使用缺省值可以保证内存分配得更合理。

本例中第二个参数是10,意即增加一个数组元素会分配10个元素大小的内存供数组使用。

您可以随时使用SetSize函数设定数组的大小,如果第一个参数值小于数组已有成员数量,多于第一个参数值的成员将被截去并释放相应内存。

再次强调:在使用CArray数组前,最好先使用SetSize确定其大小并申请存储空间。如果不这样做,向数组中增加元素时,需要不断地移动和拷贝元素造成运行的低效率和内存碎块。

3. SetAtGrow()和SetAt()函数

SetAtGrow有两个参数,第一个参数决定数组元素的序号值,第二个参数是元素的值。该函数根据序号值设置相应数组元素的值,功能与SetAt相近,不同之处是使用该函数设置元素值时,如果序号值大于数组的上界,数组会自动增长。举例如下:

CArray m_string; CString sztiger("tiger"); CString szbear("bear"); CString szdog("dog"); m_string.SetAtGrow(0,sztiger); m_string.SetAtGrow(2,szdog); m_string.InsertAt(1,szbear); int count=m_string.GetSize();

编译运行程序,细心的读者您可能会看到,输出结果如下:

第一行字符是“tiger”,第二行字符是“bear”,这是我们预料之中的,但第三行是空串,第四行是“dog”。空串是怎样造成的呢?细分析下面三行代码就可以知道:

m_string.SetAtGrow(0,sztiger);

m_string.SetAtGrow(2,szdog);

m_string.InsertAt(1,szbear);

第一行设定元素0为“tiger”,这是没有疑义的。

第二行设定元素2为“dog”,但是在设定元素2的同时自动将元素1填充为空串。

第三行插入“bear”为元素1,同时原来的元素1和元素2后移为元素2和元素3。

怎么样,这回明白了吧。

4. RemoveAt()和InsertAt()函数

InsertAt函数在指定序号处插入相应元素,该函数在执行过程中,插入点后面的元素会自动后移。

RemoveAt只有一个参数,即元素序号值。该函数根据元素序号值删除相应元素值,后面的元素会自动前移。

最后再说明一点:RemoveAt,InsertAt函数操作时会使得数组元素移位,运行时间大于SetAt,RemoveAll,Add函数。

CArray使用详解

MFC的数组类支持的数组类似于常规数组,可以存放任何数据类型。常规数组在使用前必须将其定义成能够容纳所有可能需要的元素,即先确定大小,而MFC数组类创建的对象可以根据需要动态地增大或减小,数组的起始下标是0,而上限可以是固定的,也可以随着元素的增加而增加,数组在内存中的地址仍然是连续分配的。
  MFC定义了数组模板类CArray,并针对各种常用变量类型定义了CByteArray,CArray,CUIntArray,CDArray,CStringArray,CObArray,CPtrArray。详见下表: 数组类
变量类型
变量数值范围
   头文件

CArray
通过模板类的参数类型设定各种类型 
      Afxtempl.h

CByteArray
8位无符号整数 BYTE类型
0—255
       Afxcoll.h

CArray
16位无符号整数 WORD类型
0—65535
       Afxcoll.h

CDArray
32位无符号整数 DWORD类型
0—4294967295
       Afxcoll.h

CUIntArray
32位无符号整数 UINT类型
0—4294967295
      Afxcoll.h

CStringArray
CString字符串 string字符串 
      Afxcoll.h

CObArray
CObject类及其派生类  
      Afxcoll.h

CPtrArray
void* 类型指针 
      Afxcoll.h


MFC数组类使用方法基本相同,下面分别以CArray和CUIntArray为例演示说明数组类的使用方法。

使用 CArray 打开VC++ 6.0,创建基于对话框的工程Array。CArrayDlg类声明文件(ArrayDlg.h)中添加语句:

#include

 

请记住:使用CArray一定要包含头文件afxtempl.h。

打开主对话框资源IDD_ARRAY_DIALOG,添加一个按钮IDC_ARRAY_CPOINT,标题为CArray_CPoint,双击该按钮,在OnArrayCpoint()函数中添加如下代码:

void CArrayDlg::OnArrayCpoint() { CArray m_Array; m_Array.SetSize(10,10); CPoint pt1(10,10); m_Array.Add(pt1); CPoint pt2(10,50); m_Array.Add(pt2); CPoint pt3(10,100); m_Array.Add(pt3); int size=m_Array.GetSize(); CClientDC dc(this); dc.MoveTo(0,0); CPoint pt; for(int i=0;i

代码简要说明:

CArray m_Array;

  该语句定义一个CArray数组对象,模板类CArray有两个参数,第一个参数为数组元素的类型,该例中是CPoint,即m_Array是CPoint数组;第二个参数为引用类型,一般有两种选择,一种选择与第一个参数类型相同,它意味着数组对象作为参数传递时,传递的是数组对象。第二种选择是第一个参数类型的引用,它意味着数组对象作为参数传递时,传递的是数组对象的指针。因此,尤其对于较复杂的数组结构类型,推荐使用引用传递,节约内存同时加快程序运行速度,正如本例使用的是CPoint&。

m_Array.SetSize(10,10);

  SetSize函数设定数组的大小,该函数有两个参数,第一个参数设定数组的大小;第二个参数设定数组增长时内存分配的大小,缺省值是-1,使用缺省值可以保证内存分配得更加合理。本例中第二个参数是10,意即增加一个数组元素会分配10个元素大小的内存供数组使用。
  您可以随时使用SetSize函数设定数组的大小,如果第一个参数值小于数组已有成员数量,多于第一个参数值的成员将被截去并释放相应内存。
  在使用CArray数组前,最好先使用SetSize确定其大小并申请存储空间。如果不这样做,向数组中增加元素时,需要不断地移动和拷贝元素造成运行的低效率和内存碎块。

m_Array.Add(pt1);

Add函数添加数组元素。

int size=m_Array.GetSize();

GetSize返回数组元素的数目。

for(int i=0;i

  为了直观显示,该段代码将各数组元素作成折线画到屏幕上,其中GetAt(int index)通过index值得到相应的元素值。编译并运行程序,观察运行结果。

继续演示如何使用CArray

  再次打开主对话框资源IDD_ARRAY_DIALOG,添加一个按钮IDC_ARRAY_CSTRING,标题为CArray_CString,双击该按钮,在OnArrayCstring ()函数中添加如下代码:

void CArrayDlg::OnArrayCstring() { CArray m_string; CString sztiger("tiger"); CString szbear("bear"); CString szdog("dog"); m_string.SetAtGrow(0,sztiger); m_string.SetAtGrow(2,szdog); m_string.InsertAt(1,szbear); int count=m_string.GetSize(); CClientDC dc(this); dc.SetBkMode(TRANSPARENT); TEXTMETRIC textMetric; dc.GetTextMetrics(&textMetric); int fontHeight=textMetric.tmHeight; int displayPos=10; for(int x=0;x

代码简要说明:

m_string.SetAtGrow(2,szdog);

  SetAtGrow有两个参数,第一个参数决定数组元素的序号值,第二个参数是元素的值。该函数根据序号值设置相应数组元素的值,功能与SetAt相近,不同之处是使用该函数设置元素值时,如果序号值大于数组的上界,数组会自动增长。
  编译运行程序,细心的读者您可能会看到,第一行字符是“tiger”,第二行字符是“bear”,这是我们预料之中的,但第三行是空串,第四行是“dog”。空串是怎样造成的呢?细分析下面三行代码就可以知道:

m_string.SetAtGrow(0,sztiger);m_string.SetAtGrow(2,szdog);m_string.InsertAt(1,szbear);

第一行设定元素0为“tiger”,这是没有疑义的。
第二行设定元素2为“dog”,但是在设定元素2的同时自动将元素1填充为空串。
第三行插入“bear”为元素1,同时原来的元素1和元素2后移为元素2和元素3。

怎么样,这回明白了吧。

m_string.InsertAt(1,szbear);

  InsertAt函数在指定序号处插入相应元素,该函数在执行过程中,插入点后面的元素会自动后移。dc.TextOut(10,displayPos,m_string[x]);其中,m_string[x]是数组类对操作符[]的重载,数组类CArray允许使用[]操作符,类似于的常规数组。m_string[x]也可以用m_string.GetAt(x)替代。

m_string.RemoveAt(2);

RemoveAt只有一个参数,即元素序号值。该函数根据元素序号值删除相应元素值,后面的元素会自动前移。

m_string.RemoveAll();

RemoveAll删除所有元素值

  最后再说明一点:RemoveAt,InsertAt函数操作时会使得数组元素移位,运行时间大于SetAt,RemoveAll,Add函数。

 

从std::vector< std::string > 和 CArray< std::string >的性能差别

 

std::vector在移动元素时是挨个调用元素的拷贝函数。
CArray在移动元素时则是直接使用memcopy 和 memmove。

操作方式的不同导致了它们极大的性能差异。
我使用随机方式在std::vector和CArray
里插入字符串,经过测试,CArray平均比std::vector快十几倍。

经分析发现,所有的c++类[类型]都可以分为简单和复杂两种类型。
简单类型定义:类的成员不可能引用该类对象自己的内存。否则为复杂类。
任何从复杂类派生或包含复杂类成员的类都是复杂类。
例如,所有C++基本数据类型、std::string、std::vector、MFC的CString
及CArray等等都是简单类型。

下面的类则是复杂类:
class CA {
char _buf[10];
char* _p;
public:
CA() : _p(_buf) {}
};

特别的,对于简单类型,以下操作是安全的:

struct CSimp
{
char* _p;

CSimp()
{
_p = new char[100];
strcpy(_p, "constructed");
}
~CSimp()
{
delete[] _p;
}
};

void test()
{
CSimp* p = new CSimp();
CSimp* p2 = (CSimp*)new char[sizeof(CSimp)];
memcpy(p2, p, sizeof(CSimp));

// 以后可以把p2当作CSimp*安全的使用,但p2必须析构
// 但p以后就不能再安全的使用了,也不能析构,但只需释放内存即可
strcpy(p2->_p, "use it");
// ...

delete p2;
delete (char*)p;
}

这就是CArray中在插入、删除[简单类型的]元素时可以不调用
元素的拷贝函数的设计原理。

std::vector是按照通用类考虑设计的,所有类型的都可以安全的使用它。
CArray则只可以使用简单类型,使用复杂类型一定出错。

但在使用简单类型是显然CArray的方式效率要高得多,因为在插入、删除数
组成员时少去了大量的元素拷贝函数的调用。

所以当元素是复杂类型时,必须使用std::vector,当元素是简单类型是,则
应该使用CArray,如果不是MFC环境,可以弄个和CArray同样设计原理的数组。

CArray如何使用

首先大家应该知道MFC为我们提供了一个极其有用的模板类库,但是很多初学者,不但对这个库不太了解,甚至就连模板的含义,都成问题,所以,在此,我先不谈具体的模板定义,只告诉大家如何简单的使用一个模板类。

    先以CArray为例,下面是我编写的一小段测试代码:

    struct Node

    {

        int index;

        char name[16];

    };

    以下是类模板实现为一个类的标准语法。其中,Node是出参,Node&是入参,其实也就是说:当你使用Add函数的时候,所传入的参数是后者,也就是Add(Node& node)这样的形式,而当你GetAt()取出一个元素的时候,返回的是一个Node结构体。  typedef CArray CArrNode; //实现一个以Node为元素的数组类 //入参和出参都是一个指向Node的指针 typedef CArray CArrNodePtr; //元素是Node*,一个指针 int main() { CArrNode arrNode; Node node; node.index = 1; strcpy(node.name, "first"); arrNode.Add(node); //向动态数组中添加一个元素 size_t size = arrNode.GetSize(); for(size_t i = 0; i < size; i++){ //打印出来,看看是否正确 printf("index: %d, name: %s/n", arrNode[i].index, arrNode[i].name); } CArrNodePtr arrNodePtr; Node* pnode = new Node; assert(pnode); pnode->index = 2; strcpy(pnode->name, "second"); arrNodePtr.Add(pnode); //向数组中添加一个元素,这个元素是个指针,指向堆内存 size_t nsize = arrNodePtr.GetSize(); for(size_t j = 0; j < nsize; j++){ printf("index: %d, name: %s/n", arrNodePtr[j]->index, arrNodePtr[j]->name); } //注意:在使用指针作为CArray元素时,必须在必要的时候释放曾经申请的内存,否则将会造成严重的内存泄露。 for(j = 0; j < nsize; j++){ if( arrNodePtr[j] ){ delete arrNodePtr[j]; arrNodePtr[j] = NULL; } } arrNodePtr.RemoveAll(); return 0; }  

这段代码,所描述的是最简单的CArray的用法,如果插入CArray中的元素不是一个C格式的结构体,或者内置类型的话,你必须要给这个元素(可能是一个类)追加必要的构造函数和析构函数。下面,我们来看另外的一个例子:

    假如,我们建立这样的一个动态数组,它会稳定良好的运行吗?CStrx是一个字符串类,它将会完成一些类似CString的功能。

    class CStrx

    {

    public:

        CStrx() :m_pdata(NULL), m_size(0) //初始化成员

    {

    }

        ~CStrx(){}

    private:

        char* m_pdata;

        size_t m_size;

    };

    typedef CArray                    CArrStrx;

    很遗憾,它无法按照你的意图去安全的工作,原因都在CArray的内部,暂时,我先不铺开,但我要说明的就是,在CArray的内部,需要调用元素的赋值构造函数,那么为什么我们刚刚在上个例子中,却没有为Node建立一个赋值构造函数呢?原因很简单,那就是,如果,你不提供赋值构造函数的话,C++编译器将会默认按位拷贝,而我们给出的是一个规则的结构体,并且不存在指针(像本例中的m_pdata,它可能指向一块动态分配的内存),所以不会出现问题,但一旦需要向本例这样需要深拷贝的话,那么CArray将会无法工作。所以,我们必须要为这个类,提供一个赋值构造函数。这个函数可以像这样:

    CStrx& operator=CStrx(const CStrx& str)

    {

        ……

        return *this;

    }

    然而,我们为什么要用指针作为元素传入CArray呢?其实,主要是为了效率的考虑,如果你传入的是一个对象,或者对象的引用,那么CArray将不得不动用你的赋值构造函数,可能会进行大规模的数据赋值,然而对于指针来说,它就相当于一个整数,效率将会显著提高,但是入CArray析构之前,你必须要对这些元素进行释放。

你可能感兴趣的:(STL)