🍑C预处理器
2021-1-27
| 2023-8-2
0  |  阅读时长 0 分钟
type
status
date
slug
summary
tags
category
icon
password
Property
 

翻译处理

在预处理之前,编译器必须对该程序进行一些翻译处理。
  1. 编译器把源代码中出现的字符映射到源字符集。该过程处理多字节字符和三字符序列——字符扩展让C更加国际化。
  1. 编译器定位每个反斜杠后面跟着换行符的实例,并删除它们
    1. 注意,在这种场合中,“换行符”的意思是通过按下Enter键在源代码文件中换行所生成的字符,而不是指符号表征\n。
      由于预处理表达式的长度必须是一个逻辑行,所以这一步为预处理器做好了准备工作。一个逻辑行可以是多个物理行。
  1. 编译器把文本划分成预处理记号序列、空白序列和注释序列(记号是由空格、制表符或换行符分隔的项)。这里要注意的是,编译器将用一个空格字符替换每一条注释。因此:
    1. 而且,实现可以用一个空格替换所有的空白字符序列(不包括换行符)。
  1. 最后,程序已经准备好进入预处理阶段,预处理器查找一行中以#号开始的预处理指令。
 
 
 

明示常量#define

#define预处理器指令和其他预处理器指令一样,以#号作为一行的开始。
ANSI和后来的标准都允许#号前面有空格或制表符,而且还允许在#和指令的其余部分之间有空格。但是旧版本的C要求指令从一行最左边开始,而且#和指令其余部分之间不能有空格。
指令可以出现在源文件的任何地方,其定义从指令出现的地方到该文件末尾有效。大量使用#define指令来定义明示常量(manifest constant)(也叫做符号常量),但是该指令还有许多其他用途。
预处理器指令从#开始运行,到后面的第1个换行符为止。也就是说,指令的长度仅限于一行。在预处理开始前,编译器会把多行物理行处理为一行逻辑行。
 
每行#define(逻辑行)都由3部分组成:
notion image
  • 第1部分是#define指令本身
  • 第2部分是选定的缩写,也称为。有些宏代表值,这些宏被称为类对象宏(object-like macro)。C语言还有类函数宏(function-like macro)。宏的名称中不允许有空格,而且必须遵循C变量的命名规则。
  • 第3部分(指令行的其余部分)称为替换列表或替换体
一旦预处理器在程序中找到宏的示实例后,就会用替换体代替该宏(也有例外)。从宏变成最终替换文本的过程称为宏展开(macro expansion)。注意,可以在#define行使用标准C注释。
由于编译器在编译期对所有的常量表达式(只包含常量的表达式)求值,所以预处理器不会进行实际的乘法运算,这一过程在编译时进行。预处理器不做计算,不对表达式求值,它只进行替换。
 
C语言现在也支持const关键字,提供了更灵活的方法。用const可以创建在程序运行过程中不能改变的变量,可具有文件作用域或块作用域。另一方面,宏常量可用于指定标准数组的大小和const变量的初始值。
在C中,非自动数组的大小应该是整型常量表达式,这意味着表示数组大小的必须是整型常量的组合(如5)、枚举常量和sizeof表达式,不包括const声明的值(这也是C++和C的区别之一,在C++中可以把const值作为常量表达式的一部分)。但是,有的实现可能接受其他形式的常量表达式。例如,GCC 4.7.3不允许data2的声明,但是Clang 4.6允许。
 

记号

从技术角度来看,可以把宏的替换体看作是记号(token)型字符串,而不是字符型字符串。C预处理器记号是宏定义的替换体中单独的“词”。用空白把这些词分开。
如果预处理器把该替换体解释为字符型字符串,将用4 * 8替换EIGHT。即,额外的空格是替换体的一部分。如果预处理器把该替换体解释为记号型字符串,则用3个的记号4 * 8(分别由单个空格分隔)来替换EIGHT。换而言之,解释为字符型字符串,把空格视为替换体的一部分;解释为记号型字符串,把空格视为替换体中各记号的分隔符。在实际应用中,一些C编译器把宏替换体视为字符串而不是记号。在比这个例子更复杂的情况下,两者的区别才有实际意义。 顺带一提,C编译器处理记号的方式比预处理器复杂。由于编译器理解C语言的规则,所以不要求代码中用空格来分隔记号。例如,C编译器可以把2*2直接视为3个记号,因为它可以识别2是常量,*是运算符。
 

