Kodewick
第 28 课,共 30 课目录

预处理

第 28 课,共 30 课 · 第 6 部分 · 再往前 · 约 15 分钟

以 # 开头的行不是 C 的语句。编译之前,有一个叫预处理器的工具先把代码过一遍,专门处理这些行:把文件抄进来,把名字换成文字。编译器看到的是处理完的结果。

main.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
main.c
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 定义常量

一个数在好几个地方出现,最好给它起个名字。要改的时候,只改一行:

main.c
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 没有 =,结尾也没有 ;。它不是语句,预处理器会把名字后面的全部文字照抄过去。不小心多写一个分号,看看会怎样:

main.c
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}

SQUARE(4) 得 16,没问题;可参数是个算式,或者外面还有运算,结果就错了:

main.c
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 外面加一层,让传进来的东西抱成一团;整个外面再加一层,让外面的运算拆不开它:

main.c
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 时想多显示点东西,这个很好用:

main.c
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。

main.c
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 元
#defineconst
是什么预处理器换进去的文字不能改的变量
有没有类型没有有
单步时看得到吗看不到看得到
能当数组大小吗能定长数组不行

所以 C 里数组的大小还是用 #define,比如上面的 int booked[SEATS];别的常量,优先用 const。

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。

练习 1

给数起名字

在最上面加上 #define ADULT 12 和 #define CHILD 7,用它们算出 2 个大人、3 个小孩的票价,显示 合计:45 元。

要显示
合计:45 元
exercise1.c
1#include <stdio.h>2 3int main(void) {4    int adults = 2;5    int children = 3;6 7    return 0;8}
输出
点「运行」或按 Ctrl+Enter
练习 2

修好这个宏

DOUBLE(5) * 2 应该是 20,可程序显示 15。改宏(不改 main),让它显示 20。

要显示
20
exercise2.c
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}
输出
点「运行」或按 Ctrl+Enter
练习 3

有时候才加税

只有定义了 WITH_TAX 时才给价格加税。用 #ifdef 写,让程序显示 55.00(删掉 #define WITH_TAX 后显示 50.00)。

要显示
55.00
exercise3.c
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}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • 预处理器在编译之前处理 # 开头的行:把文件抄进来,把文字换掉。
  • #include <名字.h> 拿进来头文件,里面是库函数的原型。
  • #define 名字 值 把名字换成后面的文字。没有 =,也没有 ;:多写的 ; 也会被抄过去。
  • 带参数的宏换的是文字,不是值:每个参数和整个外面都要加括号,((x) * (x))。
  • #ifdef 名字 ... #endif 只在名字定义过时保留代码;#ifndef 和 #else 管另外的情况。
  • 常量一般用 const 更好:有类型,单步时看得到。数组大小还是用 #define。