js333 > 计算机互联网 > C语言预处理指令,程序员笔记预处理

原标题:C语言预处理指令,程序员笔记预处理

浏览次数:139 时间:2019-11-12

【好程序员笔记分享】——预处理,程序员笔记预处理

iOS培训------我的c语言笔记,期待与您交流!

其实在C语言的远行过程中,有这样一个流程,

编译:C----〉可执行文件(可以运行的)

1:.C------.i 预处理(之前和之后还是C语法)
2: .i-------.s 编译(之前是C语法,之后是汇编语法)
3: .s------.o 汇编
4: .o------可执行程序 链接

下面我们就来说说一说第一个步骤预处理阶段:

C语言预处理命令详解

一  前言

     预处理(或称预编译)是指在进行编译的第一遍扫描(词法扫描和语法分析)之前所作的工作。预处理指令指示在程序正式编译前就由编译器进行的操作,可放在程序中任何位置。

 

     预处理是C语言的一个重要功能,它由预处理程序负责完成。当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。

 

     C语言提供多种预处理功能,主要处理#开始的预编译指令,如宏定义(#define)、文件包含(#include)、条件编译(#ifdef)等。合理使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。

 

     本文参考诸多资料,详细介绍常用的几种预处理功能。因成文较早,资料来源大多已不可考,敬请谅解。

 

 

 

 

 

二  宏定义

     C语言源程序中允许用一个标识符来表示一个字符串,称为“宏”。被定义为宏的标识符称为“宏名”。在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去代换,这称为宏替换或宏展开。

 

     宏定义是由源程序中的宏定义命令完成的。宏替换是由预处理程序自动完成的。

 

     在C语言中,宏定义分为有参数和无参数两种。下面分别讨论这两种宏的定义和调用。

 

2.1 无参宏定义

     无参宏的宏名后不带参数。其定义的一般形式为:

 

        #define  标识符  字符串

 

     其中,“#”表示这是一条预处理命令(以#开头的均为预处理命令)。“define”为宏定义命令。“标识符”为符号常量,即宏名。“字符串”可以是常数、表达式、格式串等。

 

     宏定义用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名。这只是一种简单的文本替换,预处理程序对它不作任何检查。如有错误,只能在编译已被宏展开后的源程序时发现。

 

     注意理解宏替换中“换”的概念,即在对相关命令或语句的含义和功能作具体分析之前就要进行文本替换。

 

   【例1】定义常量:

 

1 #define MAX_TIME 1000

     若在程序里面写if(time < MAX_TIME){.........},则编译器在处理该代码前会将MAX_TIME替换为1000。

 

     注意,这种情况下使用const定义常量可能更好,如const int MAX_TIME = 1000;。因为const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行简单的字符文本替换,没有类型安全检查,并且在字符替换时可能会产生意料不到的错误。

 

    【例2】反例:

 

1 #define pint (int*)

2 pint pa, pb;

     本意是定义pa和pb均为int型指针,但实际上变成int* pa,pb;。pa是int型指针,而pb是int型变量。本例中可用typedef来代替define,这样pa和pb就都是int型指针了。因为宏定义只是简单的字符串代换,在预处理阶段完成,而typedef是在编译时处理的,它不是作简单的代换,而是对类型说明符重新命名,被命名的标识符具有类型定义说明的功能。typedef的具体说明见附录6.4。

 

     无参宏注意事项:

 

宏名一般用大写字母表示,以便于与变量区别。

宏定义末尾不必加分号,否则连分号一并替换。

宏定义可以嵌套。

可用#undef命令终止宏定义的作用域。

使用宏可提高程序通用性和易读性,减少不一致性,减少输入错误和便于修改。如数组大小常用宏定义。

预处理是在编译之前的处理,而编译工作的任务之一就是语法检查,预处理不做语法检查。

宏定义写在函数的花括号外边,作用域为其后的程序,通常在文件的最开头。

字符串" "中永远不包含宏,否则该宏名当字符串处理。

宏定义不分配内存,变量定义分配内存。

2.2 带参宏定义

     C语言允许宏带有参数。在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数。

 

     对带参数的宏,在调用中,不仅要宏展开,而且要用实参去代换形参。

 

     带参宏定义的一般形式为:

 

       #define  宏名(形参表)  字符串

 

     在字符串中含有各个形参。

 

     带参宏调用的一般形式为:

 

宏名(实参表);

 

     在宏定义中的形参是标识符,而宏调用中的实参可以是表达式。

 

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

 

    【例3】

 

1 #define INC(x) x+1  //宏定义

2 y = INC(5);         //宏调用

     在宏调用时,用实参5去代替形参x,经预处理宏展开后的语句为y=5+1。

 

    【例4】反例:

 

1 #define SQ(r) r*r

     上述这种实参为表达式的宏定义,在一般使用时没有问题;但遇到如area=SQ(a+b);时就会出现问题,宏展开后变为area=a+b*a+b;,显然违背本意。

 

     相比之下,函数调用时会先把实参表达式的值(a+b)求出来再赋予形参r;而宏替换对实参表达式不作计算直接地照原样代换。因此在宏定义中,字符串内的形参通常要用括号括起来以避免出错。

 

     进一步地,考虑到运算符优先级和结合性,遇到area=10/SQ(a+b);时即使形参加括号仍会出错。因此,还应在宏定义中的整个字符串外加括号,

 

     综上,正确的宏定义是#define SQ(r) ((r)*(r)),即宏定义时建议所有的层次都要加括号。

 

    【例5】带参函数和带参宏的区别:

 

复制代码

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

 2 int Square(int x){

 3     return (x * x); //未考虑溢出保护

 4 }

 5 

 6 int main(void){

 7     int i = 1;

 8     while(i <= 5)

 9         printf("i = %d, Square = %dn", i, Square(i++));

10 

11     int j = 1;

12     while(j <= 5)

13         printf("j = %d, SQUARE = %dn", j, SQUARE(j++));

14     

15     return 0;

16 }

复制代码

     执行后输出如下:

 

复制代码

1 i = 2, Square = 1

2 i = 3, Square = 4

3 i = 4, Square = 9

4 i = 5, Square = 16

5 i = 6, Square = 25

6 j = 3, SQUARE = 1

7 j = 5, SQUARE = 9

8 j = 7, SQUARE = 25

复制代码

     本例意在说明,把同一表达式用函数处理与用宏处理两者的结果有可能是不同的。

 

     调用Square函数时,把实参i值传给形参x后自增1,再输出函数值。因此循环5次,输出1~5的平方值。

 

     调用SQUARE宏时,SQUARE(j++)被代换为((j++)*(j++))。在第一次循环时,表达式中j初值为1,两者相乘的结果为1。相乘后j自增两次变为3,因此表达式中第二次相乘时结果为3*3=9。同理,第三次相乘时结果为5*5=25,并在此次循环后j值变为7,不再满足循环条件,停止循环。

 

     从以上分析可以看出函数调用和宏调用二者在形式上相似,在本质上是完全不同的。

 

     带参宏注意事项:

 