重定义常量

假设先把LIMIT定义为20,稍后在该文件中又把它定义为25。这个过程称为重定义常量。不同的实现采用不同的重定义方案。除非新定义与旧定义相同,否则有些实现会将其视为错误。另外一些实现允许重定义,但会给出警告。ANSI标准采用第1种方案,只有新定义和旧定义完全相同才允许重定义。
具有相同的定义意味着替换体中的记号必须相同,且顺序也相同。
因此,下面两个定义相同:
这两条定义都有 3 个相同的记号,额外的空格不算替换体的一部分。而下面的定义则与上面两条宏定义不同:
这条宏定义中只有一个记号,因此与前两条定义不同。如果需要重定义宏,使用#undef 指令。
如果确实需要重定义常量,使用const关键字和作用域规则更容易些。
 
 

在#define中使用参数

notion image
在#define中使用参数可以创建外形和作用与函数类似的类函数宏。带有参数的宏看上去很像函数,因为这样的宏也使用圆括号。类函数宏定义的圆括号中可以有一个或多个参数,随后这些参数出现在替换体中
这看上去像函数调用,但是它的行为和函数调用完全不同
函数调用在程序运行时把参数的值传递给函数。宏调用在编译之前把参数记号传递给程序。这两个不同的过程发生在不同时期。
 
预处理器把出现x的地方都替换成x+2。因此,x*x变成了x+2*x+2。如果x为5,那么该表达式的值为:
是否可以修改宏定义让SQUARE(x+2)得36?当然可以,要多加几个圆括号:
 
但是,这并未解决所有的问题。下面的输出行:
把SQUARE(x)定义为下面的形式可以解决这种混乱:
 
要处理前面的两种情况,要这样定义:
因此,必要时要使用足够多的圆括号来确保运算和结合的正确顺序。
 
尽管如此,这样做还是无法避免程序中最后一种情况的问题。SQUARE(++x)变成了++x*++x,递增了两次x,一次在乘法运算之前,一次在乘法运算之后:
解决这个问题最简单的方法是,避免用++x 作为宏参数。一般而言,不要在宏中使用递增或递减运算符。但是,++x可作为函数参数,因为编译器会对++x求值得5后,再把5传递给函数。
 
 

用宏参数创建字符串:#运算符

下面是一个类函数宏:
假设这样使用宏:
注意双引号字符串中的X被视为普通文本,而不是一个可被替换的记号。
 
C允许在字符串中包含宏参数。在类函数宏的替换体中,#号作为一个预处理运算符,可以把记号转换成字符串。例如,如果x是一个宏形参,那么#x就是转换为字符串"x"的形参名。这个过程称为字符串化
 

预处理器黏合剂:##运算符

与#运算符类似,##运算符可用于类函数宏的替换部分。而且,##还可用于对象宏的替换部分。##运算符把两个记号组合成一个记号。例如,可以这样做:
宏XNAME(4)将展开为x4
PRINT_XN()宏用#运算符组合字符串,##运算符把记号组合为一个新的标识符
 

变参宏:...和__ VA_ARGS __

一些函数(如 printf())接受数量可变的参数。stdvar.h 头文件提供了工具,让用户自定义带可变参数的函数。C99/C11也对宏提供了这样的工具。虽然标准中未使用“可变”这个词,但是它已成为描述这种工具的通用词(虽然,C标准的索引添加了字符串化(stringizing)词条,但是,标准并未把固定参数的函数或宏称为固定函数和不变宏)。 通过把宏参数列表中最后的参数写成省略号(即,3个点...)来实现这一功能。这样,预定义宏
__ VA_ARGS __可用在替换部分中,表明省略号代表什么。例如,下面的定义:
假设稍后调用该宏:
对于第1次调用,__ VA_ARGS__ 展开为1个参数:"Howdy"。 对于第2次调用, __VA_ARGS __展开为3个参数:"weight = %d,shipping = $%.2f\n"、wt、sp。
因此,展开后的代码是:
 
该程序使用了字符串的串联功能和#运算符
 

宏和函数的选择

