C语言学习------很好很强大

2005年02月05日
c/c++的参数压栈顺序

曾经看到一篇文章上面说:c/c++参数压栈顺序是从右到左,pascal参数压栈是从左到右.

为了这句话丢了很多次人.无所谓了,反正咱脸皮厚.

   总结一下:   编译出来的c/c++程序的参数压栈顺序只和编译器相关!
   
下面列举了一些常见的编译器的调用约定

VC6:


        调用约定        堆栈清除    参数传递
        __cdecl                  调用者      从右到左,通过堆栈传递
        __stdcall                  函数体         从右到左,通过堆栈传递
        __fastcall                 函数体         从右到左,优先使用寄存器(ECX,EDX),然后使用堆栈
        thiscall                     函数体         this指针默认通过ECX传递,其它参数从右到左入栈


__cdecl是C/C++的默认调用约定; VC的调用约定中并没有thiscall这个关键字,它是类成员函数默认调用约定;
C/C++中的main(或wmain)函数的调用约定必须是__cdecl,不允许更改;
默认调用约定一般能够通过编译器设置进行更改,如果你的代码依赖于调用约定,请明确指出需要使用的调用约定;

C++Builder6:


        调用约定        堆栈清除    参数传递
        __fastcall                  函数体      从左到右,优先使用寄存器(EAX,EDX,ECX),然后使用堆栈 (兼容Delphi的register)
        (register与__fastcall等同)
        __pascal                     函数体      从左到右,通过堆栈传递
        __cdecl                    调用者    从右到左,通过堆栈传递(与C/C++默认调用约定兼容)
        __stdcall                     函数体      从右到左,通过堆栈传递(与VC中的__stdcall兼容)
        __msfastcall                函数体      从右到左,优先使用寄存器(ECX,EDX),然后使用堆栈(兼容VC的__fastcall)

上述资料来源于HouSisong的一篇文章:http://www.allaboutprogram.com/index.php?option=content&task=view&id=29&Itemid=31.
由于能力和资源有限,只能找到这些东西,主要的差异体现在fastcall上面,vc是前两个参数放入寄存器,后面的压栈,bcb是前3哥参数使用寄存器,
更有变态的,一个朋友朋友说有的参数超过7个以后前5个从左到右传递,后面的从右到走,上面说的不可不信,不可全信.
如何确定你的编译采用的那种顺序那?
#include
int f(int i,int j,int k);
int main()
{
static int i=0;
f(i++,i++,i++);
return 0;
}

int f(int i,int j,int k)
{
int l;
int g;
printf(“k=%d:[%x]/n”,k,&k);
printf(“j=%d:[%x]/n”,j,&j);
printf(“i=%d:[%x]/n”,i,&i);
printf(“___________/n”);
printf(“l:%x/n”,&l);
printf(“g:%x/n”,&g);
}
看看k->i的地址的增长顺序和l->g的顺序是否相同,如果相同则是从右到左,否则从左到右.
PS:
   本来通过打印参数的值来判断那个先入栈,结果被一个朋友批评了,
   他说:压栈顺序和参数计算顺序不是一回事,所以还是看地址更有保证.

内存管理知识

内存管理向来是C/C++程序设计的一块雷区,大家都不怎么愿意去碰她,但是有时不得不碰它。虽然利用C++中的smart pointer已经可以完全避免使用指针,但是对于对于指针的进一步了解,有助于我们编写出更有效率的代码,也有助于我们读懂以前编写的程序。

    五大内存分区
    在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
    栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区。里面的变量通常是局部变量、函数参数等。
    堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
    自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
    全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
    常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多,在《const的思考》一文中,我给出了6种方法)
   
    明确区分堆与栈
    在bbs上,堆与栈的区分问题,似乎是一个永恒的话题,由此可见,初学者对此往往是混淆不清的,所以我决定拿他第一个开刀。
    首先,我们举一个例子:
    void f() { int* p=new int[5]; }
    这条短短的一句话就包含了堆与栈,看到new,我们首先就应该想到,我们分配了一块堆内存,那么指针p呢?他分配的是一块栈内存,所以这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针p。在程序会先确定在堆中分配内存的大小,然后调用operator new分配内存,然后返回这块内存的首地址,放入栈中,他在VC6下的汇编代码如下:
    00401028   push        14h
    0040102A   call        operator new (00401060)
    0040102F   add         esp,4
    00401032   mov         dword ptr [ebp-8],eax
    00401035   mov         eax,dword ptr [ebp-8]
    00401038   mov         dword ptr [ebp-4],eax
    这里,我们为了简单并没有释放内存,那么该怎么去释放呢?是delete p么?澳,错了,应该是delete []p,这是为了告诉编译器:我删除的是一个数组,VC6就会根据相应的Cookie信息去进行释放内存的工作。
    好了,我们回到我们的主题:堆和栈究竟有什么区别?
    主要的区别由以下几点:
    1、管理方式不同;
    2、空间大小不同;
    3、能否产生碎片不同;
    4、生长方向不同;
    5、分配方式不同;
    6、分配效率不同;
    管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。
    空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。当然,我们可以修改:   
    打开工程,依次操作菜单如下:Project->Setting->Link,在Category 中选中Output,然后在Reserve中设定堆栈的最大值和commit。
注意:reserve最小值为4Byte;commit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。
    碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。
    生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
    分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
    分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
    从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址, EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。
    虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。
    无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,那时候debug可是相当困难的:)
    对了,还有一件事,如果有人把堆栈合起来说,那它的意思是栈,可不是堆,呵呵,清楚了?

浮点数的比较

在数学运算当中经常会涉及到判断两个数是否相等的情况
对于整数很好处理 A==B这样的一个语句就可以解决全部的问题
但是对于浮点数是不同的

首先,浮点数在计算机当中的二进制表达方式就决定了大多数浮点数都是无法精确的表达的
现在的计算机大部分都是数字计算机,不是模拟机,数字机的离散化的数据表示方法自然无法精确表达大部分的数据量的。

其次计算机浮点数的精度在单精度float类型下,只有7位,在进行浮点运算的时候,这个精度往往会导致运算的结果和实际期望的结果之间有误差

因为前两个原因,我们很难用 A==B来判定两个浮点数是否相同

很自然,我们可以想到  fabs(A-B) < epsilon 这样的一种判别方法
但是这种判别方法稳妥吗?
它也不稳妥。

首先, epsilon是一个绝对的数据,也就是误差分析当中说说的绝对误差
使用一个固定的数值,对于float类型可以表达的整个数域来说是不可以的
比如epsilon取值为0.0001,而a和b的数值大小也是0.0001附近的,那么显然不合适
另外对于a和b大小是10000这样的数据的时候,它也不合适,因为10000和10001也可以认为是相等的呢
适合它的情况只是a或者b在1或者0附近的时候

既然绝对误差不可以,那么自然的我们就会想到了相对误差
bool IsEqual(float a, float b, float relError ) {
       return ( fabs ( (a-b)/a ) < relError ) ? true : false;
}
这样写还不完善,因为是拿固定的第一个参数做比较的,那么在调用
IsEqual(a, b, relError ) 和 IsEqual(b, a, relError ) 的时候,可能得到不同的结果
同时如果第一个参数是0的话,就有可能是除0溢出
这个可以改造
把除数选取为a和b当中绝对数值较大的即可
bool IsEqual(float a, float b, relError )
{
      if (fabs(a) relError ) ? true : false;
      return  (fabs( (a-b)/b) > relError ) ? true : false;
};

