博文

[置顶] JPEG2000压缩技术(2008-4-19 14:41:00)


JPEG2000压缩技术  http://i.cn.yahoo.com/jielin/blog/p_28/

一、引言

传统图像编码采用离散余弦变换(DCT)技术,可以较好地去除图像信息的统计冗余,具有良好的实时性和块操作与运动估计的匹配性。目前该技术日臻成熟,形成了JPEG、MPEG等国际标准。但是由于DCT变换过程采用分块技术,在高压缩比条件下容易导致方块效应,严重影响主观质量。而且DCT不适合带宽较宽(拥有较多边缘轮廓信息)的图像信号。于是人们将目光转向小波变换。

传统的卷积小波变换是一种不受带宽约束的图像处理方法,支持多分辨率、多码率传输方案,而且不对图像进行分块,避免了DCT变换的方块效应。但是,采用卷积运算方法,过程复杂,运算量大,实时性不好,不利于硬件的实现。而基于JPEG2000标准推荐的提升小波变换,采用9/7、5 /3滤波器组对分解的图像进行提升运算,不但继承了传统小波变换的优点,而且实时性好、运算简单、易于硬件实现。

JPEG2000具有以下主要特点:

(1)    良好的低比特率压缩性能这是JPEG2000最主要的特征。

目前的JPEG标准,对于细节分量多的灰度图像,当码率低于0.25bpp(0.25bit/piexl)时,视觉失真大。为克服这一点,要求JPEG2000在低比特率下有良好的率失真性能,以适应网络、移动通信等有限带宽的应用需要。

(2)连续色调和二值图像压缩

目前的JPEG标准对于自然图像具有较好的压缩性能,但是当用于计算机图形和二值文本的压缩时,性能变差,不适用于复合文本压缩。为了改进这一点,JPEG2000在统一系统中采用相似的方法,能够对自然图像、复合文本、医学图像、计算机图形等具有不同特征、不同类型的图像进行压缩。

(3)有损和无损压缩

对于目前的JPEG标准,在同一个压缩码流中不能同时提供有损和无损两种压缩,而在JPEG2000系统中,通过选择参数,能够对图像进行有损和无损两种压缩,可满足图像质量要求很高的医学图像、图像库等方面的处理需要。

(4) 按照像素精度或者分辨率进行累进式传输

累进式图像传输允许图像按照所需的分辨率或像素精度进行重构,用户根据需要,对图像传输进行控制,在获得所需的图像分辨率或质量要求后,便可终止解码,而不必接收整个图像压缩码流。

(5)随机存取和处理码流

由于JPEG2000采用小波技术,利用其局部分辨特性,在不解压的情况下,可随机存取某些感兴趣的图像区域ROI (Region of Interest)的压缩码流,对压缩的图像数据进行传输、滤波等操作。

(6)强的抗误码特性

在无线通信信道上,噪声干扰大,这就希望压缩码流具有较强的容错性能。JPEG2000系统通过设计适当的码流格式和相应的编码措施,来减小因解码失败造成的损失。

(7)固定速率、固定大小、有限的存储空间

由于处理的图像越来越大,这为硬件实现以及带宽资源和存储空间有限的应用提出了问题。JPEG2000使用分块技术,对每个小块进行处理,来解决这类问题。

除了上述主要特点外,JPEG2000还采用开放式结构,并对图像安全保护、图像交换等方面做了考虑。

JPEG2000的核心部分是图像编码系统。其编码器和解码器的框图分别如图2.1和图2.2所示。JPEG2000图像编码系统基于David Taubman提出的EBCOT (Embedded Block Coding with Optimized Truncation)算法,使用小波变换,采用两层编码策略,对压缩位流分层组织,不仅获得较好的压缩效率,而且压缩码流具有较大的灵活性。

在编码时,首先对源图像进行离散小波变换,根据变换后的小波系数特点进行量化。将量化后的小波系数划分成小的数据单元—码块,对每个码块进行独立的嵌入式编码。将得到的所有码块的嵌入式位流,按照率失真最优原则分层组织,形成不同质量的层。对每一层,按照一定的码流格式打包,输出压缩码流。

解码过程相对比较简单。根据压缩码流中存储的参数,对应于编码器各部分进行逆向操作,输出重构图像数据。

 

二、JPEG2000的编码过程

JPEG2000的编码过程主要分为以下几个过程:预处理、核心处理和位流组织。预处理部分包括对图像的分片、直流电平(DC)位移和分量变换。核心处理部分由离散小波变换、量化和熵编码组成。位流组织部分则包括区域划分、码块、层和包的组织。

1、预处理

(1)图像分片

分片是指把源图像分割成相互不重叠的矩形块——图像片,每一个图像片作为一个独立的图像进行压缩编码。编码中的所有错作都是针对图像片进行的。图像片是进行变换和编解码的基本单元。图像的分片降低了对存储空间的要求,并且由于他们重构是也是独立进行的,所以可以用来对图像的特定区域而不是整幅图像进行解码。当然,图像分片会影响图像质量。比较小的图像片会比大的图像片产生更大的失真。图像分片在低比特率表示图像的时候所造成的图像失真会更加严重。

(2)DC电平位移

在对每一图像片进行正向离散小波变换之前,都要进行直流电位平移。目的是在解码时,能够从有符号的数值中正确恢复重构的无符号样本值。直流电平位移是对仅有无符号数组成的图像片的像素进行的。电平位移并不影响图像的质量。在解码端,在离散小波反变换之后,对重构的图像进行反向直流电平位移。

(3)分量变换

JPEG2000支持多分量图像,不同的分量不需要有相同的比特深度,也不需要都是无符号或有符号数。对于可恢复(无损)系统,唯一的要求是每一个输出分量图像的比特深度必须跟相应输入分量图像的比特深度保持一致。

2、核心处理

(1)    小波变换

不同于传统的DCT变换,小波变换具有对信号进行多分辨分析和反映信号局部特征的特定。通过对图像片进行离散小波变换,得到小波系数图像,而分解的级数视具体情况而定。小波系数图像由几种子带系数图像组成。这些子带系数图像描述的是图像片水平和垂直方向的空间频率特性。不同子带的小波系数反映图像片不同空间分辨率的特性。通过多级小波分解,小波系数既能表示图像片中局部区域的高频信息(如图像边缘),也能表示图像片中的低频信息(如图像背景)。这样,即使在低比特率的情况下,我们也能保持较多的图像细节(如边缘)。另外,下一级分解得到的系数所表示的图像在水平和垂直方向的分辨率只有上一级小波系数所表示的图像的一半。所以,通过对系数图像的不同级数进行解码,就可以得到具有不同空间分辨率(或清晰,或模糊)的图像。

小波变换因其具有这样的优点被JPEG2000标准所采用。在编码系统中,对每个图像片进行Mallat塔式小波分解。经过大量的测试,JPEG2000选用两种小波滤波器:LeGall 5/3滤波器和Daubechies 9/7滤波器。前者,可用于有损或无损图像压缩,后者只能用于有损压缩。

在JPEG2000标准中,小波滤波器可以有两种实现模式:基于卷积的和基于提升机制的。而具体实现时,对图像边缘都要进行周期对称延伸,这样可以防止滤波器对图像边缘操纵时产生失真。另外,为了减小变换时所需空间的开销,标准中还应用了基于行的小波变换技术。

(2)    量化

由于人类视觉系统对图像的分辨率要求有一定的局限,通过适当的量化减小变换系数的精度,可在不影响图像主观质量的前提下,达到图像压缩的目的。量化的关键是根据变换后图像的特征、重构图像质量要求等因素设计合理的量化补偿。量化操纵是有损的,会产生量化误差,不过一直情况除外,那就是量化步长为1,并且小波系数都是整数,利用可恢复整数5/3小波滤波器进行小波变换得到的结果就符合这种情况。

