欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

c进阶->预编译器->define指令,宏的使用方式,##结构,宏与函数

程序员文章站 2022-05-12 09:17:02
...

c进阶->预编译器->define指令,宏的使用方式,##结构,宏与函数

1)预编译器

编译C语言程序的第一个步骤称为预处理阶段,C预处理器会在源代码编译时进行文本性的一些操作,主要任务包括:

  1. 删除注释
  2. 插入被#include 指令包含的文件内容
  3. 定义和替换由#define指令定义的符号
  4. 确定代码的部分内容是否应该根据一些编译指令进行编译

一、预处理符号

符号 样例值 含义
_ _ FILE _ _ “name.c” 进行编译的源文件名
_ _ LINE _ _ 25 文件当前行的行号
_ _ DATE _ _ “Jan 31 1997” 文件被编译的日期
_ _ TIME _ _ “18:04:30” 文件被编译的时间
_ _ STDC _ _ 1 如果编译器遵循 ANSI C ,其值就为1,否则就未定义

二、#define 指令

#define name stuff

指令含义:有了这条指令,每当有符号name出现在这条指令后面时,预处理器就会把它替换成stuff

注意这里是文本性质的操作指令,而替换文本也并不仅限于字面值常量,使用#define可以将任何的文本替换到程序中。

例:

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

#define reg register:(相当于我定义了一个reg,实际上这个的使用是为了排列,但是我的功能目的是为了使用register,所以在编写的时候为了我的编写方便我可以使用reg,但是编译的时候就必须转换成register才能使得计算机识别)这个定义其实是为关键字register 创建一个简短的别名,通过使用这个较短的别名使得各个声明更容易通过制表符进行排列。

#define do_forever for( ; ; ):(这里也是为了方便使用do_forever 的目的是为了使用一个无限循环,为了写代码时的方便,然后在编译的时候也是为了机器识别就必须替换掉)使用一个更加具有描述性的符号来代替一种用于实现无限循环的for语句类型。

**#define CASE break;case:**定义了一种简短记法,以便在switch语句中进行使用,它自动的把一个break放到每个case之前,使得switch语句看上去很像其他语言的case语句

如果定义的stuff过长时,可以分成几行,除了最后一行外,每行的末尾都要加一个反斜杠

#define DEBUG_PRINT printf("FILE %s line %d:"\
                            "x = %d,y = %d ,z = %d",\
                            __FILE__,__LINE__,\
                            x,y,z)

也就是说在stuff(翻译过来时填充物)中可以使用反斜杠作为行连接符

利用相邻的字符串常量被自动连接为一个字符串这个特性,当你调试时,这种类型的声明非常的有用,可以很容易的插入一条调试语句打印出值。

示例:

#include<stdio.h>

#define DEBUG_PRINT printf("FILE %s line %d:"\
                            "x = %d,y = %d ,z = %d",\
                            __FILE__,__LINE__,\
                            x,y,z)
int main(){
    int x = 1,y = 2,z =3;
    x *= 2;
    y += x;
    z = x * y ;
    DEBUG_PRINT
}

打印输出:

c进阶->预编译器->define指令,宏的使用方式,##结构,宏与函数

**注意:**这里在使用DEBUG_PRINT时会自动在它的后面加一个分号,所以不应该在宏定义的尾部加上分号,如果这样做就会产生两条语句,一条是 print语句 再加 一条空语句。有些场合只允许出现一条语句,所以在宏定义编写时不能加分号,在使用定义的name的时候也不能加分号,例如这样的场合:

if(.....)
	DEBUG_PRINT;
else 
	....

在这种情况下使用宏定义还加了分号就会报错

同时也可以使用#define 指令把一个序列语句插入到程序中,比如说:

#define PROCESS_LOOP          \
			  for(int i = 0 ; i < 10 ; i+=1){    \
			  			sum += i;                      \
			  			if( i > 0)                        \
			  				prod *= 1;               \
			  }