宏名和形参表的括号间不能有空格。

宏替换只作替换,不做计算,不做表达式求解。

函数调用在编译后程序运行时进行,并且分配内存。宏替换在编译前进行,不分配内存。

宏的哑实结合不存在类型,也没有类型转换。

函数只有一个返回值,利用宏则可以设法得到多个值。

宏展开使源程序变长,函数调用不会。

宏展开不占用运行时间,只占编译时间,函数调用占运行时间(分配内存、保留现场、值传递、返回值)。

为防止无限制递归展开,当宏调用自身时,不再继续展开。如:#define TEST(x)  (x + TEST(x))被展开为1 + TEST(1)。

2.3 实践用例

     包括基本用法(及技巧)和特殊用法(#和##等)。

 

     #define可以定义多条语句,以替代多行的代码,但应注意替换后的形式,避免出错。宏定义在换行时要加上一个反斜杠””,而且反斜杠后面直接回车,不能有空格。

 

2.3.1 基本用法

     1. 定义常量:

 

1 #define PI   3.1415926

     将程序中出现的PI全部换成3.1415926。

 

     2. 定义表达式:

 

1 #define M   (y*y+3*y)

     编码时所有的表达式(y*y+3*y)都可由M代替,而编译时先由预处理程序进行宏替换,即用(y*y+3*y)表达式去置换所有的宏名M,然后再进行编译。

 

     注意,在宏定义中表达式(y*y+3*y)两边的括号不能少,否则可能会发生错误。如s=3*M+4*M在预处理时经宏展开变为s=3*(y*y+3*y)+4*(y*y+3*y),如果宏定义时不加括号就展开为s=3*y*y+3*y+4*y*y+3*y,显然不符合原意。因此在作宏定义时必须十分注意。应保证在宏替换之后不发生错误。

 

     3. 得到指定地址上的一个字节或字:

 

1 #define MEM_B(x)     (*((char *)(x)))

2 #define MEM_W(x)     (*((short *)(x)))

     4. 求最大值和最小值:

 

1 #define MAX(x, y)     (((x) > (y)) ? (x) : (y))

2 #define MIN(x, y)     (((x) < (y)) ? (x) : (y))

     以后使用MAX (x,y)或MIN (x,y),就可分别得到x和y中较大或较小的数。

 

     但这种方法存在弊病,例如执行MAX(a++, b)时,a++被执行多少次取决于a和b的大小!所以建议用内联函数而不是这种方法提高速度。不过,虽然存在这样的弊病,但宏定义非常灵活,因为a和b可以是各种数据类型。

 

     5. 得到一个成员在结构体中的偏移量(lint 545告警表示"&用法值得怀疑",此处抑制该警告):

 

1 #define FPOS( type, field )

2 /*lint -e545 */ ((int)&((type *)0)-> field) /*lint +e545 */

     6. 得到一个结构体中某成员所占用的字节数:

 

1 #define FSIZ(type, field)    sizeof(((type *)0)->field)

     7. 按照LSB格式把两个字节转化为一个字(word):

 

1 #define FLIPW(arr)          ((((short)(arr)[0]) * 256) + (arr)[1])

     8. 按照LSB格式把一个字(word)转化为两个字节:

 

1 #define FLOPW(arr, val )

2     (arr)[0] = ((val) / 256);

3     (arr)[1] = ((val) & 0xFF)

     9. 得到一个变量的地址:

 

1 #define B_PTR(var)       ((char *)(void *)&(var))

2 #define W_PTR(var)       ((short *)(void *)&(var))

     10. 得到一个字(word)的高位和低位字节:

 

1 #define WORD_LO(x)       ((char)((short)(x)&0xFF))

2 #define WORD_HI(x)       ((char)((short)(x)>>0x8))

     11. 返回一个比X大的最接近的8的倍数:

 

1 #define RND8(x)           ((((x) + 7) / 8) * 8)

     12. 将一个字母转换为大写:

 

1 #define UPCASE(c)         (((c) >= 'a' && (c) <= 'z') ? ((c) - 0x20) : (c))

     13. 判断字符是不是10进值的数字:

 

1 #define ISDEC(c)          ((c) >= '0' && (c) <= '9')

     14. 判断字符是不是16进值的数字:

 

1 #define ISHEX(c)          (((c) >= '0' && (c) <= '9') ||

2     ((c) >= 'A' && (c) <= 'F') ||

3     ((c) >= 'a' && (c) <= 'f'))

     15. 防止溢出的一个方法:

 

1 #define INC_SAT(val)      (val = ((val)+1 > (val)) ? (val)+1 : (val))

     16. 返回数组元素的个数:

 

1 #define ARR_SIZE(arr)     (sizeof((arr)) / sizeof((arr[0])))

     17. 对于IO空间映射在存储空间的结构,输入输出处理:

 

1 #define INP(port)           (*((volatile char *)(port)))

2 #define INPW(port)          (*((volatile short *)(port)))

3 #define INPDW(port)         (*((volatile int *)(port)))

4 #define OUTP(port, val)     (*((volatile char *)(port)) = ((char)(val)))

5 #define OUTPW(port, val)    (*((volatile short *)(port)) = ((short)(val)))

6 #define OUTPDW(port, val)   (*((volatile int *)(port)) = ((int)(val)))

     18. 使用一些宏跟踪调试:

 

     ANSI标准说明了五个预定义的宏名(注意双下划线),即:__LINE__、__FILE __、__DATE__、__TIME__、__STDC __。

 

     若编译器未遵循ANSI标准,则可能仅支持以上宏名中的几个,或根本不支持。此外,编译程序可能还提供其它预定义的宏名(如__FUCTION__)。

 

     __DATE__宏指令含有形式为月/日/年的串,表示源文件被翻译到代码时的日期;源代码翻译到目标代码的时间作为串包含在__TIME__中。串形式为时:分:秒。

 

     如果实现是标准的,则宏__STDC__含有十进制常量1。如果它含有任何其它数,则实现是非标准的。

 

     可以借助上面的宏来定义调试宏,输出数据信息和所在文件所在行。如下所示:

 

1 #define MSG(msg, date)      printf(msg);printf(“[%d][%d][%s]”,date,__LINE__,__FILE__)

     19. 用do{…}while(0)语句包含多语句防止错误:

 

1 #define DO(a, b) do{

2     a+b;

3     a++;

4 }while(0)

     20. 实现类似“重载”功能

 

     C语言中没有swap函数,而且不支持重载,也没有模板概念,所以对于每种数据类型都要写出相应的swap函数,如:

 

1 IntSwap(int *,  int *);  

2 LongSwap(long *,  long *);  

3 StringSwap(char *,  char *); 

     可采用宏定义SWAP(t,x,y)以交换t类型的两个参数(要使用程序块结构):

 