有些编程任务既可以用带参数的宏完成,也可以用函数完成。应该使用宏还是函数?
使用宏比使用普通函数复杂一些,稍有不慎会产生奇怪的副作用。一些编译器规定宏只能定义成一行。不过,即使编译器没有这个限制,也应该这样做。
宏和函数的选择实际上是时间和空间的权衡。宏生成内联代码,即在程序中生成语句。如果调用20次宏,即在程序中插入20行代码。如果调用函数20次,程序中只有一份函数语句的副本,所以节省了空间。然而另一方面,程序的控制必须跳转至函数内,随后再返回主调程序,这显然比内联代码花费更多的时间。
宏的一个优点是,不用担心变量类型(宏处理的是字符串,而不是实际的值)。因此,只要能用int或float类型都可以使用SQUARE(x)宏。
C99提供了第3种可替换的方法——内联函数。
 
简单的函数通常使用宏:
宏名中不允许有空格,但是在替换字符串中可以有空格。ANSI C允许在参数列表中使用空格。
 
用圆括号把宏的参数和整个替换体括起来。这样能确保被括起来的部分在下面这样的表达式中正确地展开:
用大写字母表示宏函数的名称。该惯例不如用大写字母表示宏常量应用广泛。但是,大写字母可以提醒程序员注意,宏可能产生的副作用。 如果打算使用宏来加快程序的运行速度,那么首先要确定使用宏和使用函数是否会导致较大差异。在程序中只使用一次的宏无法明显减少程序的运行时间。在嵌套循环中使用宏更有助于提高效率。许多系统提供程序分析器以帮助程序员压缩程序中最耗时的部分。

文件包含:#include

当预处理器发现#include指令时,会查看后面的文件名并把文件的内容包含到当前文件中,即替换源文件中的#include指令。这相当于把被包含文件的全部内容输入到源文件#include指令所在的位置。#include指令有两种形式:
在 UNIX 系统中,尖括号告诉预处理器在标准系统目录中查找该文件。双引号告诉预处理器首先在当前目录中(或文件名中指定的其他目录)查找该文件,如果未找到再查找标准系统目录:
 
为什么要包含文件?因为编译器需要这些文件中的信息。例如,stdio.h文件中通常包含EOF、NULL、getchar()putchar()的定义。getchar()putchar()被定义为宏函数。此外,该文件中还包含C的其他I/O函数。
C语言习惯用.h后缀表示头文件,这些文件包含需要放在程序顶部的信息。头文件经常包含一些预处理器指令。有些头文件(如stdio.h)由系统提供,当然你也可以创建自己的头文件。
包含一个大型头文件不一定显著增加程序的大小。在大部分情况下,头文件的内容是编译器生成最终代码时所需的信息,而不是添加到最终代码中的材料。
 
头文件中最常用的形式如下:
  • 明示常量——例如,stdio.h中定义的EOF、NULL和BUFSIZE(标准I/O 缓冲区大小)。
  • 宏函数——例如,getc(stdin)通常用getchar()定义,而getc()经常用于定义较复杂的宏,头文件ctype.h通常包含ctype系列函数的宏定义。
  • 函数声明——例如,string.h头文件(一些旧的系统中是strings.h)包含字符串函数系列的函数声明。在ANSI C和后面的标准中,函数声明都是函 数原型形式。
  • 结构模版定义——标准I/O函数使用FILE结构,该结构中包含了文件和与文件缓冲区相关的信息。FILE结构在头文件stdio.h中。
  • 类型定义——标准 I/O 函数使用指向 FILE 的指针作为参数。通常, stdio.h用#define 或typedef把FILE定义为指向结构的指针。类似地,size_ttime_t类型也定义在头文件中。
 
另外,还可以使用头文件声明外部变量供其他文件共享。例如,如果已经开发了共享某个变量的一系列函数,该变量报告某种状况(如,错误情况),这种方法就很有效。这种情况下,可以在包含这些函数声明的源代码文件定义一个文件作用域的外部链接变量:
然后,可以在与源代码文件相关联的头文件中进行引用式声明:
这行代码会出现在包含了该头文件的文件中,这样使用该系列函数的文件都能使用这个变量。虽然源代码文件中包含该头文件后也包含了该声明,但是只要声明的类型一致,在一个文件中同时使用定义式声明和引用式声明没问题。
需要包含头文件的另一种情况是,使用具有文件作用域、内部链接和const限定符的变量或数组。const 防止值被意外修改,static 意味着每个包含该头文件的文件都获得一份副本。因此,不需要在一个文件中进行定义式声明,在其他文件中进行引用式声明。
 

