目录
一、预处理命令简介
二、#include用法介绍
三、#define用法介绍
3.1 定义
3.2 作用域
3.3 不生效情况
3.4 简单特点
3.5 #define和#typedef区别:
3.6 C语言带参数的宏定义(参数外面最好套括号)
3.7 C语言宏参数的字符串化-#的用法
3.8 C语言宏参数的连接-##的用法
3.9 C语言中几个预定义宏、
VS下的输出结果:Date : Mar 6 2016Time : 11:47:15File : main.cLine : 8四、条件编译预处理命令
五、C语言#error命令(阻止程序编译)
六、C++预处理命令总结:
使用库函数之前,应该用#include
引入对应的头文件。这种以#
号开头的命令称为预处理命令。
编译是针对单个源文件的,一次编译操作只能编译一个源文件。如果程序中有多个源文件,就需要多次编译操作。
链接(Link)是针对多个文件的,它会将编译生成的多个目标文件以及系统中的库、组件等合并成一个可执行程序。
在实际开发中,有时候在编译之前还需要对源文件进行简单的处理。例如,我们希望自己的程序在 Windows 和 Linux 下都能够运行,那么就要在 Windows 下使用 VS 编译一遍,然后在 Linux 下使用 GCC 编译一遍。但是现在有个问题,程序中要实现的某个功能在 VS 和 GCC 下使用的函数不同(假设 VS 下使用 a(),GCC 下使用 b()),VS 下的函数在 GCC 下不能编译通过,GCC 下的函数在 VS 下也不能编译通过,怎么办呢?
这就需要在编译之前先对源文件进行处理:如果检测到是 VS,就保留 a() 删除 b();如果检测到是 GCC,就保留 b() 删除 a()。
这些在编译之前对源文件进行简单加工的过程,就称为预处理(即预先处理、提前处理)。
预处理主要是处理以#
开头的命令,例如#include
等。预处理命令要放在所有函数之外,而且一般都放在源文件的前面。
预处理是C语言的一个重要功能,由预处理程序完成。当对一个源文件进行编译时,系统将自动调用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。
编译器会将预处理的结果保存到和源文件同名的.i
文件中,例如 main.c 的预处理结果在 main.i 中。和.c
一样,.i
也是文本文件,可以用编辑器打开直接查看内容。
C语言提供了多种预处理功能,如宏定义、文件包含、条件编译等,合理地使用它们会使编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
举例:
假如现在要开发一个C语言程序,让它暂停 5 秒以后再输出内容,并且要求跨平台,在 Windows 和 Linux 下都能运行,怎么办呢?
这个程序的难点在于,不同平台下的暂停函数和头文件都不一样:
void Sleep(DWORD dwMilliseconds)
(注意 S 是大写的),参数的单位是“毫秒”,位于 unsigned int sleep (unsigned int seconds)
,参数的单位是“秒”,位于
不同的平台下必须调用不同的函数,并引入不同的头文件,否则就会导致编译错误,因为 Windows 平台下没有 sleep() 函数,也没有
#include
//不同的平台下引入不同的头文件
#if _WIN32 //识别windows平台
#include
#elif __linux__ //识别linux平台
#include
#endif
int main() {
//不同的平台下调用不同的函数
#if _WIN32 //识别windows平台
Sleep(5000);
#elif __linux__ //识别linux平台
sleep(5);
#endif
puts("http://c.biancheng.net/");
return 0;
}
你看,在不同的平台下,编译之前(预处理之后)的源代码都是不一样的。这就是预处理阶段的工作,它把代码当成普通文本,根据设定的条件进行一些简单的文本替换,将替换以后的结果再交给编译器处理。
使用尖括号< >
和双引号" "
的区别在于头文件的搜索路径不同:
< >
,编译器会到系统路径下查找头文件;" "
,编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找。所以系统头文件一般用尖括号,自定义头文件一般用双引号。
「在头文件中定义定义函数和全局变量」这种认知是原则性的错误!不管是标准头文件,还是自定义头文件,都只能包含变量和函数的声明,不能包含定义,否则在多次引入时会引起重复定义错误。
#define 叫做宏定义命令,它也是C语言预处理命令的一种。所谓宏定义,就是用一个标识符来表示一个字符串,如果在后面的代码中出现了该标识符,那么就全部替换成指定的字符串。#define不加引号。
宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束(问题:在另一个源文件中也有用吗,需要测试一下,如果暗中全局变量的理解来看是有用的,但是我觉得会有变化)。如要终止其作用域可使用#undef
命令。例如:
#define PI 3.14159
int main(){
// Code
return 0;
}
#undef PI
void func(){
// Code
}
表示 PI 只在 main() 函数中有效,在 func() 中无效。
代码中的宏名如果被引号包围,那么预处理程序不对其作宏代替,例如:
#include
#define OK 100
int main(){
printf("OK\n");
return 0;
}
宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名,在宏展开时由预处理程序层层代换。
习惯上宏名用大写字母表示,以便于与变量区别。但也允许用小写字母。
可用宏定义表示数据类型,但是尽量不要表示指针类型,因为*号结合标识符而不是数据类型。例如:
#define UINT unsigned int
宏定义只是简单的字符串替换,由预处理器来处理;而 typedef 是在编译阶段由编译器处理的,它并不是简单的字符串替换,而给原有的数据类型起一个新的名字,将它作为一种新的数据类型。
#define PIN1 int *
typedef int *PIN2; //也可以写作typedef int (*PIN2);
PIN1 a, b;
在宏代换后变成:
int * a, b;
表示 a 是指向整型的指针变量,而 b 是整型变量。
然而:
PIN2 a,b;
表示 a、b 都是指向整型的指针变量。因为 PIN2 是一个新的、完整的数据类型。
由这个例子可见,宏定义虽然也可表示数据类型, 但毕竟只是简单的字符串替换。
在使用时要格外小心,以避出错。
尽量不要用宏来简化指针数据类型,麻烦。
#include
#define MAX(a,b) (a>b) ? a : b
无需注明a,b的数据类型,因为不给a,b分配内存,纯粹代码替换而已,预处理就替换过宏了,不参加编译。
函数则不同,形参和实参是两个不同的变量,都有自己的作用域。形参是要求有数据类型的。
int main(){
int x , y, max;
printf("input two numbers: ");
scanf("%d %d", &x, &y);
max = MAX(x, y);
printf("max=%d\n", max);
return 0;
}
比较下面两个代码:
(1)
#include
#define SQ(y) (y)*(y)
int main(){
int a, sq;
printf("input a number: ");
scanf("%d", &a);
sq = SQ(a+1);
printf("sq=%d\n", sq);
return 0;
}
(2)
#include
#define SQ(y) y*y
int main(){
int a, sq;
printf("input a number: ");
scanf("%d", &a);
sq = SQ(a+1);
printf("sq=%d\n", sq);
return 0;
}
#
用来将宏参数转换为字符串,也就是在宏参数的开头和末尾添加引号。例如有如下宏定义:
#define STR(s) #s
那么:
printf("%s", STR(c.biancheng.net));
printf("%s", STR("c.biancheng.net"));
分别被展开为:
printf("%s", "c.biancheng.net");
printf("%s", "\"c.biancheng.net\"");
可以发现,即使给宏参数“传递”的数据中包含引号,使用#仍然会在两头添加新的引号,而原来的引号会被转义。
#include
#define STR(s) #s
int main() {
printf("%s\n", STR(c.biancheng.net));
printf("%s\n", STR("c.biancheng.net"));
return 0;
}
运行结果:
c.biancheng.net
"c.biancheng.net"
##称为连接符,用来将宏参数或其他的串连接起来。例如有如下的宏定义:
#define CON1(a, b) a##e##b
#define CON2(a, b) a##b##00
那么:
printf("%f\n", CON1(8.5, 2));
printf("%d\n", CON2(12, 34));
将被展开为:
printf("%f\n", 8.5e2);
printf("%d\n", 123400);
ANSI C 规定了以下几个预定义宏,它们在各个编译器下都可以使用:
#include
#include
int main() {
printf("Date : %s\n", __DATE__);
printf("Time : %s\n", __TIME__);
printf("File : %s\n", __FILE__);
printf("Line : %d\n", __LINE__);
system("pause");
return 0;
}
(1)#if的用法
#include
int main(){
#if _WIN32
system("color 0c");
printf("http://c.biancheng.net\n");
#elif __linux__
printf("\033[22;31mhttp://c.biancheng.net\n\033[22;30m");
#else
printf("http://c.biancheng.net\n");
#endif
return 0;
}
(2)#ifdef用法
VS/VC 有两种编译模式,Debug 和 Release。在学习过程中,我们通常使用 Debug 模式,这样便于程序的调试;而最终发布的程序,要使用 Release 模式,这样编译器会进行很多优化,提高程序运行效率,删除冗余信息。
没有#elif。
为了能够清楚地看到当前程序的编译模式,我们不妨在程序中增加提示,请看下面的代码:
#include
#include
int main(){
#ifdef _DEBUG
printf("正在使用 Debug 模式编译程序...\n");
#else
printf("正在使用 Release 模式编译程序...\n");
#endif
system("pause");
return 0;
}
(3)#ifndef 的用法
#ifndef 宏名
程序段1
#else
程序段2
#endif
如果当前的宏未被定义即是假的,则对“程序段1”进行编译,否则对“程序段2”进行编译,这与 #ifdef 的功能正好相反。
注意:三者之间的区别
#if 后面跟的是“整型常量表达式”,而 #ifdef 和 #ifndef 后面跟的只能是一个宏名,不能是其他的。
例如,下面的形式只能用于 #if:
#include
#define NUM 10
int main(){
#if NUM == 10 || NUM == 20
printf("NUM: %d\n", NUM);
#else
printf("NUM Error\n");
#endif
return 0;
}
#error 指令用于在编译期间产生错误信息,并阻止程序的编译,其形式如下:
#error error_message
例如,我们的程序针对 Linux 编写,不保证兼容 Windows,那么可以这样做:
#ifdef WIN32
#error This programme cannot compile at Windows Platform
#endif
这将导致程序编译失败。
再如,当我们希望以 C++ 的方式来编译程序时,可以这样做:
#ifndef __cplusplus
#error 当前程序必须以C++方式编译
#endif
指令 | 说明 |
---|---|
# | 空指令,无任何效果 |
#include | 包含一个源代码文件 |
#define | 定义宏 |
#undef | 取消已定义的宏 |
#if | 如果给定条件为真,则编译下面代码 |
#ifdef | 如果宏已经定义,则编译下面代码 |
#ifndef | 如果宏没有定义,则编译下面代码 |
#elif | 如果前面的#if给定条件不为真,当前条件为真,则编译下面代码 |
#endif | 结束一个#if……#else条件编译块 |