柏动软件

这是译者2020年8月所译《现代C语言》正文首章,原发在微薄上,当时曾想正式出版,但到目前也没有 找到出版合作方,所以到现在也没有出版成(如有机会,还是想出版的)。 本来正式版译文应该还有一些,但匆忙间没有找到,所以暂时就只发这一章。 对于一部完整的技术书籍来说,只有个别章节是不管用的,但还是选择发在这里,一来是充实网站内容, 再者,也大概能够作为一种我们可以做什么的现实例证。作为软件公司,当然想要更多的业务机会, 但如何证明自己的技术能力也是一个重要问题,这也算是一个努力吧。

请勿转载

层级0. 初遇

本层级的吉祥物是喜鹊——地球上人类以外最聪明的动物。它们不仅能够举行复杂的社会仪式,还能够使用工具。


本书的第一个层级有可能是你第一次接触C编程语言。它提供了有关C程序的概略性知识,它们的目的,它们的结构,以及怎样去使用它们等。但这并不意味着本层级会提供完整的视图,这不大可能做到,甚至都没有这样尝试过。相反,本层级的目的是向读者提供所有这些相关内容的一般性想法,揭示问题,引出想法和概念;然后再到更高层级对这些内容做更详细的解释。



1. 准备开始

本章包括

  • 介绍命令式编程

  • 编译及运行代码

本章会给大家介绍一个简单的程序,之所以选择这个程序,是因为它用到了C语言的很多构件。如果你之前有过编程经验,可能会觉得本章的某些内容是无需赘言的废话;如果以前从未编写过程序,则可能会被一连串新看到的术语和概念弄得手足无措。

但不论是哪种情况,都请耐心一点。对于那些有编程经验的读者来说,甚至是有C语言编程经验的读者,也很有可能能在这里看到某些之前未曾注意过的微妙细节,或者发现之前对这个语言的某些想法是错误的。而对那些第一次接触编程的读者,相信在大概10页以后,你的理解就会上升很大一个台阶,并对编程到底代表着什么有一个更清晰的认识。

在编程届有一句普适而重要的格言,尤其适用于本书,那就是从道格拉斯.AdamsAdams.1986】的《星系搭车指南》中摘录的一句总结:

要点B:不要恐慌。

因为这完全没有必要。书中有很多交叉引用、链接、和辅助信息,最后还有一个索引。如果遇到问题可以先看看它们,或者先停一下歇一会。

C语言编程就是让计算机完成某些指定的任务。C程序按照给定的指令执行,就像人类语言在很多祁使语句中表达的那样;因此用术语“命令式编程”来指称这种组织计算机程序的特殊方式。如果想看看我们正在说的是什么,可以先思考一下后面的代码列表1.1给出的示例。

1.1 命令式编程

从示例可以看出,这种语言使用了像mainincludefor这样的怪异单词(它们一般会以特殊的颜色显示和排列),并夹杂着许多奇怪的字符、数字,和(看上去像普通)文本(如“Doing some work”)之类的内容。设计它是为了用它来连接我们人类(程序员)和机器(计算机),以告诉机器该做什么:即给它下达“指令”。

要点 0.1.1.1 C是一种命令式编程语言。

在书中,我们不仅会看到C编程语言,还会看到一些词汇(用英语方言说就是“C语言的行话”),它们是一种可以帮助我们讨论C语言的语言。我们不太可能在所有术语刚出现的时候就立即对它们进行详细的解释,但会在必要的时候解释它们的每一个;而且对它们全部做了索引,以方便读者在需要时查找并跳转C到更具解释性的文本处,当然这需要自己动手1

通过这第一个例子可以看出,C程序有不同的组件,构成了相互交织的不同层面的内容。下面就尝试由内而外地解析这个程序。运行它可以看到的结果是在命令终端输出的5行文本,在作者的电脑上是这样:

列表1.1: C语言程序的第一个示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* This may look like nonsense , but really is -*- mode : C -*- */
# include <stdlib.h>
# include <stdio.h> 

/* The main thing that this program does . */
int main ( void ) {
 // Declarations
 double A [5] = {
  [0] = 9.0 ,
  [1] = 2.9 ,
  [4] = 3. E +25 ,
  [3] = .00007 ,
 };

 // Doing some work
 for ( size_t i = 0; i < 5; ++ i) {
  printf (" element ␣% zu ␣ is ␣%g ,␣\ tits ␣ square ␣ is ␣%g\n" ,
     i,
     A[i],
     A[i ]* A[i ]) ;
 }

 return EXIT_SUCCESS;
}

Terminal
0 >./getting-started
1 element 0 is 9, its square is 81
2 element 1 is 2.9, its square is 8.41
3 element 2 is 0, its square is 0
4 element 3 is 7e-05, its square is 4.9e-09
5 element 4 is 3e+25, its square is 9e+50

很容易就能在程序中找出与输出(C的行话叫打印C)文本相关的地方,就是第17行处用双引号括起来的部分。在C语言中,将类似于17行到20行这样的动作称为语句C【译注——C语言中单条语句都是以分号结束,多个语句可以共同组成复合语句(块)在其他语言中可能会使用“指令”这个术语,以能更好地反映它的目的(所以C中似乎有些误用的成分)。此处这个特殊的语句是对名为printf函数C调用C:

