• C和指针 第14章 预处理器 14.2 #define


    14.2 #define
        #define的一个简单用法就是为数值命名一个符号。本节将介绍#define指令的更多用途。首先让我们观察一下它的更为正式的描述。
        #define name stuff
        有了这条指令以后,每当有符号name出现在这条指令后面时,预处理器就会把它替换成stuff。
        K&R C:
        早期的C编译器要求#include出现在每行的起始位置,不过它的后面可以跟一些空白。在ANSI C中,这条限制被取消了。
        替换文本并不仅限于数值字面值常量。使用#define指令,可以把任何问题替换到程序中。这里有几个例子:
        #define reg         register;
        #define do_forever     for(;;) 
        #define CASE        break; case
        第1个定义只是为关键字register创建了一个简短的别名。这个较短的名字使各个声明更容易通过制表符进行排列。第2条声明用一个更具描述性的符号来创建一种用于实现无限循环的for语句类型。最后一个#define定义了一个简短记法,以便在switch语句中使用。它自动地把一个break放在每个case之前,这使得switch语句看上去更像其他语言的case语句。

    /*
    ** #define。 
    */
    #include <stdio.h>
    #include <stdlib.h>

    #define ARR_LEN 5
    #define reg register
    #define do_forever for(;;)
    #define CASE break; case

    int main( void ){
        int array[ARR_LEN] = { 1, 2, 3, 4, 5 };
        
        for( reg int i = 0; i < ARR_LEN; ++i ){
            printf( "%d ", array[i] );
        }
        printf( "\n" );
        int j = 0;
        do_forever{
            printf( "%d ", array[j] );
            j++;
            if( j == 5 ){
                break;
            }
        }
        printf( "\n" );
        int grades = 99;
        switch( grades / 10 ){
            CASE 10:
                printf( "Have a congratulations on you, you get full mark!\n" );
            CASE 9:
                printf( "grade you get is more than or equal to 90.\n" );
                break;
            default:
                printf( "grade you get is unknown!\n" );
        }
        
        return EXIT_SUCCESS;
    }
    /* 输出:

    */ 

        如果定义中的stuff非常长,它可以分成几行,除了最后一行,每行的末尾都要加一个反斜杠,如下面的例子所示:
        #define DEBUG_PRINT printf( "File %s line %d:" \
                                    "x=%d, y=%d, z=%d", \
                                    __FILE__, __LINE__, \
                                    x, y, z )
        这里利用了“相邻的字符串常量被自动连接为一个字符串”这个特性。在调试一个存在许多涉及一组变量的不同计算过程的程序时,这种类型的声明非常有用。我们可以很容易地插入一条调试语句,打印它们的当前值。
        x *= 2;
        y += x;
        z = x * y;
        DEBUG_PRINT;
        警告:
        这条语句在DEBUG_PRINT后面加了一个分号,所以不应该在宏定义的尾部加上分号。如果这样做了,结果就会产生两条语句---一条printf语句后面再加一条空语句。有些场合只允许出现一条语句,如果放入两条语句就会出现问题,例如:
        if( ... )
            DEBUG_PRINT;
        else
            ...
        也可以使用#define指定把一序列语句插入到程序中。这里有一个完整循环的声明:
        #define PROCESS_LOOP     \
            for( i = 0; i < 10; i+= 1 ){      \
                sum += i;                        \
                if( i > 0 )                          \
                    prod *= i;                    \
            } 

    /*
    ** DEBUG_PRINT。 
    */
    #include <stdio.h>
    #include <stdlib.h>

    #define DEBUG_PRINT printf( "File %s line %d:" \
                                    "x=%d, y=%d, z=%d", \
                                    __FILE__, __LINE__, \
                                    x, y, z )
    #define DEBUG_PRINT_2 printf( "File %s line %d:" \
                                    "x=%d, y=%d, z=%d", \
                                    __FILE__, __LINE__, \
                                    x, y, z );
                                    
    int main( void ){
        int x;
        int y;
        int z;
        int flag;
        int i;
        int prod;
        int sum;
        
        x = 2; 
        y = 1;
        x *= 2;
        y += x;
        z = x * y;
        flag = 1;
        prod = 1;
        sum = 0;
        #define PROCESS_LOOP           \
            for( i = 0; i < 10; i += 1 ){           \
                sum += i;                               \
                if( i > 0 )                                \
                    prod *= i;                           \
            } 
        PROCESS_LOOP;
        printf( "i = %d, prod = %d\n", i, prod );
        DEBUG_PRINT; 
        printf( "\n" );
        
        /*
        ** Can't pass compilation.
        ** Because 'else' without a previous 'if'
        ** if( flag )
        **       DEBUG_PRINT_2;
        ** else
        **       printf( "I don't know." );
        */ 

        return EXIT_SUCCESS;
    }
    /* 输出:

    */ 

        提示:
        不要滥用这种技巧。如果相同的代码需要出现在程序的几个地方,通常更好的方法是把它实现为一个函数。本章后面将详细讨论#define宏和函数之间的优劣。

        14.2.1 宏
        #define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(defined macro)。下面是宏的声明形式:
        #define name(paramter_list) stuff
        其中,parameter_list(参数列表)是一个由逗号分隔的符号列表,它们可能出现在stuff中。参数列表的左括号必须与name紧邻。如果两者之间由任何空白存在,参数列表就会被解释为stuff的一部分。
        当宏被调用时,名字后面是一个由逗号分隔的值的列表,每个值都与宏定义中的一个参数相对应,整个列表用一对括号包围。当参数出现在程序中时,与每个参数的实际值都将替换到stuff中。
        这里有一个宏,它接受一个参数:
        #define SQUARE( x ) x * x
        如果在上述声明之后,把SQUARE( 5 )置于程序中,预处理器就会用下面这个表达式替换上面的表达式:
        5 * 5
        警告:
        但是,这个宏存在一个问题。观察下面的代码段:
        a = 5;
        printf( "%d\n", SQUARE( a + 1 ) );
        乍一看,可能觉得这段代码将打印36这个值。事实上,它将打印11。为什么?请观察被替换的宏文本。参数x被文本a+1替换,所以这条语句实际上变成了
        printf( "%d\n", a + 1 * a + 1 );
        现在问题清楚了:由替换产生的表达式并没有按照预想的次序进行求值。
        在宏定义中加上两个括号,这个问题便很轻松地解决了:
        #define DOUBLE( x ) ( x ) + ( x )
        定义中使用了括号,用于避免前面出现的问题。但是,使用这个宏,可能会出现一个不同的错误。下面这段代码将打印出什么值?
        a = 5;
        printf( "%d\n", 10 * DOUBLE( a ) );
        警告:
        看上去它好像将打印100,但事实上它打印的是55。再一次,通过观察宏替换产生的文本,就能够发现问题所在:
        printf( "%d\n", 10 * ( a ) + ( a ) );
        乘法运算在宏所定义的加法运算之前执行。这个错误很容易修正:在定义宏时,只要在整个表达式两边加上一对括号就可以了。
        #define DOUBLE( x ) ( ( x ) + ( x ) )
        提示:
        所有用于对数值表达式进行求值的宏定义都应该用这种方式加上括号;避免在使用宏时,参数中的操作符或临近的操作符之间发生不可预料的相互作用。 

    /*
    ** SQUARE。 
    */
    #include <stdio.h>
    #include <stdlib.h>

    #define SQUARE( x ) x * x
    #define SQUARE_2( x ) (x) * (x)
    #define DOUBLE( x ) (x) + (x)
    #define DOUBLE_2( x ) ((x) + (x))
    int main( void ){
        printf( "SQUARE( 5 ) = %d\n", SQUARE( 5 ) );
        int a = 5;
        printf( "%d\n", SQUARE( a + 1 ) );
        /* 
        ** the same form.
        ** printf( "%d\n", a + 1 * a + 1 );
        */ 
        printf( "%d\n", SQUARE_2( a + 1 ) ); 
        a = 5;
        printf( "%d\n", 10 * DOUBLE( a ) );
        /*
        ** the same form.
        ** printf( "%d\n", 10 * ( a ) + ( a ) );
        */ 
        printf( "%d\n", 10 * DOUBLE_2( a ) );
        
        return EXIT_SUCCESS;
    }
    /* 输出:

    */ 

        下面是一对有趣的宏:
        #define repeat do
        #define until(x) while( !( x ) )
        这两个宏创建了一种“新”的循环,其工作过程类似于其他语言中的repeat/until循环。它按照下面这样的方式使用:
        repeat {
            statement;
        } until( i >= 10 );
        预处理器将用下面的代码进行替换:
        do {
            statements;
        } while( !( i >= 10) );
        表达式i>=10两边的括号用于确保在!操作符执行之前先完成这个表达式的求值。

    /*
    ** repeat...until。 
    */
    #include <stdio.h>
    #include <stdlib.h>

    #define repeat do
    #define until(x) while( !( x ) )
        
    int main( void ){
        int i;
        
        i = 5;
        repeat {
            printf( "i = %d\n", i );
            i++;
        } until( i >= 10 );
        printf( "\n" );
        i = 5;
        do {
            printf( "i = %d\n", i );
            i++;
        } while( !(i >= 10) );
        
        return EXIT_SUCCESS;
    }
    /* 输出:

    */ 

        提示:
        创建一套#define宏,用一种看上去很像其他语言的方式编写C程序完全是可能的。在绝大多数情况下,都应该避免这种诱惑,因为这样编写出来的程序使其他C程序员很难理解。它们必须时常查阅这些宏的定义以便弄清实际的代码时什么意思。即使与这个项目生命期各个阶段相关的所有人都熟悉那种被模仿的语言,这个技巧仍然可能引起混淆,因为准确模仿其他语言的各个方面是极其
    困难的。

        14.2.2 #define替换
        在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
        1.在调用宏时,首先对参数进行检查,看看是否包含了任何由#define定义的符号。如果是,它们首先被替换。
        2.替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被它们的值所替代。
        3.最后,再次对结果文本进行扫描,看看它是否包含了任何由#define定义的符号。如果是,就重复上述处理过程。

    /*
    ** #define替换。 
    */
    #include <stdio.h>
    #include <stdlib.h>

    #define LEN 5
    #define ARRAY_NAME array_name
    #define print_array( ARRAY_NAME, LEN )  \
        int i = 0;                                \
        while( i < LEN ){                   \
            printf( "%d ", ARRAY_NAME[i] ); \
            ++i;                                \
        }                                        \
        printf( "\n" );               

    int main( void ){
        int ARRAY_NAME[5] = { 1, 2, 3, 4, 5 };
        
        print_array( ARRAY_NAME, sizeof(ARRAY_NAME) / sizeof(*ARRAY_NAME) );
        
        return EXIT_SUCCESS;
    }
    /* 输出:

    */ 

        这样,宏参数和#define定义的符号。但是,宏不可以出现递归。
        当预处理器搜索#define定义的符号时,并不检查字符串常量的内容。如果想把宏参数插入到字符串常量中,可以使用两个技巧。第一个技巧是,邻近字符串自动连接的特性使我们很容易把一个字符串分成几段,每段实际上都是一个宏参数。示例如下:
        #define PRINT( FORMAT, VALUE ) \
            printf( "The value is " FORMAT "\n", VALUE )
        ...
        这个技巧只有当字符串常量作为宏参数给出时才能使用。
        第二个技巧是使用预处理器把一个宏参数转换为一个字符串。#argument这种结构被预处理器翻译为“argument”。这种翻译可以让你像下面这样编写代码:
        #define PRINT( FORMAT, VALUE ) \
            printf( "The value of " #VALUE \
            " is " FORMAT "\n", VALUE )
        ...
        PRINT( "%d", x + 3 );
        它将产生下面的输出:
        The value of x + 3 is 25
        ##结构则执行一种不同的任务。它把位于自己两边的符号链接成一个符号。作为用途之一,它允许宏定义从分离的文本片段创建标识符。
        下面这个例子使用这种连接把一个值添加到几个变量之一:
        #define ADD_TO_SUM( sum_number, value )\
            sum ## sum_number += value
        ...
        ADD_TO_SUM( 5, 25 );
        最后一条语句把值25加到变量sum5。注意,这种连接必须产生一个合法的标识符,否则,其结果就是未定义的。

    /*
    ** PRINT。 
    */
    #include <stdio.h>
    #include <stdlib.h>

    #define PRINT( FORMAT, VALUE ) \
            printf( "The value is " FORMAT "\n", VALUE )
    #define PRINT_2( FORMAT, VALUE ) \
            printf( "The value of " #VALUE \
            " is " FORMAT "\n", VALUE )
    #define ADD_TO_SUM( sum, sum_number, value )\
            sum ## sum_number += value

    int main( void ){
        int x = 1;
        
        PRINT( "%d", x + 3 );
        PRINT_2( "%d", x + 3 );

        int sum5 = 1;
        printf( "original sum5 = %d\n", sum5 );
        int sum_number = 5;
        int value = 25;
        ADD_TO_SUM( sum, 5, value );
        printf( "after ADD_TO_SUM( sum, 5, value), sum5 = %d\n", sum5 );
        
        
        return EXIT_SUCCESS;
    }
    /* 输出:

    */ 

        14.2.3 宏于函数
        宏非常频繁地用于执行简单的计算,比如在两个表达式中寻找其中较大(或较小)的一个:
        #define MAX( a, b ) ( ( a ) > ( b ) ? ( a ) : ( b ) )
        为什么不用函数来完成这个任务呢?有两个原因。首先,用于调用和从函数返回的代码很可能比实际执行这个小型计算工作的代码更大,所以使用宏比使用函数在程序的规模和速度方面都更胜一筹。
        其次,更为重要的是,函数的参数必须声明为一种特定的类型,所以它只能在类型合适的表达式中使用。反之,上面这个宏可以用于整型,长整型、单浮点数、双浮点数以及其他任何可以用>操作符比较值大小的类型。换句话说,宏是与类型无关的。
        和使用函数相比,使用宏的不利之处在于每次使用宏时,一份宏定义代码的副本都将插入到程序中。除非宏非常短,否组使用宏可能会大幅增加程序的长度。
        还有一些任务根本无法用函数实现。让我们仔细观察定义于11.1a中的宏。这个宏的第2个参数是一种类型,它无法作为函数参数进行传递。
        #define MALLOC( n, type ) \
            ( (type *)malloc( n * sizeof(type) ) )
        现在可以观察一下这个宏确切的工作过程。下面这里例子中的第1条语句被预处理器转换为第2条语句:
        pi = MALLOC( 25, int );
        pi = ( (int *)malloc( 25 * sizeof(int) ) ); 
        同样,请注意宏定义并没有用一个分号结束。分号出现在调用这个宏的语句中。

    /*
    ** 宏与函数。 
    */
    #include <stdio.h>
    #include <stdlib.h>

    #define MAX( a, b ) ((a) > (b) ? (a) : (b)) 
    #define MALLOC( n, type ) ((type *)malloc(n * sizeof(type)))

    int main( void ){
        int *pi;
        
        /* note that semicolon */
        pi = MALLOC( 25, int );
        /*
        ** the same form.
        ** pi = ((int*)malloc(25 * sizeof(int)));
        */ 
        free( pi );
        
        return EXIT_SUCCESS;
    }

        14.2.4 带副作用的宏参数
        当宏参数在宏定义中出现的次数超过一次时,如果这个参数具有副作用,那么在使用这个宏时就可能出现危险,导致不可预料的结果。副作用就是在表达式求值时出现永久性的效果。例如,下面这个表达式
        x + 1
        可以重复执行几百次,它每次获得的结果都是一样的。这个表达式不具有副作用。但是
        x++
        就具有副作用:它增加x的值。当这个表达式下一次执行时,它将产生一个不同的结果。MAX宏可以证明具有副作用的参数所引起的问题。观察下列代码,你认为它将打印出什么?
        #define MAX( a, b ) ( ( a ) > ( b ) ? ( a ) : ( b ) )
        ...
        x = 5;
        y = 8;
        z = MAX( x++, y++ );
        printf( "x=%d, y=%d, z=%d\n", x, y, z );
        这个问题并不轻松。记住第一个表达式是一个条件表达式,用于确定执行另外两个表达式中的哪一个,剩余的那个表达式将不会执行。其结果是:x=6, y=10, z=9。
        和往常一样,只要检查一下用宏替换后产生的代码,这个奇怪的结果就一目了然了。
        z = ( (x++) > (y++) ? (x++) : (y++) );
        虽然那个较小的值只增值了一次,但那个较大的值却增加了两次---第1次是在比较时,第2次在执行?符号后面的表达式时。 
        副作用并不仅限于修改变量的值。下面这个表达式
        getchar()
        也具有副作用。调用这个函数将“消耗”输入的一个字符,所以该函数的后续调用将得到不同的字符。如果用户的意图并不是想“消耗”输入字符,就不能重复调用这个函数。
        考虑下面这个宏。
        /*
        **这个函数返回参数值中值为1的位的个数。 
        */
        int count_one_bits( unsigned value )
        {
            int ones;
            /*
            **当这个值还有一些值为1的位时。 
            */
            for( ones = 0; value != 0; value = value >> 1 ){
                /*
                **如果最低位的值为1,计数加1。
                */
                if( value % 2 != 0 ){
                    ones = ones + 1;
                }
            }    
            return ones;
        } 
        #define EVENPARITY( ch ) \
            ( (count_one_bit( ch ) & 1 ) ? \
            ( ch ) | PARITYBIT : ( ch ) )
        它使用了程序5.1中的count_one_bit函数,该函数返回它的参数的二进制位模式中1的个数。这个宏的目的是产生一个具有偶校验的字符。它首先计数字符中位1的个数,如果结果是一个奇数,PARITYBIT值(一个值为1的位)与该字符执行OR操作,否则该字符就保留不变。
        但是,当这个宏以下面这种方式使用时,请想象一下会发生什么?
        ch = EVENPARITY( getchar() );
        这条语句看上去很合理:读取一个字符并计算它的校验位。但是,它的结果是失败的,因为它实际上读入了两个字符。

    /*
    ** 带副作用的宏参数。 
    */
    #include <stdio.h>
    #include <stdlib.h>

    #define MAX( a, b ) ((a) > (b) ? (a) : (b))
    #define PARITYBIT 1
    #define EVENPARITY( ch ) \
            ((count_one_bits( ch ) & 1) ? \
            (ch) | PARITYBIT : (ch))
    /*
    **这个函数返回参数值中值为1的位的个数。 
    */
    int count_one_bits( unsigned value )
    {
        int ones;
        /*
        **当这个值还有一些值为1的位时。 
        */
        for( ones = 0; value != 0; value = value >> 1 ){
            /*
            **如果最低位的值为1,计数加1。
            */
            if( value % 2 != 0 ){
                ones = ones + 1;
            }
        }    
        return ones;

    int main( void ){
        int x;
        int y;
        int z;
        
        x = 5;
        y = 8;
        z = MAX( x++, y++ );
        /*
        ** the same form.
        ** z = ((x++) > (y++) ? (x++) : (y++)); 
        */
        printf( "x=%d, y=%d, z=%d\n", x, y, z );
        
        int ch;
        ch = EVENPARITY( getchar() );
        printf( "ch = %d, '5' = %d\n", ch, '5' );
        
        return EXIT_SUCCESS;
    }
    /* 输出:

    */ 

        14.2.5 命名约定
        #define宏的行为和真正的函数相比存在一些不同的地方,表14.2对此进行了总结。由于这些不同之处,因此让程序员知道一个标识符究竟是一个宏还是一个函数是非常重要的。不幸的是,使用宏的语法和使用函数的语法是完全一样的,所以语言本身并不能帮助你区分这两者。
        提示:
        为宏定义(对于绝大多数由#define定义的符号也是如此)采纳一种命名约定是很重要的,上面这种混淆就是促使人们这样做的原因之一。一个常见的约定就是把宏名字全部大写。在下面这条语句的所有头文件来找出它的真实身份。另外,请看下面这条语句
        value = MAX( a, b );
        命名约定使MAX的身份一清二楚。如果宏名可能具有副作用的参数,这个约定尤为重要,因为它可以提醒程序员在使用宏之前先把参数存储到临时变量中。
    表14.2                                                                                        

    宏和函数的不同之处
    属性#define宏函数
    代码长度每次使用时,宏代码都被插入到程序中。除了非常小的宏,程序的长度将大幅增长。函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码。 
    执行速度更快。存在函数调用/返回的额外开销。
    操作符优先级宏参数的求值是在所有周围表达式的上下文环境里,除非它们加上括号,否则邻近操作符的优先级可能会产生不可预料的结果。函数参数只在函数调用时求值一次,它的结果值传递给函数。 表达式的求值结果更容易预测。 
    参数求值参数每次用于宏定义时,它们都将重新求值。由于多次求值,具有副作用的参数可能会产生不可预料的结果。 参数在函数被调用前只求值一次。在函数中多次使用参数并不会导致多个求值过程。参数的副作用并不会造成任何特殊的问题。 
    参数类型宏与类型无关。只要对参数的操作是合法的,它可以用于任何参数类型。函数的参数是与类型有关的。如果参数的类型不同,就需要使用不同的函数,即使它们执行的任务是相同的。 

        14.2.6 #undef
        #undef预处理指令用于移除一个宏定义:
        #undef name
        如果一个现存的名字需要被重新定义,那么首先必须用#undef移除它的旧定义。

    /*
    ** #undef。 
    */
    #include <stdio.h>
    #include <stdlib.h>

    #define ARR_LEN 5
    #undef ARR_LEN
    #define ARR_LEN 10
    int main( void ){
        printf( "ARR_LEN = %d\n", ARR_LEN );
        
        return EXIT_SUCCESS;
    }
    /* 输出:

    */ 

        14.2.7 命令行定义
        许多C编译器提供了一种能力,允许在命令行中定义符号,用于启动编译过程。当根据同一个源文件编译一个程序的不同版本时,这个特性是很有用的。例如,假定某个程序声明了一个某种长度的数组。如果某个机器的内存很有限,这个数组必须很小,但在另一个内存充裕的机器上,你可能希望数组能够大一些。如果数组是用类似下面的形式进行声明的:
        int array[ARRAY_SIZE];
        那么,在编译程序时,ARRAY_SIZE的值可以在命令行中指定。
        在UNIX编译器中,-D选项可以完成这项任务。我们可以用如下两种方式使用这个选项:
        -Dname
        -Dname=stuff
        第1中形式定义了符号name,它的值为1。第2中形式把该符号的值定义为等号后面的stuff。用于MS-DOS的Borland C编译器使用相同的语法提供相同的功能。请查阅你的编译器文档,获取和系统有关的信息。
        回到这个例子。在UNIX系统中,编译这个程序的命令行可能是下面这个样子:
        cc -DARRAY_SIZE=100 prog.c
        这个例子说明了在程序中使用诸如数组长度这样的参数化量的另一个好处。如果在数组的声明中,它的长度以字面值常量的形式给出,如果需要在循环内部用一个字面值常量作为限量访问数组,这种技巧就无法使用。在需要引用数组长度的地方,都必须使用符号常量。
        提供符号命令行定义的编译器通常也提供在命令行中去除符号的定义。在UNIX编译器上,-U选项用于执行这项任务。指定-Uname将导致程序中符号name的初始定义被忽略。当它与条件编译结合使用时,这个特性是很有用的。 

    /*
    ** 命令行定义。 
    */
    #include <stdio.h>
    #include <stdlib.h>
     
    int main( void ){
        int array[ARRAY_SIZE];
        printf( "ARRAY_SIZE = %d\n", ARRAY_SIZE );

        return EXIT_SUCCESS;
    }
    /* 输出:

    */

    /* -Uname有兴趣自己可以试试看呢 */ 

  • 相关阅读:
    聊聊Mybatis的Executor之CachingExecutor
    【C语言】-动态内存管理
    【计算机网络】 TCP协议总结
    OS操作系统——设备管理(测试习题)
    Mygin上下文之sync.Pool复用
    基于python下django框架 实现校园教室图书馆座位预约系统详细设计
    皓量科技入选《中国数字营销生态图2022版》4大赛道!
    SPP-学习笔记
    基于vue+node+MySQL的导航可视化系统webapp设计
    取色器实战(Qt含源码)
  • 原文地址:https://blog.csdn.net/weixin_40186813/article/details/125608401