华亮's profile灌吧PhotosBlogListsMore Tools Help

Blog


    1/14/2009

    [转]c/c++预处理指令

    由ANSI的标准规定, 预处理指令主要包括:
         #define
         #error
         #if
         #else
         #elif
         #endif
         #ifdef
         #ifndef
         #undef
         #line
         #pragma
        由上述指令可以看出, 每个预处理指令均带有符号"#"。下面只介绍一些常
    用指令。
        1. #define 指令
        #define指令是一个宏定义指令, 定义的一般形式是:
         #define 宏替换名字符串(或数值)
        由#define指令定义后,  在程序中每次遇到该宏替换名时就用所定义的字符
    串(或数值)代替它。
        例如: 可用下面语句定义TRUE表示数值1, FALSE表示0。
         #define TRUE 1
         #define FALSE 0
        一旦在源程序中使用了TRUE和FALSE, 编译时会自动的用1和0代替。
        注意:
        1. 在宏定义语名后没有";"
        2. 在Turbo C程序中习惯上用大写字符作为宏替换名, 而且常放在程序开头。
        3. 宏定义还有一个特点, 就是宏替换名可以带有形式参数,  在程序中用到
    时, 实际参数会代替这些形式参数。
        例如:
         #define MAX(x, y) (x>y)?x:y
         main()
         {
              int i=10, j=15;
              printf("The Maxmum is %d", MAX(i, j));
         }
        上例宏定义语句的含义是用宏替换名MAX(x, y)代替x, y中较大者,  同样也
    可定义:
         #define MIN(x, y) (x<y)?x:y
        表示用宏替换名MIN(x, y)代替x, y中较小者。

        2. #error指令
        该指令用于程序的调试, 当编译中遇到#error指令就停止编译。其一般形式
    为:
         #error 出错信息
        出错信息不加引号, 当编译器遇到这个指令时, 显示下列信息并停止编译。
          Fatal: filename linename error directive

        3. #include 指令
           #include 指令的作用是指示编译器将该指令所指出的另一个源文件嵌入
    #include指令所在的程序中, 文件应使用双引号或尖括号括起来。Turbo C 库函
    数的头文件一般用#include指令在程序开关说明。
        例如:
         #include <stdio.h>
        程序也允许嵌入其它文件, 例如:
         main()
         {
              #include <help.c>
         }
        其中help.c为另一个文件, 内容可为
           printf("Glad to meet you here!");
        上例编译时将按集成开发环境的Options/Directories/Include directories
    中指定的包含文件路径查找被嵌入文件。
        4. #if、#else、#endif指令
        #if、#els和#endif指令为条件编择指令, 它的一般形式为:
         #if 常数表达式
              语句段;
         #else
              语句段;
         #endif
        上述结构的含义是: 若#if指令后的常数表达式为真, 则编译#if到#else 之
    间的程序段; 否则编译#else到#endif之间的程序段。
        例如:
         #define MAX 200
         main()
         {
              #if MAX>999
                   printf("compiled for bigger\n");
              #else
                   printf("compiled for small\n");
              #endif
         }
        5. #undef指令
        #undef指令用来删除事先定义的宏定义, 其一般形式为:
         #undef 宏替换名
        例如:
          #define TRUE 1
           ...
          #undef TURE
        #undef主要用来使宏替换名只限定在需要使用它们的程序段中
        6.#pragma
        其格式一般为: #Pragma Para
    其中Para 为参数,下面来看一些常用的参数。
    (1)message 参数。 Message 参数是我最喜欢的一个参数,它能够在编译信息输出窗
    口中输出相应的信息,这对于源代码信息的控制是非常重要的。其使用方法为:
    #Pragma message(“消息文本”)
    当编译器遇到这条指令时就在编译输出窗口中将消息文本打印出来。
    当我们在程序中定义了许多宏来控制源代码版本的时候,我们自己有可能都会忘记有没有正
    确的设置这些宏,此时我们可以用这条指令在编译的时候就进行检查。假设我们希望判断自
    己有没有在源代码的什么地方定义了_X86这个宏可以用下面的方法
    #ifdef _X86
    #Pragma message(“_X86 macro activated!”)
    #endif
    当我们定义了_X86这个宏以后,应用程序在编译时就会在编译输出窗口里显示“_
    X86 macro activated!”。我们就不会因为不记得自己定义的一些特定的宏而抓耳挠腮了。
    (2)另一个使用得比较多的pragma参数是code_seg。格式如:
    #pragma code_seg( [\section-name\[,\section-class\] ] )
    它能够设置程序中函数代码存放的代码段,当我们开发驱动程序的时候就会使用到它。
    (3)#pragma once (比较常用)
    只要在头文件的最开始加入这条指令就能够保证头文件被编译一次,这条指令实际上在VC6
    中就已经有了,但是考虑到兼容性并没有太多的使用它。
    (4)#pragma hdrstop表示预编译头文件到此为止,后面的头文件不进行预编译。BCB可以预
    编译头文件以加快链接的速度,但如果所有头文件都进行预编译又可能占太多磁盘空间,所
    以使用这个选项排除一些头文件。
    有时单元之间有依赖关系,比如单元A依赖单元B,所以单元B要先于单元A编译。你可以用#p
    ragma startup指定编译优先级,如果使用了#pragma package(smart_init) ,BCB就会根据优先级的大小先后编译。
    (5)#pragma resource \*.dfm\表示把*.dfm文件中的资源加入工程。*.dfm中包括窗体
    外观的定义。
    (6)#pragma warning( disable : 4507 34; once : 4385; error : 164 )
    等价于:
    #pragma warning(disable:4507 34) // 不显示4507和34号警告信息
    #pragma warning(once:4385) // 4385号警告信息仅报告一次
    #pragma warning(error:164) // 把164号警告信息作为一个错误。
    同时这个pragma warning 也支持如下格式:
    #pragma warning( push [ ,n ] )
    #pragma warning( pop )
    这里n代表一个警告等级(1---4)。
    #pragma warning( push )保存所有警告信息的现有的警告状态。
    #pragma warning( push, n)保存所有警告信息的现有的警告状态,并且把全局警告
    等级设定为n。
    #pragma warning( pop )向栈中弹出最后一个警告信息,在入栈和出栈之间所作的
    一切改动取消。例如:
    #pragma warning( push )
    #pragma warning( disable : 4705 )
    #pragma warning( disable : 4706 )
    #pragma warning( disable : 4707 )
    //.......
    #pragma warning( pop )
    在这段代码的最后,重新保存所有的警告信息(包括4705,4706和4707)。
    (7)pragma comment(...)
    该指令将一个注释记录放入一个对象文件或可执行文件中。
    常用的lib关键字,可以帮我们连入一个库文件。
    (8)·通过#pragma pack(n)改变C编译器的字节对齐方式
    在C语言中,结构是一种复合数据类型,其构成元素既可以是基本数据类型(如int、
    long、float等)的变量,也可以是一些复合数据类型(如数组、结构、联合等)的
    数据单元。在结构中,编译器为结构的每个成员按其自然对界(alignment)条件分
    配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和
    整个结构的地址相同。
    例如,下面的结构各成员空间分配情况:
    struct test
    {
         char x1;
         short x2;
         float x3;
         char x4;
    };
         结构的第一个成员x1,其偏移地址为0,占据了第1个字节。第二个成员x2为
    short类型,其起始地址必须2字节对界,因此,编译器在x2和x1之间填充了一个
    空字节。结构的第三个成员x3和第四个成员x4恰好落在其自然对界地址上,在它
    们前面不需要额外的填充字节。在test结构中,成员x3要求4字节对界,是该结构
    所有成员中要求的最大对界单元,因而test结构的自然对界条件为4字节,编译器
    在成员x4后面填充了3个空字节。整个结构所占据空间为12字节。更改C编译器的
    缺省字节对齐方式
        在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配
    空间。一般地,可以通过下面的方法来改变缺省的对界条件:
      · 使用伪指令#pragma pack (n),C编译器将按照n个字节对齐。
         · 使用伪指令#pragma pack (),取消自定义字节对齐方式。
         另外,还有如下的一种方式:
         · __attribute((aligned (n))),让所作用的结构成员对齐在n字节自然边界上。
    如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。
         · __attribute__ ((packed)),取消结构在编译过程中的优化对齐,按照实际
    占用字节数进行对齐。
    以上的n = 1, 2, 4, 8, 16... 第一种方式较为常见。
    应用实例
      在网络协议编程中,经常会处理不同协议的数据报文。一种方法是通过指针偏移的
    方法来得到各种信息,但这样做不仅编程复杂,而且一旦协议有变化,程序修改起来
    也比较麻烦。在了解了编译器对结构空间的分配原则之后,我们完全可以利用这
    一特性定义自己的协议结构,通过访问结构的成员来获取各种信息。这样做,
    不仅简化了编程,而且即使协议发生变化,我们也只需修改协议结构的定义即可,
    其它程序无需修改,省时省力。下面以TCP协议首部为例,说明如何定义协议结构。
    其协议结构定义如下:
    #pragma pack(1) // 按照1字节方式进行对齐
    struct TCPHEADER
    {
         short SrcPort; // 16位源端口号
         short DstPort; // 16位目的端口号
         int SerialNo; // 32位序列号
         int AckNo; // 32位确认号
         unsigned char HaderLen : 4; // 4位首部长度
         unsigned char Reserved1 : 4; // 保留6位中的4位
         unsigned char Reserved2 : 2; // 保留6位中的2位
         unsigned char URG : 1;
         unsigned char ACK : 1;
         unsigned char PSH : 1;
         unsigned char RST : 1;
         unsigned char SYN : 1;
         unsigned char FIN : 1;
         short WindowSize; // 16位窗口大小
         short TcpChkSum; // 16位TCP检验和
         short UrgentPointer; // 16位紧急指针
    };
    #pragma pack() // 取消1字节对齐方式

    12/15/2008

    [转] 更深入一点理解 switch 语句 及 c/c++ 对 const 的处理

    更深入一点理解 switch 语句 及 c/c++ 对 const 的处理
                                        谢煜波
    前段时间在论坛上看见台湾李维在<<Borland传奇>>一书中对windows编程模式中,消息处理部分有如下的一些分析:
    他说,在消息处理循环中,一般的形式是这样的
    MSG msg ;
    switch( msg ){
            case WM_XXXXXXX :
                    ....
            case WM_XXXXXXX :
                    ....
            case WM_XXXXXXX :
                    ....
    } ;
    李维说,这种模式是很低效的,因应经过汇编后,这种C代码会产生如下的汇编代码
            cmp .... .....
            jnz .... .....
            cmp .... .....
            jnz .... .....
            cmp .... .....
            jnz .... .....
    如果你的 case 足够多,比如,你有一万条消息需要处理,而不幸的是你把一条最常用的消息
    放在了最后一位,那么当这条消息要得到处理,会首先经过一万次的cmp与jnz, 李维认为,这
    是非常非常低效的,实在是低效的忍无可忍,无需再忍~~:P
    在起初,我也是这样认为的,但近来的阅读及实验却发现,这种看法非常片面,今天就来谈谈这个问题( 所有实验在 linux 平台下完成 )
    首先看一到用 c 编写的程序
    /* -------------------- filename : ta.c --------------- */
    int switch_test_first( int x )
    {
            int res ;
            switch( x ){
                    case 100 :
                            res = 1 ;
                            break ;
                    case 102 :
                            res = 2 ;
                            break ;
                    case 103 :
                            res = 3 ;
                            break ;
            }
            return res ;
    }
    然后,我们用 gcc 将它编译成汇编文件( 使用 -S 开关 )
    gcc -S ta.c
    将得到如下的汇编文件( ta.s )
            .file   "ta.c"
            .text
    .globl switch_test_first
            .type   switch_test_first,@function
    switch_test_first:
            pushl   %ebp
            movl    %esp, %ebp
            subl    $8, %esp
            movl    8(%ebp), %eax
            .file   "ta.c"
            .text
    .globl switch_test_first
            .type   switch_test_first,@function
    switch_test_first:
            pushl   %ebp
            movl    %esp, %ebp
            subl    $8, %esp
            movl    8(%ebp), %eax
            movl    %eax, -8(%ebp)
            cmpl    $102, -8(%ebp)          // 1
            je      .L4                     // 2
            cmpl    $102, -8(%ebp)          // 3   
            jg      .L8                     // 4
            cmpl    $100, -8(%ebp)          // 5
            je      .L3                     // 6
            jmp     .L2                     // 7
    .L8:
            cmpl    $103, -8(%ebp)
            je      .L5
            jmp     .L2
    .L3:
            movl    $1, -4(%ebp)
            jmp     .L2
    .L4:
            movl    $2, -4(%ebp)
            jmp     .L2
    .L5:
            movl    $3, -4(%ebp)
    .L2:
            movl    -4(%ebp), %eax
            leave
            ret
    .Lfe1:
            .size   switch_test_first,.Lfe1-switch_test_first
            .ident  "GCC: (GNU) 3.2.2 20030222 (Red Hat Linux 3.2.2-5)"
    注意看文件中 // 1 ~ // 7 的部份,从这个部份,我们可以看出,gcc确实是把一些case语句转成了李维所说的那种方式进行处理,我们看见了代码中存在有众多的 cmpl 与 jmp 语句
    这就相当于你使用if..else..一样,但是否总是这样呢?
    我们下面改动一下 ta.c 这个文件,在里面再多加一些 case 语句
    /* -------------- filename : new_ta.c ------------------- */
    int switch_test_first( int x )
    {
            int res ;
            switch( x ){
                    case 100 :
                            res = 1 ;
                            break ;
                    case 102 :
                            res = 2 ;
                            break ;
                    case 103 :
                            res = 3 ;
                            break ;
                    case 104 :
                            res = 4 ;
                            break ;
                    case 105 :
                            res = 5 ;
                            break ;
                    case 106 :
                            res = 6 ;
                            break ;
            }
            return res ;
    }
    这个 new_ta.c 与原来的 ta.c 在结构上完全相同,唯一不同的就是 case 语句的数量变多了,下面我们来编译一下这个文件
    gcc -S new_ta.c
    下面是我们产生的更新的汇编文件
            .file   "new_ta.c"
            .text
    .globl switch_test_first
            .type   switch_test_first,@function
    switch_test_first:
            pushl   %ebp
            movl    %esp, %ebp
            subl    $8, %esp
            movl    8(%ebp), %eax
            subl    $100, %eax
            movl    %eax, -8(%ebp)
            cmpl    $6, -8(%ebp)
            ja      .L2
            movl    -8(%ebp), %edx
            movl    .L9(,%edx,4), %eax
            jmp     *%eax
            .section        .rodata
            .align 4
            .align 4
    .L9:                             // A
            .long   .L3
            .long   .L2
            .long   .L4
            .long   .L5
            .long   .L6
            .long   .L7
            .long   .L8
            .text
    .L3:                            // 1
            movl    $1, -4(%ebp)
            jmp     .L2
    .L4:                            // 2
            movl    $2, -4(%ebp)
            jmp     .L2
    .L5:                            // 3
            movl    $3, -4(%ebp)
            jmp     .L2             // 4  
    .L6:
            movl    $4, -4(%ebp)
            jmp     .L2             // 5
    .L7:
            movl    $5, -4(%ebp)    // 6
            jmp     .L2
    .L8:                            // 7
            movl    $6, -4(%ebp)
    .L2:                           
            movl    -4(%ebp), %eax
            leave
            ret
    .Lfe1:
            .size   switch_test_first,.Lfe1-switch_test_first
            .ident  "GCC: (GNU) 3.2.2 20030222 (Red Hat Linux 3.2.2-5)"
    仔细比较一下这个最新的 new_ta.s 与前面的 ta.s,精华全在里面了!
    首先 new_ta.s 比前面的 ta.s 多了一个 .L9 部分,而且它的 // 1 ~ // 7 中没有了前面
    ta.s 文件中所存在的众多的 cmpl 与 jmp 语句,那么,现在这样的代码又是怎么实现
    switch 语句中的跳转的呢?我们来仔细分析一下它新多出来的 .L9 部份。
            .section        .rodata
            .align 4
            .align 4
    .L9:
            .long   .L3
            .long   .L2
            .long   .L4
            .long   .L5
            .long   .L6
            .long   .L7
            .long   .L8
            .text
    显而易见,.L9 部份是一个我们最常见的数据结构——表,它的每一项都是一个标号,而这个标号,恰恰是每个 case 语句的入口标号!
    这很容易让我们想到,它很可能是用了一张表来存放所有的 case 语句的入口,然后,在
    执行 switch 语句的时候就从这个表中直接检出相应的 case 语句的入口地址,然后跳转
    到相应的 case 语句去执行,就像hash_table似的。具体是不是这样呢?我们看看进入
    switch 部份的代码:
            pushl   %ebp
            movl    %esp, %ebp
            subl    $8, %esp
            movl    8(%ebp), %eax
            subl    $100, %eax
            movl    %eax, -8(%ebp)
            cmpl    $6, -8(%ebp)
            ja      .L2
            movl    -8(%ebp), %edx
            movl    .L9(,%edx,4), %eax // 1
            jmp     *%eax              // 2
    果然如此!首先在 // 1 处根据%edp的值(其值相当于表的下标)在.L9的表中找到相应
    case 语句的入口地址,并把这个地址存到%eax中,然后通过 // 2 (这是一个间接跳转
    语句)转到%eax存放的地址中,也即相应的case语句处。
    C编译器,果然聪明!
    通过这个分析我们可以知道如下两点:
    1. 当 case 语句少的时候,C编译器将其转成 if..else.. 类型进行处理,运用较多的
       cmp 与 jmp 语句 ,而当 case 语句较多的时候,C编译器会出成一个跳转表,而直
       接通过跳转表进行跳转,这让 switch 具有非常高的效律,而且效律几乎不会因为
       case 语句的增长而减小,李维所担忧的问题是完全不会发生的
    2. 可以问答下面几个问题:
       1. 为什么 case 语句中需要的是整数类型而不能是其余的类型?
          这是因为,case 语句中的这个值是用来做跳转表的下标的,因此,当然必须是整数
       2. 为什么 case 语句在不加break的时候具有直通性?
          这是因为跳转是在进入 switch 是计算出的,而不是在case语句中计算出的,整个
          case 语句群就是一块完整而连续的代码,只是switch让其从不同的位置开始执行。
    上面的内容,在《Computer Systems A Programmer's Perspective》中有很详细的论述,
    感兴趣可以去找来仔细看看~~~
    既然,case 语句需要的是整数的常量值,那么我们是否可用 const 类型呢?比如下面
    一段代码:
    const int c_1 = 100 ;
    const int c_2 = 102 ;
    void test( int x )
    {
            switch( x ){
                    case c_1 :
                            ++x ;
                    case c_2 :
                            --x ;
            }
    }
    这段代码,用 c 编译器编译,编译器会提示错误,但在 c++ 编译器中却不会,这主要是由于 c , 与 c++ 编译器对 const 这个东东的处理不同。我们来看看下面一段 c 程序
    /*------------- filename : const_c.c -----------*/
    const int a = 15 ;
    void f( int x )
    {
            x = a ;
    }
    同样用 gcc 编译
    gcc -S const_c.c
    然后,来看看它的汇编文件
            .file   "const_c.c"
    .globl a
            .section        .rodata
            .align 4
            .type   a,@object
            .size   a,4
    a:                             // 1
            .long   15
            .text
    .globl f
            .type   f,@function
    f:
            pushl   %ebp
            movl    %esp, %ebp
            movl    a, %eax        // 2 
            movl    %eax, 8(%ebp)
            leave
            ret
    .Lfe1:
            .size   f,.Lfe1-f
            .ident  "GCC: (GNU) 3.2.2 20030222 (Red Hat Linux 3.2.2-5)"
    注意 // 1 处,C 编译器为 a 分配了地址,并把它的值设为 15 ,而在 // 2 处,它是将
    a 这个地址中的值赋给了 %eax,这同一般的普通变量而非const 变量赋值没什么两样
    下面我们用 c++ 编译器来编译这段代码,它产生的汇编文件如下:
            .file   "const_cpp.cpp"
            .text
            .align 2
    .globl _Z1fi
            .type   _Z1fi,@function
    _Z1fi:
    .LFB2:
            pushl   %ebp
    .LCFI0:
            movl    %esp, %ebp
    .LCFI1:
            movl    $15, 8(%ebp)  // 1
            leave
            ret
    .LFE2:
    .Lfe1:
            .size   _Z1fi,.Lfe1-_Z1fi
            .section        .rodata
            .align 4
            .type   a,@object
            .size   a,4
    a:
            .long   15
            .ident  "GCC: (GNU) 3.2.2 20030222 (Red Hat Linux 3.2.2-5)"
    同样注意// 1 处,它以经把 a 的值用 15 来取代了,
    也就是说,在c中const变量的行为更像一个非const变量,而在cpp中,const变量的行为就像是#define
    由于 c++ 中,const 变量的值是在编译时就计算出来的,因此,它可以用在 case 语句中,而 c 中,const值在编译时只是一个变量的地址,因此,它无法用在 case 语句中.
    -----------------------------------------------------------------------------
    参考文献:<<Computer Systems A Programmer's Perspective>>
    转载请注明原作者,以出处~~

    [转] 在VC中使用Windows管道技术编程

      不知你是否用过这样的程序,他们本身并没有解压缩的功能,而是调用DOS程序PKZIP完成ZIP包的解压缩。但是在程序运行时又没有DOS控制台的窗口出现而且一切本应该在DOS下显示的信息都出现在了那个安装程序的一个文本框里。这种设计既美观又可以防止少数眼疾手快的用户提前关了你的DOS窗口。
      现在就来讨论一下,如何用匿名管道技术实现这个功能。
      管道技术由来已久,相信不少人对DOS命令里的管道技术最为熟悉。当我们type一个文件的时候如果想让他分页现实可以输入
      C:\>type autoexec.bat|more
      这里“|”就是管道操作符。他以type输出的信息为读取端,以more的输入端为写入端建立的管道。
      Windows中使用较多的管道也是匿名管道,它通过API函数CreatePipe创建。

    BOOL CreatePipe(
     PHANDLE hReadPipe, // 指向读端句柄的指针
     PHANDLE hWritePipe, // 指向写端句柄的指针
     LPSECURITY_ATTRIBUTES lpPipeAttributes, // 指向安全属性结构的指针
     DWORD nSize // 管道的容量
    );

      上面几个参数中要注意hReadPipe,hWritePipe是指向句柄的指针,而不是句柄(我第一次用的时候就搞错了)。nSize一般指定为0,以便让系统自己决定管道的容量。现在来看安全属性结构,SECURITY_ATTRIBUTES。

    typedef struct _SECURITY_ATTRIBUTES { // sa
     DWORD nLength;
     LPVOID lpSecurityDescriptor;
     BOOL bInheritHandle;
    } SECURITY_ATTRIBUTES;

      nLength 是结构体的大小,自然是用sizeof取得了。lpSecurityDescriptor是安全描述符(一个C-Style的字符串)。 bInheritHandle他指出了安全描述的对象能否被新创建的进程继承。先不要管他们的具体意义,使用的时候自然就知道了。
      好,现在我们来创建一个管道

    HANDLE hReadPipe, hWritePipe;
    SECURITY_ATTRIBUTES sa;
    sa.nLength = sizeof(SECURITY_ATTRIBUTES);
    sa.lpSecurityDescriptor = NULL; file://使用系统默认的安全描述符
    sa.bInheritHandle = TRUE; file://一定要为TRUE,不然句柄不能被继承。
    CreeatePipe(&hReadPipe,&hWritePipe,&sa,0);

      我们的管道建好了。当然这不是最终目的,我们的目的是把DOS上的一个程序输出的东西重定向到一个Windows程序的Edit控件。所以我们还需要先启动一个DOS的程序,而且还不能出现DOS控制台的窗口(不然不就露馅了吗)。我们用CreateProcess创建一个DOS程序的进程。

    BOOL CreateProcess(
     LPCTSTR lpApplicationName, // C-style字符串:应用程序的名称
     LPTSTR lpCommandLine, // C-style字符串:执行的命令
     LPSECURITY_ATTRIBUTES lpProcessAttributes, // 进程安全属性
     LPSECURITY_ATTRIBUTES lpThreadAttributes, // 线程安全属性
     BOOL bInheritHandles, // 是否继承句柄的标志
     DWORD dwCreationFlags, // 创建标志
     LPVOID lpEnvironment, // C-Style字符串:环境设置
     LPCTSTR lpCurrentDirectory, // C-Style字符串:执行目录
     LPSTARTUPINFO lpStartupInfo, // 启动信息
     LPPROCESS_INFORMATION lpProcessInformation // 进程信息
    );

      先别走,参数是多了点,不过大部分要不不用自己填要不填个NULL就行了。lpApplication随便一点就行了。lpCommandLine可是你要执行的命令一定要认真写好。来,我们瞧瞧lpProcessAttributes和lpThreadAttributes怎么设置。哎?这不就是刚才那个吗。对阿,不过可比刚才简单。由于我们只是创建一个进程,他是否能在被继承不敢兴趣所以这两个值全为NULL。bInHeritHandles也是一定要设置为TRUE的,因为我们既然要让新的进程能输出信息到调用他的进程里,就必须让新的进程继承调用进程的句柄。我们对创建的新进程也没什么别的苛求,所以dwCreationFlags就为NULL了。lpEnvironment和lpCurrentDirectory根据你自己的要求是指一下就行了,一般也是NULL。接下来的lpStartupInfo可是关键,我们要认真看一下。

    typedef struct _STARTUPINFO { // si
     DWORD cb;
     LPTSTR lpReserved;
     LPTSTR lpDesktop;
     LPTSTR lpTitle;
     DWORD dwX;
     DWORD dwY;
     DWORD dwXSize;
     DWORD dwYSize;
     DWORD dwXCountChars;
     DWORD dwYCountChars;
     DWORD dwFillAttribute;
     DWORD dwFlags;
     WORD wShowWindow;
     WORD cbReserved2;
     LPBYTE lpReserved2;
     HANDLE hStdInput;
     HANDLE hStdOutput;
     HANDLE hStdError;
    } STARTUPINFO, *LPSTARTUPINFO;

      这么多参数,一个一个写肯定累死了。没错,MS早就想到会累死人。所以提供救人一命的API函数GetStartupInfo。

    VOID GetStartupInfo(
     LPSTARTUPINFO lpStartupInfo
    );

      这个函数用来取得当前进程的StartupInfo,我们新建的进程基本根当前进程的StartupInfo差不多,就借用一下啦。然后再小小修改一下即可。

      我们要改的地方有这么几个:cb,dwFlags,hStdOutput,hStdError,wShowWindow。先说cb,他指的是 STARTUPINFO的大小,还是老手法sizeof。再说wShowWindow,他制定了新进程创建时窗口的现实状态,这个属性当然给为 SW_HIDE了,我们不是要隐藏新建的DOS进程吗。哈哈,看到hStdOutput和hStdError,标准输出和错误输出的句柄。关键的地方来了,只要我们把这两个句柄设置为hWrite,我们的进程一旦有标准输出,就会被写入我们刚刚建立的匿名管道里,我们再用管道的hReadPipe句柄把内容读出来写入Edit控件不就达到我们的目的了吗。呵呵,说起来也真是听容易的阿。这几个关键参数完成了以后,千万别忘了dwFlags。他是用来制定 STARTUPINFO里这一堆参数那个有效的。既然我们用了hStdOutput,hStdError和wShowWindow那dwFlags就给为 STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES。
      现在回到CreateProcess的最后一个参数lpProcessInformation(累!)。呵呵,这个参数不用自己填了,他是CreateProcess返回的信息,只要给他一个PROCESS_INFORMATION结构事例的地址就行了。
      大功高成了,我们管道一端连在了新进程的标准输出端了,一端可以自己用API函数ReadFile读取了。等等,不对,我们的管道还有问题。我们把 hWrite给了hStdOutput和hStdError,那么在新的进程启动时就会在新进程中打开一个管道写入端,而我们在当前进程中使用了 CreatePipe创建了一个管道,那么在当前进程中也有这个管道的写入端hWrite。好了,这里出现了一个有两个写入端和一个读出端的畸形管道。这样的管道肯定是有问题的。由于当前进程并不使用写端,因此我们必须关闭当前进程的写端。这样,我们的管道才算真正的建立成功了。来看看VC++写的源程序:

    /*
    * 通过管道技术,将dir /?的帮助信息输入到MFC应用程序的一个CEdit控件中。
    * VC++6.0 + WinXP 通过
    *
    * detrox, 2003
    */
    void CPipeDlg::OnButton1()
    {
     SECURITY_ATTRIBUTES sa;
     HANDLE hRead,hWrite;
     sa.nLength = sizeof(SECURITY_ATTRIBUTES);
     sa.lpSecurityDescriptor = NULL;
     sa.bInheritHandle = TRUE;
     if (!CreatePipe(&hRead,&hWrite,&sa,0)) {
      MessageBox("Error On CreatePipe()");
      return;
     }
     STARTUPINFO si;
     PROCESS_INFORMATION pi;
     si.cb = sizeof(STARTUPINFO);
     GetStartupInfo(&si);
     si.hStdError = hWrite;
     si.hStdOutput = hWrite;
     si.wShowWindow = SW_HIDE;
     si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
     if (!CreateProcess(NULL,"c:\\windows\\system32\\cmd.exe/c dir /?"
    ,NULL,NULL,TRUE,NULL,NULL,NULL,&si,&pi)) {
      MessageBox("Error on CreateProcess()");
      return;
     }
     CloseHandle(hWrite);
     char buffer[4096] = {0};
     DWORD bytesRead;
     while (true) {
      if (ReadFile(hRead,buffer,4095,&bytesRead,NULL) == NULL)
       break;
      m_Edit1 += buffer;
      UpdateData(false);
      Sleep(200);
     }
    }

    12/12/2008

    const volatile的使用

    volatile修饰符告诉complier变量值可以以任何不被程序明确指明的方式改变,最常见的例子就是外部端口的值,它的变化可以不用程序内的任何赋值语句就有可能改变的,这种变量就可以用volatile来修饰,complier不会优化掉它。
    const修饰的变量在程序里面是不能改变的,但是可以被程序外的东西修改,就象上面说的外部端口的值,如果仅仅使用const,有可能complier会优化掉这些变量,加上volatile就万无一失了。
    所以使用const volatile修饰变量表明变量值只由外部条件改变,不会出现其它的副作用。
    例如const volatile char *port  = (const volatile char *)0x00ff;

    C++辨析系列谈

                            C++辨析系列谈
                           ·郑力群·yesky

      static 是C++中很常用的修饰符,它被用来控制变量的存储方式和可见性,下
    面我将从 static 修饰符的产生原因、作用谈起,全面分析static 修饰符的实质


    static 的两大作用:

    一、控制存储方式:

      static被引入以告知编译器,将变量存储在程序的静态存储区而非栈上空间。


      1、引出原因:函数内部定义的变量,在程序执行到它的定义处时,编译器为
    它在栈上分配空间,大家知道,函数在栈上分配的空间在此函数执行结束时会释放
    掉,这样就产生了一个问题: 如果想将函数中此变量的值保存至下一次调用时,如
    何实现? 
    最容易想到的方法是定义一个全局的变量,但定义为一个全局变量有许多缺点,最
    明显的缺点是破坏了此变量的访问范围(使得在此函数中定义的变量,不仅仅受此
    函数控制)。

      2、 解决方案:因此C++ 中引入了static,用它来修饰变量,它能够指示编译
    器将此变量在程序的静态存储区分配空间保存,这样即实现了目的,又使得此变量
    的存取范围不变。

    二、控制可见性与连接类型 :

      static还有一个作用,它会把变量的可见范围限制在编译单元中,使它成为一
    个内部连接,这时,它的反义词为”extern”.

      Static作用分析总结:static总是使得变量或对象的存储形式变成静态存储,
    连接方式变成内部连接,对于局部变量(已经是内部连接了),它仅改变其存储方
    式;对于全局变量(已经是静态存储了),它仅改变其连接类型。

    类中的static成员:

    一、出现原因及作用:

      1、需要在一个类的各个对象间交互,即需要一个数据对象为整个类而非某个
    对象服务。

      2、同时又力求不破坏类的封装性,即要求此成员隐藏在类的内部,对外不可见


      类的static成员满足了上述的要求,因为它具有如下特征:有独立的存储区,
    属于整个类。

    二、注意:

      1、对于静态的数据成员,连接器会保证它拥有一个单一的外部定义。静态数
    据成员按定义出现的先后顺序依次初始化,注意静态成员嵌套时,要保证所嵌套的
    成员已经初始化了。消除时的顺序是初始化的反顺序。

      2、类的静态成员函数是属于整个类而非类的对象,所以它没有this指针,这
    就导致了它仅能访问类的静态数据和静态成员函数。 


    C++辨析系列谈(二)
    3/14/2001 10:1:23· 郑力群·yesky


      const 是C++中常用的类型修饰符,但我在工作中发现,许多人使用它仅仅是
    想当然尔,这样,有时也会用对,但在某些微妙的场合,可就没那么幸运了,究其
    实质原由,大多因为没有搞清本源。故在本篇中我将对const进行辨析。溯其本源
    ,究其实质,希望能对大家理解const有所帮助,根据思维的承接关系,分为如下
    几个部分进行阐述。

    C++中为什么会引入const

      C++的提出者当初是基于什么样的目的引入(或者说保留)const关键字呢?,
    这是一个有趣又有益的话题,对理解const很有帮助。

    1. 大家知道,C++有一个类型严格的编译系统,这使得C++程序的错误在编译阶段
    即可发现许多,从而使得出错率大为减少,因此,也成为了C++与C相比,有着突出
    优点的一个方面。

    2. C中很常见的预处理指令 #define VariableName VariableValue 可以很方便
    地进行值替代,这种值替代至少在三个方面优点突出:

      一是避免了意义模糊的数字出现,使得程序语义流畅清晰,如下例:
      #define USER_NUM_MAX 107 这样就避免了直接使用107带来的困惑。

      二是可以很方便地进行参数的调整与修改,如上例,当人数由107变为201时,
    进改动此处即可, 

      三是提高了程序的执行效率,由于使用了预编译器进行值替代,并不需要为这
    些常量分配存储空间,所以执行的效率较高。

      鉴于以上的优点,这种预定义指令的使用在程序中随处可见。

    3. 说到这里,大家可能会迷惑上述的1点、2点与const有什么关系呢?,好,请接
    着向下看来:

      预处理语句虽然有以上的许多优点,但它有个比较致命的缺点,即,预处理语
    句仅仅只是简单值替代,缺乏类型的检测机制。这样预处理语句就不能享受C++严
    格类型检查的好处,从而可能成为引发一系列错误的隐患。

    4.好了,第一阶段结论出来了:
    结论: Const 推出的初始目的,正是为了取代预编译指令,消除它的缺点,同时
    继承它的优点。

    现在它的形式变成了:

    Const DataType VariableName = VariableValue ;


    为什么const能很好地取代预定义语句? 
    const 到底有什么大神通,使它可以振臂一挥取代预定义语句呢?

    1. 首先,以const 修饰的常量值,具有不可变性,这是它能取代预定义语句的基
    础。

    2. 第二,很明显,它也同样可以避免意义模糊的数字出现,同样可以很方便地进
    行参数的调整和修改。

    3. 第三,C++的编译器通常不为普通const常量分配存储空间,而是将它们保存在
    符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得
    它的效率也很高,同时,这也是它取代预定义语句的重要基础。

    这里,我要提一下,为什么说这一点是也是它能取代预定义语句的基础,这是因为
    ,编译器不会去读存储的内容,如果编译器为const分配了存储空间,它就不能够
    成为一个编译期间的常量了。

    4. 最后,const定义也像一个普通的变量定义一样,它会由编译器对它进行类型
    的检测,消除了预定义语句的隐患。


    const 使用情况分类详析

    1.const 用于指针的两种情况分析:
     int const *A;  //A可变,*A不可变
     int *const A;  //A不可变,*A可变 

      分析:const 是一个左结合的类型修饰符,它与其左侧的类型修饰符和为一个
    类型修饰符,所以,int const 限定 *A,不限定A。int *const 限定A,不限定*A。


    2.const 限定函数的传递值参数:

     void Fun(const int Var);

      分析:上述写法限定参数在函数体中不可被改变。由值传递的特点可知,Var
    在函数体中的改变不会影响到函数外部。所以,此限定与函数的使用者无关,仅与
    函数的编写者有关。

    结论:最好在函数的内部进行限定,对外部调用者屏蔽,以免引起困惑。如可改写
    如下:

    void Fun(int Var){
    const int & VarAlias = Var;

    VarAlias ....

    .....



    3.const 限定函数的值型返回值:

    const int Fun1(); 

    const MyClass Fun2();

     分析:上述写法限定函数的返回值不可被更新,当函数返回内部的类型时(如
    Fun1),已经是一个数值,当然不可被赋值更新,所以,此时const无意义,最好
    去掉,以免困惑。当函数返回自定义的类型时(如Fun2),这个类型仍然包含可以
    被赋值的变量成员,所以,此时有意义。

    4. 传递与返回地址: 此种情况最为常见,由地址变量的特点可知,适当使用
    const,意义昭然。

    5. const 限定类的成员函数:

    class ClassName {

     public:

      int Fun() const;

     .....

    }

      注意:采用此种const 后置的形式是一种规定,亦为了不引起混淆。在此函数
    的声明中和定义中均要使用const,因为const已经成为类型信息的一部分。

    获得能力:可以操作常量对象。

    失去能力:不能修改类的数据成员,不能在函数中调用其他不是const的函数。

      在本篇中,const方面的知识我讲的不多,因为我不想把它变成一本C++的教科
    书。我只是想详细地阐述它的实质和用处. 我会尽量说的很详细,因为我希望在一
    种很轻松随意的气氛中说出自己的某些想法,毕竟,编程也是轻松,快乐人生的一
    部分。有时候,你会惊叹这其中的世界原来是如此的精美。


    C++辨析系列谈(三)
    3/6/2001 10:3:34· 郑力群·yesky

      在上篇谈了const后,本篇再来谈一下inline这个关键字,之所以把这篇文章
    放在这个位置,是因为inline这个关键字的引入原因和const十分相似,下面分为如
    下几个部分进行阐述。

    C++中引入inline关键字的原因:

      inline 关键字用来定义一个类的内联函数,引入它的主要原因是用它替代C中
    表达式形式的宏定义。

    表达式形式的宏定义一例:

       #define ExpressionName(Var1,Var2) (Var1+Var2)*(Var1-Var2)

    为什么要取代这种形式呢,且听我道来:

      1. 首先谈一下在C中使用这种形式宏定义的原因,C语言是一个效率很高的语
    言,这种宏定义在形式及使用上像一个函数,但它使用预处理器实现,没有了参数
    压栈,代码生成等一系列的操作,因此,效率很高,这是它在C中被使用的一个主要
    原因。

      2. 这种宏定义在形式上类似于一个函数,但在使用它时,仅仅只是做预处理
    器符号表中的简单替换,因此它不能进行参数有效性的检测,也就不能享受C++编
    译器严格类型检查的好处,另外它的返回值也不能被强制转换为可转换的合适的类
    型,这样,它的使用就存在着一系列的隐患和局限性。

      3. 在C++中引入了类及类的访问控制,这样,如果一个操作或者说一个表达
    式涉及到类的保护成员或私有成员,你就不可能使用这种宏定义来实现(因为无法
    将this指针放在合适的位置)。

      4. inline 推出的目的,也正是为了取代这种表达式形式的宏定义,它消除
    了它的缺点,同时又很好地继承了它的优点。

    为什么inline能很好地取代表达式形式的预定义呢?

    对应于上面的1-3点,阐述如下:

      1. inline 定义的类的内联函数,函数的代码被放入符号表中,在使用时直
    接进行替换,(像宏一样展开),没有了调用的开销,效率也很高。 

      2. 很明显,类的内联函数也是一个真正的函数,编译器在调用一个内联函数
    时,会首先检查它的参数的类型,保证调用正确。然后进行一系列的相关检查,就
    像对待任何一个真正的函数一样。这样就消除了它的隐患和局限性。 

      3. inline 可以作为某个类的成员函数,当然就可以在其中使用所在类的保
    护成员及私有成员。 


    在何时使用inline函数:

      首先,你可以使用inline函数完全取代表达式形式的宏定义。

      另外要注意,内联函数一般只会用在函数内容非常简单的时候,这是因为,内
    联函数的代码会在任何调用它的地方展开,如果函数太复杂,代码膨胀带来的恶果
    很可能会大于效率的提高带来的益处。 内联函数最重要的使用地方是用于类的存
    取函数。 

    如何使用类的inline函数: 

    简单提一下inline 的使用吧:

    1.在类中定义这种函数:

    class ClassName{

    .....

    ....

    GetWidth(){return m_lPicWidth;}; // 如果在类中直接定义,可以不使用
    inline修饰

    ....

    ....



    2.在类中声明,在类外定义:

    class ClassName{

    .....

    ....

    GetWidth(); // 如果在类中直接定义,可以不使用inline修饰

    ....


    ....



    inline GetWidth(){

    return m_lPicWidth;

    }

      在本篇中,谈了一种特殊的函数,类的inline函数,它的源起和特点在某种说
    法上与const很类似,可以与const搭配起来看。另外,最近有许多朋友与我Mail交
    往,给我谈论了许多问题,给了我很多启发,在此表示感谢。 


    C++辨析系列谈之四
    4/26/2001 9:23:18· 郑力群 ·yesky


      前言

      面向对象程序设计的基本观点是用程式来仿真大千世界,这使得它的各种根本
    特性非常人性化,如封装、继承、多态等等,而虚拟函数就是C++中实现多态性的
    主将。为了实现多态性,C++编译器也革命性地提供了动态联编(或叫晚捆绑)这
    一特征。

      虚拟函数亦是MFC编程的关键所在,MFC编程主要有两种方法:一是响应各种消
    息,进行对应的消息处理。二就是重载并改写虚拟函数,来实现自己的某些要求或
    改变系统的某些默认处理。

      虚函数的地位是如此的重要,对它进行穷根究底,力求能知其然并知其所以然
     对我们编程能力的提高大有好处。下面且听我道来。

      多态性和动态联编的实现过程分析

      一、基础略提(限于篇幅,请参阅相应的C++书籍):

      1、多态性:使用基础类的指针动态调用其派生类中函数的特性。


      2、动态联编:在运行阶段,才将函数的调用与对应的函数体进行连接的方式
    ,又叫运行时联编或晚捆绑。

      二、过程描述:

      1、编译器发现一个类中有虚函数,编译器会立即为此类生成虚拟函数表 
    VTABLE(后面有对VTABLE的分析)。虚拟函数表的各表项为指向对应虚拟函数的指
    针。

      2、编译器在此类中隐含插入一个指针VPTR(对VC编译器来说,它插在类的第
    一个位置上)。

      有一个办法可以让你感知这个隐含指针的存在,虽然你不能在类中直接看到它
    ,但你可以比较一下含有虚拟函数时的类的尺寸和没有虚拟函数时的类的尺寸,你
    能够发现,这个指针确实存在。 

      class CNoVirtualFun
       {
        private:
        LONG lMember;
        public:
        LONG GetMemberValue();
       } class CHaveVirtualFun
       {
        private:
         LONG lMember;
        public:
         virtual LONG GetMemberValue();
        } 

       CNoVirtualFun obj;
       sizeof(obj) -> == 4; 
       CHaveVirtualFun obj;
       sizeof(obj) -> == 8; 

      3、在调用此类的构造函数时,在类的构造函数中,编译器会隐含执行VPTR与
    VTABLE的关联代码,将VPTR指向对应的VTable。这就将类与此类的VTABLE联系了起
    来。

      4、在调用类的构造函数时,指向基础类的指针此时已经变成指向具体的类的
    this指针,这样依靠此this指针即可得到正确的VTABLE,从而实现了多态性。在此
    时才能真正与函数体进行连接,这就是动态联编。


    三、VTABLE 分析:

      分析1:虚拟函数表包含此类及其父类的所有虚拟函数的地址。如果它没有重
    载父类的虚拟函数,VTABLE中对应表项指向其父类的此函数。反之,指向重载后的
    此函数。

      分析2:虚拟函数被继承后仍旧是虚拟函数,虚拟函数非常严格地按出现的顺
    序在 VTABLE 中排序,所以确定的虚拟函数对应 VTABLE 中一个固定的位置n,n是
    一个在编译时就确定的常量。所以,使用VPTR加上对应的n,就可得到对应函数的
    入口地址。

      四、编译器调用虚拟函数的汇编码(参考Think in C++):

      push FunParam ;先将函数参数压栈

      push si ;将this指针压栈,以确保在当前类上操作

      mov bx,word ptr[si] ;因为VC++编译器将VPTR放在类的第一个位置上,所以
    bx内为VPTR

      call word ptr[bx+n] ;调用虚拟函数。n = 所调用的虚拟函数在对应 
    VTABLE 中的位置 


    纯虚函数:

      一、引入原因:

      1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。

      2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基
    类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。

      为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:
    virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重载
    以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很
    好地解决了上述两个问题。

      二、纯虚函数实质:

      1、类中含有纯虚函数则它的VTABLE表不完全,有一个空位,所以,不能生成
    对象(编译器绝对不允许有调用一个不存在函数的可能)。在它的派生类中,除非
    重载这个函数,否则,此派生类的VTABLE表亦不完整,亦不能生成对象,即它也成
    为一个纯虚基类。


      虚函数与构造、析构函数:

      1、构造函数本身不能是虚拟函数;并且虚机制在构造函数中不起作用(在构
    造函数中的虚拟函数只会调用它的本地版本)。

      想一想,在基类构造函数中使用虚机制,则可能会调用到子类,此时子类尚未
    生成,有何后果!?。

      2、析构函数本身常常要求是虚拟函数;但虚机制在析构函数中不起作用。

      若类中使用了虚拟函数,析构函数一定要是虚拟函数,比如使用虚拟机制调用
    delete,没有虚拟的析构函数,怎能保证delete的是你希望delete的对象。

      虚机制也不能在析构函数中生效,因为可能会引起调用已经被delete掉的类的
    虚拟函数的问题。

      对象切片:

      向上映射(子类被映射到父类)的时候,会发生子类的VTABLE 完全变成父类的
    VTABLE的情况。这就是对象切片。

      原因:向上映射的时候,接口会变窄,而编译器绝对不允许有调用一个不存在
    函数的可能,所以,子类中新派生的虚拟函数的入口在VTABLE中会被强行“切”掉
    ,从而出现上述情况。

      虚拟函数使用的缺点

      优点讲了一大堆,现在谈一下缺点,虚函数最主要的缺点是执行效率较低,看
    一看虚拟函数引发的多态性的实现过程,你就能体会到其中的原因。

    VC++动态链接库(DLL)编程深入浅出(zz)

    1.概论  先来阐述一下DLL(Dynamic Linkable Library)的概念,你可以简单的把DLL看成一种仓库,它提供给你一些可以直接拿来用的变量、函数或类。在仓库的发展史上经历了“无库-静态链接库-动态链接库”的时代。   静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,lib中的指令都被直接包含在最终生成的EXE文件中了。但是若使 用DLL,该DLL不必被包含在最终EXE文件中,EXE文件执行时可以“动态”地引用和卸载这个与EXE独立的DLL文件。静态链接库和动态链接库的另 外一个区别在于静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。  对动态链接库,我们还需建立如下概念:  (1)DLL 的编制与具体的编程语言及编译器无关  只要遵循约定的DLL接口规范和调用方式,用各种语言编写的DLL都可以相互调用。譬如Windows提供的系统DLL(其中包括了Windows的API),在任何开发环境中都能被调用,不在乎其是Visual Basic、Visual C++还是Delphi。  (2)动态链接库随处可见   我们在Windows目录下的system32文件夹中会看到kernel32.dll、user32.dll和gdi32.dll,windows的 大多数API都包含在这些DLL中。kernel32.dll中的函数主要处理内存管理和进程调度;user32.dll中的函数主要控制用户界面; gdi32.dll中的函数则负责图形方面的操作。  一般的程序员都用过类似MessageBox的函数,其实它就包含在user32.dll这个动态链接库中。由此可见DLL对我们来说其实并不陌生。  (3)VC动态链接库的分类  Visual C++支持三种DLL,它们分别是Non-MFC DLL(非MFC动态库)、MFC Regular DLL(MFC规则DLL)、MFC Extension DLL(MFC扩展DLL)。   非MFC动态库不采用MFC类库结构,其导出函数为标准的C接口,能被非MFC或MFC编写的应用程序所调用;MFC规则DLL 包含一个继承自CWinApp的类,但其无消息循环;MFC扩展DLL采用MFC的动态链接版本创建,它只能被用MFC类库所编写的应用程序所调用。由于本文篇幅较长,内容较多,势必需要先对阅读本文的有关事项进行说明,下面以问答形式给出。  问:本文主要讲解什么内容?  答:本文详细介绍了DLL编程的方方面面,努力学完本文应可以对DLL有较全面的掌握,并能编写大多数DLL程序。  问:如何看本文?  答:本文每一个主题的讲解都附带了源代码例程,可以随文下载(每个工程都经WINRAR压缩)。所有这些例程都由笔者编写并在VC++6.0中调试通过。  当然看懂本文不是读者的最终目的,读者应亲自动手实践才能真正掌握DLL的奥妙。  问:学习本文需要什么样的基础知识?  答:如果你掌握了C,并大致掌握了C++,了解一点MFC的知识,就可以轻松地看懂本文。 2.静态链接库   对静态链接库的讲解不是本文的重点,但是在具体讲解DLL之前,通过一个静态链接库的例子可以快速地帮助我们建立“库”的概念。

    图1 建立一个静态链接库

      如图1,在VC++6.0中new一个名称为libTest的static library工程(单击此处下载本工程附件),并新建lib.h和lib.cpp两个文件,lib.h和lib.cpp的源代码如下:

    //文件:lib.h #ifndef LIB_H #define LIB_H extern "C" int add(int x,int y);   //声明为C编译、连接方式的外部函数 #endif //文件:lib.cpp #include "lib.h" int add(int x,int y) { return x + y; }

      编译这个工程就得到了一个.lib文件,这个文件就是一个函数库,它提供了add的功能。将头文件和.lib文件提交给用户后,用户就可以直接使用其中的add函数了。  标准Turbo C2.0中的C库函数(我们用来的scanf、printf、memcpy、strcpy等)就来自这种静态库。

    下面来看看怎么使用这个库,在libTest工程所在的工作区内new一个libCall工程。libCall工程仅包含一个main.cpp文件,它演示了静态链接库的调用方法,其源代码如下:

    #include <stdio.h> #include "..\lib.h" #pragma comment( lib, "..\\debug\\libTest.lib" )  //指定与静态库一起连接 int main(int argc, char* argv[]) { printf( "2 + 3 = %d", add( 2, 3 ) ); }

      静态链接库的调用就是这么简单,或许我们每天都在用,可是我们没有明白这个概念。代码中#pragma comment( lib , "..\\debug\\libTest.lib" )的意思是指本文件生成的.obj文件应与libTest.lib一起连接。   如果不用#pragma comment指定,则可以直接在VC++中设置,如图2,依次选择tools、options、directories、library files菜单或选项,填入库文件路径。图2中加红圈的部分为我们添加的libTest.lib文件的路径。

    图2 在VC中设置库文件路径

    这个静态链接库的例子至少让我们明白了库函数是怎么回事,它们是哪来的。我们现在有下列模糊认识了:  (1)库不是个怪物,编写库的程序和编写一般的程序区别不大,只是库不能单独执行;  (2)库提供一些可以给别的程序调用的东东,别的程序要调用它必须以某种方式指明它要调用之。  以上从静态链接库分析而得到的对库的懵懂概念可以直接引申到动态链接库中,动态链接库与静态链接库在编写和调用上的不同体现在库的外部接口定义及调用方式略有差异。 3.库的调试与查看   在具体进入各类DLL的详细阐述之前,有必要对库文件的调试与查看方法进行一下介绍,因为从下一节开始我们将面对大量的例子工程。   由于库文件不能单独执行,因而在按下F5(开始debug模式执行)或CTRL+F5(运行)执行时,其弹出如图3所示的对话框,要求用户输入可执行文 件的路径来启动库函数的执行。这个时候我们输入要调用该库的EXE文件的路径就可以对库进行调试了,其调试技巧与一般应用工程的调试一样。

    图3 库的调试与“运行”

       通常有比上述做法更好的调试途径,那就是将库工程和应用工程(调用库的工程)放置在同一VC工作区,只对应用工程进行调试,在应用工程调用库中函数的语 句处设置断点,执行后按下F11,这样就单步进入了库中的函数。第2节中的libTest和libCall工程就放在了同一工作区,其工程结构如图4所 示。

    图4 把库工程和调用库的工程放入同一工作区进行调试

    上述调试方法对静态链接库和动态链接库而言是一致的。所以本文提供下载的所有源代码中都包含了库工程和调用库的工程,这二者都被包含在一个工作区内,这是笔者提供这种打包下载的用意所在。动态链接库中的导出接口可以使用Visual C++的Depends工具进行查看,让我们用Depends打开系统目录中的user32.dll,看到了吧?红圈内的就是几个版本的MessageBox了!原来它真的在这里啊,原来它就在这里啊!

    图5 用Depends查看DLL

      当然Depends工具也可以显示DLL的层次结构,若用它打开一个可执行文件则可以看出这个可执行文件调用了哪些DLL。  好,让我们正式进入动态链接库的世界,先来看看最一般的DLL,即非MFC DLL(待续...)

     

    上节给大家介绍了静态链接库与库的调试与查看(动态链接库(DLL)编程深入浅出(一)),本节主要介绍非MFC DLL。

    4.非MFC DLL

    4.1一个简单的DLL   第2节给出了以静态链接库方式提供add函数接口的方法,接下来我们来看看怎样用动态链接库实现一个同样功能的add函数。  如图6,在VC++中new一个Win32 Dynamic-Link Library工程dllTest(单击此处下载本工程附件)。注意不要选择MFC AppWizard(dll),因为用MFC AppWizard(dll)建立的将是第5、6节要讲述的MFC 动态链接库。

    图6 建立一个非MFC DLL

      在建立的工程中添加lib.h及lib.cpp文件,源代码如下:

    /* 文件名:lib.h */ #ifndef LIB_H #define LIB_H extern "C" int __declspec(dllexport)add(int x, int y); #endif /* 文件名:lib.cpp */ #include "lib.h" int add(int x, int y) { return x + y; }

    与第2节对静态链接库的调用相似,我们也建立一个与DLL工程处于同一工作区的应用工程dllCall,它调用DLL中的函数add,其源代码如下:

    #include <stdio.h> #include <windows.h> typedef int(*lpAddFun)(int, int); //宏定义函数指针类型 int main(int argc, char *argv[]) { HINSTANCE hDll; //DLL句柄 lpAddFun addFun; //函数指针 hDll = LoadLibrary("..\\Debug\\dllTest.dll"); if (hDll != NULL) { addFun = (lpAddFun)GetProcAddress(hDll, "add"); if (addFun != NULL) { int result = addFun(2, 3); printf("%d", result); } FreeLibrary(hDll); } return 0; }

       分析上述代码,dllTest工程中的lib.cpp文件与第2节静态链接库版本完全相同,不同在于lib.h对函数add的声明前面添加了 __declspec(dllexport)语句。这个语句的含义是声明函数add为DLL的导出函数。DLL内的函数分为两种:  (1)DLL导出函数,可供应用程序调用;  (2) DLL内部函数,只能在DLL程序使用,应用程序无法调用它们。  而应用程序对本DLL的调用和对第2节静态链接库的调用却有较大差异,下面我们来逐一分析。  首先,语句typedef int ( * lpAddFun)(int,int)定义了一个与add函数接受参数类型和返回值均相同的函数指针类型。随后,在main函数中定义了lpAddFun的实例addFun;  其次,在函数main中定义了一个DLL HINSTANCE句柄实例hDll,通过Win32 Api函数LoadLibrary动态加载了DLL模块并将DLL模块句柄赋给了hDll;  再次,在函数main中通过Win32 Api函数GetProcAddress得到了所加载DLL模块中函数add的地址并赋给了addFun。经由函数指针addFun进行了对DLL中add函数的调用;  最后,应用工程使用完DLL后,在函数main中通过Win32 Api函数FreeLibrary释放了已经加载的DLL模块。  通过这个简单的例子,我们获知DLL定义和调用的一般概念:  (1)DLL中需以某种特定的方式声明导出函数(或变量、类);  (2)应用工程需以某种特定的方式调用DLL的导出函数(或变量、类)。  下面我们来对“特定的方式进行”阐述。 4.2 声明导出函数    DLL中导出函数的声明有两种方式:一种为4.1节例子中给出的在函数声明 中加上__declspec(dllexport),这里不再举例说明;另外一种方式是采用模块定义(.def) 文件声明,.def文件为链接器提供了有关被链接程序的导出、属性及其他方面的信息。  下面的代码演示了怎样同.def文件将函数add声明为DLL导出函数(需在dllTest工程中添加lib.def文件):

    ; lib.def : 导出DLL函数 LIBRARY dllTest EXPORTS add @ 1

    .def文件的规则为:  (1)LIBRARY语句说明.def文件相应的DLL;  (2)EXPORTS语句后列出要导出函数的名称。可以在.def文件中的导出函数名后加@n,表示要导出函数的序号为n(在进行函数调用时,这个序号将发挥其作用);  (3).def 文件中的注释由每个注释行开始处的分号 (;) 指定,且注释不能与语句共享一行。  由此可以看出,例子中lib.def文件的含义为生成名为“dllTest”的动态链接库,导出其中的add函数,并指定add函数的序号为1。 4.3 DLL的调用方式   在4.1节的例子中我们看到了由“LoadLibrary-GetProcAddress-FreeLibrary”系统Api提供的三位一体“DLL加载-DLL函数地址获取-DLL释放”方式,这种调用方式称为DLL的动态调用。  动态调用方式的特点是完全由编程者用 API 函数加载和卸载 DLL,程序员可以决定 DLL 文件何时加载或不加载,显式链接在运行时决定加载哪个 DLL 文件。   与动态调用方式相对应的就是静态调用方式,“有动必有静”,这来源于物质世界的对立统一。“动与静”,其对立与统一竟无数次在技术领域里得到验证,譬如 静态IP与DHCP、静态路由与动态路由等。从前文我们已经知道,库也分为静态库与动态库DLL,而想不到,深入到DLL内部,其调用方式也分为静态与动 态。“动与静”,无处不在。《周易》已认识到有动必有静的动静平衡观,《易.系辞》曰:“动静有常,刚柔断矣”。哲学意味着一种普遍的真理,因此,我们经 常可以在枯燥的技术领域看到哲学的影子。  静态调用方式的特点是由编译系统完成对DLL的加载和应用程序结束时 DLL 的卸载。当调用某DLL的应用程序结束时,若系统中还有其它程序使用该 DLL,则Windows对DLL的应用记录减1,直到所有使用该DLL的程序都结束时才释放它。静态调用方式简单实用,但不如动态调用方式灵活。  下面我们来看看静态调用的例子(单击此处下载本工程附件),将编译dllTest工程所生成的.lib和.dll文件拷入dllCall工程所在的路径,dllCall执行下列代码:

    #pragma comment(lib,"dllTest.lib") //.lib文件中仅仅是关于其对应DLL文件中函数的重定位信息 extern "C" __declspec(dllimport) add(int x,int y); int main(int argc, char* argv[]) { int result = add(2,3); printf("%d",result); return 0; }

      由上述代码可以看出,静态调用方式的顺利进行需要完成两个动作:  (1)告诉编译器与DLL相对应的.lib文件所在的路径及文件名,#pragma comment(lib,"dllTest.lib")就是起这个作用。  程序员在建立一个DLL文件时,连接器会自动为其生成一个对应的.lib文件,该文件包含了DLL 导出函数的符号名及序号(并不含有实际的代码)。在应用程序里,.lib文件将作为DLL的替代文件参与编译。  (2)声明导入函数,extern "C" __declspec(dllimport) add(int x,int y)语句中的__declspec(dllimport)发挥这个作用。   静态调用方式不再需要使用系统API来加载、卸载DLL以及获取DLL中导出函数的地址。这是因为,当程序员通过静态链接方式编译生成应用程序时,应用 程序中调用的与.lib文件中导出符号相匹配的函数符号将进入到生成的EXE 文件中,.lib文件中所包含的与之对应的DLL文件的文件名也被编译器存储在 EXE文件内部。当应用程序运行过程中需要加载DLL文件时,Windows将根据这些信息发现并加载DLL,然后通过符号名实现对DLL 函数的动态链接。这样,EXE将能直接通过函数名调用DLL的输出函数,就象调用程序内部的其他函数一样。 4.4 DllMain函数    Windows在加载DLL的时候,需要一个入口函数,就如同控制台 或DOS程序需要main函数、WIN32程序需要WinMain函数一样。在前面的例子中,DLL并没有提供DllMain函数,应用工程也能成功引用 DLL,这是因为Windows在找不到DllMain的时候,系统会从其它运行库中引入一个不做任何操作的缺省DllMain函数版本,并不意味着 DLL可以放弃DllMain函数。  根据编写规范,Windows必须查找并执行DLL里的DllMain函数作为加载DLL的依据,它使得DLL得以保留在内存里。这个函数并不属于导出函数,而是DLL的内部函数。这意味着不能直接在应用工程中引用DllMain函数,DllMain是自动被调用的。  我们来看一个DllMain函数的例子(单击此处下载本工程附件)。

    BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: printf("\nprocess attach of dll"); break; case DLL_THREAD_ATTACH: printf("\nthread attach of dll"); break; case DLL_THREAD_DETACH: printf("\nthread detach of dll"); break; case DLL_PROCESS_DETACH: printf("\nprocess detach of dll"); break; } return TRUE; }

       DllMain函数在DLL被加载和卸载时被调用,在单个线程启动和终止时,DLLMain函数也被调用,ul_reason_for_call指明了 被调用的原因。原因共有4种,即PROCESS_ATTACH、PROCESS_DETACH、THREAD_ATTACH和 THREAD_DETACH,以switch语句列出。来仔细解读一下DllMain的函数头BOOL APIENTRY DllMain( HANDLE hModule, WORD ul_reason_for_call, LPVOID lpReserved )。  APIENTRY被定义为__stdcall,它意味着这个函数以标准Pascal的方式进行调用,也就是WINAPI方式;   进程中的每个DLL模块被全局唯一的32字节的HINSTANCE句柄标识,只有在特定的进程内部有效,句柄代表了DLL模块在进程虚拟空间中的起始地 址。在Win32中,HINSTANCE和HMODULE的值是相同的,这两种类型可以替换使用,这就是函数参数hModule的来历。  执行下列代码:

    hDll = LoadLibrary("..\\Debug\\dllTest.dll"); if (hDll != NULL) { addFun = (lpAddFun)GetProcAddress(hDll, MAKEINTRESOURCE(1)); //MAKEINTRESOURCE直接使用导出文件中的序号 if (addFun != NULL) { int result = addFun(2, 3); printf("\ncall add in dll:%d", result); } FreeLibrary(hDll); }

      我们看到输出顺序为:  process attach of dll   call add in dll:5   process detach of dll   这一输出顺序验证了DllMain被调用的时机。   代码中的GetProcAddress ( hDll, MAKEINTRESOURCE ( 1 ) )值得留意,它直接通过.def文件中为add函数指定的顺序号访问add函数,具体体现在MAKEINTRESOURCE ( 1 ),MAKEINTRESOURCE是一个通过序号获取函数名的宏,定义为(节选自winuser.h):

    #define MAKEINTRESOURCEA(i) (LPSTR)((DWORD)((WORD)(i))) #define MAKEINTRESOURCEW(i) (LPWSTR)((DWORD)((WORD)(i))) #ifdef UNICODE #define MAKEINTRESOURCE MAKEINTRESOURCEW #else #define MAKEINTRESOURCE MAKEINTRESOURCEA

    4.5 __stdcall约定    如果通过VC++编写的DLL欲被其他语言编写的程序调用,应将 函数的调用方式声明为__stdcall方式,WINAPI都采用这种方式,而C/C++缺省的调用方式却为__cdecl。__stdcall方式与 __cdecl对函数名最终生成符号的方式不同。若采用C编译方式(在C++中需将函数声明为extern "C"),__stdcall调用约定在输出函数名前面加下划线,后面加“@”符号和参数的字节数,形如_functionname@number;而 __cdecl调用约定仅在输出函数名前面加下划线,形如_functionname。  Windows编程中常见的几种函数类型声明宏都是与__stdcall和__cdecl有关的(节选自windef.h):

    #define CALLBACK __stdcall //这就是传说中的回调函数 #define WINAPI __stdcall //这就是传说中的WINAPI #define WINAPIV __cdecl #define APIENTRY WINAPI //DllMain的入口就在这里 #define APIPRIVATE __stdcall #define PASCAL __stdcall

      在lib.h中,应这样声明add函数:

    int __stdcall add(int x, int y);

      在应用工程中函数指针类型应定义为:

    typedef int(__stdcall *lpAddFun)(int, int);

      若在lib.h中将函数声明为__stdcall调用,而应用工程中仍使用typedef int (* lpAddFun)(int,int),运行时将发生错误(因为类型不匹配,在应用工程中仍然是缺省的__cdecl调用),弹出如图7所示的对话框。

    图7 调用约定不匹配时的运行错误

    图8中的那段话实际上已经给出了错误的原因,即“This is usually a result of …”。  单击此处下载__stdcall调用例子工程源代码附件4.6 DLL导出变量   DLL定义的全局变量可以被调用进程访问;DLL也可以访问调用进程的全局数据,我们来看看在应用工程中引用DLL中变量的例子(单击此处下载本工程附件)。

    /* 文件名:lib.h */ #ifndef LIB_H #define LIB_H extern int dllGlobalVar; #endif /* 文件名:lib.cpp */ #include "lib.h" #include <windows.h> int dllGlobalVar; BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: dllGlobalVar = 100; //在dll被加载时,赋全局变量为100 break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; } ;文件名:lib.def ;在DLL中导出变量 LIBRARY "dllTest" EXPORTS dllGlobalVar CONSTANT ;或dllGlobalVar DATA GetGlobalVar

      从lib.h和lib.cpp中可以看出,全局变量在DLL中的定义和使用方法与一般的程序设计是一样的。若要导出某全局变量,我们需要在.def文件的EXPORTS后添加:

    变量名 CONSTANT   //过时的方法

      或

    变量名 DATA     //VC++提示的新方法

    在主函数中引用DLL中定义的全局变量:

    #include <stdio.h> #pragma comment(lib,"dllTest.lib") extern int dllGlobalVar; int main(int argc, char *argv[]) { printf("%d ", *(int*)dllGlobalVar); *(int*)dllGlobalVar = 1; printf("%d ", *(int*)dllGlobalVar); return 0; }

       特别要注意的是用extern int dllGlobalVar声明所导入的并不是DLL中全局变量本身,而是其地址,应用程序必须通过强制指针转换来使用DLL中的全局变量。这一点,从* (int*)dllGlobalVar可以看出。因此在采用这种方式引用DLL全局变量时,千万不要进行这样的赋值操作:

    dllGlobalVar = 1;

      其结果是dllGlobalVar指针的内容发生变化,程序中以后再也引用不到DLL中的全局变量了。  在应用工程中引用DLL中全局变量的一个更好方法是:

    #include <stdio.h> #pragma comment(lib,"dllTest.lib") extern int _declspec(dllimport) dllGlobalVar; //用_declspec(dllimport)导入 int main(int argc, char *argv[]) { printf("%d ", dllGlobalVar); dllGlobalVar = 1; //这里就可以直接使用, 无须进行强制指针转换 printf("%d ", dllGlobalVar); return 0; }

      通过_declspec(dllimport)方式导入的就是DLL中全局变量本身而不再是其地址了,笔者建议在一切可能的情况下都使用这种方式。 4.7 DLL导出类   DLL中定义的类可以在应用工程中使用。  下面的例子里,我们在DLL中定义了point和circle两个类,并在应用工程中引用了它们(单击此处下载本工程附件)。

    //文件名:point.h,point类的声明 #ifndef POINT_H #define POINT_H #ifdef DLL_FILE class _declspec(dllexport) point //导出类point #else class _declspec(dllimport) point //导入类point #endif { public: float y; float x; point(); point(float x_coordinate, float y_coordinate); }; #endif //文件名:point.cpp,point类的实现 #ifndef DLL_FILE #define DLL_FILE #endif #include "point.h" //类point的缺省构造函数 point::point() { x = 0.0; y = 0.0; } //类point的构造函数 point::point(float x_coordinate, float y_coordinate) { x = x_coordinate; y = y_coordinate; } //文件名:circle.h,circle类的声明 #ifndef CIRCLE_H #define CIRCLE_H #include "point.h" #ifdef DLL_FILE class _declspec(dllexport)circle //导出类circle #else class _declspec(dllimport)circle //导入类circle #endif { public: void SetCentre(const point ¢rePoint); void SetRadius(float r); float GetGirth(); float GetArea(); circle(); private: float radius; point centre; }; #endif //文件名:circle.cpp,circle类的实现 #ifndef DLL_FILE #define DLL_FILE #endif #include "circle.h" #define PI 3.1415926 //circle类的构造函数 circle::circle() { centre = point(0, 0); radius = 0; } //得到圆的面积 float circle::GetArea() { return PI *radius * radius; } //得到圆的周长 float circle::GetGirth() { return 2 *PI * radius; } //设置圆心坐标 void circle::SetCentre(const point ¢rePoint) { centre = centrePoint; } //设置圆的半径 void circle::SetRadius(float r) { radius = r; }

    类的引用:

    #include "..\circle.h"  //包含类声明头文件 #pragma comment(lib,"dllTest.lib"); int main(int argc, char *argv[]) { circle c; point p(2.0, 2.0); c.SetCentre(p); c.SetRadius(1.0); printf("area:%f girth:%f", c.GetArea(), c.GetGirth()); return 0; }

      从上述源代码可以看出,由于在DLL的类实现代码中定义了宏DLL_FILE,故在DLL的实现中所包含的类声明实际上为:

    class _declspec(dllexport) point //导出类point { … }

      和

    class _declspec(dllexport) circle //导出类circle { … }

      而在应用工程中没有定义DLL_FILE,故其包含point.h和circle.h后引入的类声明为:

    class _declspec(dllimport) point //导入类point { … }

      和

    class _declspec(dllimport) circle //导入类circle { … }

    不错,正是通过DLL中的

    class _declspec(dllexport) class_name //导出类circle  { … }

      与应用程序中的

    class _declspec(dllimport) class_name //导入类 { … }

      匹对来完成类的导出和导入的!   我们往往通过在类的声明头文件中用一个宏来决定使其编译为class _declspec(dllexport) class_name还是class _declspec(dllimport) class_name版本,这样就不再需要两个头文件。本程序中使用的是:

    #ifdef DLL_FILE class _declspec(dllexport) class_name //导出类 #else class _declspec(dllimport) class_name //导入类 #endif

      实际上,在MFC DLL的讲解中,您将看到比这更简便的方法,而此处仅仅是为了说明_declspec(dllexport)与_declspec(dllimport)匹对的问题。  由此可见,应用工程中几乎可以看到DLL中的一切,包括函数、变量以及类,这就是DLL所要提供的强大能力。只要DLL释放这些接口,应用程序使用它就将如同使用本工程中的程序一样!  本章虽以VC++为平台讲解非MFC DLL,但是这些普遍的概念在其它语言及开发环境中也是相同的,其思维方式可以直接过渡。  接下来,我们将要研究MFC规则DLL(待续...)

     

    第4节我们对非MFC DLL进行了介绍,这一节将详细地讲述MFC规则DLL的创建与使用技巧。   另外,自从本文开始连载后,收到了一些读者的e-mail。有的读者提出了一些问题,笔者将在本文的最后一次连载中选取其中的典型问题进行解答。由于时 间的关系,对于读者朋友的来信,笔者暂时不能一一回复,还望海涵!由于笔者的水平有限,文中难免有错误和纰漏,也热诚欢迎读者朋友不吝指正! 5. MFC规则DLL 5.1 概述   MFC规则DLL的概念体现在两方面:  (1) 它是MFC的  “是MFC的”意味着可以在这种DLL的内部使用MFC;  (2) 它是规则的  “是规则的”意味着它不同于MFC扩展DLL,在MFC规则DLL的内部虽然可以使用MFC,但是其与应用程序的接口不能是MFC。而MFC扩展DLL与应用程序的接口可以是MFC,可以从MFC扩展DLL中导出一个MFC类的派生类。  Regular DLL能够被所有支持DLL技术的语言所编写的应用程序调用,当然也包括使用MFC的应用程序。在这种动态连接库中,包含一个从CWinApp继承下来的类,DllMain函数则由MFC自动提供。  Regular DLL分为两类:  (1)静态链接到MFC 的规则DLL   静态链接到MFC的规则DLL与MFC库(包括MFC扩展 DLL)静态链接,将MFC库的代码直接生成在.dll文件中。在调用这种DLL的接口时,MFC使用DLL的资源。因此,在静态链接到MFC 的规则DLL中不需要进行模块状态的切换。  使用这种方法生成的规则DLL其程序较大,也可能包含重复的代码。  (2)动态链接到MFC 的规则DLL    动态链接到MFC 的规则DLL 可以和使用它的可执行文件同时动态链接到 MFC DLL 和任何MFC扩展 DLL。在使用了MFC共享库的时候,默认情况下,MFC使用主应用程序的资源句柄来加载资源模板。这样,当DLL和应用程序中存在相同ID的资源时(即 所谓的资源重复问题),系统可能不能获得正确的资源。因此,对于共享MFC DLL的规则DLL,我们必须进行模块切换以使得MFC能够找到正确的资源模板。我们可以在Visual C++中设置MFC规则DLL是静态链接到MFC DLL还是动态链接到MFC DLL。如图8,依次选择Visual C++的project -> Settings -> General菜单或选项,在Microsoft Foundation Classes中进行设置。

    图8 设置动态/静态链接MFC DLL

    5.2 MFC规则DLL的创建   我们来一步步讲述使用MFC向导创建MFC规则DLL的过程,首先新建一个project,如图9,选择project的类型为MFC AppWizard(dll)。点击OK进入如图10所示的对话框。

    图9 MFC DLL工程的创建

    图10所示对话框中的1区选择MFC DLL的类别。   2区选择是否支持automation(自动化)技术, automation 允许用户在一个应用程序中操纵另外一个应用程序或组件。例如,我们可以在应用程序中利用 Microsoft Word 或Microsoft Excel的工具,而这种使用对用户而言是透明的。自动化技术可以大大简化和加快应用程序的开发。  3区选择是否支持Windows Sockets,当选择此项目时,应用程序能在 TCP/IP 网络上进行通信。 CWinApp派生类的InitInstance成员函数会初始化通讯端的支持,同时工程中的StdAfx.h文件会自动include <AfxSock.h>头文件。添加socket通讯支持后的InitInstance成员函数如下:

    BOOL CRegularDllSocketApp::InitInstance() { if (!AfxSocketInit()) { AfxMessageBox(IDP_SOCKETS_INIT_FAILED); return FALSE; } return TRUE; }

      4区选择是否由MFC向导自动在源代码中添加注释,一般我们选择“Yes,please”。

    图10 MFC DLL的创建选项

    5.3 一个简单的MFC规则DLL   这个DLL的例子(属于静态链接到MFC 的规则DLL)中提供了一个如图11所示的对话框。

    图11 MFC规则DLL例子

    在DLL中添加对话框的方式与在MFC应用程序中是一样的。  在图11所示DLL中的对话框的Hello按钮上点击时将MessageBox一个“Hello,pconline的网友”对话框,下面是相关的文件及源代码,其中删除了MFC向导自动生成的绝大多数注释(下载本工程附件):第一组文件:CWinApp继承类的声明与实现

    // RegularDll.h : main header file for the REGULARDLL DLL #if !defined(AFX_REGULARDLL_H__3E9CB22B_588B_4388_B778_B3416ADB79B3__INCLUDED_) #define AFX_REGULARDLL_H__3E9CB22B_588B_4388_B778_B3416ADB79B3__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 #ifndef __AFXWIN_H__ #error include 'stdafx.h' before including this file for PCH #endif #include "resource.h" // main symbols class CRegularDllApp : public CWinApp { public: CRegularDllApp(); DECLARE_MESSAGE_MAP() }; #endif // RegularDll.cpp : Defines the initialization routines for the DLL. #include "stdafx.h" #include "RegularDll.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif BEGIN_MESSAGE_MAP(CRegularDllApp, CWinApp) END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// // CRegularDllApp construction CRegularDllApp::CRegularDllApp() { } ///////////////////////////////////////////////////////////////////////////// // The one and only CRegularDllApp object CRegularDllApp theApp;

      分析:   在这一组文件中定义了一个继承自CWinApp的类CRegularDllApp,并同时定义了其的一个实例theApp。乍一看,您会以为它是一个 MFC应用程序,因为MFC应用程序也包含这样的在工程名后添加“App”组成类名的类(并继承自CWinApp类),也定义了这个类的一个全局实例 theApp。  我们知道,在MFC应用程序中CWinApp取代了SDK程序中WinMain的地位,SDK程序WinMain所完成的工作由CWinApp的三个函数完成:

    virtual BOOL InitApplication( ); virtual BOOL InitInstance( ); virtual BOOL Run( ); //传说中MFC程序的“活水源头”

       但是MFC规则DLL并不是MFC应用程序,它所继承自CWinApp的类不包含消息循环。这是因为,MFC规则DLL不包含CWinApp::Run 机制,主消息泵仍然由应用程序拥有。如果DLL 生成无模式对话框或有自己的主框架窗口,则应用程序的主消息泵必须调用从DLL 导出的函数来调用PreTranslateMessage成员函数。  另外,MFC规则DLL与MFC 应用程序中一样,需要将所有 DLL中元素的初始化放到InitInstance 成员函数中。  第二组文件 自定义对话框类声明及实现(点击查看附件)   分析:  这一部分的编程与一般的应用程序根本没有什么不同,我们照样可以利用MFC类向导来自动为对话框上的控件添加事件。MFC类向导照样会生成类似ON_BN_CLICKED(IDC_HELLO_BUTTON, OnHelloButton)的消息映射宏。  第三组文件 DLL中的资源文件

    //{{NO_DEPENDENCIES}} // Microsoft Developer Studio generated include file. // Used by RegularDll.rc // #define IDD_DLL_DIALOG 1000 #define IDC_HELLO_BUTTON 1000

      分析:  在MFC规则DLL中使用资源也与在MFC应用程序中使用资源没有什么不同,我们照样可以用Visual C++的资源编辑工具进行资源的添加、删除和属性的更改。  第四组文件 MFC规则DLL接口函数

    #include "StdAfx.h" #include "DllDialog.h" extern "C" __declspec(dllexport) void ShowDlg(void) { CDllDialog dllDialog; dllDialog.DoModal(); }

      分析:  这个接口并不使用MFC,但是在其中却可以调用MFC扩展类CdllDialog的函数,这体现了“规则”的概类。  与非MFC DLL完全相同,我们可以使用__declspec(dllexport)声明或在.def中引出的方式导出MFC规则DLL中的接口。 5.4 MFC规则DLL的调用   笔者编写了如图12的对话框MFC程序(下载本工程附件)来调用5.3节的MFC规则DLL,在这个程序的对话框上点击“调用DLL”按钮时弹出5.3节MFC规则DLL中的对话框。

    图12 MFC规则DLL的调用例子

      下面是“调用DLL”按钮单击事件的消息处理函数:

    void CRegularDllCallDlg::OnCalldllButton() { typedef void (*lpFun)(void); HINSTANCE hDll; //DLL句柄 hDll = LoadLibrary("RegularDll.dll"); if (NULL==hDll) { MessageBox("DLL加载失败"); } lpFun addFun; //函数指针 lpFun pShowDlg = (lpFun)GetProcAddress(hDll,"ShowDlg"); if (NULL==pShowDlg) { MessageBox("DLL中函数寻找失败"); } pShowDlg(); }

      上述例子中给出的是显示调用的方式,可以看出,其调用方式与第4节中非MFC DLL的调用方式没有什么不同。  我们照样可以在EXE程序中隐式调用MFC规则DLL,只需要将DLL工程生成的.lib文件和.dll文件拷入当前工程所在的目录,并在RegularDllCallDlg.cpp文件(图12所示对话框类的实现文件)的顶部添加:

    #pragma comment(lib,"RegularDll.lib") void ShowDlg(void);

      并将void CRegularDllCallDlg::OnCalldllButton() 改为:

    void CRegularDllCallDlg::OnCalldllButton() { ShowDlg(); }

    5.5 共享MFC DLL的规则DLL的模块切换    应用程序进程本身及其调用的每个DLL模块都具有一个全局唯一的HINSTANCE句柄,它们代表了DLL或EXE模块在进程虚拟空间中的起始地址。进 程本身的模块句柄一般为0x400000,而DLL模块的缺省句柄为0x10000000。如果程序同时加载了多个DLL,则每个DLL模块都会有不同的 HINSTANCE。应用程序在加载DLL时对其进行了重定位。   共享MFC DLL(或MFC扩展DLL)的规则DLL涉及到HINSTANCE句柄问题,HINSTANCE句柄对于加载资源特别重要。EXE和DLL都有其自己的 资源,而且这些资源的ID可能重复,应用程序需要通过资源模块的切换来找到正确的资源。如果应用程序需要来自于DLL的资源,就应将资源模块句柄指定为 DLL的模块句柄;如果需要EXE文件中包含的资源,就应将资源模块句柄指定为EXE的模块句柄。  这次我们创建一个动态链接到MFC DLL的规则DLL(下载本工程附件),在其中包含如图13的对话框。

    图13 DLL中的对话框

      另外,在与这个DLL相同的工作区中生成一个基于对话框的MFC程序,其对话框与图12完全一样。但是在此工程中我们另外添加了一个如图14的对话框。

    图14 EXE中的对话框

    图13和图14中的对话框除了caption不同(以示区别)以外,其它的都相同。  尤其值得特别注意,在DLL和EXE中我们对图13和图14的对话框使用了相同的资源ID=2000,在DLL和EXE工程的resource.h中分别有如下的宏:

    //DLL中对话框的ID #define IDD_DLL_DIALOG 2000 //EXE中对话框的ID #define IDD_EXE_DIALOG 2000

      与5.3节静态链接MFC DLL的规则DLL相同,我们还是在规则DLL中定义接口函数ShowDlg,原型如下:

    #include "StdAfx.h" #include "SharedDll.h" void ShowDlg(void) { CDialog dlg(IDD_DLL_DIALOG); //打开ID为2000的对话框 dlg.DoModal(); }

      而为应用工程主对话框的“调用DLL”的单击事件添加如下消息处理函数:

    void CSharedDllCallDlg::OnCalldllButton() { ShowDlg(); }

      我们以为单击“调用DLL”会弹出如图13所示DLL中的对话框,可是可怕的事情发生了,我们看到是图14所示EXE中的对话框! 惊讶?   产生这个问题的根源在于应用程序与MFC规则DLL共享MFC DLL(或MFC扩展DLL)的程序总是默认使用EXE的资源,我们必须进行资源模块句柄的切换,其实现方法有三:  方法一 在DLL接口函数中使用:

    AFX_MANAGE_STATE(AfxGetStaticModuleState());

      我们将DLL中的接口函数ShowDlg改为:

    void ShowDlg(void) { //方法1:在函数开始处变更,在函数结束时恢复 //将AFX_MANAGE_STATE(AfxGetStaticModuleState());作为接口函数的第一//条语句进行模块状态切换 AFX_MANAGE_STATE(AfxGetStaticModuleState()); CDialog dlg(IDD_DLL_DIALOG);//打开ID为2000的对话框 dlg.DoModal(); }

      这次我们再点击EXE程序中的“调用DLL”按钮,弹出的是DLL中的如图13的对话框!嘿嘿,弹出了正确的对话框资源。 AfxGetStaticModuleState是一个函数,其原型为:

    AFX_MODULE_STATE* AFXAPI AfxGetStaticModuleState( );

      该函数的功能是在栈上(这意味着其作用域是局部的)创建一个AFX_MODULE_STATE类(模块全局数据也就是模块状态)的实例,对其进行设置,并将其指针pModuleState返回。 AFX_MODULE_STATE类的原型如下:

    // AFX_MODULE_STATE (global data for a module) class AFX_MODULE_STATE : public CNoTrackObject { public: #ifdef _AFXDLL AFX_MODULE_STATE(BOOL bDLL, WNDPROC pfnAfxWndProc, DWORD dwVersion); AFX_MODULE_STATE(BOOL bDLL, WNDPROC pfnAfxWndProc, DWORD dwVersion,BOOL bSystem); #else AFX_MODULE_STATE(BOOL bDLL); #endif ~AFX_MODULE_STATE(); CWinApp* m_pCurrentWinApp; HINSTANCE m_hCurrentInstanceHandle; HINSTANCE m_hCurrentResourceHandle; LPCTSTR m_lpszCurrentAppName; … //省略后面的部分 }

      AFX_MODULE_STATE类利用其构造函数和析构函数进行存储模块状态现场及恢复现场的工作,类似汇编中call指令对pc指针和sp寄存器的保存与恢复、中断服务程序的中断现场压栈与恢复以及操作系统线程调度的任务控制块保存与恢复。  许多看似不着边际的知识点居然有惊人的相似!  AFX_MANAGE_STATE是一个宏,其原型为:

    AFX_MANAGE_STATE( AFX_MODULE_STATE* pModuleState )

      该宏用于将pModuleState设置为当前的有效模块状态。当离开该宏的作用域时(也就离开了pModuleState所指向栈上对象的作用域),先前的模块状态将由AFX_MODULE_STATE的析构函数恢复。  方法二 在DLL接口函数中使用:

    AfxGetResourceHandle(); AfxSetResourceHandle(HINSTANCE xxx);

      AfxGetResourceHandle用于获取当前资源模块句柄,而AfxSetResourceHandle则用于设置程序目前要使用的资源模块句柄。  我们将DLL中的接口函数ShowDlg改为:

    void ShowDlg(void) { //方法2的状态变更 HINSTANCE save_hInstance = AfxGetResourceHandle(); AfxSetResourceHandle(theApp.m_hInstance); CDialog dlg(IDD_DLL_DIALOG);//打开ID为2000的对话框 dlg.DoModal(); //方法2的状态还原 AfxSetResourceHandle(save_hInstance); }

      通过AfxGetResourceHandle和AfxSetResourceHandle的合理变更,我们能够灵活地设置程序的资源模块句柄,而方法一则只能在DLL接口函数退出的时候才会恢复模块句柄。方法二则不同,如果将ShowDlg改为:

    extern CSharedDllApp theApp; //需要声明theApp外部全局变量 void ShowDlg(void) { //方法2的状态变更 HINSTANCE save_hInstance = AfxGetResourceHandle(); AfxSetResourceHandle(theApp.m_hInstance); CDialog dlg(IDD_DLL_DIALOG);//打开ID为2000的对话框 dlg.DoModal(); //方法2的状态还原 AfxSetResourceHandle(save_hInstance); //使用方法2后在此处再进行操作针对的将是应用程序的资源 CDialog dlg1(IDD_DLL_DIALOG); //打开ID为2000的对话框 dlg1.DoModal(); }

      在应用程序主对话框的“调用DLL”按钮上点击,将看到两个对话框,相继为DLL中的对话框(图13)和EXE中的对话框(图14)。  方法三 由应用程序自身切换  资源模块的切换除了可以由DLL接口函数完成以外,由应用程序自身也能完成(下载本工程附件)。  现在我们把DLL中的接口函数改为最简单的:

    void ShowDlg(void) { CDialog dlg(IDD_DLL_DIALOG); //打开ID为2000的对话框 dlg.DoModal(); }

      而将应用程序的OnCalldllButton函数改为:

    void CSharedDllCallDlg::OnCalldllButton() { //方法3:由应用程序本身进行状态切换 //获取EXE模块句柄 HINSTANCE exe_hInstance = GetModuleHandle(NULL); //或者HINSTANCE exe_hInstance = AfxGetResourceHandle(); //获取DLL模块句柄 HINSTANCE dll_hInstance = GetModuleHandle("SharedDll.dll"); AfxSetResourceHandle(dll_hInstance); //切换状态 ShowDlg(); //此时显示的是DLL的对话框 AfxSetResourceHandle(exe_hInstance); //恢复状态 //资源模块恢复后再调用ShowDlg ShowDlg(); //此时显示的是EXE的对话框 }

      方法三中的Win32函数GetModuleHandle可以根据DLL的文件名获取DLL的模块句柄。如果需要得到EXE模块的句柄,则应调用带有Null参数的GetModuleHandle。   方法三与方法二的不同在于方法三是在应用程序中利用AfxGetResourceHandle和AfxSetResourceHandle进行资源模块 句柄切换的。同样地,在应用程序主对话框的“调用DLL”按钮上点击,也将看到两个对话框,相继为DLL中的对话框(图13)和EXE中的对话框(图 14)。  在下一节我们将对MFC扩展DLL进行详细分析和实例讲解,欢迎您继续关注本系列连载。

     

    这是《VC++动态链接库(DLL)编程深入浅出》的第四部分,阅读本文前,请先阅读前三部分:(一)(二)(三)。  MFC扩展DLL的内涵为MFC的扩展,用户使用MFC扩展DLL就像使用MFC本身的DLL一样。除了可以在MFC扩展DLL的内部使用MFC以外, MFC扩展DLL与应用程序的接口部分也可以是MFC。我们一般使用MFC扩展DLL来包含一些MFC的增强功能,譬如扩展MFC的CStatic、 CButton等类使之具备更强大的能力。

      使用Visual C++向导生产MFC扩展DLL时,MFC向导会自动增加DLL的入口函数DllMain:

    extern "C" int APIENTRY DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved) { // Remove this if you use lpReserved UNREFERENCED_PARAMETER(lpReserved); if (dwReason == DLL_PROCESS_ATTACH) {   TRACE0("MFCEXPENDDLL.DLL Initializing!\n");   // Extension DLL one-time initialization   if (!AfxInitExtensionModule(MfcexpenddllDLL, hInstance))    return 0;   // Insert this DLL into the resource chain   // NOTE: If this Extension DLL is being implicitly linked to by   //  an MFC Regular DLL (such as an ActiveX Control)   //  instead of an MFC application, then you will want to   //  remove this line from DllMain and put it in a separate   //  function exported from this Extension DLL.  The Regular DLL   //  that uses this Extension DLL should then explicitly call that   //  function to initialize this Extension DLL.  Otherwise,   //  the CDynLinkLibrary object will not be attached to the   //  Regular DLL's resource chain, and serious problems will   //  result.   new CDynLinkLibrary(MfcexpenddllDLL); } else if (dwReason == DLL_PROCESS_DETACH) {   TRACE0("MFCEXPENDDLL.DLL Terminating!\n");   // Terminate the library before destructors are called   AfxTermExtensionModule(MfcexpenddllDLL); } return 1;   // ok }

      上述代码完成MFC扩展DLL的初始化和终止处理。

      由于MFC扩展DLL导出函数和变量的方式与其它DLL没有什么区别,我们不再细致讲解。下面直接给出一个MFC扩展DLL的创建及在应用程序中调用它的例子。

    6.1 MFC扩展DLL的创建

       下面我们将在MFC扩展DLL中 导出一个按钮类CSXButton(扩展自MFC的CButton类),类CSXButton是一个用以取代 CButton的类,它使你能在同一个按钮上显示位图和文字,而MFC的按钮仅可显示二者之一。类CSXbutton的源代码在Internet上广泛流 传,有很好的“群众基础”,因此用这个类来讲解MFC扩展DLL有其特殊的功效。

      MFC中包含一些宏,这些宏在DLL和调用DLL的应用程序中被以不同的方式展开,这使得在DLL和应用程序中,使用统一的一个宏就可以表示出输出和输入的不同意思:

    // for data #ifndef AFX_DATA_EXPORT #define AFX_DATA_EXPORT __declspec(dllexport) #endif #ifndef AFX_DATA_IMPORT #define AFX_DATA_IMPORT __declspec(dllimport) #endif // for classes #ifndef AFX_CLASS_EXPORT #define AFX_CLASS_EXPORT __declspec(dllexport) #endif #ifndef AFX_CLASS_IMPORT #define AFX_CLASS_IMPORT __declspec(dllimport) #endif // for global APIs #ifndef AFX_API_EXPORT #define AFX_API_EXPORT __declspec(dllexport) #endif #ifndef AFX_API_IMPORT #define AFX_API_IMPORT __declspec(dllimport) #endif #ifndef AFX_EXT_DATA #ifdef _AFXEXT   #define AFX_EXT_CLASS       AFX_CLASS_EXPORT   #define AFX_EXT_API         AFX_API_EXPORT   #define AFX_EXT_DATA        AFX_DATA_EXPORT   #define AFX_EXT_DATADEF #else   #define AFX_EXT_CLASS       AFX_CLASS_IMPORT   #define AFX_EXT_API         AFX_API_IMPORT   #define AFX_EXT_DATA        AFX_DATA_IMPORT   #define AFX_EXT_DATADEF #endif #endif

      导出一个类,直接在类声明头文件中使用AFX_EXT_CLASS即可,以下是导出CSXButton类的例子:

    #ifndef _SXBUTTON_H #define _SXBUTTON_H #define SXBUTTON_CENTER -1 class AFX_EXT_CLASS CSXButton : public CButton { // Construction public: CSXButton(); // Attributes private: // Positioning BOOL  m_bUseOffset;    CPoint  m_pointImage; CPoint  m_pointText; int   m_nImageOffsetFromBorder; int   m_nTextOffsetFromImage; // Image HICON  m_hIcon;     HBITMAP  m_hBitmap; HBITMAP  m_hBitmapDisabled; int   m_nImageWidth, m_nImageHeight; // Color Tab char  m_bColorTab;    COLORREF m_crColorTab; // State BOOL  m_bDefault; UINT  m_nOldAction; UINT  m_nOldState; // Operations public: // Positioning int  SetImageOffset( int nPixels ); int  SetTextOffset( int nPixels ); CPoint SetImagePos( CPoint p ); CPoint SetTextPos( CPoint p ); // Image BOOL SetIcon( UINT nID, int nWidth, int nHeight ); BOOL SetBitmap( UINT nID, int nWidth, int nHeight ); BOOL SetMaskedBitmap( UINT nID, int nWidth, int nHeight, COLORREF crTransparentMask ); BOOL HasImage() { return (BOOL)( m_hIcon != 0  | m_hBitmap != 0 ); } // Color Tab void SetColorTab(COLORREF crTab); // State BOOL SetDefaultButton( BOOL bState = TRUE ); private: BOOL SetBitmapCommon( UINT nID, int nWidth, int nHeight, COLORREF crTransparentMask, BOOL bUseMask ); void CheckPointForCentering( CPoint &p, int nWidth, int nHeight ); void Redraw(); // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CSXButton) public: virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct); //}}AFX_VIRTUAL // Implementation public: virtual ~CSXButton(); // Generated message map functions protected: //{{AFX_MSG(CSXButton) afx_msg LRESULT OnGetText(WPARAM wParam, LPARAM lParam); //}}AFX_MSG DECLARE_MESSAGE_MAP() }; #endif

       把SXBUTTON.CPP文件直接添加到工程,编译工程,得到“mfcexpenddll.lib”和“mfcexpenddll.dll”两个文 件。我们用Visual Studio自带的Depends工具可以查看这个.dll,发现其导出了众多符号(见图15)。

    图15 导出类时导出的大量符号 (+放大该图片)

      这些都是类的构造函数、析构函数及其它成员函数和变量经编译器处理过的符号,我们直接用__declspec(dllexport)语句声明类就导出了这些符号。

       如果我们想用.lib文件导出这些符号,是非常困难的,我们需要在工程中生成.map文件,查询.map文件的符号,然后将其一一导出。如图16,打开 DLL工程的settings选项,再选择Link,勾选其中的产生MAP文件(Generate mapfile)就可以产生.map文件了。

      打开mfcexpenddll工程生成的.map文件,我们发现其中包含了图15中所示的符号(symbol)

    0001:00000380  ?HasImage@CSXButton@@QAEHXZ 10001380 f i SXBUTTON.OBJ 0001:000003d0  ??0CSXButton@@QAE@XZ       100013d0 f   SXBUTTON.OBJ 0001:00000500  ??_GCSXButton@@UAEPAXI@Z   10001500 f i SXBUTTON.OBJ 0001:00000570  ??_ECSXButton@@UAEPAXI@Z   10001570 f i SXBUTTON.OBJ 0001:00000630  ??1CSXButton@@UAE@XZ       10001630 f   SXBUTTON.OBJ 0001:00000700 ?_GetBaseMessageMap@CSXButton@@KGPBUAFX_MSGMAP@@XZ 10001700 f   SXBUTTON.OBJ 0001:00000730 ?GetMessageMap@CSXButton@@MBEPBUAFX_MSGMAP@@XZ 10001730 f   SXBUTTON.OBJ 0001:00000770    ?Redraw@CSXButton@@AAEXXZ  10001770 f i SXBUTTON.OBJ 0001:000007d0    ?SetIcon@CSXButton@@QAEHIHH@Z 100017d0 f   SXBUTTON.OBJ ……………………………………………………………………..//省略

    图16 产生.map文件 (+放大该图片)

      所以,对于MFC扩展DLL,我们不宜以.lib文件导出类。

    6.2 MFC扩展DLL的调用

      在DLL所在工作区新增一个dllcall工程,它是一个基于对话框的MFC EXE程序。在其中增加两个按钮SXBUTTON1、SXBUTTON2,并设置其属性为“Owner draw”,如图17。

    图17 设置按钮属性为“Owner draw”

      在工程中添加两个ICON资源:IDI_MSN_ICON(MSN的图标)、IDI_REFBAR_ICON(Windows的系统图标)。

      修改工程的“calldllDlg.h”头文件为:

    #include "..\..\mfcexpenddll\SXBUTTON.h"  //包含dll的导出类头文件 #pragma comment(lib,"mfcexpenddll.lib")    //隐式链接dll ///////////////////////////////////////////////////////////////////////////// // CCalldllDlg dialog class CCalldllDlg : public CDialog { // Construction public: CCalldllDlg(CWnd* pParent = NULL); // standard constructor // Dialog Data //{{AFX_DATA(CCalldllDlg) enum { IDD = IDD_CALLDLL_DIALOG }; //增加与两个按钮对应的成员变量 CSXButton m_button1;   CSXButton m_button2; … }

      同时,修改“calldllDlg.cpp”文件,使得m_button1、m_button2成员变量与对话框上的按钮控件建立关联:

    void CCalldllDlg::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); //{{AFX_DATA_MAP(CCalldllDlg) DDX_Control(pDX, IDC_BUTTON2, m_button2); DDX_Control(pDX, IDC_BUTTON1, m_button1); //}}AFX_DATA_MAP }

      修改BOOL CCalldllDlg::OnInitDialog()函数,在其中增加对两个按钮设置ICON的代码:

    BOOL CCalldllDlg::OnInitDialog() { CDialog::OnInitDialog(); // Add "About..." menu item to system menu. // IDM_ABOUTBOX must be in the system command range. ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX); ASSERT(IDM_ABOUTBOX < 0xF000); CMenu* pSysMenu = GetSystemMenu(FALSE); if (pSysMenu != NULL) {   CString strAboutMenu;   strAboutMenu.LoadString(IDS_ABOUTBOX);   if (!strAboutMenu.IsEmpty())   {    pSysMenu->AppendMenu(MF_SEPARATOR);    pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);   } } // Set the icon for this dialog.  The framework does this automatically //  when the application's main window is not a dialog SetIcon(m_hIcon, TRUE);   // Set big icon SetIcon(m_hIcon, FALSE);  // Set small icon // TODO: Add extra initialization here m_button1.SetIcon(IDI_MSN_ICON,16,16); m_button2.SetIcon(IDI_REFBAR_ICON,16,16); return TRUE;  // return TRUE  unless you set the focus to a control }

      运行程序,将出现如图18的对话框,图形和文字同时出现在按钮上,这说明我们正确地调用了MFC扩展DLL。

    图18 DLL扩展的按钮被显示

      如果我们不修改void CCalldllDlg::DoDataExchange(CDataExchange* pDX),即不增加下列代码:

    DDX_Control(pDX, IDC_BUTTON2,  m_button2); DDX_Control(pDX, IDC_BUTTON1,  m_button1);