复制代码

 1 #define SWAP(t, x, y) do{

 2     t temp = *y;

 3     *y = *x;

 4     *x = temp;

 5 }while(0)

 6 

 7 int main(void){

 8     int a = 10, b = 5;

 9     SWAP(int, &a, &b);

10     printf("a=%d, b=%dn", a, b);

11     return 0;

12 }

复制代码

     21. 1年中有多少秒(忽略闰年问题) :

 

1 #define SECONDS_PER_YEAR    (60UL * 60 * 24 * 365)

     该表达式将使一个16位机的整型数溢出,因此用长整型符号L告诉编译器该常数为长整型数。

 

     注意,不可定义为#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL,否则将产生(31536000)UL而非31536000UL,这会导致编译报错。

 

     以下几种写法也正确:

 

1 #define SECONDS_PER_YEAR    60 * 60 * 24 * 365UL

2 #define SECONDS_PER_YEAR    (60UL * 60UL * 24UL * 365UL)

3 #define SECONDS_PER_YEAR    ((unsigned long)(60 * 60 * 24 * 365))

     22. 取消宏定义:

 

          #define [MacroName] [MacroValue]       //定义宏

 

          #undef [MacroName]                               //取消宏

 

     宏定义必须写在函数外,其作用域为宏定义起到源程序结束。如要终止其作用域可使用#undef命令:

 

复制代码

1 #define PI   3.14159

2 int main(void){

3     //……

4 }

5 #undef PI

6 int func(void){

7     //……

8 }

复制代码

     表示PI只在main函数中有效,在func1中无效。

 

2.3.2 特殊用法

     主要涉及C语言宏里#和##的用法,以及可变参数宏。

 

2.3.2.1 字符串化操作符#

     在C语言的宏中,#的功能是将其后面的宏参数进行字符串化操作(Stringfication),简单说就是将宏定义中的传入参数名转换成用一对双引号括起来参数名字符串。#只能用于有传入参数的宏定义中,且必须置于宏定义体中的参数名前。例如:

 

1 #define EXAMPLE(instr)      printf("The input string is:t%sn", #instr)

2 #define EXAMPLE1(instr)     #instr

     当使用该宏定义时,example(abc)在编译时将会展开成printf("the input string is:t%sn","abc");string str=example1(abc)将会展成string str="abc"。

 

     又如下面代码中的宏:

 