这个printf函数接收了四个参数C,它们被括在一对小括号C内(…):

  • 双引号括起来的看上去有些古怪的文本称为字符串字面量C, 它在这里的作用是指定输出的格式C。此处这个格式串内有三个标记格式指示符C),它们都以字符%开头,用来指定输出中3数字的位置。这个格式中还包含了一些特殊的转义字符C,以反斜杠开头:即\t\n

  • 第一个逗号后面的单词i,表示的是将会在打印字符串的第一个格式指示符%zu处出现的数字;

  • 随后的逗号用于分隔下一个参数A[i],代表的是第二个格式指示符、即第一个%g处将出现的内容;

  • 最后一个参数A[i]*A[i],也是由逗号分隔,对应的是最后一个%g

后面会介绍所有这些参数的含义,现在则只需要弄清楚这个程序的主要目的(是打印一些文本行到终端),以及用于完成这一目的的“指令”(是printf函数)即可。其余内容只是一些C译注——有益的辅助性语句等,用于指定需要打印的数字,以及它们的个数等。

1.2 编译和运行

如上所述,程序文本是用来表达人们想要计算机去做的事情的,是程序员编写并保存在硬盘上某个文件中的另一种文本片段,但是这样的程序文本却不能被计算机理解。所以用一种称为“编译器”的特殊程序,将C程序文本翻译成计算机可以理解的形式:即二进制代码C可执行程序C。至于翻译程序是什么样子以及这个翻译过程是怎样完成的,对于现在这个阶段来说则太过复杂2,甚至于这一整本书也不可能把它解释清楚,那是另外一整本书才能完成的主题。不过在这一阶段,也不需要理解的那样深,只要知道这个工具为我们做了所有工作即可。

要点0.1.2.1 C是一种编译型语言。

至于编译器的名字以及它们的命令行参数很大程度上依赖于其所运行的平台,之所以会这样原因很简单:目标二进制代码是依赖于平台的C,也就是说,它们的格式和细节依赖于运行它们的计算机。个人计算机的要求与电话不同,电冰箱所说的“语言”与机顶盒也不相同。事实上,这也正是C语言存在的原因之一:即,它为所有这些不同的机器特定的语言(通常称为汇编C)提供了一个统一的抽象层。

要点0.1.2.2 正确的C程序是可以在不同的平台间迁移的。

本书会花费很大的气力来展示怎样写出“正确的”C程序以确保它的可迁移性。但遗憾的是有一些平台虽然宣称使用的是“C”,却并不符合最新的C语言标准;还有一些平台虽然符合标准却又同时接受错误的程序或支持一些扩展的C标准,因而阻碍了广泛移植的可能性。所以,在某一单个平台上运行和测试程序并不总是能够保证它的可移植性。

保证程序的这种可移植性是编译器的工作,亦即,一旦这样的程序在某个合适的平台上通过了编译,就应该能够正确的运行在相应的个人计算机上、运行在相应的电话上、甚至是机顶盒和电冰箱上。

如果读者使用的是符合POSIX标准的系统(比如LinuxmacOS),则有很大的机会能在系统内看到一个名为c99的程序,它实际上就是一个C编译器。那么就可以尝试使用下面的命令编译上面的示例程序:

终端

0

> c99 -Wall -o getting-started getting-started.c -lm



运行上面命令时应该是没有任何警示信息,并在当前文件夹内生成一个名为getting-started的可执行文件【练习3】。上面的命令行中:

  • c99是编译器程序;

  • -Wall告诉编译器对发现的任何非正常情况给出警告;

  • -o getting-started 告诉编译器将编译输出C保存到一个名为getting-started的文件

  • getting-started.c源代码文件C名,在这个文件内包含了C的程序代码,结尾的扩展名.c用来指示C编程语言

  • -lm告诉编译器在必要的时候添加一些标准数学函数;稍后会用到它们。

现在就可以执行C这个新编译得到的可执行程序C了:


终端

0

> ./getting-started

读者看到的输出跟书中展示的应该是一样的。这也正是可迁移 的含义:即不论在什么地方运行程序,它们的行为C都应该是一样的。

如果读者不够幸运,上面的编译命令不能正常运行,则必须查找一下系统文档以找出在所用系统中编译器C的名字,甚至于如果系统中尚未安装的话,还必须自己安装一个。不同的系统中编译器的名字可能是不同的4,下面几个是可能的变体:

0

> clang -Wall -lm -o getting-started gettting-started.c

1

> gcc -std=c99 -Wall -lm -o getting-started getting-started.c

2

> icc -std=c99 -Wall -lm -o getting-started getting-started.c



即使你的计算机中存在这些程序,也未必能够没有任何警示信息地通过编译【练习5】

列表1.1中的程序,展示了一种理想情况:程序可以在所有平台上正常运行并产生相同的结果。不幸的是,在自己编程时,可能经常会遇到只能部分工作,甚至产生错误或不稳定结果的情况。为了演示这一点,特给出一个与前面程序非常相似的程序1.2