在JPEG2000标准中,对每一个子带可有不同的量化步长。但是在一个子带中只有一个量化步长。量化以后,每个小波系数由2部分来表示:符合和幅值。对量化后的小波系数进行编码。对于无损压缩,量化步长必须是1。

(3)    熵编码

图像经过变换、量化后,在一定程度上减少了空域和频域上的冗余度,但是这些数据在统计意义上还存在一定的相关性,为此采用熵编码来消除数据间的统计相关。将量化后的子带系数划分成小的矩形单元——码块(code block)。

 


图2.3 JPEG2000 两层编码框图

如图2.3所示,采用两层编码策略,首先使用基于上下文的算术编码器,每个码块进行独立的嵌入式码块编码,得到码块的嵌入式压缩位流。然后,根据率失真优化原则,采用PCRD(Post Compression Rate Distortion)优化算法思想,将所有码块的压缩位流适当截取,组织成具有不同质量的图像。在分层组织压缩位流时,须对每个码块在每一层上的贡献信息进行编码,即对码块位流在该层的截断点信息的编码。由于图像采用小波变换,整个图像压缩位流具有分辨率可分级性,从而,压缩位流可同时具有质量上和分辨率上的可分级性。由于对码块进行独立编码,因此,可根据需要,随机获取并解码相应的码块压缩位流,重构出所需的图像区域。

①    第一层编码算法

与传统的依次对每个系数进行算术熵编码不同,JPEG2000编码系统把码块中的量化系数组织成若干个位平面,从最高有效位平面(MSB)开始,依次对每个位平面上的小波系数位进行算术编码。

第一层编码可以看作两部分:上下文的生成(CF)和算术熵编码(AE)。在上下文的生成中,以一定的顺序扫描码块中的所有位。在码块的每个位平面上,从左上角系数开始,从左到右,从上到下进行扫描,并为每一位生成一个上下文。算术编码器根据生成的上下文,对每一位进行算术编码。

在量化后,小波系数被转化为符号-振幅模式。在从MSB到LSB编码时,当遇到第一个为1的比特时,这个像素被称作是显著的,否则,为不显著的。所有比特的上下文都是由他们的邻域通过以下的4种方法产生:

零编码(Zero Coding,ZC)用来编码非显著像素在当前的位平面中是否将变得显著;

游程编码(Run-length Coding,RLC)用来编码位于同一列中的4个非显著性像素,如果他们的邻域都是非显著的;

符号编码(Sign Coding, SC)当该位变得显著后,编码他的符号位;

幅度编码(Magnitude Refinement, MR)用来编码显著位。

每个位平面都在3个编码通道中进行编码。通道1是重要性传播通道(Significance Propagation Pass),至少有一个重要性邻域的像素,在此通道进行编码,使用ZC和SC。通道2是幅度细化通道(Magnitude Refinement Pass),所以的重要位在此通道进行编码,使用MR。通道3是清除通道(Cleanup Pass),所有没有在上两个通道中进行编码的像素,在此通道中进行编码,使用ZC,LRC,SC。位平面中的每一位在3个通道中进行检查来确定是否应当被编码。

由编码通道得到的上下文和其对应的数据一起,送至算术编码器进行编码。在这里,采用了自适应二进制算术编码,主要是考虑到计算的复杂度及实行的方便性。在进行算术编码后,对每一个码块,得到了一个独立的嵌入式码块压缩位流。

②    第二层编码算法

在第二层编码算法中,采用PCRD率失真优化算法思想,对所有码块的嵌入式压缩位流进行适当的截取,分层组织,形成整个图像的具有质量可分级的压缩码流。第二层编码算法也可以看作两部分:速率控制和分层组织压缩位流。速率控制是指通过一定的编解码措施,获得给定压缩率下的最佳重构图像质量。分层组织压缩位流根据编码参数所规定的分层层数以及每一层的编码速率,估算每一层的率失真门限,然后根据每一层估计出的率失真门限,按照码块率失真算法,找到一个码块嵌入式压缩位流在该层的截断点,将截断的码块压缩位流进行打包,按照规定的格式存储,形成图像压缩码流。将码流分层组织,每一层含有一定的质量信息,在前面的基础上改善图像质量。这样用户可以根据自己的需要,控制图像的传输,在取得满意的图像效果后,中止传输,在某种程度上缓解当前网络带宽有限而图像数据量大而造成的瓶颈问题。

(3)位流编码

为了适应图像交换,更好地应用JPEG2000压缩码流的功能,JPEG2000标准规定了存放压缩位流和解码所需参数的格式,把压缩码流以包为单元进行组织,形成最终的码流。

3、JPEG2000种的关键技术

(1)    离散小波变换

JPEG2000与传统JPEG最大的不同在于它放弃了JPEG所采用的以离散余弦变换(DCT)为主的区块编码方式,转而采用以小波变换(DWT)为主的多解析编码方式。

余弦变换是经典的谱分析工具,它考察的是整个时域过程的频域特征或整个频域过程的时域特征,因此对于平稳过程,它有很好的效果,但对于非平稳过程,这种算法靠丢弃频率信息实现压缩,因而图像的压缩率越高,频率信息被丢弃的越多。在极端的情况下,JPEG图像只保留了反映图像外貌的基本信息,精细的图像细节都损失了。小波变换是现代谱分析工具,它既能考查局部时域过程的频域特征,又能考察局部频域过程的时域特征,因此即使对于非平稳过程,处理起来也得心应手。它能将图像变换为一系列小波系数,这些系数可以被高效压缩和存储,此外,小波的粗略边缘可以更好地表现图像,因为它消除了DCT压缩普遍具有的方块效应。

(2)    速率控制算法

JPEG2000通过采用速率控制方法来计算码流的理想截断点,从而获得给定压缩码流下的最佳重构图像质量。速率控制使用了PCRD率失真优化算法。率失真优化,即给定整个压缩码流的最大编码速率,找出每个码块压缩位流的适当截断点,在满足的条件下,使重构图像失真最小。从而使得嵌入式码块编码具有如下特点:生成的压缩位流可根据需要,被截断成不同长度的位流子集;将所有码块的截断位流组织起来,可重构出一定质量的图像。

(3)    渐进性传输

现在网络上的JPEG图像下载时是按“块”传输的,因此只能逐行显示,而采用JPEG2000格式的图像支持渐进传输。JPEG2000中的渐进传输有2种,按照分辨率的渐进传输和按照质量的渐进传输。按照质量的渐进传输就是先传输图像的轮廓数据,然后再逐步传输细节数据来不断提高图像质量,而按照分辨率的渐进传输则先传输分辨率较低的图像,后一幅图像在前一幅图像的基础上提高其分辨率。图像的渐进传输使得用户不需要像以前那样等图像全部下载后才决定是否需要,有助于快速地浏览和选择大量图片,从而有效的解决了网络传输的瓶颈问题。

(4)    感兴趣区域压缩

JPEG2000的一个极其重要的优点就是ROI(Region of interest,感兴趣区域)。用户可以任意指定图片上感兴趣的区域,然后在压缩时对这些区域指定压缩质量,或在恢复时指定某些区域的解压缩要求。这是因为子波在空间和频率上具有局域性,要完全恢复图像中的某个局部,并不需要所有的编码都被精确保留,只要对应它的一部分编码没有误差就可以了。在实际应用中,我们就可以对一幅图像中感兴趣的部分采用低压缩比以获得较好的图像效果,而对其他部分采用高压缩比以节省存储空间。这样就能在保证不丢失重要信息的同时又有效地压缩了数据量,实现了真正的“交互式”压缩。

 


