柏动软件

印象中,这一章的正式译文以前也是做了的,却怎么也没找着。好在这一章的内容不是太长,又是第一部分的尾章,所以这几天赶着整理出来,好使文章的列表不会太单薄。 但整理完后,就算是自己再读,也有种怪怪的感觉,怪在哪里却又说不出。兼且文章的格式从文档转换成网页的时候,也有一些不太一致的地方,整理格式也是个费劲的事,且又不是正式印刷,就有点将就的感觉了。

请勿转载


2. C程序的基本结构

本章包含

  • C的语法

  • 声明标识符

  • 定义对象

  • 语句

与上一章中的示例程序相比,现实中的程序可能会复杂的多、包含更多的构件,但它们的结构都是非常相似的。列表1.1中的程序已经包含了C语言中的大部分结构元素。

C语言中有两大方面的内容需要考虑:它们是语法(怎样编写程序以让编译器理解它),和语义(告诉程序我们想让它做什么)。下面会分别介绍语句结构(语法)和3个不同的语义内容:即声明(是什么)、定义对象(在哪里)、还有语句(指定要做的事情)。

2.1 语法

通观C程序的整个结构可以看到,它是由某种语法组织起来的不同类型的文本元素组成。这些文本元素包括:

特殊字:在列表1.1中,使用了下面这些特殊单词【字】8#include, int, void, double, for ,还有return。在本书给出的程序中,会把这些字用粗体打印。C语言用这些特殊的单词表达某种概念和功能,并且不能改变。

标点符号C语言使用好几种类型的标点符号来组织程序文本。

  • 4种不同的括号:{…}(…)[…], 以及<...>(译注——原文将/*... */也列在标点符号列表,译者以为可能是笔误,因为注释在下面又被单独列出了)。括号可以对程序的不同部分进行分组,并且始终应该是成对出现。不过<...>C程序中很少出现,而且也只能像示例中那样使用即只能用在一个逻辑行中。其他4种则不限于单行文本,它们包含的内容可以跨越多行,就像前面的printf函数中那样。

  • 2种不同的分隔符或结束符:逗号(,)和分号(;)。在调用printf函数时,使用了逗号分隔其中的4个参数,从行12可以看出,逗号也能够出现在元素列表的最后一个元素的后面。

getting-started.c
12
 [3] = .00007,

C语言新手来说,其中一个难点是同一个标点符号可以用来表示不同的概念。例如列表1.1中用到的{}[ ],每一个都分别有3种不同的用途【练习9】

要点 0.2.1.1 同一个标点符号可以表达不同的含义。

注释C:结构 /* … */ 用来告诉编译器,包含在其内部的内容只是注释(解释),参看上例行5处:

getting-started.c
5   /* The main thing that this program does. */

注释会被编译器忽略掉,所以它们是对代码进行解释和写文档的理想地方。这些即景附带的文档能够(而且应该)极大的提升代码的可读性和可理解性。还有另一种C++风格的注释形式,如行15处给出的那样,它们都以双斜杠// 开头。C++风格的注释都是从//处开始,直到行末结束。

字面量C:在上面的程序中包含了好几个表示固定值的项目:013459.02.93.E+25.00007,还有element␣%zu␣is␣%g,␣\tits␣square␣is%g\n”,它们都被称为字面量C

标识符C这些都是程序员(或C语言标准)给程序中某实体指定的“名称”。上例中使用了Aimainprintfsize_t, 以及 EXIT_SUCCESS标识符。标识符在程序中可以充当不同的角色,在其他内容中用它们来指示:

  • 数据对象(如Ai)。这类标识符也被称为变量C

  • 类型C别名,比如size_t,此处用来指定新对象i的“种类【类型】”。观察一下其名称结尾的_t,在C语言标准中使用这种命名风格来提醒阅读者,该标识符表示的是一种类型。

  • 函数,比如mainprintf

  • 常量,比如EXIT_SUCCESS

函数C:上面的mainprintf 这两个标识符表示的是函数。其中printf用来打印一些输出。而函数main则通过下面方式定义:在声明C int main(void)的后面紧随一个用大括号{ … }括起来的程序C,其中大括号括起来的部分用于指定该函数所要做的事情。在列表1.1中,main函数的定义从行6处开始到行24处结束。在C程序中,main函数有一个特殊的角色,即它总是作为程序开始执行的启动点。

