这一篇在干嘛?

上一章知道了”程序怎么变成可执行文件”,这一章正式打开第一个 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("...") 没有换行?下面分两遍讲解。

图:C 程序的解剖图——#include 指令、main 函数头、注释、花括号包住函数体、声明、语句、return、右花括号

第一遍:快速浏览(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 文件的全部内容原样粘贴到这一行的位置——本质是个复制粘贴操作。#includeC 预处理器指令(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; 是声明语句,一次宣布两件事:

  1. 有一个名叫 num变量(variable)
  2. num 的类型是 int(整数,没有小数点的数)——编译器据此为它安排合适的内存空间。
  • intC 关键字(keyword),代表一种基本数据类型,不能挪作他用(不能拿它当变量名);
  • num标识符(identifier)——你给变量、函数起的名字;
  • 行尾的分号是语句的一部分,标识一条 C 语句的结束(不像 Pascal 里只是分隔符)。

**C 中所有变量必须先声明再使用。**传统规则要求声明放在块的开头;C99 起允许声明出现在块的任何位置(只要在首次使用之前),但本书为兼容旧编译器仍用传统写法。

起名的规则:可用字符是小写字母、大写字母、数字、下划线(_)——第一个字符必须是字母或下划线。C99 允许名字最长 63 个字符(外部标识符 31 个)。名字区分大小写starsStarsSTARS 是三个不同的名字。

合法名字非法名字
wiggles$Z]**
cat22cat
Hot_TubHot-Tub
taxRatetax rate

新手坑:别用下划线开头,别抄操作系统 reserved 的名字

操作系统和 C 库常用一两个下划线开头的标识符(如 _kcab),这些是保留标识符(reserved identifiers)——用了虽不报语法错,但可能引发命名冲突。同理,标准库函数名(如 printf)也别拿去当变量名。

为什么必须声明变量?四个理由

  1. 把变量集中在 one place 列出,读者一眼看清程序要什么(配合有意义的名字,如 sheep_count 而不是 x3);
  2. 想清楚要声明哪些变量,逼你先规划再动手
  3. 防范”拼错变量名”这种极隐蔽的 bug:在没有声明的语言里,把 RADIUS1 误打成 RADIUSl(数字 1 换成字母 l),程序会悄悄新建一个变量,结果错误却极难查出。C 里编译器会直接对未声明的名字报错;
  4. 不声明就编译不过——C 是强制的。

赋值(Assignment)

num = 1; 把值 1 存进 num 的内存位置。注意方向:从右往左赋——右边是值,左边是变量。之后随时可以给 num 赋新值,这就是”变量”名字的由来。

图:赋值语句 num = 1; 的结构——数据流向是从右到左,分号结束语句

printf() 函数

printf("I am a simple");
printf("computer.\n");
printf("My favorite number is %d because it is first.\n", num);

括号里引号包住的内容是传给 printf() 的参数(argument),printf() 把它打印到屏幕上。

图: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!

三个新知识点:

  1. 文档注释:程序第一行用 // 注释标明文件名和用途,浏览一堆文件时特别有用;
  2. 多重声明int feet, fathoms; 逗号分隔一次声明两个变量,等价于分开写两行 int feet;int fathoms;
  3. 打印多个值:第一个 printf 里有两个 %d,引号后的变量列表依次填入——第一个 %dfeet(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() 在程序中出现了三次,各司其职:

  1. 原型(prototype)void butler(void); 告诉编译器”我要用这个函数”。第一个 void 表示无返回值,第二个 void(括号里的)表示无参数。有了原型,编译器在 main() 里用到 butler() 时就能检查用法对不对;
  2. 函数调用:main() 里的 butler();,执行到这行就跳去执行 butler,干完回来继续下一句;
  3. 函数定义:文件末尾的完整实现,格式与 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 的语法错误有四处:

  1. 函数体用了圆括号 ( ) 而不是花括号 { }——合法符号用错了地方;
  2. 声明写成了 int n, int n2, int n3;——正确写法是 int n, n2, n3; 或分三行各写一个;
  3. 注释没有闭合,缺 */
  4. 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)

一个系统化方法:假装自己是计算机,逐步执行并记录每个变量的值——

  1. 声明 n、n2、n3:画三个盒子,贴上变量名;
  2. n = 5:往 n 盒里写 5;
  3. n2 = n * n:查 n 盒得 5,5×5=25,写进 n2 盒;
  4. n3 = n2 * n2:查 n2 盒得 25,25×25=625,写进 n3 盒——啊哈!是拿 n2 自乘了,而不是乘 n!

图:跟踪程序——用三个标着 n、n2、n3 的盒子模拟变量,逐步填写 5、25、625 的过程

程序状态(program state)就是程序执行到某一点时所有变量的值的集合——当前计算的一张快照。跟踪状态的手段:

  • 手工模拟:像上面那样逐行推演( faithful 一点,照代码执行而不是照”你以为你写的”执行);
  • 插入 printf():在关键位置临时打印变量值,看它怎么变化,调完再删;
  • 调试器(debugger):让程序一步步跑、随时查看变量值的程序,好一点的还能显示当前执行到哪一行。编译器自带的调试器值得花时间学。

新手坑:把"编译通过"当成"程序正确"

编译器只能抓语法错误(规则违反),抓不住语义错误(意思错误)。n3 = n2 * n2; 完全合法地算出了错误答案。永远要核对输出的数值是否符合预期,别看见程序跑完没报错就提交。

关键字与保留标识符(Keywords and Reserved Identifiers)

关键字(keyword)是 C 语言的词汇表,不能用作标识符(变量名、函数名)。用于指定类型(如 int),或控制语句执行顺序(如 if)。主要关键字列表:

autoenumrestrictunsigned
breakexternreturnvoid
casefloatshortvolatile
charforsignedwhile
constgotosizeof_Bool
continueifstatic_Complex
defaultinlinestruct_Imaginary
dointswitch
doublelongtypedef
elseregisterunion

拿关键字当变量名,编译器直接报语法错。此外还有保留标识符:下划线开头的标识符和标准库函数名(如 printf)。它们不报语法错(因为是合法名字),但语言已占用或保留占用权,挪用会出问题。

本章关键概念(Key Concepts)

编译器不讲情面:跟朋友聊天说错几个词、句子没说完,朋友照样懂你;对编译器而言,“几乎正确”就等于错误。程序是你对”想让计算机怎么表现”的描述,编译器把你这 1KB 的描述扩写成 60KB 的机器语言——但它没有智能,你必须用它能处理的格式(C 标准的正式规则)来表达。

记忆口诀:先声明,再使用;语法管形式,语义管意思

两条主线记牢本章:① 变量必须先声明后使用,名字要见名知义;② 错误分两类——语法错误编译器帮你抓(还可能误报连串、报错晚一行),语义错误编译器不管,要靠核对结果、手工跟踪程序状态、插 printf 或用调试器自己抓。