阅读全文(1187) | 评论:8 | 复制链接

运输法虚重载(2008-11-3 21:47:00)

你这儿是没有办法把<<申明为virtual,因为作为成员函数   
  <<只能是ostream的member。   
  不过换一种方法还是可以的   
  大致如下哈:   
    
  class   FtpMessage   {   
  protected:   
      virtual   void   ToString(ostream   &os)   =   0;   
  public:   
      ...   
      friend   ostream&   operator   <<(ostream&   os,   FtpMessage   &   msg);   
  };   
    
  ostream&   operator   <<(ostream&   os,   FtpMessage   &   msg)   
  {   
      msg.ToString(os);   
      return   os;   
  }   
    
  class   FtpCtrlMsgCtoS   :   public   FtpMessage   
  {   
  protected:   
      virtual   void   ToString(ostream   &os){   
            //specific   handling   for   FtpCtrlMsgCtoS     
      }   
  };   
    
  class   CtoSPORT   :   public   FtpCtrlMsgCtoS   
  {   
  protected:   
      virtual   void   ToString(ostream   &os){   
            //specific   handling   for   CtoSPORT     
      }   
  }   
    
  namespace   std   
  {   
      template<>   
          class   ostream<FtpMessage>{   
          };   
  }   
    
  ...   
  void   FtpDaemon::SendFtpCtrlMsgCtoS(FtpCtrlMsgCtoS&   msg)   
  {   
          ostringstream   os;   
          os   <<   msg;   
          send(socketFtpCtrl,   os.str().c_str(),   os.str().length()+1,   0);   
  }   


说明:有的C++编译系统没有完全实现C++标准,它所提供不带后缀.h的头文件不支持把成员函数重载为友元函数。

简单说明一下吧:
#include <iostream>
using namespace std;

class A
{
public:
A operator *(A & a)
{
return a;
}
int value;
};

int main()
{
A * test_a=new A;
test_a->value=2;
cout<<(* test_a).value<<endl;
return 0;
}

流操作符 < <和>>重载返回值为引用,这两个操作符常常希望被连续使用,例如:cout < < "hello" < < endl; 因此这两个操作符的返回值应该是一个仍然支持这两个操作符的流引用。可选的其它方案包括:返回一个流对象和返回一个流对象指针。但是对于返回一个流对象,程序必须重新(拷贝)构造一个新的流对象,也就是说,连续的两个 < <操作符实际上是针对不同对象的!这无法让人接受。对于返回一个流指针则不能连续使用 < <操作符。因此,返回一个流对象引用是惟一选择。这个唯一选择很关键,它说明了引用的重要性以及无可替代性,也许这就是C++语言中引入引用这个概念的原因吧。 赋值操作符=。这个操作符象流操作符一样,是可以连续使用的,例如:x = j = 10;或者(x=10)=100;赋值操作符的返回值必须是一个左值,以便可以被继续赋值。因此引用成了这个操作符的惟一返回值选择。。

而+-*/ 四则运算符。它们不能返回引用,主要原因是这四个操作符没有side effect,因此,它们必须构造一个对象作为返回值,可选的方案包括:返回一个对象、返回一个局部变量的引用,返回一个new分配的对象的引用、返回一个静态对象引用。根据前面提到的引用作为返回值的三个规则,第2、3两个方案都被否决了。静态对象的引用又因为((a+b) == (c+d))会永远为true而导致错误。所以可选的只剩下返回一个对象了。


对模板来说,友元函数有三种:
1.非模板友元类或者友元函数,这个没什么好说的,直接写就是了。
2.绑定的友元类或者友元函数。这个就是类模板和友元函数(类)之间的一对一的映射。
      声明形式就是:  
          template  <class  Type>
          class  classname{
            ...
              friend  int  func <Type> (param...);
          }            

3.非绑定的友元类或者友元函数。这个就是类模板和友元函数(类)之间的一对多的映射。
      声明形式就是:  
          template  <class  Type>
          class  classname{
            ...
            template  <class  T>
              friend  int  func(param...);
          }  



首先感谢大家的关注和回答.但是发现大家热心有余,认真不足-------因为没有一个人真的编译过我的问题代码..
我在帖子中给的问题代码,因为去除了所有其他的不相关代码,所以是可以编译通过的.如下:
/////////////// Matrix.h 文件内容 (部分代码)//////////////////
#include <iostream>
//// Matrix 模板前向声明 /////
template <typename T>
class Matrix;
/////////////////////////////
template <typename T>
std::ostream& operator * (std::ostream& os,const Matrix <T>& mat);///  编译通过

template <typename T>
std::ostream& operator < < (std::ostream& os,const Matrix <T>& mat);  ///  ///  编译通过


template <typename T>
class Matrix
{
friend std::ostream& operator * <T> (std::ostream&,const Matrix <T>&);    ///  编译 通过
friend std::ostream& operator < < <T> (std::ostream&,const Matrix <T>&);  ///  编译  通过  
};


而我之前之所以不能编译通过,是因为我的实际代码中声明 了 一个 operator * 的重载成员函数,
导致它和友元的operator * 模板函数冲突  (但实际不应该这样啊.没看见过C++有这类语法要求.)

真正编译不通过的代码如下:

/////////////// Matrix.h 文件内容 (这段代码编译不通过)//////////////////
#include <iostream>
//// Matrix 模板前向声明 /////
template <typename T>
class Matrix;
/////////////////////////////
template <typename T>
std::ostream& operator * (std::ostream& os,const Matrix <T>& mat);///  编译通过

template <typename T>
std::ostream& operator < < (std::ostream& os,const Matrix <T>& mat);  ///  ///  编译通过


template <typename T>
class Matrix
{
        const Matrix operator *(const Matrix&) const; ////////  注意..这里是问题的根源!!!!!!
friend std::ostream& operator * <T> (std::ostream&,const Matrix <T>&);    ///  编译不通过
friend std::ostream& operator < < <T> (std::ostream&,const Matrix <T>&);  ///  编译  通过  
};

昨天发现出现编译不通过的问题后,我被引入了误区,一直以为是operator * 的问题
甚至发现 operator +, operator -,operator / 都不可以.  
唯独 operator < < 可以.
后来我发现如果在类中也声明一个  operator < < 的重载成员函数,那么  
friend std::ostream& operator < < <T> (std::ostream&,const Matrix <T>&);
这一段也将编译不通过...

所以找到解决方案------ 把所有的friend 声明,都写在类的成员定义之前.代码如下:

/////////////// Matrix.h 文件内容 ( 编译 成功!yeah)//////////////////
#include <iostream>
//// Matrix 模板前向声明 /////
template <typename T>
class Matrix;
/////////////////////////////
template <typename T>
std::ostream& operator * (std::ostream& os,const Matrix <T>& mat);///  编译通过

template <typename T>
std::ostream& operator < < (std::ostream& os,const Matrix <T>& mat);  ///  ///  编译通过


template <typename T>
class Matrix
{
        //////注意,,把所有 friend 声明都放在成员定义之前..////////////
        friend std::ostream& operator * <T> (std::ostream&,const Matrix <T>&);    ///  编译 通过
friend std::ostream& operator < < <T> (std::ostream&,const Matrix <T>&);  ///  编译  通过  
        const Matrix operator *(const Matrix&) const; ////////  注意..这里是问题的根源!!!!!!
};


template<typename T>
class Matrix;

template <typename T>
std::ostream&  operator* (T,const Matrix<T>   &a);  

template   <typename T>   
class       Matrix   
{       
    friend       std::ostream&  operator* <>(T,const Matrix<T>   &a);   //////编译通过      
    friend       std::ostream&  operator  << <T> (T,const Matrix<T>   &a);   //////编译通过   
} ;  