操作符CC语言中有多种操作符,而我们的程序只用到了其中的几个:

  • = 用于初始化C赋值C

  • < 用于比较操作,

  • ++ 用于增加一个变量的值(每次只将其值增加1),以及

  • * 用于将两个值相乘。

与自然语言一样,需要对C程序中的各种词汇元素和语法构件的真正含义进行仔细甄别和区分。与自然语言不同的是,它们的含义必须严格指定,且不能有任何模糊的空间。下面三个子章节将深入介绍三种不同的语义成分:它们分别是声明,定义,和语句。

2.2. 声明

要在程序中使用某个特定的标识符,必须先给编译器一个声明C用于指示该标识符表示的是什么。这也是标识符与关键字不同的地方:关键字是由语言预定义,且不能进行重新定义或声明。

要点 0.2.2.1 程序中的所有标识符都必须进行声明。

前面示例中对用到的3个标识符都进行了有效声明:mainA,以及imain的声明前面已经提到过 后面会介绍其他标识符的来源(printfsize_t,以及EXIT_SUCCESS)。若把这三个声明都孤立地看作“仅仅是声明”,则会像下面这样:

int main(void);

double A[5];

size_t i;

3个声明都遵循了同一个模式,即每一个都有一个标识符(分别是mainA,和i),以及与该标识符相关联的某种属性的详细说明:

  • i类型Csize_t;

  • main后面还另外跟着一对小括号(...),因此它声明了一个类型为int的函数;

  • A后面跟着一对中括号[…], 它声明的则是一个数组C。数组是相同类型的若干项目的集合。此处这个数组由5个类型为double的项目组成,这5个项目按序排列,且能够用一个整数引用,这个整数称为索引C,例中的索引值从04

这些声明都以一个类型C开始:intdouble,和size_t。它们的含义会在后面介绍,现在只要知道,这三个标识符在使用它们的语句中,都是某种类型的“数字”即可。

iA声明的是两个变量C,它们是命了名的可以存储C的对象。若用可视化的方式将它们表示为可以包含某种特殊类型“事物”的盒子,则如下所示:

i  size_t ?? 


[0] [1] [2] [3] [4]
 A  double ??    double ??    double ??    double ??    double ??  

从概念上说,区分盒子本身(对象)、它的详细说明(它的类型)、盒子的内容(它的值),以及写在其上面的名字或标签是很重要的(标识符)。在这样的图表中,当我们不知道其具体的值时,就用??来代表。

对另外三个标识符,printfsize_t,和EXIT_SUCCESS,没有看到它们的任何声明。事实上它们是预声明的标识符,但在尝试编译列表1.2中的程序时可以看到,这些标识符并不是没有来处。所以我们必须告诉编译器从哪里获取它们的信息,列表1.1中行2和行3处的代码,正是用来完成这件事:printfstdio.h中来,而size_tEXIT_SUCCESS则从stdlib.h中来。这些标识符的实际声明是由存储在计算机中某个地方的.h文件提供,而它们看上去可能是这样:

int printf(char const format[static 1], …);

typedef unsigned long size_t;

#define EXIT_SUCCESS 0

因为这些预声明标识符的指定相对来说次要一些,通常把它们放在包含文件C或称头文件C(*.h)。如果想要弄清它们的语义,直接查看声明它们的源文件通常不是一个好方法,因为这样的声明通常不利于阅读。相反,在随系统安装的文档中查找它们的信息通常会更好。对于更勇敢些的读者,作者总是建议它们直接阅读最新的C语言标准,因为那才是所有这些预声明特性的来源(译者注——这对于标准定义才是可行的,实际编程中可能会用到很多非标准函数)。而对于不是那么有勇气的,下面这些命令可能会很有帮助:

0

1

2

> apropos printf

> man printf

> man 3 printf

声明只是描述了一个标识符,并没有创建它,所以重复一项声明不会带来太多害处,但会增加冗余。

要点 0.2.2.2 标识符可能会有多个一样的声明。

很显然,如果在程序的同一个部分存在一个标识符的多个不一致的声明,则不论是对阅读者还是编译器,都会造成实际的困惑。所以,通常这是不允许的。C语言对于什么是“在程序的相同部分”是有相当精确的含义的:用范围C描述,即指某个标识符在程序中可见C的那个区块。

