一.预处理器
C 预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。简言之,C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。我们将把 C 预处理器(C Preprocessor)简写为 CPP。
所有的预处理器命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。
(1)预处理器指令
下面列出了所有重要的预处理器指令:
指令 | 含义 |
---|---|
#define | 定义宏 |
#include | 包含一个源代码文件 |
#undef | 取消已定义的宏 |
#ifdef | 如果宏已经定义,则返回真 |
#ifndef | 如果宏没有定义,则返回真 |
#if | 如果给定条件为真,则编译下面代码 |
#else | #if 的替代方案 |
#elif | 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码 |
#endif | 结束一个 #if……#else 条件编译块 |
#error | 当遇到标准错误时,输出错误消息 |
#pragma | 使用标准化方法,向编译器发布特殊的命令到编译器中 |
//使用 #define 定义常量来增强可读性
#define MAX_ARRAY_LENGTH 20//个指令告诉 CPP 把所有的 MAX_ARRAY_LENGTH 定义为 20
#include <stdio.h>//告诉 CPP 从系统库中获取 stdio.h,并添加文本到当前的源文件中
#include "myheader.h"//告诉 CPP 从本地目录中获取 myheader.h,并添加内容到当前的源文件中
#undef FILE_SIZE
#define FILE_SIZE 42 //告诉 CPP 取消已定义的 FILE_SIZE,并定义它为 42
#ifndef MESSAGE
#define MESSAGE "You wish!"
#endif //告诉 CPP 只有当 MESSAGE 未定义时,才定义 MESSAGE
#ifdef DEBUG
/* Your debugging statements here */
#endif
//告诉 CPP 如果定义了 DEBUG,则执行处理语句。在编译时,如果您向 gcc 编译器传递了 -DDEBUG 开关量,这个指令就非常有用。它定义了 DEBUG,您可以在编译期间随时开启或关闭调试
(2)预定义宏
宏 | 含义 |
---|---|
DATE | 当前日期,一个以 “MMM DD YYYY” 格式表示的字符常量。 |
TIME | 当前时间,一个以 “HH:MM:SS” 格式表示的字符常量。 |
FILE | 这会包含当前文件名,一个字符串常量。 |
LINE | 这会包含当前行号,一个十进制常量。 |
STDC | 当编译器以 ANSI 标准编译时,则定义为 1。 |
#include <stdio.h>
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
(3)预处理器运算符
C 预处理器提供了下列的运算符来帮助您创建宏。
运算符 | 含义 |
---|---|
宏延续运算符(\) | 一个宏通常写在一个单行上。但是如果宏太长,一个单行容纳不下,则使用宏延续运算符(\) |
字符串常量化运算符(#) | 在宏定义中,当需要把一个宏的参数转换为字符串常量时,则使用字符串常量化运算符(#)。在宏中使用的该运算符有一个特定的参数或参数列表 |
标记粘贴运算符(##) | 宏定义内的标记粘贴运算符(##)会合并两个参数。它允许在宏定义中两个独立的标记被合并为一个标记 |
defined() 运算符 | 预处理器 defined 运算符是用在常量表达式中的,用来确定一个标识符是否已经使用 #define 定义过。如果指定的标识符已定义,则值为真(非零)。如果指定的标识符未定义,则值为假(零) |
#include <stdio.h>
#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!
#include <stdio.h>
#define tokenpaster(n) printf ("token" #n " = %d", token##n)
int main(void)
{
int token34 = 40;
tokenpaster(34);
return 0;
}
token34 = 40
#include <stdio.h>
#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!
(4)参数化的宏
CPP 一个强大的功能是可以使用参数化的宏来模拟函数。
下面的代码是计算一个数的平方:
int square(int x) {
return x * x;
}
#define square(x) ((x) * (x))//使用宏重写上面的代码
在使用带有参数的宏之前,必须使用 #define 指令定义。参数列表是括在圆括号内,且必须紧跟在宏名称的后边。宏名称和左圆括号之间不允许有空格。
#include <stdio.h>
#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
二.typedef
C 语言提供了 typedef 关键字,您可以使用它来为类型取一个新的名字。
typedef unsigned char byte;
byte b1, b2;
您也可以使用 typedef 来为用户自定义的数据类型取一个新的名字。例如,您可以对结构体使用 typedef 来定义一个新的数据类型名字,然后使用这个新的数据类型来直接定义结构变量,如下:
#include <stdio.h>
#include <string.h>
typedef struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
} Book;
int main( )
{
Book book;
strcpy( book.title, "C 教程");
strcpy( book.author, "xiaoxin");
strcpy( book.subject, "编程语言");
book.book_id = 12345;
printf( "书标题 : %s\n", book.title);
printf( "书作者 : %s\n", book.author);
printf( "书类目 : %s\n", book.subject);
printf( "书 ID : %d\n", book.book_id);
return 0;
}
书标题 : C 教程
书作者 : xiaoxin
书类目 : 编程语言
书 ID : 12345
typedef 与 #define
#define 是 C 指令,用于为各种数据类型定义别名,与 typedef 类似,但是它们有以下几点不同:
1)typedef 仅限于为类型定义符号名称,#define 不仅可以为类型定义别名,也能为数值定义别名,比如您可以定义 1 为 ONE。
2)typedef 是由编译器执行解释的,#define 语句是由预编译器进行处理的。
三.头文件
头文件是扩展名为 .h 的文件,包含了 C 函数声明和宏定义,被多个源文件中引用共享。有两种类型的头文件:程序员编写的头文件和编译器自带的头文件。
在程序中要使用头文件,需要使用 C 预处理指令 #include 来引用它。前面我们已经看过 stdio.h 头文件,它是编译器自带的头文件。引用头文件相当于复制头文件的内容,但是我们不会直接在源文件中复制头文件的内容,因为这么做很容易出错,特别在程序是由多个源文件组成的时候。C 或 C++ 程序中,建议把所有的常量、宏、系统全局变量和函数原型写在头文件中,在需要的时候随时引用这些头文件。
1)引用头文件的语法
#include <file>//用于引用系统头文件。它在系统目录的标准列表中搜索名为 file 的文件。在编译源代码时,您可以通过 -I 选项把目录前置在该列表前
#include "file"//用于引用用户头文件。它在包含当前文件的目录中搜索名为 file 的文件。在编译源代码时,您可以通过 -I 选项把目录前置在该列表前
2)只引用一次头文件
如果一个头文件被引用两次,编译器会处理两次头文件的内容,这将产生错误。为了防止这种情况,标准的做法是把文件的整个内容放在条件编译语句中。这种结构就是通常所说的包装器 #ifndef。当再次引用头文件时,条件为假,因为HEADER_FILE 已定义。此时,预处理器会跳过文件的整个内容,编译器会忽略它。
#ifndef HEADER_FILE
#define HEADER_FILE
the entire header file file
#endif
3)有条件引用
有时需要从多个不同的头文件中选择一个引用到程序中。例如,需要指定在不同的操作系统上使用的配置参数。您可以通过一系列条件来实现这点,如下:
#if SYSTEM_1
# include "system_1.h"
#elif SYSTEM_2
# include "system_2.h"
#elif SYSTEM_3
...
#endif
//它不是用头文件的名称作为 #include 的直接参数,需要使用宏名称代替即可
#define SYSTEM_H "system_1.h"
...
#include SYSTEM_H