初学的话给你说概念越说越迷糊,下面举例子来说,看代码:


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


#include <iostrem.h>

namespace myns {

class clsA {
protected:
static int m_ia;
public:
int m_ib;
public:
clsA():m_ib(0) {};
~clsA() {};
};

int clsA::m_ia=32; // 正确。m_ia是类clsA的静态变量,需要在类定义域外部初始化。

}

int myns::clsA::m_ia=32; // 同样正确,理由同上。但是m_ia只能初始化一次,因此本行与上一行仅能二者取其一。

void main()
{
myns::clsA c1; // 正确。类clsA定义在名字空间myns中,cl为clsA类型的变量。
cl.m_ib = 1; // 正确。点操作符用于引用类实例的成员。

using namespace myns;
clsA *pc1 = NULL; // 正确。先声明名字空间myns,然后可以直接声明其中定义的类的实例或指向他们的指针。
pc1 = &c1; // 取c1的地址赋值给pc1。
pc1->m_ib = 2; // 正确。箭头操作符用于引用指针对象的成员。

cout << c1.m_ib; // 输出2。

cout << c1.m_ia; // 错误,不能引用类中的保护成员。
cout << c1->m_ib; // 错误。箭头操作符不能引用非指针对象的成员。
cout << pc1.m_ib; // 错误。点操作符不能引用指针对象的成员。

}


阅读全文(471) | 评论:0 | 复制链接

select函数详解及应用(2008-10-31 21:26:00)

Select在Socket编程中还是比较重要的,可是对于初学Socket的人来说都不太爱用Select写程序,他们只是习惯写诸如connect、 accept、recv或recvfrom这样的阻塞程序(所谓阻塞方式block,顾名思义,就是进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即返回)。可是使用Select就可以完成非阻塞(所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况——读写或是异常。下面详细介绍一下!

Select的函数格式(我所说的是Unix系统下的伯克利socket编程,和windows下的有区别,一会儿说明):

int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);

先说明两个结构体:

第一,struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符(file descriptor),即文件句柄,这可以是我们所说的普通意义的文件,当然Unix下任何设备、管道、FIFO等都是文件形式,全部包括在内,所以毫无疑问一个socket就是一个文件,socket句柄就是一个文件描述符。fd_set集合可以通过一些宏由人为来操作,比如清空集合 FD_ZERO(fd_set *),将一个给定的文件描述符加入集合之中FD_SET(int ,fd_set *),将一个给定的文件描述符从集合中删除FD_CLR(int ,fd_set*),检查集合中指定的文件描述符是否可以读写FD_ISSET(int ,fd_set* )。一会儿举例说明。

第二,struct timeval是一个大家常用的结构,用来代表时间值,有两个成员,一个是秒数,另一个是毫秒数。

具体解释select的参数:

int maxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!在Windows中这个参数的值无所谓,可以设置不正确。

fd_set *readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。

fd_set *writefds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。

fd_set *errorfds同上面两个参数的意图,用来监视文件错误异常。

struct timeval* timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态,第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;第三,timeout的值大于0,这就是等待的超时时间,即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。

返回值:

负值:select错误 正值:某些文件可读写或出错 0:等待超时,没有可读写或错误的文件

在有了select后可以写出像样的网络程序来!举个简单的例子,就是从网络上接受数据写入一个文件中。

例子:

main()

{

int sock;

FILE *fp;

struct fd_set fds;

struct timeval timeout={3,0}; //select等待3秒,3秒轮询,要非阻塞就置0

char buffer[256]={0}; //256字节的接收缓冲区

/* 假定已经建立UDP连接,具体过程不写,简单,当然TCP也同理,主机ip和port都已经给定,要写的文件已经打开

sock=socket(...);

bind(...);

fp=fopen(...); */

while(1)

{

FD_ZERO(&fds); //每次循环都要清空集合,否则不能检测描述符变化

FD_SET(sock,&fds); //添加描述符

FD_SET(fp,&fds); //同上

maxfdp=sock>fp?sock+1:fp+1; //描述符最大值加1

switch(select(maxfdp,&fds,&fds,NULL,&timeout)) //select使用

{

case -1: exit(-1);break; //select错误,退出程序

case 0:break; //再次轮询

default:

if(FD_ISSET(sock,&fds)) //测试sock是否可读,即是否网络上有数据

{

recvfrom(sock,buffer,256,.....);//接受网络数据

if(FD_ISSET(fp,&fds)) //测试文件是否可写

fwrite(fp,buffer...);//写入文件

buffer清空;

}// end if break;

}// end switch

}//end while

}//end main


阅读全文(1203) | 评论:0 | 复制链接

利用结构体返回多个函数值(2008-10-24 23:53:00)

#include <iostream.h>
#include <malloc.h>
#include <string.h>

typedef struct student
{
    char name[10];
    int age;
}Student;

Student* fun1()
{
    Student* ps = new Student;
    strcpy(ps->name, "zhong");
    ps->age  = 0x100;
    return ps;
}

void main()
{
    Student* ps=fun1();  
    cout<<"name:"<<ps->name<<"\t"<<"age:"<<hex<<(*ps).age<<endl;
    delete ps;
}
一般的函数只能由一个返回值
如何同时返回 name 和age 呢?
我们可以将其封装到一个结构体中,这样就可以通过结构体的方式同时返回这两个参数

注意:每次使用了fun1后,一定要释放其内部声请的内存空间
比如本程序中的 delete ps; ,否则将造成内存泄漏


阅读全文(519) | 评论:0 | 复制链接

DLL(动态链接库)专题(2008-10-21 18:54:00)

(0)   Windows API中所有的函数都包含在dll中,其中有3个最重要的DLL

      (1)   Kernel32.dll

      它包含那些用于管理内存、进程和线程的函数,例如CreateThread函数;

      (2)   User32.dll

     它包含那些用于执行用户界面任务(如窗口的创建和消息的传送)的函数,例如CreateWindow函数;

      (3)   GDI32.dll

     它包含那些用于画图和显示文本的函数。

 

1.      静态库和动态库

(1)   静态库

         函数和数据被编译进一个二进制文件(通常扩展名为.LIB)。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其他模块组合起来创建最终的可执行文件(.Exe文件).当发布产品时,只需要发布这个可执行文件,并不需要发布被使用的静态库。

(2)   动态库

      在使用动态库的时候,往往提供两个文件:一个引入库(.lib)文件和一个DLL(.dll)文件。虽然引入库的后缀名也是”lib”,但是动态库的引入库文件和静态库文件有着本质上的区别,对一个DLL来说,其引入库文件(.lib)包含该DLL导出的函数和变量的符号名,而.dll文件包含该DLL实际的函数和数据。在使用动态库的情况下,在编译链接可执行文件时,只需要链接该DLL的引入库文件,该DLL中的函数代码和数据并不复制到可执行文件中,直到可执行程序运行时,才去加载所需的DLL,将该DLL映射到进程的地址空间外,然后访问DLL中导出的函数。这时,发布产品时,除了发布可执行文件以外,同时还要发布该程序将要调用的动态链接库。

2.      在导出库头文件中的标准写法:

#ifdef LIBDAQ_EXPORTS

#define LIBDAQ_API __declspec(dllexport)

#else

#define LIBDAQ_API __declspec(dllimport)

#endif

   将该头文件添加到某客户代码中时,会自动展开。如果客户代码没有定义LIBDAQ_EXPORTS,那么LIBDAQ_EXPORTS会被定义为__declspec(dllimport)表示有LIBDAQ_EXPORTS头的函数都是从该DLL中导入的

