• C语言之预处理,头文件


    1 预处理

    1.1 简介

    预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。简言之,预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。我们将把 C 预处理器(C Preprocessor)简写为 CPP

    所有的预处理器命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。
    这些预处理器命令必须在一行的开头以(#)开始,末尾不再加号,以区别于C语言的其他语句

    下面列出了所有重要的预处理器指令:

    指令描述
    #define定义宏
    #include包含一个源代码文件
    #undef取消已定义的宏
    #ifdef如果宏已经定义,则返回真
    #ifndef如果宏没有定义,则返回真
    #if如果给定条件为真,则编译下面代码
    #else#if 的替代方案
    #elif如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码
    #endif结束一个 #if……#else 条件编译块
    #error当遇到标准错误时,输出错误消息
    #pragma使用标准化方法,向编译器发布特殊的命令到编译器中

    1.2 预处理器实例

    分析下面的实例来理解不同的指令。

    #define MAX_ARRAY_LENGTH 20
    
    • 1

    这个指令告诉 CPP 把所有的 MAX_ARRAY_LENGTH 定义为 20。使用 #define 定义常量来增强可读性。

    #include   
    #include "myheader.h"
    
    • 1
    • 2

    这些指令告诉 CPP 从系统库中获取 stdio.h,并添加文本到当前的源文件中。下一行告诉 CPP 从本地目录中获取 myheader.h,并添加内容到当前的源文件中。

    #undef  FILE_SIZE
    #define FILE_SIZE 42
    
    • 1
    • 2

    这个指令告诉 CPP 取消已定义的 FILE_SIZE,并定义它为 42。

    #ifndef MESSAGE
       #define MESSAGE "You wish!"
    #endif
    
    • 1
    • 2
    • 3

    这个指令告诉 CPP 只有当 MESSAGE 未定义时,才定义 MESSAGE。

    #ifdef DEBUG
       /* Your debugging statements here */
    #endif
    
    • 1
    • 2
    • 3

    这个指令告诉 CPP 如果定义了 DEBUG,则执行处理语句。在编译时,如果向 gcc 编译器传递了 -DDEBUG 开关量,这个指令就非常有用。它定义了 DEBUG,可以在编译期间随时开启或关闭调试。

    调试程序,在程序开头定义一个符号常数DEBUG(值为1或0),在程序任何需要设置调试信息之处,加入以下信息:

    if DEBUG
        调试代码
    endif
    
    • 1
    • 2
    • 3

    这里定义DEBUG为1时,为调试状态,即输出跟踪信息。在调试时,程序中定义DEBUG的语句为:#define DEBUG 1
    而调试结束后,将这条语句改为:#define DEBUG 0
    这样,只需修改#define DEBUG这一条语句,便达到了自动删除或增加程序中众多调试语句的效果

    1.3 预定义宏

    1.3.1 简介

    ANSIC 定义了许多宏。在编程中可以使用这些宏,但是不能直接修改这些预定义的宏

    描述
    __DATE__当前日期,一个以 “MMM DD YYYY” 格式表示的字符常量。
    __TIME__当前时间,一个以 “HH:MM:SS” 格式表示的字符常量。
    __FILE__这会包含当前文件名,一个字符串常量。
    __LINE__这会包含当前行号,一个十进制常量。
    __STDC__当编译器以 ANSI 标准编译时,则定义为 1。

    注意:上面前后都是两横线

    让我们来尝试下面的实例:

    #include 
    main()
    {
       printf("File :%s\n", __FILE__ );
       printf("Date :%s\n", __DATE__ );
       printf("Time :%s\n", __TIME__ );
       printf("Line :%d\n", __LINE__ );
       printf("ANSI :%d\n", __STDC__ );
     
    }
    
    结果:
    
    File :test.c
    Date :Jun 2 2012
    Time :03:36:24
    Line :8
    ANSI :1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    1.3.2 不带参数的宏

    不带参数的宏定义命令行形式如下:

    #define 宏名 替换文本
    
    • 1

    #define宏名替换文本之间用空格隔开
    例如:

    #define LENGTH 300
    
    • 1

    其中标识符LENGTH称为宏名,是用户自定义的标识符,不能与同在一段程序中的其他标识符同名。编译时,编译预处理程序对源程序中所有名为LENGTH的标识符用300来替换,这个替换过程称为宏展开。

    使用宏名替代一个字符串,其中一个主要的目的是减少程序中重复书写某些字符串的工作量,比如在程序中一些不太好记忆的参数,重复书写容易出错且很繁琐,这时用宏名来代替该字符串就可以使程序简单明了。其次当程序的一些常量需要改变时,如果没有宏名,那么整个程序用到该常量的地方都需人工一 一修改,若用宏名,则只需改变宏定义命令行,一改全改

    同一个宏名不能重复定义。
    如:

    #define PI 0.14159
    #define PI 0.1416
    
    • 1
    • 2

    二条语句不能同时出现在一段程序中。

    宏定义是用宏名代替一个字符串,也就是简单的置换,并不作语法检查。如:

    #define PI 0.14159
    
    • 1

    中的0.14159如果输入时不小心将数字1误输为字母i,那么在替换时也照样代入,只有在对已作宏展开后的源程序编译链接时才会出现错误。

    宏定义与一般C语言语句不同,末尾不能加分号,否则会连分号一起置换

    可以用undef命令终止宏定义的作用域。一般而言,一个宏定义,从被定义开始直至文件末尾,全程有效。若要更改其作用域则可以用undef命令。如:

    define AAA 10
    main()
    {
    ...
    }
    undef AAA
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    undef AAA语句后,AAA的作用域终止,不能再使用未定义的标识符AAA

    1.3.3 参数化的宏

    在C语言程序设计中,宏定义还可以用于带参数的宏。带参数的宏定义的一般形式为:

    define 宏名(参数表) 字符串
    
    • 1

    CPP 一个强大的功能是可以使用参数化的宏来模拟函数。例如,下面的代码是计算一个数的平方:

    int square(int x) {
       return x * x;
    }
    
    • 1
    • 2
    • 3

    我们可以使用宏重写上面的代码,如下:

    #define square(x) ((x) * (x))
    
    • 1

    在使用带有参数的宏之前,必须使用 #define 指令定义。参数列表是括在圆括号内,且必须紧跟在宏名称的后边。宏名称和左圆括号之间不允许有空格

    #include 
     
    #define MAX(x,y) ((x) > (y) ? (x) : (y))
     
    int main(void)
    {
       printf("Max between 20 and 10 is %d\n", MAX(10, 20));  
       return 0;
    }
    结果:
    Max between 20 and 10 is 20
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    从形式上,带参数的宏定义和函数似乎容易混淆,从上面的例子可以看到,二者在程序中起的作用、以及书写规范上,确实存在一定的相似之处,然而,二者并不相同,主要有以下区别:

    • 函数在调用时,先求出实参表达式的值,再用这个值赋给形式参数;但带参数的宏定义仅仅进行字符替换。
    • 函数调用是在程序运行时分配临时内存单元;而宏展开是在编译时进行,展开时不分配内存单元,不进行值传递。
    • 宏替换不占运行时间,只占编译时间;函数调用占运行时间。
    • 宏替换后使源程序增长;而函数调用不会使源程序增长。
    • 在使用带参数的宏的时候,对带参数的宏的展开只是用语句中宏名后的实参字符串代替#define命令中的形参,这与函数参数传递时,实参将值传递给形参不同,在编写带参数的宏时需加以注意。

    1.4 预处理器运算符

    预处理器提供了下列的运算符来创建宏

    1.4.1 宏延续运算符(\)

    宏延续运算符(\):一个宏通常写在一个单行上。但是如果宏太长,一个单行容纳不下,则使用宏延续运算符(\)。例如:

    #define  message_for(a, b)  \
        printf(#a " and " #b ": We love you!\n")
    
    • 1
    • 2

    1.4.2 字符串常量化运算符(#)

    字符串常量化运算符(#
    在宏定义中,当需要把一个宏的参数转换为字符串常量时,则使用字符串常量化运算符(#)。在宏中使用的该运算符有一个特定的参数或参数列表。

    #include 
     
    #define  message_for(a, b)  \
        printf(#a " and " #b ": We love you!\n")
     
    int main(void)
    {
       message_for(Carole, Debra);
       return 0;
    }
    
    结果:
    
    Carole and Debra: We love you!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    1.4.3 标记粘贴运算符(##)

    宏定义内的标记粘贴运算符(##)会合并两个参数。它允许在宏定义中 两个独立的标记被合并为一个标记

    #include 
    #define tokenpaster(n) printf ("token" #n " = %d", token##n)
    int main(void)
    {
       int token34 = 40;
       
       tokenpaster(34);
       return 0;
    }
    
    结果:
    token34 = 40
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这是怎么发生的,因为这个实例会从编译器产生下列的实际输出:

    printf ("token34 = %d", token34);
    
    • 1

    这个实例演示了 token##n 会连接到 token34 中,在这里,我们使用了字符串常量化运算符(#)和标记粘贴运算符(##)。

    1.4.4 defined() 运算符

    预处理器 defined 运算符是用在常量表达式中的,用来确定一个标识符是否已经使用 #define 定义过。如果指定的标识符已定义,则值为真(非零)。如果指定的标识符未定义,则值为假(零)。下面的实例演示了 defined() 运算符的用法:

    #include 
     
    #if !defined (MESSAGE)
       #define MESSAGE "You wish!"
    #endif
     
    int main(void)
    {
       printf("Here is the message: %s\n", MESSAGE);  
       return 0;
    }
    
    结果:
    Here is the message: You wish!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    2 头文件

    2.1 头文件讲解

    在编写C语言程序时,可以把宏定义语句按照功能不同分别存入到不同的文件,当需要某一类宏定义时,就无需在程序中重新定义,只要把这些宏定义所在的文件,包含在程序的开头就可以了。
    文件包含就是在一个文件中,包含另外一个文件的全部内容,用include命令来实现文件包含的功能。

    include命令的形式如下:

    #include "文件名"或 include <文件名>
    
    • 1
    • 如果文件名是用双引号括起来的,系统将先在源程序所在的目录内查找指定的包含文件,如果找不到,再按系统指定的标准方式到相关的目录去寻找。
    • 如果文件名用尖括号括起来,则系统将直接按照指定的标准方式到相关的目录中去查找。在预编译时,预编译程序将用指定文件中的内容来替换此命令行。

    文件包含在使用时应注意:
    一条文件包含命令只能包含一个文件。如果需要包含多个文件,就必须使用多条文件包含命令。 例如,如果要包含两个文件file1.h和file2.h就不能这样写:

    #include "file1.h,file2.h"#include "file1.h" ,"file2.h"
    必须写成:
    #include "file1.h"
    #include "file2.h"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    其先后顺序与文件内容无关。但如果一个文件要使用另一个文件的内容,则后者文件应写在前面,即如果file1.h要使用file2.h文件中的内容,则文件包含语句必须按如下顺序写:

    include "file2.h"
    include "file1.h"
    
    • 1
    • 2

    文件包含的定义是可以嵌套的,文件包含的嵌套是指一个被包含的文件中还可以包含其他文件。
    被包含的文件常以.h结尾,这类文件中往往是程序所需要的一些说明定义,如符号常量的定义类型定义带参数的宏定义数组结构共用体和枚举的定义等等,以及外部变量的定义、函数的定义或说明。但是被包含的文件可以是任意的文件,不一定是.h文件,也可以是C语言源文件

    2.2 头文件引用

    引用头文件相当于复制头文件的内容,但是我们不会直接在源文件中复制头文件的内容,因为这么做很容易出错,特别在程序是由多个源文件组成的时候。那么建议把所有的常量、宏、系统全局变量和函数原型写在头文件中,在需要的时候随时引用这些头文件

    2.2.1 只引用一次头文件

    如果一个头文件被引用两次,编译器会处理两次头文件的内容,这将产生错误。为了防止这种情况,标准的做法是把文件的整个内容放在条件编译语句中,如下:

    #ifndef HEADER_FILE
    #define HEADER_FILE
    the entire header file file
    #endif
    
    • 1
    • 2
    • 3
    • 4

    这种结构就是通常所说的包装器 #ifndef。当再次引用头文件时,条件为假,因为 HEADER_FILE 已定义。此时,预处理器会跳过文件的整个内容,编译器会忽略它

    2.2.2 有条件引用

    有时需要从多个不同的头文件中选择一个引用到程序中。例如,需要指定在不同的操作系统上使用的配置参数。可以通过一系列条件来实现这点,如下:

    #if SYSTEM_1
       # include "system_1.h"
    #elif SYSTEM_2
       # include "system_2.h"
    #elif SYSTEM_3
       ...
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    但是如果头文件比较多的时候,这么做是很不妥当的,预处理器使用宏来定义头文件的名称。这就是所谓的有条件引用。它不是用头文件的名称作为 #include 的直接参数,只需要使用宏名称代替即可:

     #define SYSTEM_H "system_1.h"
     ...
     #include SYSTEM_H
    
    • 1
    • 2
    • 3

    SYSTEM_H 会扩展,预处理器会查找 system_1.h,就像 #include 最初编写的那样。SYSTEM_H 可通过 -D 选项的 Makefile 定义

  • 相关阅读:
    Linux C++ 051-设计模式之中介者模式
    Flutter各版本发布时间及特性概述
    《Python+Kivy(App开发)从入门到实践》自学笔记:高级UX部件——Bubble气泡
    倒计时2天,“文化数字化战略新型基础设施暨文化艺术链生态建设发布会”启幕在即
    .NET Core中使用gRPC
    【汉字识别】基于模板匹配实现汉字精准识别附Matlab代码
    【代码随想录】链表专栏(Java)
    RAID知识点总结
    Leetcode238. 除自身以外数组的乘积
    在线ios免签是干什么的?
  • 原文地址:https://blog.csdn.net/u012060033/article/details/127602530