1 define WARN_IF(exp) do{

2     if(exp)

3         fprintf(stderr, "Warning: " #exp"n");

4 }while(0)

     则代码WARN_IF (divider == 0)会被替换为:

 

1 do{

2     if(divider == 0)

3         fprintf(stderr, "Warning" "divider == 0" "n");

4 }while(0)

     这样,每次divider(除数)为0时便会在标准错误流上输出一个提示信息。

 

     注意#宏对空格的处理:

 

忽略传入参数名前面和后面的空格。如str= example1(   abc )会被扩展成 str="abc"。

当传入参数名间存在空格时,编译器会自动连接各个子字符串,每个子字符串间只以一个空格连接。如str= example1( abc    def)会被扩展成 str="abc def"。

2.3.2.2 符号连接操作符##

     ##称为连接符(concatenator或token-pasting),用来将两个Token连接为一个Token。注意这里连接的对象是Token就行,而不一定是宏的变量。例如:

 

1 #define PASTER(n)     printf( "token" #n " = %d", token##n)

2 int token9 = 9;

     则运行PASTER(9)后输出结果为token9 = 9。

 

     又如要做一个菜单项命令名和函数指针组成的结构体数组,并希望在函数名和菜单项命令名之间有直观的、名字上的关系。那么下面的代码就非常实用:

 

1 struct command{

2     char * name;

3     void (*function)(void);

4 };

5 #define COMMAND(NAME)   {NAME, NAME##_command}

     然后,就可用一些预先定义好的命令来方便地初始化一个command结构的数组:

 

1 struct command commands[] = {

2     COMMAND(quit),

3     COMMAND(help),

4     //...

5 }

     COMMAND宏在此充当一个代码生成器的作用,这样可在一定程度上减少代码密度,间接地也可减少不留心所造成的错误。

 

     还可以用n个##符号连接n+1个Token,这个特性是#符号所不具备的。如:

 

1 #define  LINK_MULTIPLE(a, b, c, d)      a##_##b##_##c##_##d

2 typedef struct record_type LINK_MULTIPLE(name, company, position, salary);

     这里这个语句将展开为typedef struct record_type name_company_position_salary。

 

     注意:

 

当用##连接形参时,##前后的空格可有可无。

连接后的实际参数名,必须为实际存在的参数名或是编译器已知的宏定义。

凡是宏定义里有用'#'或'##'的地方,宏参数是不会再展开。如:

1 #define STR(s)       #s

2 #define CONS(a,b)    int(a##e##b)

     则printf("int max: %sn", STR(INT_MAX))会被展开为printf("int max: %sn", "INT_MAX")。其中,变量INT_MAX为int型的最大值,其值定义在<climits.h>中。printf("%sn", CONS(A, A))会被展开为printf("%sn", int(AeA)),从而编译报错。

 

     INT_MAX和A都不会再被展开,多加一层中间转换宏即可解决这个问题。加这层宏是为了把所有宏的参数在这层里全部展开,那么在转换宏里的那一个宏(如_STR)就能得到正确的宏参数。

 

1 #define _STR(s)         #s 

2 #define STR(s)          _STR(s)       // 转换宏

3 #define _CONS(a,b)      int(a##e##b)

4 #define CONS(a,b)       _CONS(a,b)    // 转换宏

     则printf("int max: %sn", STR(INT_MAX))输出为int max: 0x7fffffff;而printf("%dn", CONS(A, A))输出为200。

 

     这种分层展开的技术称为宏的Argument Prescan,参见附录6.1。

 

    【'#'和'##'的一些应用特例】

 

     1. 合并匿名变量名

 

1 #define ___ANONYMOUS1(type, var, line)   type  var##line

2 #define __ANONYMOUS0(type, line)         ___ANONYMOUS1(type, _anonymous, line)

3 #define ANONYMOUS(type)                  __ANONYMOUS0(type, __LINE__)

     例:ANONYMOUS(static int)即static int _anonymous70,70表示该行行号。

 

     第一层:ANONYMOUS(static int)  →  __ANONYMOUS0(static int, __LINE__)

 

     第二层:                                   →  ___ANONYMOUS1(static int, _anonymous, 70)

 

     第三层:                                   →  static int _anonymous70

 

     即每次只能解开当前层的宏,所以__LINE__在第二层才能被解开。

 

     2. 填充结构

 

复制代码

 1 #define FILL(a)   {a, #a} 

 2 

 3 enum IDD{OPEN, CLOSE};

 4 typedef struct{

 5     IDD id;

 6     const char * msg; 

 7 }T_MSG;

复制代码

     则T_MSG tMsg[ ] = {FILL(OPEN), FILL(CLOSE)}相当于:

 

1 T_MSG tMsg[] = {{OPEN,  "OPEN"},

2                 {CLOSE, "CLOSE"}};

     3. 记录文件名

 

1 #define _GET_FILE_NAME(f)     #f

2 #define GET_FILE_NAME(f)      _GET_FILE_NAME(f)

3 static char  FILE_NAME[] = GET_FILE_NAME(__FILE__);

     4. 得到一个数值类型所对应的字符串缓冲大小

 

1 #define _TYPE_BUF_SIZE(type)   sizeof #type

2 #define TYPE_BUF_SIZE(type)    _TYPE_BUF_SIZE(type)

3 char  buf[TYPE_BUF_SIZE(INT_MAX)];

4      //-->  char  buf[_TYPE_BUF_SIZE(0x7fffffff)];

5      //-->  char  buf[sizeof "0x7fffffff"];

     这里相当于:char  buf[11]; 

 

2.3.2.3 字符化操作符@#

     @#称为字符化操作符(charizing),只能用于有传入参数的宏定义中,且必须置于宏定义体的参数名前。作用是将传入的单字符参数名转换成字符,以一对单引号括起来。

 

1 #define makechar(x)    #@x

2 a = makechar(b);

     展开后变成a= 'b'。 

 

2.3.2.4 可变参数宏

     ...在C语言宏中称为Variadic Macro,即变参宏。C99编译器标准允许定义可变参数宏(Macros with a Variable Number of Arguments),这样就可以使用拥有可变参数表的宏。

 

     可变参数宏的一般形式为:

 

                        #define  DBGMSG(format, ...)  fprintf (stderr, format, __VA_ARGS__)

 

     省略号代表一个可以变化的参数表,变参必须作为参数表的最右一项出现。使用保留名__VA_ARGS__ 把参数传递给宏。在调用宏时,省略号被表示成零个或多个符号(包括里面的逗号),一直到到右括号结束为止。当被调用时,在宏体(macro body)中,那些符号序列集合将代替里面的__VA_ARGS__标识符。当宏的调用展开时,实际的参数就传递给fprintf ()。

 

     注意:可变参数宏不被ANSI/ISO C++所正式支持。因此,应当检查编译器是否支持这项技术。 

 

     在标准C里,不能省略可变参数,但却可以给它传递一个空的参数,这会导致编译出错。因为宏展开后,里面的字符串后面会有个多余的逗号。为解决这个问题,GNU CPP中做了如下扩展定义:

 

                       #define  DBGMSG(format, ...)  fprintf (stderr, format, ##__VA_ARGS__)

 

     若可变参数被忽略或为空,##操作将使编译器删除它前面多余的逗号(否则会编译出错)。若宏调用时提供了可变参数,编译器会把这些可变参数放到逗号的后面。

 

     同时,GCC还支持显式地命名变参为args,如同其它参数一样。如下格式的宏扩展:

 

                              #define  DBGMSG(format, args...)  fprintf (stderr, format, ##args)

 

     这样写可读性更强,并且更容易进行描述。

 

     用GCC和C99的可变参数宏, 可以更方便地打印调试信息,如:

 

1 #ifdef DEBUG

2     #define DBGPRINT(format, args...)

3         fprintf(stderr, format, ##args)

4 #else

5     #define DBGPRINT(format, args...)

6 #endif

     这样定义之后,代码中就可以用dbgprint了,例如dbgprint ("aaa [%s]", __FILE__)。

 

     结合第4节的“条件编译”功能,可以构造出如下调试打印宏:

 

复制代码

 1 #ifdef LOG_TEST_DEBUG

 2     /* OMCI调试日志宏 */

 3     //以10进制格式日志整型变量

 4     #define PRINT_DEC(x)          printf(#x" = %dn", x)

 5     #define PRINT_DEC2(x,y)       printf(#x" = %dn", y)

 6     //以16进制格式日志整型变量

 7     #define PRINT_HEX(x)          printf(#x" = 0x%-Xn", x)

 8     #define PRINT_HEX2(x,y)       printf(#x" = 0x%-Xn", y)

 9     //以字符串格式日志字符串变量

10     #define PRINT_STR(x)          printf(#x" = %sn", x)

11     #define PRINT_STR2(x,y)       printf(#x" = %sn", y)

12 

13     //日志提示信息

14     #define PROMPT(info)          printf("%sn", info)

15 

16     //调试定位信息打印宏

17     #define  TP                   printf("%-4u - [%s<%s>]n", __LINE__, __FILE__, __FUNCTION__);

18 

19     //调试跟踪宏,在待日志信息前附加日志文件名、行数、函数名等信息

20     #define TRACE(fmt, args...)

21     do{

22         printf("[%s(%d)<%s>]", __FILE__, __LINE__, __FUNCTION__);

23         printf((fmt), ##args);

24     }while(0)

25 #else

26     #define PRINT_DEC(x)

27     #define PRINT_DEC2(x,y)

28 

29     #define PRINT_HEX(x)

30     #define PRINT_HEX2(x,y)

31 

32     #define PRINT_STR(x)

33     #define PRINT_STR2(x,y)

34 

35     #define PROMPT(info)

36 

37     #define  TP

38 

39     #define TRACE(fmt, args...)

40 #endif

复制代码

 

 

 

 

三  文件包含

     文件包含命令行的一般形式为:

 

#include"文件名"

 

     通常,该文件是后缀名为"h"或"hpp"的头文件。文件包含命令把指定头文件插入该命令行位置取代该命令行,从而把指定的文件和当前的源程序文件连成一个源文件。

 

     在程序设计中,文件包含是很有用的。一个大程序可以分为多个模块,由多个程序员分别编程。有些公用的符号常量或宏定义等可单独组成一个文件,在其它文件的开头用包含命令包含该文件即可使用。这样,可避免在每个文件开头都去书写那些公用量,从而节省时间,并减少出错。

 

     对文件包含命令要说明以下几点:

 

包含命令中的文件名可用双引号括起来,也可用尖括号括起来,如#include "common.h"和#include<math.h>。但这两种形式是有区别的:使用尖括号表示在包含文件目录中去查找(包含目录是由用户在设置环境时设置的include目录),而不在当前源文件目录去查找;使用双引号则表示首先在当前源文件目录中查找,若未找到才到包含目录中去查找。用户编程时可根据自己文件所在的目录来选择某一种命令形式。

一个include命令只能指定一个被包含文件,若有多个文件要包含,则需用多个include命令。

文件包含允许嵌套,即在一个被包含的文件中又可以包含另一个文件。

 

 

 

 

四  条件编译

     一般情况下,源程序中所有的行都参加编译。但有时希望对其中一部分内容只在满足一定条件才进行编译,也就是对一部分内容指定编译的条件,这就是“条件编译”。有时,希望当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句。

 

     条件编译功能可按不同的条件去编译不同的程序部分,从而产生不同的目标代码文件。这对于程序的移植和调试是很有用的。

 

     条件编译有三种形式,下面分别介绍。

 

4.1 #ifdef形式

#ifdef  标识符  (或#if defined标识符)

 

    程序段1

 

#else

 

    程序段2

 

#endif

 

     如果标识符已被#define命令定义过,则对程序段1进行编译;否则对程序段2进行编译。如果没有程序段2(它为空),#else可以没有,即可以写为:

 

#ifdef  标识符  (或#if defined标识符)

 

    程序段

 

#endif

 

     这里的“程序段”可以是语句组,也可以是命令行。这种条件编译可以提高C源程序的通用性。

 

    【例6】

 

复制代码

 1 #define NUM OK

 2 int main(void){

 3     struct stu{

 4         int num;

 5         char *name;

 6         char sex;

 7         float score;

 8     }*ps;

 9     ps=(struct stu*)malloc(sizeof(struct stu));

10     ps->num = 102;

11     ps->name = "Zhang ping";

12     ps->sex = 'M';

13     ps->score = 62.5;

14 #ifdef NUM

15     printf("Number=%dnScore=%fn", ps->num, ps->score); /*--Execute--*/

16 #else

17     printf("Name=%snSex=%cn", ps->name, ps->sex);

18 #endif

19     free(ps);

20     return 0;

21 }

复制代码

     由于在程序中插入了条件编译预处理命令,因此要根据NUM是否被定义过来决定编译哪个printf语句。而程序首行已对NUM作过宏定义,因此应对第一个printf语句作编译,故运行结果是输出了学号和成绩。

 

     程序首行定义NUM为字符串“OK”,其实可为任何字符串,甚至不给出任何字符串,即#define NUM也具有同样的意义。只有取消程序首行宏定义才会去编译第二个printf语句。

 

4.2 #ifndef形式

#ifndef  标识符

 

    程序段1

 

#else

 

    程序段2

 

#endif

 

     如果标识符未被#define命令定义过,则对程序段1进行编译,否则对程序段2进行编译。这与#ifdef形式的功能正相反。

 

     “#ifndef  标识符”也可写为“#if  !(defined 标识符)”。

 

4.3 #if形式

#if 常量表达式

 

    程序段1

 

#else

 

    程序段2

 

#endif

 

     如果常量表达式的值为真(非0),则对程序段1 进行编译,否则对程序段2进行编译。因此可使程序在不同条件下,完成不同的功能。

 

    【例7】输入一行字母字符,根据需要设置条件编译,使之能将字母全改为大写或小写字母输出。

 

复制代码

 1 #define LETTER 1

 2 int main(void){

 3     char str[20]="C Language",c;

 4     int i="0";

 5     while((c=str[i])!=''){

 6         i++;

 7 #if LETTER

 8         if(c>='a'&&c<='z') c="c-32";

 9 #else

10         if(c>='A'&&c<='Z') c="c"+32;

11 #endif

12         printf("%c",c);

13     }

14     return 0;

15 }

复制代码

     在程序第一行定义宏LETTER为1,因此在条件编译时常量表达式LETTER的值为真(非零),故运行后使小写字母变成大写(C LANGUAGE)。

 

     本例的条件编译当然也可以用if条件语句来实现。但是用条件语句将会对整个源程序进行编译,生成的目标代码程序很长;而采用条件编译,则根据条件只编译其中的程序段1或程序段2,生成的目标程序较短。如果条件编译的程序段很长,采用条件编译的方法是十分必要的。

 

4.4 实践用例

     1. 屏蔽跨平台差异

 

     在大规模开发过程中,特别是跨平台和系统的软件里,可以在编译时通过条件编译设置编译环境。

 

     例如,有一个数据类型,在Windows平台中应使用long类型表示,而在其他平台应使用float表示。这样往往需要对源程序作必要的修改,这就降低了程序的通用性。可以用以下的条件编译:

 

#ifdef WINDOWS

    #define MYTYPE long

#else

    #define MYTYPE float

#endif

     如果在Windows上编译程序,则可以在程序的开始加上#define WINDOWS,这样就编译命令行    #define MYTYPE long;如果在这组条件编译命令前曾出现命令行#define WINDOWS 0,则预编译后程序中的MYTYPE都用float代替。这样,源程序可以不必作任何修改就可以用于不同类型的计算机系统。

 

     2. 包含程序功能模块

 

     例如,在程序首部定义#ifdef FLV:

 

1 #ifdef FLV

2     include"fastleave.c"

3 #endif

     如果不许向别的用户提供该功能,则在编译之前将首部的FLV加一下划线即可。

 

     3. 开关调试信息

 

     调试程序时,常常希望输出一些所需的信息以便追踪程序的运行。而在调试完成后不再输出这些信息。可以在源程序中插入以下的条件编译段:

 

1 #ifdef DEBUG

2     printf("device_open(%p)n", file);

3 #endif

     如果在它的前面有以下命令行#define DEBUG,则在程序运行时输出file指针的值,以便调试分析。调试完成后只需将这个define命令行删除即可,这时所有使用DEBUG作标识符的条件编译段中的printf语句不起作用,即起到“开关”一样统一控制的作用。 

 

     4. 避开硬件的限制。

 

     有时一些具体应用环境的硬件不同,但限于条件本地缺乏这种设备,可绕过硬件直接写出预期结果:

 

1 #ifndef TEST

2     i = dial();  //程序调试运行时绕过此语句

3 #else

4     i = 0;

5 #endif

     调试通过后,再屏蔽TEST的定义并重新编译即可。   

 

     5. 防止头文件重复包含

 

     头文件(.h)可以被头文件或C文件包含。由于头文件包含可以嵌套,C文件就有可能多次包含同一个头文件;或者不同的C文件都包含同一个头文件,编译时就可能出现重复包含(重复定义)的问题。

 

     在头文件中为了避免重复调用(如两个头文件互相包含对方),常采用这样的结构:

 

1 #ifndef  <标识符>

2     #define  <标识符>

3     //真正的内容,如函数声明之类

4 #endif

     <标识符>可以自由命名,但一般形如__HEADER_H,且每个头文件标识都应该是唯一的。

 

     事实上,不管头文件会不会被多个文件引用,都要加上条件编译开关来避免重复包含。 

 

     6. 在#ifndef中定义变量出现的问题(一般不定义在#ifndef中)。

 

1 #ifndef PRECMPL

2     #define PRECMPL

3     int var;

4 #endif

     其中有个变量定义,在VC中链接时会出现变量var重复定义的错误,而在C中成功编译。

 

     (1) 当第一个使用这个头文件的.cpp文件生成.obj时,var在里面定义;当另一个使用该头文件的.cpp文件再次(单独)生成.obj时,var又被定义;然后两个obj被第三个包含该头文件.cpp连接在一起,会出现重复定义。

 

     (2) 把源程序文件扩展名改成.c后,VC按照C语言语法对源程序进行编译。在C语言中,遇到多个int var则自动认为其中一个是定义,其他的是声明。

 

     (3) C语言和C++语言连接结果不同,可能是在进行编译时,C++语言将全局变量默认为强符号,所以连接出错。C语言则依照是否初始化进行强弱的判断的(仅供参考)。

 

     解决方法:

 

     (1) 把源程序文件扩展名改成.c。

 

     (2) .h中只声明 extern int var;,在.cpp中定义(推荐)

 

复制代码

1 //<x.h>

2 #ifndef  __X_H

3     #define  __X_H

4     extern int var;

5 #endif

6 <x.c>

7 int var = 0;

复制代码

     综上,变量一般不要定义在.h文件中。

 

 

 

 

一 前言 预处理(或称预编译)是指在进行编译的第一遍扫描(词法扫描和语法分析)之前所作的工作。预处理指令指示在...

目录

一:预处理介绍

一.预处理的工作方式... 3

 

二:预处理中宏的定义无参宏定义
#define M (x+y+3+xy)带参宏定义
#define M(x,y) (x+xy+3+xy)三:宏的使用main(){
  int Num = M;  int Num2 = M(2,5);  printf("%d, %d", Num, Num2);
}这里我们就使用了上面定义的两个宏,也就是实现了预处理命令的使用

1.1.预处理的功能... 3

四:常用预处理命令

e>预定义的宏(不能用undef取消)
C标准中指定了一些预定义的宏,对于编程经常会用到。下面这个表中就是一些常常用到的预定义宏。
__DATE__
进行预处理的日期(“Mmm dd yyyy”形式的字符串文字)
__FILE__
代表当前源代码文件名的字符串文字
__LINE__
代表当前源代码中的行号的整数常量
__TIME__
源文件编译时间,格式微“hh:mm:ss”
__func__
当前所在函数名
对于__FILE__,__LINE__,__func__这样的宏,在调试程序时是很有用的,因为你可以很容易的知道程序运行到了哪个文件的那一行,是哪个函数。
下面一个例子是打印上面这些预定义的宏的。

1.2预处理的工作方式... 3

五:预处理总结

  • 1. 预处理功能是C语言特有的功能,它是在对源程序正式编译前由预处理程序完成的。程序员在程序中用预处理命令来调用这些功能。
  • 2. 宏定义是用一个标识符来表示一个字符串,这个字符串可以是常量、变量或表达式。在宏调用中将用该字符串代换宏名。
    1. 宏定义可以带有参数,宏调用时是以实参代换形参。而不是“值传送”。
  • 4. 为了避免宏代换时发生错误,宏定义中的字符串应加括号,字符串中出现的形式参数两边也应加括号。
  • 5. 文件包含是预处理的一个重要功能,它可用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。
  • 6. 条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。
  • 7. 使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计。

iOS培训------我的c语言笔记,期待与您交流! 其实在C语言的远行过程中,有这样一个...

二.预处理指令... 4

2.1.预处理指令... 4

2.2.指令规则... 4

三.宏定义命令----#define. 4

3.1.无参数的宏... 4

3.2带参数的宏... 5

3.3.预处理操作符#和##. 6

3.3.1.操作符#. 6

3.3.2.操作符##. 6

四.文件包含------include. 6

五.条件编译... 7

5.1使用#if 7

5.2使用#ifdef和#ifndef 9

5.3使用#defined和#undef 10

六.其他预处理命令... 11

6.1.预定义的宏名... 11

6.2.重置行号和文件名命令------------#line. 11

6.3.修改编译器设置命令 ------------#pragma. 12

6.4.产生错误信息命令 ------------#error 12

七.内联函数... 13

在嵌入式系统编程中不管是内核的驱动程序还是应用程序的编写,涉及到大量的预处理与条件编译,这样做的好处主要体现在代码的移植性强以及代码的修改方便等方面。因此引入了预处理与条件编译的概念。

在C语言的程序中可包括各种以符号#开头的编译指令,这些指令称为预处理命令。预处理命令属于C语言编译器,而不是C语言的组成部分。通过预处理命令可扩展C语言程序设计的环境。

一.预处理的工作方式

1.1.预处理的功能

在集成开发环境中,编译,链接是同时完成的。其实,C语言编译器在对源代码编译之前,还需要进一步的处理:预编译。预编译的主要作用如下:

将源文件中以”include”格式包含的文件复制到编译的源文件中。

用实际值替换用“#define”定义的字符串。

根据“#if”后面的条件决定需要编译的代码。

1.2预处理的工作方式

预处理的行为是由指令控制的。这些指令是由#字符开头的一些命令。

#define指令定义了一个宏---用来代表其他东西的一个命令,通常是某一个类型的常量。预处理会通过将宏的名字和它的定义存储在一起来响应#define指令。当这个宏在后面的程序中使用到时,预处理器”扩展”了宏,将宏替换为它所定义的值。

#include指令告诉预处理器打开一个特定的文件,将它的内容作为正在编译的文件的一部分“包含”进来。例如:下面这行命令:

#include

指示预处理器打开一个名字为stdio.h的文件,并将它的内容加到当前的程序中。

预处理器的输入是一个C语言程序,程序可能包含指令。预处理器会执行这些指令,并在处理过程中删除这些指令。预处理器的输出是另外一个程序:原程序的一个编辑后的版本,不再包含指令。预处理器的输出被直接交给编译器,编译器检查程序是否有错误,并经程序翻译为目标代码。

二.预处理指令

2.1.预处理指令

大多数预处理器指令属于下面3种类型:

宏定义:#define 指令定义一个宏,#undef指令删除一个宏定义。

文件包含:#include指令导致一个指定文件的内容被包含到程序中。

条件编译:#if,#ifdef,#ifndef,#elif,#else和#dendif指令可以根据编译器可以测试的条件来将一段文本包含到程序中或排除在程序之外。

剩下的#error,#line和#pragma指令更特殊的指令,较少用到。

2.2.指令规则

指令都是以#开始。#符号不需要在一行的行首,只要她之前有空白字符就行。在#后是指令名,接着是指令所需要的其他信息。

在指令的符号之间可以插入任意数量的空格或横向制表符。

指令总是第一个换行符处结束,除非明确地指明要继续。

指令可以出现在程序中德任何地方。我们通常将#define和#include指令放在文件的开始,其他指令则放在后面,甚至在函数定义的中间。

注释可以与指令放在同一行。

三.宏定义命令----#define

使用#define命令并不是真正的定义符号常量,而是定义一个可以替换的宏。被定义为宏的标示符称为“宏名”。在编译预处理过程时,对程序中所有出现的“宏名”,都用宏定义中的字符串去代换,这称为“宏代换”或“宏展开”。

在C语言中,宏分为有参数和无参数两种。

3.1.无参数的宏

其定义格式如下:

#define 宏名字符串

在以上宏定义语句中,各部分的含义如下:

#:表示这是一条预处理命令(凡是以“#”开始的均为预处理命令)。

define:关键字“define”为宏定义命令。

宏名:是一个标示符,必须符合C语言标示符的规定,一般以大写字母标示宏名。

字符串:可以是常数,表达式,格式串等。在前面使用的符号常量的定义就是一个无参数宏定义。

Notice:

预处理命令语句后面一般不会添加分号,如果在#define最后有分号,在宏替换时分号也将替换到源代码中去。在宏名和字符串之间可以有任意个空格。

Eg:#define PI 3.14

在使用宏定义时,还需要注意以下几点:

宏定义是宏名来表示一个字符串,在宏展开时又以该字符串取代宏名。这只是一种简单的代换,字符串中可以含任何字符,可以是常数,也可以是表达式,预处理程序对它不作任何检查。如有错误,只能在编译已被宏展开后的源程序时发现。

宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。

宏名在源程序只能够若用引号括起来,则预处理程序不对其作宏替换。

宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名。在宏展开时由预处理程序层层替换。

习惯上宏名可用大写字母表示,以方便与变量区别。但也允许用小写字母。

3.2带参数的宏

#define命令定义宏时,还可以为宏设置参数。与函数中的参数类似,在宏定于中的参数为形式参数,在宏调用中的参数称为实际参数。对带参数的宏,在调用中,不仅要宏展开,还要用实参去代换形参。

带参宏定义的一般形式为:

#define 宏名(形参表)字符串

在定义带参数的宏时,宏名和形参表之间不能有空格出现,否则,就将宏定义成为无参数形式,而导致程序出错。

Eg:#define ABS(x) (x)<0?-(x):(x)

以上的宏定义中,如果x的值小于0,则使用一元运算符(-)对其取负,得到正数。

带参的宏和带参的函数相似,但其本质是不同的。使用带参宏时,在预处理时将程序源代码替换到相应的位置,编译时得到完整的目标代码,而不进行函数调用,因此程序执行效率要高些。而函数调用只需要编译一次函数,代码量较少,一般情况下,对于简单的功能,可使用宏替换的形式来使用。

3.3.预处理操作符#和##

3.3.1.操作符#

在使用#define定义宏时,可使用操作符#在字符串中输出实参。Eg:

#define AREA(x,y) printf(“长为“#x”,宽为“#y”的长方形的面积:%d\n”,(x)*(y));

3.3.2.操作符##

与操作符#类似,操作符##也可用在带参宏中替换部分内容。该操作符将宏中的两个部分连接成一个内容。例如,定义如下宏:

#define VAR(n)v##n

当使用一下方式引用宏:

VAR(1)

预处理时,将得到以下形式:

V1

如果使用以下宏定义:

#define FUNC(n)oper##n

当实参为1时,预处理后得到一下形式:

oper1

四.文件包含------include

当一个C语言程序由多个文件模块组成时,主模块中一般包含main函数和一些当前程序专用的函数。程序从main函数开始执行,在执行过程中,可调用当前文件中的函数,也可调用其他文件模块中的函数。

如果在模块中要调用其他文件模块中的函数,首先必须在主模块中声明该函数原型。一般都是采用文件包含的方法,包含其他文件模块的头文件。

文件包含中指定的文件名即可以用引号括起来,也可以用尖括号括起来,格式如下:

#include< 文件名>

#include“文件名”

如果使用尖括号<>括起文件名,则编译程序将到C语言开发环境中设置好的 include文件中去找指定的文件。

因为C语言的标准头文件都存放在include文件夹中,所以一般对标准头文件采用尖括号;对编程自己编写的文件,则使用双引号。如果自己编写的文件不是存放在当前工作文件夹,可以在#include命令后面加在路径。

#include命令的作用是把指定的文件模块内容插入到#include所在的位置,当程序编译链接时,系统会把所有#include指定的文件链接生成可执行代码。文件包含必须以#开头,表示这是编译预处理命令,行尾不能用分号结束。

#include所包含的文件,其扩展名可以是“.c”,表示包含普通C语言源程序。也可以是 “.h”,表示C语言程序的头文件。C语言系统中大量的定义与声明是以头文件形式提供的。

通过#define包含进来的文件模块中还可以再包含其他文件,这种用法称为嵌套包含。嵌套的层数与具体C语言系统有关,但是一般可以嵌套8层以上。

五.条件编译

预处理器还提供了条件编译功能。在预处理时,按照不同的条件去编译程序的不同部分,从而得到不同的目标代码。使用条件编译,可方便地处理程序的调试版本和正式版本,也可使用条件编译使程序的移植更方便。

5.1使用#if

与C语言的条件分支语句类似,在预处理时,也可以使用分支,根据不同的情况编译不同的源代码段。

#if 的使用格式如下:

#if 常量表达式

程序段

#else

程序段

#endif

该条件编译命令的执行过程为:若常量表达式的值为真(非0),则对程序段1进行编译,否则对程序段2进行编译。因此可以使程序在不同条件下完成不同的功能。

Eg:

#define DEBUG 1

int main()

{

int i,j;

char ch[26];

for(i='a';j=0;i<='z';i++,j++)

{

ch[j]=i;

#if DEBUG

printf("ch[%d]=%c\n",j,ch[j]);

#endif

}

for(j=0;j<26;j++)

{

printf("%c",ch[j]);

}

return 0;

}

#if预编译命令还可使用多分支语句格式,具体格式如下:

#if 常量表达式 1

程序段 1

#elif 常量表达式 2

程序段 2

… …

#elif 常量表达式 n

程序段 n

#else

程序段 m

#endif

关键字#elif与多分支if语句中的else if类似。

Eg:

#define os win

#if os=win

#include"win.h"

#elif os=linux

#include"linux.h"

#elif os=mac

#include"mac.h"

#endif

#if和#elif还可以进行嵌套,C89标准中,嵌套深度可以到达8层,而C99允许嵌套达到63层。在嵌套时,每个#endif,#else或#elif与最近的#if或#elif配对。

Eg:

#define MAX 100

#define OLD -1

int main()

{

int i;

#if MAX>50

{

#if OLD>3

{

i=1;

{

#elif OLD>0

{

i=2;

}

#else

{

i=3;

}

#endif

}

#else

{

#if OLD>3

{

i=4;

}

#elif OLD>4

{

i=5;

}

#else

{

i=6;

}

#endif

}

#endif

return 0;

}

5.2使用#ifdef和#ifndef

在上面的#if条件编译命令中,需要判断符号常量定义的具体值。在很多情况下,其实不需要判断符号常量的值,只需要判断是否定义了该符号常量。这时,可不使用#if命令,而使用另外一个预编译命令———#ifdef.

#ifdef命令的使用格式如下:

#ifdef 标识符

程序段 1

#else

程序段 2

#endif

其意义是,如果#ifdef后面的标识符已被定义过,则对“程序段1”进行编译;如果没有定义标识符,则编译“程序段2”。一般不使用#else及后面的“程序2”。

而#ifndef的意义与#ifdef相反,其格式如下:

#ifndef 标识符

程序段 1

#else

程序段 2

#endif

其意义是:如果未定义标识符,则编译“程序段1”;否则编译“程序段2”。

5.3使用#defined和#undef

与#ifdef类似的,可以在#if命令中使用define来判断是否已定义指定的标识符。例如:

#if defined 标识符

程序段 1

#endif

与下面的标示方式意义相同。

#ifdef 标识符

程序段 1

#endif

也可使用逻辑运算符,对defined取反。例如:

#if ! define 标识符

程序段 1

#endif

与下面的标示方式意义相同。

#ifndef 标识符

程序段 1

#endif

在#ifdef和#ifndef命令后面的标识符是使用#define进行定义的。在程序中,还可以使用#undef取消对标识符的定义,其形式为:

#undef 标识符

Eg:

#define MAX 100

……

#undef MAX

在以上代码中,首先使用#define定义标识符MAX,经过一段程序代码后,又可以使用#undef取消已定义的标识符。使用#undef命令后,再使用#ifdef max,将不会编译后的源代码,因为此时标识符MAX已经被取消定义了。

六.其他预处理命令

6.1.预定义的宏名

ANSI C标准预定义了五个宏名,每个宏名的前后均有两个下画线,避免与程序员定义相同的宏名(一般都不会定义前后有两个下划线的宏)。这5个宏名如下:

__DATE__:当前源程序的创建日期。

__FILE__:当前源程序的文件名称(包括盘符和路径)。

__LINE__:当前被编译代码的行号。

__STDC__:返回编译器是否位标准C,若其值为1表示符合标准C,否则不是标准C.

__TIME__:当前源程序的创建时间。

Eg:

#include

int main()

{

int j;

printf("日期:%s\n",__DATE__);

printf("时间:%s\n",__TIME__};

printf("文件名:%s\n",__FILE__);

printf("这是第%d行代码\n",__LINE__);

printf("本编译器%s标准C\n",(__STD__)?"符合":"不符合");

return 0;

}

6.2.重置行号和文件名命令------------#line

使用__LINE__预定义宏名赈灾编译的程序行号。使用#line命令可改变预定义宏__LINE__与__FILE__的内容,该命令的基本形如下:

#line number[“filename”]

其中的数字为一个正整数,可选的文件名为有效文件标识符。行号为源代码中当前行号,文件名为源文件的名字。命令为#line主要用于调试以及其他特殊应用。

Eg:

1:#include

2:#include

4:#line 1000

6:int main()

7:{

8:printf("当前行号:%d\n",__LINE__);

9:return 0;

10:}

在以上程序中,在第4行中使用#line定义的行号为从1000开始(不包括#line这行)。所以第5行的编号将为1000,第6行为1001,第7行为1002,第8行为1003.

6.3.修改编译器设置命令 ------------#pragma

#pragma命令的作用是设定编译器的状态,或者指示编译器完全一些特定的动作。#pragma命令对每个编译器给出了一个方法,在保持与C语言完全兼容的情况下,给出主机或者操作系统专有的特征。其格式一般为:

#pragma Para

其中,Para为参数,可使用的参数很多,下面列出常用的参数:

Message参数,该参数能够在编译信息输出窗口中输出对应的信息,这对于源代码信息的控制是非常重要的,其使用方法是:

#pragma message(消息文本)

当编译器遇到这条指令时,就在编译输出窗口中将消息文本显示出来。

另外一个使用比较多得pragma参数是code_seg.格式如:

#pragma code_seg([“section_name”[,section_class]])

它能够设置程序中函数代码存放的代码段,在开发驱动程序的时候就会使用到它。

参数once,可保证头文件被编译一次,其格式为:

#pragma once

只要在头文件的最开始加入这条指令就能够保证头文件被编译一次。

6.4.产生错误信息命令 ------------#error

#error命令强制编译器停止编译,并输出一个错误信息,主要用于程序调试。其使用如下:

#error 信息错误

注意,错误信息不用双括号括起来。当遇到#error命令时,错误信息将显示出来。

例如,以下编译预处理器命令判断预定义宏__STDC__,如果其值不为1,则显示一个错误信息,提示程序员该编译器不支持ANSI C标准。

#if __STDC__!=1

#error NOT ANSI C

#endif

七.内联函数

在使用#define定义带参数宏时,在调用函数时,一般需要增加系统的开销,如参数传递,跳转控制,返回结果等额外操作需要系统内存和执行时间。而使用带参数宏时,通过宏替换可再编译前将函数代码展开导源代码中,使编译后的目标文件含有多段重复的代码。这样做,会增加程序的代码量,都可以减少执行时间。

在C99标准钟,还提供另外一种解决方法:使用内联函数。

在程序编译时,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来进行替代。显然,这种做法不会产生转去转回得问题。都是由于在编译时将函数体中的代码被替代到程序中,因此会增加目标代码量,进而增加空间的开销,而在时间开销上不像函数调用时那么大,可见它是以增加目标代码为代码来换取时间的节省。

定义内联函数的方法很简单,只要在定义函数头的前面加上关键字inline即可。内联函数的定义与一般函数一样。例如,定于一个两个整数相加的函数:

#include

#include

inline int add(int x,int y);

inline int add(int x,int y)

{

return x+y;

}

int main()

{

int i,j,k;

printf("请输入两个整数的值:\n");

scanf("%d %d",&i,&j);

k=add(i,j);

printf("k=%d\n",k);

return 0;

}

在程序中,调用函数add时,该函数在编译时会将以上代码复制过来,而不是像一般函数那样是运行时被调用。

内联函数具有一般函数的特性,它与一般函数所不同之处在于函数调用的处理。一般函数进行调用时,要讲程序执行权转导被调函数中,然后再返回到调用到它的函数中;而内联函数在调用时,是将调用表达式用内联函数体来替换。在使用内联函数时,应该注意如下几点:

在内联函数内部允许用循环语句和开关语句。

内联函数的定义必须出现在内联函数第一次被调用之前。

其实,在程序中声明一个函数为内联时,编译以后这个函数不一定是内联的,

即程序只是建议编译器使用内联函数,但是编译器会根据函数情况决定是否使用内联,所以如果编写的内联函数中出现循环或者开关语句,程序也不会提示出错,但那个函数已经不是内联函数了。

一般都是讲一个小型函数作为内联函数。

本文由js333发布于计算机互联网,转载请注明出处:C语言预处理指令,程序员笔记预处理

关键词:

上一篇:字幕图标控件,winForm启动最小化到任务栏右侧通

下一篇:没有了