如果读者亲自编译这个程序,很有可能会看到类似下面这样的诊断警示C信息【译注——编译提示信息可能会因编译器、编译器的版本,以及操作系统和操作系统版本,还有语言设置等多方面因素的影响而有差别,初学者勿为怪】:



程序列表 1.2. 一个有瑕疵的C程序示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* This may look like nonsense , but really is -*- mode : C -*- */

/* The main thing that this program does . */
void main () {
	// Declarations
	int i;
	double A [5] = {
		9.0 ,
		2.9 ,
		3. E +25 ,
		.00007 ,
	};

	// Doing some work
	for (i = 0; i < 5; ++ i) {
		printf (" element ␣%d␣ is ␣%g ,␣\ tits ␣ square ␣ is ␣%g\n" ,
			i,
			A[i],
			A[i ]* A[i ]) ;
	}
	
	return 0;
}


Terminal
0
1
2
3
4
5
> c99 -Wall -o bad bad.c
bad.c:4:6: warning: return type of 'main' is not 'int' [-Wmain]
bad.c: In function 'main':
bad.c:16:6: warning: implicit declaration of function 'printf' [-Wimplicit-function...
bad.c:16:6: warning: incompatible implicit declaration of built-in function 'printf' ...
bad.c:22:3: warning: 'return' with a value, in function returning void [enabled by de...

这里有很多很长的“警告”(warning)行,有些行甚至长到终端屏幕显示不下。但是最后编译器还是生成了一个可执行程序,然而当我们运行这个程序时,产生的输出却不一样。这些警告信息是我们必须加以重视的信号,并且需要在这些细节上花些精力。

clang甚至比gcc更加挑剔,给出的诊断警示甚至更长:



Terminal
0 
1 
2 
3
4
5
6
7
8
9
10
11
12
13
> clang -Wall -o getting-started-badly bad.c
bad.c:4:1: warning: return type of 'main' is not 'int' [-Wmain-return-type]
void main() {
bad.c:16:6: warning: implicitly declaring library function 'printf' with type
'int (const char *, ...)'
printf("element %d is %g, \tits square is %g\n", /*@\label{printf-start-badly}*/
^
bad.c:16:6: note: please include the header  or explicitly provide a declaration
for 'printf'
bad.c:22:3: error: void function 'main' should not return a value [-Wreturn-type]
return 0;
^
~
2 warnings and 1 error generated.

但这是好事!它的诊断输出C给出了更多信息,尤其是给出了两个线索main函数应该使用另一种返回类型;还有,它期望我们提供类似于代码列表1.1中第3行那样的代码来指定printf函数的出处。需要留心的是,clang不像gcc,它没有生成可执行程序。它将行22处的问题看作严重的错误,并把这看做是一种功能(译注——遇到这种错误时不生成可执行程序)。

读者可以强制编译器拒绝产生该种诊断信息。具体需依所用平台而定,比如gcc,对应的命令行选项应该是-Werror

现在我们已经看到了代码列表1.11.2中的两点不同,且正是这两处改动,把一个好的、符合标准的、可迁移的程序变成了一个不合格的程序。我们也看到编译器帮了我们大忙,它将程序中导致问题的行指出,稍具经验的读者就能够理解它所给出的提示【练习6】【练习7】

要点0.1.2.3 C程序在编译时应该清爽而无警告信息。



小结

  • C语言被设计用来给计算机发送指令,因此它是程序员和计算机间的媒介;

  • C程序必须编译成可执行的形式。编译器就是用来将我们所理解的C语言翻译成特定平台所要求的形式;

  • C通过一个抽象层提供了迁移能力。使得一个C程序可以在许多不同的计算机架构上使用;

  • C编译器对我们有莫大帮助。如果它对程序给出了某种警告,就听从它。





1来自C语言行话的这些特殊术语都用C加以标注,就像这里显示的那样。

2事实上,这个翻译过程本身也是经过几个步骤完成的,先是文本替换,然后做合适的编译,最后进行链接。但是,所有捆绑在一起的这些工具,在传统上都是称为编译器,而不叫翻译器,虽然后者或许更贴切些。

【练习3】 在自己的终端上运行这个编译命令。

4这在使用微软操作系统时尤其必要。因为微软自带的编译器甚至还未完全支持C99,因此本书讨论的很多功能不能正常工作。作为一种替代,读者或许不得不看一下Chris Wellon的博客“Four Ways to Compile C for Windows” (Windows系统中编译C程序的4种方法)()

【练习5】从现在起可以写一个文本报告,来总结阅读本书时所做的测试。注意记下你所使用的命令。

【练习6】一步一步地更正列表1.2中的程序。首先从第一个诊断警示行开始,修改它所提及的代码,再重新编译。以此往复,直到编译时不再有警告信息为止。

【练习7】上面的两个程序间还有第3处不同点我们并未提及,请将它找出。

上海柏动软件研究开发中心沪ICP备2021007358号 2025年