这一篇在干嘛?
上一章知道了”程序怎么变成可执行文件”,这一章正式打开第一个 C 程序,逐行解剖它:
#include是干嘛的、main()是谁、%d和\n有什么玄机。还会学到如何声明变量、写注释、调用自定义函数,以及新手最常见的两类错误怎么找。
一个简单的 C 程序示例(A Simple Example)
先看完整代码,试着猜猜它会输出什么:
程序清单 2.1 first.c 程序
#include <stdio.h>
int main(void) /* a simple program */
{
int num; /* define a variable called num */
num = 1; /* assign a value to num */
printf("I am a simple"); /* use the printf() function */
printf("computer.\n");
printf("My favorite number is %d because it is first.\n",num);
return 0;
}编译运行,输出如下——和你猜的一致吗?
I am a simple computer.
My favorite number is 1 because it is first.疑问来了:\n 和 %d 去哪了?为什么两次 printf("...") 没有换行?下面分两遍讲解。

第一遍:快速浏览(Pass 1: Quick Synopsis)
逐行看每个部件的作用:
#include <stdio.h>← 包含另一个文件:把stdio.h文件的内容”复制”进来,它提供键盘输入和屏幕输出的支持;int main(void)← 函数名:main是程序的入口函数;int表示它返回整数,void表示不接受任何参数;/* a simple program */← 注释:给读者看的说明,编译器完全忽略;{← 函数体的开始,最后必有配对的};int num;← 声明语句(declaration):宣布要用一个叫num的变量,类型是int(整数);num = 1;← 赋值语句(assignment):把 1 装进num;printf("I am a simple");← 函数调用(function call):在屏幕上打印引号里的文字,光标留在同一行;printf("computer.\n");← 接着上一行继续打印,\n让光标跳到下一行行首;printf("My favorite number is %d because it is first.\n", num);←%d是占位符,打印时被num的值(1)替换;return 0;← 返回语句:把 0 返回给调用方(操作系统);}← 结束。
第二遍:程序细节(Pass 2: Program Details)
include 指令与头文件
#include <stdio.h> 的效果,等价于把 stdio.h 文件的全部内容原样粘贴到这一行的位置——本质是个复制粘贴操作。#include 是 C 预处理器指令(preprocessor directive):编译器在正式编译前先对源代码做预处理。
stdio.h 名字来自 standard input/output header(标准输入输出头文件)。C 习惯把放在文件开头的成组信息称为”头(header)“。头文件里装的是给编译器用的信息(常量定义、函数名及其用法),函数的实际代码在库文件里,由链接器去找。
为什么输入输出不直接内置在语言里?因为不是所有程序都需要 I/O,C 的哲学是不携带多余的重量——这正是 C 在嵌入式编程(比如汽车油路控制芯片)里流行的原因。
新手坑:漏写头文件
用了
printf()就要#include <stdio.h>。有时漏写了程序”碰巧”也能编译过,但这依赖编译器的宽松行为,不可依赖。规则:用哪个库函数,就包含标准规定的哪个头文件。
main() 函数
int main(void) 这行宣布了一个叫 main 的函数。C 程序总是从 main() 函数开始执行(个别例外不担心)。其他函数你可以随便起名,main() 必须在。
int:main() 的返回类型——它返回一个整数给操作系统;void:括号里表示不传任何信息进来;- 括号本身就是函数名的标志。
古董代码里常见 main() 开头(C90 勉强容忍,C99 已不认)或 void main()(任何标准从未收录)。坚持写 int main(void),换编译器不折腾。
注释(Comments)
/* 和 */ 之间的一切都被编译器忽略。注释可放任何位置:行尾、独占一行、跨多行都行。
C99 新增了第二种风格,来自 C++ 和 Java:// 开始的单行注释,行尾即注释尾。新风格还有个实际好处——老式 /* */ 注释如果误删了一个 */,可能把后面一大段正常代码一起吞成注释(“代码消失”事故);// 注释不跨行,没这个风险。
记忆口诀:注释是写给"三个月后的自己"的
选有意义的变量名 + 在关键处写注释,是提高程序可读性的两大基本技巧,二者互补:变量叫
width就不用注释解释它是什么,但变量叫video_routine_4就必须写注释了。
花括号、函数体和块
{ } 圈出函数体的开始和结束,必须存在,不能省。注意:只有花括号(braces)干这活,圆括号 ( ) 和方括号 [ ] 都不行。花括号还可以把函数内的若干语句聚成一个块(block)——相当于 Pascal 里的 begin/end。
声明(Declarations)
int num; 是声明语句,一次宣布两件事:
- 有一个名叫
num的变量(variable); num的类型是int(整数,没有小数点的数)——编译器据此为它安排合适的内存空间。
int是 C 关键字(keyword),代表一种基本数据类型,不能挪作他用(不能拿它当变量名);num是标识符(identifier)——你给变量、函数起的名字;- 行尾的分号是语句的一部分,标识一条 C 语句的结束(不像 Pascal 里只是分隔符)。
**C 中所有变量必须先声明再使用。**传统规则要求声明放在块的开头;C99 起允许声明出现在块的任何位置(只要在首次使用之前),但本书为兼容旧编译器仍用传统写法。
起名的规则:可用字符是小写字母、大写字母、数字、下划线(_)——第一个字符必须是字母或下划线。C99 允许名字最长 63 个字符(外部标识符 31 个)。名字区分大小写:stars、Stars、STARS 是三个不同的名字。
| 合法名字 | 非法名字 |
|---|---|
| wiggles | $Z]** |
| cat2 | 2cat |
| Hot_Tub | Hot-Tub |
| taxRate | tax rate |
新手坑:别用下划线开头,别抄操作系统 reserved 的名字
操作系统和 C 库常用一两个下划线开头的标识符(如
_kcab),这些是保留标识符(reserved identifiers)——用了虽不报语法错,但可能引发命名冲突。同理,标准库函数名(如printf)也别拿去当变量名。
为什么必须声明变量?四个理由
- 把变量集中在 one place 列出,读者一眼看清程序要什么(配合有意义的名字,如
sheep_count而不是x3); - 想清楚要声明哪些变量,逼你先规划再动手;
- 防范”拼错变量名”这种极隐蔽的 bug:在没有声明的语言里,把
RADIUS1误打成RADIUSl(数字 1 换成字母 l),程序会悄悄新建一个变量,结果错误却极难查出。C 里编译器会直接对未声明的名字报错; - 不声明就编译不过——C 是强制的。
赋值(Assignment)
num = 1; 把值 1 存进 num 的内存位置。注意方向:从右往左赋——右边是值,左边是变量。之后随时可以给 num 赋新值,这就是”变量”名字的由来。

