1.1 预处理器介绍

C预处理器是多种计算机编程语言(如C、Objective-C、C++和各种Fortran语言)的宏预处理器。预处理器提供头文件、宏扩展、条件编译和行控制等操作。

预处理器指令的语言与C语言的语法关系不大,因此有时用于处理其他类型的文本文件。

1.11 预处理器历史

预处理器于1973年左右,在Alan Snyder的敦促下引入到C语言中,也是为了认识到BCPL和PL/I中可用的文件包含机制的有用性。它的原始版本仅提供文件包含和简单的字符串替换,也就是使用#include#define来进行。不久之后,它首先由Mike Lesk扩展,然后由John Reiser扩展,以将宏与参数和条件编译合并。

C预处理器是贝尔实验室悠久的宏语言传统的一部分,该传统由Douglas Eastwood和Douglas McIlroy于1959年创立。

1.12 从代码编辑到程序运行中的过程

c++和c语言一样,从源代码编写到程序运行时,分为四大阶段:

  • 预处理阶段
  • 编译阶段
  • 汇编阶段
  • 链接阶段

执行这四个阶段的程序(预处理器、编译器、汇编器、链接器)一起构成了编译系统。

1.121 预处理阶段

预处理阶段期间,预处理器会根据源文件(.cpp/.c)里的预处理指令,修改源文件(.cpp/.c)中的一些代码,修改完后的所有代码保存在一个预处理文本文件(.i)中。

1.122 编译阶段

编译阶段期间,编译器会将预处理文本文件(.i)里的所有代码翻译成一种能被汇编语言程序所识别的代码,所有翻译后的代码保存在汇编文本文件(.s)中。

1.123 汇编阶段

汇编阶段期间,汇编器将汇编文本文件(.s)翻译成机器代码,也叫做机器语言指令(也就是由二进制值组成的代码,计算器能够识别的代码),并将这些机器语言指令打包成一种叫做可重定位目标程序(relocate object program)的格式,并将结果保存在一个目标文件(.obj/.o)中。

1.124 链接阶段

通常我们所用的程序并不单单是由一个源文件的代码转换而来。 绝大多数都是由多个源文件的代码组合而来。所以就有了链接阶段。 链接阶段期间,链接器将会把所有程序代码转换而成的目标文件(.obj/.o)和可能要用到静态库文件的某些函数的部分(.lib/.a)(一些由常用代码的目标文件打包而成的二进制文件)链接到一起(如果要用到动态库文件,则会对这些用到的代码位置做上标记,而不会直接合在一起),生成一个操作系统能够运行的程序文件,也叫做可执行目标文件(.exe/.out)。


虽然编程时可以不需要预处理阶段的指令就能够生成出可执行程序文件了。但实际开发中,常常需要在编译阶段前对源文件进行一些简单的处理,比如替换一些代码,删除一些代码等操作。

例如,我们希望自己的程序在Windows和Linux下都能够运行,那么就要在Windows下使用VS编译一遍,然后在Linux下使用GCC编译一遍。但是现在有个问题,程序中要实现的某个功能在VS和GCC下使用的函数不同(假设VS下使用a(),GCC下使用b()),怎么办呢? 这就需要在编译之前先对源文件进行处理:如果检测到是VS,就保留a()删除b();如果检测到是GCC,就保留b()删除a()。

c提供了一些预处理功能,也就叫预处理指令。能够让我们在预处理阶段对代码进行一些处理工作,c++也继承了这些功能。

1.13 预处理阶段