3.      名字改编和”extern “C””

         C++编译器在生成DLL时,会对导出的函数进行名字改编,并且不同的编译器使用的改变规则不一样,因此改编后的名字会不一样。这样,如果利用不同的编译器分别生成DLL和访问该DLL的客户端代码程序的话,后者在访问该DLL的导出函数时会出现问题。为了实现通用性,需要加上限定符:extern “C”

         但是利用限定符extern “C”可以解决C++C之间相互调用时函数命名的问题,但是这种方法有一个缺陷,就是不能用于导出一个类的成员函数,只能用于导出全局函数。

 
4.      显示加载方式加载DLL

         使用动态方式来加载动态链接库时,需要用到LoadLibrary函数。该函数的作用就是将指定的可执行模块映射到调用进程的地址空间。调用原型为:

HMODULE LoadLibrary(LPCTSTR lpFileName);

         LoadLibrary函数不仅可以加载DLL,还可以加载可执行模块(Exe)。当加载可执行模块时,主要是为了访问该模块内的一些资源,例如对话框资源、位图资源或图标资源等。LoadLibrary函数有一个字符串类型(LPCTSTR)的参数,该参数指定了可执行模块的名称,既可以是一个dll文件,也可以是一个exe文件。如果调用成功,LoadLibrary函数将返回所加载的那个模块的句柄。返回类型HMODULEHINSTANCE可以通用(32位windows系统)。

         当加载到动态链接库模块的句柄后,接下来就要想办法获取该动态链接库中导出函数的地址,这可以通过调用GetProcAddress函数来实现。该函数用来获取DLL导出函数的地址,其原型声明如下所示:

FARPROC GetProcAddress(HMODULE hModule, LPCSTR lpProcName);

参数hModule:指定动态链接库模块的句柄,即LoadLibrary函数的返回值。

参数lpProcName:一个指向常量的字符指针,指定DLL导出函数的名字或函数的序号。如果是序号,则序号必须在低位字节中,高位字节必须是0

如果调用成功,GetProcAddress函数将返回指定导出函数的地址;否则返回NULL

 

例如:

HINSTANCE hInst;

hInst = LoadLibrary(“DllTest.dll”);

typedef int (*ADDPROC)(int a, int b);

ADDPROC add = (ADDPROC)GetProcAddress(hInst, “add”);

if (!add)

print(“Failure”);

else

process next events

FreeLibrary(hInst);

调用语法:

BOOL FreeLibrary(HMODULE hModule);

5.      加载DLL的两种方式优缺点:

       采用动态加载方式,那么可以在需要时才加载DLL,而隐式链接方式实现起来比较简单,在编写客户端代码时就可以把链接工作做好,在程序中可以随时调用DLL导出的函数。但是如果程序需要访问十多个DLL时,如果都采用隐式链接方式加载它们的话,那么在该程序启动时,这些DLL都需要被加载到内存中,并映射到调用进程的地址空间,这样将加大程序的启动时间。而且一般来说,在程序运行过程中只是在某个条件满足时才需要访问某个DLL中的某个函数,其它情况下都不需要访问这些DLL中的函数。但是这时所有的DLL都已经被加载到内存中,资源浪费是比较严重的。这个时候就需要采用显示加载的方式来访问DLL,在需要时才加载所需的DLL。也就是说在需要时才被加载到内存中,并被映射到调用进程的地址控件中。需要说明的是,隐式链接方式访问DLL时,在程序启动时也是通过LoadLibrary函数加载该进程需要的动态链接库的。

6.      DllMain函数

         如果提供了DllMain函数(该函数是可以选择存在的),那么在此函数中不要进行太复杂的调用。因为在加载该动态链接库时,可能还有一些核心动态链接库没有被加载。例如Use32.dllGDI32.dll。我们自己编写的DLL会比较靠前地被加载。

 


阅读全文(753) | 评论:1 | 复制链接

内存空间操作(2008-10-10 17:53:00)

一个new肯定只需要写一个delete:
int (*arr)[10] = new int[20][10];
//use it...
delete []arr;
或者:
int **arr2 = new int*[20];
for (int i = 0; i < 20; ++i) arr2[i] = new int[10];
//use it...
for (int i = 0; i < 20; ++i) delete arr2[i];
delete arr2;


阅读全文(455) | 评论:0 | 复制链接

函数可以独立使用的特例(2008-10-1 20:14:00)

如下代码:
const static LPCTSTR g_szIPCCustomMsg =
    _T("{34F673E2-878F-11D5-B98A-00B0D07B8C7C}");
const static UINT g_wmScanPassword =    RegisterWindowMessage(g_szIPCCustomMsg);
函数在函数体外定义是合法的。

解释: 静态对象和全局对象的初始化是在main函数执行之前进行的. 初始化(包括构造, 析构)对象的时候可以调用函数, 并且也就这一种情况可以在主函数外部调用函数.
MFC里面不是有个全局变量theApp吗, 也是这样子.
另外,C语言只有规定过不能在函数体内定义函数,没有规定在函数体外不能调用函数,既然没有限制就是允许的!


阅读全文(381) | 评论:0 | 复制链接

介绍Windows的窗口、消息、子类化和超类化(2008-9-21 14:15:00)



眼见为实(2):介绍Windows的窗口、消息、子类化和超类化

介绍Windows的窗口、消息、子类化和超类化

这篇文章本来只是想介绍一下子类化和超类化这两个比较“生僻”的名词。为了叙述的完整性而讨论了Windows的窗口和消息,也简要讨论了进程和线程。子类化(Subclassing)和超类化(Superclassing)是伴随Windows窗口机制而产生的两个复用代码的方法。不要把“子类化、超类化”与面向对象语言中的派生类、基类混淆起来。“子类化、超类化”中的“类”是指Windows的窗口类。

0 运行程序

希望读者在阅读本节前先看看"谈谈Windows程序中的字符编码"开头的第0节和附录0。第0节介绍了Windows系统的几个重要模块。附录0概述了Windows的启动过程,从上电到启动Explorer.exe。本节介绍的是运行程序时发生的事情。

0.1 程序的启动

当我们通过Explorer.exe运行一个程序时,Explorer.exe会调用CreateProcess函数请求系统为这个程序创建进程。当然,其它程序也可以调用CreateProcess函数创建进程。

系统在为进程分配内部资源,建立独立的地址空间后,会为进程创建一个主线程。我们可以把进程看作单位,把线程看作员工。进程拥有资源,但真正在 CPU上运行和调度的是线程。系统以挂起状态创建主线程,即主线程创建好,不会立即运行,而是等待系统调度。系统向Win32子系统的管理员 csrss.exe登记新创建的进程和线程。登记结束后,系统通知挂起的主线程可以运行,新程序才开始运行。

这时,在创建进程中CreateProcess函数返回;在被创建进程中,主线程在完成最后的初始化后进入程序的入口函数(Entry-point)。创建进程与被创建进程在各自的地址空间独立运行。这时,即使我们结束创建进程,也不会影响被创建进程。

0.2 程序的执行

可执行文件(PE文件)的文件头结构包含入口函数的地址。入口函数一般是Windows在运行时库中提供的,我们在编译时可以根据程序类型设定。在VC中编译、运行程序的小知识点讨论了Entry-point,读者可以参考。

入口函数前的过程可以被看作程序的装载过程。在装载时,系统已经做过全局和静态变量(在编译时可以确定地址)的初始化,有初值的全局变量拥有了它们的初值,没有初值的变量被设为0,我们可以在入口函数处设置断点确认这一点。

进入入口函数后,程序继续运行环境的建立,例如调用所有全局对象的构造函数。在一切就绪后,程序调用我们提供的主函数。主函数名是入口函数决定的,例如main或WinMain。如果我们没有提供入口函数要求的主函数,编译时就会产生链接错误。

0.3 进程和线程

