预处理指令

预处理指令是以#号开头的代码行,# 号必须是该行除了任何空白字符外的第一个字符。# 后是指令关键字,在关键字和 # 号之间允许存在任意个数的空白字符,整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码做某些转换。 预处理指令只能占一行,但是在写代码时可以用'\'分隔多行,但处理时仍会将这多行合为一行。有些指令带参数,参数需与指令由空白字符分隔。

主要作用:

  • 引入头文件
  • 宏展开
  • 条件编译
  • line control
  • 诊断: 可在编译器检查程序时,发出errors或者warnings

头文件

用#include 包含文件, 有两种形式:

  • #include <file>
    • 用于系统文件,Preprocessor将在标准文件目录下搜寻文件file
    • linux中一般在/usr/include 目录下
    • 可以用编译器的 -I 选项来将目录添加到这个list
  • #include “file”
    • 用于程序自身的头文件。搜索路径如下:
      • 先在包含该文件的当前目录下搜索
      • 然后在引用的目录下搜索,可以用编译器的-iquote选项来将目录添加到quote directories中
      • 最后在标准目录下搜索

如果一个头文件被include两次,编译器就会处理两次,因此可能会出错,如重定义等等,标准做法是用所谓的wrapper #ifndef将头文件的内容包起来

#ifndef _MY_H
#define _MY_H

extern int a;

#endif

代码片段中的宏_MY_H叫做控制宏或者防护宏, 在系统头文件中,该宏的名字需要以__(双下划线)开头以免与用户程序头文件冲突。在任意类型的头文件中,该宏的名字应该包含头文件文件名再加上额外的文字以避免与其他头文件冲突。

宏(Macros)

宏是赋予名字的一段代码,每次使用时都将名字替换成宏内容。宏分为两种,它们在使用时有很大的不同:

  • Object-like macros:使用时像用data objects一样,
  • Function-like macros:使用时像函数调用一样

对象形式的宏

#define NAME macro_body

宏展开是递归进行的,preprocessor将一个宏展开后会接着处理展开后的结果,如果这里面有其他的宏,会继续展开下去。但是如果结果里面再次出现刚刚展开的这个宏的话将不会展开第二次,以免出现无限递归的情况 。

函数形式的宏

#define name(params_list) body
注意,小括号()必须和宏的名字连在一起,否则会被当成object-like宏来展开,同时,在使用时也必须用name()的形式(此时name和()间可以有空格),只用name的话不会被展开

  • 如果有参数,则注意参数数量必须与定义时的数量一致

  • 在带参宏定义中,形参不分配内存单元,因此不必作类型定义。而宏调用中的实参有具体的值,要用它们去代换形参,因此必须作类型说明,这点与函数不同。函数中形参和实参是两个不同的量,各有自己的作用域,调用时要把实参值赋予形参,进行“值传递”。而在带参宏中只是符号代换,不存在值传递问题。

  • 宏定义时建议所有的层次都要加括号, 如 #define SQ(r) ((r)*(r))

#define min(X, Y) ((X) < (Y) ? (X) : (Y))

使用时:

>x = min(a, b);          →  x = ((a) < (b) ? (a) : (b));

y = min(1, 2); → y = ((1) < (2) ? (1) : (2));
z = min(a + 28, p); → z = ((a + 28) < (p) ? (a + 28) : (*p));

  • 当参数有一个前导#时,preprocessor会将其替换为实参,再转换成字符串常量**

  • 预处理操作符##用于在宏body中将两个tokens拼在一起,如A ## B将展开为AB**

  • #undef name用于取消宏定义,name可以是object-like宏的名字,或者是function-like宏的名字(不用加小括号以及参数列表)

使用宏定义交换2个数

#define TSWAP(type, x, y) do{ \
    type _y = y; \
    y = x;       \
    x = _y;      \
}while(0)
#define SWAP(x, y) do{ \
    x = x + y;   \
    y = x - y;   \
    x = x - y;   \
}while(0)

int main(void){
    int a = 10, b = 5;
    TSWAP(int, a, b);
    printf(“a=%d, b=%d\n”, a, b);
    return 0;
}

条件编译

使用场景:

  • 根据机器架构或操作系统的不同使用不同的代码;
  • 将原文件编译成两个不同的程序,其中一个版本可能会用于输出一些data进行debugging等等;
  • 使用#if 0来将排除一段代码,但将其保留在源文件中用作注释。

#ifdef 形式

#ifdef 标识符 (或 #if defined 标识符)
    程序段1
#else 
    程序段2
#endif

如果定义了标识符,则编译程序段1, 否则编译程序段2

#ifndef 形式

#ifndef 标识符 (或 #if !(defined 标志符)
    程序段1
#else
    程序段2
#endif

如果没有定义标志符,则编译程序段1, 否则编译程序段2

#if 形式

#if 常量表达式
    程序段1
#else 
    程序段2
#endif

如果常量表达式的值为真(非0),则对程序段1 进行编译,否则对程序段2进行编译

表达式可以是以下几种:

  • 整形常量
  • 字符常量
  • 数学运算表达式和逻辑运算表达式(遵循短路求值)
  • 宏,在计算宏所代表的表达式前将先展开所有的宏
  • defined预处理指令
  • 所有不是宏的标志符都视为数字0,函数形式的宏但没有调用实参列表也视为0

诊断信息

  • #error 导致preprocessor产生一个fatal error,#error所在行的剩余tokens组成错误信息
  • #warning导致preprocessor产生一个warning并继续预处理,#warning所在行的剩余tokens组成错误信息
#if  defined MACRO
    #error defined MACRO
#endif

pragma

用于指示编译器完成一些特定的动作, 在处理阶段进行, 不同的编译器对其作用不同,因此比较少用。

常用的有:

  • #pragma once 用于保证头文件只被编译一次
#inlcude <stdio.h>
#include <test.h>
#include <test.h>

int main(){
....
}
#pragma once 

int a =10;
  • #pragma pack 用于指定内存对齐(一般用在结构体)
  • #pragma comment 将一个注释记录放入一个对象文件或可执行文件中,如 \pragma comment(“lib”, “user32.lib”) 表示将user32.lib库文件加入到本工程中
文档更新时间: 2021-03-10 10:15   作者:周国强