C-09\编译预处理

一、预处理

  • C语言在对源程序进行正常编译之前,会先对一些特殊的预处理命令作解释,产生一个新的源程序,该过程称为编译预处理
  • 为了区分预处理命令和一般的C语句,所有预处理命令行都以"#"开头,并且结尾不用分号
  • 预处理命令可以出现在程序的任何位置
  • C语言提供的预处理命令主要有三类
    • 宏定义
    • 文件包含
    • 条件编译

二、文件包含

  • 命令格式:#include <文件名>#include "文件名"

  • 作用:通知预处理器将指定文件的内容包含在指令出现的位置,也就是直接复制文件内容替换该指令

  • 两者区别:

    • <>:先根据每个/I编译器选项指定的路径查找,然后再根据环境变量include命令设置的路径去找

    • ""

      • 如果文件名给定的是全路径,则只搜索该路径
      • 如果是相对路径,则先在源文件所在的目录查找,然后根据每个/I编译器选项指定的路径查找,最后再根据环境变量include命令设置的路径去找
    • vs开发环境中,将忽略环境变量,改为使用"项目属性"的”包含目录“中指定的值

三、宏

  • 宏定义分为不带参数的宏定义和带参数的宏定义
  • 区别于函数,其标识符一般用大写,多个单词用_隔开
  • 程序中用双引号或单引号括起来的字符串内的字符,不会进行宏的替换操作

不带参宏

  • 定义:

    #define 标识符 语句序列或表达式
    
  • 作用:在编译预处理时,将源程序中所有标识符替换成语句序列

    #define PR printf	// 定义后面出现的所有 PR 标识符在编译预处理时将被 printf 代替
    #define N 2			// 定义后面出现的所有 N 标识符在编译预处理时将被 2 代替
    
  • 注意:

    • 编译预处理时宏名与字符串进行替换时,不作语法检查,只是简单的字符替换,只有在编译时才对已经展开的宏名的源程序进行语法检查

    • 宏名的有效范围从欧诺个定义位置到文件结束

    • 可以使用#undef提前终止宏定义的作用域

      #undef 标识符
      

带参宏

  • 定义:

    #define 标识符(参数表) 字符串
    
  • 作用:在编译预处理时,将源程序中所有标识符替换成字符串,并将字符串中的参数用实际使用的参数替换

    #define S(a,b,c) (a+b+c)/2	// 后面如果使用了 S(3,4,5), 在编译预处理时将替换为 (3+4+5)/2
    
  • 展开顺序

    1. 首先用实参代替形参,将实参代入宏文本中

    2. 如果实参也是宏,则展开实参

    3. 最后继续处理宏替换后的宏文本,如果仍包含宏,则继续展开

    4. 注意:如果在第2步中,实参代入宏文本后,实参之前或之后遇到###操作符,实参不再展开

      #define _T(x) L##x
      #define __T(x) _T(x)
      一般使用下面的 __T(x) 防止宏套宏
          
      #define MYSTR "Hello world"
      
      int main()
      {
          printf(_T(MYSTR));	// 这样编译后就成为 LMYSTR
          printf(__T(MYSTR)); // 这样编译后就正确成为 L"Hello world"
      }
      
  • 注意:

    • 定义时,宏名和参数表之间不能有空格,否则空格后面的所有字符序列都作为替换的字符串

    • 宏展开时,只做简单的字符和参数的替换,不进行任何计算操作。因此为了防止某些场合得出错误的结论,一般在定义宏时,字符串中的形式参数都会加一个小括号,且整个字符串也会加一个小括号

      #define GETAREA(R) R * R * 3.14
      // GETAREA(3 + 5)  处理后 3 + 5 * 3 + 5 * 3.14		可以看出处理后的结果并不是我们要的
      
      #define GETAREA(R) (R) + (R) + 3.14
      // GETAREA(3 + 5) * 6  处理后 (3 + 5) + (3 + 5) + 3.14 * 6		可以看出处理后的结果并不是我们要的
      

特殊符号

  • #字符串化操作符

    • 作用:将宏定义中的传入参数名转换为一堆双引号括起来的参数名字符串

      define DEF_STRING(s) #s		// DEF_STRING(hello world) 会被转换为"hello world"字符串
      
    • 注意:

      • 忽略传入参数名前面和后面的空格

        DEF_STRING(  hello world  ) 将被扩展为 "hello world"
        
      • 当传入参数名中间存在空格时,编译器将会自动连接各个子字符串,用每个子字符串中只以一个空格连接,忽略其中多余一个的空格

        DEF_STRING(hello      world) 将被扩展为 "hello world"
        
  • ##:符号连接操作符

    • 作用:将符号前后的两个字符串拼接起来

      #define NAME_CAT(n) qls##n	// ##会连接前后的		NAME_CAT(hl) 会被转换为 qlshl
      
    • 注意:

      • ##前后的空格没有影响

      • 连接后的符号必须已经存在或是编译器已知的宏定义

        #define NAME_CAT(n) qls##n
        
        int qlsHL = 1;		// 如果去掉这句,会报错,找不到 qlsHL 符号
        
        int main(int argc, char* argv[])
        {
            NAME_CAT(HL);
            printf("Hello World!\n");
            return 0;
        }
        
  • #@:字符化操作符

    • 作用:将传的单字符参数名转换成字符,以一对单引号括起来

      #define makechar(x) #@x
      a = makechar(b)				// 展开后变成了 	a = 'b'
      
  • \:续行符

    • 作用:当定义的宏不能用一行表达完整时,可以用 \表示下一行继续此宏的定义,例如下面的例子,方便代码阅读

      #define DEF_ADD(type) \
      type type##Add(type x,type y) \
      { \
          return x+y; \
      }
      