我们通常把存储介质(例如硬盘)上的可执行文件称作程序。程序被装载、运行后就成为进程。系统会为每个进程创建一个主线程,主线程通过入口函数进入我们提供的主函数。我们可以在程序中创建其它线程。

线程可以创建一个或多个窗口,也可以不创建窗口。系统会为有窗口的线程建立消息队列。有消息队列的线程就可以接收消息,例如我们可以用PostThreadMessage函数向线程发送消息。

没有窗口的线程只要调用了PeekMessage或GetMessage,系统也会为它创建消息队列。

1 窗口和消息

1.1 线程的消息队列

每个运行的程序就是一个进程。每个进程有一个或多个线程。有的线程没有窗口,有的线程有一个或多个窗口。

我们可以向线程发送消息,但大多数消息都是发给窗口的。发给窗口的消息同样放在线程的消息队列中。我们可以把线程的消息队列看作信箱,把窗口看作收信人。我们在向指定窗口发送消息时,系统会找到该窗口所属的线程,然后把消息放到该线程的消息队列中。

线程消息队列是系统内部的数据结构,我们在程序中看不到这个结构。但我们可以通过Windows的API向消息队列发送、投递消息;从消息队列接收消息;转换和分派接收到的消息。

1.2 最小的Windows程序

Windows的程序员大概都看过这么一个最小的Windows程序

// 例程1
#include "windows.h"

static const char m_szName[] = "窗口";

//////////////////////////////////////////////////////////////////////////////////////////////////// 
// 主窗口回调函数 如果直接用 DefWindowProc, 关闭窗口时不会结束消息循环
static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{ 
	switch (uMsg) {
	case WM_DESTROY:
		PostQuitMessage(0);	// 关闭窗口时发送WM_QUIT消息结束消息循环
		break;

	default:
		return DefWindowProc(hWnd, uMsg, wParam, lParam);
	} 
	return 0;
}

//////////////////////////////////////////////////////////////////////////////////////////////////// 
// 主函数 
int __stdcall WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine, int nCmdShow)
{
	WNDCLASS wc; 
	memset(&wc, 0, sizeof(WNDCLASS));
	wc.style = CS_VREDRAW|CS_HREDRAW;
	wc.lpfnWndProc = (WNDPROC)WindowProc;
	wc.hCursor = LoadCursor(NULL, IDC_ARROW);
	wc.hbrBackground = (HBRUSH)(COLOR_WINDOW);
	wc.lpszClassName = m_szName;
	RegisterClass(&wc);		// 登记窗口类

	HWND hWnd;
	hWnd = CreateWindow(m_szName,m_szName,WS_OVERLAPPEDWINDOW,100,100,320,240,
		NULL,NULL,hInstance,NULL);	// 创建窗口
	ShowWindow(hWnd, nCmdShow);		// 显示窗口

	MSG sMsg; 
	while (int ret=GetMessage(&sMsg, NULL, 0, 0)) {		// 消息循环
		if (ret != -1) {
			TranslateMessage(&sMsg);
			DispatchMessage(&sMsg);
		}
	}
	return 0;
}

这个程序虽然只显示一个窗口,但经常被用来说明Windows程序的基本结构。在MFC框架内部我们同样可以找到类似的程序结构。这个程序包含以下基本概念:

  • 窗口类、窗口和窗口过程
  • 消息循环

下面分别介绍。

1.3 窗口类、窗口和窗口过程

创建窗口时要提供窗口类的名字。窗口类相当于窗口的模板,我们可以基于同一个窗口类创建多个窗口。我们可以使用Windows预先登记好的窗口类。但在更多的情况下,我们要登记自己的窗口类。在登记窗口类时,我们要登记名称、风格、图标、光标、菜单等项,其中最重要的就是窗口过程的地址。

窗口过程是一个函数。窗口收到的所有消息都会被送到这个函数处理。那么,发到线程消息队列的消息是怎么被送到窗口的呢?

1.4 消息循环

熟悉嵌入式多任务程序的程序员,都知道任务(相当于Windows的线程)的结构基本上都是:

	while (1) {
		等待信号;
		处理信号;
	}

任务收到信号就处理,否则就挂起,让其它任务运行。这就是消息驱动程序的基本结构。Windows程序通常也是这样:

	while (int ret=GetMessage(&sMsg, NULL, 0, 0)) {		// 消息循环
		if (ret != -1) {
			TranslateMessage(&sMsg);
			DispatchMessage(&sMsg);
		}
	}

GetMessage从消息队列接收消息;TranslateMessage根据按键产生WM_CHAR消息,放入消息队列;DispatchMessage根据消息中的窗口句柄将消息分发到窗口,即调用窗口过程函数处理消息。

1.5 通过消息通信

创建窗口的函数会返回一个窗口句柄。窗口句柄在系统范围内(不是进程范围)标识一个唯一的窗口实例。通过向窗口发送消息,我们可以实现进程内和进程间的通信。

我们可以用SendMessage或PostMessage向窗口发送或投递消息。SendMessage必须等到目标窗口处理过消息才会返回。我试过:如果向一个没有消息循环的窗口SendMessage,SendMessage函数永远不会返回。PostMessage在把消息放入线程的消息队列后立即返回。

其实只有投递的消息才是通过DispatchMessage分派到窗口过程的。通过SendMessage发送的消息,在线程GetMessage时,就已经被分派到窗口过程了,不经过DispatchMessage。

1.5.1 窗口程序与控制台程序的通信实例

大家是不是觉得“例程1”没什么意思,让我们用它来做个小游戏:让“例程1”和一个控制台程序做一次亲密接触。我们首先将“例程1”的窗口过程修改为:

static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static DWORD tid = 0;
	
	switch (uMsg) {
	case WM_DESTROY:
		PostQuitMessage(0);	// 关闭窗口时发送WM_QUIT消息结束消息循环
		break;

	case WM_USER:
		tid = wParam;	// 保存控制台程序的线程ID
		SetWindowText(hWnd, "收到");
		break;

	case WM_CHAR:
		if (tid) {
			switch(wParam) {
			case '1':
				PostThreadMessage(tid, WM_USER+1, 0, 0);	// 向控制台程序发送消息1
				break;
			case '2':
				PostThreadMessage(tid, WM_USER+2, 0, 0);	// 向控制台程序发送消息2
				break;
			}
		}
		break;

	default:
		return DefWindowProc(hWnd, uMsg, wParam, lParam);
	} 
	return 0;
}

然后,我们创建一个控制台程序,代码如下:

#include "windows.h"
#include "stdio.h"

static HWND m_hWnd = 0;
void process_msg(UINT msg, WPARAM wp, LPARAM lp)
{
	char buf[100];
	static int i = 1;

	if (!m_hWnd) {
		return;
	}

	switch (msg) {
	case WM_USER+1:
		SendMessage(m_hWnd, WM_GETTEXT, sizeof(buf), (LPARAM)buf);
		printf("你现在叫:%s\n\n", buf);		// 读取、显示对方的名字
		break;

	case WM_USER+2:
		sprintf(buf, "我是窗口%d", i++);
		SendMessage(m_hWnd, WM_SETTEXT, sizeof(buf), (LPARAM)buf);	// 修改对方名字
		printf("给你改名\n\n");
		break;
	}
}

int main()
{
	MSG sMsg;

	printf("Start with thread id %d\n", GetCurrentThreadId());
	m_hWnd = FindWindow(NULL,"窗口");
	if (m_hWnd) {
		printf("找到窗口%x\n\n", m_hWnd);
		SendMessage(m_hWnd, WM_USER, GetCurrentThreadId(), 0);
	}
	else {
		printf("没有找到窗口\n\n");
	}

	while (int ret=GetMessage(&sMsg, NULL, 0, 0)) {		// 消息循环
		if (ret != -1) {
			process_msg(sMsg.message, sMsg.wParam, sMsg.lParam);
		}
	}
	return 0;
}