其他指令

预处理器提供一些指令,程序员通过修改#define的值即可生成可移植的代码。#undef指令取消之前的#define定义。#if、#ifdef、#ifndef、#else、#elif和#endif指令用于指定什么情况下编写哪些代码。#line指令用于重置行和文件信息,#error指令用于给出错误消息,#pragma指令用于向编译器发出指令。

#undef

#undef指令用于“取消”已定义的#define指令:
现在就可以把LIMIT重新定义为一个新值。即使原来没有定义LIMIT,取消LIMIT的定义仍然有效。如果想使用一个名称,又不确定之前是否已经用过,为安全起见,可以用#undef 指令取消该名字的定义。
 
从C预处理器角度看已定义
预处理器在识别标识符时,遵循与C相同的规则。当预处理器在预处理器指令中发现一个标识符时,它会把该标识符当作已定义的或未定义的。这里的已定义表示由预处理器定义。如果标识符是同一个文件中由前面的#define指令创建的宏名,而且没有用#undef 指令关闭,那么该标识符是已定义的。如果标识符不是宏,假设是一个文件作用域的C变量,那么该标识符对预处理器而言就是未定义的。
已定义宏可以是对象宏,包括空宏或类函数宏:
#define宏的作用域从它在文件中的声明处开始,直到用#undef指令取消宏为止,或延伸至文件尾。
如果宏通过头文件引入,那么#define在文件中的位置取决于#include指令的位置。
几个预定义宏,如__DATE__和__FILE__。这些宏一定是已定义的,而且不能取消定义。
 
 

条件编译

可以使用其他指令创建条件编译。也就是说,可以使用这些指令告诉编译器根据编译时的条件执行或忽略信息(或代码)块。

#ifdef、#else和#endif指令