预处理过程由C标准中规定的前四个(共八个)翻译阶段所定义:

  • 三元组替换(Trigraph replacement):预处理器将所有三元组序列替换为它们所表示的字符。此阶段将会在C23标准中被删除。
  • 行拼接(Line splicing):结尾带有\字符的原始的源码行将被拼接成一起,形成逻辑行。
  • 符号化(Tokenization):预处理器将上一步的结果分解为预处理符号和空格。并将所有注释都替换为空格。
  • 宏扩展和预处理指令处理(Macro expansion and directive handling):执行预处理指令行(#line),文件包含(#include)和条件编译(#if, #else, …)。预处理器同时也会进行宏扩展,并处理_Pragma运算符(从1999年的C标准版本起)。

1.2 预处理语法介绍

在介绍预处理功能之前,我们需要了解一些概念。 c++语言中的代码文本,都是由两大部分组成:

  • 词法元素
  • 空白符

1.21 词法元素

词法元素(token)是c++程序中的==基本意义单元==,所有能被预处理器和编译器==识别==的==可见符号==都是词法元素,比如变量名,数据类型,各种语言的关键字等。

词法元素是由一个或多个空白符分隔来区分的。 当两个词法元素之间没有空白符时,预处理器和编译器会认为这是一个词法元素。

词法元素是指有其==自身意义==的==可见==符号单元,所以空白符不是词法元素,字符、字符串常量、字符串字面值和注释内的所有文本都不是词法元素。

词法元素分为以下几种:

  • 标识符(Identifiers)
    • 关键字(Keywords)
  • 字面值常量(Literals)
  • 标点符号(Punctuators)
    • 运算符符号(Operators)
1.211 标识符

标识符就是用于表示各种对象,实体,操作等事物的==字符序列==(sequence of characters)。

标识符可以用于表示以下这几种事物:

  1. 对象或变量的名称
  2. 类型、结构体、联合类和枚举类的名称以及它们成员的名称
  3. 函数名称以及其参数的名称
  4. 类型别名
  5. 标签名称
  6. 宏名称以及其参数的名称

同一作用域下的某些事物的名称必须有所区别:

  • 各种类型的变量名要和函数名有所区别
  • 各种类型的变量名之间要有所区别
  • 各种类型名之间要有所区别

标识符只能由==字母、数字、下划线_和美元符号$组成==,其中必须以字母、下划线或者美元符号开头。标识符的长度没有限制,但是对大小写字母敏感。

同时C++语言也保留了一些名字供语言本身使用,这些名字也叫做关键字,关键字不能被当做名称来使用。

关键字

关键字也就是在c++中被预先定义好了的一些标识符,这些关键字用在c++中各种操作中,如class, if, else等都属于关键字。 以下是c++的关键字: Keywords

1.212 字面值常量

字面值常量是一种可以直接表示值的词法元素,字面值常量的类型分为以下几种,之后我们会详细介绍字面值常量。

  • 整型字面值
  • 浮点型字面值
  • 字符字面值
  • 字符串字面值
  • 布尔字面值
  • 指针字面值
  • 自定义字面值
1.213 标点符号

标点符号也是C++中的一种词法元素。之后会详细介绍标点符号。 C++中的标点符号分为两种:

  • 运算符符号
  • 其他符号

对于非运算符符号来说,它们具有语法意义和语义含义,但它们本身不会指定一个产生数值的操作。 而运算符符号是一种有作用对象的特殊符号。运算符符号会根据其作用对象的值来产生一个新的值。

以下为c++的标点符号:

! % ^ & * ( ) - + = { } ~ [ ] \ ; ‘ : “ < > ? , . / #

标点符号 []、 ( ) 和 {} 必须成对出现

要想某个标点符号不产生其特殊的意义,而只是单纯的当一个符号来使用时(也就是当成一个字符),则需要在其符号之前紧跟一个转义字符\来使其变成普通的字符,形式为: \符号

如: \{ \"

转义字符\本身也可以用这种方式来使用自己的普通字符版本

1.22 空白符

空白符一般是指在c++代码编辑中不可见的符号,如空格,换行等。

空白符会影响代码中的语句分割,从而影响预处理器分析语法,以下是空白符的分类:

  • 空格符(Blanks)
  • 制表符(Tabs)
    • 水平或者垂直制表符(Horizontal or vertical tabs)
  • 换行符(New lines)
  • 换页符(Form feeds)
  • 注释(Comments)
    • 单行注释
    • 多行注释

各种空白符在字符串常量、字符串字面值中没有特殊含义。

1.221 空格符

空格符就是用键盘space键打出来的符号。 在没有明确说明的情况下,各种语句,表达式的词法元素之前、之间和之后可以有一个或多个空格符。

1.222 制表符

制表符就是用键盘tab键打出来的符号。 在大多数代码编辑器的编辑中,一个制表符是由多个空格符代替的。

1.223 换行符

换行符就是用键盘enter键打出来的符号。 在大多数代码编辑器的编辑中,换行符之后也就是另起一行了。

在一般的表达式和语句中,换行符对其没有影响,不会产生切割作用(也就是将其分为两个语句或表达式)。 但在以下情形中,换行符会产生切割作用:

  • 字符串中
  • 单行注释中
  • 宏定义中

要使换行符不产生切割作用,则要在该换行符之前紧跟一个转义字符\来使其变成一个普通的字符

cout << "Im a good
student.";  // 错误:字符串被切割,出错
cout << "Im a good\
student.";  // 正确:没有被切割,还是一个字符串
1.224 换页符
1.225 注释

注释是对于程序员非常有用的文本,通常用于批注代码以供将来参 考。 预处理器会将注释视为多个空格符。

以下是注释的分类:

  • 单行注释
  • 多行注释

注释字符 (/* 、 */ 和 //) 在字符串常量、字符串字面值或注释中没有特殊含义。 多行注释不能嵌套。

单行注释

单行注释是//(两个斜杠,中间不能有空白符)开头的,后面跟任何字符序列的序列。一个单行注释以一个有切割作用的换行符作为结束。

单行注释内的文本可以是任何字符(除了有切割作用的换行符),所以可以嵌套。

// 这是一个单行注释,由换行符结束
// 这是一个单行注释,\
    无切割作用的换行符\
    不会结束一个单行注释。

多行注释

多行注释是由/*(斜杠、星号,中间不能有空白符)开头的,后面跟任何字符序列的序列。多行注释*/(星号、斜杠,中间不能有空白符)作为结束

多行注释内的文本可以是任何字符(除了*/),所以可以跨越多行,但不能嵌套。

// 下面是一个多行注释,由*/结束
/* 这是一个多行注释 */ 

// 错误,多行注释不能嵌套
/* 这是一个多行注释
/*嵌套注释*/ */ 

1.23 预处理指令

c提供了一些预处理功能,也就叫预处理指令。能够让我们在预处理阶段对代码进行一些处理工作,c++也继承了这些功能。

  • 宏定义
  • 条件编译
  • 阻止编译
  • 包含编译
  • 调试操作
  • 杂注操作

预处理指令不是表达式,不是语句,但是所有预处理指令在使用时必须要在一行的开头。

所有预处理指令都可以在任何地方使用(函数内,类内,控制语句块内,命名空间内)。但预处理指令没有局部全局作用域之分。

如无明确说明,预处理指令的作用范围默认为从使用位置后到包含该指令的文件末尾。

预处理指令都是以#开头的。符号#和指令的标识符之间可以有一个或多个空白符。

所有预处理指令和单行注释一样,以有切割作用的换行符作为结束

所有预处理指令内部不能包含有任何其他的预处理指令(包括自身类型的预处理指令)。 也就是预处理指令不能嵌套。

最简单的预处理指令为空指令(Null),使用形式为:

#

它只有一个#,后面不能有任何符号,它没有作用

1.231 宏

宏(Macros),也叫做预处理变量,类似于内联函数。宏是用宏指令定义的标识符或参数化的标识符与词法元素串的关联。

宏的作用就是把其定义位置后的代码中的所有与该宏名相同的标识符(不管在什么位置)替换为其关联的词法元素串,这也称之为宏展开。

1.2311 宏的定义

宏根据定义分为两种:

  • 对象类宏: 对象类宏不包含任何参数, 定义形式为#define 宏名 (可选 词法元素串)
  • 函数类宏: 对象类宏会包含参数(虽然可能为空), 定义形式为#define 宏名(可选 (形参名1, 形参名2)) (可选 词法元素串)

要注意函数类宏的参数表要紧跟宏名,之间不能含有空白符,否则就会被当成对象类宏而出现定义问题

带参数的宏定义形式中的,参数表内只需要填形参名,不同形参名之间必须要用逗号,分隔。

参数表内不需要填也不必填其类型,参数表内不能有默认实参。

每个参数名可以在词法元素串中可以出现0次到多次,并且名称可以按任意顺序出现。

带参数的宏定义形式中的,参数表内还可以用省略符形参。

和普通情况一样,省略符形参只能放在参数表最后一个位置,省略符形参与其他形参可以用逗号,分隔,也可以不用,效果一样。

宏名必须为标识符,且尽量使用大写的字母来命名。 形参名也必须为标识符。

如果一个宏在定义时没有词法元素串,则该宏的作用是将该文件中所有与其相同的标识符删除。

可以定义多个相同宏名的宏,但是宏不能重载,后面定义的宏会覆盖前面定义的同名宏,且预处理器会警告。(不带参数的宏和带参数的宏都适用)。

// 定义了一个宏APPLE
#define APPLE 8
/* 等价于
int var1 = 8;*/
int var1 = APPLE;

// 定义了一个宏FUNC
#define FUNC(x,y) x + y;
/* 等价于
double dou = 3.5 + 8.48;*/
double dou = FUNC(3.5,8.48)

int ins = 66;
cout << ins; // 正确:ins为int的变量,输出66.
#define ins
cout << ins; // 错误:ins已被删除,不存在名为ins的变量

词法元素串

词法元素串是指由一个或多个词法元素(如关键字、变量、表达式或语句)组合而成的序列。

每个词法元素之间也必须有一个或多个空白符分隔,在词法元素串内的空白符和在最后一个词法元素之后的空白符不会被视为词法元素串的一部分。

无特殊说明时,词法元素串中的词法元素可以是任何词法元素。


词法元素串与宏名之间必须有一个或多个空白符分隔。

宏生效于定义时,宏的替换范围为所在文件内。是将其定义位置后的代码中的所有与该宏名相同的标识符(不管在什么位置)替换为其关联的词法元素串。

宏的词法元素串可以包含宏名,也就是可以包含其他宏的名称。该宏生效时会按照宏展开顺序原则依次进行宏展开。

// 定义TYPE宏为int类型
#define TYPE int
// VAR_DEF宏嵌套TYPE宏用于变量定义
#define VAR_DEF TYPE ins = 5;
/*
宏展开顺序为:
1. 先展开VAR_DEF宏,为: TYPE ins = 5;
2. 再展开TYPE宏, 为: int ins = 5;
*/
VAR_DEF
std::cout << ins; // 输出5

宏名和形参名可以是任意的标识符,包括c++的关键字。

// 更改int关键字的含义,将其当做long long类型使用。
#define int long long
std::cout << sizeof(int); // int现在为8字节的long long,所以输出8
1.2312 宏调用

要注意,宏不是对象不是实体,宏只是预处理器在预处理阶段进行的批量替换,也就叫做宏展开。

宏调用(macro invocation)时,要将宏看作为该宏关联的词法元素串代码。每使用一次宏名,==就是在使用的位置上写一遍该宏关联的词法元素串代码==。

// 使用宏DEF_INT时\
等价于int ins = 15;
#define DEF_INT int ins = 15;
DEF_INT
cout << ins; // 输出15

对于使用带参数的宏时,要类似于函数调用的形式:

宏名(实参1, 实参2)

// 定义参数宏FUNC
#define FUNC(x,y) x + y;
int ins = 66;
double dou = 15.14;
// 等价于\
cout << ins + dou;\
输出为81.14
cout << FUNC(ins,dou)

宏调用中的实参标识符可以是空(也就是不填任何符号),也可以是任何词法元素(比如说对象,其他的宏,甚至可以是该宏本身,所以宏可以嵌套使用),该词法元素还可以是不存在于当前文件中的。

要注意非字符串的实参标识符中不能直接含有逗号,,必须要用( ) ‘ “几个标点符号将逗号,包围起来。 另外如果非字符串的实参标识符内要用( ) “ ‘这几个标点符号,则必须成对出现。

不同实参之间必须要用逗号,分隔。 每个实参之前或之后的空白符不会被当作实参标识符的一部分,会被忽略。 非可变参数宏的实参数目必须与该宏的形参数目一致。

// 定义一个加法功能的宏
#define ADD(a,b) a + b
// 宏调用合法,但由于不能只用\
逗号符(,),所以出错。
cout << ADD(ADD(25,25),ADD(,));
// 正确:输出为70
cout << ADD(ADD(20,25),ADD(15,10));

// 定义一个无功能的宏
#define NO_USE(a,b) a;b;
// 宏调用合法,不出错
NO_USE("a_noUse",  #b_noUse)
// 宏调用合法,不出错
NO_USE(,)
// 宏调用不合法,实参数目不匹配,有3个
NO_USE(,18,15)
// 宏调用不合法,语法错误
NO_USE(a_n(oUse,1815)
// 宏调用合法,不出错
NO_USE(a_n(o,Us)e,b_n'o,Us'e)
1.2313 宏展开流程

在C99标准中,规定了符合标准的C预处理器的宏展开(Macro replacement)流程(c99_6.10.3),该流程基本如下:

  1. 对于源码来说,预处理器以从左到右,从上到下的顺序,逐个扫描每个词法元素,并进行以下操作:
    • 对于每个#define/#undef行,将对应的宏的标识符从已定义宏集合def_m加入/移除,并将函数类宏从已定义函数类宏集合def_fm加入/移除。
    • 对于其他的行,确认该词法元素(非字符/字符串字面值)是否为宏并决定后续相应的宏替换。当词法元素为集合def_m中时,它都会被替换为对应的词法元素串,该串可以为空。对于声明为函数类宏的宏名标识符,仅当紧跟其后的符号为宏调用实参表的左括号时,才会对该宏名标识符进行替换(以与其匹配的右括号为终止,忽略中间所匹配的括号对)。此时还会进行宏调用合法检测,如果调用语法不合法,则会报错。
  2. 检测到宏后,进行此宏的宏替换。宏替换流程如下:
    1. 参数替换(argument substitution): 对于函数类宏来说,预处理器找到该宏对应的词法元素串,从左到右,逐个对词法串中存在的所有形参(包括__VA_ARGS__),用对应的实参进行替换,并对替换后的参数(除了被字符串化运算符(#)以及词法元素粘贴运算符(##)所运算的以外)进行宏展开操作,直至所有参数已经完成宏展开操作。
    2. 宏运算符处理(# and ## preprocessing): 然后,预处理器找到该宏对应的词法元素串(对于函数类宏来说就是上一步处理完毕的词法串),从左到右,对该词法元素串存在的字符串化运算符(#)以及词法元素粘贴运算符(##),进行相应的预处理(也就是相关的词法元素都被替换为对应的运算形式)。
    3. 重扫替换(rescanning and further replacement): 在已替换宏集合rep_m中加入该宏,标记是否已被替换(replaced macro tracking),防止递归替换同一个宏。 接着,预处理器根据上一步处理完毕的词法串,从左到右,对该词法元素串中的每个词法元素进行扫描,并进行以下操作,直至所有词法元素都已进行了该操作。最后,在源码该宏处的位置,用操作完毕后的词法元素串替换该宏的标识符,此时宏替换流程完毕,并清空集合rep_m:
      1. 检测该词法元素是否为宏: 如果是,则进行下一步;否则该词法元素操作结束,进行下一个词法元素的操作。
      2. 检测宏是否已被替换: 在已替换宏集合rep_m中搜索是否存在该词法元素对应的宏:如果存在,则该宏不进行任何替换(painted blue);否则对该宏进行宏展开操作,完毕后该词法元素操作结束,进行下一个词法元素的操作。

要注意宏展开后的词法元素串不会被作为预处理指令(就算形式一样),因此无法用宏来动态生成预处理指令。

#include <iostream>

// 定义了一个普通宏
#define NUMBER_MACRO_M1 66
// 定义了一个字符串化参数宏,参数不进行宏展开
#define STRINGIZE(x) #x
// 定义了一个字符串化参数宏,参数进行宏展开
#define STRINGIZE_VALUE_OF(x) STRINGIZE(x)
// 定义了一个词法元素粘贴参数宏,参数不进行宏展开
#define CONT(a, b) a ## b
// 定义了一个词法元素粘贴参数宏,参数进行宏展开
#define CONT_VALUE_OF(a, b) CONT(a,b)
// 定义了一个加法参数宏,参数进行宏展开
#define ADD(a, b) a + b
// 定义了一个普通宏,该宏包含另一个宏
#define IDENTIFIER_MACRO_M1 NUMBER_MACRO_
// 定义了一个普通宏,该宏包含另一个宏
#define IDENTIFIER_MACRO_M2 M1
// 定义了一个普通宏
#define IDENTIFIER_MACRO_M1IDENTIFIER_MACRO_M2 -1
// 定义了一个普通宏
#define NUMBER_MACRO_ 8
// 定义了一个普通宏
#define M1 7
// 定义了一个比较参数宏,参数进行宏展开
#define _MAX(x, y) (((x) > (y)) ? (x) : (y))
// 定义了一个普通宏,该宏包含另一个宏
#define MAX1 _MAX(1,
// 定义了一个普通宏,该宏包含另一个宏
#define MAX2 _MAX(2,0))
// 定义了一个普通宏,该宏包含另一个宏
#define MAX21 _MAX(2,0)
// 定义了一个普通宏,该宏包含另一个宏
#define RIGHT _MAX(1,_MAX(2,0))
// 定义了一个普通宏,该宏包含另一个宏
#define WRONG MAX1 MAX2
// 定义了一个普通宏,该宏包含另一个宏
#define WRONG2 MAX1 MAX21)
// 定义了一个普通宏,该宏包含另一个宏,参数不进行宏展开
#define CON_MACRO NUMBER_MACRO_ ## M1

int main()
{
    /**
     * 以下为展开演变:
     * $ 20 + 25
     * 
     * 输出为:
     * 45
     */
    std::cout << ADD(20,25) << std::endl;
    /**
     * 以下为展开演变:
     * $ (NUMBER_MACRO_ ## M1)
     * $ NUMBER_MACRO_M1
     * $ 66
     * 
     * 输出为:
     * 66
     */
    std::cout << CON_MACRO << std::endl;
    /**
     * 以下为展开演变:
     * $ #(ADD(20,25))
     * $ "ADD(20,25)"
     * 
     * 输出为:
     * ADD(20,25)
     */
    std::cout << STRINGIZE(ADD(20,25)) << std::endl;
    /**
     * 以下为展开演变:
     * $ STRINGIZE(ADD(20,25))
     * $ STRINGIZE(20 + 25)
     *  $ #(20 + 25)
     *  $ "20 + 25"
     * 
     * 输出为:
     * 20 + 25
     */
    std::cout << STRINGIZE_VALUE_OF(ADD(20,25)) << std::endl;
    /**
     * 以下为展开演变:
     * $ CONT(NUMBER_MACRO_, M1) + 25
     *  $ (NUMBER_MACRO_ ## M1) + 25
     *  $ NUMBER_MACRO_M1 + 25
     *  $ 66 + 25
     * 
     * 输出为:
     * 91
     */
    std::cout << ADD(CONT(NUMBER_MACRO_, M1), 25) << std::endl;
    /**
     * 以下为展开演变:
     * $ CONT_VALUE_OF(NUMBER_MACRO_, M1) + 25
     *  $ CONT(NUMBER_MACRO_,M1) + 25
     *  $ CONT(8,7) + 25
     *      $ (8 ## 7) + 25
     *      $ 87 + 25
     * 
     * 输出为:
     * 112
     */
    std::cout << ADD(CONT_VALUE_OF(NUMBER_MACRO_, M1), 25) << std::endl;
    /**
     * 以下为展开演变:
     * $ CONT(IDENTIFIER_MACRO_M1, IDENTIFIER_MACRO_M2) + 25
     *  $ (IDENTIFIER_MACRO_M1 ## IDENTIFIER_MACRO_M2) + 25
     *  $ IDENTIFIER_MACRO_M1IDENTIFIER_MACRO_M2 + 25
     *  $ -1 + 25
     * 
     * 输出为:
     * 24
     */
    std::cout << ADD(CONT(IDENTIFIER_MACRO_M1, IDENTIFIER_MACRO_M2), 25) << std::endl;
    /**
     * 以下为展开演变:
     * $ CONT_VALUE_OF(IDENTIFIER_MACRO_M1, IDENTIFIER_MACRO_M2) + 25
     *  $ CONT(IDENTIFIER_MACRO_M1,IDENTIFIER_MACRO_M2) + 25
     *  $ CONT(NUMBER_MACRO_,M1) + 25
     *      $ (NUMBER_MACRO_ ## M1) + 25
     *      $ NUMBER_MACRO_M1 + 25
     *      $ 66 + 25
     * 
     * 输出为:
     * 112
     */
    std::cout << ADD(CONT_VALUE_OF(IDENTIFIER_MACRO_M1, IDENTIFIER_MACRO_M2), 25) << std::endl;
    /**
     * 以下为展开演变:
     * $ #(CONT(NUMBER_MACRO_, M1))
     * $ "CONT(NUMBER_MACRO_, M1)"
     * 
     * 输出为:
     * CONT(NUMBER_MACRO_, M1)
     */
    std::cout << STRINGIZE(CONT(NUMBER_MACRO_, M1)) << std::endl;
    /**
     * 以下为展开演变:
     * $ #(CONT_VALUE_OF(NUMBER_MACRO_, M1))
     * $ "CONT_VALUE_OF(NUMBER_MACRO_, M1)"
     * 
     * 输出为:
     * CONT_VALUE_OF(NUMBER_MACRO_, M1)
     */
    std::cout << STRINGIZE(CONT_VALUE_OF(NUMBER_MACRO_, M1)) << std::endl;
    /**
     * 以下为展开演变:
     * $ #(CONT(IDENTIFIER_MACRO_M1, IDENTIFIER_MACRO_M2))
     * $ "CONT(IDENTIFIER_MACRO_M1, IDENTIFIER_MACRO_M2)"
     * 
     * 输出为:
     * CONT(IDENTIFIER_MACRO_M1, IDENTIFIER_MACRO_M2)
     */
    std::cout << STRINGIZE(CONT(IDENTIFIER_MACRO_M1, IDENTIFIER_MACRO_M2)) << std::endl;
    /**
     * 以下为展开演变:
     * $ #(CONT_VALUE_OF(IDENTIFIER_MACRO_M1, IDENTIFIER_MACRO_M2))
     * $ "CONT_VALUE_OF(IDENTIFIER_MACRO_M1, IDENTIFIER_MACRO_M2)"
     * 
     * 输出为:
     * CONT_VALUE_OF(IDENTIFIER_MACRO_M1, IDENTIFIER_MACRO_M2)
     */
    std::cout << STRINGIZE(CONT_VALUE_OF(IDENTIFIER_MACRO_M1, IDENTIFIER_MACRO_M2)) << std::endl;
    /**
     * 以下为展开演变:
     * $ STRINGIZE(CONT(NUMBER_MACRO_, M1))
     *  $ STRINGIZE((NUMBER_MACRO_ ## M1))
     *  $ STRINGIZE(NUMBER_MACRO_M1)
     *  $ STRINGIZE(66)
     * $ #(66)
     * $ "66"
     * 
     * 输出为:
     * 66
     */
    std::cout << STRINGIZE_VALUE_OF(CONT(NUMBER_MACRO_, M1)) << std::endl;
    /**
     * 以下为展开演变:
     * $ STRINGIZE(CONT_VALUE_OF(NUMBER_MACRO_, M1))
     *  $ STRINGIZE(CONT(NUMBER_MACRO_, M1))
     *  $ STRINGIZE(CONT(8, 7))
     *  $ STRINGIZE((8 ## 7))
     *  $ STRINGIZE(87)
     * $ #(87)
     * $ "87"
     * 
     * 输出为:
     * 87
     */
    std::cout << STRINGIZE_VALUE_OF(CONT_VALUE_OF(NUMBER_MACRO_, M1)) << std::endl;
    /**
     * 以下为展开演变:
     * $ STRINGIZE(CONT(IDENTIFIER_MACRO_M1, IDENTIFIER_MACRO_M2))
     *  $ STRINGIZE((IDENTIFIER_MACRO_M1 ## IDENTIFIER_MACRO_M2))
     *  $ STRINGIZE(IDENTIFIER_MACRO_M1IDENTIFIER_MACRO_M2)
     *  $ STRINGIZE(-1)
     * $ #(-1)
     * $ "-1"
     * 
     * 输出为:
     * -1
     */
    std::cout << STRINGIZE_VALUE_OF(CONT(IDENTIFIER_MACRO_M1, IDENTIFIER_MACRO_M2)) << std::endl;
    /**
     * 以下为展开演变:
     * $ STRINGIZE(CONT_VALUE_OF(IDENTIFIER_MACRO_M1, IDENTIFIER_MACRO_M2))
     *  $ STRINGIZE(CONT(IDENTIFIER_MACRO_M1, IDENTIFIER_MACRO_M2))
     *  $ STRINGIZE(CONT(NUMBER_MACRO_, M1))
     *  $ STRINGIZE(CONT(8, 7))
     *  $ STRINGIZE((8 ## 7))
     *  $ STRINGIZE(87)
     * $ #(87)
     * $ "87"
     * 
     * 输出为:
     * 87
     */
    std::cout << STRINGIZE_VALUE_OF(CONT_VALUE_OF(IDENTIFIER_MACRO_M1, IDENTIFIER_MACRO_M2)) << std::endl;
    /**
     * 以下为展开演变:
     * $ ADD(20,25) + ADD(15,10)
     * $ 20 + 25 + ADD(15,10)
     * $ 20 + 25 + 15 + 10
     * 
     * 输出为:
     * 70
     */
    std::cout << ADD(ADD(20,25),ADD(15,10)) << std::endl;
    /**
     * 以下为展开演变:
     * $ _MAX(1,_MAX(2,0))
     * $ (((1) > (_MAX(2,0))) ? (1) : (_MAX(2,0)))
     *  $ (((1) > ((((2) > (0)) ? (2) : (0)))) ? (1) : (_MAX(2,0)))
     *  $ (((1) > ((((2) > (0)) ? (2) : (0)))) ? (1) : ((((2) > (0)) ? (2) : (0))))
     * 
     * 输出为:
     * 2
     */
    std::cout << RIGHT << std::endl;
    /**
     * 以下为展开演变:
     * $ MAX1 MAX21)
     * $ _MAX(1, MAX21)
     *  $ (((1) > (MAX21)) ? (1) : (MAX21))
     *  $ (((1) > (_MAX(2,0))) ? (1) : (_MAX(2,0)))
     *      $ (((1) > ((((2) > (0)) ? (2) : (0)))) ? (1) : (_MAX(2,0)))
     *      $ (((1) > ((((2) > (0)) ? (2) : (0)))) ? (1) : ((((2) > (0)) ? (2) : (0))))
     * 
     * 输出为:
     * 2
     */
    std::cout << WRONG2 << std::endl;
    /**
     * 以下为展开演变:
     * $ MAX1 MAX2
     * $ _MAX(1, MAX2
     * $ error occurred!
     * 
     * 编译错误:因为按照展开顺序原则,MAX1先被展开,展开后为_MAX参数宏,当想展开_MAX参数宏时因为格式不正确导致出错。
     */
    std::cout << WRONG << std::endl;
    return 0;
}

根据宏展开流程可以看出,预处理器进行宏展开的操作只有当某词法元素串当前为宏时才进行展开,而对于要靠另一些宏展开后才会变成宏的词法元素串来说,预处理器并不会自动识别。需要利用宏展开流程特性,让该词法元素串变成宏后再次进行宏展开:

// 用于使x进行一次宏展开
#define EXPAND_1(x) x
// 空宏
#define M_EMPTY
// 括号宏
#define M_PARANTHESIS() ()
// 包装了括号宏的宏,需要宏展开2次才会变为()
#define M_WRAP_PARANTHESIS M_PARANTHESIS M_EMPTY ()
// 定义int_var变量
#define DEF_INT_VAR() int int_var = 34;
#define M_WRAP_DEF_INT_VAR() DEF_INT_VAR M_EMPTY ()
#define M_WRAP2_DEF_INT_VAR M_WRAP_DEF_INT_VAR M_EMPTY ()

/**
 * 以下为展开演变:
 * $ DEF_INT_VAR ()
 */
DEF_INT_VAR M_EMPTY ()
/**
 * 以下为展开演变:
 * $ DEF_INT_VAR M_EMPTY ()
 * $ DEF_INT_VAR ()
 *  $ int int_var = 34;
 */
EXPAND_1(DEF_INT_VAR M_EMPTY ())
/**
 * 以下为展开演变:
 * $ DEF_INT_VAR M_EMPTY M_WRAP_PARANTHESIS
 * $ DEF_INT_VAR M_WRAP_PARANTHESIS
 * $ DEF_INT_VAR M_PARANTHESIS M_EMPTY ()
 * $ DEF_INT_VAR M_PARANTHESIS ()
 *  $ DEF_INT_VAR ()
 */
EXPAND_1(DEF_INT_VAR M_EMPTY M_WRAP_PARANTHESIS)
/**
 * 以下为展开演变:
 * $ EXPAND_1(DEF_INT_VAR M_EMPTY M_WRAP_PARANTHESIS)
 * $ DEF_INT_VAR M_EMPTY M_WRAP_PARANTHESIS
 * $ DEF_INT_VAR M_WRAP_PARANTHESIS
 * $ DEF_INT_VAR M_PARANTHESIS M_EMPTY ()
 * $ DEF_INT_VAR M_PARANTHESIS ()
 *  $ DEF_INT_VAR ()
 *   $ int int_var = 34;
 */
EXPAND_1(EXPAND_1(DEF_INT_VAR M_EMPTY M_WRAP_PARANTHESIS))

// 最终展开为
// DEF_INT_VAR ()
M_WRAP_DEF_INT_VAR()
// 最终展开为
// int int_var = 34;
EXPAND_1(M_WRAP_DEF_INT_VAR())
// 最终展开为
// M_WRAP_DEF_INT_VAR ()
M_WRAP2_DEF_INT_VAR
// 最终展开为
// DEF_INT_VAR ()
EXPAND_1(M_WRAP2_DEF_INT_VAR)
// 最终展开为
// int int_var = 34;
EXPAND_1(EXPAND_1(M_WRAP2_DEF_INT_VAR))
1.2314 可变参数宏的使用

当调用一个含有省略符形参的宏时,也就是调用了一个可变参数宏,当调用可变参数宏时,实参的数目必须要大于等于减去了省略符形参的形参数目。多余的实参都传递给了省略符形参。

有一个系统定义的宏,名为__VA_ARGS__。这个宏只能用于含有省略符形参的宏定义的词法元素串中(用于其他类型的宏和其他位置时会出错)。 使用该宏的位置会被替换成传给省略符形参的所有实参标识符(包括这些实参之间的逗号),所以可以用该宏来定义可变参数宏。

有一些编译器会规定必须至少将一个参数传递给省略符形参,以确保宏不会解析为带有尾随逗号的表达式。如果没有参数传递给省略号,该编译则会出错。

有一些编译器提供了一个扩展(如gcc编译器提供编译参数std=gnucxxx),允许##出现在逗号之后和__VA_ARGS__之前。在这种情况下,##在变量参数存在时不执行任何操作,但在变量参数不存在时删除逗号:这使得定义像fprintf (stderr, format, ##__VA_ARGS__)等宏成为可能。但这也可以使用宏__VA_OPT__以c++标准方式实现(c++20以后)。

// variadic_macros.cpp
#include <stdio.h>
#define EMPTY
#define CHECK1(x, ...) if (!(x)) { printf(__VA_ARGS__); }
#define CHECK2(x, ...) if ((x)) { printf(__VA_ARGS__); }
#define CHECK3(...) { printf(__VA_ARGS__); }
#define MACRO(s, ...) printf(s, __VA_ARGS__)

int main() 
{
    // 输出here are some varargs1(1)
    CHECK1(0, "here %s %s %s", "are", "some", "varargs1(1)\n");
    CHECK1(1, "here %s %s %s", "are", "some", "varargs1(2)\n");   // won't print

    CHECK2(0, "here %s %s %s", "are", "some", "varargs2(3)\n");   // won't print
    // 输出here are some varargs2(4)
    CHECK2(1, "here %s %s %s", "are", "some", "varargs2(4)\n");

    // always invokes printf in the macro
    // 输出here are some varargs3(5)
    CHECK3("here %s %s %s", "are", "some", "varargs3(5)\n");

    // 输出hello, world
    MACRO("hello, world\n");

    // 错误:输出error
    MACRO("error\n", EMPTY); // would cause error C2059, except VC++
                             // suppresses the trailing comma
}
1.2315 #undef指令

#undef指令移除(也就是取消定义)本文件内在该指令之前存在的,宏名为给定标识符的所有宏。 使用形式为:

#undef 标识符

#undef指令只对是宏的标识符起作用,如果给定的标识符不是宏或者根本不存在该标识符的词法元素,那么该指令不起作用(也就是无影响)

如果给定标识符的宏有多个,则移除对应的所有宏

int ins = 66;
cout << ins; // 正确:ins为int的变量,输出66。
#define ins
cout << ins; // 错误:ins已被删除,不存在名为ins的变量。
#undef ins
cout << ins; // 正确:宏ins已被移除,所以ins又恢复为int的变量,输出66。
1.2316 宏相关的运算符

对于宏,我们有两种运算符来处理其关联的词法元素串,使其满足我们的一些要求:

  • 字符串化运算符(Stringizing operator)
  • 词法元素粘贴运算符(Token-pasting operator)

这两种运算符只用于宏定义,不能用于其他操作

字符串化运算符(#)

字符串化运算符只能用于带参数的宏。是将宏参数转换为字符串字面值的运算符。

字符串化运算符只有一个运算对象,且该运算对象在其右侧,该运算对象只能是宏定义中词法元素串内的形参标识符

字符串化运算符返回一个运算对象对应的实参标识符的字符串字面值版本

字符串化运算符只作用于宏定义中词法元素串内的形参标识符,使用形式为:

#形参标识符

#和形参标识符之间不能有空白符

字符串化运算符和普通参数宏调用过程差不多。 在宏调用过程时,对于非#开头的形参标识符,预处理器将该形参标识符替换成实参表里对应的标识符;对于#开头的形参标识符,预处理器会用对应实参标识符的字符串字面值版本来进行替换。最后再进行宏展开。

如果需要字符串化的实参标识符所包含的字符在转化时需要转义序列(例如,引号"或反斜杠\字符),则预处理器会自动插入必要的转义反斜杠。

#include <stdio.h>
// 定义输出功能的宏
#define stringer(x) printf_s( #x "\n" )
int main()
{
    // 等价于printf_s( "In quotes in the printf function call" "\n" );
    // 输出为In quotes in the printf function call
    stringer( In quotes in the printf function call );
    // 等价于printf_s( "\"In quotes when printed to the screen\"" "\n" );
    // 输出为"In quotes when printed to the screen"
    stringer("In quotes when printed to the screen");
    // 等价于printf_s( "\"This: \\\" prints an escaped double quote\"" "\n" );
    // 输出为"This: \"  prints an escaped double quote"
    stringer( "This: \" prints an escaped double quote" );
    return 0;
}

词法元素粘贴运算符(##)

词法元素粘贴运算符,有时称为合并运算符或组合运算符。该运算符可用于两种形式的宏。

词法元素粘贴运算符的作用就是连接两个词法元素,使其成为一个词法元素。

词法元素粘贴运算符有两个运算对象,分别在在其左右侧,该运算对象可以是宏定义中词法元素串内的任何词法元素(包括形参标识符)

词法元素粘贴运算符返回一个其两个运算对象合并后的词法元素。该词法元素的前缀为运算符左侧运算对象,后缀为运算符右侧运算对象。

词法元素粘贴运算符的使用形式为:

词法元素1 ## 词法元素2

词法元素粘贴运算符与其运算对象之间可以有多个空白符

当词法元素粘贴运算符的某运算对象为形参标识符时,宏调用时和其他参数宏调用类似,不同之处就是把对应实参标识符和另一个运算对象组合成了一个新的词法元素。

合并后的词法元素如果不满足成为标识符的条件,则不能作为标识符使用(可以作为字符串字面值使用)。

// 定义了DEF_INT宏\
等价于int int_num = 15;
#define DEF_INT int int ## _num = 15;
DEF_INT
cout << _num; // 错误:未定义_num
cout << int_num; // 正确:输出15
// 错误定义,因为int?不是标识符,不能用于定义变量
#define DEF_INT int int? ## _num = 15;
// 等价于int No_id = id;
#define DEF_INT(id) int No_ ## id = id;
DEF_INT(26)
cout << No_id; // 错误:未定义No_id\
id是形参名,要写成具体的实参名,比如No_26
cout << No_26; // 正确:输出26
// 换句话说,扩展hash_hash会产生一个新的符号,由两个相邻的#符号组成,但此新词法元素不是##运算符。
#define hash_hash # ## #
#define mkstr(a) # a
#define in_between(a) mkstr(a)
#define join(c, d) in_between(c hash_hash d)
// 等价于
// char p[] = "x ## y";
/*
宏展开的演变为:
join(x, y)
in_between(x hash_hash y)
in_between(x ## y)
mkstr(x ## y)
"x ## y"
*/
char p[] = join(x, y);
1.232 条件编译

条件编译类的预处理指令是用来控制源文件部分的编译。 条件编译类的预处理指令有六个,分别为:

  • #if
  • #elif
  • #else
  • #endif
  • #ifdef
  • #ifndef

这几个指令的作用和条件控制语句类似,用法也是类似

#if #elif #else #endif的介绍

和条件控制语句类似,#if#elif后面也是跟着判断条件的。

和条件控制语句不一样的是这两个指令的条件判断不需要括号,且判断条件必须为常量表达式。 而且除此之外,这两个指令还有一种条件判断形式。 使用形式为:

#if 常量表达式/defined (宏名)/宏名 各种代码 (可选 #elif 常量表达式/defined (宏名)/宏名 各种代码) (可选 #else 常量表达式/defined (宏名)/宏名 各种代码) #endif

第二种条件判断形式中,宏名必须为标识符。宏名可以加括号或者不加

这两种判断条件都可以用宏,只要其宏满足对应的条件就行。

和条件控制语句一样,一组匹配的条件编译指令,有且只有一个#if,在最前面;有且只有一个#endif,在最后面;中间可以有多个#elif;可以有一个#else,且必须是#endif的上一个指令。 如果存在不匹配的条件编译指令,则会出错。

和条件控制语句一样,#if#ifdef#ifndef#elif#else指令的作用范围为从使用位置后到遇到的第一个与其匹配的其他条件编译指令为止。

和条件控制语句一样,每个#else#elif#endif指令与上一个离其最近的#if#ifdef#ifndef指令匹配。

和条件控制语句一样,条件编译指令可以嵌套,注意匹配规则就行

第一种形式中,如果给定的常量表达式所转换的布尔值为false,则预处理器就会忽略掉对应指令作用范围内的所有代码;如果为true就会忽略与其匹配的其他条件编译指令作用范围内的所有代码。

第二种形式中,如果对应指令之前存在给定的宏名的宏,那么就视为true,否则为false。之后的操作和第一种形式的一样。

// 一组匹配的条件编译指令,\
该条件编译指令表示只编译\
cout << "elif 2\n";\
最后程序输出elif 2
#define DEF_ELIF3
const int elif1_val = 0;
const int elif2_val = 0;
#if defined (DEF_IF)
cout << "if\n";
#elif elif1_val
cout << "elif 1\n";
#elif elif2_val
cout << "elif 2\n";
#elif defined DEF_ELIF3
cout << "elif 2\n";
#else
cout << "else\n";
#endif
#ifdef #ifndef 的介绍

#ifdef#ifndef的作用效果与

#if defined (宏名)/宏名 #if !defined (宏名)/宏名

一样。

使用形式为

#ifdef 宏名 #ifndef 宏名

其他特性和属性和#if一样,可以参考上面#if的介绍。

1.233 阻止编译

阻止编译是指#error指令,该指令会在编译时发出用户指定的错误消息,然后终止编译。

使用形式为:

#error 词法元素串

#error指令发出的错误消息也就是使用时的所写的词法元素串。

#if !defined(__cplusplus)
#error C++ compiler required.
#endif
1.234 文件包含指令

文件包含类预处理指令是将其他文件包含到所使用该指令的文件中。

其中C标准中的包含指令为#include指令。 部分编译器(如msvc)增加了一些指令,增加的指令如下:

  • #import指令
  • #using指令
1.2341 #include指令

#include指令是通知预处理器将指定文件的内容包含在指令出现的位置的指令。

每次使用#include指令时,预处理器会自动展开包含文件的代码到该使用位置。

#include指令的使用形式有两种:

#include "指定路径" #include <指定路径>

指定路径可以是一个文件名,也可以是文件的相对或绝对路径。指定路径的语法取决于编译程序的操作系统。

包含文件内可以包含宏和其他代码,但是要注意对象和类型重复定义的问题

父文件是#include指令所给的文件。 #include指令可以嵌套,可以出现在由#include指令的包含文件内

#include指令两种使用形式的差别主要在于预处理器搜索文件路径的顺序。

语法形式 搜索操作
带引号的形式 预处理器按以下顺序搜索包含文件:

1. 指定的路径所在的目录中

2. 当前文件之前的已展开的包含文件的目录中,以其打开的相反顺序排列搜索。从父包含文件的目录中开始进行,然后继续向上到任何祖父包含文件的目录。

3. 沿着每个编译器选项中指定的路径/I

4. 沿环境变量指定的路径。
尖括号的形式 预处理器按以下顺序搜索包含文件:

1. 沿着每个编译器选项中指定的路径/I

2. 沿环境变量指定的路径。

只要找到具有给定名称的文件,预处理器就会停止搜索。如果指定路径是以双引号(““)括起来的绝对路径,则预处理器只搜索该指定路径并忽略搜索规则。

#include通常使用头文件保护宏或#pragma once来防止头文件双重包含。

1.2342 #import指令(编译器扩展 msvc)

#import指令是c++特有的指令,是用于合并类型库的信息,该类型库的内容将会被转换成c++类,大多数用于描述COM的接口。

使用形式也是两种:

#import "文件名" [文件属性] #import <文件名> [文件属性]

文件名可以是以下类型之一:

  • 包含该类型库的文件的名称,如.olb、.tlb或.dll文件。 每个文件名之前可以使用关键字file:
  • 类型库中控件的progid。在64位操作系统上使用32位交叉编译器时,编译器只能读取32位注册表配置单元。您可能需要使用本机64位编译器才能生成和注册64位类型库。 每个控件的progid之前可以使用关键字progid:
  • 类型库的库ID。如果未指定versionlcid,则应用于的规则progid:也应用于libid:。 每个库ID之前可以使用关键字libid:
  • 可执行 (.exe) 文件。
  • 库(.dll)文件包含类型库资源(如.ocx)
  • 包含类型库的复合文档。
  • 可由 LoadTypeLib API 理解的任何其他文件格式。
#import "progid:my.prog.id.1.5"
#import "..\drawctl\drawctl.tlb" no_namespace raw_interfaces_only

文件属性可以是一个或多个#import特性,用空格或逗号分隔每个特性。

#import "..\drawctl\drawctl.tlb" no_namespace, raw_interfaces_only
#import "..\drawctl\drawctl.tlb" no_namespace raw_interfaces_only
1.2343 #using指令(编译器扩展 msvc)

#using指令是将元数据导入使用公共语言运行编译的程序中。

使用形式:

#using 文件 [as_friend]

文件要是.dll.exe.netmodule.obj后缀的文件。 as_friend是指定该文件中的所有类型为可访问的。

1.235 调试操作

有一些系统预先定义的宏和某些预处理指令可以帮助我们进行一些调试工作。

调试是在编译链接完成后的一种检查工作,是在程序运行时所进行的。

一种基本调试思想是:程序可以包含一些用于调试的代码,但是这些代码只在开发程序时使用。当应用程序编写完成准备发布时,要先屏蔽掉调试代码。

这种方法通常会用到以下这几种预处理宏和一个预处理指令:

  • assert
  • __func__
  • __FILE__
  • __LINE__
  • __TIME__
  • __DATE__
  • #line
1.2351 assert

assert宏定义在cassert头文件中。 assert宏需要传递一个表达式作为它的条件,使用形式为:

assert(表达式);

如果表达式为假(即0),assert输出信息并终止程序的执行。如果表达式为真(即非0),assert什么也不做。

assert宏常用于检查“不能发生”的条件。例如,一个对输入文本进行操作的程序可能要求所有给定单词的长度都大于某个阈值。此时,程序可以包含一条如下所示的语句

assert(word.size() > threshold);

默认状态下assert宏一直处于生效状态,但是我们可以进行设置来使assert宏不生效(也就是无论其表达式为什么,都不执行操作)。

assert的生效状态依赖于一个标识符为NDEBUG的宏的状态。如果在定义assert宏之前存在NDEBUG宏,则assert不生效,什么也不做。

// 错误操作,NDEBUG\
要在定义assert宏之前存在才行
#include <cassert>
#define NDEBUG
int main()
{
    assert(0);
    return 0;
}
// 仍然终止程序的运行
// 正确操作,NDEBUG\
在定义assert宏之前已经存在
#define NDEBUG
#include <cassert>
int main()
{
    assert(0);
    return 0;
}
// assert宏什么也不做
1.2352 输出错误信息的宏

有时我们希望在程序出错后能够输出一些有用的错误信息,方便我们去修改调试程序,所以我们需要一些宏来输出一些有用的错误信息,以下是一些用于输出有关错误信息的宏:

  • __func__
  • __FILE__
  • __TIME__
  • __DATE__
  • __LINE__

__func__

__func__宏主要用于输出当前调用点所在的函数的名称标识符,预处理器为每个函数都定义了一个__func__宏,该宏的词法元素串为一个静态字符串字面值(static const char)。

不能在函数外使用__func____func__输出的是离其调用点最里层的函数名。

__FILE__

__FILE__宏主要用于输出当前调用点所在的绝对路径文件名称(包含后缀),该宏的词法元素串为一个静态字符串字面值(static const char)。

__TIME__

__TIME__宏主要用于输出当前调用点所在文件最后一次预处理完成的时间,该时间是hh:mm:ss格式。该宏的词法元素串为一个静态字符串字面值(static const char)。

__DATE__

__DATE__宏主要用于输出当前调用点所在文件最后一次编译完成的日期,该日期是Mmm dd yyyy格式。该宏的词法元素串为一个静态字符串字面值(static const char)。

__LINE__

__LINE__宏主要用于输出当前调用点所在的行号,该日期是Mmm dd yyyy格式。该宏的词法元素串为一个整数。

#include <iostream>
using namespace std;
/* 输出为
the current function name is
main
the current filename is
e:\test.cpp
the current time of compiling is
13:56:04
May 18 2021
the current line is
13
*/
int main()
{
    cout << "the current function name is\n" << __func__;
    cout << "\nthe current filename is\n" << __FILE__;
    cout << "\nthe current time of compiling is\n" << __TIME__ << "\n" << __DATE__;
    cout << "\nthe current line is\n" << __LINE__;
}
1.2353 #line指令

#line指令是作用于__LINE__和__FILE__这两个宏的预处理指令,该指令能修改这两个宏所存的信息。

#line指令将其调用位置的下一行行号修改为指定的行号,之后的行号根据该修改的行号逐个增加。

#line指令将当前的文件名修改为指定的文件名。

#line指令的使用形式为:

#line 行号 (可选 文件名)

行号可以是任何整数字面值,也可以是宏,只要该宏展开后是整数字面值就行。

文件名可以任意的字符串字面值,如果省略文件名,则前一个指定的文件名保持不变

#line指令可以多次使用,后使用的#line指令覆盖前面的#line指令。

#include <iostream>
using namespace std;
/* 输出为:
the current filename is e:\test.cpp
the current line is 11
the current filename is e:\test.cpp
the current line is 55
the current filename is open.cpp
the current line is 13
the current filename is open.cpp
the current line is 31
*/
int main()
{
    cout << "\nthe current filename is " << __FILE__;
    cout << "\nthe current line is " << __LINE__;
    #line 50
    ;
    ;
    ;
    ; 
    cout << "\nthe current filename is " << __FILE__;
    cout << "\nthe current line is " << __LINE__;
    #line 10 "open.cpp"
    ;
    ;
    cout << "\nthe current filename is " << __FILE__;
    cout << "\nthe current line is " << __LINE__;
    #line 30
    cout << "\nthe current filename is " << __FILE__;
    cout << "\nthe current line is " << __LINE__;
}
1.236 杂注操作(编译器扩展 msvc)

杂注指令是用来指定计算机特定或操作系统特定的编译器功能的指令。 C和C++支持某些对其主机或操作系统的独特功能。例如,某些程序必须对内存中的数据位置进行精确控制,或控制某些函数接收参数的方式。杂注指令为每个编译器提供了一种提供计算机和操作系统特定功能的方法,同时保持与c和c++语言的总体兼容性。

杂注指令一般有两种形式:

#pragma 词法元素串 _Pragma (字符串字面值)

c++预处理器识别以下的pragma指令: pragma_directive

// 这些指令等价于在控制台上输入以下指令\
cl /Zp8 some_file.cpp
// some_file.cpp - packing is 8
// ...
#pragma pack(push, 1) - packing is now 1
// ...
#pragma pack(pop) - packing is 8 again
// ...