zoukankan      html  css  js  c++  java
  • 预处理器

    预处理器

    1、预处理器的工作原理      

            预处理器的行为是由预处理指令(由#字符开头的一些指令)控制的。如#define和#include。

            #define指令定义了一个宏——用来代表其他东西的一个名字,例如常量或常用的表达式。预处理器会通过将宏的名字和它的定义存储在一起来响应#define指令。当这个宏在后面的程序中使用时,预处理器“扩展”宏,将宏替换为其定义值。

            #include指令告诉预处理器打开一个特定的文件,将它的内容作为正在编译的文件的一部分“包含”进来。例如,代码行:

            #include<stdio.h>

    指示预处理器打开一个名字为stdio.h的文件,并将它的内容加到当前的程序中。(stdio.h包含了C语言标准输入/输出函数的原型。)

            预处理器并不会删除原始程序中包含指令的行,而是简单地将它们替换为空。同时,预处理器执行了指令之后,还将每一处的注释都替换为一个空格字符。有一些预处理器还会进一步删除不必要的空白字符,包括每一行开始用于缩进的空格符和制表符。

            在C语言的早期版本中,预处理器是一个单独的程序,它的输出提供给编译器。如今,预处理器通常和编译器集成在一起,而且其输出也不一定全是C代码(例如,包含<stdio.h>之类的标准头使得我们可以在程序中使用相应的函数,而不需要把头的内容复制到程序的源代码中)。然而,将预处理器和编译器认为是不同的程序是有用的。实际上,大部分C编译器都提供了一种方法,使用户可以看到预处理器的输出。在指定某个特定的选项(GCC编译器用的是-E)时编译器会产生预处理器的输出。其他一些编译器会提供一个类似于集成的预处理器的独立程序。要了解更多信息,可以查看使用的编译器文档。

            注意:预处理器仅仅知道少量C语言的规则。因此,它在执行指令非常有可能产生非法的程序。经常是原程序看起来没问题,使错误查找起来很难。对于复杂的程序,检查预处理器的输出可能是找到这类错误的有效途径。

    2、预处理指令

            大多数预处理指令都属于下面3种类型之一:

    • 宏定义:#define指令定义一个宏,#undef指令删除一个宏定义。
    • 文件包含:#include指令导致一个指定文件的内容被包含到程序中。
    • 条件编译:#if、#ifdef、#ifndef、#elif、#else和#endif指令可以根据预处理器可以测试的条件来确定是将一段文本块包含到程序中还是将其排除在程序之外。

    剩下的#error、#line和#pragma指令是更特殊的指令,较少用到。

            在进一步讨论之前,先来看几条适用于所有指令的规则。

    • 指令都以#开始。#符号不需要在一行的行首,只要它之前只有空白字符就行。在#后是指令名,接着是指令所需要的其他信息。
    • 在指令的符号之间可以插入任意数量的空格或水平制表符。例如,下面的指令是合法的:

             #           define          N              100

    • 指令总是在第一个换行符处结束,除非明确地指明要延续。如果想在下一行延续指令,我们必须在当前行的末尾使用  字符。例如,下面的指令定义了一个宏来表示硬盘的容量,按字节计算:

              #define DISK_CAPCCITY( SIDES *                                

                                                        TRACKS_PER_SIDE *          

                                                         SECTOS_PER_TRACK *    

                                                         BYTES_PER_SECTOR )

    • 指令可以出现在程序中的任何地方。但是我们通常将#define和#include指令放在文件的开始,其他指令则放在后面,甚至可以放在函数定义的中间。
    • 注释可以与指令放在同一行。实际上,在宏定义的后面加一个注释来解释宏的含义是一种比较好的习惯:

              #define FREEZING_PT 32.0f           // freezing point of water

    3、宏定义

    • 简单的宏

           简单的宏(C标准中称为对象式宏)的定义有如下格式:

            [ #define指令(简单的宏)]         #define 标识符 替换列表

            替换列表是一系列的预处理记号。宏的替换列表可以包括标识符、关键字、数值常量、字符常量、字符串字面值、操作符和排列。当预处理器遇到一个宏定义时,会做一个“标识符”代表“替换列表”的记录。在文件后面的内容中,不管标识符在哪里出现,预处理器都会用替换列表代替它。

    注意:

             不要在宏定义中放置任何额外的符号,否则它们会被作为替换列表的一部分。一种常见的错误就是在宏定义中使用 = :

             #define N = 100      /***WRONG***/

             ...

            int a[N];                  /***becomes int a[ = 100 ];***/

    在上面的例子中,我们(错误地)把N定义成两个几号(= 和100)。

            在宏定义的末尾使用分号结尾是另一个常见错误:

             #define N 100;       /*** WRONG***/

             ...

            int a[N];                  /*** becomes int a[ 100; ]; ***/

    这里N被定义为100和;两个记号。

            编译器可以检查到宏定义中绝大多数由多余符号所导致的错误。但是,编译器只会将每一个使用这个宏的地方标记为错误,而不会直接找到错误的根源——宏定义本身,因为宏定义已经被预处理器删除了。

            简单的宏主要用来定义那些被Kernighan和Ritchie称为“明示常量”(manifest constant)的东西。我们可以使用宏给数值、字符值和字符串值命名。

            #define    STE_LEN     80

            #define    TRUE            1

            #define    FALSE          0

            #define    PI                   3.14159

            #define    CR                 ' '

            #define    EOS               ' '

            #define    MEN_ERR    "Error: not enough memory"

            使用#define来为常量命名有很多显著的优点:

    • 程序会更加易读。一个认真选择的名字可以帮助读者理解常量的意义。否则,程序将包含大量的“魔法术”,很容易迷惑读者。
    • 程序会更易于修改。我们仅需要改变一个宏定义,就可以改变整个程序中出现的所有该常量的值。“硬编码的”常量会更难于修改,特别是当他们以稍微不同的形式出现时。
    • 可以帮助避免前后不一致或键盘输入错误

            虽然简单的宏常用于定义常量名,但是它们还有其他应用。

    • 可以对C语法做小的修改。我们可以通过定义宏的方式给C语言符号添加别名,从而改变C语言的语法。例如,对于习惯用Pascal的begin和end(而不是C语言的{和})的程序员,可以定义下面的宏:

            #define    BEGIN      {

            #define    END          }

           我们甚至可以发明自己的语言。例如,我们可以创建一个LOOP“语句”,来实现一个无限循环:

            #define   LOOP   for( ; ; )

    • 对类型重命名。我们通过重命名int创建了一个布尔类型:

            #define BOOL int

            虽然有些程序员会使用宏定义的方式来实现此目的,但类型定义仍然是定义新类型的最佳方法。

    • 控制条件编译。宏在控制条件编译中起着重要的作用。例如,在程序中出现的下面这行宏定义可能表明需要将程序在“调试模式”下进行编译,并使用额外的语句输出调试信息:

             #define     DEBUG

            这里顺便说一下,在上面的例子中,宏定义中的替换列表为空是合法的。

    • 带参数的宏:

            带参数的宏(也称之为函数式宏)的定义有如下格式:

            [#define指令(带参数的宏)]            #define 标识符(x1,x2, ... xn)替换列表

    其中x1,x2, ... xn是标识符(宏的参数)。这些参数可以在替换列表中根据需要出现任意次。

    注意:在宏的名字和左括号之间必须没有空格。如果有空格,预处理器会认为是在定义一个简单的宏,其中(x1,x2, ... xn)是替换列表的一部分。

            当预处理器遇到带参数的宏时,会将宏定义存储起来以便后面使用。在后面的程序中,如果任何地方出现了标识符(y1,y2, ... yn)格式的宏调用(其中y1,y2, ... yn是一系列标记号),预处理器会使用替换列表替代——使用y1替换x1,y2替换x2,以此类推。

            例如,假定我们定义了如下的宏:

            #define   MAX(x,y)        ((x)>(y) ? (x) : (y))

            #define   IS_EVEN(n)    ((n)%2 == 0) 

    (宏定义中的圆括号似乎过多,后面会解释原因)现在如果后面的程序中有如下语句:

            i = MAX(j + k, m-n);

            if  (IS_EVEN(i))  i++;

    预处理器会将这些行替换为:

            i = MAX( (j + k) > (m - n) ? (j + k) : (m - n) );

            if  ( ( (i) %2 ==0) )  i++;

    如这个例子所示,带参数的宏经常用来作为简单的函数使用。MAX类似一个从两个值选取较大值的函数,IS_EVEN则类似于一种当参数为偶数时返回1,否则返回0的函数。

  • 相关阅读:
    (双指针 二分) leetcode 167. Two Sum II
    (双指针) leetcode 485. Max Consecutive Ones
    (双指针) leetcode 27. Remove Element
    (String) leetcode 67. Add Binary
    (数组) leetcode 66. Plus One
    (N叉树 BFS) leetcode429. N-ary Tree Level Order Traversal
    (N叉树 递归) leetcode 590. N-ary Tree Postorder Traversal
    (N叉树 递归) leetcode589. N-ary Tree Preorder Traversal
    (N叉树 DFS 递归 BFS) leetcode 559. Maximum Depth of N-ary Tree
    (BST 递归) leetcode98. Validate Binary Search Tree
  • 原文地址:https://www.cnblogs.com/wyxsq/p/5242385.html
Copyright © 2011-2022 走看看