概述
#define:
对于预处理指令#define,是程序已经准备好进入预处理阶段,预处理器查找一行中以#号开始的预处理指令。
预处理器不做计算,不对表达式求值,它只进行替换。一般而言,预处理器发现程序中的宏后,会用宏等价的替换文本进行替换。如果替换的字符串中还包含宏,则继续替换这些宏。
如果这样定义一条预处理指令:
#define FOUR 2+2
然后这样调用:
int hex = FOUR * FOUR;
打印hex的值为8而不是想要的16。因为在预处理阶段只进行替换操作,所以hex=2+2*2+2=8。所以在声明预处理指令时要多加括号,比如下面的定义就能解决问题:
#define FOUR (2+2)
只进行替换,所以下面两条定义是不同的:
#define SIX 2 * 3
#define SIX 2*3
因为第一条定义里面包含两个空格。
类函数宏定义:可以在#define中使用参数,当作函数来用。其实,在stdio.h标准库中的getc()函数就是类函数宏。
#define SQUARE(X) X*X
可以这样使用:z = SQUARE(2);
就像之前所述,要多加括号,特别是对表达式或每个变量。比如有如下的运用宏:
int x = 5;
int y = SQUARE(x+2);
y=17而不是49,替换一下试试便知:y = 5+2*5+2=17。所以要这样定义宏(多加括号):
#define SQUARE(X) ((X)*(X))
同样对于定义一个函数宏处理z=(x+y)/2,就要像下面这样定义,否则也会在特定条件下出现计算错误:
#define AVERAGE(X,Y) (((X)+(Y))/2)
看到上面对每个变量都加了(),(X)+(Y)加了(),同时最外层也加了(),保证不会在替换时出现任何的计算错误。
#define PSQR(X) printf("The square of X is %d.n", ((X)*(X)));
PSQR(8);
输出为:
The square of X is 64.
①记号转字符串:#运算符
可以在字符串中包含宏参数,这时就要用到预处理运算符#:把记号转换成字符串。如果x是宏形参,#x就转换为字符串"x"的形参名。这个过程称为字符串化(stringizing)。
#include <stdio.h>
#define PSQR(x) printf("The square of " #x " is %d.n",((x)*(x)))
int main(void) {
int y = 5;
PSQR(y);
PSQR(2 + 4);
return 0;
}
The square of y is 25.
The square of 2 + 4 is 36.
②预处理器黏合剂:##运算符
与#运算符类似,##运算符可用于类函数宏的替换部分。而且,##还可用于对象宏的替换部分。##运算符把两个记号组合成一个记号。
#define XNAME(n) x ## n
宏XNAME(4)将展开为:x4
是不是黏合起来了:x ## n -> xn
#include <stdio.h>
#define XNAME(n) x ## n
#define PRINT_XN(n) printf("x" #n " = %dn", x ## n);
int main(void) {
int XNAME(1) = 14; // 变成 int x1 = 14;
int XNAME(2) = 20; // 变成 int x2 = 20;
int x3 = 30;
PRINT_XN(1); // 变成 printf("x1 = %dn", x1);
PRINT_XN(2); // 变成 printf("x2 = %dn", x2);
PRINT_XN(3); // 变成 printf("x3 = %dn", x3);
return 0;
}
x1 = 14
x2 = 20
x3 = 30
③变参宏:...和_ _VA_ARGS_ _
#define PR(...) printf(_ _VA_ARGS_ _)
PR("Howdy");
PR("weight = %d, shipping = $%.2fn", wt, sp);
展开后的代码是:
printf("Howdy");
printf("weight = %d, shipping = $%.2fn", wt, sp);
宏可以完成函数的功能,那么宏和函数如何选择?
宏和函数的选择实际上是时间和空间的权衡。宏生成内联代码,即在程序中生成语句。如果调用20次宏,即在程序中插入20行代码。如果调用函数20次,程序中只有一份函数语句的副本,所以节省了空间。然而另一方面,程序的控制必须跳转至函数内,随后再返回主调程序,这显然比内联代码花费更多的时间。
宏的一个优点是,不用担心变量类型(这是因为宏处理的是字符串,而不是实际的值)。因此,只要能用int或float类型都可以使用SQUARE(x)宏。
对于简单的函数,程序员通常使用宏,如下所示:
#define MAX(X,Y) ((X) > (Y) ? (X) : (Y))
#define ABS(X) ((X) < 0 ? -(X) : (X))
#define ISSIGN(X) ((X) == '+' || (X) == '-' ? 1 : 0)
如果打算使用宏来加快程序的运行速度,那么首先要确定使用宏和使用函数是否会导致较大差异。在程序中只使用一次的宏无法明显减少程序的运行时间。在嵌套循环中使用宏更有助于提高效率。许多系统提供程序分析器以帮助程序员压缩程序中最耗时的部分。
#include:
当预处理器发现#include 指令时,会查看后面的文件名并把文件的内容包含到当前文件中,即替换源文件中的#include指令。这相当于把被包含文件的全部内容输入到源文件#include指令所在的位置。
#include <stdio.h> ←查找系统目录
#include "hot.h" ←查找当前工作目录
#include "/usr/biff/p.h" ←查找/usr/biff目录
头文件.h可以去用作类似JAVA的接口的函数原型,然后在另一个.c文件中实现这些“接口”。
“接口”——头文件:
// 头文件:names_st.h
#include <string.h>
#define SLEN 32 // 常量
struct names_st { // 结构声明
char first[SLEN];
char last[SLEN];
};
typedef struct names_st names; // 类型定义
// 函数原型
void get_names(names *);
void show_names(const names *);
char * s_gets(char * st, int n);
“实现”——.c文件:
// names_st.c -- 定义 names_st.h中的函数
#include <stdio.h>
#include "names_st.h" // 包含头文件
// 函数定义
void get_names(names * pn) {
printf("Please enter your first name: ");
s_gets(pn->first, SLEN);
printf("Please enter your last name: ");
s_gets(pn->last, SLEN);
}
void show_names(const names * pn) {
printf("%s %s", pn->first, pn->last);
}
char * s_gets(char * st, int n) {
char * ret_val;
char * find;
ret_val = fgets(st, n, stdin);
if (ret_val) {
find = strchr(st, 'n'); // 查找换行符
if (find) // 如果地址不是NULL,
*find = '