要点 0.2.2.3 声明是与它们出现在的那个范围相绑定的。

语法对这些标识符的范围做了无歧义的描述。在列表1.1中,几个声明具有不同的范围:

  • A只在main的定义内可见,且从声明它的行8处开始,直到包含它的声明的语句块的最内层大括号对 {...}的结尾括号(行24)结束。

  • i的可见范围更小一些,只与其声明所在的for构件相绑定,即从行16开始,到与该for构件相连的语句块{...}结束处(行21)结束。

  • main并没有被包含在任何语句块{...}中,所以从它声明开始的地方,一直到该文件的结尾处都是可见的。

通常将前面两种范围称为块范围C,因为它们的范围是由对应的C{...}限定的。适用于main的第三种范围,因为没有被包含在任何大括号对{...}内,所以称为文件范围C。通常又将文件范围内的标识符称作全局的。

2.3 定义

通常,声明只是给定了该标识符所指对象的类型,并没有给出其具体的值,也没有给出该对象的存储空间。这个重要的任务则由定义C来完成。

要点 0.2.3.1 声明指定标识符,定义才确定对象。

再往后会发现,现实中事情要更复杂一些,但现在可以简化成“我们总是可以初始化我们的变量”。初始化是一种语法构件,它提供一个声明,同时定义一个对象,并为其提供一个初始值。例如:

size_t i = 0;

该语句声明了i,同时给它提供了初始值0

C语言中,像这样带着初始化式的声明,同时也为该名字定义了一个对象:亦即,它告诉编译器为该变量提供一块存储空间,以用于保存它的值。

要点 0.2.3.2 对象在其变量被初始化的同时就被定义了。

前面用于图形化说明的盒子,现在则可以用0来填充:

i  size_t

A要相对复杂一些,因为它是由多个元素组成:

getting-started.c
8
9
10
11
12
13
double A [5] = {
 [0] = 9.0,
 [1] = 2.9,
 [4] = 3.E+25,
 [3] = .00007,
};

上面语句将A5个元素按顺序分别初始化为9.02.90.00.0007,3.0E+25。如图:

[0] [1] [2] [3] [4]
 A  double 9.0    double 2.9    double 0.0    double 0.0007    double 3.0E+25  

此处使用的这个初始化式又称为指定式的C:即在一对中括号内,使用整数来指定数组中对应位置处的元素。例如[4] = 3.E+25是将数组A最后一个元素的值设置为3.E+25。在该种初始化式中,任何没有被指定的位置处的元素都将被设置为0。本例中的缺失元素[2]即被填充为0.010

要点 0.2.3.3 初始化式中的缺失元素取默认值0。

读者应该注意到,数组中第一个元素的位置——即索引C,并不是从1开始,而是从0开始。可以将数组中元素的索引(位置)看成是相应元素距数组开始处的距离。

要点 0.2.3.4 对于有n个元素的数组,其第一个元素的索引是0,其最后一个元素的索引是n-1

对于函数,如果在它的声明后面紧跟了一个大括号对{…},包含了该函数的代码,就说它有了一个定义(与仅仅只有声明相区别)。

int main(void){

}

在前面的例子中,出现了两种不同的事物:对象C,例如iA; 以及函数C,如mainprintf。虽然对象或函数可以有多个声明(必须是一致的),但是它们的定义只能是唯一的。也就是说,要使C程序可执行,所用到的任何对象或函数都必须有一个定义(否则执行时将不知道到哪里能找到它们),而且其定义不能多于一个(否则其执行将变得不一致)。

要点 0.2.3.5 每一个对象或函数都必需有且仅有一个精确的定义。

2.4 语句

main函数的第二个部分主要由语句C组成。语句是用来告诉编译器做什么事情的指令,且通常会用到之前声明的标识符。

getting-started.c
16
17
18
19
20
21
22
23
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;

前面讨论了调用printf函数的语句,还有其他类型的语句:forreturn语句,还有运算符C++执行的自增操作。接下来稍微深入点介绍另外3种类别的语句:即迭代(做某事情若干次),函数调用(将执行委托到其他地方),和函数返回(从调用其他函数的地方接着往下执行)。

2.4.1 迭代

for语句告诉编译器,让程序把调用printf函数的行执行若干次。这是C语言提供的形式最简单的域迭代C。它由4个不同的部分组成。将要被重复执行的代码称为循环体C:它是紧跟在for(...)后面的语句(块)(译注——可以是单条语句,或语句块{...})。其他3个部分在小括号对里面(...),由分号分隔开来:

