Conner's profile☆ Conner Wang ☆PhotosBlogListsMore Tools Help

Blog


    June 09

    Headers and Includes: Why and How

    一篇很好的介绍C++头文件如何组织和引用的文章:

    Headers and Includes: Why and How [By Disch]

    February 15

    GDB中查看STL容器类的内容

        使用GDB的"p variable-name"查看STL容器类,只会显示该容器的一些信息,并不能很友好的显示该容器的内容。使用stl-views.gdb这个脚本可以很好地解决这一问题。下载stl-veiws.gdb文件,将其放到~/目录下,直接将其改名为~/.gdbinit,或者在你已有的.gdbinit文件中用source ~/.stl-views.gdb命令将其包含。这样你就可以用下面的命令显示STL容器类了。

     容器类型 GDB 命令
    std::vector<T> pvector stl_variable
    std::list<T> plist stl_variable T
    std::map<T,T> pmap stl_variable
    std::multimap<T,T> pmap stl_variable
    std::set<T> pset stl_variable T
    std::multiset<T> pset stl_variable
    std::deque<T> pdequeue stl_variable
    std::stack<T> pstack stl_variable
    std::queue<T> pqueue stl_variable
    std::priority_queue<T> ppqueue stl_variable
    std::bitset<n><td> pbitset stl_variable
    std::string pstring stl_variable
    std::widestring pwstring stl_variable

    举例:

    如果你的C++代码中有定义: set<string> s;
    则在GDB中可以使用如下命令查看该set的信息与内容:
    pset s - 打印该集合s的定义和大小
    pset s char* - 打印该集合s的大小以及该集合的所有元素

    May 13

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

    摘自:《面向对象分析设计与编程(OOA/OOD/OOP/AOP)》  第二版  吴炜煜 编著  清华大学出版社

        在这里,我们讨论类的两种特殊成员两数——构造函数和析构函数在虚函数机制中的使用。构造函数为对象分配存储空间,使一个对象初始化,而析构函数在该对象生命期完结时做相应的扫尾工作并释放出构造函数分配的内存。那么,这两种函数是否可以声明为虚函数呢?
        构造函数不能是虚函数。这一点可以从两个方而来解释。从概念上来说,虚函数机制只有在应用于地址时才有效,因为地址在编译阶段提供的类型信息不完全。构造函数的功能是为一个对象在内行中分配空间,也就是说,此时该对象的类型已经确定了,编译系统确切的知道应该调用哪—个类的构造函数。不需要也不可能应用动态绑定。从实现上来说,每个对象的VPTR需要构造函数来创始化(当然是由编译系统自动加进去的代码来实现)。在构造函数没有调用之前VPTR没有形成,根本就不可能实现动态绑定。
        那么,当构造函数内部有虚函数时,又会出现什么情况呢?结果是,只有在该类中的虚函数版本被调用。也就是说,此时虚函数机制不起作用了,调用虚函数如同调用—般的成员函数一样。
        构造函数不能是虚函数,那么析构函数呢?我们说,析构函数可以是虚函数,而且应该被声明为虚函数。与一般成员函数相似,析构函数被调用时,对象的构造已经完成,VPTR和VTABLE也已被正确初始化,因此虚析构函数在实现上是可能的。从设计角度来看,析构函数的任务是释放内存,因此它必须确切知道被释放的对象的类型,否则可能破坏有用的数据,产生不可预知的后果。例如,我们用基类指针指向了派生类对象,那么释放内作时,必须是释放派生类对象的存储空间。所以,析构函数经常被声明为虚函数。由于效率上的原因,并不把析构函数缺省为虚函数。但作为一条实践经验、可以给有虚函数的每个基类声明虚析构函数。
        同样的问题是,当析构函数内部有虚函数时,会如何工作呢?与构造函数相同,只有“局部”的版本被调用。但是,行为相同,原因是不一样的。构造函数只能调用“局部”版本,是因为调用时还没有派生类版本的信息。析构函数则是因为派生类版本的信息已经不可靠了。我们知道,析构函数的调用顺序与构造函数相反,是从派生类的析构函数到基类的析构函数。当某个类的析构函数被调用时,其下一级的析构函数已经被调用了,相应的数据也已被丢失,如果再调用虚函数的最后一级的版本,就相当于对一些不可靠的数据进行操作,这是非常危险的。因此,在析构函数中,虚函数机制也是不起作用的。

        另外说明一点,析构函数不能为纯虚函数,但你可以为其定义一个什么都不做的实现(里面是空语句)。

    April 13

    为二维数组动态分配连续内存

        在Robert Sedgewick所著的Algorithm In C中,把二维数组当成数组的数组为它动态分配内存。

        首先,分配一个指针的数组,然后为每一行分配内存。
    函数定义:
        void **malloc2d(int r, int c, int size)
        {
            int i;
            int **t = malloc(r * sizeof(void*));
            for(i=0; i<r; i++)
                t[i] = malloc(c * size);
            return t;
        }

        void free2d(void **arr, int r)
        {
            int i = 0;
            for(i=0; i<r; i++)

                free(arr[i]);
            free(arr);
        }

        这种方法好处是可以用a[i][j]的方式来访问数组元素,但是它需要分配额外内存来存储行指针。

        另外,这种方法分配的内存是不连续的,而且在分配和释放二维数组时,要多次调用malloc和free操作,增加了系统开销。而使用以下两种方法为二维数组分配的内存是连续的,可以避免因频繁分配和释放内存而带来的内存开销。

    方法一:

        用一维数组来实现二维数组的功能,在内存中二维数组还是线性排列的。对于想申请a[m][n]数组可以如下实现:
        int   *ptr_array;
        ptr_array   =   (int*) malloc(m*n*sizeof(int));
    当需要访问a[i][j]时,   用如下方式访问:  
      *(ptr_array   +   i   *   n   +   j);
    当需要释放空间时,用如下方式访问:
        free ptr_array;

        这种方式实际上是把二维数组用一维数组表示,  再模拟成二维数组使用.

    方法二:

    void **malloc2d(int row, int col, int size)
    {
        void **arr;
        arr = (void **) malloc(sizeof(void *) * row + size * row * col);
        if (arr != NULL)
        {
            void *head;
            head = (void *) arr + sizeof(void *) * row;
            memset(arr, 0, sizeof(void *) * row + size * row * col);
            while (row--)
                arr[row] = head + size * row * col;
        }
        return arr;
    }

    void free2d(void **arr)
    {
        if (arr != NULL)
        free(arr);
    }

        这种方法的好处和不足之处同Robert Sedgewick的方法一样,可以用a[i][j]的方式来访问,但仍然需要额外分配内存来存储行指针。但它分配的空间是连续的,可以避免因频繁分配和释放内存而带来的内存开销,这一点又可与方法一媲美。

    最值得推荐的方法是方法二。

    April 05

    看完了C语言参考手册第一部分

        C A Referenc Manual  (Fifth Edition)

        By Samuel P. Harbison III, Guy L. Steele Jr.

        这又是一本工具书级的书,该书在C语言书籍中的地位相当于Herbert SchildtThe C++: Complete Reference在C++书籍中的地位,都是经典中的经典。

        这本书的影印版从上学期就买来了,一直只是当工具书来用,没有时间系统地学习。前一段时间又开始用C写程序,写完之后就有了一股想好好复习一次C语言的冲动。于是又拿出了这本书,抽空就看看。平均每天看二三十页,零零散散看了一周多,终于把该书的第一部分看完了。

        全书分两大部分,第一部分(308页以前)讨论了C语言的所有语言特征,包括词法、预处理机制、声明、类型、表达式、语句及函数等基本语言特性。第二部分讨论了C语言的标准库。

        该书与纯粹的语言手册不同,除了分门别类并且详细地介绍语言的基本特性以外,还加入了一些说明性的实例,提供一些背景介绍或解释。该书对细节的描述毫不含糊,好些平时编程时经常拿不准的东西,都解释的很彻底很详细。读本书的时候经常有一种既之其然又知其所以然的豁然开朗的感觉。

        本书的另外一个特点就是将K&R C、ANSI C、和C99放在同一个框架里,互相对照着一起介绍。并且每一章的最后一部分都归纳了该章内容与C++标准的兼容性问题。

        该书无论作为一本参考书还是一本系统学习C语言的中级教材都非常值得推荐。

        准备下一步把第二部分(既C语言标准库)也看完。

        另外人民邮电出版的这本影印版书籍纸张还是相当不错的,又厚又白,就是字有点太小,阅读时间长了会有点累。

    February 28

    C语言中的宽字符与多字节字符[整理]

        C语言原本是在英文环境中设计的,主要的字符集是7位的ASCII码,8位的byte(字节)是最常见的字符编码单位。但是国际化软件必须能够表示不同的字符,而这些字符数量庞大,无法使用一个字节编码。
        C95标准化了两种表示大型字符集的方法:宽字符(wide character,该字符集内每个字符使用相同的位长)以及多字节字符(multibyte character,每个字符可以是一到多个字节不等,而某个字节序列的字符值由字符串或流(stream)所在的环境背景决定)。
        自从1994年的增补之后,C语言不只提供char类型,还提供wchar_t类型(宽字符),此类型定义在stddef.h 头文件中。wchar_t指定的宽字节类型足以表示某个实现版本扩展字符集的任何元素。
        在多字节字符集中,每个字符的编码宽度都不等,可以是一个字节,也可以是多个字节。源代码字符集和运行字符集都可能包含多字节字符。多字节字符可以被用于字符的常量、字符串字面值(string literal)、标识符(identifier)、注释(comment),以及头文件。
        C语言本身并没有定义或指定任何编码集合,或任何字符集(基本源代码字符集和基本运行字符集除外),而是由其实现指定如何编码宽字符,以及要支持什么类型的多字节字符编码机制。
        虽然C标准没有支持Unicode字符集,但是许多实现版本使用Unicode转换格式UTF-16和UTF-32(参考http://www.unicode.org)来处理宽字符。如果遵循Unicode标准,wchar_t类型至少是16或32位长,而wchar_t类型的一个值就代表一个Unicode字符。
        UTF-8是一个由Unicode Consortium(万国码联盟)定义的实现,可以表示Unicode字符集的所有字符。UTF-8字符所使用的空间大小从一个字节到四个字节都有可能。
        多字节字符和宽字符(也就是wchar_t)的主要差异在于宽字符占用的字节数目都一样,而多字节字符的字节数目不等,这样的表示方式使得多字节字符串比宽字符串更难处理。比方说,即使字符'A'可以用一个字节来表示,但是要在多字节的字符串中找到此字符,就不能使用简单的字节比对,因为即使在某个位置找到相符合的字节,此字节也不见得是一个字符,它可能是另一个不同字符的一部分。然而,多字节字符相当适合用来将文字存储成文件。
        C提供了一些标准函数,可以将多字节字符转换为wchar_t,或将宽字符转换为多字节字符。比方说,如果C 编译器使用Unicode 标准的UTF-16 和UTF-8,那么下面调用wctomb()函数就可以获得字符的多字节表示方式(注:wctomb = wide character to multibyte)。

    参考:
    [1] C in a Nutshell, Peter Prinz, Tony Crawford
    [2] A Reference Manual, 5ed, Samuel P. Harbison III, Guy L. Steele Jr.

    重新用C

        最近想写个算法并评估一下运行效率,打算用标准C来实现。

        其实从程序设计的角度来看,还是用C++更方便一些。但是C++提供的各种高级功能,如封装、继承、多态等面向对象技术,隐藏了许多底层的细节,不利于对于算法本身的优劣做评估。偶以为用C语言来评估算法是最简单和直接的。

        但我发现好久没用纯C写程序了,还真有些生疏了。从去年做项目开始,就一直用C++写程序,现在又用C#写程序。它们的抽像级别越来越高,IDE环境做得也越来越友好,编译出错输出的信息也越来越清晰。今天用C语言写了一个小程序,在VS2005下编译竟然出错,提示 ,我看着这么几行的小程序就傻了眼,怎么也找不出哪个地方错了。无耐之下我把编译器改为C++编译模式,发现一切正常,连个警告都没有。后来我仔细想了想才知道错在哪里,原来是有个变量定义的位置不对。C语言强制变量在函数或语句块的一开始就定义,而C++F却允许随用随定义,所以才出现C编译器编译出错而C++编译器却能编译通过的情况。

        重新审视C语言,拿出买来已久的 C A Reference Manual 翻了翻,发现C++和C还是有不少不一致的地方的。看来我又得好好地用用C语言了。以后写C程序尽量能同时符合C和C++标准,既所谓的Clean C程序。

    November 09

    C/C++语言的I/O系统(下):C++语言的I/O系统

    [读书笔记]<The C++: Complete Reference>第四版 Herbert Schildt著 周志荣 朱德芳 于秀山 等译

        和基于C的I/O一样,C++的I/O系统是一个完全集成的系统,该系统的不同方面(例如,控制台I/O和磁盘I/O)实际上只是从不同角度看待同一种机制。

    一、C++输入/输出系统基础

    1.C++的流
        像基于C的I/O系统一样,C++I/O系统通过流进行操作。我们可以把流归纳如下:一个流是一种既可以产生信息又可以消耗信息的逻辑设备,它通过I/O系统与一个物理设备相连。尽管流所产生连接的设备可以完全不同,但是所有的流以同样的方式运作。因为所有流的运作方式相同,所以实际上可以利用同样的I/O函数操作所有类型的物理设备。例如,可以利用同样的函数把信息写到文件或写到打印机和屏幕。采用这种方法的优点是只需要掌握一种I/O系统。
    2.C++的流类
        标准C++在头文件<iostream>中提供了对I/O系统的支持,在这个头文件中定义了一套相当复杂的层次结构以支持I/O操作。标准C++I/O类以一个模板类开始,创建了两种I/O模板类的说明:一个针对8位字符,另一个针对宽字符。
        C++I/O系统建立在两 个既相关又不同的模板类层次结构之上。第一个类层次结构是从低级I/O类派生而来,称为basic_streambuf。这个类提供基本的输入/输出操作,而且还对可个C++I/O系统提供底层支持。除非你正在进行高级的I/O编程,否则不必直接使用basic_streambuf。最常使用的类层次是从basic_ios派生而来的,这是一个高级I/O类,可以提供格式化、错误检查和与I/O相关的状态信息(一个basic_iso的基类称为ios_base,它定义了几个为basic_ios所用的非模板特性)。basic_ios用作几个派生类的基类,这些派生类包括basic_istream,basic_ostream和basic_iostream,利用这些类可以分别创建输入流、输出流和输入/输出流。要说明的一点是:ios类包含许多成员函数和变量,它们可以监控基本的流操作,而且将被频繁地引用。
    3.C++的预定义流
        一个C++程序开始执行时将自动打开四个内置流,这些内置流是:

    含义 默认设备
    cin 标准输入 键盘
    cout 标准输出 屏幕
    cerr 标准错误输出 屏幕
    clog cerr的缓冲版本 屏幕

        cin,cout和cerr流与C的stdin,stdout和stderr相对应。
        默认情况下,标准流用来与控制台通信。然而,在支持I/O重定向的环境中(例如,DOS,Unix,OS/2和Windows),标准流可以被重定向到其他设备或文件。标准C++还定义了四个附加的流:win,wout,werr和wlog,它们都是宽字符版本的标准流。宽字符的类型是wchar_t,一般为16位。宽字符用于存放与一些自然语言有关的大型字符集。
    4.格式化的I/O
        C++I/O系统允许你对I/O操作进行格式化。例如,可以设置域宽度、指定数字基数以及决定显示小数点后面的数字位数。有两种相互关联但概念上不同的格式化数据的方式。第一种方式可以直接访问ios类的成员,确切的说,可以设置ios类内定义的各种格式化状态标记或调用各种ios成员函数;第二种方式可以使用称之为操纵算子(manipulator)的特殊函数,该函数是I/O表达式的一部分。
    5.插入器和析取器
        <<和>>运算符在C++中被重载,从而可以对C++的内置类型执行I/O操作。你还可以对这些运算符重载,以使其对你所创建的类型执行I/O操作。
        在C++中,因为<<可以把字符插入到一个流中,所以该输出运算符被称为插入运算符。同样,>>输入运算符被称为析取运算符,因为该运算符可以从 一个流中提取字符。重载插入和析取运算符的函数通常被称为插入器(inserter)和析取器(extractor)。
        通常将插入器<<和析取器>>定义为类的友员函数,此举既可以满足将插入或析取器的第一个参数作为流的一个要求,又可以保证该函数可访问重载它的类的私有成员。

    二、C++文件的输入/输出

         尽管C++的I/O形成了一个完整的系统,但是文件I/O却十分特殊,以至于被普遍看做是一种特例,它们有自己的约束和特点。之所以会这样,部分是因为最常用的文件是磁盘文件,而磁盘文件有一些其他设备所有具备的性能和功能。然而要记住磁盘文件的I/O只是一般I/O系统的一个特例。
        为了执行文件的I/O,必须在程序中包括头文件<fstream>。该头文件定义了一些类,其中包括ifstream,ofstream和fstream,这些类分别从istream,ostream和iostream派生而来。记住,因为istream,ostream和iostream是从ios派生而来的,所以ifstream,ofstream和fstream同样可以访问ios定义的所有操作。文件系统使用的另一个类是filebuf,这个类提供用于管理文件流的低级功能程序,通常情况下,我们不直接使用filebuf,而是将其作为其他文件类的一部分。

    C/C++语言的I/O系统(上):C语言的I/O系统

    [读书笔记]<The C++: Complete Reference>第四版 Herbert Schildt著 周志荣 朱德芳 于秀山 等译

        C++支持两种I/O系统。第一种是从C中继承来的,第二种是由C++定义的面向对象的I/O系统。本文上篇讨论C语言的I/O子系统,下篇将讨论C++的I/O系统。
        在C语言中,输入和输出是通过库函数来完成的。既有控制台函数,也有文件I/O函数。从技术上讲,控制台I/O和文件I/O之间只有很小的差别。然而,从概念上讲,它们是两个截然不同的范畴。标准CI/O函数都是使用头文件stdio.h。C++程序也可以使用C++风格的头文件<cstdio>。

    一、C风格的控制台I/O
        控制台I/O函数从键盘输入并输出到屏幕上。然而,这些函数实际上让系统的标准输入和标准输出作为它们的I/O操作的目的和源。此外,标准输入和标准输出也可重定向到其他设备。
    1.最简单的控制台I/O函数是getchar()和putchar()
        其原型如下:
      int getchar(void);
      int putchar(int c);
    2.gets()和puts()是控制台I/O中更复杂、更高效率的函数,它们使你能够读写字符串。
        其原型如下:
      char *gets(char *str);
      int puts(const char *str);
    3.格式化的控制台I/O
        printf()和scanf()是两个实现格式化输入输出的函数。它们在用户的控制下以不同的格式读写数据。两者都可以操作包括字符、字符串和数字在内的任何内嵌数据类型。其原型如下:
      int prinf(const char *control_string,...);
      int scanf(const char *control_string,...);

    二、C风格的文件I/O
        在开始讨论C语言的文件系统之前,了解流和文件这两个概念之间的差别是很重要的。C的I/O系统给程序员提供了一个独立于设备进行信息访问的友好界面。即,C的I/O系统在程序员和实际设备之间提供了一级抽象,这个抽象称为流,而物理设备称为文件。
    1.流
        C文件系统可在包括终端、磁盘驱动器和磁带驱动器等众多设备上工作。不管设备有多大差异,文件系统都把它们转换成“流”的逻辑设备,且所有流的行为是类似的。因为流具有极大的设备无关性,所以向一个磁盘文件进行写操作的同一个函数也可以完成向另一种设备(如控制台)的写操作。有两种类型的流:文本流和二进制流。
        文本流由一系列字符组成,标准C允许(但不要求)一个文本被组织成多行,以换行符终止一行。然而,在最后一行上换行符是可选的(实际上,大多数C/C++编译器都不需要使用换行符终止文本流)。在文本中,特定字符的转换是由主机环境要求的,例如,新行可以被转换成回车/换行符对。因而,被写(或读)的字符与存储在外设中的字符之间并无一一对应的关系。同样,由于可能的转换,被写(或读)的字符的数量可能与外设中存储的字符不一致。
        二进制流是指一系列字节,这一系列字节与外设中存储的字节有一对一的头关系,也就是说,不存在字符转换。此外,写(或读)字节的数量与外设中存储的字节数相同。然而,根据实现决定的空字节数可以追加到一个二进制流中。例如,这些空字节有时用来补充信息以填满磁盘的某个扇区。
    2.文件
        在C/C++中文件可以是从磁盘文件到终端或打印机的任何东西。流通过执行打开操作与某个文件联系起来。文件一旦打开,里面的信息就可能在程序与该文件之间交换。
        并非所有文件都有相同的功能。例如,磁盘文件支持随机访问,但某些打印机却不行。这就说明了C语言中I/O系统的一个重要特征:所有的流都是相同的,但文件却不同。
        如果文件支持定位请求,那么打开文件也使文件位置指针初始化到文件的开始处。当字符从某个文件中读出或写到某个文件中时,文件位置指针加1,从而确保可以遍历整个文件。
        文件通过关闭操作与特定流断开连接。当关闭一个为输出而打开的文件时,与它相关的流的内容(如果存在的话)被写到外设中。这一过程通常称为“清空”流,它保证了在磁盘缓冲区中不留下任何信息。当程序正常终止时所有文件都自动关闭,其关闭方式是要么通过返回到操作系统的main(),要么通过调用exit()。如果程序员因故障崩溃或调用abort()而终止,则文件不关闭。
        与文件相关的流都有一个类型为FILE的文件控制结构。永远都不要对它进行任何修改操作。
    3.标准流
        虽然与C文件系统有关,程序一旦开始执行,就有三种流被自动打开。它们是标准输入(stdin)、标准输出(stdout)及标准错误(stderr)。通常,它们对应于控制台、但也可以通过操作系统重定向到支持可以重定向I/O环境(如Windows,Unix,OS/2和Dos)的其它设备中。
        通常,stdin用于从控制台读,stdout用于从控制台写,stderr也用于从控制台写。可以在使用FILE*类型变量的任何函数中,把stdin,stdout和stderr当作文件指针使用。
        例如,putchar()可以定义如下:
    int putchar(char c)
    {
        return putc(c, stdout);
    }
        再例如,可以使用fgets()从控制台输入一个字符口串,按如下方式调用:
    char str[255];
    fgets(str, 80, stdin);
        事实上,按这种方式使用fgets()是相当有用的。因为,当使用gets()时,可能越出数组的边界(现在正使用该数组来接收由用户键入的字符),因为gets()为提供边界检查。当和stdin一起使用时,fgets()提供了一个有用的替代物,因为它能限制所读的字符数,因而防止数组越界。惟一的麻烦是fgets()没有删除换行符而gets()删除了,所有你不得不手工删除它,如下面的程序所示:
    #include <stdio.h>
    #include <string.h>
    int main(void)
    {
        char str[80];
        int i;

        printf("Enter a string: ");
        fgets(str, 10, stdin);

        /*remove newline, if present*/
        i = strlen(str)-1;
        if(str[i] == '\n') str[i] = '\0';

        printf("This is your string: s", str);

        return 0;
    }
        记住,stdin,stdout和stderr不是普通变量,也不能通过fopen()进行赋值。还有,像这些文件指针在程序执行时自动创建一样,它们在结束时自动关闭,用户无法随意关闭它们。
    4.控制台I/O连接
        实际上,控制台I/O和文件I/O之间的区别很小。控制台I/O函数通过stdin或stdout完成I/O操作。实际上,控制台I/O函数只是一些并行文件函数的特例,它们的存在只是为了方便程序员。
        正如上面3中所描述的那样,可以利用任何文件系统函数来完成控制台I/O。然而,最令人惊奇的是还可以使用控制台I/O函数来操作文件,如prinf()来完成磁盘文件I/O。这是因为所有的控制台I/O函数都操作于stdin和stdout。在允许I/O重定向的环境中,这意味着stdin和stdout可对应于包括键盘和屏幕的其他设备。例如,考虑如下程序:
    #include <stdio.h>
    int main(void)
    {
        char str[80];

        printf("Enter a string: ");
        gets(str);
        printf(str);

        return 0;
    }

        假定这个程序称为TEST,如果正常执行TEST,则在屏幕上显示提示,从键盘中读一个串,然后将该串显示在屏幕上。然而,在支持I/O重定向的环境中,stdin,stdout或两者都能被重定向到一个文件中。例如,在DOS或Windows环境中,执行TEST如下:
      TEST > OUTPUT
    于是TEST的输出结果写到了OUTPUT文件中。若执行如下:
      TEST < INPUT > OUTPUT
    stdin被导向INPUT文件中,并发送输出到OUTPUT文件中。
        当程序终止时,任何重定向流都复位到它们的默认状态。
    5.使用freopen()来重定向标准流
        使用freopen()可以重定向标准流。这个函数用于将某一现存流与一个新文件联系起来。因此,可以用它将一个标准流与一个新文件联系起来。它的原型是:
      FILE *freopen(const char *filename, const char *mode, FILE *stream);
        其中,filename是一个指向你希望与stream指向的流相关联的文件名的指针。该文件以mode值方式打开,与fopen()相同。如果成功则返回流,否则,返回NULL指针。
        下面的程序使用freopen()来重定向stdout到OUTPUT文件。
    #include <stdio.h>
    int mai(void)
    {
        char str[80];

        freopen("OUTPU", "w", stdout);

        printf("Enter a string: ");
        gets(str);
        printf(str);

        return 0;
    }
        通常,用freopen()重定向标准流在一些特殊情况下(例如调试)是很有用的。然而,通过重定向stdin和stdout来完成磁盘I/O不一定与用fread()和fwrite()函数一样有效。

    November 19

    VC中OnPaint()的工作原理[转载]

        用了两年的VC,其实对OnPaint的工作原理一直都是一知半解。这两天心血来潮,到BBS上到处发帖询问,总算搞清楚了,现在总结一下。

        对于窗口程序,一般有个特点:窗口大部分的区域保持不变,只有不分区域需要重新绘制。如果将整个窗口全部刷新的画,就做了许多不必要的工作,因而,MFC采用了一套基于无效区的处理机制。在分析无效区处理之前,我们要明白一个现实,现在的机器还不够牛,如果够牛的话,我们干脆将整个窗口不断的重新绘制好了。事实上即使够牛也不行,对于一个单线程程序,通过一个while循环不断的刷新窗口,程序也无法相应其他消息(除非使用多线程),看来使用无效区的处理机制还是有其必然性的。

        VC程序是基于消息机制的,你所做的任何操作,比如点击鼠标,拖动窗口,首先进入系统的消息队列。这里的系统消息队列包括多个程序的消息,系统再将消息发送给相应的程序。既然是队列,这就有一个先进先出的问题,屏幕上的无效区更新消息出现的频率就会特别高。比如当左上角更新的消息还没有处理,右下角更新的消息已经过来了。为了避免多次处理WM_PAINT消息,系统就将这些窗口更新消息合并到一条,只是将无效区范围变成包括这两次更新无效区范围在内的矩形区域。这样就减少了WM_PAINT消息的处理次数,提高了效率。

        那么,在OnPaint消息处理函数中,又是怎样实现更新无效区的呢?首先,要明白MFC中所有绘图操作都是基于设备描述表(Device Context,简称DC)的,具体信息可参看任何一本VC教材。DC中包含了绘图设备的各种信息,对于屏幕绘图,其实就是有一块内存(显存),专门用来存放要显示到屏幕上的信息,显示器以85HZ的频率(我以前的显示器)将其内容刷新的屏幕上。这里就到了关键点,显示器的刷新是将显存中的内容完全更新到显示器上,不存在无效区处理的问题,那么,无效区的处理一定发生在DC的绘图处理上。事实确实如此,当程序调用OnPaint消息时,首先将无效区范围传递给DC,DC在进行绘图操作时,就只更新无效区范围内的信息,其他地方的不管,这就提高了效率。

        现在你明白OnPaint的处理是怎么一回事了吧?这里还想说一下Invalidate和UpdateWindow的区别。Invalidate在消息队列中加入一条WM_PAINT消息,其无效区为整个客户区。而UpdateWindow直接发送一个WM_PAINT消息,其无效区范围就是消息队列中WM_PAINT消息(最多只有一条)的无效区。效果很明显,调用Invalidate之后,屏幕不一定马上更新,因为WM_PAINT消息不一定在队列头部,而调用UpdateWindow会使WM_PAINT消息马上执行的,绕过了消息队列。如果你调用Invalidate之后想马上更新屏幕,那就加上UpdateWindow()这条语句。

    November 18

    C++内存管理详解[转载]

    伟大的Bill Gates 曾经失言:   640K ought to be enough for everybody — Bill Gates 1981   程序员们经常编写内存管理程序,往往提心吊胆。如果不想触雷,唯一的解决办法就是发现所有潜伏的地雷并且排除它们,躲是躲不了的。本文的内容比一般教科书的要深入得多,读者需细心阅读,做到真正地通晓内存管理。

    伟大的Bill Gates 曾经失言:

      640K ought to be enough for everybody — Bill Gates 1981

      程序员们经常编写内存管理程序,往往提心吊胆。如果不想触雷,唯一的解决办法就是发现所有潜伏的地雷并且排除它们,躲是躲不了的。本文的内容比一般教科书的要深入得多,读者需细心阅读,做到真正地通晓内存管理。

      1、内存分配方式

      内存分配方式有三种:

      (1)从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。

      (2)在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

      (3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。

      2、常见的内存错误及其对策

      发生内存错误是件非常麻烦的事情。编译器不能自动发现这些错误,通常是在程序运行时才能捕捉到。而这些错误大多没有明显的症状,时隐时现,增加了改错的难度。有时用户怒气冲冲地把你找来,程序却没有发生任何问题,你一走,错误又发作了。 常见的内存错误及其对策如下:

      * 内存分配未成功,却使用了它。

      编程新手常犯这种错误,因为他们没有意识到内存分配会不成功。常用解决办法是,在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行

      检查。如果是用malloc或new来申请内存,应该用if(p==NULL) 或if(p!=NULL)进行防错处理。

      * 内存分配虽然成功,但是尚未初始化就引用它。

      犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。 内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,我们宁可信其无不可信其有。所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。

      * 内存分配成功并且已经初始化,但操作越过了内存的边界。

      例如在使用数组时经常发生下标“多1”或者“少1”的操作。特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。

      * 忘记了释放内存,造成内存泄露。

      含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。

      动态内存的申请与释放必须配对,程序中malloc与free的使用次数一定要相同,否则肯定有错误(new/delete同理)。

      * 释放了内存却继续使用它。
     
      有三种情况:

      (1)程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。

      (2)函数的return语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。

      (3)使用free或delete释放了内存后,没有将指针设置为NULL。导致产生“野指针”。

      【规则1】用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。

      【规则2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。

      【规则3】避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。

      【规则4】动态内存的申请与释放必须配对,防止内存泄漏。

      【规则5】用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。

      3、指针与数组的对比

      C++/C程序中,指针和数组在不少地方可以相互替换着用,让人产生一种错觉,以为两者是等价的。

      数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。

      指针可以随时指向任意类型的内存块,它的特征是“可变”,所以我们常用指针来操作动态内存。指针远比数组灵活,但也更危险。

      下面以字符串为例比较指针与数组的特性。

      3.1 修改内容

      示例3-1中,字符数组a的容量是6个字符,其内容为hello。a的内容可以改变,如a[0]= ‘X’。指针p指向常量字符串“world”(位于静态存储区,内容为world),常量字符串的内容是不可以被修改的。从语法上看,编译器并不觉得语句p[0]= ‘X’有什么不妥,但是该语句企图修改常量字符串的内容而导致运行错误。

    char a[] = “hello”;
    a[0] = ‘X’;
    cout << a << endl;
    char *p = “world”; // 注意p指向常量字符串
    p[0] = ‘X’; // 编译器不能发现该错误
    cout << p << endl;
          示例3.1 修改数组和指针的内容

      3.2 内容复制与比较

      不能对数组名进行直接复制与比较。示例7-3-2中,若想把数组a的内容复制给数组b,不能用语句 b = a ,否则将产生编译错误。应该用标准库函数strcpy进行复制。同理,比较b和a的内容是否相同,不能用if(b==a) 来判断,应该用标准库函数strcmp进行比较。

      语句p = a 并不能把a的内容复制指针p,而是把a的地址赋给了p。要想复制a的内容,可以先用库函数malloc为p申请一块容量为strlen(a)+1个字符的内存,再用strcpy进行字符串复制。同理,语句if(p==a) 比较的不是内容而是地址,应该用库函数strcmp来比较。

    // 数组…
    char a[] = "hello";
    char b[10];
    strcpy(b, a); // 不能用 b = a;
    if(strcmp(b, a) == 0) // 不能用 if (b == a)

    // 指针…
    int len = strlen(a);
    char *p = (char *)malloc(sizeof(char)*(len+1));
    strcpy(p,a); // 不要用 p = a;
    if(strcmp(p, a) == 0) // 不要用 if (p == a)
           示例3.2 数组和指针的内容复制与比较

      3.3 计算内存容量

      用运算符sizeof可以计算出数组的容量(字节数)。示例7-3-3(a)中,sizeof(a)的值是12(注意别忘了’’)。指针p指向a,但是sizeof(p)的值却是4。这是因为sizeof(p)得到的是一个指针变量的字节数,相当于sizeof(char*),而不是p所指的内存容量。C++/C语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。

      注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。示例7-3-3(b)中,不论数组a的容量是多少,sizeof(a)始终等于sizeof(char *)。

    char a[] = "hello world";
    char *p = a;
    cout<< sizeof(a) << endl; // 12字节
    cout<< sizeof(p) << endl; // 4字节
         示例3.3(a) 计算数组和指针的内存容量

    void Func(char a[100])
    {
     cout<< sizeof(a) << endl; // 4字节而不是100字节
    }

         示例3.3(b) 数组退化为指针

      4、指针参数是如何传递内存的?

      如果函数的参数是一个指针,不要指望用该指针去申请动态内存。示例7-4-1中,Test函数的语句GetMemory(str, 200)并没有使str获得期望的内存,str依旧是NULL,为什么?

    void GetMemory(char *p, int num)
    {
     p = (char *)malloc(sizeof(char) * num);
    }
    void Test(void)
    {
     char *str = NULL;
     GetMemory(str, 100); // str 仍然为 NULL
     strcpy(str, "hello"); // 运行错误
    }
          示例4.1 试图用指针参数申请动态内存

      毛病出在函数GetMemory中。编译器总是要为函数的每个参数制作临时副本,指针参数p的副本是 _p,编译器使 _p = p。如果函数体内的程序修改了_p的内容,就导致参数p的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p申请了新的内存,只是把_p所指的内存地址改变了,但是p丝毫未变。所以函数GetMemory并不能输出任何东西。事实上,每执行一次GetMemory就会泄露一块内存,因为没有用free释放内存。

      如果非得要用指针参数去申请内存,那么应该改用“指向指针的指针”,见示例4.2。

    void GetMemory2(char **p, int num)
    {
     *p = (char *)malloc(sizeof(char) * num);
    }
    void Test2(void)
    {
     char *str = NULL;
     GetMemory2(&str, 100); // 注意参数是 &str,而不是str
     strcpy(str, "hello");
     cout<< str << endl;
     free(str);
    }
          示例4.2用指向指针的指针申请动态内存

      由于“指向指针的指针”这个概念不容易理解,我们可以用函数返回值来传递动态内存。这种方法更加简单,见示例4.3。

    char *GetMemory3(int num)
    {
     char *p = (char *)malloc(sizeof(char) * num);
     return p;
    }
    void Test3(void)
    {
     char *str = NULL;
     str = GetMemory3(100);
     strcpy(str, "hello");
     cout<< str << endl;
     free(str);
    }
           示例4.3 用函数返回值来传递动态内存

      用函数返回值来传递动态内存这种方法虽然好用,但是常常有人把return语句用错了。这里强调不要用return语句返回指向“栈内存”的指针,因为该内存在函数结束时自动消亡,见示例4.4。

    char *GetString(void)
    {
     char p[] = "hello world";
     return p; // 编译器将提出警告
    }
    void Test4(void)
    {
     char *str = NULL;
     str = GetString(); // str 的内容是垃圾
     cout<< str << endl;
    }
          示例4.4 return语句返回指向“栈内存”的指针

      用调试器逐步跟踪Test4,发现执行str = GetString语句后str不再是NULL指针,但是str的内容不是“hello world”而是垃圾。
    如果把示例4.4改写成示例4.5,会怎么样?

    char *GetString2(void)
    {
     char *p = "hello world";
     return p;
    }
    void Test5(void)
    {
     char *str = NULL;
     str = GetString2();
     cout<< str << endl;
    }
         示例4.5 return语句返回常量字符串

      函数Test5运行虽然不会出错,但是函数GetString2的设计概念却是错误的。因为GetString2内的“hello world”是常量字符串,位于静态存储区,它在程序生命期内恒定不变。无论什么时候调用GetString2,它返回的始终是同一个“只读”的内存块。

      5、杜绝“野指针”

      “野指针”不是NULL指针,是指向“垃圾”内存的指针。人们一般不会错用NULL指针,因为用if语句很容易判断。但是“野指针”是很危险的,if语句对它不起作用。 “野指针”的成因主要有两种:

      (1)指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。例如

    char *p = NULL;
    char *str = (char *) malloc(100);

      (2)指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针。

      (3)指针操作超越了变量的作用范围。这种情况让人防不胜防,示例程序如下:

    class A
    {
     public:
      void Func(void){ cout << “Func of class A” << endl; }
    };
    void Test(void)
    {
     A *p;
     {
      A a;
      p = &a; // 注意 a 的生命期
     }
     p->Func(); // p是“野指针”
    }


      函数Test在执行语句p->Func()时,对象a已经消失,而p是指向a的,所以p就成了“野指针”。但奇怪的是我运行这个程序时居然没有出错,这可能与编译器有关。

      6、有了malloc/free为什么还要new/delete?

      malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。

      对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。

       因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。我们先看一看malloc/free和new/delete如何实现对象的动态内存管理,见示例6。

    class Obj
    {
     public :
      Obj(void){ cout << “Initialization” << endl; }
      ~Obj(void){ cout << “Destroy” << endl; }
      void Initialize(void){ cout << “Initialization” << endl; }
      void Destroy(void){ cout << “Destroy” << endl; }
    };
    void UseMallocFree(void)
    {
     Obj *a = (obj *)malloc(sizeof(obj)); // 申请动态内存
     a->Initialize(); // 初始化
     //…
     a->Destroy(); // 清除工作
     free(a); // 释放内存
    }
    void UseNewDelete(void)
    {
     Obj *a = new Obj; // 申请动态内存并且初始化
     //…
     delete a; // 清除并且释放内存
    }
         示例6 用malloc/free和new/delete如何实现对象的动态内存管理

      类Obj的函数Initialize模拟了构造函数的功能,函数Destroy模拟了析构函数的功能。函数UseMallocFree中,由于malloc/free不能执行构造函数与析构函数,必须调用成员函数Initialize和Destroy来完成初始化与清除工作。函数UseNewDelete则简单得多。

      所以我们不要企图用malloc/free来完成动态对象的内存管理,应该用new/delete。由于内部数据类型的“对象”没有构造与析构的过程,对它们而言malloc/free和new/delete是等价的。

      既然new/delete的功能完全覆盖了malloc/free,为什么C++不把malloc/free淘汰出局呢?这是因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。

      如果用free释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如果用delete释放“malloc申请的动态内存”,理论上讲程序不会出错,但是该程序的可读性很差。所以new/delete必须配对使用,malloc/free也一样。

      7、内存耗尽怎么办?

      如果在申请动态内存时找不到足够大的内存块,malloc和new将返回NULL指针,宣告内存申请失败。通常有三种方式处理“内存耗尽”问题。

      (1)判断指针是否为NULL,如果是则马上用return语句终止本函数。例如:

    void Func(void)
    {
     A *a = new A;
     if(a == NULL)
     {
      return;
     }
     …
    }

      (2)判断指针是否为NULL,如果是则马上用exit(1)终止整个程序的运行。例如:

    void Func(void)
    {
     A *a = new A;
     if(a == NULL)
     {
      cout << “Memory Exhausted” << endl;
      exit(1);
     }
     …
    }

      (3)为new和malloc设置异常处理函数。例如Visual C++可以用_set_new_hander函数为new设置用户自己定义的异常处理函数,也可以让malloc享用与new相同的异常处理函数。详细内容请参考C++使用手册。

      上述(1)(2)方式使用最普遍。如果一个函数内有多处需要申请动态内存,那么方式(1)就显得力不从心(释放内存很麻烦),应该用方式(2)来处理。

      很多人不忍心用exit(1),问:“不编写出错处理程序,让操作系统自己解决行不行?”

      不行。如果发生“内存耗尽”这样的事情,一般说来应用程序已经无药可救。如果不用exit(1) 把坏程序杀死,它可能会害死操作系统。道理如同:如果不把歹徒击毙,歹徒在老死之前会犯下更多的罪。

      有一个很重要的现象要告诉大家。对于32位以上的应用程序而言,无论怎样使用malloc与new,几乎不可能导致“内存耗尽”。我在Windows 98下用Visual C++编写了测试程序,见示例7。这个程序会无休止地运行下去,根本不会终止。因为32位操作系统支持“虚存”,内存用完了,自动用硬盘空间顶替。我只听到硬盘嘎吱嘎吱地响,Window 98已经累得对键盘、鼠标毫无反应。

      我可以得出这么一个结论:对于32位以上的应用程序,“内存耗尽”错误处理程序毫无用处。这下可把Unix和Windows程序员们乐坏了:反正错误处理程序不起作用,我就不写了,省了很多麻烦。

      我不想误导读者,必须强调:不加错误处理将导致程序的质量很差,千万不可因小失大。

    void main(void)
    {
     float *p = NULL;
     while(TRUE)
     {
      p = new float[1000000];
      cout << “eat memory” << endl;
      if(p==NULL)
       exit(1);
     }
    }


      示例7试图耗尽操作系统的内存

      8、malloc/free 的使用要点

      函数malloc的原型如下:

    void * malloc(size_t size);

      用malloc申请一块长度为length的整数类型的内存,程序如下:

    int *p = (int *) malloc(sizeof(int) * length);

      我们应当把注意力集中在两个要素上:“类型转换”和“sizeof”。

      * malloc返回值的类型是void *,所以在调用malloc时要显式地进行类型转换,将void * 转换成所需要的指针类型。

      * malloc函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。我们通常记不住int, float等数据类型的变量的确切字节数。例如int变量在16位系统下是2个字节,在32位下是4个字节;而float变量在16位系统下是4个字节,在32位下也是4个字节。最好用以下程序作一次测试:

    cout << sizeof(char) << endl;
    cout << sizeof(int) << endl;
    cout << sizeof(unsigned int) << endl;
    cout << sizeof(long) << endl;
    cout << sizeof(unsigned long) << endl;
    cout << sizeof(float) << endl;
    cout << sizeof(double) << endl;
    cout << sizeof(void *) << endl;

      在malloc的“()”中使用sizeof运算符是良好的风格,但要当心有时我们会昏了头,写出 p = malloc(sizeof(p))这样的程序来。

      * 函数free的原型如下:

    void free( void * memblock );

      为什么free函数不象malloc函数那样复杂呢?这是因为指针p的类型以及它所指的内存的容量事先都是知道的,语句free(p)能正确地释放内存。如果p是NULL指针,那么free对p无论操作多少次都不会出问题。如果p不是NULL指针,那么free对p连续操作两次就会导致程序运行错误。

      9、new/delete 的使用要点

      运算符new使用起来要比函数malloc简单得多,例如:

    int *p1 = (int *)malloc(sizeof(int) * length);
    int *p2 = new int[length];

      这是因为new内置了sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言,new在创建动态对象的同时完成了初始化工作。如果对象有多个构造函数,那么new的语句也可以有多种形式。例如

    class Obj
    {
     public :
      Obj(void); // 无参数的构造函数
      Obj(int x); // 带一个参数的构造函数
      …
    }
    void Test(void)
    {
     Obj *a = new Obj;
     Obj *b = new Obj(1); // 初值为1
     …
     delete a;
     delete b;
    }

      如果用new创建对象数组,那么只能使用对象的无参数构造函数。例如

    Obj *objects = new Obj[100]; // 创建100个动态对象

      不能写成

    Obj *objects = new Obj[100](1);// 创建100个动态对象的同时赋初值1

      在用delete释放对象数组时,留意不要丢了符号‘[]’。例如

    delete []objects; // 正确的用法
    delete objects; // 错误的用法

      后者相当于delete objects[0],漏掉了另外99个对象。

      10、一些心得体会

      我认识不少技术不错的C++/C程序员,很少有人能拍拍胸脯说通晓指针与内存管理(包括我自己)。我最初学习C语言时特别怕指针,导致我开发第一个应用软件(约1万行C代码)时没有使用一个指针,全用数组来顶替指针,实在蠢笨得过分。躲避指针不是办法,后来我改写了这个软件,代码量缩小到原先的一半。

      我的经验教训是:

      (1)越是怕指针,就越要使用指针。不会正确使用指针,肯定算不上是合格的程序员。

      (2)必须养成“使用调试器逐步跟踪程序”的习惯,只有这样才能发现问题的本质。

    内存句柄与指针的区别[转载]

        操作系统: windows
        编程工具: visual c++6.0
        问题: 请专家详细介绍一下内存句柄与指针的区别。
        水平: 刚入门(star) 
       
         指针对应着一个数据在内存中的地址,得到了指针就可以自由地修改该数据。Windows并不希望一般程序修改其内部数据结构,因为这样太不安全。所以Windows给每个使用GlobalAlloc等函数声明的内存区域指定一个句柄(本质上仍是一个指针,但不要直接操作它),平时你只是在调用API函数时利用这个句柄来说明要操作哪段内存。当你需要对某个内存进行直接操作时,可以使用GlobalLock锁住这段内存并获得指针来直接进行操作。
        
        lshgao的意见:
        句柄是指针的“指针”,使用句柄主要是为了利于windows在进程内存地址空间移动分配的内存块,以防止进程的内存空间被撕的四分五裂而存在过多的碎片。
        
        阿城的意见:
        句柄是一些表的索引也就是指向指针的指针。间接的引用对象,windows可以修改对象的"物理"地址和
        描述器的值,但是句柄的值是不变的。
        
        刘志用的意见:
        句柄和指针都是地址,不同在于:
        1,句柄所指的可以是一个很复杂的结构,并且很有可以是与系统有关的,比如说上面所说的线程的句柄,它指向的就是一个很类或者结构,他和系统有很密切的关系,当一个线程由于不可预料的原因,而终止时在系统就可以回它所占用的资料,如CPU,内存等等,反过来想可以知道,这个句柄中的某一些项,是与系统进行交互的。由于Windows系统,是一个多任务的系统,它随时都可能要分配内存,回收内存,重组内存。
        2,指针它也可以指向一个复杂的结构,但是通常是用户定义的,所以的必需的工作都要用户完成,特别是在删除的时候。
        但在VC++6.0中也有一些指针,它们都是处理一些小问题才用的,如最常见的字符的指针,它也是要用户处理的如果你动态分配了内存;但是Cstring就不要用户处理了,它其实是VC++中的一个类,所以的操作都由成员函数完成,产生(分配)由构造函数,删除(回收)由析构函数完成。

        zjf问:
        你好,我在学习用vc++6.0编译多线程程序中遇到了很多句柄,但是不明白他的具体作用以及如何使用句柄,希望您能给我举几个具体实例,不甚感激!
        比如说: HANDLE hThread,它是怎样具体使用的?
        答:你使用CreateThead后函数会返回一个句柄,它代表这个线程。你可能会调用SetThreadPriority去修改线程的优先级,使用ResumeThread去重新开始一个线程的运行,在调用这些函数时你都需要告诉系统你到底要操作哪个线程,而刚才返回的句柄派上用处了,这些函数的第一个参数就是线程的句柄。 

    句柄和指针[整理]

    所谓句柄实际上是一个数据,是一个Long (整长型)的数据。

    句柄是WONDOWS用来标识被应用程序所建立或使用的对象的唯一整数,WINDOWS使用各种各样的句柄标识诸如应用程序实例,窗口,控制,位图,GDI对象等等。WINDOWS句柄有点象C语言中的文件句柄。

    从上面的定义中的我们可以看到,句柄是一个标识符,是拿来标识对象或者项目的,它就象我们的姓名一样,每个人都会有一个,不同的人的姓名不一样,但是,也可能有一个名字和你一样的人。从数据类型上来看它只是一个16位的无符号整数。应用程序几乎总是通过调用一个WINDOWS函数来获得一个句柄,之后其他的WINDOWS函数就可以使用该句柄,以引用相应的对象。

    如果想更透彻一点地认识句柄,句柄是一种指向指针的指针。我们知道,所谓指针是一种内存地址。应用程序启动后,组成这个程序的各对象是住留在内存的。如果简单地理解,似乎我们只要获知这个内存的首地址,那么就可以随时用这个地址访问对象。但是,如果您真的这样认为,那么您就大错特错了。我们知道,Windows是一个以虚拟内存为基础的操作系统。在这种系统环境下,Windows内存管理器经常在内存中来回移动对象,依此来满足各种应用程序的内存需要。对象被移动意味着它的地址变化了。如果地址总是如此变化,我们该到哪里去找该对象呢?

    为了解决这个问题,Windows操作系统为各应用程序腾出一些内存储地址,用来专门登记各应用对象在内存中的地址变化,而这个地址(存储单元的位置)本身是不变的。Windows内存管理器在移动对象在内存中的位置后,把对象新的地址告知这个句柄地址来保存。这样我们只需记住这个句柄地址就可以间接地知道对象具体在内存中的哪个位置。这个地址是在对象装载(Load)时由系统分配给的,当系统卸载时 (Unload)又释放给系统。


    句柄地址(稳定)→记载着对象在内存中的地址────→对象在内存中的地址(不稳定)→实际对象


    本质:WINDOWS程序中并不是用物理地址来标识一个内存块,文件,任务或动态装入模块的,相反的,WINDOWS API给这些项目分配确定的句柄,并将句柄返回给应用程序,然后通过句柄来进行操作。


    但是必须注意的是程序每次从新启动,系统不能保证分配给这个程序的句柄还是原来的那个句柄,而且绝大多数情况的确不一样的。假如我们把进入电影院看电影看成是一个应用程序的启动运行,那么系统给应用程序分配的句柄总是不一样,这和每次电影院售给我们的门票总是不同的一个座位是一样的道理。

    VC中窗口ID,句柄,指针三者相互转换函数

    ID--HANDLE--HWND三者之间的互相转换
    id->句柄、、、、、hWnd = ::GetDlgItem(hParentWnd,id);
    id->指针、、、、、CWnd::GetDlgItem();
    句柄->id、、、、、id = GetWindowLong(hWnd,GWL_ID);
    句柄->指针、、、、CWnd *pWnd=CWnd::FromHandle(hWnd);
    指针->ID、、、、、id = GetWindowLong(pWnd->GetSafeHwnd,GWL_ID);
    指针->句柄、、、、hWnd=cWnd.GetSafeHandle() or mywnd->m_hWnd;

    伪随机数生成及在VC++中的实现[转载]

    作者:王瑞胡
    出处:计算机与信息技术
      伪随机数在计算机软件设计中有很广泛的用途。本文介绍了基于数学方法的利用计算机产生伪随机数的一种方法,即线性同余法,任何伪随机数的产生都是运用递推的原理来生成的。

      摘 要 伪随机数在计算机软件设计中有很广泛的用途。本文介绍了基于数学方法的利用计算机产生伪随机数的一种方法,即线性同余法,任何伪随机数的产生都是运用递推的原理来生成的。以及在Visual C++环境中产生伪随机数的两个重要函数,rand和srand函数,正确地使用这两个函数是产生性能良好的伪随机数的关键,最后介绍了利用伪随机数生成技术在MFC中生成基于C/S模式应用程序的随机校验码以及利用一种软件工具ImagePassword产生随机密码。
      关键词 伪随机数生成;线性同余法;Visual C++;随机校验码

      为追求真正的随机序列,人们曾采用很多种原始的物理方法用于生成一定范围内满足精度(位数)的均匀分布序列,其缺点在于:速度慢、效率低、需占用大量存储空间且不可重现等。为满足计算机模拟研究的需求,人们转而研究用算法生成模拟各种概率分布的伪随机序列。伪随机数是指用数学递推公式所产生的随机数。从实用的角度看,获取这种数的最简单和最自然的方法是利用计算机语言的函数库提供的随机数发生器。典型情况下,它会输出一个均匀分布在0和1区间内的伪随机变量的值。其中应用的最为广泛、研究最彻底的一个算法即线性同余法。

      线性同余法LCG(Linear Congruence Generator)

      选取足够大的正整数M和任意自然数n0,a,b,由递推公式:

    ni+1=(af(ni)+b)mod M i=0,1,…,M-1

      生成的数值序列称为是同余序列。当函数f(n)为线性函数时,即得到线性同余序列:

    ni+1=(a*ni+b)mod M i=0,1,…,M-1

      以下是线性同余法生成伪随机数的伪代码:

    Random(n,m,seed,a,b)
    {
     r0 = seed
     for (i = 1;i<=n;i++)
     ri = (a*ri-1 + b) mod m
    }

      其中种子参数seed可以任意选择,常常将它设为计算机当前的日期或者时间;m是一个较大数,可以把它取为2w,w是计算机的字长;a可以是0.01w和0.99w之间的任何整数。

      应用递推公式产生均匀分布随机数时,式中参数n0,a,b,M的选取十分重要。

      例如,选取M=10,a=b =n0=7,生成的随机序列为{6,9,0,7,6,9,……},周期为4。

      取M=16,a=5,b =3,n0=7,生成的随机序列为{6,1,8,11,10,5,12,15,14,9,0,3,2,13,4,7,6,1……},周期为16。

      取M=8,a=5,b =1,n0=1,生成的随机序列为{6,7,4,5,2,3,0,1,6,7……},周期为8。

      Visual C++中伪随机数生成机制

      用VC产生随机数有两个函数,分别为rand(void)和srand(seed)。rand()产生的随机整数是在0~RAND_MAX之间平均分布的,RAND_MAX是一个常量(定义为:#define RAND_MAX 0x7fff)。它是short型数据的最大值,如果要产生一个浮点型的随机数,可以将rand()/1000.0,这样就得到一个0~32.767之间平均分布的随机浮点数。如果要使得范围大一点,那么可以通过产生几个随机数的线性组合来实现任意范围内的平均分布的随机数。

      其用法是先调用srand函数,如

    srand( (unsigned)time( NULL ) )

      这样可以使得每次产生的随机数序列不同。如果计算伪随机序列的初始数值(称为种子)相同,则计算出来的伪随机序列就是完全相同的。要解决这个问题,需要在每次产生随机序列前,先指定不同的种子,这样计算出来的随机序列就不会完全相同了。以time函数值(即当前时间)作为种子数,因为两次调用rand函数的时间通常是不同的,这样就可以保证随机性了。也可以使用srand函数来人为指定种子数。
    分析以下两个程序段,

      程序段1:

    //包含头文件
    void main() {
     int count=0;
     for (int i=0;i<10;i++){
      srand((unsigned)time(NULL));
      count++;
      cout<<"No"<<COUNT<<"="<<RAND()<<" />
      程序段2:

    //包含头文件
    void main() {
     int count=0;
     srand((unsigned)time(NULL));
     for (int i=0;i<10;i++){
      count++;
      cout<<"No"<<COUNT<<"="<<RAND()<<" />
      程序段1的运行结果为:

    No1=9694 No2=9694 No3=9694 No4=9694 No5=9694
    No6=9694 No7=9694 No8=9694 No9=9694 No10=9694

      程序段2的运行结果为:

    No1=10351 No2=444 No3=11351 No4=3074 No5=21497
    No6=30426 No7=6246 No8=24614 No9=22089 No10=21498

      可以发现,以上两个程序段由于随机数生成时选择的种子的不同,运行的结果也不一样。rand()函数返回随机数序列中的下一个数(实际上是一个伪随机数序列,序列中的每一个数是由对其前面的数字进行复杂变换得到的)。为了模仿真正的随机性,首先要调用srand()函数给序列设置一个种子。为了更好地满足随机性,使用了时间函数time(),以便取到一个随时间变化的值,使每次运行rand()函数时从srand()函数所得到的种子值不相同。伪随机数生成器将作为"种子"的数当作初始整数传给函数。这粒种子会使这个球(生成伪随机数)一直滚下去。

      程序段1中由于将srand()函数放在循环体内,而程序执行的CPU时间较快,调用time函数获取的时间精度却较低(55ms),这样循环体内每次产生随机数用到的种子数都是一样的,因此产生的随机数也是一样的。而程序段2中第1次产生的随机数要用到随机种子,以后的每次产生随机数都是利用递推关系得到的。
    September 21

    RGB/HSV色彩空间的相互转换[转载]

     

      在开发有关bitmap方面的程序时,经常需要将位图的颜色在RGB和HSV色彩空间之间转换,下面是该颜色转换的C++实现:

    RGB颜色空间转换为HSV空间颜色值:

    void Rgb2Hsv(float R, float G, float B, float& H, float& S, float&V)
    {
         // r,g,b values are from 0 to 1
        // h = [0,360], s = [0,1], v = [0,1]
        // if s == 0, then h = -1 (undefined)

        float min, max, delta,tmp;
        tmp = min(R, G);
        min = min( tmp, B );
        tmp = max( R, G);
        max = max(tmp, B );
        V = max; // v

        delta = max - min;

        if( max != 0 )
          S = delta / max; // s
        else
        {
           // r = g = b = 0 // s = 0, v is undefined
          S = 0;
          H = UNDEFINEDCOLOR;
          return;
        }
        if( R == max )
            H = ( G - B ) / delta; // between yellow & magenta
       else if( G == max )
            H = 2 + ( B - R ) / delta; // between cyan & yellow
       else
            H = 4 + ( R - G ) / delta; // between magenta & cyan

        H *= 60; // degrees
        if( H < 0 )
           H += 360;
    }

    HSV颜色空间转换为RGB空间颜色值:

    void Hsv2Rgb(float H, float S, float V, float &R, float &G, float &B)
    {
         int i;
        float f, p, q, t;

        if( S == 0 )
        {
        // achromatic (grey)
            R = G = B = V;
            return;
        }

        H /= 60; // sector 0 to 5
        i = floor( H );
        f = H - i; // factorial part of h
        p = V * ( 1 - S );
        q = V * ( 1 - S * f );
        t = V * ( 1 - S * ( 1 - f ) );

        switch( i )
        {
        case 0:
            R = V;
            G = t;
            B = p;
           break;
        case 1:
           R = q;
           G = V;
           B = p;
           break;
        case 2:
           R = p;
           G = V;
           B = t;
           break;
        case 3:
           R = p;
           G = q;
           B = V;
           break;
        case 4:
           R = t;
           G = p;
           B = V;
           break;
        default: // case 5:
           R = V;
           G = p;
           B = q;
           break;
        }
    }

    August 20

    C/C++命令行参数的原理

      为了实现命令行参数我们将使用main(int argc,char* argv[])这样的形式进行定义argc和argv可以换成你自己喜欢的名称不一定要用argv,argc这些形式只是习惯而已,char* argv[]我们前面已经讲述过,这就是一个指向指针数组,argv就是一个指针数组名,argv不是常量指针,而是具备变量特性的变量指针,它是可以移动的,由此我们可以改写成char* *argv也是正确的,int argc这个定义返回的将是参数的个数所以标记为整形(int)。
     
    #include <iostream>
    using namespace std;
    int main(int argc, char *argv[])
    {
        cout<<"你输入了"<<argc<< "个命令行参数!分别是:"<<endl;
        for(int i=0;i<argc;++i)
        {
                cout<<argv[i]<<endl;           
        }
        system("PAUSE");
        return EXIT_SUCCESS;
    }

    C/C++ 中的指针数组和指向指针的指针[转载]

      指向指针的指针,很早以前在说指针的时候说过,但后来发现很多人还是比较难以理解,这一次我们再次仔细说一说指向指针的指针。先看下面的代码,注意看代码中的注解:
    #include <iostream>
      #include <string>
      using namespace std;
    void print_char(char* array[],int len);
    //函数原形声明
    void main(void)
    {
    //---------------------
    --------段1----------
    char *a[]={"abc","cde","fgh"};
    //字符指针数组
    char* *b=a;
    //定义一个指向指针的指针,
    并赋予指针数组首地址所指向的第一个字符串的
    地址也就是abc\0字符串的首地址
    cout<<*b<<"|"<<*(b+1)
    <<"|"<<*(b+2)<<endl;
      //----------------
    --------------------------
      //----------
    -------------------段2----------
      char* test[]={"abc","cde","fgh"};
    //注意这里是引号,表示是字符串,
    以后的地址每加1就是加4位(在32位系统上)
      int num=sizeof(test)/sizeof(char*);
    //计算字符串个数
      print_char(test,num);
      cin.get();
      //-------------------
    -----------------------
      }
    void print_char(char* array[],
    int len)
    //当调用的时候传递进来的不是数组,
    而是字符指针他每加1也就是加上sizeof(char*)的长度
      {
      for(int i=0;i<len;i++)
      {
      cout<<*array++<<endl;
      }
      }
      下面我们来仔细说明一下字符指针数组和指向指针的指针,
    段1中的程序是下面的样子:
      char *a[]={"abc","cde","fgh"};
      char* *b=a;
      cout<<*b<<"|"<<*(b+1)<<"|"<<*(b+2)<<endl;
      char *a[]定义了一个指针数组,注意不是char[], char[]是不能同时初始化为三个字符的,
    定义以后的a[]其实内部有三个内存位置,
    分别存储了abc\0,cde\0,fgh\0,三个字符串的起始地址,
    而这三个位置的内存地址却不是这三个字符串的起始地址,
    在这个例子中a[]是存储在栈空间内的,
    而三个字符串却是存储在静态内存空间内的const区域中的,
    接下去我们看到了char* *b=a;
    这里是定义了一个指向指针的指针,
    如果你写成char *b=a;那么是错误的,
    因为编译器会返回一个无法将char* *[3]转换给char *的错误,b=a的赋值,实际上是把a的首地址赋给了b,
    由于b是一个指向指针的指针,
    程序的输出cout<<*b<<"|"<<*(b+1)<<"|"<<*(b+2)<<endl;
       结果是
    abc
    cde
    fgh

    可以看出每一次内存地址的+1操作事实上是一次加sizeof(char*)的操作,我们在32位的系统中sizeof(char*)的长度是4,所以每加1也就是+4,实际上是*a[]内部三个位置的+1,所以*(b+1)的结果自然就是cde了,我们这时候可能会问,为什么输出是cde而不是c一个呢?答案是这样的,在c++中,输出字符指针就是输出字符串,程序会自动在遇到\0后停止。

    我们最后分析一下段2中的代码,段2中我们调用了print_array()这个函数,这个函数中形式参数是char *array[]和代码中的char *test[]一样,同为字符指针,当你把参数传递过来的时候,事实上不是把数组内容传递过来,test的首地址传递了进来,由于array是指针,所以在内存中它在栈区,具有变量一样的性质,可以为左值,所以我们输出写成了:

    cout<<*array++<<endl;

    当然我们也可以改写为:

    cout<<array[i]<<endl

    这里在循环中的每次加1操作和段1代码总的道理是一样的,注意看下面的图!

    到这里这两个非常重要的知识点我们都说完了,说归说,要想透彻理解希望读者多动手,多观察,熟能生巧。

    August 19

    一维数组的指针访问

     
      下面的一维数组的访问方式都是正确的:
    int s[]={0,1,2,3,4,5};
    int* p=s;
    cout<<*p<<p[1]<<*(p+2)<<s[3]<<p[4]<<*(s+5);

    常量指针和指向常量的指针[整理]

     
      经常听到有关常量指针和指针常量的讨论,也经常见到有关两者区别的文章。然而,有些文章却误导了读者,他们的结论根本就是错的,关于指针常量和常量指针的讨论,结果完全颠倒了;而其他一些文章呢,充其量只是火上加油,让本来就很复杂的事情变得更加难于理解。
      其实归根结底,问题在于我们自己制造了一些名词,本意是为方便,结果却带来更大的麻烦。在《C++ Primer》第三版中文版中,常量指针出现过四次左右,而指针常量没有出现过,这也就是说,指针常量根本就不应该是一个学术术语,而是我们杜撰的。从英文的翻译来看,事实也是如此。关于指针和常量,有两种说法——const pointer和pointer to const,亦即常量指针和指向常量的指针。根本就没有指针常量的说法,而且pointer const在英语中也绝不可能出现。中文中出现指针常量的原因,估计一是因为常量指针(const pointer)的存在,二是因为方便,而前者的影响应该是最主要的。本意是为了让他们相似,便于比较,没想到却搬起石头砸了自己的脚,得不偿失。 
      关于常量指针和指向常量的指针,我想应该是很好区别的。常量指针,关键词在指针,常量为修饰语,那么这个词的意思自然是这是一个指针,而且这个指针(所指对象的地址)是一个常量,是不能修改的。而指向常量的指针,文如其义,自然就是指针所指的对象是一个常量,该对象的值不能修改。
     
      Bjarne在他的The C++ Programming Language里面给出过一个助记的方法:把一个声明从右向左读(这可能和我们平常习惯有所不同,需要特别注意)。
     char * const cp;     //( * 读成 pointer to )
     cp is a const pointer to char ,亦即指针常量,cp值不可改变,但*cp,也就是cp所指对象能够改变。
     const char * p;      //或者char const * p
     p is a pointer to const char,亦即指向常量的指针,所以p所指的对象不可改变。
      两者的区别还是容易理解的。
         
         PS:
    1.const char * const p或char const * const p则是上述两种含义的综合,估且叫做指向常量的常量指针
    2.所有的数组名都是指针而且是常量指针,不能对其赋值!