编译预处理

C语言编译中,什么时候应该使用32位编译程序?

32位编译程序应该在32位操作系统上使用。由32位编译程序生成的32位程序比16位程序运行得更快,这正是任何32位的东西都很热门的原因。
有那么多不同版本的Microsoft Windows,它们和哪种编译程序组成最佳搭配呢?

Windows 3.1和Windows for Workgroups 3.11是16位的操作系统;Microsoft Visual C++1.x是16位编译程序,它所生成的程序能在Windows 3.1上运行。Microsoft Windows NT和Windows 95是32位操作系统;Microsoft Visual C++2.o是32位编译程序,它所生成的32位程序能在这两种操作系统上运行。由Visual C++1.x生成的16位程序不仅能在Windows 3.1上运行,也能在Windows NT和Windows 95上运行。

然而,由Visual 2.O生成的32位程序无法在Windows 3.1上运行。这就给Microsoft提出了一个难题——它希望每个人都使用它的32位编译程序,但是有6千万台计算机所使用的Windows版本无法运行32位程序。为了克服这个障碍,Microsoft设计了一个叫做Win32s的转换库,用来完成由32到16位的转换,从而使由Visual C++生成的32位程序能在
Windows 3.1和Windows for Workgroups上运行。Win32s是专门为在Windows 3.1(和WFW)上运行而设计的,它不能用在Windows NT和Windows 95上,因为它们不需要为运行32位程序作什么转换。有了Win32s,你就可以用Visual C++2.0生成一个既能在Windows 3.1(和WFW)上运行,又能在Windows NT上运行的程序了。

最后还有一个问题,即编译程序本身的问题。Visual C++1.x是一个16位Windows程序,它既能在Windows 3.1上运行并生成16位程序,也能在Windows 95和Windows NT上运行并生成同样的程序。但是,作为一个32位程序,Visual C++2.O无法在Windows 3.1上运行,即使装上了Win32s也无法运行,因为出于方便的目的,Microsoft认为在启动Visual C++2.O之前,你一定运行在Windows 95或Windows NT上。

总而言之,在Windows 3.1,Windows for Workgroups,Windows 95或Windows NT上运行Visual c++1.x(更新的版本为1.51)而生成的16位程序能在任何版本的Windows上运行。由Visual C++2.O生成的32位程序在Windows 95或Windows NT上运行得很快,但它在Windows 3.1(和WFW)下也能很好地运行。