printf() 函数
printf("I am a simple");
printf("computer.\n");
printf("My favorite number is %d because it is first.\n", num);括号里引号包住的内容是传给 printf() 的参数(argument),printf() 把它打印到屏幕上。

\n 的玄机:\ 和 n 两个字符合起来表示一个字符——换行符(newline character),作用等于按一次 Enter:光标跳到下一行行首。为什么不能直接在字符串里按 Enter?因为你按 Enter 时,响应的是编辑器(结束当前行),而不是被存进源代码的指令。\n 影响的是程序运行时的输出显示。
\n 属于转义序列(escape sequence)——用来表示难以输入或无法输入的字符,均以反斜杠开头。类似的有 \t(Tab)和 \b(Backspace)。这也解释了为什么三次 printf() 只输出两行:第一条没带 \n。
%d 的玄机:%d 是占位符(placeholder),标出 num 的值要打印在哪、以什么形式打印——% 提醒”这里要打印一个变量”,d 表示按十进制(decimal)整数打印。printf 名字里的 **f 就是 format(格式化)**的意思,每种数据类型都有自己的格式说明符,后面章节随新类型逐个介绍。
return 语句
return 0; 是程序的最后一条语句。int main(void) 的 int 要求 main 返回一个整数——C 标准要求 main() 这样做。漏掉它,多数编译器会嘟囔两句但照样编译。这个返回值给操作系统用(比如 DOS 和 Unix 会检查它)。
简单程序的结构(The Structure of a Simple Program)
一般规则:程序 = 一个或多个函数,其中必须有一个叫 main()。每个函数由**函数头(header)和函数体(body)**组成:头包含函数名(带括号)等;体由花括号包住,内含一系列以分号结尾的语句。标准 C 程序的骨架:
#include <stdio.h>
int main(void)
{
statements
return 0;
}让程序更可读的技巧(Tips on Making Your Programs Readable)
- 选有意义的变量名 + 写注释(互补使用);
- 用空行分隔概念上不同的部分,如声明区和动作区之间;
- 一行一条语句。C 是自由格式(free-form),一行塞几条语句、一条语句拆几行都合法,但比如下面这样写合法却丑陋:
int main( void ) { int four; four
=
4
;
printf(
"%d\n",
four); return 0;}分号告诉编译器语句在哪结束,但人的阅读体验靠的是换行和缩进。
更进一步:第二个示例(Taking Another Step)
程序清单 2.2 fathm_ft.c 程序(英寻 fathom 换算英尺 feet,1 英寻 = 6 英尺)
// fathm_ft.c -- converts 2 fathoms to feet
#include <stdio.h>
int main(void)
{
int feet, fathoms;
fathoms = 2;
feet = 6 * fathoms;
printf("There are %d feet in %d fathoms!\n", feet, fathoms);
printf("Yes, I said %d feet!\n", 6 * fathoms);
return 0;
}输出:
There are 12 feet in 2 fathoms!
Yes, I said 12 feet!三个新知识点:
- 文档注释:程序第一行用
//注释标明文件名和用途,浏览一堆文件时特别有用; - 多重声明:
int feet, fathoms;逗号分隔一次声明两个变量,等价于分开写两行int feet;和int fathoms;; - 打印多个值:第一个 printf 里有两个
%d,引号后的变量列表依次填入——第一个%d用feet(12),第二个用fathoms(2),逗号分隔。第二个 printf 还证明:打印的不一定是变量,任何能算出正确类型值的东西(如表达式6 * fathoms)都行。
顺便一提:多个函数(While You’re at It—Multiple Functions)
程序清单 2.3 two_func.c 程序——在 main() 之外加入一个自己写的函数:
/* two_func.c -- a program using two functions in one file */
#include <stdio.h>
void butler(void); /* ISO/ANSI C function prototyping */
int main(void)
{
printf("I will summon the butler function.\n");
butler();
printf("Yes. Bring me some tea and writeable CD-ROMS.\n");
return 0;
}
void butler(void) /* start of function definition */
{
printf("You rang, sir?\n");
}输出:
I will summon the butler function.
You rang, sir?
Yes. Bring me some tea and writeable CD-ROMS.butler() 在程序中出现了三次,各司其职:
- 原型(prototype):
void butler(void);告诉编译器”我要用这个函数”。第一个void表示无返回值,第二个void(括号里的)表示无参数。有了原型,编译器在 main() 里用到 butler() 时就能检查用法对不对; - 函数调用:main() 里的
butler();,执行到这行就跳去执行 butler,干完回来继续下一句; - 函数定义:文件末尾的完整实现,格式与 main() 相同——函数头 + 花括号包住的函数体。
两个要点:
- 执行的时机由调用位置决定,与定义位置无关。就算把 butler() 的定义放到 main() 前面,它还是只在 main() 调用它的那一刻执行。所有 C 程序都从 main() 开始跑;
- C 标准建议为所有函数提供原型。标准库函数的原型由头文件代劳(
stdio.h里就有 printf() 的原型)。
记忆口诀:原型在先,调用在中,定义在后
自定义函数的三件套顺序——文件顶部写原型(带分号),main() 里调用,文件末尾给定义(带函数体)。记住这个”预告 → 使用 → 实现”的套路,以后所有多函数程序都是这个骨架。
开始调试(Introducing Debugging)
会写程序了,就有资格犯错(写 bug)了。程序错误俗称 bug,找错改错叫调试(debugging)。
程序清单 2.4 nogood.c 程序——里面埋了好几个错,先自己找找看:
/* nogood.c -- a program with errors */
#include <stdio.h>
int main(void)
(
int n, int n2, int n3;
/* this program has several errors
n = 5;
n2 = n * n;
n3 = n2 * n2;
printf("n = %d, n squared = %d, n cubed = %d\n", n, n2, n3)
return 0;
)语法错误(Syntax Errors)
语法错误 = 不遵守 C 的规则,好比英语里的语法错误(“Bugs frustrate be can”——单词都对,顺序和用词不对)。nogood.c 的语法错误有四处:
- 函数体用了圆括号 ( ) 而不是花括号 { }——合法符号用错了地方;
- 声明写成了
int n, int n2, int n3;——正确写法是int n, n2, n3;或分三行各写一个; - 注释没有闭合,缺
*/; - printf 语句漏了分号。
怎么发现?一是编译前通读源代码;二是看编译器报错——抓语法错误正是编译器的本职。不过编译器也会被搞糊涂:一个真正的错误可能引发一串”误报”。所以正确姿势是:每次只改最先报告的一两个错误,然后重新编译——后面的报错可能随前面修正而消失。
编译器还有个常见戏法:报错晚一行。比如漏分号往往到下一行才被发现,所以它抱怨某行缺分号而那行明明有分号时,检查上一行。
语义错误(Semantic Errors)
语义错误 = 意思上的错误:C 规则全遵守了,但干的不是你想干的事。好比 “Furry inflation thinks greenly”——语法通顺,意思不通。把 nogood.c 的语法错误全修好,得到清单 2.5:
程序清单 2.5 stillbad.c 程序
/* stillbad.c -- a program with its syntax errors fixed */
#include <stdio.h>
int main(void)
{
int n, n2, n3;
/* this program has a semantic error */
n = 5;
n2 = n * n;
n3 = n2 * n2;
printf("n = %d, n squared = %d, n cubed = %d\n", n, n2, n3);
return 0;
}输出:
n = 5, n squared = 25, n cubed = 625想算 n 的立方,结果 625 = 5⁴,错了!注意:编译器查不出语义错误——它不违反任何 C 规则,编译器也没法读心。找语义错误得靠自己。
跟踪程序状态(Program State)
一个系统化方法:假装自己是计算机,逐步执行并记录每个变量的值——
- 声明 n、n2、n3:画三个盒子,贴上变量名;
n = 5:往 n 盒里写 5;n2 = n * n:查 n 盒得 5,5×5=25,写进 n2 盒;n3 = n2 * n2:查 n2 盒得 25,25×25=625,写进 n3 盒——啊哈!是拿 n2 自乘了,而不是乘 n!

程序状态(program state)就是程序执行到某一点时所有变量的值的集合——当前计算的一张快照。跟踪状态的手段:
- 手工模拟:像上面那样逐行推演( faithful 一点,照代码执行而不是照”你以为你写的”执行);
- 插入 printf():在关键位置临时打印变量值,看它怎么变化,调完再删;
- 调试器(debugger):让程序一步步跑、随时查看变量值的程序,好一点的还能显示当前执行到哪一行。编译器自带的调试器值得花时间学。
新手坑:把"编译通过"当成"程序正确"
编译器只能抓语法错误(规则违反),抓不住语义错误(意思错误)。
n3 = n2 * n2;完全合法地算出了错误答案。永远要核对输出的数值是否符合预期,别看见程序跑完没报错就提交。
关键字与保留标识符(Keywords and Reserved Identifiers)
关键字(keyword)是 C 语言的词汇表,不能用作标识符(变量名、函数名)。用于指定类型(如 int),或控制语句执行顺序(如 if)。主要关键字列表:
| auto | enum | restrict | unsigned |
| break | extern | return | void |
| case | float | short | volatile |
| char | for | signed | while |
| const | goto | sizeof | _Bool |
| continue | if | static | _Complex |
| default | inline | struct | _Imaginary |
| do | int | switch | |
| double | long | typedef | |
| else | register | union |
拿关键字当变量名,编译器直接报语法错。此外还有保留标识符:下划线开头的标识符和标准库函数名(如 printf)。它们不报语法错(因为是合法名字),但语言已占用或保留占用权,挪用会出问题。
本章关键概念(Key Concepts)
编译器不讲情面:跟朋友聊天说错几个词、句子没说完,朋友照样懂你;对编译器而言,“几乎正确”就等于错误。程序是你对”想让计算机怎么表现”的描述,编译器把你这 1KB 的描述扩写成 60KB 的机器语言——但它没有智能,你必须用它能处理的格式(C 标准的正式规则)来表达。
记忆口诀:先声明,再使用;语法管形式,语义管意思
两条主线记牢本章:① 变量必须先声明后使用,名字要见名知义;② 错误分两类——语法错误编译器帮你抓(还可能误报连串、报错晚一行),语义错误编译器不管,要靠核对结果、手工跟踪程序状态、插 printf 或用调试器自己抓。
自测题(附答案)
1. C 程序的基本模块叫什么? 答:函数(function)。每个 C 程序由一个或多个函数组成,且必须包含一个 main() 函数作为程序入口。
2. 什么是语法错误?各举一个英语和 C 的例子。 答:语法错误是不遵守语言规则——合法的符号用错了地方。英语例:“Bugs frustrate be can”(词都对,语序不对);C 例:
printf("Hi.")后面漏了分号,或用圆括号 ( ) 代替花括号 { } 包住函数体。3. 什么是语义错误?各举一个英语和 C 的例子。 答:语义错误是意思错误——语法完全正确但表达的不是你想表达的意思。英语例:“Furry inflation thinks greenly”(句子结构没问题但不知所云);C 例:想算 n 的立方却写成
n3 = n2 * n2;(算出的是四次方)。4. Indiana Sloth 的程序错在哪里?
include studio.h int main{void} /* this prints the number of weeks in a year */ (int s s := 56; print(There are s weeks in a year); return 0;答:至少有这些错:第 1 行应为
#include <stdio.h>(# 号、尖括号、正确的文件名都不能少);int main{void}应为int main(void)(圆括号不是花括号);函数体用圆括号 ( ) 应改为花括号 { },且缺少闭合的 };s := 56;中的:=不是 C 的赋值符,应为s = 56;;printf,且参数应为带引号的字符串加变量:printf("There are %d weeks in a year.\n", s);,句尾还漏了分号。5. 下面的程序,第 7、8、9 行执行完后程序状态分别是什么?
int a, b; a = 5; b = 2; /* line 7 */ b = a; /* line 8 */ a = b; /* line 9 */ printf("%d %d\n", b, a);答:第 7 行后:a = 5, b = 2;第 8 行后:b = a 把 5 赋给 b,状态为 a = 5, b = 5;第 9 行后:a = b 把 5 赋给 a(注意 b 已经是 5),状态为 a = 5, b = 5。所以打印
5 5。这个例子说明连续赋值不会自动”交换”两个变量的值——交换需要第三个临时变量。