精华内容
下载资源
问答
  • 预处理命令

    2020-06-09 14:48:03
      C语言提供了多种预处理功能,如宏定义、文件包含、条件编译等。合理地使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。 (二)宏定义   在C语言源程序中允许用一个标识符来表示...

    例题:以下关于编译预处理的叙述中错误的是( C )
    A.预处理命令行必须以#开始
    B.一条有效的预处理命令必须单独占据一行
    C.预处理命令行只能位于源程序中所有语句之前
    D.预处理命令不是C语言本身的组成部分

    例题:C语言的编译系统对宏命令的处理是( D )
    A.在程序运行时进行的
    B.在程序连接时进行的
    C.和C程序中的其他语句同时进行编译的
    D.在对源程序中其他成分正式编译之前进行的

    (一)预处理命令概述
      凡是以“#”开头的均为预处理命令。所谓预处理是指在进行编译的第一遍扫描(词法扫描和语法分析)之前所作的工作。预处理是C语言的一个重要功能,它由预处理程序负责完成。当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。
      预处理命令不是C语言本身的组成部分,不能直接对它们进行编译(因为编译程序不能识别它们)。必须在对程序进行通常的编译(包括词法和语法分析、代码生成、优化等)之前,先对程序中这些特殊的命令进行“预处理”,即根据预处理命令对程序作相应的处理(例如,若程序中用#define命令定义的一个符号常量A,则在预处理时将程序中所有的A都置换为指定的字符串。若程序中用#include命令包含一个文件"stdio.h",则在预处理时将stdio.h文件中的实际内容代替该命令)。经过预处理后的程序不再包括预处理命令了,最后再由编译程序对预处理后的源程序进行通常的编译处理,得到可供执行的目标代码。现在使用的许多C编译系统都包括了预处理、编译和连接等部分,在进行编译时一气呵成。因此不少用户误认为预处理命令是C语言的一部分,甚至以为它们是C语句,这是不对的。
      C语言提供了多种预处理功能,如文件包含、宏定义、条件编译等。合理地使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
      注意:预处理指令不是C语句,因此末尾没有分号。

    (二)文件包含
      C语言提供了#include文件包含预处理命令,将一个头文件包含到源程序文件中。
      文件名可以带路径。
      如果使用尖括号,则到系统指定包含目录去查找被包含文件。如果使用双引号,则首先在系统当前目录下查找被包含文件,没找到再到系统指定包含目录去查找。
      一般使用尖括号包含系统定义的头文件,使用双引号包含用户自定义的头文件或源程序文件。

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

    (1)无参宏定义
    一般形式为:#define 标识符 字符串

    • 宏定义允许嵌套
    • 习惯上宏名用大写字母表示,以便于与变量区别
    • 宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用#undef命令。
      例如:
      #define PI 3.14159
      main(){

      }
      #undef PI
      f1(){

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

    (2)带参宏定义
    宏定义的一般形式为:#define 宏名(形参表) 字符串
    带参宏调用的一般形式为:宏名(实参表);
      C语言允许宏带有参数。在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数。对带参数的宏,在调用中,不仅要宏展开,而且要用实参去代换形参。
    例如:
    #define M(y) yy+3y /宏定义/

    K = M(5); /宏调用/

    • 宏代换只作符号代换而不作其它处理。宏定义不仅应在参数两侧加括号,也应在整个字符串外加括号。
    • 带参的宏和带参函数很相似,但有本质上的不同,把同一表达式用函数处理与用宏处理两者的结果可能是不同的。

    (四)条件编译
      预处理程序提供了条件编译的功能。可以按不同的条件去编译不同的程序部分,因而产生不同的目标代码文件。这对于程序的移植和调用是很有用的。
      条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。
      条件编译有三种形式:
    1)第一种形式

    #ifdef 标识符
    程序段1
    #else
    程序段2
    #endif
    

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

    #ifdef 标识符
    程序段
    #endif
    

    在程序的第一行宏定义中,定义NUM表示字符串OK

    #define NUM OK
    

    其实也可以为任何字符串,甚至不给出任何字符串,写为:

    #define NUM
    

    也具有同样的意义。
    2)第二种形式

    #ifndef 标识符
    程序段1
    #else
    程序段2
    #endif
    

    与第一重形式的区别是将“ifdef”改为“ifndef”。它的功能是,如果标识符未被#define命令定义过则对程序段1进行编译,否则对程序段2进行编译。这与第一种形式的功能正相反
    3)第三种形式

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

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

    展开全文
  • C语言预处理命令详解

    2021-02-08 16:26:16
    C语言预处理命令详解 一 前言 预处理(或称预编译)是指在进行编译的第一遍扫描(词法扫描和语法分析)之前所作的工作。预处理指令指示在程序正式编译前就由编译器进行的操作,可放在程序中任何位置。 预处理是C语言的...

    一 前言

    预处理(或称预编译)是指在进行编译的第一遍扫描(词法扫描和语法分析)之前所作的工作。预处理指令指示在程序正式编译前就由编译器进行的操作,可放在程序中任何位置。
    预处理是C语言的一个重要功能,它由预处理程序负责完成。当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。
    C语言提供多种预处理功能,主要处理#开始的预编译指令,如宏定义(#define)、文件包含(#include)、条件编译(#ifdef)等。合理使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。

    二 宏定义

    C语言源程序中允许用一个标识符来表示一个字符串,称为“宏”。被定义为宏的标识符称为“宏名”。在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去代换,这称为宏替换或宏展开。
    宏定义是由源程序中的宏定义命令完成的。宏替换是由预处理程序自动完成的。
    在C语言中,宏定义分为有参数和无参数两种。下面分别讨论这两种宏的定义和调用。

    2.1 无参宏定义

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

     #define  标识符  字符串
    

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

    宏定义用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名。这只是一种简单的文本替换,预处理程序对它不作任何检查。如有错误,只能在编译已被宏展开后的源程序时发现。
    注意理解宏替换中“换”的概念,即在对相关命令或语句的含义和功能作具体分析之前就要进行文本替换。
    【例1】定义常量:

    #define MAX_TIME 1000
    

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

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

     #define pint (int*)
     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】

     #define INC(x) x+1  //宏定义
     y = INC(5);         //宏调用
    

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

     #define SQ(r) r*r
    

    上述这种实参为表达式的宏定义,在一般使用时没有问题;但遇到如area=SQ(a+b);时就会出现问题,宏展开后变为area=a+ba+b;,显然违背本意。
    相比之下,函数调用时会先把实参表达式的值(a+b)求出来再赋予形参r;而宏替换对实参表达式不作计算直接地照原样代换。因此在宏定义中,字符串内的形参通常要用括号括起来以避免出错。
    进一步地,考虑到运算符优先级和结合性,遇到area=10/SQ(a+b);时即使形参加括号仍会出错。因此,还应在宏定义中的整个字符串外加括号,
    综上,正确的宏定义是#define SQ® (®
    ®),即宏定义时建议所有的层次都要加括号。
    【例5】带参函数和带参宏的区别:

      #define SQUARE(x) ((x)*(x))
      int Square(int x){
          return (x * x); //未考虑溢出保护
      }
      
      int main(void){
          int i = 1;
          while(i <= 5)
              printf("i = %d, Square = %d\n", i, Square(i++));
     
         int j = 1;
         while(j <= 5)
             printf("j = %d, SQUARE = %d\n", j, SQUARE(j++));
         
         return 0;
     }
    

    执行后输出如下:

     i = 2, Square = 1
     i = 3, Square = 4
     i = 4, Square = 9
     i = 5, Square = 16
     i = 6, Square = 25
     j = 3, SQUARE = 1
     j = 5, SQUARE = 9
     j = 7, SQUARE = 25
    

    本例意在说明,把同一表达式用函数处理与用宏处理两者的结果有可能是不同的。
    调用Square函数时,把实参i值传给形参x后自增1,再输出函数值。因此循环5次,输出1~5的平方值。
    调用SQUARE宏时,SQUARE(j++)被代换为((j++)(j++))。在第一次循环时,表达式中j初值为1,两者相乘的结果为1。相乘后j自增两次变为3,因此表达式中第二次相乘时结果为33=9。同理,第三次相乘时结果为5*5=25,并在此次循环后j值变为7,不再满足循环条件,停止循环。
    从以上分析可以看出函数调用和宏调用二者在形式上相似,在本质上是完全不同的。
    带参宏注意事项:

    • 宏名和形参表的括号间不能有空格。
    • 宏替换只作替换,不做计算,不做表达式求解。
    • 函数调用在编译后程序运行时进行,并且分配内存。
    • 宏替换在编译前进行,不分配内存。
    • 宏的哑实结合不存在类型,也没有类型转换。
    • 函数只有一个返回值,利用宏则可以设法得到多个值。
    • 宏展开使源程序变长,函数调用不会。
    • 宏展开不占用运行时间,只占编译时间,函数调用占运行时间(分配内存、保留现场、值传递、返回值)。
    • 为防止无限制递归展开,当宏调用自身时,不再继续展开。如:#define TEST(x) (x + TEST(x))被展开为1 + TEST(1)。

    2.3 实践用例

    包括基本用法(及技巧)和特殊用法(#和##等)。
    #define可以定义多条语句,以替代多行的代码,但应注意替换后的形式,避免出错。宏定义在换行时要加上一个反斜杠”\”,而且反斜杠后面直接回车,不能有空格。

    2.3.1 基本用法

    1. 定义常量:
     #define PI   3.1415926
    

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

    1. 定义表达式:
     #define M   (y*y+3*y)
    

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

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

    1. 得到指定地址上的一个字节或字:
     #define MEM_B(x)     (*((char *)(x)))
     #define MEM_W(x)     (*((short *)(x)))
    
    1. 求最大值和最小值:
     #define MAX(x, y)     (((x) > (y)) ? (x) : (y))
     #define MIN(x, y)     (((x) < (y)) ? (x) : (y))
    

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

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

    以下给出MAX宏的两个安全版本(源自linux/kernel.h):

      #define MAX_S(x, y) ({ \
          const typeof(x) _x = (x);  \
          const typeof(y) _y = (y);  \
          (void)(&_x == &_y);       \
          _x > _y ? _x : _y; })
      
      #define TMAX_S(type, x, y) ({ \
          type _x = (x);  \
          type _y = (y);  \
         _x > _y ? _x: _y; })
    

    Gcc编译器将包含在圆括号和大括号双层括号内的复合语句看作是一个表达式,它可出现在任何允许表达式的地方;复合语句中可声明局部变量,判断循环条件等复杂处理。而表达式的最后一条语句必须是一个表达式,它的计算结果作为返回值。MAX_S和TMAX_S宏内就定义局部变量以消除参数副作用。

    MAX_S宏内(void)(&_x == &_y)语句用于检查参数类型一致性。当参数x和y类型不同时,会产生” comparison of distinct pointer types lacks a cast”的编译警告。

    注意,MAX_S和TMAX_S宏虽可避免参数副作用,但会增加内存开销并降低执行效率。若使用者能保证宏参数不存在副作用,则可选用普通定义(即MAX宏)。

    1. 得到一个成员在结构体中的偏移量(lint 545告警表示"&用法值得怀疑",此处抑制该警告):
     #define FPOS(type, field) \
     /*lint -e545 */ ((int)&((type *)0)-> field) /*lint +e545 */
    
    1. 得到一个结构体中某成员所占用的字节数:
     #define FSIZ(type, field)    sizeof(((type *)0)->field)
    
    1. 按照LSB格式把两个字节转化为一个字(word):
     #define FLIPW(arr)          ((((short)(arr)[0]) * 256) + (arr)[1])
    
    1. 按照LSB格式把一个字(word)转化为两个字节:
     #define FLOPW(arr, val) \
         (arr)[0] = ((val) / 256); \
         (arr)[1] = ((val) & 0xFF)
    
    1. 得到一个变量的地址:
     #define B_PTR(var)       ((char *)(void *)&(var))
     #define W_PTR(var)       ((short *)(void *)&(var))
    
    1. 得到一个字(word)的高位和低位字节:
     #define WORD_LO(x)       ((char)((short)(x)&0xFF))
     #define WORD_HI(x)       ((char)((short)(x)>>0x8))
    
    1. 返回一个比X大的最接近的8的倍数:
     #define RND8(x)           ((((x) + 7) / 8) * 8)
    
    1. 将一个字母转换为大写或小写:
     #define UPCASE(c)         (((c) >= 'a' && (c) <= 'z') ? ((c) + 'A' - 'a') : (c))
     #define LOCASE(c)         (((c) >= 'A' && (c) <= 'Z') ? ((c) + 'a' - 'A') : (c))
    

    注意,UPCASE和LOCASE宏仅适用于ASCII编码(依赖于码字顺序和连续性),而不适用于EBCDIC编码。

    1. 判断字符是不是10进值的数字:
     #define ISDEC(c)          ((c) >= '0' && (c) <= '9')
    
    1. 判断字符是不是16进值的数字:
     #define ISHEX(c)          (((c) >= '0' && (c) <= '9') ||\
         ((c) >= 'A' && (c) <= 'F') ||\
         ((c) >= 'a' && (c) <= 'f'))
    
    1. 防止溢出的一个方法:
     #define INC_SAT(val)      (val = ((val)+1 > (val)) ? (val)+1 : (val))
    
    1. 返回数组元素的个数:
     #define ARR_SIZE(arr)     (sizeof((arr)) / sizeof((arr[0])))
    
    1. 对于IO空间映射在存储空间的结构,输入输出处理:
     #define INP(port)           (*((volatile char *)(port)))
     #define INPW(port)          (*((volatile short *)(port)))
     #define INPDW(port)         (*((volatile int *)(port)))
     #define OUTP(port, val)     (*((volatile char *)(port)) = ((char)(val)))
     #define OUTPW(port, val)    (*((volatile short *)(port)) = ((short)(val)))
     #define OUTPDW(port, val)   (*((volatile int *)(port)) = ((int)(val)))
    
    1. 使用一些宏跟踪调试:

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

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

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

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

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

     #define MSG(msg, date)      printf(msg);printf(“[%d][%d][%s]”,date,__LINE__,__FILE__)
    
    1. 用do{…}while(0)语句包含多语句防止错误:
     #define DO(a, b) do{\
         a+b;\
         a++;\
      }while(0)
    
    1. 实现类似“重载”功能
      C语言中没有swap函数,而且不支持重载,也没有模板概念,所以对于每种数据类型都要写出相应的swap函数,如:
     IntSwap(int *,  int *);  
     LongSwap(long *,  long *);  
     StringSwap(char *,  char *); 
    

    可采用宏定义TSWAP (t,x,y)或SWAP(x, y)交换两个整型或浮点参数:

      #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;
     }
    
    1. 1年中有多少秒(忽略闰年问题) :
     #define SECONDS_PER_YEAR    (60UL * 60 * 24 * 365)
    

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

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

    以下几种写法也正确:

     #define SECONDS_PER_YEAR    60 * 60 * 24 * 365UL
     #define SECONDS_PER_YEAR    (60UL * 60UL * 24UL * 365UL)
     #define SECONDS_PER_YEAR    ((unsigned long)(60 * 60 * 24 * 365))
    
    1. 取消宏定义:
         #define [MacroName] [MacroValue]       //定义宏
    
         #undef [MacroName]                     //取消宏
    

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

     #define PI   3.14159
     int main(void){
         //……
     }
     #undef PI
     int func(void){
         //……
     }
    

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

    2.3.2 特殊用法

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

    2.3.2.1 字符串化操作符#

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

     #define EXAMPLE(instr)      printf("The input string is:\t%s\n", #instr)
     #define EXAMPLE1(instr)     #instr
    

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

    又如下面代码中的宏:

     define WARN_IF(exp) do{ \
         if(exp) \
             fprintf(stderr, "Warning: " #exp"\n"); \
     }while(0)
    

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

     do{
         if(divider == 0)
             fprintf(stderr, "Warning" "divider == 0" "\n");
     }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就行,而不一定是宏的变量。例如:

     #define PASTER(n)     printf( "token" #n " = %d", token##n)
     int token9 = 9;
    

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

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

     struct command{
         char * name;
         void (*function)(void);
     };
     #define COMMAND(NAME)   {NAME, NAME##_command}
    

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

     struct command commands[] = {
         COMMAND(quit),
         COMMAND(help),
         //...
     }
    

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

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

     #define  LINK_MULTIPLE(a, b, c, d)      a##_##b##_##c##_##d
     typedef struct record_type LINK_MULTIPLE(name, company, position, salary);
    

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

    注意:

    • 当用##连接形参时,##前后的空格可有可无。
    • 连接后的实际参数名,必须为实际存在的参数名或是编译器已知的宏定义。
    • 凡是宏定义里有用’#‘或’##'的地方,宏参数是不会再展开。如:
     #define STR(s)       #s
     #define CONS(a,b)    int(a##e##b)
    

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

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

     #define _STR(s)         #s 
     #define STR(s)          _STR(s)       // 转换宏
     #define _CONS(a,b)      int(a##e##b)
     #define CONS(a,b)       _CONS(a,b)    // 转换宏
    

    则printf(“int max: %s\n”, STR(INT_MAX))输出为int max: 0x7fffffff;而printf("%d\n", CONS(A, A))输出为200。

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

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

    1. 合并匿名变量名
     #define ___ANONYMOUS1(type, var, line)   type  var##line
     #define __ANONYMOUS0(type, line)         ___ANONYMOUS1(type, _anonymous, line)
     #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__在第二层才能被解开。

    1. 填充结构
      #define FILL(a)   {a, #a} 
      
      enum IDD{OPEN, CLOSE};
      typedef struct{
          IDD id;
          const char * msg; 
      }T_MSG;
    

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

     T_MSG tMsg[] = {{OPEN,  "OPEN"},
                     {CLOSE, "CLOSE"}};
    
    1. 记录文件名
     #define _GET_FILE_NAME(f)     #f
     #define GET_FILE_NAME(f)      _GET_FILE_NAME(f)
     static char  FILE_NAME[] = GET_FILE_NAME(__FILE__);
         4. 得到一个数值类型所对应的字符串缓冲大小
    
     #define _TYPE_BUF_SIZE(type)   sizeof #type
     #define TYPE_BUF_SIZE(type)    _TYPE_BUF_SIZE(type)
     char  buf[TYPE_BUF_SIZE(INT_MAX)];
          //-->  char  buf[_TYPE_BUF_SIZE(0x7fffffff)];
          //-->  char  buf[sizeof "0x7fffffff"];
    

    这里相当于:char buf[11]; ("0x7fffffff"的sizeof大小刚好为11)

    2.3.2.3 字符化操作符@#

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

     #define makechar(x)    #@x
     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的可变参数宏, 可以更方便地打印调试信息,如:

     #ifdef DEBUG
         #define DBGPRINT(format, args...) \
             fprintf(stderr, format, ##args)
     #else
         #define DBGPRINT(format, args...)
     #endif
    

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

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

      #ifdef LOG_TEST_DEBUG
          /* OMCI调试日志宏 */
          //以10进制格式日志整型变量
          #define PRINT_DEC(x)          printf(#x" = %d\n", x)
          #define PRINT_DEC2(x,y)       printf(#x" = %d\n", y)
         //以16进制格式日志整型变量
          #define PRINT_HEX(x)          printf(#x" = 0x%-X\n", x)
          #define PRINT_HEX2(x,y)       printf(#x" = 0x%-X\n", y)
         //以字符串格式日志字符串变量
         #define PRINT_STR(x)          printf(#x" = %s\n", x)
         #define PRINT_STR2(x,y)       printf(#x" = %s\n", y)
     
         //日志提示信息
         #define PROMPT(info)          printf("%s\n", info)
     
         //调试定位信息打印宏
         #define  TP                   printf("%-4u - [%s<%s>]\n", __LINE__, __FILE__, __FUNCTION__);
     
         //调试跟踪宏,在待日志信息前附加日志文件名、行数、函数名等信息
         #define TRACE(fmt, args...)\
         do{\
             printf("[%s(%d)<%s>]", __FILE__, __LINE__, __FUNCTION__);\
             printf((fmt), ##args);\
         }while(0)
     #else
         #define PRINT_DEC(x)
         #define PRINT_DEC2(x,y)
     
         #define PRINT_HEX(x)
         #define PRINT_HEX2(x,y)
     
         #define PRINT_STR(x)
         #define PRINT_STR2(x,y)
     
         #define PROMPT(info)
     
         #define  TP
     
         #define TRACE(fmt, args...)
     #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】

    #define NUM OK
    int main(void){
        struct stu{
            int num;
            char *name;
            char sex;
            float score;
        }*ps;
        ps=(struct stu*)malloc(sizeof(struct stu));
        ps->num = 102;
        ps->name = "Zhang ping";
        ps->sex = 'M';
        ps->score = 62.5;
    #ifdef NUM
        printf("Number=%d\nScore=%f\n", ps->num, ps->score); /*--Execute--*/
    #else
        printf("Name=%s\nSex=%c\n", ps->name, ps->sex);
    #endif
        free(ps);
        return 0;
    }
    

    由于在程序中插入了条件编译预处理命令,因此要根据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】输入一行字母字符,根据需要设置条件编译,使之能将字母全改为大写或小写字母输出。

    #define CAPITAL_LETTER   1
    int main(void){
        char szOrig[] = "C Language", cChar;
        int dwIdx = 0;
        while((cChar = szOrig[dwIdx++]) != '\0')
        {
    #if CAPITAL_LETTER
            if((cChar >= 'a') && (cChar <= 'z')) cChar = cChar - 0x20;
    #else
            if((cChar >= 'A') && (cChar <= 'Z')) cChar = cChar + 0x20;
    #endif
            printf("%c", cChar);
        }
        return 0;
    }
    

    在程序第一行定义宏CAPITAL_LETTER为1,因此在条件编译时常量表达式CAPITAL_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代替。这样,源程序可以不必作任何修改就可以用于不同类型的计算机系统。

    1. 包含程序功能模块

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

     #ifdef FLV
         include"fastleave.c"
     #endif
    

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

    1. 开关调试信息

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

     #ifdef DEBUG
         printf("device_open(%p)\n", file);
     #endif
    

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

    1. 避开硬件的限制。

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

     #ifndef TEST
         i = dial();  //程序调试运行时绕过此语句
     #else
         i = 0;
     #endif
    

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

    1. 防止头文件重复包含

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

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

     #ifndef  <标识符>
         #define  <标识符>
         //真正的内容,如函数声明之类
     #endif
    

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

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

    1. 在#ifndef中定义变量出现的问题(一般不定义在#ifndef中)。
     #ifndef PRECMPL
         #define PRECMPL
         int var;
     #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中定义(推荐)

    //<x.h>
    #ifndef  __X_H
        #define  __X_H
        extern int var;
    #endif
    <x.c>
    int var = 0;
    

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

    五 小结

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

    六 附录

    6.1 Argument Prescan

    (摘自http://gcc.gnu.org/onlinedocs/cpp/Argument-Prescan.html)

    Macro arguments are completely macro-expanded before they are substituted into a macro body, unless they are stringified or pasted with other tokens. After substitution, the entire macro body, including the substituted arguments, is scanned again for macros to be expanded. The result is that the arguments are scanned twice to expand macro calls in them.

    宏参数被完全展开后再替换入宏体,但当宏参数被字符串化(#)或与其它子串连接(##)时不予展开。在替换之后,再次扫描整个宏体(包括已替换宏参数)以进一步展开宏。结果是宏参数被扫描两次以展开参数所(嵌套)调用的宏。

    若带参数宏定义中的参数称为形参,调用宏时的实际参数称为实参,则宏的展开可用以下三步来简单描述(该步骤与gcc摘录稍有不同,但更易操作):

    1. 用实参替换形参,将实参代入宏文本中;

    2. 若实参也是宏,则展开实参;

    3. 继续处理宏替换后的宏文本,若宏文本也包含宏则继续展开,否则完成展开。

    其中第一步将实参代入宏文本后,若实参前遇到字符“#”或“##”,即使实参是宏也不再展开实参,而当作文本处理。

    上述展开步骤示例如下:

     #define TO_STRING(x)    _TO_STRING(x)
     #define _TO_STRING(x)   #x
     #define FOO             4
    

    则_TO_STRING(FOO)展开为”FOO”;TO_STRING(FOO)展开为_TO_STRING(4),进而展开为”4”。相当于借助_TO_STRING这样的中间宏,先展开宏参数,延迟其字符化。

    6.2 宏的其他注意事项

    1. 避免在无作用域限定(未用{}括起)的宏内定义数组、结构、字符串等变量,否则函数中对宏的多次引用会导致实际局部变量空间成倍放大。

    2. 按照宏的功能、模块进行集中定义。即在一处将常量数值定义为宏,其他地方通过引用该宏,生成自己模块的宏。严禁相同含义的常量数值,在不同地方定义为不同的宏,即使数值相同也不允许(维护修改后极易遗漏,造成代码隐患)。

    3. 用只读变量适当替代(类似功能的)宏,例如将#define PIE 3.14改为const float PIE = 3.14。这样做的好处如下:

    1. 预编译时用宏定义值替换宏名,编译时报错不易理解;

    2. 跟踪调试时显示宏值,而不是宏名;

    3. 宏没有类型,不能做类型检查,不安全;

    4. 宏自身没有作用域;

    5. 只读变量和宏的效率同样高。

    注意,C语言中只读变量不可用于数组大小、变量(包括数组元素)初始化值以及case表达式。

    1. 用inline函数代替(类似功能的)宏函数。好处如下:
    1. 宏函数在预编译时处理,编译出错信息不易理解;

    2. 宏函数本身无法单步跟踪调试,因此也不要在宏内调用函数。但某些编译器(为了调试需要)可将inline函数转成普通函数;

    3. 宏函数的入参没有类型,不安全;

    4. inline函数会在目标代码中展开,和宏的效率一样高;

    注意,某些宏函数用法独特,不能用inline函数取代。当不想或不能指明参数类型时,宏函数更合适。

    1. 不带参数的宏函数也要定义成函数形式,如#define HELLO( ) printf(“Hello.”)。

    括号会暗示阅读代码者该宏是一个函数。

    1. 带参宏内定义变量时,应注意避免内外部变量重名的问题:
    typedef struct{
        int d;
    }T_TEST;
    T_TEST gtTest = {0};
    #define ASSIGN1(_d) do{ \
        T_TEST t = {0}; \
        t.d = _d; \
        gtTest = t; \
    }while(0)
    
    #define ASSIGN2(_p) do{ \
        int _d; \
        _d = 5; \
        (_p) = _d; \
    }while(0)
    

    若宏参数名或宏内变量名不加前缀下划线,则ASSIGN1©将会导致编译报错(t.d被替换为t.c),ASSIGN2(d)会因宏内作用域而导致外部的变量d值保持不变(而非改为5)。

    1. 不要用宏改写语言。例如:
     #define FOREVER   for ( ; ; )
     #define BEGIN     {
     #define END       }
    

    C语言有完善且众所周知的语法。试图将其改变成类似于其他语言的形式,会使读者混淆,难于理解。

    6.3 do{…}while(0)妙用

    1. 函数中使用do{…}while(0)可替代goto语句。例如:
    goto写法 替代写法
    bOk = func1();
    if(!bOk) goto errorhandle;
    bOk = func2();
    if(!bOk) goto errorhandle;
    bOk = func3();
    if(!bOk) goto errorhandle;
    //… …
    //执行成功,释放资源并返回
    delete p;
    p = NULL;
    return true;
    errorhandle:
    delete p;
    p = NULL;
    return false;
    do{
    //执行并进行错误处理
    bOk = func1();
    if(!bOk) break;
    bOk = func2();
    if(!bOk) break;
    bOk = func3();
    if(!bOk) break;
    // …
    }while(0);
    //释放资源
    delete p;
    p = NULL;
    return bOk;
    1. 宏定义中使用do{…}while(0)的原因及好处:
    1. 避免空的宏定义产生warning,如#define DUMMY( ) do{}while(0)。

    2. 存在一个独立的代码块,可进行变量定义,实现比较复杂的逻辑处理。

    注意,该代码块内(即{…}内)定义的变量其作用域仅限于该块。此外,为避免宏的实参与其内部定义的变量同名而造成覆盖,最好在变量名前加上_(基于如下编程惯例:除非是库,否则不应定义以_开始的变量)。

    1. 若宏出现在判断语句之后,可保证作为一个整体来实现。

    如#define SAFE_DELETE§ delete p; p = NULL;,则以下代码

     if(NULL != p)
         SAFE_DELETE(p)
     else
         DUMMY( );
    

    就有两个问题:

    a) 因为if分支后有两条语句,else分支没有对应的if,编译失败;

    b) 假设没有else,则SAFE_DELETE中第二条语句无论if判断是否成立均会执行,这显然违背程序设计的原始目的。

    那么,为了避免这两个问题,将宏直接用{}括起来是否可以?如:

    #define SAFE_DELETE§ {delete p; p = NULL;}

    的确,上述问题不复存在。但C/C++编程中,在每条语句后加分号是约定俗成的习惯,此时以下代码

     if(NULL != p)
         SAFE_DELETE(p);
     else
         DUMMY( );
    

    其else分支就无法通过编译(多出一个分号),而采用do{…}while(0)则毫无问题。

    使用do{…} while(0)将宏包裹起来,成为一个独立的语法单元,从而不会与上下文发生混淆。同时因为绝大多数编译器都能够识别do{…}while(0)这种无用的循环并优化,所以该法不会导致程序的性能降低。

    6.4 类型定义符typedef

    C语言不仅提供了丰富的数据类型,而且还允许由用户自己定义类型说明符,也就是说允许由用户为数据类型取“别名”。类型定义符typedef即可用来完成此功能。

    typedef定义的一般形式为:

      typedef 原类型名  新类型名
    

    其中原类型名中含有定义部分,新类型名一般用大写表示,以便于区别。

    例如,有整型量int a,b。其中int是整型变量的类型说明符。int的完整写法为integer,为增加程序的可读性,可把整型说明符用typedef定义为typedef int INTEGER。此后就可用INTEGER来代替int作整型变量的类型说明,如INTEGER a,b等效于int a,b。

    用typedef定义数组、指针、结构等类型将带来很大的方便,不仅使程序书写简单而且意义更为明确,因而增强了可读性。

    例如,typedef char NAME[20]表示NAME是字符数组类型,数组长度为20。然后可用NAME 说明变量,如NAME a1,a2,s1,s2完全等效于:char a1[20],a2[20],s1[20],s2[20]。

    又如:

     typedef struct{
         char name[20];
         int  age;
         char sex;
     }STU;
    

    然后可用STU来定义结构变量:STU body1,body2;

    有时也可用宏定义来代替typedef的功能,但是宏定义是由预处理完成的,而typedef则是在编译时完成的,后者更为灵活方便。

    此外,采用typedef重新定义一些类型,可防止因平台和编译器不同而产生的类型字节数差异,方便移植。如:

    typedef unsigned char boolean;       /* Boolean value type. */
    typedef unsigned long int uint32;    /* Unsigned 32 bit value */
    typedef unsigned short uint16;       /* Unsigned 16 bit value */
    typedef unsigned char uint8;         /* Unsigned 8 bit value */
    typedef signed long int int32;       /* Signed 32 bit value */
    typedef signed short int16;          /* Signed 16 bit value */
    typedef signed char int8;            /* Signed 8 bit value */
    //下面的不建议使用
    typedef unsigned char byte;          /* Unsigned 8 bit value type. */
    typedef unsigned short word;         /* Unsinged 16 bit value type. */
    typedef unsigned long dword;         /* Unsigned 32 bit value type. */
    typedef unsigned char uint1;         /* Unsigned 8 bit value type. */
    typedef unsigned short uint2;        /* Unsigned 16 bit value type. */
    typedef unsigned long uint4;         /* Unsigned 32 bit value type. */
    typedef signed char int1;            /* Signed 8 bit value type. */
    typedef signed short int2;           /* Signed 16 bit value type. */
    typedef long int int4;               /* Signed 32 bit value type. */
    typedef signed long sint31;          /* Signed 32 bit value */
    typedef signed short sint15;         /* Signed 16 bit value */
    typedef signed char sint7;           /* Signed 8 bit value */
    

    原文:https://www.cnblogs.com/clover-toeic/p/3851102.html

    展开全文
  • 以“#”开头的预处理命令一般都放在函数之外,而且一般放在源文件前面,他们成为预处理部分。 预处理指的是进行编译的第一遍扫描(语法扫描和语法分析)之前所作的工作。它由预处理程序负责完成。对一个源文件...

        以“#”开头的预处理命令一般都放在函数之外,而且一般放在源文件前面,他们成为预处理部分。

        预处理指的是进行编译的第一遍扫描(语法扫描和语法分析)之前所作的工作。它由预处理程序负责完成。当对一个源文件进行编译时,系统自动引用预处理程序对预处理部分进行处理,处理完毕后自动进入对源程序的编译。

        C语言的预处理功能主要有:

    • 宏定义
    • 文件包含
    • 条件编译

    宏定义

        用一个标识符来表示字符串,称为“宏”,标识符称为宏名。在编译预处理阶段,所有宏名会被宏定义中的字符串替代,称为“宏展开”或“宏代换”。宏分为无参宏和有参宏。

       无参宏定义

        宏名后不带参数

        格式:#define 标识符 字符串

        字符串可以是常数、表达式、格式串,为了宏代换不出现错误,字符串最好加括号

        宏定义作用域为宏定义命令到源程序结束,可以用#undef提前终止作用域

        带参宏定义

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

    文件包含

        作用是把指定文件插入命令行位置取代命令行,从而把指定文件和当前源程序文件连成一个源文件。

        格式:#include "文件名"

                  #include <文件名>

        使用<>表示在包含文件目录中去查找(包含目录是用户在设置环境时设置的),而不在源文件目录查找。

        使用“”表示首先在当前的源文件目录中查找,若未找到才到包含目录中去查找。

    条件编译

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

        三种形式

        1、

      #ifdef 标识符
          程序段1
      #else            //若程序段2为空,则可以省略
          程序段2
      #endif

        2、

      #ifndef 标识符    //if no define
          程序段1
      #else            //若程序段2为空,则可以省略
          程序段2
      #endif

    3、

      #ifdef 常量表达式  //根据常量表达式的值判断
          程序段1
      #else            //若程序段2为空,则可以省略
          程序段2
      #endif




        
    展开全文
  • 我们使用#include 命令时,编译器就到这个文件夹里去找对应的文件。用这种写法去包含一个我们自己编写的头文件(不在那个Include文件夹里)就会出错了。所以包含C++提供的头文件时,应该使用尖括号。  #include ...

    关于与include搭配的符号(双引号 & 尖括号):

      C++有一些编写好的头文件(比如标准函数库等等),它们存放在VC++的Include文件夹里。当我们使用#include <文件名>命令时,编译器就到这个文件夹里去找对应的文件。用这种写法去包含一个我们自己编写的头文件(不在那个Include文件夹里)就会出错了。所以包含C++提供的头文件时,应该使用尖括号。

      #include "文件名"命令则是先在当前文件所在的目录搜索是否有符合的文件,如果没有再到Include文件夹里去找对应的文件。因此,无论这个文件是C++提供的还是自己编写的,使用#include "文件名"命令一定是正确的。但系统的头文件最好还是用尖括号,那样可以加快搜索速度。


    小节

    双引号定义从当前文件目录下进行查找,双尖括号定义从标准库目录开始查找。如果不是自己的头文件,就用尖括号,这样可以加快搜索的速度;如果是自己的头文件,就用双引号。



    关于头文件里写的的内容

    头文件是一些以.h作为扩展名的标准文本文件。一般情况下,都应该把自定义的头文件和其余的程序文件放在同一个子目录里,或者在主程序目录下专门创建一个子文件夹来集中存放它们。

    可以用头文件来保存程序的任何一段代码,如函数和类的声明,但一定不要用头文件来保存它们的实现。

    与标准的C++源代码文件相比,在头文件里应该使用更多的注释。至少把它的用途和用法描述清楚

    应该在注释里说明的内容包括:创建日期、文件用途、创建者姓名、最后一次修改日期、有什么限制和前提条件等等。头文件里的每以一个类和函数也应该有说明。

    头文件典型的做法是只用它们来保存函数声明、用户自定义类型数据(结构和类)、模板和全局性的常量。

    如果你有一个程序需要多次调用一个或一组函数,或是你有一个或一组函数需要在多个程序里调用,就应该把它们的声明拿出来放到一个头文件里。
    头文件应该只包含
    最必要的代码,比如只声明一个类或只包含一组彼此相关的函数

    小节

    多写些注释,对于类和函数只写声明不写实现。

    写的内容->函数声明、用户自定义类型数据(结构和类)、模板和全局性的常量

    内容的用途->多次调用的一个或一组函数、一个或一组函数需要在多个程序里调用



    带目录的头文件:

    include时,双引号里,

    【.\SRC\】 这样写表示,当前目录中的SRC文件夹,还可以直接写【SRC\】;

    ..\SRC\】这样写表示,当前目录的上一层目录中SRC文件夹;
    【..\..\SRC\】 这样写表示,当前目录的上两层目录中SRC文件夹;


    要是不用上面这种写法,就是在项目属性的附加目录里添加所有需要包含文件的目录。



    预处理命令:



    预处理用法:

    #if FLAG
        //代码
        //代码
        //代码
        //代码
    #endif

    FLAG为0,就不执行里面的代码;要是为1,就执行里面的代码。

    这个最大的功能就是用来裁剪功能

    把功能模块放到这个结构里,裁剪时,FLAG置0,则为裁剪;FLAG置1,则为保留。

    这个用法一般是在系统裁剪时使用,当看过系统级源码时,就知道这个有多棒了!

    FLAG就是给外面的接口,用来控制是否裁剪。

    这篇文章就举例子说明的这个用法。



    #ifndef LOVE_FISHC
    #define LOVE_FISHC
    class Rational{ … };
    #endif

    ifndef先判断后面的字符是否定义过,如果没定义,就在下面定义一下。如果已经定义了,就不执行里面的代码。

    这样就利用这个字符来探测是否重定义了。

    这个字符的命名规则:将原头文件名字母都变大写,实心点用下划线代替。例:【Halcon.h -> HALCON_H】



    命名空间(作用域):

    最大的区域是全局作用域,最小的区域是一个代码块。例如:

    {
          int a = 20;
    }
    //a在此处已经不存在了
    如果没有命名空间,且工程很庞大,因为没有两样东西可以有同样的名字,只能通过详细的命名描述加以区分。

    这个时候就正好使用命名空间。使变量只在命名空间内是独一无二的就ok.创建格式如下:


    namespace myNamespace
    {
    // 全部东西
    }
    注意,结尾不加分号,这不是结构不是类。

    例如,通过这样,可以把两个person区分开,只要加上各自的命名空间就可以在同一个作用域使用啦~

    namespace author
    {
    std::string person;
    }
    namespace programmer
    {
    std::string person;
    }

    具体用法:

    把.h文件中代码用namespace括起来(注意,不要括进去预处理代码),把.cpp文件中代码也用namespace括起来(同前面一样,不要括进去预处理代码)。

    最后在调用这个.h的cpp里,在前面声明一下using namespace xxx【其实是不推荐这么用的,要是这么用,就失去了使用命名空间的意义了

    正确的用法是,在调用这个.h的cpp里,使用这样的格式:【(命名空间的名)::(变量或函数


    其实这个命名空间的使用,有三种:例)

    1. std::cout << "xxx" << std::endl;   //哪里使用,就在哪里添加命名空间的标识

    2. using namespace std;           //直接在include下一行写,不推荐这么用,失去命名空间的意义了

    3. using std::cout;             //直接在include下一行写,

      cout << "xxx" << std::endl;     (要使用哪个变量或函数,就提取出来哪个

                             cout提取了,但endl没提取,所以cout不需要std前缀,而endl仍需要。



    关于链接:

    查资料的时候,有这么几个文章讲得很好,还很仔细,但临时时间不够,留着以后再看。

    《理解C++的链接:C++内链接与外链接的意义》《c c++ static之谈》《c语言和堆栈》

    《 c++ 内部链接 外部链接》


    12月3号再整理成文字。

    ---------------------------------------------------------------------------------------------------

    提到链接性,就要把存储持续性和作用域也拎出来。

    下面这一张图,就说清了这三个概念之间的关系。



    自动:存在于代码块中,就是局部变量。比如在一个函数体里声明的变量,函数运行结束后就自动释放了,内存自动归还给系统。

    寄存器:还没到控制这种变量的水平和场合,没研究。~~~哈哈哈

    静态,无链接性:这种就还是局部变量。也是比如在函数体里声明的static前缀的变量,外面不能用,也就没有外部链接性。然后变量本身就在代码块内部,也不涉及到内部链接性。so,无链接性。静态无链接性的变量,只在该代码块中可用,但好处是,这种变量在该代码块不处于活动状态时仍存在。【这个类型是加了static的】


    静态,外部链接性:声明的地方就不在代码块里了,而是在文件中。例如在.cpp或者是.h中,声明一下变量,也不加static,这样就拥有外部链接性了。其实这个变量在文件内部里都能用,就属于是全局变量了。而所谓的外部,就是在其他文件中使用,比如在哪个文件中使用,就在那个文件中用extern再声明一下这个变量就ok啦。(测试过,用extern声明的话,不用包含对应头文件,直接extern声明一下,就能用。)【这个类型是不加static的】

    静态,内部链接性:声明的地方还是不再代码块里,应该在文件中,和上面那种外部链接性的是在一个位置声明的。只是,这个前面加一个static前缀,这样就有内部链接性了。外面文件不能使用这个变量了,就是extern也不行了。静态内部链接性extern不行的时候,使用include包含.h就ok了,这样就可以使用别的文件中的静态内链接变量了。static前缀就是为了使这种位置的变量具有内部链接性。【这个类型是加了static的


    差点儿忘说一个事儿,当外部变量和局部变量冲突的时候(也就是同名,同类型),外部变量会暂时被局部变量覆盖(就跟函数被重载了似的)。等局部代码块执行结束后,又恢复到了外部变量。此刻局部的那个变量已经被释放了,此刻只剩下这个外部变量还在内存中了。

    还有const的事儿,如果声明的静态外部链接性的变量前面有const,那会秒变内部链接性,因为const自带static属性。但如果非要外部用,可以在初始化声明的时候,最前面加个extern。注意!!!是初始化声明的时候,就加extern。然后在要用这个变量的其他文件中,就正常加上extern声明就行了,不用初始化,因为一定初始化过了。

    展开全文
  • 预处理

    2017-04-30 01:27:24
    所谓预处理是指在进行编译的第一遍扫描(词法扫描和语法分析)之前所作的工作。 预处理是C语言的一个重要功能,它由预处理程序负责完成。 对一个源文件进行编译时,系统将自动引用预处理程序对源程序中...文件包含 #
  • c语言 预处理

    2020-09-15 09:12:10
     在前面各章中,已多次使用过以“#”号开头的预处理命令。如包含命令# include,宏定义命令# define等。在源程序中这些命令都放在函数之外, 而且一般都放在源文件的前面,它们称为预处理部分。 所谓预处理是指在...
  • c++的预处理程序指令

    2016-01-15 09:48:03
    c++中的预处理功能主要是指可以在c++源程序中包含各种编译指令,用这些编译命令...常见的预处理命令有:文件包含命令、条件编译命令和宏定义命令。所有的预处理命令都是以“#”开始的,每一条命令单独占一行。预处理
  • 条件预处理

    千次阅读 2013-08-25 15:23:53
    我们在写代码的时候可以通过预处理命令来调用预处理功能。 在C语言程序中,在每个文件的开头都包含于“#”,这就是预处理的标志。所谓预处理就是在进行编译的第一遍之前(词法扫描和语法分析)所作的工作。对...
  • C语言--预处理

    2018-03-26 00:28:13
    在平时的学习中,我们看到类似于“#define ...”的时候都会说,这是一个...分别对应宏定义命令、条件编译命令文件包含命令三部分实现;2.预处理过程读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进...
  • C语言之预处理指令和宏定义 一.预处理命令基本介绍 1.以#号开头的命令称为预处理命令。...5.C语言提供了多种预处理功能,如宏定义、文件包含、条件编译等,合理的使用它们会使编写的程序便于阅读、修改、
  • C语言中预处理的解释和概述 C语言的源程序加工包括三步:预处理、编译和连接。所谓的预处理是指在进行正式编译(此法分析,代码生成,优化等)之前所做的工作。...C语言提供了·多种预处理命令,除文件包含...
  • 文件包含6.条件编译二.总结 一.预处理 1.概述   在前面各章中,已多次使用过以“#”号开头的预处理命令。如包含命令# include,宏定义命令# define等。在源程序中这些命令都放在函数之外, 而且一般都放在源文件...
  • C 预处理 之"条件编译

    2018-11-15 20:00:33
    (2)文件包含 (3)条件编译 预处理命令以符号"#"开头。 采用条件编译,可以减少被编译的语句,从而减少目标的长度。条件编译段比较多时,目标程序长度可以大大减少。 条件编译主要包括: 序号 命令 ...
  • 预处理 const #define

    2011-01-11 11:34:00
    预处理命令以符号“#”开头。 有些语句行希望在条件满足时才编译。格式:(1) #ifdef 标识符 程序段1 #else 程序段2 #endif 或 #ifdef 程序段1 #endif 标识符已经定义时,程序段1才参加编译。 格式:(2) ...
  • 2.gcc/g++编译器提供定义NDEBUG的命令行选项:  $ g++ -NDEBUG ./debug_test.cpp  类似这样的命令行等效于在debug_test.cpp文件的开头提供了#define NDEBUG预处理命令。 3.预处理器定义了四...
  • 在C51语言中,它本身不提供...对输入和输出函数使用时,须先用预处理命令“#include <stdio.h>”将该函数库包含文件中。 在C51的一般I/O函数库中定义的I/O函数都是通过串行接口实现,在使用I/O函数之前,...
  • C语言程序的特点

    千次阅读 2018-09-20 01:56:39
    程序中要使用C系统提供的 “标准函数或其他文件” 时,一定要用 include 命令包含 “函数对应的文件”,以在将对应的文件嵌入到该文件成为源程序的一部分。 预处理命令后不能加 “ ;” 形式为:#include&...
  • C++重复定义问题

    千次阅读 2019-01-08 20:38:52
    网上很多大神提出了在被包含头文件中添加编译预处理命令#ifndef _ABC_H #define _ABC_H … #endif 的方法,但是我试过之后还是不行。原因是我的头文件里写了函数实现,还定义了一些常量。我也不知道是因为什么原因,...
  • #ifdef、#ifndef

    2021-03-05 22:05:09
    预处理命令可以改变程序设计环境,提高编程效率,但本身并不是 C 语言的组成部分,因此不能直接进行编译,必须在对程序编译之前,先对程序中这些特殊的命令进行预处理”。经过预处理后,程序再由编译程序对预处理之后的源...
  • C++ :头文件保护符

    2018-07-16 11:58:18
    在看代码的时候经常会碰到#ifndef ,#endif这样的指令,这个的作用是头文件保护符,用于防止同一头文件的多次包含预处理变量有两种状态:已定义和未定义#define 命令使得某一预处理变量变为已定义#ifdef 命令当且...
  • 预处理命令还有其它几种,这里的include 称为文件包含命令,其意义是把尖括号""或引号内指定的文件包含到本程序来,成为本程序的一部分。被包含的文件通常是由系统提供的,其扩展名为.h。因此也称为头文件或首部文件...
  • 1. 预处理:C预处理器扩展代码,插入所有用#include命令指定的代码,并扩展所有用#define声明指定的宏; 2. 编译:编译器产生源文件的汇编代码(机器代码的文本表示,给出程序中的每一条指令,人可读); 3. 汇编:...
  • 预处理命令还有其它几种,这里的include 称为文件包含命令,其意义是把尖括号或引号""内指定的文件包含到本程序来,成为本程序的一部分。被包含的文件通常是由系统提供的,其扩展名为.h。因此也称为头文件或首部文件...
  • 13.8 预处理命令的其他话题 439 第14章 标准库简介 446 14.1 使用标准库的一些常识 447 14.2 对语言的补充 449 14.3 stdio.h 452 14.4 通用函数:stdlib.h 454 14.5 inttypes.h(C99) 460 14.6 string.h 462 ...
  • 过开发人员,也做过DBA,目前是 Oracle ACE总监和OakTable成员。最近几年,他专注于研究Oracle内部原理以及解决性能问题。他的博客主页是 kerryosborne.oracle-guy.com。  ROBYN SANDS 思科公司的软件...
  • init 命令将创建一个目录,其中包含最小的样板文件. book-test/ ├── book └── src ├── chapter_1.md └── SUMMARY.md book和src都是目录.src包含 markdown 文件, book目录输出渲染的目录. 请看看CLI ...
  • 过开发人员,也做过DBA,目前是 Oracle ACE总监和OakTable成员。最近几年,他专注于研究Oracle内部原理以及解决性能问题。他的博客主页是 kerryosborne.oracle-guy.com。  ROBYN SANDS 思科公司的软件...
  • MAPGIS地质制图工具

    2013-05-06 16:15:30
    辅助工具Ⅰ包含以下功能命令: 文字对齐方式:对点图元进行左对齐、右对齐、上对齐、下对齐,水平平均分配、垂直平均分配以及高(宽)度自动相等e.g.。 导入导出功能:把当前的点、线、面文件属性导出到EXCEL表格或者...

空空如也

空空如也

1 2 3
收藏数 45
精华内容 18
关键字:

当文件包含预处理命令