对于Borland C/C++,Borland的Turbo C++3.1版是版本较新的16位编译程序,Borland c++4.5版(注意该名称中没有"Turbo一词)是32位Windows编译程序。这两种编译程序都包含编译程序、Borland的OWL C++类和一个出色的集成化调试程序。

C语言异常处理和结构化异常处理有什么区别?

总的来说,结构化异常处理和异常处理之间的区别就是Microsoft对异常处理程序在实现上的不同。所谓的“普通”C++异常处理使用了三条附加的c++语句:try,catch和throw。这些语句的作用是,当正在执行的程序出现异常情况时,允许一个程序(异常处理程序)试着找到该程序的一个安全出口。异常处理程序可以捕获任何数据类型上的异常情况,包括C++类。这三条语句的实现是以针对异常处理的ISO WG21/ANSI X3J16 C++标准为基础的,Microsoft C++支持基于这个标准的异常处理。注意,这个标准只适用于C++,而不适用于C。

结构化异常处理是Microsoft c/c++编译程序的一种功能扩充,它的最大好处就是它对C和C++都适用。Microsoft的结构化异常处理使用了两种新的结构:try—except和try-finally。这两种结构既不是ANSI c++标准的子集,也不是它的父集,而是异常处理的另一种实现(Microsoft会继续在这方面努力的)。try—except结构被称为异常处理(exception handling),tryfinally结构被称为终止处理(termination handling)。try—except语句允许应用程序检索发生异常情况时的机器状态,在向用户显示出错信息时,或者在调试程序时,它能带来很大的方便。在程序的正常执行被中断时,try—finally语句使应用程序能确保去执行清理程序。尽管结构化异常处理有它的优点,但它也有缺点——它不是一种ANSI标准,因此,与使用ANSI异常处理的程序相比,使用结构化异常处理的程序的可移植性要差一些。如果你想编写一个真正的C++应用程序,那么你最好使用ANSI异常处理(即使用try,catch和throw语句)。

怎术防止C语言程序用尽内存?

如果你使用了大量的静态数据,那么你应该考虑使用动态内存分配技术。通过使用动态内存分配技术(即使用malloc()和calloc()函数),你可以在需要时动态地分配内存,在不需要时释放内存。这种方法有几个好处:首先,动态内存分配技术会使程序的效率更高,因为程序只在需要时才使用内存,并且只使用所需大小的内存空间。这样,静态和全局变量就不会占用大量的空间。其次,你可以通过检查malloc()和calloc()函数的返回值来掌握内存不足的情况。

如果你的程序特别大,你可能要使用覆盖管理程序或DOS扩展程序,或者使用其它内存分配机制,例如EMS和XMS(有关内容见18.13和18.14)。

连接过程中出现DGROUP:group exceeds 64K消息是怎么回事?

如果在连接时看到这条出错消息,那是连接程序在指示数据(DGROUP)段中的近程数据(静态数组元素,全局变量等)超过了64KB。解决这个问题的办法有以下几种:
  1. 减少一些全局变量;
  2. 减少程序的栈;
  3. 用动态存储分配技术为数据元素分配动态内态,而不把它们定义为静态型或全局型;
  4. 把数据元素说明为远程型而不是近程型。

减少一些全局变量可能要求对程序的内部结构进行重新设计,但这是值得的。从本质上讲,全局变量的维护很可能是一场恶梦,因此只有在确实需要时才能使用全局变量。如果你分配了大量的空间作为栈空间,那么你应该试试减少栈空间,看看是否能增加可用的内存。如果你在程序中使用了大量静态数据,那么你应该想办法重新安排这些静态数据,并且为它们分配动态的而不是静态的内存。这种技术可以释放近程堆,并且使你能从远程堆中分配内存(见18.15中有关近程堆和远程堆的讨论)。

如果一个程序包含多个源文件,怎样使它们都能正常工作?

编译程序中包含一个MAKE工具(通常叫做MAKE.EXE,NMAKE.EXE或其它类似的名字),其作用是记录项目以及组成项目的源文件之间的依赖关系。下面是一个典型的MAKE文件的例子。
myapp. ohj :       myapp. c               myapp. h
                   cl-c myapp. c
utility. obj :     utility. c                myapp. h
                   cl-c utility. c
myapp, exe :       myapp. obj               utility. obj
                   cl myapp. obj            utility. obj

这个例子表明myapp.obj依赖于myapp.c和myapp.h,utility.obj依赖于utility.c和myapp.h,myapp.exe依赖于myapp.obj和utility.obj。在表示依赖关系的每一行下面,都附有一条对相应的目标进行重新编译或重新连接的编译程序命令。例如,myapp.obj是通过执行下面的命令行重新生成的:
    cl-c myapp.c
在上面的例子中,只有在myapp.c或myapp.h的时间标志晚于myapp.obj的时间标志时,myapp.obj才会被重新编译。同样,只有在utility.c或myapp.h的时间标志晚于utility.obj的时间标志时,utility.obj才会被重新编译;只有在myapp.obj或utility.obj的时间标志晚于myapp.exe的时间标志时,myapp.exe才会被重新连接。

如果一个大型项目包含着源文件依赖关系,那么用MAKE文件来处理是非常方便的。

MAKE工具及其相关的命令和实现因编译程序的不同而不同——关于如何使用MAKE工具,你可以查阅你的编译程序文档。

今天,大多数编译程序都带有集成开发环境,你可以在其中用项目文件来管理程序中的多个源文件,如果你有一个集成环境,你就不必去了解MAKE工具的复杂用法,并且可以很方便地管理项目中的源文件,因为集成环境会为你记录所有的源文件依赖关系。

可以把多个库函数包含在同一个源文件中吗?为什么要建立一个库?

可以把多个库函数包含在同一个源文件中吗?

在同一个源文件中,你想要定义多少个函数,就可以定义多个函数,并且可以把它们都包含到一个库中——然而,在小组开发环境中连接程序和共享源文件时,这种编程风格存在着严重的缺陷。

当你在一个源文件中包含多个库函数时,这些函数会被编译到同一个目标(.obj)文件中。当连接程序要把其中的一个函数连接到程序中去时,目标文件中的所有函数都将被连接进来---不管程序是否用到它们。如果这些函数是无关的(在它们的定义中没有相互调用),那么会因为把不需要的代码连接进来而浪费宝贵的程序空间,见18.7中的例子。这就是要把库函数放到各自的源文件中的原因之一。

另一个原因是为了在小组开发环境下便于进行代码共享。使用独立的源文件能使小组程序员上交和收回单独一个函数,而不必先锁住源文件中的一些函数,然后才能修改源文件中的其它函数。

为什么要建立一个库?

建立一个数据库是为了把可重复使用的函数放在一起,供其它程序员和程序共享。例如,你的几个程序可能都会用到一些通用的功能函数,你不必在每个程序中都复制这些源代码,而只需把这些函数集中到一个函数库中,然后用连接程序把它们连接到你的程序中去。这种方法有利于程序的维护,因为你可以在一个集中的地方而不是几个分散的地方维护你的函数。

如果你在小组环境中工作,那么你应该把你的可重复使用的函数放到一个库中,这样其它小组成员就可以把你的函数连接到他们的程序中去,从而节省了他们复制或从头开始写这些函数的时间。此外,在一个包含几个模块的大项目中,可以把那些自始至终都要用到的“框架”支持函数包含到一个库中。

编译程序中包含一个库管理器(通常叫做LIB.EXE或其它类似的名字),可用来在函数库中增减目标代码模块(.obj)。有些编译程序允许你在它们的集成开发环境中维护你的库,而不必人为地启动库管理器。无论如何,你都应该参考一下18.7和18.8。其中有一些有关建库的重要信息和有用的技巧。

当一个库被连接到目标上时,库中的所有函数是否都会被加到一个.EXE文件中?

不会。当启动连接程序时,它会寻找“未定义的外部函数”,也就是说,它将在每一个库文件中查找源代码文件中未定义的函数。当它找到一个未定义的外部函数后,它会引入包含该函数定义的目标代码。(obj)。不幸的是,如果这个函数是在一个包含其它函数定义的源文件中被编译的话,那么这些函数也会被包含进来,你的可执行代码中将包含一些不需要的代码。因此,将库函数放到各自的源文件中是很重要的——否则会浪费宝贵的程序空间。有些编译程序包含
特殊的“精明的”连接程序,这些连接程序能查出不需要的函数并去掉它们,从而使这些函数不再进入你的程序。

下面举一个例子:假设有两个源文件,分别为libfunc1.c和libfunc2.c,它们所包含的函数都要被放到一个库中。源文件libfunc1.c包含以下两个函数:
    void func_one ()
    {
       ...
    }
    void rune_two()
    {
       ...
    }

源文件libfunc2.c包含以下函数:
    void func_three()
    {
       ...
    }
现在假设已经把这两个源文件编译到一个名为myfuncs.1ib的库中。如果一个与myfuncs.lib连接的程序要调用func_one()函数,连接程序就会在myfuncs.lib库中寻找包含func_one()函数定义的目标代码,并且把它连接进来。不幸的是,函数func_one()是在包含func_two()函数定义的同一个源文件中被编译的,因此,即使你的程序不会用到func_two(),连接程序也不得不把它连接进来。当然,这里假设func_one()中并没有包含对func_two()的调用。如果一个程序包含一个对func_three()的调用,那么只有func_othree()的目标代码会被连接进来,因为该函数是在它自己的源文件中被编译的。

一般说来,你应该尽量把库函数放到各自的源文件中。这种组织方式有助于提高程序的效率,因为程序只会和那些真正需要的函数进行连接,而不会和那些不需要的函数进行连接。这种组织方式在小组开发的情况下也是很有帮助的;在小组开发中,源文件的上交和发放非常频繁,如果一个程序员要对一个包含在其自身的源文件中的函数进行维护,那么他可以集中维护这个函数;如果这个函数所在的源文件中还包含其它一些需要维护的函数,那么这些函数就无法发放给其它小组成员,因为它们包含在一个源文件中。

C语言连接运算符“##”有什么作用?

连接运算符“##”可以把两个独立的字符串连接成一个字符串。在C的宏中,经常要用到“##”运算符,请看下例:
    #include<stdio.h>
    #define SORT(X)  sort_function # # X
    void main(vOid);
    void main(vOid)
    {
        char *array;
        int  elements,element_size;.
        SORT(3) (array,elements,element_size);
    }

在上例中,宏SORT利用“##”运算符把字符串sort_function和经参数x传递过来的字符串连接起来,这意味着语句
    SORT(3)(array,elemnts,element_size);
将被预处理程序转换为语句
    sort_function3(array,elements,element_size);
从宏SORT的用法中你可以看出,如果在运行时才能确定要调用哪个函数,你可以利用“##”运算符动态地构造要调用的函数的名称。

C语言中的包含文件最多可以嵌套几层?

尽管理论上包含文件可以嵌套任意多层,但是,如果嵌套层数太多,编译程序就会用光它的堆栈空间。因此,实际的嵌套层数是有限的,它一般取决于你的硬件设置和编译程序的版本。

在编写程序时,你应该避免过多的嵌套。一般来说,只有在确实需要时才嵌套包含文件,例如建立一个头文件,使其包含每个模块所需的每个头文件。

C语言中的包含文件可以嵌套吗?

包含文件可以嵌套,但你应该避免多次包含同一个文件(见5.3)。

过去,人们认为头文件嵌套是一种不可取的编程方法,因为它增加了MAKE程序中的依赖跟踪函数(dependencytrackingfunction)的工作负担,从而降低了编译速度。现在,通过引入预编译头文件(precompiledheaders,即所有头文件和相关的依赖文件都以一种已被预编译的状态存储起来)这样一种技术,许多流行的编译程序已经解决了上述问题。

许多程序员都喜欢建立一个自己的头文件,使其包含每个模块所需的每个头文件。这是一个头文件。

在C语言编译时,能否指定包含哪一个头文件吗?

你可以通过#if,#else和#endif这组指令实现这一点。例如,头文件alloc.h和malloc.h的作用和内容基本相同,但前者供BorlandC++编译程序使用,后者供MicrosoftC++编译程序使用。如果你在编写一个既支持BorlandC++又支持MicrosoftC++的程序,你就应该指定在编译时是包含alloc.h头文件还是包含malloc.h头文件,请看下例:
    #ifdef __BORLANDC__
    #include<alloc.h>
    #else
    #include<malloc.h>
    #endif

当用BorlandC++编译程序处理上例时,编译程序会自动定义__BORLANDC__标识符名称,因此alloc.h头文件将被包含进来;当用microsoftC++编译程序处理上例时,由于编译程序检查到__BORLANDC__标识符名称没有被定义,因此malloc.h头文件将被包含进来。

#include <file>和#include“file”有什么不同?

在C程序中包含文件有以下两种方法:
(1)用符号“<”和“>”将要包含的文件的文件名括起来。这种方法指示预处理程序到预定义的缺省路径下寻找文件。预定义的缺省路径通常是在INCLUDE环境变量中指定的,请看下例:   
    INCLUDE=C:\COMPILER\INCLUDE;S:\SOURCE\HEADERS;
对于上述INCLUDE环境变量,如果用#include<file>语句包含文件,编译程序将首先到C:\COMPILER\INCLUDE目录下寻找文件;如果未找到,则到S:\SOURCE\HEADERS目录下继续寻找;如果还未找到,则到当前目录下继续寻找。   

(2)用双引号将要包含的文件的文件名括起来。这种方法指示预处理程序先到当前目录下寻找文件,再到预定义的缺省路径下寻找文件。   

对于上例中的INCLUDE环境变量,如果用#include“file”语句包含文件,编译程序将首先到当前目录下寻找文件;如果未找到,则到C:\COMPILER\INCLUDE目录下继续寻找;如果还未找到,则到S:\SOURCE\HEADERS目录下继续寻找。

#include<file>语句一般用来包含标准头文件(例如stdio.h或stdlib.h),因为这些头文件极少被修改,并且它们总是存放在编译程序的标准包含文件目录下。#include“file”语句一般用来包含非标准头文件,因为这些头文件一般存放在当前目录下,你可以经常修改它们,并且要求编译程序总是使用这些头文件的最新版本。

在C语言程序中加入注释的最好方法是什么?

大部分C编译程序为在程序中加注释提供了以下两种方法:

(1)分别是用符号“/*”和“*/”标出注释的开始和结束,在符号“/*”和“*/”之间的任何内容都将被编译程序当作注释来处理。这种方法是在程序中加入注释的最好方法。例如,你可以在程序中加入下述注释:
    /*
    This portion Of the program contains
    a comment that is several lines long
    and is not included in the compiled
    Version Of the program.
    */

(2)用符号“// ”标出注释行,从符号“// ”到当前行末尾之间的任何内容都将被编译程序当作注释来处理。当要加入一行独立的注释时,使用符号“//”是最方便的。但是,对于上面的例子,由于一段独立的注释中有4行内容,因此使用符号“//”是不合适的,请看下例:
    // This portion Of the program contains   
    // a  comment  that  is  several  lines  long
    // and is not included in the compiled   
    // Version Ofthe program.

需要注意的是,用符号"// "加入注释的方法与ANSI标准是不兼容的,许多版本较早的编译程序不支持这种方法。

对于C语言,使用宏更好,还是使用函数更好?

这取决于你的代码是为哪种情况编写的。宏与函数相比有一个明显的优势,即它比函数效率更高(并且更快),因为宏可以直接在源代码中展开,而调用函数还需要额外的开销。但是,宏一般比较小,无法处理大的、复杂的代码结构,而函数可以。此外,宏需要逐行展开,因此宏每出现一次,宏的代码就要复制一次,这样你的程序就会变大,而使用函数不会使程序变大。

一般来说,应该用宏去替换小的、可重复的代码段,这样可以使程序运行速度更快;当任务比较复杂,需要多行代码才能实现时,或者要求程序越小越好时,就应该使用函数。

C语言编程技巧—如何使部分程序在演示版中失效?

如果你在为你的程序制作一个演示版,你可以通过预处理指令使你的程序的一部分生效或失效。以下是一个用#if和#endif指令实现上述功能的例子:
    int save_document(char * doc_name)
    {
    #if DEMO_VERSION
        printf("Sorry!  You can't save documents using the DEMO version Of
        ->this program!\n");
        return(0);
    #endif   
    }

在编写演示版程序的源代码时,如果插入了#define DEMO_VERSION这行语句,预处理程序就会将上述save_document()函数中符合编译条件的代码包含进来,这样,使用演示版的用户就无法保存他们的文件。更好的方法是,在编译选项中定义DEMO_VERSION,这样就不必修改程序的源代码了。

上述技巧在许多不同的情况下都很有用。例如,如果你编写的程序可能要在多种操作系统或操作环境下使用,你就可以定义一些象WINDOWS_VER,UNIX_VER和DOS_VER这样的宏,通过它们指示预处理程序如何根据具体条件将相应的代码包含到你的程序中去。

与用#define指令说明常量相比,用enum关键字说明常量有什么好处?

与用#define指令说明常量(即说明标识符常量)相比,用enum关键字说明常量(即说明枚举常量)有以下几点好处:

(1) 使程序更容易维护,因为枚举常量是由编译程序自动生成的,而标识符常量必须由程序员手工赋值。例如,你可以定义一组枚举常量,作为程序中可能发生的错误的错误号,请看下例:
    enum Error_Code   
    {
       OUT_OF_MEMORY,   
       INSUFFICIENT_DISK_SPACE,   
       LOGIC_ERROR,
       FILE+NOT_FOUND
    }  ;  
在上例中,OUT_OF_MEMORY等枚举常量依次被编译程序自动赋值为0,1,2和3。

同样,你也可以用#define指令说明类似的一组常量,请看下例:
    #define OUT_OF_MEMORY             0   
    #define INSUFFICIENT_DISK_SPACE    1
    #define LOGIC_ERROR                2
    #define FILE_NOT_FOUND             3
上述两例的结果是相同的。

假设你要增加两个新的常量,例如DRIVE_NOT_READY和CORRUPT_FILE。如果常量原来是用enum关键字说明的,你可以在原来的常量中的任意一个位置插入这两个常量,因为编译程序会自动赋给每一个枚举常量一个唯一的值;如果常量原来是用#define指令说明的,你就不得不手工为新的常量赋值。在上面的例子中,你并不关心常量的实际值,而只关心常量的值是否唯一,因此,用enum关键字说明常量使程序更容易维护,并且能防止给不同的常量赋予相同的值。

(2)使程序更易读,这样别人修改你的程序时就比较方便。请看下例:
    void copy_file(char*  source_file_name,  char *  dest_file_name)
    {   
        ......
        Error_Code,err;
        ......
        if(drive_ready()!=TRUE)
        err=DRIVE_NOT_READY;
        ......
    }   
在上例中,从变量err的定义就可以看出;赋予err的值只能是枚举类型Error_Code中的数值。因此,当另一个程序员想修改或增加上例的功能时,他只要检查一下Error_Code的定义,就能知道赋给err的有效值都有哪些。

  注意:将变量定义为枚举类型后,并不能保证赋予该变量的值就是枚举类型中的有效值。

在上例中,编译程序并不要求赋予err的值只能是Error—Code类型中的有效值,因此,程序员自己必须保证程序能实现这一点。

相反,如果常量原来是用#define指令说明的,那么上例就可能如下所示:
    void copy_file(char *source *file,char *dest_file)
    { 
        ...... 
        int err;
        ......
        if(drive_ready()!=TRUE)
            err=DRIVE_NOT_READY;
        ......
    }
当另一个程序员想修改或增加上例的功能时,他就无法立即知道变量err的有效值都有哪些,他必须先检查头文件中的#defineDRIVE_NOT_READY语句,并且寄希望于所有相关的常量都在同一个头文件中定义。

(3)使程序调试起来更方便,因为某些标识符调试程序能打印枚举常量的值。这一点在调试程序时是非常用的,因为如果你的程序在使用枚举常量的一行语句中停住了,你就能马上检查出这个常量的值;反之,绝大多数调试程序无法打印标识符常量的值,因此你不得不在头文件中手工检查该常量的值。

C语言enum关键字—用

C语言中,用#define指令说明常量有什么好处?

enum关键字说明常量有什么好处? 用enum关键字说明常量(即说明枚举常量)有三点好处:
    (1)用enum关键字说明的常量由编译程序自动生成,程序员不需要用手工对常量一一赋值。
    (2)用enum关键字说明常量使程序更清晰易读,因为在定义enum常量的同时也定义了一个枚举类型标识符。
    (3)在调试程序时通常可以检查枚举常量,这一点是非常有用的,尤其在不得不手工检查头文件中的常量值时。

不过,用enum关键字说明常量比用#define指令说明常量要占用更多的内存,因为前者需要分配内存来存储常量。以下是一个在检测程序错误时使用的枚举常量的例子:
    enum Error_Code
    {
        OUT_OF_MEMORY,
        INSUFFICIENT_DISK_SPACE,   
        LOGIC_ERROR,   
        FILE_NOT_FOUND   
    }  ;

与用#define说明常量相比,用enum说明常量还有其它好处,这一点将在5.7中作更详细的介绍。

C语言中,用#define指令说明常量有什么好处?

如果用#define指令说明常量,常量只需说明一次,就可多次在程序中使用,而且维护程序时只需修改#define语句,不必一一修改常量的所有实例。例如,如果在程序中要多次使用PI(约3.14159),就可以象下面这样说明一个常量:   
    #define PI 3.14159
如果想提高PI的精度,只需修改在#define语句中定义的PI值,就不必在程序中到处修改了。通常,最好将#define语句放在一个头文件中,这样多个模块就可以使用同一个常量了。

用#define指令说明常量的另一个好处是占用的内存最少,因为以这种方式定义的常量将直接进入源代码,不需要再在内存中分配变量空间。

但是,这种方法也有缺点,即大多数调试程序无法检查用#define说明的常量。

用#define指令说明的常量可以用#under指令取消。这意味着,如果原来定义的标识符(如NULL)不符合你的要求,你可以先取消原来的定义,然后重新按自己的要求定义一个标识符,详见5.31。

可以用#include指令包含类型名不是".h"的文件吗?

预处理程序将包含用#include指令指定的任意一个文件。例如,如果程序中有下面这样一条语句,那么预处理程序就会包含macros.inc文件。
    #include <macros.inc>
不过,最好不要用#include指令包含类型名不是".h"的文件,因为这样不容易区分哪些文件是用于编译预处理的。例如,修改或调试你的程序的人可能不知道查看macros.inc文件中的宏定义,而在类型名为".h"的文件中,他却找不到在macros.inc文件中定义的宏。如果将macros.inc文件改名为macros.h,就可以避免发生这种问题。

C语言中,怎样避免多次包含同一个头文件?

通过#ifndef和#define指令,你可以避免多次包含同一个头文件。在创建一个头文件时,你可以用#define指令为它定义一个唯一的标识符名称。你可以通过#ifndef指令检查这个标识符名称是否已被定义,如果已被定义,则说明该头文件已经被包含了,就不要再次包含该头文件;反之,则定义这个标识符名称,以避免以后再次包含该头文件。下述头文件就使用了这种技术:
# ifndef _FILENAME_H
#define _FILENAME_H
#define VER_NUM " 1. 00. 00"
#define REL_DATE "08/01/94"
#if _WINDOWS_
# define OS_VER     "WINDOWS"
#else
#define OS_VER      "DOS" 
# endif
# endif

当预处理程序处理上述头文件时,它首先检查标识符名称_FILENAME_H是否已被定义——如果没有被定义,预处理程序就对此后的语句进行预处理,直到最后一个#endif语句;反之,预处理程序就不再对此后的语句进行预处理。

C语言预处理程序(preprocessor)有什么作用?

C语言预处理程序的作用是根据源代码中的预处理指令修改你的源代码。预处理指令是一种命令语句(如#define),它指示预处理程序如何修改源代码。在对程序进行通常的编译处理之前,编译程序会自动运行预处理程序,对程序进行编译预处理,这部分工作对程序员来说是不可见的。

预处理程序读入所有包含的文件以及待编译的源代码,然后生成源代码的预处理版本。在预处理版本中,宏和常量标识符已全部被相应的代码和值替换掉了。如果源代码中包含条件预处理指令(如#if),那么预处理程序将先判断条件,再相应地修改源代码。

下面的例子中使用了多种预处理指令:
# include <stdio. h>
# define TRUE 1
# define FALSE (!TRUE)
# define GREATER (a, b) ((a) > (b) ? (TRUE) : (FALSE))
# define PIG-LATIN FALSE
void main (void);
void main (void)
{
    int x, y;
# if PIG_LATIN
    printf("Easeplay enternay ethay aluevay orfay xnay:") ;
    scanf("%d", &x) ;
    printf("Easeplay enternay ethay aluevay orfay ynay:");
    scanf("%d", &y);
#else
    printf(" Please enter the value for x: ");
    scanf("%d", &x);
    printf("Please enter the value for y: ");
    scanf("%d", &y);
# endif
    if (GREATER(x, y) = = TRUE)
    {
# if PIG- LATIN
    printf("xnay islay eatergray anthay ynay!\n");
#else
    printf {" x is greater than y! \n" ) ;
# endif
    }
    else
    {
# if PIG_LATIN
    printf ("xnay islay otnay eatergray anthay ynay!\n");
#else
    printf ("x is not greater than y!\n");
# endif
    }
}
上例通过预处理指令定义了3个标识符常量(即TRUE,FALSE和PIG_LATIN)和一个宏(即GREATER(a,b)),并使用了一组条件编译指令。当预处理程序处理上例中的源代码时,它首先读入stdio.h头文件,并解释其中的预处理指令,然后把所有标识符常量和宏用相应的值和代码替换掉,最后判断PIG_LATIN是否为TRUE,并由此决定是使用拉丁文还是使用英文。

如果PIG_LATIN为FALSE,则上例的预处理版本将如下所示:
/ * Here is where all the include files
would be expanded. * /
void main (void)
{
    int x, y;
    printf("Please enter the value for X: ");
    scanf("%d", &x);
    printf("Please enter the value for y: ");
    scanf("%d", &y),
    if (((x) > (y) ? (1) : (!1)) == 1)
    {
        printf("x is greater than y!\n");
    }
    else
    {
        printf{"x is not greater than y!\n");
    }
}

多数编译程序都提供了一个命令行选项,或者有一个独立的预处理程序,可以让你只启动预处理程序并将源代码的预处理版本保存到一个文件中。你可以用这种方法查看源代码的预处理版本,这对调试与宏或其它预处理指令有关的错误是比较有用的。

C语言的宏(macro)是什么?怎样使用宏?

宏是一种预处理指令,它提供了一种机制,可以用来替换源代码中的字符串,宏是用“#define"语句定义的,下面是一个宏定义的例子:
    #define VERSION—STAMP "1.02"

上例中所定义的这种形式的宏通常被称为标识符。在上例中,标识符VERSION_STAMP即代表字符串"1.02"——在编译预处理时,源代码中的每个VERSION_STAMP标识符都将被字符串“1.02”替换掉。

以下是另一个宏定义的例子:
    #define CUBE(x)((x),(x)*(x))
上例中定义了一个名为CUBE的宏,它有一个参数x。CUBE宏有自己的宏体,即((x)*(x)*(x))——在编译预处理时,源代码中的每个CUBE(x)宏都将被((x)*(x)*(x))替换掉。

使用宏有以下几点好处:
    (1)在输入源代码时,可省去许多键入操作。   
    (2)因为宏只需定义一次,但可以多次使用,所以使用宏能增强程序的易读性和可靠性。
    (3)使用宏不需要额外的开销,因为宏所代表的代码只在宏出现的地方展开,因此不会引起程序中的跳转。
    (4)宏的参数对类型不敏感,因此你不必考虑将何种数据类型传递给宏。

需要注意的是,在宏名和括起参数的括号之间绝对不能有空格。此外,为了避免在翻译宏时产生歧义,宏体也应该用括号括起来。例如,象下例中这样定义CUBE宏是不正确的:
denne CUBE(x)   x * x * x
对传递给宏的参数也要小心,例如,一种常见的错误就是将自增变量传递给宏,请看下例:
#include <stdio. h>
#include CUBE(x) (x * x * x)
void main (void);
void main (void)
{
    int x, y;
    x = 5;
    y = CUBE( + +x);
    printfC'y is %d\n" . y);
}
在上例中,y究竟等于多少呢?实际上,y既不等于125(5的立方),也不等于336(6* 7*8),而是等于512。因为变量x被作为参数传递给宏时进行了自增运算,所以上例中的CUBE宏实际上是按以下形式展开的:
    y = ((++x) * (++x) * (++x));
这样,每次引用x时,x都要自增,所以你得到的结果与你预期的结果相差很远,在上例中,由于x被引用了3次,而且又使用了自增运算符,因此,在展开宏的代码时,x实际上为8,你将得到8的立方,而不5的立方。

上述错误是比较常见的,作者曾亲眼见过有多年C语言编程经验的人犯这种错误。因为在程序中检查这种错误是非常费劲的,所以你要给予充分的注意。你最好试一下上面的例子,亲眼看一下那个令人惊讶的结果值(512)。

宏也可使用一些特殊的运算符,例如字符串化运算符“#”和。连接运算符“##”。“#”运算符能将宏的参数转换为带双引号的字符串,请看下例:
    define DEBUG_VALUE(v)  printf(#v"is equal to %d.\n",v)
你可以在程序中用DEBUG_VALUE宏检查变量的值,请看下例:
    int x=20;
    DEBUG_VALUE(x);
上述语句将在屏幕上打印"x is equal to 20"。这个例子说明,宏所使用的“#”运算符是一种非常方便的调试工具。
“##”运算符的作用是将两个独立的字符串连接成一个字符串。

C语言编译预处理概述

本专题集中讨论与预处理程序有关的问题。在编译程序对程序进行通常的编译之前,要先运行预处理程序。可能你以前没有见过这个程序,因为它通常在幕后运行,程序员是看不见它的,然而,这个程序非常有用。

预处理程序将根据源代码中的预处理指令来修改你的程序。预处理指令(如#define)为预处理程序提供特定的指令,告诉它应该如何修改你的源代码。预处理程序读入所有包含的文件和待编译的源代码,经过处理生成源代码的预处理版本。在该版本中,宏和常量标识符已用相应的代码和值代替。如果源代码中包含条件预处理指令(如#if),预处理程序将先判断条件,然后相应地修改源代码。

预处理程序有许多非常有用的功能,例如宏定义,条件编译,在源代码中插入预定义的环境变量,打开或关闭某个编译选项,等等。对专业程序员来说,深入了解预处理程序的各种特征,是创建快速和高效的程序的关键之一。

在阅读本专题时,请记住本专题采用的一些技术(以及所提到的一些常见陷阱),以便更好地利用预处理程序的各种功能。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值