注意:

需要注意的是,尽量不要滥用这种技巧,如果相同的代码需要出现在程序的几个地方,通常更好的办法就是把它实现为一个函数

三、宏

**什么是宏/定义宏:**是#define 的一个机制,允许将参数替换到文本中

#define name(parameter-list stuff)

parameter-list(参数列表)是一个由逗号分隔的符号列表,它们可能出现在stuff中。参数列表的左括号必须与name紧邻。如果两者中间有任何空白存在,参数列表就会被解释为stuff的一部分。

宏被调用时,名字后面是一个逗号分隔的值的列表,每个值都与宏定义的一个参数相对应,整个列表用一对括号包围,当参数出现在程序中时,与每个参数对应的实际值都将被替换到stuff中。

示例:

#include<stdio.h>

#define SQUARE(x)  x*x

int main(){
    printf("%d",SQUARE(5));
}

分析:一开始定义了一个宏

#define SQUARE(x) x*x

它的参数列表就是x,而对应的stuff就是x*x

在main函数中

SQUARE(5)

将5作为参数传入宏中,预编译时将这里的内容替换成了5*5

但是这里的代码有个问题

示例:

#include<stdio.h>

#define SQUARE(x)  x*x

int main(){
    int a = 5;
    printf("%d",SQUARE(a+1));
}

这里的话如果用函数的思想去想最后会输出36

但是实际上这里输出的值是11

分析:为什么本应该输出36的一串代码最后却只能输出11呢?

我首先想到的这里的原因就是因为宏定义的本质也是文本的替换,如果作为文本替换的话,这里代码替换过来就应该是

int main(){
    int a = 5;
    printf("%d",a+1*a+1);
}

而在实际的运算中是会考虑相应的运算符的优先级的,所以这里才会出现输出是11而不是36的结果

所以说无论是宏定义的核心还是#define定义的核心其本质都是文本的替换,那么真正达到初始目的代码应该是:

#include<stdio.h>

#define SQUARE(x)  (x)*(x)

int main(){
    int a = 5;
    printf("%d",SQUARE(a+1));
}

示例2:

#include<stdio.h>

#define DOUBLE(x)  (x)+(x)

int main(){
    int a = 5;
    printf("%d",10*DOUBLE(a));
}

分析:

这段代码好像是要打印100,但是实际上是输出了55,观察宏定义替换之后的文本

#include<stdio.h>

#define DOUBLE(x)  (x)+(x)

int main(){
    int a = 5;
    printf("%d",10*(a)+(a));
}

由此可以看出原因,所以修改之后达到目的代码是:

#include<stdio.h>

#define DOUBLE(x)  ((x)+(x))

int main(){
    int a = 5;
    printf("%d",10*DOUBLE(a));
}

这里要说明的其实就是:所有用于数值表达式进行求值的宏定义都应该尽能的区分运算加上括号避免在使用宏时,由于参数中的操作符或邻近的操作符之间因为优先级等顺序问题造成的相互作用的效果

宏的另一种使用方式:

示例:


#define repeat do
#define until(x) while(!(x))
int main(){
    int i = 0;
    repeat{
        printf("A new looping statements!");
    } until(i>10)}

通过这两个宏定义就好像创建了一种新的循环一样,其工作过程类似于其他语言中的repeat/until循环,它按照这样的方式使用时,预处理器会将文本替换成下面的代码:

#include "stdio.h"

#define repeat do
#define until(x) while(!(x))
int main(){
    int i = 0;
    do {
        printf("A new looping statements!");
    } while (!(i>10));
}

但是这样的使用好像就是在用其他的语言编写C程序,但是这种使用不应该广泛允许,因为这样编写出来的代码程序员很难理解,他们必须查看这些宏的定义以便能弄清实际代码是什么意思

#define 替换文本的过程