大家能看懂这游戏怎么玩吗?首先运行“例程1”wnd,然后运行控制台程序msg。msg会找到wnd的窗口,并将自己的主线程ID发给wnd。 wnd收到msg的消息后,会显示收到。这时,wnd和msg已经建立了通信的渠道:wnd可以向msg的主线程发消息,msg可以向wnd的窗口发消息。

我们如果在wnd窗口按下键'1',wnd会向msg发送消息1,msg收到后会通过WM_GETTEXT消息获得wnd的窗口名称并显示。我们如果在wnd窗口按下键'2',wnd会向msg发送消息2,msg收到后会通过WM_SETTEXT消息修改wnd的窗口名称。

这个小例子演示了控制台程序的消息循环,向线程发消息,以及进程间的消息通信。

1.5.2 地址空间的问题

不同的进程拥有独立的地址空间,如果我们在消息参数中包含一个进程A的地址,然后发送到进程B。进程B如果在自己的地址空间里操作这个地址,就会发生错误。那么,为什么上例中的WM_GETTEXT和WM_SETEXT可以正常工作?

这是因为WM_GETTEXT和WM_SETEXT都是Windows自己定义的消息,Windows知道参数的含义,并作了特殊的处理,即在进程 B的空间分配一块内存作为中转,并在进程A和进程B的缓冲区之间复制数据。例如:在1.5.1节的例子中,如果我们设置断点观察,就会发现msg发送的 WM_SETTEXT消息中的lParam不等于wnd接收到的WM_SETTEXT消息中的lParam。

如果我们在自己定义的消息中传递内存地址,系统不会做任何特殊处理,所以必然发生错误。

Windows提供了WM_COPYDATA消息用来向窗口传递数据,Windows同样会为这个消息作特殊处理。

在进程间发送这些需要额外分配内存的消息时,我们应该用SendMessage,而不是PostMessage。因为SendMessage会等待接收方处理完后再返回,这样系统才有机会额外释放分配的内存。在这种场合使用PostMessage,系统会忽略要求投递的消息,读者可以在msg程序中试验一下。

2 子类化和超类化

窗口类是窗口的模板,窗口是窗口类的实例。窗口类和每个窗口实例都有自己的内部数据结构。Windows虽然没有公开这些数据结构,但提供了读写这些数据的API。

例如:用GetClassLong和SetClassLong函数可以读写窗口类的数据;用GetWindowLong和 SetWindowLong可以读写指定窗口实例的数据。使用这些接口,可以在运行时读取或修改窗口类或窗口实例的窗口过程地址。这些接口是子类化的实现基础。

2.1 子类化

子类化的目的是在不修改现有代码的前提下,扩展现有窗口的功能。它的思路很简单,就是将窗口过程地址修改为一个新函数地址,新的窗口过程函数处理自己感兴趣的消息,将其它消息传递给原窗口过程。通过子类化,我们不需要现有窗口的源代码,就可以定制窗口功能。

子类化可以分为实例子类化和全局子类化。实例子类化就是修改窗口实例的窗口过程地址,全局子类化就是修改窗口类的窗口过程地址。实例子类化只影响被修改的窗口。全局子类化会影响在修改之后,按照该窗口类创建的所有窗口。显然,全局子类化不会影响修改前已经创建的窗口。

子类化方法虽然是二十年前的概念,却很好地实践了面向对象技术的开闭原则(OCP:The Open-Closed Principle):对扩展开放,对修改关闭。

2.2 超类化

超类化的概念更简单,就是读取现有窗口类的数据,保存窗口过程函数地址。对窗口类数据作必要的修改,设置新窗口过程,再换一个名称后登记一个新窗口类。新窗口类的窗口过程函数还是仅处理自己感兴趣的消息,而将其它消息传递给原窗口过程函数处理。使用GetClassInfo函数可以读取现有窗口类的数据。

3 MFC中的消息循环和子类化

MFC将子类化方法应用得淋漓尽致,是一个不错的例子。候捷先生的《深入浅出MFC》已经将MFC的主要框架分析得很透彻了,本节只是看看MFC的消息循环,简单分析MFC对子类化的应用。

3.1 消息循环

随便建立一个MFC单文档程序,在视图类中添加WM_RBUTTONDOWN的处理函数,并在该处理函数中设置断点。运行,断下后,查看调用堆栈:

CHelloView::OnRButtonDown(unsigned int, CPoint)
CWnd::OnWndMsg(unsigned int, unsigned int, long, long *)
CWnd::WindowProc(unsigned int, unsigned int, long)
AfxCallWndProc(CWnd *, HWND__ *, unsigned int, unsigned int, long)
AfxWndProc(HWND__ *, unsigned int, unsigned int, long)
AfxWndProcBase(HWND__ *, unsigned int, unsigned int, long)
USER32! 7e418734()
USER32! 7e418816()
USER32! 7e4189cd()
USER32! 7e4196c7()
CWinThread::PumpMessage()
CWinThread::Run()
CWinApp::Run()
AfxWinMain(HINSTANCE__ *, HINSTANCE__ *, char *, int)
WinMain(HINSTANCE__ *, HINSTANCE__ *, char *, int)
WinMainCRTStartup()
KERNEL32! 7c816fd7()

WinMainCRTStartup是这个程序的入口函数。候捷先生已经详细介绍过AfxWinMain。我们就看看CWinThread::PumpMessage中的消息循环:

BOOL CWinThread::PumpMessage()
{
	if (!::GetMessage(&m_msgCur, NULL, NULL, NULL)) {
		return FALSE;
	}

	if (m_msgCur.message != WM_KICKIDLE && !PreTranslateMessage(&m_msgCur))
	{
		::TranslateMessage(&m_msgCur);
		::DispatchMessage(&m_msgCur);
	}
	return TRUE;
}

这就是MFC程序主线程中的消息循环,它把发送到线程消息队列的消息分派到线程的窗口。

3.2 子类化

CWnd::CreateEx在创建窗口前调用SetWindowsHookEx函数安装了一个钩子函数_AfxCbtFilterHook。窗口刚创建好,钩子函数_AfxCbtFilterHook就被调用。_AfxCbtFilterHook调用SetWindowLong将窗口过程替换为 AfxWndProcBase,并将SetWindowLong返回的原窗口地址保存到成员变量oldWndProc。上节调用堆栈中的 AfxWndProcBase就是由此而来。

可见,通过CWnd::CreateEx创建的所有窗口都会被子类化,即它们的窗口过程都会被替换为AfxWndProcBase。MFC为什么要这样做?

让我们再看看调用堆栈中的CWnd::WindowProc函数:

LRESULT CWnd::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
	LRESULT lResult = 0;
	if (!OnWndMsg(message, wParam, lParam, &lResult))
		lResult = DefWindowProc(message, wParam, lParam);
	return lResult;
}

按照侯捷先生的介绍,CWnd::OnWndMsg就是“MFC消息泵”的入口,消息通过这个入口流入MFC消息映射中的消息处理函数。消息泵只会处理我们定制过的消息,我们没有添加过处理的消息会原封不动地流过"消息泵",进入DefWindowProc函数。在DefWindowProc函数中,消息会传给子类化时保存的原窗口地址oldWndProc。

CWnd::CreateEx里的钩子会子类化所有窗口吗?其实不尽然。的确,MFC所有窗口相关的类都是从CWnd派生的,这些类的实例在创建窗口时都会调用CWnd::CreateEx,都会被子类化。但是,通过对话框模板创建的窗口是通过CreateDlgIndirect创建的,不经过 CWnd::CreateEx函数。

