预处理
以 # 开头的行不是 C 的语句。编译之前,有一个叫预处理器的工具先把代码过一遍,专门处理这些行:把文件抄进来,把名字换成文字。编译器看到的是处理完的结果。
1#include <stdio.h>2 3#define DAYS_IN_WEEK 74 5int main(void) {6 int weeks = 3;7 printf("%d 周 = %d 天\n", weeks, weeks * DAYS_IN_WEEK);8 return 0;9}
3 周 = 21 天
#define DAYS_IN_WEEK 7 是告诉预处理器:「凡是出现 DAYS_IN_WEEK 的地方,都换成 7」。等代码到了编译器手里,那一行已经是 weeks * 7 了,编译器根本见不到这个名字。
#include
#include <stdio.h> 把 stdio.h 这个文件的全部内容放到这一行的位置上。stdio.h 这样的文件叫头文件,里面是 printf 这些函数的原型(第 8 课),编译器有了它才知道这些函数长什么样。到现在为止用过的头文件有这些:
| 头文件 | 里面有 |
|---|---|
<stdio.h> | printf、scanf、fopen、fgets、NULL |
<string.h> | strlen、strcpy、strcmp |
<ctype.h> | toupper、isdigit |
<stdlib.h> | malloc、realloc、free |
<limits.h> | INT_MAX |
1#include <stdio.h>2#include <string.h>3#include <ctype.h>4#include <limits.h>5 6int main(void) {7 char city[] = "lisbon";8 city[0] = toupper(city[0]);9 printf("%s 有 %zu 个字母\n", city, strlen(city));10 printf("int 最大是 %d\n", INT_MAX);11 return 0;12}
Lisbon 有 6 个字母 int 最大是 2147483647
尖括号 < > 的意思是「C 自带的头文件」。大一些的项目还会自己写头文件,用双引号拿进来,比如 #include "contacts.h"。这一页的例子都只有一个文件,用不着。
用 #define 定义常量
一个数在好几个地方出现,最好给它起个名字。要改的时候,只改一行:
1#include <stdio.h>2 3#define SEATS 54#define TICKET_PRICE 125 6int main(void) {7 int booked[SEATS] = {1, 0, 1, 1, 0};8 int sold = 0;9 for (int i = 0; i < SEATS; i++) {10 sold += booked[i];11 }12 printf("%d 个座位卖出了 %d 个\n", SEATS, sold);13 printf("收入:%d 元\n", sold * TICKET_PRICE);14 return 0;15}
5 个座位卖出了 3 个 收入:36 元
名字习惯用大写,一眼就能和变量分开。还有,#define 没有 =,结尾也没有 ;。它不是语句,预处理器会把名字后面的全部文字照抄过去。不小心多写一个分号,看看会怎样:
1#include <stdio.h>2 3#define PRICE 5;4 5int main(void) {6 int total = PRICE * 3;7 printf("%d\n", total);8 return 0;9}
main.c: In function 'main':
main.c:6:23: error: invalid type argument of unary '*' (have 'int')
6 | int total = PRICE * 3;
| ^~~换完以后,第 6 行变成了 int total = 5; * 3;。; 在 5 后面就把语句结束了,剩下一个 * 3; 孤零零的:编译器把这个 * 当成「顺着指针找过去」,可 3 不是指针。报错指着第 6 行,错却在第 3 行。看到莫名其妙的错误时,回头查查 #define。
带参数的宏
#define 还可以带参数,有点像函数。这种叫宏:
这段程序显示什么?
1#include <stdio.h>2 3#define SQUARE(x) x * x4 5int main(void) {6 printf("%d\n", SQUARE(2 + 1));7 return 0;8}
宏不会先把 2 + 1 算出来,它只是换文字:SQUARE(2 + 1) 变成 2 + 1 * 2 + 1。* 先算,所以是 2 + 2 + 1,等于 5。
SQUARE(4) 得 16,没问题;可参数是个算式,或者外面还有运算,结果就错了:
1#include <stdio.h>2 3#define SQUARE(x) x * x4 5int main(void) {6 printf("%d\n", SQUARE(4));7 printf("%d\n", SQUARE(2 + 1));8 printf("%d\n", 100 / SQUARE(5));9 return 0;10}
16 5 100
第三行本该是 100 / 25,等于 4;可它变成了 100 / 5 * 5。/ 和 * 从左往右算,是 20 * 5,等于 100。
到处加括号
办法是加括号:每个 x 外面加一层,让传进来的东西抱成一团;整个外面再加一层,让外面的运算拆不开它:
1#include <stdio.h>2 3#define SQUARE(x) ((x) * (x))4 5int main(void) {6 printf("%d\n", SQUARE(4));7 printf("%d\n", SQUARE(2 + 1));8 printf("%d\n", 100 / SQUARE(5));9 return 0;10}
16 9 4
现在 SQUARE(2 + 1) 变成 ((2 + 1) * (2 + 1)),100 / SQUARE(5) 变成 100 / ((5) * (5)),三个都对了。
还有一个坑括号也救不了:x 出现了两次,传进来的东西就会算两次。SQUARE(n++) 在同一个式子里让 n 加了两次,这是未定义行为。普通函数,比如 int square(int x),就没有这些麻烦。小东西可以用宏,拿不准就写函数。
让代码有或没有:#ifdef
#ifdef 名字 和 #endif 之间的代码,只有这个名字用 #define 定义过才保留;没定义过,预处理器在编译前就把它删掉。找 bug 时想多显示点东西,这个很好用:
1#include <stdio.h>2 3#define DEBUG4 5int main(void) {6 int prices[4] = {12, 8, 15, 5};7 int total = 0;8 for (int i = 0; i < 4; i++) {9 total += prices[i];10#ifdef DEBUG11 printf("[debug] i=%d total=%d\n", i, total);12#endif13 }14 printf("合计:%d\n", total);15 return 0;16}
[debug] i=0 total=12 [debug] i=1 total=20 [debug] i=2 total=35 [debug] i=3 total=40 合计:40
把 #define DEBUG 这一行删掉,再运行:[debug] 那几行没了,因为那个 printf 已经根本不在程序里了。#ifndef 名字 正好反过来(名字没有定义时才保留),#else 给出另一种情况:
| 写法 | 什么时候保留代码 |
|---|---|
#ifdef 名字 ... #endif | 名字定义过 |
#ifndef 名字 ... #endif | 名字没定义过 |
#ifdef 名字 ... #else ... #endif | 定义过留前一段,没定义过留后一段 |
const 还是 #define?
表示一个不变的值,一般用 const 变量(第 2 课)更好。它有类型,编译器会检查;它是个真的变量:点单步,能在变量里看到 TAX_RATE。SERVICE 却怎么也看不到,因为编译器见到的只是 3。
1#include <stdio.h>2 3#define SERVICE 34 5int main(void) {6 const double TAX_RATE = 0.08;7 double meal = 40.0;8 double bill = meal + meal * TAX_RATE + SERVICE;9 printf("账单:%.2f 元\n", bill);10 return 0;11}
账单:46.20 元
#define | const | |
|---|---|---|
| 是什么 | 预处理器换进去的文字 | 不能改的变量 |
| 有没有类型 | 没有 | 有 |
| 单步时看得到吗 | 看不到 | 看得到 |
| 能当数组大小吗 | 能 | 定长数组不行 |
所以 C 里数组的大小还是用 #define,比如上面的 int booked[SEATS];别的常量,优先用 const。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
给数起名字
✓在最上面加上 #define ADULT 12 和 #define CHILD 7,用它们算出 2 个大人、3 个小孩的票价,显示 合计:45 元。
合计:45 元
1#include <stdio.h>2 3int main(void) {4 int adults = 2;5 int children = 3;6 7 return 0;8}
修好这个宏
✓DOUBLE(5) * 2 应该是 20,可程序显示 15。改宏(不改 main),让它显示 20。
20
1#include <stdio.h>2 3#define DOUBLE(x) x + x4 5int main(void) {6 printf("%d\n", DOUBLE(5) * 2);7 return 0;8}
有时候才加税
✓只有定义了 WITH_TAX 时才给价格加税。用 #ifdef 写,让程序显示 55.00(删掉 #define WITH_TAX 后显示 50.00)。
55.00
1#include <stdio.h>2 3#define WITH_TAX4 5int main(void) {6 const double TAX_RATE = 0.1;7 double price = 50.0;8 9 printf("%.2f\n", price);10 return 0;11}
这一课学会了
- 预处理器在编译之前处理
#开头的行:把文件抄进来,把文字换掉。 #include <名字.h>拿进来头文件,里面是库函数的原型。#define 名字 值把名字换成后面的文字。没有=,也没有;:多写的;也会被抄过去。- 带参数的宏换的是文字,不是值:每个参数和整个外面都要加括号,
((x) * (x))。 #ifdef 名字...#endif只在名字定义过时保留代码;#ifndef和#else管另外的情况。- 常量一般用
const更好:有类型,单步时看得到。数组大小还是用#define。