第 1 级:相识
本级吉祥物
本级的吉祥物是渡鸦。这种鸦科鸟类非常合群,以解决问题的能力著称。渡鸦会组成团队,人们甚至观察到成年渡鸦也会玩耍。
本级将带你与 C 程序设计语言相识,也就是提供足够的知识,使你能够编写和使用良好的 C 程序。这里的“良好”建立在对这门语言的现代理解之上:避开 C 早期方言的大多数陷阱,并使用一些过去尚不存在、如今却能移植到绝大多数现代计算机体系结构上的构造——从手机到大型机都是如此。
学完这些内容后,你应当能够编写满足日常需要的短小代码。它们不会极其复杂,但会实用且可移植。
系好安全带
从许多方面看,C 是一门相当宽容的语言。程序员若执意朝自己的脚或身体其他部位开枪,C 并不会设法阻止。因此,我们暂时要引入一些限制。本级会尽量避免把枪交到你手上,先把枪柜钥匙放到够不着的地方,再用巨大而醒目的感叹号标出它的位置。
C 中最危险的构造是所谓的强制转换,所以本级暂且跳过它。不过,还有许多陷阱没有这么容易避开。我们会采用一些你可能不熟悉的方式处理它们;如果你在上个千年学习了 C 的基础知识,或者接触 C 时所用的平台多年没有升级到当代 ISO C,尤其会有这种感觉。
有经验的 C 程序员
以下内容可能需要一段适应时间,甚至会引起过敏反应。若阅读这里的代码时身上突然起了荨麻疹,请深呼吸并设法放松,但不要跳过这些页面。
尚无经验的 C 程序员
下面许多讨论可能暂时超出你的理解范围,例如,其中可能使用你从未听说过的术语。若是如此,可以把它当作一段插叙,先跳到第 3 章开头,感觉更从容时再回来阅读;但务必在学完本级之前读完。
适应本级方法的过程,部分可能与材料的侧重点和讲解顺序有关:
- 我们主要关注整数类型的
unsigned版本。 - 我们分步介绍指针:先把它伪装成函数的形参(第 6.1.4 节),再讨论它的状态,也就是是否有效(第 6.2 节),最后在下一级第 11 章发挥它的全部潜力。
- 只要可能,我们就会优先使用数组。
下面的风格约定也可能令你意外。下一级会用完整的第 9 章讨论这些问题,所以暂且耐心接受它们。
1. 修饰符、限定符与属性向左结合
我们希望在视觉上把标识符与它的类型分开,因此通常写成:
char* name;其中 char* 是类型,name 是标识符。限定符和属性同样遵循向左结合规则:
char const* const path_name [[deprecated]];第一个 const 限定它左侧的 char,* 将其构成指针,第二个 const 再限定其左侧的整体。属性 [[deprecated]] 则明确附着于标识符 path_name。
2. 不使用连续声明
连续声明会掩盖类型声明符的结合关系。例如:
unsigned const* const a, b;这里,b 的类型是 unsigned const:第一个 const 属于类型,第二个 const 只属于 a 的声明。这类规则极易混淆,而你还有更重要的内容要学。
3. 指针形参采用数组记法
只要形参要求指针不能为空,我们就采用数组记法:
/* 这些声明强调实参不能为空。 */
size_t strlen(char const string[static 1]);
int main(int argc, char* argv[argc + 1]);
/* 相同函数的兼容声明。 */
size_t strlen(char const* string);
int main(int argc, char** argv);2
3
4
5
6
7
第一个例子强调,strlen 必须接收有效的非空指针,并至少访问 string 的一个元素。第二个例子概括了这样一项事实:main 接收一个由 argc + 1 个 char 指针组成的数组,其中包括程序名称、argc - 1 个程序实参,以及一个终止数组的空指针。
第二组声明只是为编译器已经知道的特性补充了等价声明。因为它们是声明而非定义,所以允许这样重复声明;不过在本例中,它没有增加新信息。
4. 函数指针形参采用函数记法
同理,只要确定函数指针不能为空,我们便采用函数记法:
/* 此声明强调 handler 实参不能为空。 */
int atexit(void handler(void));
/* 相同函数的兼容声明。 */
int atexit(void (*handler)(void));2
3
4
5
atexit 的第一项声明强调,从语义上说,它接收一个名为 handler 的函数作为实参,并且不允许空函数指针。从技术上说,函数形参 handler 会被“改写”为函数指针,类似于数组形参会被改写为对象指针;但在描述功能时,这只是次要细节。
再次注意,前面的代码本身完全有效,第二项声明只为 atexit 增加了等价声明。
5. 尽可能靠近首次使用处定义对象
缺少初始化,尤其是指针未初始化,是 C 初学者最容易遇到的主要陷阱之一。因此,只要可能,就应把对象的声明与对它的首次赋值结合起来。C 为此提供的工具是定义:带有初始化的声明。它为值赋予名称,并在首次使用该名称的位置引入它。
这一做法对 for 循环尤其方便。不同循环中的迭代对象在语义上并不是同一对象,所以应在 for 内部声明,使其始终局限在该循环的主要块中。
6. 代码块采用前缀记法
要顺利阅读代码块,必须容易看清两件事:它的用途和范围。因此:
- 所有
{都放在引入该块的语句或声明所在行的末尾。 - 块内代码缩进一级。
- 终止块的
}另起一行,与引入该块的语句处在同一级。 - 如果块语句在
}后还有延续部分,则把延续部分写在同一行。
int main(int argc, char* argv[argc + 1]) {
puts("Hello world!");
if (argc > 1) {
while (true) {
puts("some programs never stop");
}
} else {
do {
puts("but this one does");
} while (false);
}
return EXIT_SUCCESS;
}2
3
4
5
6
7
8
9
10
11
12
13
7. 数值使用数字分隔符
人眼不善于感知多位数的数量级。因此,在普通文本中,大型十进制数采用逗号分隔千位;小数点后的数字也沿用这种写法,例如 10,035.677,789。年份是一个显著例外,不使用千位分隔符。
C23 也为数值字面量提供了数字分隔符,也就是直接出现在两个连续数字之间的 ',例如 10'035.677'789。本书会在所有数值字面量中使用它:十进制数字每三位一组,十六进制数字每四位一组,二进制数字每八位一组。