但这点其实也不是问题,因为如果我们想通过MFC定制一个控件的消息映射,就必须先子类化这个控件,MFC还是有机会将窗口过程替换成自己的AfxWndProcBase。下一节将介绍对话框控件的子类化。

4 子类化和超类化的例子

我写了一个很简单的对话框程序,用来演示子类化和超类化。这个对话框程序有两个编辑框,我将编辑框的右键菜单换成了一个消息框。两个编辑框的定制分别采用了子类化和超类化技术:

4.1 子类化的例子

首先从CEdit派生出CMyEdit1,定制WM_RBUTTONDOWN的处理。很多文章都建议我们在对话框的OnInitDialog中用SubclassDlgItem实现子类化:

	m_edit1.SubclassDlgItem(IDC_EDIT1, this);

这样做当然可以。其实如果我们已经为IDC_EDIT1添加过CMyEdit1对象:

void CSubclassingDlg::DoDataExchange(CDataExchange* pDX)
{
	CDialog::DoDataExchange(pDX);
	//{{AFX_DATA_MAP(CSubclassingDlg)
	DDX_Control(pDX, IDC_EDIT1, m_edit1);
	//}}AFX_DATA_MAP
}

DDX_Control会自动帮我们完成子类化,没有必要手工调用SubclassDlgItem。大家可以通过在PreSubclassWindow中设置断点看看。

通过DDX_Control或者SubclassDlgItem子类化控件的效果是一样的,MFC都是把窗口过程替换成AfxWndProcBase。用户添加过处理函数的消息通过MFC消息泵流入用户的处理函数。

4.2 必经之路:PreSubclassWindow

PreSubclassWindow是一个很好的定制控件的位置。如果我们通过重载CWnd::PreCreateWindow定制控件,而用户在对话框中使用控件。由于对话框中的控件窗口是通过CreateDlgIndirect创建,不经过CWnd::CreateEx函数,PreCreateWindow函数不会被调用。

其实,用户要在对话框中使用定制控件,必须用DDX或者SubclassDlgItem函数子类化控件,这时PreSubclassWindow一定会被调用。

如果用户直接创建定制控件窗口,CWnd::CreateEx函数就一定会被调用,控件窗口一定会被子类化以安装MFC消息泵。所以在MFC中,PreSubclassWindow是创建窗口的必经之路。

4.3 超类化的例子

我很少看到超类化的例子(除了罗云彬的Win32汇编),在大多数应用中,子类化技术已经足够了。但我还是写了一个例子:CMyEdit2从 CEdit派生。CMyEdit2::RegisterMe获取窗口类Edit的信息,保存原窗口过程,设置新窗口过程MyWndProc和新名称 MyEdit,登记一个新窗口类。新窗口过程MyWndProc定制自己需要处理的消息,将其它消息送回原窗口过程。

我在对话框的OnInitDialog中先调用CMyEdit2::RegisterMe登记新窗口类,然后创建窗口。这样创建窗口必须经过 CWnd::CreateEx,所以MFC还是会把窗口过程换成AfxWndProcBase。没有被MFC消息映射拦截的消息才会流入 MyWndProc。

5 结束语

这篇文章介绍了一些Windows和MFC的基础知识。写这篇文章的目的不是介绍什么编程技巧,而是让我们更了解程序运行时发生的事情。惟有深入其中,方能跳出其外,不受羁绊。



阅读全文(704) | 评论:0 | 复制链接

强制CPU开始新的周期的方法(2008-9-18 1:02:00)

#include <afx.h>
#include <stdio.h>

void GetCylc( unsigned int *lowPart, unsigned int *highPart )
{
    __asm {
        cpuid         // 强制CPU开始新的周期
        mov ecx, lowPart;
        mov ebx, highPart;
        rdtsc
        mov [ecx], eax;
        mov [ebx], edx;
    }
}

unsigned int lowPart1, lowPart2, highPart;

int main()
{
    char *s1 = "2o3i4jiojrewjeroiksopf";

    GetCylc( &lowPart1, &highPart );

    for(int i1=0;s1[i1]!=0;i1++)
        printf("%c",s1[i1]);
    printf("\n");

    CString s2 = "2o3i4jiojrewjeroiksopf";
    for(int i2=0;i2<s2.GetLength();i2++)
        printf("%c",s2[i2]); // s2.GetAt(i2)
    printf("\n");
    GetCylc( &lowPart2, &highPart );
    printf("finish time = lowPart2 - lowPart1 = %d\n",lowPart2 - lowPart1);
    getchar();
    return 0;
}



阅读全文(433) | 评论:0 | 复制链接

模式对话框(2008-9-17 19:10:00)

模式对话框
作者:冯明德

一、概述

对话框是一种特殊的窗口,它依据对话框模板资源而建立。
它与一般的窗口有些不同,很多过程由系统完成了,虽然用户还是要提供一个消息处理函数,但在此消息处理函数中,不需要将不关心的消息交由缺省消息处理函数。
实际上,调用缺省处理的过程又系统完成。

二、对话框消息处理函数

对话框也需要用户提供一个消息处理函数,但这个处理函数没有普通窗口的消息处理函数"权利大"。
对话框是一种系统定义的“窗口类”,它已经定义好了对应的消息处理函数。客户所作的消息处理函数,并不是直接与窗口连接,而是对对话框消息处理函数的一种补充,或者说“嵌入”。
因此,对话框处理函数不需要调用“缺省消息处理函数”。
当有消息被处理时,返回TRUE,没有消息需要处理时,返回FALSE,此时退出用户消息处理函数后,系统会去调缺省消息处理函数。

//对话框消息处理函数
//返回值类型为BOOL,与普通窗口处理函数不同。
BOOL CALLBACK AboutDlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{

switch (message)
{
case WM_INITDIALOG :
return TRUE ; //返回真,表示消息被处理了。

case WM_COMMAND :
switch (LOWORD (wParam))
{
case IDOK :
case IDCANCEL :
EndDialog (hDlg, 0) ; //使用EndDialog关闭对话框
return TRUE ; //返回真,表示消息被处理了。
}
break ;
}
return FALSE ; ////返回假,表示消息未被用户处理,又缺省消息处理函数去处理。
}
三、模式对话框建立

使用DialogBox。
INT_PTR DialogBox(
HINSTANCE hInstance, // handle to module
LPCTSTR lpTemplate, // dialog box template
HWND hWndParent, // handle to owner window
DLGPROC lpDialogFunc // dialog box procedure
);
例:
case WM_COMMAND:
switch(LOWORD(wParam))
{
case ID_ABOUT:
DialogBox (hinst, MAKEINTRESOURCE(IDD_ABOUT), hwnd, AboutDlgProc) ;
break;
}
return 0;
四、模式对话框与程序的交互

模式对话框中,可以对程序中的数据进行更改。
结束对话框时,在EndDialog第二个参数中传入退出参数
这个参数将被DialogBox作为返回值,然后对话框的用户根据此返回值作相应的操作。

1.初始化
对话框消息处理函数中,在接到WM_INITDIALOG消息时,作一些初始化工作。
如从全局变量读取初始值来设置各控件状态。

2.退出时
若退出时,更改需要生效,(如按了“确定”),则根据控件状态设置全局变量,并相应的在EndDialg中使用一个表示成功的值(如TRUE)。
若更改不需要生效(如按了“取消”),则不保存结果,并相应的在EndDialg中使用一个表示取消的值(如FALSE)。

3.对话框用户作出反应
根据DialogBox的返回值不同,而进行不同的操作
如,返回TRUE时,重绘窗口:
if (DialogBox (hInstance, TEXT ("AboutBox"), hwnd, AboutDlgProc))
InvalidateRect (hwnd, NULL, TRUE) ;


阅读全文(436) | 评论:0 | 复制链接