使用相对误差就很完善吗?
也不是, 在某些特殊情况下, 相对误差也不能代表全部
比如在判断空间三点是否共线的时候,使用判断点到另外两个点形成的线段的距离的方法的时候
只用相对误差是不够的,应为线段距离可能很段,也可能很长,点到线段的距离,以及线段的长度做综合比较的时候,需要相对误差和绝对误差结合的方式才可以
相对完整的比较算法应该如下:
bool IsEqual(float a, float b, float absError, float relError )
{
         if (a==b) return true;
        if (fabs(a-b)        if (fabs(a>b) return  (fabs((a-b)/a>relError ) ? true : false;
       return  (fabs((a-b)/b>relError ) ? true : false;
}
这样才相对完整

C程序优化之路(三)

       本文讲述在编写C程序代码的常用优化办法,分为I/O篇,内存篇,算法篇。MMX本来我也想归在这里的,但是由于内容和标题不太符和,决定换一个名字,叫MMX技术详解,和H263视频压缩技术中的MMX应用两篇文章。

三.算法篇

       在上一篇中我们讲述了对内存操作的优化,这一篇则主要讲述一些常用的优化算法。这个东东太多,内容可能会有点凌乱,见谅。

I.从小处说起:

       先说说一些小地方先:

(1) 比如n/2写为n>>1这个是常用的方法,不过要注意的是这两个不是完全等价的!因为:如果n3的话,n/2=1;n>>1=1;但是,如果n-3的话,n/2=-1;n>>1=-2所以说在正数的时候,他们都是向下取整,但是负数的时候就不一样了。(在JPG2000中的整数YUVRGB变换一定要使用>>来代替除法就是这个道理)

(2) 还有就是a=a+1要写为a++;  a=a+b要写为a+=b(估计一般用VB的才会写a=a+1

(3) 将多种运算融合:比如a[i++];就是先访问a[i],再令i1;从汇编的角度上说,这个确实是优化的,如果写为a[i],和i++的话,有可能就会有两次的对i变量的读,一次写(具体要看编译器的优化能力了),但是如果a[i++]的话,就一定只读写i变量一次。不过这里有一个问题要注意:在条件判断内的融合一定要小心,比如:(idct变换中的0块判断,陈王算法)

  if (!((x1 = (blk[8*4]<<8)) | (x2 = blk[8*6]) | (x3 = blk[8*2]) | (x4 = blk[8*1]) | (x5 = blk[8*7]) | (x6 = blk[8*5]) | (x7 = blk[8*3])))

在条件判断中融合了赋值语句,但是实际上如果条件为真的话,是不需要这些赋值语句的,也就是说当条件真的时候,多了一些垃圾语句,这些是在h263源码上的问题,虽然这些垃圾语句使得计算0块的时候,时间增加了30%,但是由于idct仅仅占1%的时间,0块又仅仅30%~70%的时间,所以这些性能损失是没有什么关系的。(这是后来我用汇编改写源码的时候得到的结论)。这里也说明了,程序优化一定重点在最耗时的地方。对于不耗时的代码优化是没有太大的实用意义的。

II.以内存换速度:

天下总是难有双得的事情,编程也是一样,大多数情况,速度同内存(或者是性能,比如说压缩性能什么的)是不可兼得的。目前程序加速的常用算法一个大方面就是利用查表来避免计算(比如在jpghuffman码表,在YUVRGB变换也有变换表)这样原来的复杂计算现在仅仅查表就可以了,虽然浪费了内存,不过速度显著提升,还是很划算的。在数据库查询里面也有这样的思想,将热点存储起来以加速查询。 现在介绍一个简单的例子,(临时想的,呵呵):比如,在程序中要经常(一定要是经常!)计算10002000的阶乘,那么我们可以使用一个数组a[1000]先把这些值算好,保留下来,以后要计算1200!的时候,查表a[1200-1000]就可以了。

III.化零为整

       由于零散的内存分配,以及大量小对象建立耗时很大,所以对它们的优化有时会很有效果,比如上一篇我说的链表存在的问题,就是因为大量的零散内存分配。现在就从一个vb的程序说起,以前我用vb给别人编小程序的时候,(呵呵,主要是用vb编程比vc快,半天就可以写一个)在使用MSFlexGrid控件的时候(就是一个表格控件),发现如果一行一行的增加新行,刷新速度十分的慢,所以我就每次增加100行,等到数据多到再加新行的时候,再加100行,这样就“化零为整”了,使用这样的方法,刷新的速度比原来快了n倍!其实这样的思想应用很多,如:程序运行的时候,其实就占用了一定的空间,后来的小块内存分配是先在这个空间上的,这就保证了内存碎片尽可能的少,同时加快运行速度。

IV.条件语句或者case语句将最有可能的放在前面

       优化效果不明显。想得到就用吧,想不到就算了。

V.为了程序的可读性,不去做那些编译器可以做的或者优化不明显的处理:

       这个是很重要的,一个普通程序的好坏,主要是它的可读性,可移植性,可重用性,然后才是它的性能。所以,如果编译器本身可以帮助我们优化的话,我们就没有必要写那些大家都不怎么看得懂的东西。比如a52(结束)-16(起始);这样写可能是因为在别人读程序的时候,一下就明白了a的含义。我们不用写为a36,因为编译器是会帮我们算出来的。

IV.具体情况具体分析:

       具体情况具体分析,这是放之四海而皆准的真理。没有具体的分析,就不能针对问题灵活应用解决的办法。下面我就说说分析的方法。即如何找到程序的耗时点:(从最简单的办法说起,先说明一个函数GetTickCount(),这个函数在头尾各调用一次,返回值相减就是程序的耗时,精确到1ms

(1) 对于认为是比较耗时的函数,运行两次,或者将函数内部的语句注释掉(要保证程序可以运行),看看多(或者少了)多少时间。这个办法简单不精确。

(2) 每个地方都用GetTickCount()函数测试时间,注意GetTickCount()只能精确到ms。一般的小于10ms就不太精确了。

(3) 使用另外一个函数QueryPerformanceCounter&Counter)和QueryPerformanceFrequency(&Frequency),前面计算cpu时钟周期,后面是cpu频率相除就是时间。不过如果你要精确到这一步的话,建议将进程设置为最高级别,防止它被阻塞。

最后讲讲我处理的一个程序:程序要求我忘了,反正里面有一个函数,函数里面有一个大的循环,循环内部的处理比较耗时。结果最初程序表现出来的状况是开始还很快,越到后面越慢;我在跟踪程序中变量的时候,发现最初的循环在循环几次后就跳出了,而后面的循环次数越来越多。找到了为什么慢的原因,就可以对症下药了,我的处理是每次循环不是从头开始,而是从上一次循环跳出的地方开始左右循环(因为可能下一次循环跳出的地方别上一次的小,所以也要遍历前面的),这样程序的速度在后面也很快了。我讲这个的道理就是在实际运用中,要具体的分析程序慢的真正原因,才能达到最佳的优化效果。

C程序优化之路(二)

  本文讲述在编写C程序代码的常用优化办法,分为I/O篇,内存篇,算法篇,MMX汇编篇。

二.内存篇

       在上一篇中我们讲述了如何优化文件的读写,这一篇则主要讲述对内存操作的优化,主要有数组的寻址,指针链表等,还有一些实用技巧。

I.优化数组的寻址

       在编写程序时,我们常常使用一个一维数组a[M×N]来模拟二维数组a[N][M],这个时候访问a[]一维数组的时候:我们经常是这样写a[j×M+i](对于a[j][i])。这样写当然是无可置疑的,但是显然每个寻址语句j×M+i都要进行一次乘法运算。现在再让我们看看二维数值的寻址,说到这里我们不得不深入到C编译器在申请二维数组和一维数组的内部细节上――实际在申请二位数组和一维数组,编译器的处理是不一样的,申请一个a[N][M]的数组要比申请一个a[M×N]的数组占用的空间大!二维数组的结构是分为两部分的:

(1) 是一个指针数组,存储的是每一行的起始地址,这也就是为什么在a[N][M]中,a[j]是一个指针而不是a[j][0]数据的原因。

(2) 是真正的M×N的连续数据块,这解释了为什么一个二维数组可以象一维数组那样寻址的原因。(即a[j][i]等同于(a[0])[j×M+i])

清楚了这些,我们就可以知道二维数组要比(模拟该二维数组的)一维数组寻址效率高。因为a[j][i]的寻址仅仅是访问指针数组得到j行的地址,然后再+i,是没有乘法运算的!

    所以,在处理一维数组的时候,我们常常采用下面的优化办法:(伪码例子)

    int a[M*N];

    int *b=a;

    for(…){

    b[…]=…;

    …………

    b[…]=…;

    b+=M;

}

这个是遍历访问数组的一个优化例子,每次b+=M就使得b更新为下一行的头指针。当然如果你愿意的话,可以自己定义一个数组指针来存储每一行的起始地址。然后按照二维数组的寻址办法来处理一维数组。不过,在这里我建议你干脆就直接申请一个二维数组比较的好。下面是动态申请和释放一个二维数组的C代码。

int get_mem2Dint(int ***array2D, int rows, int columns)     //h.263源代码

{

int i;

 

if((*array2D      = (int**)calloc(rows,        sizeof(int*))) == NULL) no_mem_exit(1);

if(((*array2D)[0] = (int* )calloc(rows*columns,sizeof(int ))) == NULL) no_mem_exit(1);

 

for(i=1 ; i

(*array2D)[i] =  (*array2D)[i-1] + columns  ;

 

return rows*columns*sizeof(int);

}

void free_mem2D(byte **array2D)

{

if (array2D){

        if (array2D[0])  free (array2D[0]);

        else error (“free_mem2D: trying to free unused memory”,100);

        free (array2D);

    } else{

        error (“free_mem2D: trying to free unused memory”,100);

    }

}

顺便说一下,如果你的数组寻址有一个偏移量的话,不要写为a[x+offset],而应该为 b=a+offset,然后访问b[x]。

不过,如果你不是处理对速度有特别要求的程序的话,这样的优化也就不必要了。记住,如果编普通程序的话,可读性和可移值性是第一位的。

 

II.从负数开始的数组

       在编程的时候,你是不是经常要处理边界问题呢?在处理边界问题的时候,经常下标是从负数开始的,通常我们的处理是将边界处理分离出来,单独用额外的代码写。那么当你知道如何使用从负数开始的数组的时候,边界处理就方便多了。下面是静态使用一个从-1开始的数组:

int a[M];

int *pa=a+1;

现在如果你使用pa访问a的时候就是从-1M2了,就是这么简单。(如果你动态申请a的话,freea)可不要freepa)因为pa不是数组的头地址)

 

III.我们需要链表吗

       相信大家在学习《数据结构》的时候,对链表是相当熟悉了,所以我看有人在编写一些耗时算法的时候,也采用了链表的形式。这样编写当然对内存的占用(似乎)少了,可是速度呢?如果你测试:申请并遍历10000个元素链表的时间与遍历相同元素的数组的时间,你就会发现时间相差了百倍!(以前测试过一个算法,用链表是1分钟,用数组是4秒钟)。所以这里我的建议是:在编写耗时大的代码时,尽可能不要采用链表!

       其实实际上采用链表并不能真正节省内存,在编写很多算法的时候,我们是知道要占用多少内存的(至少也知道个大概),那么与其用链表一点点的消耗内存,不如用数组一步就把内存占用。采用链表的形式一定是在元素比较少,或者该部分基本不耗时的情况下。

(我估计链表主要慢是慢在它是一步步申请内存的,如果能够象数组一样分配一个大内存块的话,应该也不怎么耗时,这个没有具体测试过。仅仅是猜想 )

作者:liyuming1978

C程序优化之路(一)

  本文讲述在编写C程序代码的常用优化办法,分为I/O篇,内存篇,算法篇,MMX汇编篇。

一.I/O

  如果有文件读写的话,那么对文件的访问将是影响程序运行速度的一大因素。提高文件访问速度的主要办法有两个:一是采用内存映射文件,二是使用内存缓冲。下面是一组测试数据(见《UNIX环境高级编程》3.9节),显示了用18种不同的缓存长度,读1 468 802字节文件所得到的结果。

缓冲大小

用户CPU(秒)

系统CPU(秒)

时钟时间(秒)

循环次数(秒)

1

23.8

397.9

423.4

1 468 802

2

12.3

202.0

215.2

734 401

4

6.1

100.6

107.2

367 201

8

3.0

50.7

54.0

183 601

16

1.5

25.3

27.0

91 801

32

0.7

12.8

13.7

45 901

64

0.3

6.6

7.0

22 951

128

0.2

3.3

3.6

11 476

256

0.1

1.8

1.9

5 738

512

0.0

1.0

1.1

2 869

1 024

0.0

0.6

0.6

1 435

2 048

0.0

0.4

0.4

718

4 096

0.0

0.4

0.4

359

8 192

0.0

0.3

0.3

180

16 384

0.0

0.3

0.3

90

32 768

0.0

0.3

0.3

45

65 536

0.0

0.3

0.3

23

131 072

0.0

0.3

0.3

12

可见,一般的当内存缓冲区大小为8192的时候,性能就已经是最佳的了,这也就是为什么在H.263等图像编码程序中,缓冲区大小为8192的原因(有的时候也取2048大小)。使用内存缓冲区方法的好处主要是便于移植,占用内存少,便于硬件实现等。下面是读取文件的C伪码:

    int Len;

BYTE buffer[8192];

    ASSERT(buffer==NULL);

    If buffer is empty{

        Len=read(File,ld->rdbfr,8192);

        If(len==0) No data and exit;

    }

  但是如果内存比较大的时候,采用内存映射文件可以达到更佳性能,并且编程实现简单。内存映射的具体使用说明见msdn October 2001中的Platform SDK

Documentation—Base Services—File Storage—File Mapping。下面是一点建议:

(1) 内存映射文件不能超过虚拟内存的大小,最好也不要太大,如果内存映射文件接近虚拟内存大小的时候,反而会大大降低程序的速度(其实是因为虚拟内存不足导致系统运行效率降低),这个时候,可以考虑分块映射,但是我觉得如果这样,还不如直接使用内存缓冲来得直接一些。

(2) 可以将两种方法统一使用,如我在编大图像文件数据处理的时候(因为是Unix工作站,内存很大GB单位)使用了内存映射文件,但是为了最佳性能,也使用了一行图像缓存,这样在读取文件中数据的时候,就保证了仅仅是顺序读写(内存映射文件中,对顺序读写有专门的优化)。

(3) 在写文件的时候使用内存映射文件要有一点小技巧:应该先创建足够大的文件,然后将这个文件映射,在处理完这个文件的时候,用函数SetFilePointer和SetEndOfFile来对文件进行截尾。

(4) 对内存映射文件进行操作与对内存进行操作类似(使用起来就象数组一样),那么如果有大块数据读写的时候,切记使用memcpy()函数(或者CopyMemory()函数)

 


  总之,如果要使用内存映射文件,必须1.处理的文件比较的小,2.处理的文件很大,但是运行环境内存也很大,并且一般在运行该程序的时候不运行其他消耗内存大的程序,同时用户对速度有特别的要求,而且对内存占用没有什么要求。如果以上两个条件不满足的时候,建议使用内存缓冲区的办法。

总之,如果要使用内存映射文件,必须1.处理的文件比较的小,2.处理的文件很大,但是运行环境内存也很大,并且一般在运行该程序的时候不运行其他消耗内存大的程序,同时用户对速度有特别的要求,而且对内存占用没有什么要求。如果以上两个条件不满足的时候,建议使用内存缓冲区的办法。

作者:liyuming1978

CppUnit – 测试驱动开发入门

测试驱动开发是一个现在软件界最流行的词汇之一,可是很多人还是不得其门而入。这篇文章想通过对于CppUnit的介绍,给予读者一个基本的映像。如果你熟知CppUnit的使用,请参阅我的另一篇文章:CppUnit代码简介 – 第一部分,核心类来获得对于CppUnit进一步的了解。

I. 前言
测试驱动开发是一个现在软件界最流行的词汇之一,可是很多人还是不得其门而入。这篇文章想通过对于CppUnit的介绍,给予读者一个基本的映像。如果你熟知CppUnit的使用,请参阅我的另一篇文章:CppUnit代码简介 – 第一部分,核心类来获得对于CppUnit进一步的了解。

II. 测试驱动开发
要理解测试驱动开发,必须先理解测试。测试就是通过对源代码的运行或者别的方式的检测来确定源代码之中是否含有已知或者未知的错误。所谓测试驱动开发,就是在开发前根据对将要开发的程序的要求,先写好所有测试代码,并且在开发过程中不时地通过运行测试代码来获得所开发的代码与所要求的结果之间的差距。很多人赡芑嵊幸晌剩杭热晃一姑挥锌夹创耄以趺茨芄恍床馐源肽兀空馐且蛭淙晃颐腔姑挥行闯鋈魏问迪执耄俏颐强梢愿菸颐嵌源氲囊蟠邮褂谜叩慕嵌刃闯霾馐源搿J率瞪希诳⑶靶闯霾馐源耄梢约觳饽愕囊笫遣皇峭晟坪途罚蛭绻阈床怀霾馐源耄硎灸愕男枨蠡共还磺逦?BR>这篇文章通过一个文件状态操作类来展示测试驱动开发相对于普通开发方法的优势。

III. 文件状态操作类(FileStatus)需求
构造函数,接受一个const std::string&作为文件名参数。
DWORD getFileSize()函数,获取这个文件的长度。
bool fileExists()函数,获取这个文件是否存在。
void setFileModifyDate(FILETIME ft)函数,设定这个文件的修改日期。
FILETIME getFileModifyDate()函数,返回这个文件的修改日期。
std::string getFileName()函数,返回这个文件的名字。

IV. CppUnit简介
我们所进行的测试,某种意义上说,就是一个或者多个函数。通过对这些函数的运行,我们可以检测我们是否有错误。假设我们要对构造函数和getFileName函数进行测试,这里面有一个很显然的不变式,就是对一个FileStatus::getFileName函数的调用,应该与传给这个FileStatus对象的构造函数的参数相同。于是我们有这样一个函数:
bool testCtorAndGetFileName()
{
    const string fileName( “a.dat” );
    FileStatus status( fileName );
    return ( status.getFileName() == fileName );
}
我们只需要测试这个函数的返回值就可以知道是否正确了。在CppUnit中,我们可以从TestCase派生出一个类,并且重载它的runTest函数。

class MyTestCase:public CPPUNIT_NS::TestCase
{
public:
    virtual void runTest()
    {
        const std::string fileName( “a.dat” );
        FileStatus status( fileName );
        CPPUNIT_ASSERT_EQUAL( status.getFileName(), fileName );
    }
};

CPPUNIT_ASSERT_EQUAL是一个宏,在它的两个参数不相等的时候,会抛出异常。所以,理论上说,我们可以通过:

MyTestCase m;
m.runTest();

来进行测试,如果有异常抛出,那么就说明代码写错了。可是,这显然不方便,也不是我们使用CppUnit的初衷。下面我们给出完整的代码:

// UnitTest.cpp : Defines the entry point for the console application.
//

#include “CppUnit/TestCase.h”
#include “CppUnit/TestResult.h”
#include “CppUnit/TextOutputter.h”
#include “CppUnit/TestResultCollector.h”

#include
#include

class FileStatus
{
    std::string mFileName;
public:
    FileStatus( const std::string& fileName ):mFileName( fileName )
    {}
    std::string getFileName() const
    {
        return mFileName;
    }
};

class MyTestCase:public CPPUNIT_NS::TestCase
{
public:
    virtual void runTest()
    {
        const std::string fileName( “a.dat” );
        FileStatus status( fileName );
        CPPUNIT_ASSERT_EQUAL( status.getFileName(), fileName );
    }
};

int main()
{
    MyTestCase m;
    CPPUNIT_NS::TestResult r;
    CPPUNIT_NS::TestResultCollector result;
    r.addListener( &result );
    m.run( &r );
    CPPUNIT_NS::TextOutputter out( &result, std::cout );
    out.write();
    return 0;
}

这里我先说一下怎样运行这个程序。假设你的CppUnit版本是1.10.2,解压后,你会在src文件夹中,发现一个CppUnitLibraries.dsw,打开它,并且编译。你会在lib文件夹中,发现一些lib和dll,我们的程序需要依赖当中的某些。接着,创建一个Console应用程序,假设我们仅使用Debug模式,在Project Settings中,把预编译选项(Precompiled Header)选成No,把CppUnit的include路径加入到Additional Include Directories中,并且把Code Generation改成Multi-threaded Debug Dll,接着把CppUnitD.lib加入到你的项目中去。最后把我们的这个文件替换main.cpp。这个时候,就可以编译运行了。
这个文件中,前面四行分别是CppUnit相应的头文件,在CppUnit中,通常某个类就定义在用它的类名命名的头文件中。接着是我们的string和iostream头文件。然后是我们类的一个简单实现,只实现了这个测试中有意义的功能。接下去是我们的TestCase的定义,CPPUNIT_NS是CppUnit所在的名字空间。main中,TestResult其实是一个测试的控制器,你在调用TestCase的run时,需要提供一个TestResult。run作为测试的进行方,会把测试中产生的信息发送给TestResult,而TestResult作为一个分发器,会把所收到的信息再转发给它的Listener。也就是说,我简单的定义一个TestResult并且把它的指针传给TestCase::run,这个程序也能够编译通过并且正确运行,但是它不会有任何输出。TestResultCollector可以把测试输出的信息都收集起来,并且最后通过TextOutputter输出出来。在上述的例子中,你所获得的输出是:

OK (1 tests)

这说明我们一共进行了1个测试,并且都通过了。如果我们人为地把“return mFileName;”改成“return mFileName + ‘a’;”以制造一个错误,那么测试的结果就会变成:

!!!FAILURES!!!
Test Results:
Run:  1   Failures: 1   Errors: 0


1) test:  (F) line: 31 c:unittestunittest.cpp
equality assertion failed
- Expected: a.data
- Actual  : a.dat

这个结果告诉我们我们的实现出现了问题。前面说到,CPPUNIT_ASSERT_EQUAL在两个参数不等时会抛出异常,可是这里为什么没有异常退出了?这是因为,我们执行每一个TestCase的run的时候,它使用了一种特殊的机制把函数包起来,任何异常都会被捕获。具体细节请参考我的CppUnit代码简介一文。

如果我们把#include “CppUnit/TextOutputter.h”替换成#include “CppUnit/CompilerOutputter.h”,并且把TextOutputter替换成CompilerOutputter,输出就变成:

c:unittestunittest.cpp(32) : error : Assertion
Test name:
equality assertion failed
- Expected: a.data
- Actual  : a.dat

Failures !!!
Run: 1   Failure total: 1   Failures: 1   Errors: 0

这个输出,在编译器的信息窗口里面,可以通过双击文件名加行号的那一行来到达相应的位置。

V. 迭代开发

上面的例子中我们先针对需求的一部分写了测试用例,然后就实现了相应的功能。我们可以在这些功能被测试后,继续实现别的功能的测试用例,然后继续实现相应的功能,这是一个迭代的过程,我们不断地增加测试用例和实现代码,最后达成需求。还有一种方法是,先写好所有的测试用例(这个时候通常会编译不通过),然后再添加能够让编译通过所需要的实现(这个时候通常运行测试会有很多错误),接着通过正确实现使得没有任何测试错误,最后,对代码作优化和更新,并且不断的保证测试通过。在这里我们着重介绍第二种方法。首先我们先写下所有的测试用例,在这里,由于有很多测试用例,我们不再使用TestCase,因为TestCase通常用在单一测试任务的情况下。这次我们从TestFixture派生我们的测试类:

class MyTestCase:public CPPUNIT_NS::TestFixture
{
public:
    void testCtorAndGetName()
    {
        const std::string fileName( “a.dat” );
        FileStatus status( fileName );
        CPPUNIT_ASSERT_EQUAL( status.getFileName(), fileName );
    }
    void testGetFileSize()
    {
        const std::string fileName( “a.dat” );
        FileStatus status( fileName );
        CPPUNIT_ASSERT_EQUAL( status.getFileSize(), 0 );//?
    }
};

写到这里,我们发现了两个问题,首先我们不停的初始化一些测试所需的对象,重复了很多代码;其次我们发现了一个接口设计错误,我们的接口设计上没有考虑一个文件不存在的情况。从中可见,先写好测试用例,不仅是对实现的测试,也是对我们设计的测试。TestFixture定义了两个成员函数setUp和tearDown,在每一个测试用例被执行的时候,和它定义在同一个类内部的setUp和tearDown会被调用以进行初始化和清除工作。我们可以用这两个函数来进行统一的初始化代码。并且,我们修改getFileSize、setFileModifyDate和getFileModifyDate使得它们在出现错误的时候,抛出异常FileStatusError。下面是我们的测试用例:

class MyTestCase:public CPPUNIT_NS::TestFixture
{
    std::string mFileNameExist;
    std::string mFileNameNotExist;
    std::string mTestFolder;
    enum DUMMY
    {
        FILE_SIZE = 1011
    };
public:
    virtual void setUp()
    {
        mTestFolder = “c:justfortest”;
        mFileNameExist = mTestFolder + “exist.dat”;
        mFileNameNotExist = mTestFolder + “notexist.dat”;
        if( GetFileAttributes( mTestFolder.c_str() ) != INVALID_FILE_ATTRIBUTES )
            throw std::exception( “test folder already exists” );
        if( ! CreateDirectory( mTestFolder.c_str() ,NULL ) )
            throw std::exception( “cannot create folder” );
        HANDLE file = CreateFile( mFileNameExist.c_str(), GENERIC_READ | GENERIC_WRITE,
            0, NULL, CREATE_NEW, 0, NULL );
        if( file == INVALID_HANDLE_VALUE )
            throw std::exception( “cannot create file” );
        char buffer[FILE_SIZE];
        DWORD bytesWritten;
        if( !WriteFile( file, buffer, FILE_SIZE, &bytesWritten, NULL ) ||
            bytesWritten != FILE_SIZE )
        {
            CloseHandle( file );
            throw std::exception( “cannot write file” );
        }
        CloseHandle( file );
    }
    virtual void tearDown()
    {
        if( ! DeleteFile( mFileNameExist.c_str() ) )
            throw std::exception( “cannot delete file” );
        if( ! RemoveDirectory( mTestFolder.c_str() ) )
            throw std::exception( “cannot remove folder” );
    }
    void testCtorAndGetName()
    {
        FileStatus status( mFileNameExist );
        CPPUNIT_ASSERT_EQUAL( status.getFileName(), mFileNameExist );
    }
    void testGetFileSize()
    {
        FileStatus exist( mFileNameExist );
        //这里FILE_SIZE缺省是int,而getFileSize返回DWORD,不加转换会导致模版不能正确匹配。
        CPPUNIT_ASSERT_EQUAL( exist.getFileSize(), (DWORD)FILE_SIZE );
        FileStatus notExist( mFileNameNotExist );
        CPPUNIT_ASSERT_THROW( notExist.getFileSize(), FileStatusError );
    }
    void testFileExist()
    {
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT( exist.fileExist() );
        FileStatus notExist( mFileNameNotExist );
        CPPUNIT_ASSERT( ! notExist.fileExist() );
    }
    void testFileModifyDateBasic()
    {
        FILETIME fileTime;
        GetSystemTimeAsFileTime( &fileTime );
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT_NO_THROW( exist.getFileModifyDate() );
        CPPUNIT_ASSERT_NO_THROW( exist.setFileModifyDate( &fileTime ) );
        FileStatus notExist( mFileNameNotExist );
        CPPUNIT_ASSERT_THROW( notExist.getFileModifyDate(), FileStatusError );
        CPPUNIT_ASSERT_THROW( notExist.setFileModifyDate( &fileTime ), FileStatusError );
    }
    void testFileModifyDateEqual()
    {
        FILETIME fileTime;
        GetSystemTimeAsFileTime( &fileTime );
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT_NO_THROW( exist.setFileModifyDate( &fileTime ) );
        FILETIME get = exist.getFileModifyDate();
        // 这里 FILETIME 没有定义 operator==,所以不能直接使用 CPPUNIT_ASSERT_EQUAL
        CPPUNIT_ASSERT( CompareFileTime( &get, &fileTime ) == 0 );
    }
};

接着我们编写一个FileStatus类的骨架,使得这段测试代码可以被编译通过。

class FileStatusError
{};

class FileStatus
{
public:
    FileStatus(const std::string& fileName)
    {}
    DWORD getFileSize() const
    {
        return 0;
    }
    bool fileExist() const
    {
        return false;
    }
    void setFileModifyDate( const FILETIME* )
    {
    }
    FILETIME getFileModifyDate() const
    {
        return FILETIME();
    }
    std::string getFileName() const
    {
        return “”;
    }
};

下面给出完整的程序:

// UnitTest.cpp : Defines the entry point for the console application.
//

#include “CppUnit/TestCase.h”
#include “CppUnit/TestResult.h”
#include “CppUnit/TextOutputter.h”
#include “CppUnit/TestResultCollector.h”
#include “CppUnit/TestCaller.h”
#include “CppUnit/extensions/HelperMacros.h”

#include
#include
#include
#include

class FileStatusError
{};

class FileStatus
{
public:
    FileStatus(const std::string& fileName)
    {}
    DWORD getFileSize() const
    {
        return 0;
    }
    bool fileExist() const
    {
        return false;
    }
    void setFileModifyDate( const FILETIME* )
    {
    }
    FILETIME getFileModifyDate() const
    {
        return FILETIME();
    }
    std::string getFileName() const
    {
        return “”;
    }
};

class MyTestCase:public CPPUNIT_NS::TestFixture
{
    std::string mFileNameExist;
    std::string mFileNameNotExist;
    std::string mTestFolder;
    enum DUMMY
    {
        FILE_SIZE = 1011
    };
public:
    virtual void setUp()
    {
        mTestFolder = “c:justfortest”;
        mFileNameExist = mTestFolder + “exist.dat”;
        mFileNameNotExist = mTestFolder + “notexist.dat”;
        if( GetFileAttributes( mTestFolder.c_str() ) != INVALID_FILE_ATTRIBUTES )
            throw std::exception( “test folder already exists” );
        if( ! CreateDirectory( mTestFolder.c_str() ,NULL ) )
            throw std::exception( “cannot create folder” );
        HANDLE file = CreateFile( mFileNameExist.c_str(), GENERIC_READ | GENERIC_WRITE,
            0, NULL, CREATE_NEW, 0, NULL );
        if( file == INVALID_HANDLE_VALUE )
            throw std::exception( “cannot create file” );
        char buffer[FILE_SIZE];
        DWORD bytesWritten;
        if( !WriteFile( file, buffer, FILE_SIZE, &bytesWritten, NULL ) ||
            bytesWritten != FILE_SIZE )
        {
            CloseHandle( file );
            throw std::exception( “cannot write file” );
        }
        CloseHandle( file );
    }
    virtual void tearDown()
    {
        if( ! DeleteFile( mFileNameExist.c_str() ) )
            throw std::exception( “cannot delete file” );
        if( ! RemoveDirectory( mTestFolder.c_str() ) )
            throw std::exception( “cannot remove folder” );
    }
    void testCtorAndGetName()
    {
        FileStatus status( mFileNameExist );
        CPPUNIT_ASSERT_EQUAL( status.getFileName(), mFileNameExist );
    }
    void testGetFileSize()
    {
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT_EQUAL( exist.getFileSize(), (DWORD)FILE_SIZE );
        FileStatus notExist( mFileNameNotExist );
        CPPUNIT_ASSERT_THROW( notExist.getFileSize(), FileStatusError );
    }
    void testFileExist()
    {
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT( exist.fileExist() );
        FileStatus notExist( mFileNameNotExist );
        CPPUNIT_ASSERT( ! notExist.fileExist() );
    }
    void testFileModifyDateBasic()
    {
        FILETIME fileTime;
        GetSystemTimeAsFileTime( &fileTime );
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT_NO_THROW( exist.getFileModifyDate() );
        CPPUNIT_ASSERT_NO_THROW( exist.setFileModifyDate( &fileTime ) );
        FileStatus notExist( mFileNameNotExist );
        CPPUNIT_ASSERT_THROW( notExist.getFileModifyDate(), FileStatusError );
        CPPUNIT_ASSERT_THROW( notExist.setFileModifyDate( &fileTime ), FileStatusError );
    }
    void testFileModifyDateEqual()
    {
        FILETIME fileTime;
        GetSystemTimeAsFileTime( &fileTime );
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT_NO_THROW( exist.setFileModifyDate( &fileTime ) );
        FILETIME get = exist.getFileModifyDate();
        CPPUNIT_ASSERT( CompareFileTime( &get, &fileTime ) == 0 );
    }
};

int main()
{
    CPPUNIT_NS::TestResult r;
    CPPUNIT_NS::TestResultCollector result;
    r.addListener( &result );
    CPPUNIT_NS::TestCaller testCase1( “testCtorAndGetName”, MyTestCase::testCtorAndGetName );
    CPPUNIT_NS::TestCaller testCase2( “testGetFileSize”, MyTestCase::testGetFileSize );
    CPPUNIT_NS::TestCaller testCase3( “testFileExist”, MyTestCase::testFileExist );
    CPPUNIT_NS::TestCaller testCase4( “testFileModifyDateBasic”, MyTestCase::testFileModifyDateBasic );
    CPPUNIT_NS::TestCaller testCase5( “testFileModifyDateEqual”, MyTestCase::testFileModifyDateEqual );
    testCase1.run( &r );
    testCase2.run( &r );
    testCase3.run( &r );
    testCase4.run( &r );
    testCase5.run( &r );
    CPPUNIT_NS::TextOutputter out( &result, std::cout );
    out.write();
    return 0;
}

这里的TestCaller可以把从TestFixture派生而来的类的成员函数转化为一个TestCase。这段代码可以编译通过,运行后一共进行了5个测试,完全失败。这是我们意料之中的结果,因此我们进一步实现我们的功能,完成后的代码为:

// UnitTest.cpp : Defines the entry point for the console application.
//

#include “CppUnit/TestCase.h”
#include “CppUnit/TestResult.h”
#include “CppUnit/TextOutputter.h”
#include “CppUnit/TestResultCollector.h”
#include “CppUnit/TestCaller.h”
#include “CppUnit/extensions/HelperMacros.h”

#include
#include
#include
#include

class FileStatusError
{};

class FileStatus
{
    std::string mFileName;
public:
    FileStatus(const std::string& fileName):mFileName( fileName )
    {}
    DWORD getFileSize() const
    {
        DWORD fileSize = INVALID_FILE_SIZE;
        HANDLE file = CreateFile( mFileName.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
            NULL, OPEN_EXISTING, 0, NULL );
        if( file != INVALID_HANDLE_VALUE )
        {
            fileSize = GetFileSize( file, NULL );
            CloseHandle( file );
        }
        if( fileSize == INVALID_FILE_SIZE )
            throw FileStatusError();
        return fileSize;
    }
    bool fileExist() const
    {
        return GetFileAttributes( mFileName.c_str() ) != INVALID_FILE_ATTRIBUTES;
    }
    void setFileModifyDate( const FILETIME* fileTime )
    {
        BOOL result = FALSE;
        HANDLE file = CreateFile( mFileName.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
            NULL, OPEN_EXISTING, 0, NULL );
        if( file != INVALID_HANDLE_VALUE )
        {
            result = SetFileTime( file, NULL, NULL, fileTime );
            int i = GetLastError();
            CloseHandle( file );
        }
        if( ! result )
            throw FileStatusError();
    }
    FILETIME getFileModifyDate() const
    {
        FILETIME time;
        BOOL result = FALSE;
        HANDLE file = CreateFile( mFileName.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
            NULL, OPEN_EXISTING, 0, NULL );
        if( file != INVALID_HANDLE_VALUE )
        {
            result = GetFileTime( file, NULL, NULL, &time );
            CloseHandle( file );
        }
        if( ! result )
            throw FileStatusError();
        return time;
    }
    std::string getFileName() const
    {
        return mFileName;
    }
};

class MyTestCase:public CPPUNIT_NS::TestFixture
{
    std::string mFileNameExist;
    std::string mFileNameNotExist;
    std::string mTestFolder;
    enum DUMMY
    {
        FILE_SIZE = 1011
    };
public:
    virtual void setUp()
    {
        mTestFolder = “c:justfortest”;
        mFileNameExist = mTestFolder + “exist.dat”;
        mFileNameNotExist = mTestFolder + “notexist.dat”;
        if( GetFileAttributes( mTestFolder.c_str() ) != INVALID_FILE_ATTRIBUTES )
            throw std::exception( “test folder already exists” );
        if( ! CreateDirectory( mTestFolder.c_str() ,NULL ) )
            throw std::exception( “cannot create folder” );
        HANDLE file = CreateFile( mFileNameExist.c_str(), GENERIC_READ | GENERIC_WRITE,
            0, NULL, CREATE_NEW, 0, NULL );
        if( file == INVALID_HANDLE_VALUE )
            throw std::exception( “cannot create file” );
        char buffer[FILE_SIZE];
        DWORD bytesWritten;
        if( !WriteFile( file, buffer, FILE_SIZE, &bytesWritten, NULL ) ||
            bytesWritten != FILE_SIZE )
        {
            CloseHandle( file );
            throw std::exception( “cannot write file” );
        }
        CloseHandle( file );
    }
    virtual void tearDown()
    {
        if( ! DeleteFile( mFileNameExist.c_str() ) )
            throw std::exception( “cannot delete file” );
        if( ! RemoveDirectory( mTestFolder.c_str() ) )
            throw std::exception( “cannot remove folder” );
    }
    void testCtorAndGetName()
    {
        FileStatus status( mFileNameExist );
        CPPUNIT_ASSERT_EQUAL( status.getFileName(), mFileNameExist );
    }
    void testGetFileSize()
    {
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT_EQUAL( exist.getFileSize(), (DWORD)FILE_SIZE );
        FileStatus notExist( mFileNameNotExist );
        CPPUNIT_ASSERT_THROW( notExist.getFileSize(), FileStatusError );
    }
    void testFileExist()
    {
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT( exist.fileExist() );
        FileStatus notExist( mFileNameNotExist );
        CPPUNIT_ASSERT( ! notExist.fileExist() );
    }
    void testFileModifyDateBasic()
    {
        FILETIME fileTime;
        GetSystemTimeAsFileTime( &fileTime );
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT_NO_THROW( exist.getFileModifyDate() );
        CPPUNIT_ASSERT_NO_THROW( exist.setFileModifyDate( &fileTime ) );
        FileStatus notExist( mFileNameNotExist );
        CPPUNIT_ASSERT_THROW( exist.getFileModifyDate(), FileStatusError );
        CPPUNIT_ASSERT_THROW( exist.setFileModifyDate( &fileTime ), FileStatusError );
    }
    void testFileModifyDateEqual()
    {
        FILETIME fileTime;
        GetSystemTimeAsFileTime( &fileTime );
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT_NO_THROW( exist.setFileModifyDate( &fileTime ) );
        FILETIME get = exist.getFileModifyDate();
        CPPUNIT_ASSERT( CompareFileTime( &get, &fileTime ) == 0 );
    }
};

int main()
{
    CPPUNIT_NS::TestResult r;
    CPPUNIT_NS::TestResultCollector result;
    r.addListener( &result );
    CPPUNIT_NS::TestCaller testCase1( “testCtorAndGetName”, MyTestCase::testCtorAndGetName );
    CPPUNIT_NS::TestCaller testCase2( “testGetFileSize”, MyTestCase::testGetFileSize );
    CPPUNIT_NS::TestCaller testCase3( “testFileExist”, MyTestCase::testFileExist );
    CPPUNIT_NS::TestCaller testCase4( “testFileModifyDateBasic”, MyTestCase::testFileModifyDateBasic );
    CPPUNIT_NS::TestCaller testCase5( “testFileModifyDateEqual”, MyTestCase::testFileModifyDateEqual );
    testCase1.run( &r );
    testCase2.run( &r );
    testCase3.run( &r );
    testCase4.run( &r );
    testCase5.run( &r );
    CPPUNIT_NS::TextOutputter out( &result, std::cout );
    out.write();
    return 0;
}

运行测试,发现两个错误:

1) test: testFileModifyDateBasic (F) line: 140 c:unittestunittest.cpp
assertion failed
- Unexpected exception caught


2) test: testFileModifyDateEqual (F) line: 150 c:unittestunittest.cpp
assertion failed
- Unexpected exception caught

调试发现,原来我的setFileModifyDate中,文件的打开方式为GENERIC_READ,只有读权限,自然不能写。把这个替换为 GENERIC_READ | GENERIC_WRITE,再运行,一切OK!
其实上面的测试以及实现代码还有一些问题,譬如说,测试用例分得还不够细,有些测试可以继续细分为几个函数,这样一旦遇到测试错误,你可以很精确的知道错误的位置(因为抛出异常错误是不能知道行数的)。不过用来说明怎样进行测试驱动开发应该是足够了。

VI. 测试集

CPPUNIT_NS::TestCaller testCase1( “testCtorAndGetName”, MyTestCase::testCtorAndGetName );
CPPUNIT_NS::TestCaller testCase2( “testGetFileSize”, MyTestCase::testGetFileSize );
CPPUNIT_NS::TestCaller testCase3( “testFileExist”, MyTestCase::testFileExist );
CPPUNIT_NS::TestCaller testCase4( “testFileModifyDateBasic”, MyTestCase::testFileModifyDateBasic );
CPPUNIT_NS::TestCaller testCase5( “testFileModifyDateEqual”, MyTestCase::testFileModifyDateEqual );

这段代码虽然还不够触目惊心,但是让程序员来做这个,的确是太浪费了。CppUnit为我们提供了一些机制来避免这样的浪费。我们可以修改我们的测试代码为:

class MyTestCase:public CPPUNIT_NS::TestFixture
{
    std::string mFileNameExist;
    std::string mFileNameNotExist;
    std::string mTestFolder;
    enum DUMMY
    {
        FILE_SIZE = 1011
    };
    CPPUNIT_TEST_SUITE( MyTestCase );
        CPPUNIT_TEST( testCtorAndGetName );
        CPPUNIT_TEST( testGetFileSize );
        CPPUNIT_TEST( testFileExist );
        CPPUNIT_TEST( testFileModifyDateBasic );
        CPPUNIT_TEST( testFileModifyDateEqual );
    CPPUNIT_TEST_SUITE_END();
public:
    virtual void setUp()
    {
        mTestFolder = “c:justfortest”;
        mFileNameExist = mTestFolder + “exist.dat”;
        mFileNameNotExist = mTestFolder + “notexist.dat”;
        if( GetFileAttributes( mTestFolder.c_str() ) != INVALID_FILE_ATTRIBUTES )
            throw std::exception( “test folder already exists” );
        if( ! CreateDirectory( mTestFolder.c_str() ,NULL ) )
            throw std::exception( “cannot create folder” );
        HANDLE file = CreateFile( mFileNameExist.c_str(), GENERIC_READ | GENERIC_WRITE,
            0, NULL, CREATE_NEW, 0, NULL );
        if( file == INVALID_HANDLE_VALUE )
            throw std::exception( “cannot create file” );
        char buffer[FILE_SIZE];
        DWORD bytesWritten;
        if( !WriteFile( file, buffer, FILE_SIZE, &bytesWritten, NULL ) ||
            bytesWritten != FILE_SIZE )
        {
            CloseHandle( file );
            throw std::exception( “cannot write file” );
        }
        CloseHandle( file );
    }
    virtual void tearDown()
    {
        if( ! DeleteFile( mFileNameExist.c_str() ) )
            throw std::exception( “cannot delete file” );
        if( ! RemoveDirectory( mTestFolder.c_str() ) )
            throw std::exception( “cannot remove folder” );
    }
    void testCtorAndGetName()
    {
        FileStatus status( mFileNameExist );
        CPPUNIT_ASSERT_EQUAL( status.getFileName(), mFileNameExist );
    }
    void testGetFileSize()
    {
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT_EQUAL( exist.getFileSize(), (DWORD)FILE_SIZE );
        FileStatus notExist( mFileNameNotExist );
        CPPUNIT_ASSERT_THROW( notExist.getFileSize(), FileStatusError );
    }
    void testFileExist()
    {
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT( exist.fileExist() );
        FileStatus notExist( mFileNameNotExist );
        CPPUNIT_ASSERT( ! notExist.fileExist() );
    }
    void testFileModifyDateBasic()
    {
        FILETIME fileTime;
        GetSystemTimeAsFileTime( &fileTime );
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT_NO_THROW( exist.getFileModifyDate() );
        CPPUNIT_ASSERT_NO_THROW( exist.setFileModifyDate( &fileTime ) );
        FileStatus notExist( mFileNameNotExist );
        CPPUNIT_ASSERT_THROW( notExist.getFileModifyDate(), FileStatusError );
        CPPUNIT_ASSERT_THROW( notExist.setFileModifyDate( &fileTime ), FileStatusError );
    }
    void testFileModifyDateEqual()
    {
        FILETIME fileTime;
        GetSystemTimeAsFileTime( &fileTime );
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT_NO_THROW( exist.setFileModifyDate( &fileTime ) );
        FILETIME get = exist.getFileModifyDate();
        CPPUNIT_ASSERT( CompareFileTime( &get, &fileTime ) == 0 );
    }
};

CPPUNIT_TEST_SUITE_REGISTRATION( MyTestCase );

int main()
{
    CPPUNIT_NS::TestResult r;
    CPPUNIT_NS::TestResultCollector result;
    r.addListener( &result );
    CPPUNIT_NS::TestFactoryRegistry::getRegistry().makeTest()->run( &r );
    CPPUNIT_NS::TextOutputter out( &result, std::cout );
    out.write();
    return 0;
}

这里的

    CPPUNIT_TEST_SUITE( MyTestCase );
        CPPUNIT_TEST( testCtorAndGetName );
        CPPUNIT_TEST( testGetFileSize );
        CPPUNIT_TEST( testFileExist );
        CPPUNIT_TEST( testFileModifyDateBasic );
        CPPUNIT_TEST( testFileModifyDateEqual );
    CPPUNIT_TEST_SUITE_END();

最重要的内容其实是定义了一个函数suite,这个函数返回了一个包含了所有CPPUNIT_TEST定义的测试用例的一个测试集。CPPUNIT_TEST_SUITE_REGISTRATION通过静态注册把这个测试集注册到全局的测试树中,最后通过CPPUNIT_NS::TestFactoryRegistry::getRegistry().makeTest()生成一个包含所有测试用例的测试并且运行。具体的内部运行机制请参考CppUnit代码简介。

VII. 小节

这篇文章简要的介绍了CppUnit和测试驱动开发的基本概念,虽然CppUnit还有很多别的功能,譬如说基于GUI的测试环境以及和编译器Post Build相连接的测试输出,以及对于测试系统的扩展等,但是基本上掌握了本文中的内容就可以进行测试驱动的开发了。

此外,测试驱动开发还可以检验需求的错误。其实我选用GetFileTime和SetFileTime作为例子是因为,有些系统上,SetFileTime所设置的时间是有一定的精度的,譬如说按秒,按天,…,因此你设置了一个时间后,可能get回来的时间和它不同。这其实是一个需求的错误。当然由于我的系统上没有这个问题,所以我也就不无病呻吟了。具体可以参考MSDN中对于这两个函数的介绍。

CppUnit代码简介 – 第一部分,核心类

某种意义上说,CppUnit的代码并不是很好的C++代码。正因为它不是很好的C++代码,并且代码量不是很大(主库80K),所以我觉得比较适合想大量使用CppUnit并且需要深入了解的人或是初步涉足C++,想阅读一些简单的源代码/库的人。

这篇文章不适合于从未使用过CppUnit的人,如果你从未使用过CppUnit,但是对于测试驱动开发很感兴趣,可以参阅我的另一篇文章:CppUnit入门。

I. 目的

前一段时间拿到公司新的引擎,发现里面使用了CppUnit来做单元测试,于是小小的研究了一把,顺便把阅读CppUnit代码的心得写下,和大家分享。某种意义上说,CppUnit的代码并不是很好的C++代码,这主要是由历史原因引起的:首先CppUnit是对JUnit的一个移植,所以很多地方是把C++作为Java在用,从风格上到语义上,都是Java的;当然这也无可厚非,因为对于移植来说,只要能够做到和原先一样用起来,别的都是第二位的。其次,CppUnit(过度的?)考虑了兼容性,为了在不同的C++编译器和标准库下都能使用,它给自己制定了很多编码标准,譬如说“不要使用mutable”、“不要使用typename”、甚至“不要使用STL容器中的at成员”,…。正因为它不是很好的C++代码,并且代码量不是很大(主库80K),所以我觉得比较适合想大量使用CppUnit并且需要深入了解的人或是初步涉足C++,想阅读一些简单的源代码/库的人(如果你很有经验,想学习最新的C++技术,或是想阅读大型的库,那么你可以阅读别的开源库。)

这篇文章不适合于从未使用过CppUnit的人,如果你从未使用过CppUnit,但是对于测试驱动开发很感兴趣,可以参阅我的另一篇文章:CppUnit入门。

II. 代码

CppUnit作为一个UnitTest框架,它的主要机制就是:让你为每一段代码写出测试用例,并且把大量的测试用例按照某种方式管理起来,并且提供给你不同的界面以及输出方式。所以我觉得它的代码也可以分为相应的部分:
1.测试用例及其管理,这部分只是管理和测试相关的数据结构,它们往往是静态的初始化的,在真正的测试还没有进行前就已经完全执行完毕了。
2.实际进行测试的代码,这部分包含了测试的初始化,测试中的异常处理以及测试进程和结果的通知等。这部分代码是你的测试运行中所执行的代码。
3.测试的界面和输入。譬如说,CppUnit提供了一个基于MFC的测试界面,你可以选择一些测试用例进行测试,并且获得结果。这部分代码我们将不作详细介绍,因为和测试本身其实关系不大。

III. 测试用例及其管理

这部分的类有Test、TestLeaf、TestCase、TestComposite和TestSuite以及一些像TestPath之类的辅助类。这一部分代码其实和UnitTest本身毫无关系,它只是一个对象树的管理。CppUnit把测试系统中所有可以用来测试的对象作为一棵树进行管理,每一个测试都是树上的一个节点,这些节点都是以Test为父类的。

1. TestPath

如果把整个测试用例树比作为文件树的话,TestPath表示的是一个相对/绝对路径。它内部保存的是一个Test的队列,也就是说,它把一个类似于/root/suite/case或者是/suite/child/case的路径变为一个路径上所经过的Test对象的队列。通过这个结构,我可以很方便的在树结构中的节点间随意移动。

2. Test

Test类中,除了虚析构函数和virtual void run( TestResult *result )=0 这个纯虚函数用来执行测试以外,别的函数都是用来做树管理的。因为树是递归定义的,作为一个节点,它只需要能够枚举自身的子节点就可以了。这些相关的函数为:countTestCases用来返回这个节点及其子节点中包含了多少个有效的Test对象,这样做主要是处于计数的目的,因为Test树上很多节点本身并不包含任何测试代码;getChildTestCount返回直接的子节点个数;getChildTestAt接受一个索引,返回相应的子节点;getName返回这个节点的名字。上述函数都是纯虚函数,由子类给出具体定义。
getChildTestAt是一个虚函数,它内部实际调用的是checkIsValidIndex和doGetChildTestAt,并且CppUnit的设计者也建议你不要重载它,而是重载doGetChildTestAt函数。这个是GoF里面的Template Method模式,不过既然这里不建议重载,就不应该使用virtual函数。或者和可能是JUnit的影子。
Test类真正实现的功能是对非直接子节点的处理。如前所述,对于直接子节点可以通过一个index来表示,而对于非直接子节点则是通过TestPath来表示的。Test提供了findTestPath、findTest以及resolveTestPath函数来对这些功能提供支持。虽然这些函数是以虚函数的形式提出的,也就是说理论上可以进行扩展,但是事实上在整个CppUnit体系中,只有Test类对它们作了实现。

3. TestLeaf

TestLeaf是Test的子类,它代表了Test树上的一个叶节点。它的实现很简单,对于countTestCases返回1,因为它自身就是一个有效的Test;getChildTestCount则返回0,因为没有子节点;它的doGetChildTestAt在正常情况下根本不应该被调用。

4. TestComposite

TestComposite也是Test的子类,它代表了Test树上的一个非根节点,这里对它的使用类似于一个GoF中Composite模式。它的countTestCases返回的是对所有子节点的countTestCases的累加,从这点上可以看出,TestComposite本身只是一个Test的容器,不作为一个有效的Test。TestComposite的run函数会调用自身的doStartSuite函数,在这个函数中,会对TestResult的startSuite进行调用;然后run函数再通过doRunChildTests函数间接调用所有子节点的run,最后通过doEndSuite函数间接调用TestResult的endSuite函数。TestComposite的run函数其实也是一个Template Method模式,因为通常希望通过这三个do****函数来对其进行定制。对于TestRunner::startSuite/endSuite的调用是为了让传入的TestRunner在每个Suite被测试的前后都得到通知,以做一些簿记工作。(当然窃以为这里的名字最好叫startComposite/endComposite。)

5. TestCase

TestCase继承自TestLeaf和TestFixture,是整个CppUnit中最常用最重要的类之一。顾名思义,TestCase就代表了通常UnitTest中所指的测试用例,也是整个Test树中最常用的叶节点。这个类既在Test树中有它的位置,也直接的参与测试的进行,所以被放在下一节介绍。

6. TestSuite

TestSuite是从TestComposite继承而来的,TestComposite为那些非根节点提供了运行机制,而TestSuite则在此基础上提供了对于子节点的管理。譬如说,通过addTest函数可以加入Test,deleteContents删除所有的子Test对象,它还实现了getChildTestCount和doGetChildTestAt函数以返回子节点的个数和指针。

IV. 实际进行测试的代码

这些类包括TestFixture、TestCase、TestCaller、TestRunner、TestListener、TestResult和Protector类体系。

1. TestFixture
与其说Test类是所有Test的根,不如说TestFixture是CppUnit中所有“测试”的根。因为仅仅从Test类继承而来的类只是这个体系的“管理部门”,而从TestFixture继承而来的类,才是真正进行测试的“执行部门”。TestFixture除了一个虚析构函数(C++中“请从我派生”的代名词)以外,就定义了两个虚函数:setUp和tearDown,前者初始化一次测试,后者清除一次测试所产生的所有副作用。这三个函数在TestFixture中的定义都是空的。

2. TestCase
这是我们第二次看到它了,因为它处于两个类体系的交汇点,使用多重继承从TestLeaf和TestFixture继承。这个类既没有对TestLeaf中对于作为Test树子节点方面的功能进行加强,也没有重新定义TestFixture中为空的setUp和tearDown。唯一做的就是定义了run。它的run里面,会先调用TestRunner::startCase,然后调用setUp,接着调用TestCase中新定义的runTest函数,接着调用tearDown和TestRunner::endCase。从注释中可以看出作者希望把runTest作为一个纯虚函数定义,也就是说,其实你可以从TestCase派生,并且定义一个自己的runTest函数,以进行一些简单/单一的测试工作。如果要进行复杂的测试工作/构建复杂的测试用例树,那么应该使用别的机制。这样说的原因有两方面:首先从管理角度说,如果你有几项测试任务,就应该把它们作为Test树中的不同节点,这样你可以对总任务数/失败数进行统计,而TestCase是一个TestLeaf,如果你把它们堆砌在一个TestCase中,不利于管理;其次,如果你有几项测试任务要做,并且共享同样的初始化/清除代码,那么你想从TestCase派生来做这件事情,就必须重写setUp、tearDown和run。重写前两者也算了,要是连run也重写了,这,我干嘛还从TestCase派生?

3. TestCaller
TestCaller是一个GoF中的Adapter模式,它可以把任意一个定义了setUp/tearDown的类的对象包装为一个TestCase,并且在runTest中对这个对象的某个函数进行调用。也就是说,如果我有一个TestFixture的派生类FooBarTest,其中有一个fooTest和一个barTest函数,那么TestCaller(FooBarTest::fooTest)以及TestCaller(FooBarTest::batTest)就是两个TestCase派生类,它们的实例可以作为TestCase被加入到Test树中,也可以独立的进行测试运行。
TestCaller从TestCase派生而来,并且接受某个类的实例(也可以自己通过new生成一个)以及那个方法的指针作为构造函数参数并且保存在内部,在setUp和tearDown函数中,它调用了那个对象的setUp和tearDown,并且在runTest函数中使用保存的对象对它的指定的成员函数进行调用。使用TestCaller的好处是,你可以把一组相关的Test任务放在某个从TestFixture派生而来的类中,并且用TestCaller把它们包装成若干个TestCase。这样一来便于对相关的测试任务的管理,二来也能让不同的任务成为Test树的不同子节点。

4. TestListener
TestListener其实是一个测试事件的接收器,它定义了startTest、endTest、startSuite、endSuite、startTestRun、endTestRun和addFailure方法,这些都是空的虚函数,你可以定义自己的派生类并且对自己感兴趣的事件进行处理。

5. TestResult
TestResult是从SynchronizedObject继承而来的,SynchronizedObject其实就是对于Java中synchronized的模拟,SynchronizedObject和SynchronizedObject::ExclusiveZone是一个典型的用RAII来对某一个作用域进行互斥访问的例子。TestResult定义了addListener和removeListener方法来管理事件的订阅者,并且它也定义了所有Listener的方法,当你对TestResult的某个事件方法调用时,它会把这个事件发送给所有的Listener,不过CppUnit的开发者并不认为TestResult是一个TestListener,所以即使它同相同的方法实现了TestListener的所有函数,也没有从TestListener继承。
TestResult内部维护了一个测试过程是否被强行中止的标志,并且通过reset、stop和shouldStop对其进行管理,这给予运行中的测试一个响应强行中止的机会。
TestResult增加了几个函数,runTest就是其中之一,它接收一个Test的指针作为参数,并且调用这个Test对象的run。当你有一个Test对象和一个TestResult对象的时候,你可以通过Test::run(TestResult*)或者TestResult::runTest(Test*)来完成一次测试任务,区别在于后者在调用Test::run的前后会对TestResult::startTestRun和TestResult::endTestRun进行调用。
另一些比较有趣的函数是protect、pushProtector和popProtector。这三个函数其实是维护了一个ProtectorChain对象,在protect中调用了ProtectorChain::protect来为测试提供一个受保护的环境。

6.  Protector类体系
大家都知道,我们运行某个测试任务时,可以根据返回值来判断成功还是失败,可是有些“不良”函数会抛出异常,我们必须对异常进行捕捉,否则就不能进入下一个测试而会提前用一种极其可悲的方式结束。基本的Protector只提供了一些报错的辅助函数。通过查看DefaultProtector::protect的代码,可以得知在它的protect中,会尝试捕获Exception和std::exception,并且对所有未知的异常用…进行捕捉。DefaultProtector适合作为一个“最后的选择”来使用,因为通常我们希望知道我们是否抛出了某个或者多个特定的异常,这需要使用ProtectorChain。ProtectorChain::protect中有这样一段代码:

Functors functors;
for ( int index = m_protectors.size()-1; index >= 0; –index )
{
 const Functor &protectedFunctor = functors.empty() ? functor : *functors.back();
 functors.push_back( new ProtectFunctor( m_protectors[index], protectedFunctor, context ) );
}

这段代码中,m_protectors是一组Protector,这段代码通过ProtectFunctor来把这些Protector连接在一起,并且它的根是functor,也就是那个需要保护的函数。最后对这个functors最后一个元素调用的时候,它先建立自己的保护机制,然后调用它的functor,那个functor就是它的前一个元素,可能是另一个Protector,也可能是最原始的受保护protector,这样一来,也就是说,所有的保护被一层一层的嵌套起来。

V. 小结

其实整个CppUnit中还有不少别的有意义的代码,譬如说它的测试结果输出机制,它的那些辅助宏和TestSuite的全局注册机制等,我将在以后的文章中介绍。

关于STL中stack的实现的讨论

文章讨论了 为什么大多数STL的stack的实现中,对于内部的容器默认选择deque容器;并且给出了几个自己按不同想法实现的stack;并进行了简单的性能比较测试;

关于STL中stack的实现的讨论
[email protected]  2004.11.07整理

文章来源于abp论坛中的一篇讨论帖子: http://bbs.allaboutprogram.com/viewtopic.php?t=1026这是自己开始接触泛型和STL时形成的一篇讨论;文章中借用了Elminster,papercrane,Innocentius,PolyRandom等人的部分观点

文章讨论了为什么大多数STL的stack的实现中,对于内部的容器默认选择deque容器;并且给出了自己的几个不同想法实现的stack;并进行了简单的性能比较测试;

 

1:为什么大多数STL的stack的实现中,对于内部的容器默认选择deque容器?而不是vector? 
    STL中,stack对内部使用容器的函数调用主要有:push_back,back,pop_back等,也就是顺序容器都满足要求(包括vector,deque,list)。很多人应该和我一样,在STL之前看到的stack实现都是以动态数组来(甚至静态数组)实现为主,也就是接近于使用vector方案;那为什么STL偏偏选择deque呢!?
    我的分析:
    a.用vector实现中(push_back动作为分期摊还常数时间),如果发生容器的大小改变时,将可能产生一个大动作(申请空间,拷贝构造,释放原来的元素和空间,该动作成线性复杂度) 而且vector的很多实现版本中,容器在任何情况下都从不缩减已经申请的空间容量(swap技巧除外);
    b.用deque实现时,容器的大小改变时(数据量较大),动作比vector就小多了(常数复杂度),并且当容器的大小变小时,还可以适当减小容量;但push_back 的逻辑相对vector复杂一点;
    c.用list实现时,不用考虑空间容量变化;但每次的压入弹出开销(内存时间)较大,但很平稳;那么,经过分析,在不同的应用场合,为stack选择不同的内部容器是很有必要的;如果对stack有性能上的要求,就应该考虑这一点(甚至重新写一个最适应问题要求的stack); 比如:要求有最快的平均访问速度,而且大概的容量要求也清楚(比较衡定),那么,使用vector是个不错的选择 要求每次的访问时间平稳,而不在乎平均访问时间时,那么,可以考虑使用list;所以,库默认的deque是个不错的选择,它介于vector和list之间,并且很好的综合了两者的优势;另papercrane:“oncrete policy deque相对于stack来说就像傻瓜机,乱用也不会有什么太大的问题。如你所说的平均时间和最差时间的要求,我觉得就好像hash map和tree map的性能差别一样。 ”

    (提示:文章后面还有两种想进一步融合这三种方式各自优势的stack的实现,特别是最后那个实现也许推翻了这里的表面上得到的看法);


2.自己也来写一个stack;
  由于看到VC6中实现的太差,所以自己简单写了一个stack模版实现,性质比较接近于stack >, 代码如下:


template
class mystack
//测试用
//没有考虑异常时的rollback语义
//另: Elminster指出“使用 count 不是一个好主意。保存一个指向“下一个位置”的指针应该会效率更高,而且也更易读”;
{
public:
    typedef T               value_type;
    typedef unsigned int      size_type;
    mystack():_lenght(0),_count(0) {}
    ~mystack()
   {
      if (_lenght!=0)
      {
         for (int i=0;i<_count;++i)
         {
            ((T*)(&_vData[i*sizeof(T)]))->T::~T();
         }
      }
   }
    bool empty() const   
      { return (0==_count); }
    size_type size() const
      { return _count; }
    value_type& top()
      { return *(T*)(&_vData[(_count-1)*sizeof(T)]); }
    const value_type& top() const
      { return *(T*)(&_vData[(_count-1)*sizeof(T)]); }
    void push(const value_type& x)
   {
      if (_count>=_lenght)
      {
         _resize();
      }
      new ((T*)(&_vData[_count*sizeof(T)])) T(x) ;
      ++_count;
   }
    void pop()
   {
      –_count;
      ((T*)(&_vData[_count*sizeof(T)]))->T::~T();
   }
protected:
   std::vector   _vData;
   size_type      _lenght;
   size_type      _count;
   void _resize()
   {
      if (0==_lenght)
      {
         _lenght=32;
         _vData.resize(_lenght*sizeof(T));
      }
      else
      {
         _lenght*=2;
         std::vector new_vData;
         new_vData.resize(sizeof(T)*_lenght);
         for (int i=0;i<_count;++i)
         {
            T& x=*(T*)(_vData.begin()+i*sizeof(T));
            new ((T*)(new_vData.begin()+i*sizeof(T))) T(x);
            (&x)->T::~T();
         }
         _vData.swap(new_vData);
      }
   }
};

 



测试环境:VC6,赛扬1G,256M内存
测试代码:(不好意思,代码风格被VC的环境影响太久,想改变这种风格ing)
另: Elminster指出测试代码里面,TestStack(“stack_deque_T0 : “,stack_deque_T0()) 这个做法也不太妥当。TestStack 的第二个参数是 stackT&,把一个临时对象绑在非常量引用上可能会带来问题。

 


typedef int Test0_T;//简单 POD 类型

 

class Test1_T   //较复杂的类
{
public:
   char _c;
   double _d;
   int  _i;
   char* _p;
   Test1_T():_c(),_d(),_i(),_p(new char[20])
      {}
   Test1_T(const Test1_T& x)
      :_c(),_d(),_i(),_p(new char[20])
      { (*this).operator =(x);  }
   ~Test1_T(){ delete[] _p; }
   Test1_T& operator =(const Test1_T& x)
   {
      _c=x._c; _d=x._d; _i=x._i;
      for (int i=0;i<20;++i)
         _p[i]=x._p[i];
      return *this;
   }
};

__declspec( naked ) __int64 CPUCycleCounter()//获取当前CPU周期计数(CPU周期数)
{
 __asm
 {
  RDTSC    //0F 31  //eax,edx
  ret
 }
}

template
int testProc(stackT& s,int Count)
{

   typename stackT::value_type vl=stackT::value_type();
   typename stackT::value_type vx;

   __int64   t0=::CPUCycleCounter();//返回CPU启动以来运行的周期数

   for (int c=0;c<100;++c)
   {
      int i;
      for (i=0;i         s.push(vl);
      for (i=0;i         vx=s.top();
      for (i=0;i         s.pop();
   }

   __int64   t1=::CPUCycleCounter();

   return  int((t1-t0)/100);
}

template
CString TestStack(PCSTR lab,stackT& s)
{
   int   t0=testProc(s,10);
   int   t1=testProc(s,1000);
   int   t2=testProc(s,100000);

   CString str;
   str.Format(“%10d,%10d,%10d”,t0,t1,t2);
   str+=char(13);str+=char(10);
   return lab+str;
}


void CSTACKTESTDlg::OnBUTTONTest()
{
   // TODO: Add your control notification handler code here
   using namespace std;
   typedef stack >   stack_deque_T0;
   typedef stack >   stack_deque_T1;
   typedef stack >   stack_vector_T0;
   typedef stack >   stack_vector_T1;
   typedef stack >   stack_list_T0;
   typedef stack >   stack_list_T1;
   typedef mystack            mystack_T0;
   typedef mystack            mystack_T1;

   CString str;
   str+=TestStack(“stack_deque_T0 : “,stack_deque_T0());
   str+=TestStack(“stack_vector_T0: “,stack_vector_T0());
   str+=TestStack(“stack_list_T0  : “,stack_list_T0());
   str+=TestStack(“mystack_T0     : “,mystack_T0());
   str+=char(13);str+=char(10);
   str+=TestStack(“stack_deque_T1 : “,stack_deque_T1());
   str+=TestStack(“stack_vector_T1: “,stack_vector_T1());
   str+=TestStack(“stack_list_T1  : “,stack_list_T1());
   str+=TestStack(“mystack_T1     : “,mystack_T1());

   this->m_str=str;//::MessageBox(0,str,”",0);
   UpdateData(FALSE);
}

 


测试结果:(不同使用环境下的测试情况可能不同,该数据仅作参考)
(另:测试中使用的STL是VC6自带的)

 

(计时单位:万CPU周期)
N: 10 1000 100000
stack_deque_T0  : 2695,  65621,  12191532
stack_vector_T0 :  579,  47750,   9169942
stack_list_T0   : 6028, 957515, 167660924
mystack_T0      :  230,  13766,   2550403 (效果很好嘛)

stack_deque_T1 : 10699, 1168983, 270182336
stack_vector_T1:  8043, 1247187, 250648378
stack_list_T1  : 13043, 1988924, 424801657
mystack_T1     :  6796, 1240879, 252690706(对于复杂对象,与stack_vector_T1的差不多)

Elminster给出了他的测试结果:
(计时单位:tick count) :
stack_deque_T0  : 0 156 12969
stack_vector_T0 : 0  63  7562
mystack_T0      : 0  47  6766

stack_deque_T1  : 16 500 57765
stack_vector_T1 :  0 813 91843
mystack_T1      :  0 734 85469

环境是 amd athlon 1600+, win2k sp4, 256M ddr, vs.net 2003,最大速度优化。

“结论比较有趣。对于拷贝动作比较轻量级的 T0,你的方案比 deque 和 vector 都快,但与 vector 相差不大。此时 deque 的性能落的比较后面,原因应该是 deque 的 push_back 的逻辑相对复杂(我看了看)。对于拷贝动作比较重的 T1 ,你的方案和 vector 反而要比 deque 慢。这里的原因应该是 resize 的时候拷贝的开销太重。其实我认为对于 stack 的行为模式,类似 deque 的存储结构会比较好,因为空间完全不需要连续,像vector 那样需要拷贝的 resize 是毫无必要的。你自己实现一个简洁的 deque style 数据结构,相信可以把stack 的性能再提升一个台阶。”

/
对上面自己写的stack做了些改进:


 template
class mystack
{
public:
   enum{ type_sizes=sizeof(T) };
    typedef T               value_type;
    typedef unsigned int      size_type;
    mystack():_begin(0),_end(0),_last(0) {}
    ~mystack()
   {
      if (size()>0)
      {
         for (T* i=_begin;i<_end;++i)
            i->T::~T();
      }
      if (_begin!=0) delete[] (unsigned char*)_begin;
   }
    bool empty() const   
      { return (0==size()); }
    size_type size() const
      { return (_end-_begin); }
    value_type& top()
      { return *(_end-1); }
    const value_type& top() const
      { return *(_end-1); }
    void push(const value_type& x)
   {
      if (_end<_last)
      {
         new (_end) T(x) ;
         ++_end;
      }
      else
      {
         _resize();
         new (_end) T(x);
         ++_end;
      }
   }
    void pop()
   {
      –_end;
      _end->T::~T();
   }
protected:
   T*            _begin;
   T*            _end;
   T*            _last;
   void _resize()
   {
      if (0==_begin)
      {
         const unsigned int lenght=32;
         _begin=(T*)( new unsigned char[lenght*type_sizes]);
         _end  =_begin;
         _last =_begin+lenght;
      }
      else
      {
         unsigned int old_size=size();
         unsigned int lenght=(old_size<<1);
         T* _pNewData=(T*)( new unsigned char[lenght*type_sizes]);

 

         int i=0;
         try
         {
            for (;i<(int)old_size;++i)
               new (_pNewData + i) T(*(_begin+i));
         }
         catch(…)//rollback语义
         {
            for (int r=0;r               (_pNewData + r)->T::~T();
            delete[] (unsigned char*)_pNewData;
            throw;
         }

         for (int j=0;j<(int)old_size;++j)
            (_begin+j)->T::~T();
         T* old_begin=_begin;
         _begin=_pNewData;
         _end  =_begin+old_size;
         _last =_begin+lenght;
         delete[] (unsigned char*)old_begin;
      }
   }
};

 


测试环境:VC6,XP,赛扬1G,256M内存
测试结果:

 

stack_deque_T0  : 2704,   65916,  11726143
stack_vector_T0 :  581,   45713,   9192746
stack_list_T0   : 5941, 1010118, 181184020
mystack_T0      :  172,   12210,   2590757

stack_deque_T1  : 10587, 1962722, 255087740
stack_vector_T1 :  8145, 1237245, 243123956
stack_list_T1   : 13128, 2005212, 398256062
mystack_T1      :  7127, 1173165, 251515265

测试环境:VS.net,XP,赛扬1G,256M内存
stack_deque_T0  :  525,   33407,   6468531
stack_vector_T0 :  574,   34710,   5236028
stack_list_T0   : 4753, 1060449, 165353094
mystack_T0      :  160,   10200,   2290310

stack_deque_T1  :  7470,  963957, 266757848
stack_vector_T1 :  8412, 1275307, 278246615
stack_list_T1   : 12001, 1993198, 475480319
mystack_T1      :  7317, 1209379, 262822216

对比VC6, vs.net的 deque的性能好像提高了不少,但和vector一样还有优化的空间

 

3.利用x86的虚拟空间地址原理来实现stack
    deque的实现中一般内部维护一个动态指针数组,这些指针指向数据块(每块保存多个元素),这些块在内存中是不连续的,然而deque利用软件的方式提供了一个对外的线性访问的假象;
    看一下现在的x86CPU,也有一个机制和这很像,即:保护的虚拟地址内存模型; 它把不连续的物理内存映射为一维线性内存模型,由于是硬件支持的映射,所以这种机制不会损失任何性能(deque、stack、vector等的实现也可以利用这一点)
    实现stack时,可以先开辟很大一块内存空间(足够),但不一次全部提交,开始只提交很少 的一部分物理页面,当需要增大容器容量时,只需要再提交部分物理页面给它,当需要减少容器容量时 可以收回部分页面;这个方案很像是deque的实现,但它可以获得vector式的线性内存访问能力和性能;
    当然这会占用较大量的虚拟内存地址空间,这个方案也可能和具体平台相关,但不会浪费真正的物理内存空间(如果是64位CPU,那么虚拟内存地址空间的浪费就可以不用考虑了:))


新的容器ExStack,它同时具有vector的线性访问能力,和deque的内存管理方式,所以我期待它具有这两者的性质:

 


class TAlloc//移植时 在不同的平台下需要改变的部分
{
public:
   static void* reserve(unsigned int size)         //申请保留虚拟空间地址
   {
      return ::VirtualAlloc(0,size,MEM_RESERVE,PAGE_READWRITE);
   }
   static bool commit(void* pbase,unsigned int offset,unsigned int size)   //提交物理空间
   {
      return 0!=::VirtualAlloc(((BYTE*)pbase)+offset,size,MEM_COMMIT,PAGE_READWRITE);
   }
   static bool free(void* pbase) //解除提交的物理空间,并释放申请的虚拟空间地址
   {
      return 0!=::VirtualFree(pbase,0,MEM_RELEASE);
   }
};

 

template
class ExStack
{
public:
   enum{ type_sizes=sizeof(T) };
    typedef T               value_type;
    typedef unsigned int      size_type;
    ExStack():_begin(0),_end(0),_last(0) {}
    ~ExStack()
   {
      if (size()>0)
      {
         for (T* i=_begin;i<_end;++i)
            i->T::~T();
      }
      if (_begin!=0) TAlloc::free(_begin);
   }
    bool empty() const   
      { return (0==size()); }
    size_type size() const
      { return (_end-_begin); }
    value_type& top()
      { return *(_end-1); }
    const value_type& top() const
      { return *(_end-1); }
    void push(const value_type& x)
   {
      if (_end<_last)
      {
         //std::_Construct(_end,x) ;
         new (_end) T(x) ;
         ++_end;
      }
      else
      {
         _resize();
         //std::_Construct(_end,x);
         new (_end) T(x) ;
         ++_end;
      }
   }
    void pop()
   {
      //没有实现收回物理内存的语义
      –_end;
      _end->T::~T();
   }
protected:
   T*            _begin;
   T*            _end;
   T*            _last;
   void _resize()
   {
      if (0==_begin)
      {
         const unsigned int lenght=4*1024;//4KB 边界对齐
         _begin=(T*)TAlloc::reserve(256*1024*1024);//预留地址空间 256 MB
         TAlloc::commit((void*)_begin,0,lenght*type_sizes);
         _end  =_begin;
         _last =_begin+lenght;
      }
      else
      {
         unsigned int old_size=size();
         unsigned int lenght=(old_size<<1);
         TAlloc::commit(_begin,old_size*type_sizes,(lenght-old_size)

*type_sizes);
         _last =_begin+lenght;
      }
   }
};

 


//测试条件更加接近于一般使用环境, 测试也更合理
int testProc(stackT& s,int Count)//注意测试条件变了很多!!!
{

 

   typename stackT::value_type vl=stackT::value_type();
   typename stackT::value_type vx;

   __int64   t0=::CPUCycleCounter();//返回CPU启动以来运行的周期数

   for (int c=0;c<100;++c)
   {
      stackT temps;//   
      int i;
      for (i=0;i         temps.push(vl);
      for (i=0;i         vx=temps.top();
      for (i=0;i         temps.pop();
      for (i=0;i<(Count>>1);++i)//!
         temps.push(vl);
   }

   __int64   t1=::CPUCycleCounter();

   return  int((t1-t0)/100);
}

 


测试环境:XPvc.net赛扬1GHz256MB

 

测试结果:

            N  =         10       1000     100000
stack_deque_T0 :       2942,    205538,  54123660
stack_vector_T0:       4903,     66692,  21136702
stack_list_T0  :       8003,   1232967, 239859174
mystack_T0     :       1120,     20431,  12025250
ExStack_T0     :      40710,     75998,   5908666

stack_deque_T1 :      18444,   2383833, 490387023
stack_vector_T1:      35764,   3281555,1130011712
stack_list_T1  :      19624,   2842182, 588527582
mystack_T1     :      11764,   2285440, 749906486
ExStack_T1     :      56646,   1680889, 372481178

mystack: 在大多数情况下都很优秀,但当数据量较大并且数据类型较复杂时,性能迅速下降std::stack >: 性质与mystack一致,但VC6版实现得太差了;(这种库看了就让人生气)std::stack >: 绝对速度(平均速度)没法和其他实现比,但他的优点不在这;ExStack: 初始化和销毁开销太大了些(否则ExStack在很多测试中都将领先),数据量较大时,不出意料的在简单和复杂数据类型中都领先于对手(这时才显示出综合了vector与deque的优势);但是ExStack的适用范围实在太小了,比我预期的适用范围差;

(PolyRandom:我觉得这个ExStack不错,而且可以用的地方应该不少。)


4. 极速stack的诞生myfast_stack

    一次尝试(前奏):   还是忍不住自己写了一个deque内存管理方式的_myfast_stack;性质接近于std::static >; 其实比我想象中简单多了,很快就实现出来了(因为不需要实现一个完整的deque),测试时各项性能也很优秀;只是在“简单数据类型、元素个数N很小”时才输给了vector内存管理方式实现的mystack! 这一点好像在意料之中。

    是否这就是极限了呢?我准备把源码和测试结果发布出来的时候,却突然有了新的想法… 

一般deque内存管理需要用一个动态数组来保存指向数据块的指针,因为deque要求随机访问能力;但stack访问时明显没有随机访问特性的要求,所以 保存这些指针的数据结构最低需求也是满足stack接口就足够了;进一步的改进方案出来了,先用一个list来管理这些数据块,再把list的自己的数据成员与需要管理的数据空间合成放在一起(放在同一个数据块上);


哈哈,综合性能新的明星myfast_stack诞生了; 让人不敢相信的测试结果!!!


测试环境:Win2000,VC6,赛杨466,128M ( 自己的老古董电脑 )

             N =         20       1000      50000
stack_deque_T0 :        434,      9355,    494481
stack_vector_T0:        509,     11770,    583668
stack_list_T0  :       1330,     82371,   7071753
mystack_T0     :        103,      3248,    323565
ExStack_T0     :       3258,      5580,    163168
myfast_stack_T0:         97,      2682,    190075

stack_deque_T1 :       2464,    135898,  11981644
stack_vector_T1:       4042,    228555,  22766841
stack_list_T1  :       3281,    207232,  16723651
mystack_T1     :       1916,    206413,  21782916
ExStack_T1     :       5228,    129881,  10933717
myfast_stack_T1:       1983,    124296,  10399781


实现的实质还是deque方式的,不管从那方面来看我认为它都可以将vector和list的实现淘汰掉!

!!!myfast_stack太恐怖了,几乎没有缺陷!!!

 


///源代码//

 

//管理内存的list
template
class Tdata_list//管理myfast_stack的内存
{
   struct TNode//节点类型
   {
      TNode*         pPrev;
      TNode*         pNext;
      unsigned char   Data[byte_size];//数据空间
   };
public:
   Tdata_list():pNodeBegin(0),pNodeCur(0),_size(0){}
   unsigned int size() const { return _size; }
   void push()//配置空间
   {
      if (0==pNodeBegin)
      {
         pNodeBegin=new TNode;
         pNodeBegin->pPrev=0;
         pNodeBegin->pNext=0;
         pNodeCur=pNodeBegin;
      }
      else if (0!=pNodeCur->pNext)//还有一个空余的Node
      {
         pNodeCur=pNodeCur->pNext;
      }
      else
      {
         TNode* pNodeEnd=new TNode;
         pNodeEnd->pPrev=pNodeCur;
         pNodeEnd->pNext=0;
         pNodeCur->pNext=pNodeEnd;
         pNodeCur=pNodeEnd;   
      }
      ++_size;
   }
   ~Tdata_list()
   {
      for (TNode* i=pNodeBegin;i!=0; )
      {
         TNode* pNext=i->pNext;
         delete i;
         i=pNext;
      }
   }
   unsigned char* top()
   {
         return pNodeCur->Data;
   }
   void pop()
   {
      TNode*& pTmp=pNodeCur->pNext;
      if (pTmp!=0)
      {
         delete pTmp;//留一个空余Node,多余的释放
         pTmp=0;
      }
      pNodeCur=pNodeCur->pPrev;
      –_size;
   }
private:
   TNode*   pNodeCur;
   TNode*   pNodeBegin;
   unsigned int _size;//用来追踪list的使用大小
};


//myfast_stack
//注意它的实现并没有以降低stack的通用能力来提高性能
//改进可能:1.提供专署的内存分配器,而不是默认的new/delete; (就可以和SGI中的stack对比测试了)
template
class myfast_stack
{
public:
   enum{ type_sizes=sizeof(T),//!
         node_width=(1020/type_sizes)+1//使用这种策略,stack >也没有存在必要了
      };
    typedef T                           value_type;
    typedef unsigned int                  size_type;
   typedef Tdata_list   Tbase_alloc;

    myfast_stack():_node_begin(0),_node_cur(0),_node_last(0) {}
    ~myfast_stack()
   {
      if ((!IsPOD)&&(_NodeList.size()>0))
      {
         for (T* i=_node_begin;i<_node_cur;++i)
            i->T::~T();
         int nsize=(int)_NodeList.size();
         for (int j=0;j<(nsize-1);++j)
         {
            _NodeList.pop();
            _node_cur=(T*)_NodeList.top();
            for (int i=0;i            {
               _node_cur->T::~T();
               ++_node_cur;
            }
         }
      }
   }
    bool empty() const   
      { return (0==size()); }
    size_type size() const
      { return (_node_last-_node_begin)+node_width*(min((int)_NodeList.size()-1,0)); }
    value_type& top()
      { return *(_node_cur-1); }
    const value_type& top() const
      { return *(_node_cur-1); }
    void push(const value_type& x)
   {
      if (_node_cur==_node_last)
      {
         _ToNextNode();
      }
      //std::_Construct(_node_cur,x);
      new (_node_cur) T(x) ;
      ++_node_cur;
   }
    void pop()
   {
      if (_node_cur==_node_begin)
      {
         _ToPrevNode();
      }
      –_node_cur;
      _node_cur->T::~T();
   }
protected:
   T*            _node_begin;
   T*            _node_cur;
   T*            _node_last;
   Tbase_alloc      _NodeList;
   void _ToNextNode()
   {
      _NodeList.push();   
      _node_begin=(T*)(_NodeList.top());
      _node_last=_node_begin+node_width;
      _node_cur=_node_begin;
   }
   void _ToPrevNode()
   {
      _NodeList.pop();
      _node_begin=(T*)(_NodeList.top());
      _node_last=_node_begin+node_width;
      _node_cur=_node_last;
   }
};

C++ 中的 cast(显式类型转换)

C++ 引入了 const_cast, reinterpret_cast 之类的新的显式类型转换方式,不仅大多数 C 程序员觉得不是很习惯,就连某些有经验的C++ 程序员都会在一些细节上犯错。诚然,既然我们可以简单的写出:

int i = (int)p;// p is a pointer

这样的显式转换,为什么还要使用

int i = reinterpret_cast( p );

这么复杂的形式呢?

这篇文章的目的是简单介绍 C++ 的类型转换系统,并对使用和扩展进行一些讨论。

C++ 引入了 const_cast, reinterpret_cast 之类的新的显式类型转换方式,不仅大多数 C 程序员觉得不是很习惯,就连某些有经验的C++ 程序员都会在一些细节上犯错。诚然,既然我们可以简单的写出:

int i = (int)p;// p is a pointer

这样的显式转换,为什么还要使用

int i = reinterpret_cast( p );

这么复杂的形式呢?

这篇文章的目的是简单介绍 C++ 的类型转换系统,并对使用和扩展进行一些讨论。

1.  为什么需要类型转换?
类型转换被用来把一个类型的值转换成另一个类型。类似于 C++ 这样的编程语言是强类型的,因此每一个值都有它相应的类型。当你需要把一个值转换为另一个类型时,你需要使用下列方式中的一种:隐式转换,显式转换和无法转换。假设我们使用老式的显式转换:

char c = ‘a’;
int* p = NULL;
int a = c;// 隐式转换
a=(int) p; // 显式转换
double d=(double) p;// 无法转换

通常,隐式转换意味着编译器认为你的转换是合理的或者是安全的;显式转换意味着编译器能够找到一个转换方式,但是它不保证这个转换是否安全,所以需要程序员额外指出;而无法转换则意味着编译器无法发现一条直接的路径来进行类型转换。

2.  为什么需要 C++ 风格的显式转换?
C++ 风格的显式转换为我们提供了更精确的语义和对其进一步扩展的可能。在 C 语言中,我们可以用一个简单的 (int*) 来完成下面的转换:

const char* s = 0;
int* p = (int*) s;

这一句语句,不仅转换了类型,还把 const 也去掉了。通常如果我们看到一句游离的显式转换,我们不能立即知道作者的意图,这也为今后的错误埋下了伏笔。C++ 风格的现实转换通过区分各种转换情况来增加安全性:通过 const_cast 来取消 const、volatile 之类的修饰,通过 static_cast 来做相关类型的转换,通过 reinterpret_cast 来做低级的转换,…。有一个例子可以说明这些转换的“精确”程度:

class Interface
{
    int member;
};

class Base {};

class Derived : public Interface, public Base {};

int main()
{
    Base* b = (Base*)100;
    Derived* d1 = reinterpret_cast( b );
    Derived* d2 = static_cast( b );
}

这段代码中,两个 cast 都是合法的,但是意义不同。前者意味着“把 b 的指针的值直接赋给 d1,并且把它作为 Derived 类型解释”,后者意味着“根据相关类型信息来做转换,如果可能,对 b 指针做一些偏移”。在上面这个例子里面,d1 和 d2 是不相等的!可能由于很多书上都说:如果你要在指针之间互相转换,应该使用 reinterpret_cast,所以不少程序员使用 reinterpret_cast 来做一切指针类型转换,虽然通常他们不会得到错误,但是的确是不小的隐患。

本来我这个例子的黑体部分使用的是0,有一位网友指出,C++在进行cast的时候会对0进行特殊处理。也就是说,在上面的例子中,如果使用0,那么d1和d2是一样的。

3.  一些例子

(1)  itf_cast
在 COM 中,如果我有一个指向 IHtmlDocument 的接口指针,并且我想从中获得一个 IPersistFile 的指针,我们可以用下述代码:

IPersistFile* pPersistFile;
if( FAILED( pHtmlDocument->QueryInterface(IID_IPersistFile, (LPVOID*) &pPersistFile) ) )
    throw something;

这段代码很简单但是不够直接,所以我们可以实现这样一个函数:

template
T1 itf_cast(T2 v2)
{
    if( v2 == 0 )
        return 0;
    T1 v1;
    if( FAILED( v2->QueryInterface( __uuidof(*v1), (LPVOID*)&v1 ) ) )
        throw bad_cast();
    return v1;
}

然后我们可以把上面的语句写成

pPersistFile = itf_cast( pHtmlDocument );

这非常的直观。仔细的读者可能会发现 __uuidof 不是标准的 C++ 所定义的,而是 VC 的一个扩展。事实上,在这里你可以用 traits 很简单的实现同样的功能。

(2)  stream_cast
有时候,我们经常会遇到一些自定义类型之间转换问题,譬如说 std::string 和 double,甚至 std::string 和 RECT 之类的转换。如果参与转换的两个类型都定义了输入输出运算(精确的说,源类型支持输出运算,目的类型支持输入运算),那么我们可以用以下的方式来进行转换:

template
T1 stream_cast(const T2& v2)
{
    std::stringstream str;
    str << v2;
    T1 t1;
    str >> t1;
    if( !str )
        throw bad_cast();
    return t1;
}

这样一来,你可以用以下语句进行转换:

string s(“0.5″);
double d = stream_cast( s );

(3)  safe_cast
有时候我们希望我们的显式转换是安全的,这里安全的定义是,这个值在两个类型中德表示都是无损的。也就是说,(T2)(T1)v1 == v1。那我们可以定义这样的显式转换:

template
T1 stream_cast(const T2& v2)
{
    if( (T2)(T1) v2 != v2 )
        throw bad_cast();
    return (T1) v2;
}

于是,stream_cast(1000); 这样的语句就会抛出异常。

上面是我个人对于 C++ 风格的显式类型转换的一些理解和看法,希望有兴趣的人一起讨论。

你可能感兴趣的:(C语言,C++,VC,c,语言,测试,exception,vector,file)