功能分类

表达式宏

  • 作用:用来描述一个表达式的一样,增加可读性

    #define GETAREA(R) ((R) * (R) * 3.14)			// 求圆的面积的表达式
    

符号名称宏

  • 作用:用来描述一个常量值,增加可读性

    #define PI 3.14
    #define FILE_CLOSE 0x80000001
    #define FILE_OPEN  0x80000002
    

语句块宏

  • 作用:类似于函数,用来完成某项功能

    #define SHOW_MSG(s) printf(s);printf("\r\n")
    
  • 注意:如果在条件判断或循环语句块中使用语句块宏,带上{},否则可能会得到错误的结论

    for(int i = 0; i < 5; i++)
    	SHOW_MSG("hello,world");	// 此处不用 {} 括起来,则会连续打印五次后再换行
    

兼容性宏

  • 作用:让代码兼容各种平台或编译器版本

    #define for if(1)for			// for(int i = 0; i < 5; i++)
    #define for if(0){}else for
    // 因为vc++6.0中 for(int i = 0;i<5;i++)中i没不是在语句块内,而高版本修复了,所以为了兼容可以使用此宏,将for语句变为if的语句块,从而将i的作用域变为块作用域
    
    #define INT int				// 当觉得 int 不好用时,可以直接修改宏就好了
    #define near				// 为了兼容以前的代码,以前16位的指针分短指针(2字节)和长指针(4字节)
    #define far					// 为了兼容以前的代码,以前16位的指针分短指针(2字节)和长指针(4字节)
    

说明性宏

  • 作用:起到说明的作用,便于阅读,对代码无影响

    #define IN			// 说明是一个输入参数
    #define OUT			// 说明是一个输出参数
    #define AFXMSG		// 说明是一个消息
    
    void foo(IN int nCount, OUT int *pout)
    {
        int AFXMSG nMsg;
    }
    

编译选项宏

  • 作用:用于区别不同的编译环境,配合条件编译使用

    // 在命令选项中使用下面命令,则程序起来的时候就会包含该宏名
    /D"宏名"		// 例如: /D"ISDEBUG"
    

编译器内置宏(厂商相关)

  • 作用:编译器自己内置的一些宏,可以获取当前编译器的相关信息以及当前工程的相关信息

    _MSC_VER	// 编译器高版本号
    __FILE__	// 当前文件的全路径
    __LINE__	// 当前代码行数
    

条件宏

  • 作用:用于区别不同条件下代码的编译,配合条件编译使用

    #define ISDEBUG
    
    int main()
    {
        int nSum = 0;
        for(int i = 1; i <= 100; i++)
        {
    
    #ifdef ISDEBUG
            printf("%d\r\n", nSum);
    #else
    
    #endif
            nSum = nSum + i; 
        }
    
        return 0;
    }
    

宏的花样玩法

宏实现自动识别类型

#define SET_NAME(s) #s
#define DEF_ARRAY(type,ary,nCount,size) type ary[nCount][size] = {SET_NAME(ary)}

int main()
{
    DEF_ARRAY(char,ary,5,20);
    return 0;
}

宏实现模板

#define DEF_ADD(type) \
type type##Add(type x,type y) \
{ \
    return x+y; \
}
#define CALL_ADD(type,x,y) type##Add(x,y)

DEF_ADD(int);
DEF_ADD(float);
DEF_ADD(double);

int main()
{
    int n = CALL_ADD(int,2,4);
    float f = CALL_ADD(float,2.0,3.0);
    double dbl = CALL_ADD(double,2.0,4.0);
    
    return 0;
}

四、条件编译

#define     		定义宏
#undef      		取消已定义的宏
#if         		如果给定条件为真,则编译下面代码
#ifdef      		如果宏已经定义,则编译下面代码
#if defined(宏名)		如果宏已经定义,则编译下面代码
#ifndef     		如果宏没有定义,则编译下面代码
#if !defined(宏名)	如果宏没有定义,则编译下面代码
#elif       		如果前面的#if给定条件不为真,当前条件为真,则编译下面代码,其实就是else if的简写
#endif      		结束一个#if……#else条件编译块
#error     	 		停止编译并显示错误信息

五、重复包含的解决办法

// MY_LIB_H 为文件名,但是单使用文件名,可能会和别人的一样,所以在后面加上全球唯一标识保存宏名的唯一性(使用guidgen获取全球唯一标识)
#ifndef MY_LIB_H_1EF0DD15_D807_4ed9_85EE_02962DBCBAA4
#define MY_LIB_H_1EF0DD15_D807_4ed9_85EE_02962DBCBAA4

#endif	// #ifndef MY_LIB_H_1EF0DD15_D807_4ed9_85EE_02962DBCBAA4

posted @ 2022-03-22 22:28  逸聆君  阅读(171)  评论(0编辑  收藏  举报