1循环变量Ci的声明,定义和初始化。这个初始化会在整个for语句的所有其他部分执行之前执行(且仅执行)一次。

2循环条件Ci < 5 指定for迭代应该重复多少次。此处是告诉编译器,只要i的值严格小于5,就继续重复执行。循环条件会在循环体每次执行之前检查一次。

3++i,会在循环体每次执行完成后执行一次。此处是将i的值每次增加1

将所有这些放在一起,是要求程序将循环体内的程序块执行5次,分别在每次迭代时将i的值设置为0123,和4。事实上根据每次迭代时i的值可以将该迭代视为在C0,…,4上的迭代。在C语言中有不止一种方法可以做到这一点,但是for是其中最简单,最清晰,也是最好的一种方法。

要点 0.2.4.1 域迭代应该使用for语句。

除了上面介绍的这种写法,for语句还可以有其他不同的写法。有时,人们会将循环变量的定义放在for前面的地方,甚至在多个循环中重复使用一个循环变量。但我不建议这样做:为帮助偶然的代码阅读者和编译器更好地理解代码,让该变量只具有该for循环的迭代计数器一种用途很重要。【让他们知道该变量只具有给定for循环的迭代计数器一种用途很重要】

要点 0.2.4.2 循环变量应该在for语句的初始化部分进行定义。

2.4.2 函数调用

函数调用是一种特殊的语句,其执行时会将当前函数的执行挂起(开始时通常是main函数),然后将控制转交给被调用的函数。前面的例子中,

getting-started.c
17
18
19
20
printf(“element␣%zu␣is␣%g,␣\tits␣square␣is␣%g\n”,i,A[i],A[i]*A[i]);

被调用的函数是printf。函数调用除了要提供函数的名字,有时还要提供调用参数。此处的调用参数是一长串的字符串,iA[i],以及A[i]*A[i]。这些参数的值会被传递给被调用函数,这里的值就是printf将要打印的信息。这里强调使用了“值”这个字:虽然i是其中一个参数,但printf却永远不能改变i本身的值。这种机制称作传值调用。其他编程语言还提供了按引用调用的机制,这样被调用函数就可以修改作为参数的变量的值。C语言没有提供按引用调用的机制,但它可以通过另外的机制,将一个变量的控制传给其他函数:那就是获取变量的地址并将其作为指针传递。这些内容要在较靠后面的章节中才会介绍。

2.4.3 函数返回

main函数的最后一条语句是return。它告诉main函数返回到刚才调用它的地方。上例中,因为在main声明的前面有int,所以return必须返回一个类型为int的值给它的调用者。此处返回的值是EXIT_SUCCESS

虽然我们看不到具体的定义,但printf中一定包含了一个相似的return语句。在行17处调用函数的那个地方,main中语句的执行会被暂时挂起,执行会继续在printf函数中进行,直到遇到一个return语句。在从printf返回后,main函数中刚才被暂停的那个语句会继续往下执行。

图表2.1

2.1给出了我们的示例程序的执行机制图,即它的控制流。首先,系统使用了一个启动进程程序(左侧)调用了我们提供的函数main(中间)。接着main调用了printf(右侧),该函数是C函数库C的一部分。一旦在printf中遇到了return语句,控制会返回给main。并在遇到main中的return后,返回给启动进程。从程序员的视角来看,后面一个返回,就是程序执行的结束。



小结

  • C语言区分对待词汇结构(标点符号,标识符,还有数字等),语法结构(语句结构),以及程序的语义(含义)。

  • 所有标识符(名称)都必须进行声明,以使人们(或编译器)知道它们所表示的概念的属性。

  • 所有对象(处理的目标事物)和函数(用来处理事情的方法)都必须进行定义,这样才知道它们从何处来,该如何使用等。

  • 语句指示了事情将被怎样处理:迭代(for)重复某种任务若干次,函数调用(printf(...))把一项任务委托给一个函数,而函数返回(return something;)则返回到该函数被调用的地方。


8 C语言的行话中,称它们为指令C关键字C保留C

【练习9】 找出这两种括号的不同用处。

10后面会介绍这些带有点(.)和指数(E+25)的数字字面量。

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