在程序中替换#define 定义的符号和宏时,需要涉及几个步骤

  1. 在调用宏时,首先对参数进行检查,看看是否包含了任何由#define 定义的符号,如果包含则他们首先会被替换掉
  2. 替换文本随后被插入到程序中原来文本的位置,对于宏,参数名被他们的值所取代
  3. 最后对结果文本进行扫描,看看它是否包含了任何由#define 定义的符号,如果是,则重复上述处理过程

简而言之,就是在#define 定义的宏或者是符号中 可以由其他#define 中定义的符号,但是不能在定义中调用自身,也就是说不能递归使用。

宏参数插入字符串的技巧

因为当预处理器搜索#define 定义的符号时,字符串常量的内容并不进行检查,如果想把宏参数插入到字符串常量中,可以使用两种技巧:

  1. 技巧一

    1. 根据:根据邻近字符串自动连接的特性

    2. 作用:这样可以将同一个字符串割成几部分,让每一个部分都是一共宏参数

    3. 示例:

      #include "stdio.h"
      
      #define PRINT(FORMAT,VALUE) printf("The value is"FORMAT"\n",VALUE)
      int main(){
          int x = 0;
          PRINT("%d",x+3);
      }
      
    4. 分析:实际上是定义了宏FORMAT 和 VALUE 参数,其实对应的就是一部分在字符串中的%d,和一部分值value,通过字符串自动相邻相连的特性将目标参数添加到指定的字符串中

  2. 技巧二

    1. 根据:#argument 这种结构被预处理器翻译成字符串 “argument”

    2. 作用:这样就可以达到通过预处理器把一个宏参数转换成一个字符串

    3. 示例:

      #include "stdio.h"
      
      #define PRINT(FORMAT,VALUE) printf("The value of " #VALUE \
                                          " is " FORMAT "\n",VALUE)
      int main(){
          int x = 0;
          PRINT("%d",x+3);
      }
      
    4. 分析:本质上就是#宏参数 会将宏参数以字符串的形式输出

##结构

它的作用比较特殊,它把位于它两边的符号连接成一个符号,作为用途之一,它允许宏定义从分离的文本片段创建标识符。

示例:

#include "stdio.h"

#define ADD_TO_SUM(sum_number,value) \
                    sum ## sum_number += value
int main(){
    int sum5 = 0;
    ADD_TO_SUM(5,25);
    printf("%d",sum5);
}

分析:也就是说通过##符号可以灵活的连接文本内容,比如上面就产生了一个标识符,但是必须是一个已定义的合法标识符,否则它的结果就是未定义的。

四、宏与函数

宏和函数使用的比较:

宏的适用场景:

  1. 频繁的用于执行简单的计算

    1. 示例:#define MAX(a,b) ((a) > (b) ? (a) : (b))
    2. 分析:对于这一段代码来说如果用函数去编写的话其调用和从函数返回的代码可能比执行这个小型的运算工作的代码更大,所以使用宏比使用函数在程序的规模和速度方面都更胜一筹
  2. 不需要担心参数的类型

    1. 分析:对于函数的构建必须要凌驾到一种特定的类型之上,但是宏的参数和类型无关,或者说宏本身就与类型无关
  3. 完成一些函数不能完成的任务

    1. 示例:

      #define MALLOC(n  , type) \
                                ((type *)malloc((n)* sizeof(type)))
      
      pi = MALLOC(25,int);
      pi = ((int *) malloc ((25) * sizeof(int)));
      
    2. 分析:很明显,在malloc函数使用的时候必须要指定一个type也就是代表数据类型的关键字,但是如果通过函数来完成这个功能很明显是无法通过函数参数进行传递进而实现的

  4. 使用的宏的一点不利就在,每一次宏调用都会将一份宏定义代码拷贝到程序中,除非宏非常短,否则就会大幅度的增加程序的长度

同样宏定义并没有用一个分号结尾,分号出现在调用这个宏的语句中

相关标签: C进阶 c语言