#ifdef指令说明,如果预处理器已定义了后面的标识符(MAVIS),则执行#else或#endif指令之前的所有指令并编译所有C代码(先出现哪个指令就执行到哪里)。如果预处理器未定义MAVIS,且有 #else指令,则执行#else和#endif指令之间的所有代码。
#ifdef #else很像C的if else。两者的主要区别是,预处理器不识别用于标记块的花括号({}),因此它使用#else(如果需要)和#endif(必须存在)来标记指令块。这些指令结构可以嵌套。也可以用这些指令标记C语句块。
如果省略JUST_CHECKING定义(把它放在C注释中,或者使用#undef指令取消它的定义)并重新编译该程序,只会输出最后一行。可以用这种方法在调试程序。定义JUST_CHECKING并合理使用#ifdef,编译器将执行用于调试的程序代码,打印中间值。调试结束后,可移除JUST_CHECKING定义并重新编译。如果以后还需要使用这些信息,重新插入定义即可。这样做省去了再次输入额外打印语句的麻烦。#ifdef还可用于根据不同的C实现选择合适的代码块。
 

#ifndef指令

#ifndef指令与#ifdef指令的用法类似,也可以和#else、#endif一起使用,但是它们的逻辑相反。#ifndef指令判断后面的标识符是否是未定义的,常用于定义之前未定义的常量。如下所示:
通常,包含多个头文件时,其中的文件可能包含了相同宏定义。#ifndef指令可以防止相同的宏被重复定义。在首次定义一个宏的头文件中用#ifndef指令激活定义,随后在其他头文件中的定义都被忽略。
#ifndef指令还有另一种用法。假设有上面的arrays.h头文件,然后把下面一行代码放入一个头文件中:
SIZE被定义为100。但是,如果把下面的代码放入该头文件:
SIZE则被设置为10。这里,当执行到#include "arrays.h"这行,处理array.h中的代码时,由于SIZE是已定义的,所以跳过了#define SIZE 100这行代码。鉴于此,可以利用这种方法,用一个较小的数组测试程序。测试完毕后,移除#define SIZE 10并重新编译。这样,就不用修改头文件数组本身了。 #ifndef指令通常用于防止多次包含一个文件。也就是说,应该像下面这样设置头文件:
假设该文件被包含了多次。当预处理器首次发现该文件被包含时,THINGS_H_是未定义的,所以定义了THINGS_H_,并接着处理该文件的其他部分。当预处理器第2次发现该文件被包含时,THINGS_H_是已定义的,所以预处理器跳过了该文件的其他部分。 为何要多次包含一个文件?最常见的原因是,许多被包含的文件中都包含着其他文件,所以显式包含的文件中可能包含着已经包含的其他文件。这有什么问题?在被包含的文件中有某些项(如,一些结构类型的声明)只能在一个文件中出现一次。C标准头文件使用#ifndef技巧避免重复包含。但是,这存在一个问题:如何确保待测试的标识符没有在别处定义。通常,实现的供应商使用这些方法解决这个问题:用文件名作为标识符、使用大写字母、用下划线字符代替文件名中的点字符、用下划线字符做前缀或后缀(可能使用两条下划线)。例如,查看stdio.h头文件,可以发现许多类似的代码:
你也可以这样做。但是,由于标准保留使用下划线作为前缀,所以在自己的代码中不要这样写,避免与标准头文件中的宏发生冲突。
 

#if和#elif指令

#if指令很像C语言中的if。#if后面跟整型常量表达式,如果表达式为非零,则表达式为真。可以在指令中使用C的关系运算符和逻辑运算符:
可以按照if else的形式使用#elif(早期的实现不支持#elif)。例如,可以这样写:
较新的编译器提供另一种方法测试名称是否已定义,即用#if defined(VAX)代替#ifdef VAX。 这里,defined是一个预处理运算符,如果它的参数是用#defined定义过,则返回1;否则返回0。这种新方法的优点是,它可以和#elif一起使用。
 

预定义宏

C标准规定了一些预定义宏
notion image
C99 标准提供一个名为 __ func__的预定义标识符,它展开为一个代表函数名的字符串(该函数包含该标识符)。那么, __func__ 必须具有函数作用域,而从本质上看宏具有文件作用域。因此, __func __是C语言的预定义标识符,而不是预定义宏。
 

#line和#error

#line指令重置__ LINE__ 和__FILE__宏报告的行号和文件名。可以这样使用#line:
#error 指令让预处理器发出一条错误消息,该消息包含指令中的文本。如果可能的话,编译过程应该中断。可以这样使用#error指令:
 

#pragma

在现在的编译器中,可以通过命令行参数或IDE菜单修改编译器的一些设置。#pragma把编译器指令放入源代码中。例如,在开发C99时,标准被称为C9X,可以使用下面的编译指示(pragma)让编译器支持C9X:
一般而言,编译器都有自己的编译指示集。例如,编译指示可能用于控制分配给自动变量的内存量,或者设置错误检查的严格程度,或者启用非标准语言特性等。C99 标准提供了 3 个标准编译指示。
 
C99还提供_Pragma预处理器运算符,该运算符把字符串转换成普通的编译指示。例如:
由于该运算符不使用#符号,所以可以把它作为宏展开的一部分:
然后,可以使用类似下面的代码:
 
顺带一提,下面的定义看上去没问题,但实际上无法正常运行:
问题在于这行代码依赖字符串的串联功能,而预处理过程完成之后才会串联字符串。 _Pragma运算符完成“解字符串”(destringizing)的工作,即把字符串中的转义序列转换成它所代表的字符。因此,
变成了:
 
 

泛型选择(C11)

在程序设计中,泛型编程指那些没有特定类型,但是一旦指定一种类型,就可以转换成指定类型的代码。C++在模板中可以创建泛型算法,然后编译器根据指定的类型自动使用实例化代码。C没有这种功能。然而,C11新增了一种表达式,叫作泛型选择表达式(generic selection expression),可根据表达式的类型(即表达式的类型是int、double 还是其他类型)选择一个值。泛型选择表达式不是预处理器指令,但是在一些泛型编程中它常用作#define宏定义的一部分。
 
一个泛型选择表达式的示例:
_Generic是C11的关键字。_Generic后面的圆括号中包含多个用逗号分隔的项。第1个项是一个表达式,后面的每个项都由一个类型、一个冒号和一个值组成,如float: 1。第1个项的类型匹配哪个标签,整个表达式的值是该标签后面的值。例如,假设上面表达式中x是int类型的变量,x的类型匹配int:标签,那么整个表达式的值就是0。如果没有与类型匹配的标签,表达式的值就是default:标签后面的值。泛型选择语句与 switch 语句类似,只是前者用表达式的类型匹配标签,而后者用表达式的值匹配标签。
下面是一个把泛型选择语句和宏定义组合的例子:
宏必须定义为一条逻辑行,但是可以用\把一条逻辑行分隔成多条物理行。在这种情况下,对泛型选择表达式求值得字符串。例如,对MYTYPE(5)求值得"int",因为值5的类型与int:标签匹配。
对一个泛型选择表达式求值时,程序不会先对第一个项求值,它只确定类型。只有匹配标签的类型后才会对表达式求值。可以像使用独立类型(“泛型”)函数那样使用_Generic 定义宏。
 
 

内联函数(C99)

通常,函数调用都有一定的开销,因为函数的调用过程包括建立调用、传递参数、跳转到函数代码并返回。使用宏使代码内联,可以避免这样的开销。C99还提供另一种方法:内联函数(inline function)。
 
C99和C11标准中叙述的是:“把函数变成内联函数建议尽可能快地调用该函数,其具体效果由实现定义”。因此,把函数变成内联函数,编译器可能会用内联代码替换函数调用,并(或)执行一些其他的优化,但是也可能不起作用。
 
创建内联函数的定义有多种方法。标准规定具有内部链接的函数可以成为内联函数,还规定了内联函数的定义与调用该函数的代码必须在同一个文件中。因此,最简单的方法是使用函数说明符 inline 和存储类别说明符static。通常,内联函数应定义在首次使用它的文件中,所以内联函数也相当于函数原型。如下所示:
 
编译器查看内联函数的定义(也是原型),可能会用函数体中的代码替换 eatline()函数调用。也就是说,效果相当于在函数调用的位置输入函数体中的代码:
由于并未给内联函数预留单独的代码块,所以无法获得内联函数的地址(实际上可以获得地址,不过这样做之后,编译器会生成一个非内联函数)。另外,内联函数无法在调试器中显示。 内联函数应该比较短小。把较长的函数变成内联并未节约多少时间,因为执行函数体的时间比调用函数的时间长得多。 编译器优化内联函数必须知道该函数定义的内容。这意味着内联函数定义与函数调用必须在同一个文件中。鉴于此,一般情况下内联函数都具有内部链接。因此,如果程序有多个文件都要使用某个内联函数,那么这些文件中都必须包含该内联函数的定义。最简单的做法是,把内联函数定义放入头文件,并在使用该内联函数的文件中包含该头文件即可。
 
一般都不在头文件中放置可执行代码,内联函数是个特例。因为内联函数具有内部链接,所以在多个文件中定义同一个内联函数不会产生什么问题。 与C++不同的是,C还允许混合使用内联函数定义和外部函数定义(具有外部链接的函数定义)。
例如,一个程序中使用下面3个文件:
3个文件中都定义了square()函数。file1.c文件中是inline static定义;file2.c 文件中是普通的函数定义(因此具有外部链接);file3.c 文件中是 inline 定义,省略了static。
3个文件中的函数都调用了square()函数,这会发生什么情况?。file1.c文件中的main()使用square()的局部static定义。由于该定义也是inline定义,所以编译器有可能优化代码,也许会内联该函数。file2.c 文件中,spam()函数使用该文件中 square()函数的定义,该定义具有外部链接,其他文件也可见。file3.c文件中,编译器既可以使用该文件中square()函数的内联定义,也可以使用file2.c文件中的外部链接定义。如果像file3.c那样,省略file1.c文件inline定义中的static,那么该inline定义被视为可替换的外部定义。
GCC在C99之前就使用一些不同的规则实现了内联函数,所以GCC可以根据当前编译器的标记来解释inline。
 
 

_Noreturn函数(C11)

C99新增inline关键字时,它是唯一的函数说明符(关键字extern和static是存储类别说明符,可应用于数据对象和函数)。C11新增了第2个函数说明符_Noreturn,表明调用完成后函数不返回主调函数。exit()函数是_Noreturn 函数的一个示例,一旦调用exit(),它不会再返回主调函数。注意,这与void返回类型不同。void类型的函数在执行完毕后返回主调函数,只是它不提供返回值。 _Noreturn的目的是告诉用户和编译器,这个特殊的函数不会把控制返回主调程序。告诉用户以免滥用该函数,通知编译器可优化一些代码
 
  • C
  • 位操作C库
    目录