C premier plus Chapter 2

第二章 C语言介绍

你将会在这一章学到
1.赋值运算符
2.函数main()与printf()
3.一个简单C语言程序的构成
4.创造整型变量,赋值并在屏幕上显示它们的值
5.换行符'\n'
6.如何在程序中添加注释,让你的程序中同时拥有多个函数
并能找出程序中的错误。
7.关键字是什么

C语言的程序是什么样子的?如果你大体浏览过这本书,你会看到很多的例子的。几乎一致的是,你会觉得C语言程序看起来很怪异,有些需要用到类似{,cp->tort,*ptr这样的符号。随着你通读完这本书,你就会慢慢觉得这些符号不是那么陌生了,会变得更加熟悉,甚至有可能会很高兴遇见它们。在这一章中,我们从给出并解释一个简单的实例起步。同时,我们也会强调一些C语言的基本特征。

2.1 一个简单的例子

让我们先研究一个简单的C语言程序。在图2.1展示的这个程序,专门为了突出一些C语言编程的特征。在你将要读接下来对于程序的逐行解释之前,先看看图2.1,试试你是不是能够靠自己搞明白它会做什么事情。
小例子

1.#include <stdio.h> 包含头文件
2.int main(void) 定义一个返回值为整数,输入值为空的主函数
{
int num; 定义一个名为num的整型变量
num = 1; 给num赋值1

printf("I am a simple "); 输出一行字
printf("computer. \n");
输出一个词,接在刚才的一句话之后,并换行
printf("My favorite number is %d because it is first.\n",num);
return 0; 返回值0,表示结束。

}

注释中的三个部分:1.定义变量 2.赋值 3.使用函数

如果你认为这个函数将会在你的屏幕上打印一些东西。是的,你是对的。但对于明确的什么东西将会被打印到屏幕上可能并不明显,所以我们运行这个程序,看看会发生什么。

  • 第一步,使用你最喜欢的编辑器
    或者你的编译器中最喜欢的编辑器
  • 第二步,创造一个包括了图2.1中文本的文件,
    并且以.c为结尾,从而满足你的系统对于文件名的需要。
    举个例子first.c就是合法的名称。
  • 第三步,编译并运行程序。
    查看第一章来得知这方面的详细过程。

如果一切都运转良好的话,你会在屏幕上看到这样的输出。
输出

总之,这个结果并不是很惊人,但是\n%d的作用是什么呢?
程序中有几行也看起来很陌生,这就到解释的时候了。

2.2 解释这个例子

我们会对这个例子解释两遍,第一遍(Pass1:速览大纲),强调了每一行的意义,从而你能够大体上得知发生了什么。第二遍(Pass2:程序细节),探索了特定部分的含义,并且给出了细节以便你能够更深刻地理解。

图2.1总结了C语言程序的每一个部分,
除了我们第一个例子使用的,它们还包含着更多要素与步骤。

Pass1:速览大纲:

这一部分包括了对于程序每一行的简短解释。
下一部分会更加充分地探索这些主题的内容。

#include <stdio.h> 包含另一个文件。

这一行告诉编译器要从文件stdio.h中收集信息,这个文件是所有C语言编译器安装包的基础部分,提供了对于键盘输入与显示输出的支持。

int main(void) 一个函数名

对C语言程序的分析

C语言包括一个或者多个函数,它是C语言程序的基本模块。这个程序由一个叫做main的函数组成,圆括号表示了main()是个函数的名字。int表明了main()函数会返回一个整数,void表明main()不会获取任何的参数,这些都是我们过会会深入研究的事情。现在,接受voidint是在ISO/ANSI标准下定义main()函数的方法(如果你有在ISO/ANSI标准之前的编译器,省略void就好,但你可能也想获取更新的版本来避免不协调。)

/* a simple program*/ 一条注释。

/**/符号框定了一条能够帮助阐明一个程序的注释,它们是只为阅读者准备的,会被编译器忽略掉。

{ 函数体的开头

起始大括号{标志着函数中语句的开始,
函数的定义由终止大括号}结束。

int num; 一个声明语句

这个语句声明了你正在使用一个叫做num的变量,而且num会是int(integer-十进制整数)类型。

num = 1; 一个赋值语句

这个语句num = 1把值1赋给叫做num的变量。

printf(“I am a simple “); 一个调用函数的语句

第一句使用了printf()函数来在屏幕上显示I am a simple 这句话,把光标留在了同一行。这里的printf()函数是标准C语言库的一部分。它被称作一个函数,而在程序中使用一个函数叫调用函数

printf(“computer.\n”); 另一个函数调用语句

第二次调用printf()函数是在上一句打印下来的话后面添加了computer\n符号是告诉电脑要另起一行-也就是说把光标移到下一行的起始。

printf(“My favorite number is %d because it is first.\n”);

最后一次使用printf()是基于引用的语句把num的数值(1)打印到屏幕上,%d指示电脑从哪里,以什么方式输出num的数值。

return 0;

一个C语言函数是可以提供,或者说***return(返回)***,一个数值

} 结束。

正像说过的,程序以终止大括号结束。

Pass2:程序细节

既然你已经大体了解了图2.1中的内容,我们要更详尽地研究一下它了。再一次,我们将会逐行地查看程序,为了能够培养一种更宏观的C语言编程视角,这一句我们会以每一行代码作为起点,然后一步步深入代码之后的细节。

# include 指示与头文件

# include <stdio.h>

这是一个程序的开始行,**#include <stdio.h>** 的作用就和你把stdio.h中所有的内容都打字写在程序#include的位置是一样的。因此,这是一个剪切粘贴的操作,在共享对于很多程序都一样的信息上,include文件提供了一种便利的方式。

#include语句是C语言预处理指令的一个范例。大体上,C语言编译器会在编译之前对于源码做一些准备工作,这在术语中称为预处理

在大部分上,头文件包含着编译器制作最终的可执行文件需要使用的信息。例如,它们可能会定义一些常量或者声明一些函数的名字,与它们应该被怎样使用,但是库中函数真正的代码-编译前代码,不是在头文件中的。编译器的一部分-连接器会负责找到你需要的这一部分库代码。简单地说,头文件会帮助引导编译器来正确地把程序组装起来。

ISO/ANSI标准下的C语言必须提供头文件,有些程序需要包含stdio.h头文件,有些不需要。对于C语言实例的记录应该包含使用的C语言标准库函数的描述,这些函数描述就确认了需要什么头文件。例如对于printf()的描述就表明了需要使用stdio.h,省略恰当的头文件可能并不会影响特定的程序,但是最好不要依赖它。这本书每一次使用库函数时,我们都会为函数使用include来把ISO/ANSI标准制定的的文件包括在程序内。

为什么输入输出不是内建函数?

为什么输入输出并不是内建函数?
你有可能想知道为什么像输入输出这样基本的东西并不会被自动包含进程序中。一种回答是,并不是所有的程序都要使用I/O(input/output)包,而且C语言的哲学中也说到要避免承担不必要的重量。这个经济学上资源利用的准则让C语言受到嵌入式编程的欢迎-例如,为控制自动燃油系统编写代码。顺带一说,#include这一行甚至不是C语言的语句。#符号阐述了这一行是在编译器之前被预处理器处理的代码。你会在后面学习预处理指令时遇到很多例子,而且在第16章对这个主题还有更加充分的讲述。

main函数

int main(void)

程序的下一行声明了一个名字为main的函数,main是个挺普通的程序,但是它是唯一的选择。一个C语言程序(有些我们无需担心的例外),通常是以名叫main的函数开始运行的,其他函数的名字是可以随意选择的,但是作为一切的开端,main函数必须存在。那么圆括号呢?它们是为了确定main()是个函数,你会很快学习学到关于函数的更多内容。现在,记住这个函数是C语言程序的基础模块。

intmain()函数的返回值类型,它意味着main()返回的数值种类必须是十进制整数(integer)。那么返回到哪里?返回到操作系统-我们会在第六章讨论循环的时候回到这个问题上来。

跟随在函数名后面的圆括号大体上囊括了需要传递给函数的信息,举个简单的例子,没有东西被传递,那么这个括号就包含着void-空类型(在第11章,“字符指针与字符串函数”介绍了第二种从操作系统把信息传递给main()函数的形式)

如果你查看过很久以前的C语言代码,你会经常看到程序是以下面的形式开始的:

1
main()

C90标准勉强地包容了这个形式,但是C99没有,所以即使你现在的编译器让你写成这个样子,不要听。

下面是另一种你可能会看见的形式

1
void main()

有一些编译器是允许这样写的,但是没有一个标准把它列为一种选择,
因此,编译器也没有必要接受这个形式,有几种确实也没有。再一次,坚守标准的形式,你就不会在更换编译器的时候陷入困境。

注释

1
/*a simple program*/

这一部分由/ * * /符号框定起来的内容就是注释,使用注释可以让一些人(包括你自己)更好地理解你的程序。C语言的一个很好的特点就是注释可以放在任何地方,甚至与它们解释的“原文内容”放在一行。长一点的知识可以放在一行甚至延伸至多行,以/ * 开始并以 * /结束的内容都会被编译器忽略。下面是注释的合法与非法写法。

注释示例

翻译

(1.这是一行C语言注释)
(2.这个注释延伸到了两行)
(3.你也可以这样做)
(4.但是这是不合法的因为没有结束符号)

C99添加了注释的另一种形式,它在C++与Java中很受欢迎,这种新形式使用//符号来生成只在一行内的注释。

新形式

因为一行的结束标志这这行注释的结束,这种方式需要在注释开始的时候添加注释符号。

新形式的出现是为了响应旧形式的潜在问题,假设你有下面的代码。

我希望这可以运行...

下一次,我推断你要删除第四行
然后意外地把第三行删掉了,
然后这代码就会变成这个样子

哦不...

现在编译器把第一行的起始符与第四行的结束符进行配对,结果使这四行全部成为了注释,包括那本应该是代码的一部分的一行。因为//并不会延伸到另一行,它不会引起代码的“消失”。

一些编译器有可能并不支持C99的这个特点,还有些有可能需要改变一些编译器的设置才能够应用C99的特性。

大括号、函数体与代码块

1
2
3
{
...
}

在图2.1,大括号划定了main()的界限。大体上,所有的C语言函数都需要使用大括号来标志着函数体的起始与结束。它们是必须出现的,所以一定不要漏了。只有大括号是起这个作用的,不要使用圆括号或者中括号。

大括号还可以被用来把语句聚集在一个函数中,形成一个单元或者语句块。如果你对于Pascal,ADA,Modula-2或者Algol熟悉的话,你就会意识到大括号划定起始与结束在这些语言里也是相似的。

声明

1
int num;

程序的这一行术语中称为声明语句,声明语句是C语言的其中一个最重要的特点。这个特例声明了两件事,一是在函数的某一个部分有一个叫num的变量(好好笑啊哈哈,在那遥远的函数中,有个小变量。)第二,int声明了num是一个十进制整数-也就是说,没有十进制小数点。(int数据类型的一个例子),编译器使用这些信息为num变量准备合适的内存空间。结尾的分号标志了这一行是C语言的语句或者指令。分号也是这个语句的一部分,并不只是像在Pascal里的语句之间的分隔符。

C语言中,int是一个确定了一种数据类型的关键字,关键字是我们用来表达语言的单词,而且你不能把它们强占用于其他目的。例如,你不能把int作为函数或者是变量的名称。这些限制在语言之外就会失去作用,反而你把一只猫或者你最喜欢的孩子叫做int是没有问题的。(除非本地官员或者法律不准用这个名字)。

num标识符的一个例子,也就是你为变量、函数或者其他实型取的名字。从而声明把特定的标识符与电脑的特定的内存区联系在了一起,而且它也建立了信息的类型,或者说数据的类型,来放在那个区域里。

在C语言中,所有的变量都需要在被使用之前先声明,这也就意味着你必须对于所有在一个程序中使用的变量列出包括数据类型的声明。人们也认为声明变量是一个优秀的编程技巧,而且,在C语言中,它是必须进行的。

通常来说,C语言要求在代码块的起始部分声明变量,而且其他的语句是不允许放在这之前的。也就是说,main()函数的函数体会是这个样子。

传统方式

C99标准学习了C++的时间,现在可以让你在代码块的任何地方声明变量。然而,你还是需要在第一次使用变量之前声明它。所以如果你的编译器支持的话,你就可以像下面这样写:

C99新方式(1)
C99新方式(2)

为了能够和更旧的系统兼容,这本书还是会坚持原来的标准。(一些更新的编译器只有当你设置之后才会支持C99的特点)。

就此,你很可能有三个问题:

  • 1.数据类型是什么?
  • 2.在取名的时候我们可以怎么选择?
  • 3.为什么你需要把变量全部声明出来。
    那就让我们探究这些问题的答案吧。

数据类型

C语言主要处理的是这几种数据:十进制整数、字符、浮点数,仅举几例。声明一个整型或者字符类型变量可以使电脑恰当地储存、抓取与翻译数据。你会在下一章研究到更多可用的数据类型。

名字的选择

你应该为变量使用有意义的名字(像是sheep_count而不是x3如果你的程序是数羊的),如果名字不能达到表意的需求,那么就使用注释来解释变量代表了什么。以这种方式为程序做记录是其中一种优秀的编程技巧。

你可以使用的字符要视实际情况而定。C99标准要求不多于63个字符,除非是外部的标识符。(在第12章“内存分级、链接性与储存管理”中会提到),相比于分别只能识别31个字符与6个字符的C90,这是一个巨大的提升,而且老的C语言编译器最多只能接受8个字符。事实上你可以使用更多的字符的,但是编译器会忽略它们。因此,当一个系统有着8个字符的限制时,shakespeareshakespencil就会被认为是一样的了,因为它们的前八个字符是一样的。(如果你想要63个字符的例子,可以自己造一个)。

你可以在大写字母、小写字母与下划线之间任意选择。第一个字符必须是一个字母或者一个下划线。下面是一些示范。
示范
操作系统与C语言标准库通常会使用1或2个下划线字符,比如_kcab,所以最好避免自己使用它。以一个或者两个下划线字符开始的,标准的标签,比如库中的标识符,会被储存起来,也就是说,即使使用它们作为名字并没有任何的语义错误,它是会造成一些名字冲突的。

C的标识符是区分大小写的,也就意味着大学字母是被看做与对应的小写字母字符不同的。因此,starsStarsSTARS是不同的。

为了让C语言能够更加国际化,C99使用通用字符名称机制(UCN)进行了巨大的扩充,在附录B中的参考单元VII“更广大的字符支持”,我们讨论了这些加入的部分。

声明变量的四个好处

一些年代更远的语言,比如FORTRAN与BASIC语言的原型版本,可以让你在没有声明的情况下使用变量。那么为什么我们不能在C语言中使用这种简单易行的方法呢?这里就是一些原因:

  • 把所有的变量放在一个地方能够让阅读者更容易地知道程序是关于什么的。这点好处在你给变量取有意义的名称的时候尤其明显,(就像taxrater),如果名字不能达意就使用注释来解释,以这种方式给程序做记录是一种良好的编程技巧。
  • 通过思考声明变量是为了什么鼓励你全心投入地去写程序之前做一些准备。程序开始需要什么类型的信息?我到底想要程序输出什么?呈现数据的最好方式是什么?
  • 声明变量有助于我们预防编程中更多难以发现又狡猾的错误之一-也就是拼错了变量的名字。举个例子,假设在一些没有声明的语言中,你写下了这个语句。
1
radius1 = 20.4;

然后在另一个地方,你拼错了变量名。

1
> circum = 6.28 * radiusl;

你无意地把数字1换成了l,那种语言也会创造一个radiusl的变量,然后用未知的值(或许是0,或许是垃圾值),circum就会被给予一个错误的值,而且你可能需要极其多的时间来搞清楚为什么,这在C语言是不可能出现的(除非你确实是笨到家了去定义两个这样相似的变量名),因为编译器会在radiusl出现的时候报错。

  • 如果你不声明变量的话,你的C语言程序是不会进行编译的。如果先前的原因没有动摇你的话,对于这一点你可就得好好想想了。

既然你需要声明需要使用的变量,它们会到哪里呢?就像上文提过的,C语言在C99之前就要求声明需要在代码块之前完成,遵循这种把声明放在一起的做法,你就可以更容易地得知程序正在做什么。当然,把你的声明分布在各个地方也是有好处的,正像现在C99所允许的。这也就是在你赋值之前先声明变量。那让你更难以忘记给他们赋值。作为关键性的事情,许多的编译器还不支持C99的这个规则。

赋值

1
num = 1;

下一行的程序是一个赋值语句,C语言中的其中一种基础运算。这个例子的意思是“把值1赋给变量num,之前的int num”这一行在电脑内存中为num分配了空间,这一行也就在这个空间中储存值。如果想要的的话你可以过会给num赋另一个值,那也就是num被叫做变量的原因。需要注意的是赋值语句会把值从右边到左边传递。
同时,这一句也是以分号结尾的,就像图2.2中显示的。
赋值图示

printf()函数

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

这些行使用了一个C标准库中叫printf的函数,圆括号表明了printf是一个函数的名字,在圆括号之中的内容就是main()函数向printf()函数传递的信息。例如,第一行就传递了这句话 I am a simple这样的信息叫做参数,或者更加充分地,叫做函数的实际参数(见图2.3),那么printf()函数会对对这种语句做些什么呢?它会抓取在双引号之间的一切,然后把文本打印在屏幕上。

函数调用

第一行printf()是你调用函数的一个很好的例子,你只需要打字写下函数的名称,把需要的参数放在圆括号内。当程序运行到这一行时,控制流就转换到已经定义的函数(在这个例子中是printf())中,然后当函数已经完成了它需要做的事情之后,控制流就又回到原来的(调用)函数-在这个例子中是main()

下一行的printf呢?它在引号中包含了\n这个字符,而且它并没有被打印下来?发生了什么?\n的作用其实是另起一行,\n的组合体(打印下来是两个字符)就组成了\n这个叫换行符的新字符。对于printf()它意味着“在左下角再起一行”。也就是说,打印换行符和你在常规键盘上按下enter键的作用是差不多的。那么为什么不在打字写printf的时候用Enter键呢?那是因为因为那只会被认为是对于你的编译器的即时的指令,而不是对于源码的指令,也就是说,当你按下enter键时编辑器会退出当前的行,然后另起一行。换行符,影响着程序是如何显示的。

换行符是转义字符的范例,转义字符是用来呈现难以或者几乎不能通过打印显示出来的字符的。其他的还有\t对应着Tab\b对应着Backspace,每一个转义字符都会由\来起始,我们会在第三章:C语言与数据再回到这个话题深入研究。

这也就解释了为什么三个printf却只有两行文字了,第一个printf()命令没有换行符,但是第二个与第三个是有的。

最后的printf()又带来了另外一个异样的地方:当这行文字被打印的时候%d发生了什么?你应该还记得,输出是这个样子的。

1
My favorite number is 1 because it is first.

啊哈,当被打印出来的时候,数字1就是%d的替换物,而且变量num的值就是1%d是展示num的值在什么时候被打印的占位符,这一行与下面的BASIC语句是相似的:

1
PRINT "My favorite number is "; num; because it is first."

事实上,C语言的版本比这个做的工作还要多一点,%告诉程序有一个变量需要在这个区域被输出,d表达了要把这个变量以十进制整数的方式输出。printf还可以以其他变量形式进行输出,包括十六进制数、带有十进制小数点的数。事实上,printf()中的f表明了它是一个格式化打印函数。每种数据类型都有自己的指示符,这本书也会在介绍新的数据类型的同时引入对应的指示符。

返回语句

1
return 0;

返回语句是这个程序的最后一个语句,在int main(void)中的int意味着main()函数应该返回一个十进制整数。main()函数这样返回是C语言标准要求的。返回值的C语言函数都会使用return语句来进行这个操作,return语句包含着关键字return与返回的值,以一个分号结束。如果你遗漏了这一句,大多数编译器会报错,但是它们仍然会对程序进行编译。这样的话,你可以把return语句看成为了增加逻辑的连贯性的东西,但是它对于一些操作系统是由实际用处的,包括DOS和Unix,我们在第11章会更深入地对于这个话题给出解释。

2.3 简单程序的结构

既然你已经看过了具体的例子,你已经准备好了解C语言程序的一些基本规则了。程序由包含一个函数或者多个函数的组组成,其中一个必须叫main()对于函数的描述包括函数头与函数体,函数头包括预处理语句,如#include,与函数的名字。你可以通过看圆括号的方式来得知这是个函数,即使圆括号可能是空的。函数体是由大括号框定的,它包括了一系列的语句,每一个都是由一个分号结尾的(见图2.4)。在例子中有声明语句,它是用来声明变量的类型与名字的,还有赋值语句,它给予变量一个值。然后还有三个print语句,每一个都调用了printf()函数,这些print语句是函数调用语句的一些示范。最后main()函数以返回值结束。

简单来说,一个简单的、标准的C语言程序应该使用下面的格式。
图2.4
标准格式

2.4 可读性建议

使你的程序变得更可读是一种优秀的编程实践。可读性高的程序很容易理解,也就很容易去修改、纠正。让一个程序变得可读的过程也帮助你理清你对于程序执行自己的概念。

你已经看过两种可以增加可读性的技巧了:选择变量名与使用注释。注意这两种技巧是相互补充的。如果你给一个变量注名width,你就不需要对于这个变量表示什么做任何解释了。但是叫做video_routine_4的变量就需要对于video routine 4的解释。

另一个技巧是使用空行来分隔函数中的概念模块。例如,简单的实例程序就使用了空行分开了声明部分与行动部分。C语言不需要空行,但是它能够提高可读性。

第四个技巧就是每行使用一个语句。再一次,这是一个针对于可读性的惯例,并不是C语言的要求。C语言的格式是形式自由的,你可以把几句话放在一行或者一直延伸到第二行。下面的代码是合法的,但是看起来很丑。
丑
分号向编译器指示了一个语句什么时候结束和下一句什么时候开始。但是如果你能够遵从这种在本章的例子中使用的传统手法,程序的逻辑会清晰很多。(见图2.5)
可读性规范

2.5 更进一步

我们的第一个C语言例子还是很简单的,接下来的图2.2就是第二个例子,但也并不是非常难。

例子

新在哪里呢?代码中提供了对于程序的描述(在注释中),声明了很多变量,做了一些乘法运算,然后把这两个变量的值打印下来,让我们把这些点深入地了解一下。

记录 Documentation

第一,程序是以注释开始的(使用的是新的注释形式),它确定了文件名与程序的目的。这种程序的记录只花费一分钟就可以做,而且之后会在你浏览多个文件并打印它们的时候起到很大帮助。

多行声明 Multiple Declarations

然后,程序在一行中声明了两个而不是一个变量。为了达成这个目的,可以把两个变量(feetfathoms)的声明使用一个逗号隔开,也就是下面这样。

1
int feet, fathoms;

它和下面的形式是等价的。

1
2
int feet;
int fathoms;

运算 Multiplication

第三步,程序进行运算,它利用了计算机系统庞大的算力来计算2乘6。
在C语言中,同样也在很多语言中,*是乘法运算的符号。
因此,下面的语句:

1
feet = 6 * fathoms;

它的意思就是:查看fathoms的值,乘以6,然后把这个运算的结果的值赋给变量feet

打印多个值 Printing Multiple Values

最终,程序更多样地使用了printf()如果你编译运行过这个例子,输出应该是这个样子的。

1
2
There are 12 feet in 2 fathoms!
Yes, I said 12 feet!

这一次,代码在第一次使用printf()的时候,使用了两次替代符,在双引号中的第一个%d被在后面引用列表的第一个变量的值替换了(feet)然后第二个%d被第二个变量代替了。要注意的是待打印变量列表是在双引号部分后面,另外也注意每一个都是用括号分开的。

第二次使用printf()表明了打印的值并不一定是变量的,只是必须要指定一个值,就像6 * fathoms这样,可以是恰当类型的值。

这个程序受篇幅不长,但是它可以呈现把fathoms转换到feet的核心。我们需要的就是动态地去赋其他的值给feet我们会在以后的章节解释如何做。

2.6 顺便一提:多重函数

目前为止,这些程序已经使用过了基本的printf()函数。图2.3向你展示了如何把自己的函数-除了main()以外的函数-加到程序里。

两个函数的程序

输出是这个样子的。

1
2
3
I will summon the butler function.
You rang, sir?
Yes,Bring me some tea and writeable CD-ROMS.

butler()函数在程序中出现了三次,第一次出现是在原型中,那告诉了编译器要使用的函数。第二次出现是在main()函数中,在函数调用中出现,最终,程序呈现了函数的定义过程,那是函数本身的源码。让我们再来看看这三次出现。

C90标准添加了原型,老翻译器可能并不能识别它们(我们将会讲到当你使用那种编译器的时候要怎么做。)原型就是一种告诉编译器你正在使用一个特定的函数的声明。它也指定了函数的属性。例如,butler()函数的第一个词void指明了它没有返回值。(大体上,函数可以为了使用值而返回这些值,但butler没有),第二个void-在butler(void)-的意思是它没有参数。因此,当编译到达main()函数中butler被使用的这一行时,它会检查是否butler是被恰当地使用的。需要注意,void是被用来表示empty(空),而不是invalid(不合法、无效)。

早先的C语言支持的函数声明类型有限,那时你只能定下返回值的类型,但是对于参数的描述就被省略。

1
void butler();

那时的C语言代码使用的函数声明就像是上述的样子,而不是函数原型,C90与C99标准都能够辨认出这种更老的形式,但是它们表示这种形式会随着时间被淘汰,所以不要用。如果你从一些更老的地方拿到了C语言代码,你有可能想要把老式的声明变成原型。以后的章节会回到原型制作、函数声明与返回值上。

接下来,你通过给出butler的名字与圆括号来援引函数。(调用),当butler完成了它的工作,程序就会移向main()函数中的下一行。

最终,函数butler()以与main()函数一样的形式被定义,有函数头与用大括号框定的函数体。函数头重复了你在原型中提供的信息:butler不使用信息,而且不返回值。对于老旧的编译器,省略第二个void

有一点需要注意的,就是在main()函数中butler()被调用的位置-而不是butler()函数被定义的位置-决定了butler()函数的运行位置。比如,你可以把butler()函数的定义放在main()函数的前面,程序还是会照旧那样把butler()放在两个printf()中间运行。记住,所有的C语言程序都是以main()函数开始的,无论main()函数在程序文件的什么位置。然而,C语言实际上是先把main()函数列出来,因为它通常提供了基本的程序框架。

C语言标准建议你为所有你使用的函数提供原型,标准的include文件为标准库函数做这个工作,例如,在标准C语言下,stdio.h文件就有printf()函数的原型,第六章的最后一个例子会告诉你如何去更广泛地为非void的函数制作原型,第九章会全面地讲述函数。

2.7 调试引入

现在你已经可以写一个简单的C语言程序了,你有可能海域犯些简单的错误。程序的错误通常被叫做bugs,寻找与修复它们就叫做调试,图2.4就展现了有些bugs的程序,看看你是否能够找到。

找找吧,有几处?

语法错误 Syntax Errors

图2.4的例子犯了几个语法错误,当你没有遵循C语言的规则,你就会犯语法错误,这也可以类比到英语的语法错误。例如,思考一下这句话:Bugs frustrate be can. 这句话使用的单词是正确的,但是并没有遵循单词顺序的规律,而且使用的单词形式也不太对,不管怎样,C语言的语法错误就是在错误的地方使用了正确的C语言标志。

所以nogood.c这个程序究竟犯了什么错误呢?第一,它使用的是括号而不是大括号来标记函数体,也是把正确的C语言符号用在了错误的地方,第二,声明应该是这个样子的

1
int n, n2, n3;

或者这样

1
2
3
int n;
int n2;
int n3;

而且,这个例子遗漏了安置一条注释必须要使用的*/符号对(作为替换,你可以用//形式替换/*),最后,它遗漏了printf()后的本应作为语句结束标志的分号。

你如何来检查语法错误呢?第一,在编译之前,浏览你的源码,看你是否看到一些明显的错误。第二,你可以检查编译器找到的错误,因为它的一部分工作就是检查语义错误。当你编译程序的时候,编译器会反馈找到的所有错误,并且会明确错误类型与出错的位置。

然而,编译器有时也会混乱,实际上的一个区域内的语义错误有可能会让编译器错误地认为它找到了其他种类的错误。例如,因为这个例子没有恰当地声明n2n3,所以编译器会认为它在被使用的地方找到了错误。事实上,与其试图一次把所有报告的错误修正过来,不如只修改一两个然后重新编译,这样的话有些其他种类的错误就会消失。继续这样做,直到程序正常运行。另一个常见的编译器“诡计”是它会在后面的行报错,但其实出错是在前面的行里。例如,编译器有可能直到下一行才能判断出上一行遗漏了分号。所以如果编译器报错说缺少分号,那么在这一行之前进行检查。

语义错误 Semantic Errors

语义错误是在意义上的错误,例如,思考这一句话: Furry inflation thinks greenly. 语法上是正确的,因为形容词、名词、动词、还有副词都在合适的位置上,但这句话就是一派胡言。在C语言中,当你遵循了C语言的规则但是导致了不合适的结果,这个例子中就有这样的错误。

1
n3 = n2 * n2;

n3本应该是用来展现n的3次方的,结果程序把它算成了n的4次方。编译器是检测不出来语义错误的,因为它们并不违背C语言的规则。编译器没有能力去猜测你程序的意图。那也就只能由你来找到这些种类的错误了。一种方式是把程序做的事情与程序应该做的事情进行对比。例如,假定你已经找到了例子中的语法错误,那么它现在看起来是这个图2.5样子的。

还是错的...

它的输出是

1
n = 5, n squared = 25, n cubed = 625 

如果你可以口算立方的话,那么你就会注意到625是个错误的值。下一阶段就是找到你是如何陷入这个错误的。例如,你有可能会通过观察找到错误。大体上,然而你还需要一些系统的方法。一种方法是假装你是电脑,然后一步步地跟着程序走,让我们现在就尝试一下那个方法。

程序体以声明三个变量n1, n2与n3开始,你可以通过画三个盒子并把它们各自标注名称来模拟这个情况。(看图2.6)然后,程序会把5赋给n,通过把数字5写在盒子里来模拟,然后,程序用nn然后把它的值赋给n2,所以看向盒子n,看着5乘5得到了25,然后把25放在盒子里。为了模拟下一句,也是同样,看向盒子2,然后用25乘25,然后把这个值给予n3,啊哈,你做乘法的时候使用的是n的平方而不是用n

嗯,有可能这个过程对于这个程序有点过了,但是以这种方式一步步地浏览程序总会是得知发生了什么最好的方法。

程序状态

通过手工地一步步地追踪程序,追踪每一个变量,你监视了程序的状态,程序状态就是所有变量在一个时间点上被给予的一组值。它是运算状态的快照。

追踪程序状态

我们刚刚讨论了一种追踪程序状态的方法:通过自己一步一步地运行程序。在一个进行1000次迭代的程序中,你可能觉得并不能完成那个工作。然而,你还是可以经历一些迭代过程,看看你的程序是不是按你想要的方式来运行的。然而,通常是会有这种情况的,你会按照你想要的方式去“执行”它们,而不是真正把它们写下来,所以在写真正的代码的时候要努力做到正确。

另一个方法就是通过在程序的自始至终放置几个printf语句来定位语义问题。在程序运行的关键点监测选定变量的数值。看看数值是怎么变化的可以让你知道正在发生什么。在你让程序按照你满意的方式运行之后,你就可以去掉多余的语句去重新编译了。

第三种监测的方法就是使用调试器。Debugger是一种能够让你一步一步地运行程序并且查看程序变量的值的程序。它们使用的难易程度与复杂度都各有不同。更高级的编译器可以显示哪一行代码正在被运行。这对于多路径的程序是非常方便的,因为可以很容易地看到走的是哪一条路径。如果你的编译器中有调试器,那么花谢时间学学则呢么使用吧。比如像图2.4那样尝试使用它。

2.8 关键字与内置标识符

关键字就是C语言中的词汇,因为它们对于C语言是特别的,所以你不能把它们作为标识符来使用。有一些关键字分别了多种多样的类型,比如说int,其他的比如说if是被用来控制程序运行语句的顺序的。在以下的C语言关键字的列表中,黑体表示在ISO/ANSI C90标准下的关键字,斜体的是C99标准加入的新关键字

关键字

如果你尝试把关键字用于变量的名字,编译器会把它识别为语法错误,还有其他的标识符,它们叫做内建标识符,你也是不应该使用的,他们不会导致语法错误,因为它们是合法的名称,但是语言已经使用过或者还保留着使用它们的权利,所以如果你使用这些标识符来表示其他的东西会引起问题。内置的标识符包括以下划线开始的词还有库函数的名称,就像printf().

关键概念

电脑编程是一项有挑战性的活动,它需要复杂与概念化的思考,而且还需要对于细节格外注意。你会发现编译器会强制你去注意细节。当你对朋友说话的时候,你可能会把几个词用错,犯几个语法错误,可能话没有说完,但是你的朋友仍然能够理解你在说什么。但是编译器不会允许这种情况。对于编译器来说,几乎对了也是错的。

编译器并不能在概念问题帮到你多少,比如在这里说的这些东西。所以这本书会尽力通过给出每一章关键概念的大纲来弥合那个差距。

对这一章,你的目标是你需要理解C语言程序是什么,你可以认为程序就是你对于想让电脑干什么的描述。编译器精细化地解决把你的描述变为含蓄的机器语言。(对于编译器做多少工作的测量显示,它可以从你1KB的源码文件创造出来60KB的可执行程序)即使一个简单的C语言程序也需要很多的机器语言来代表它们。)因为编译器并没有智能,你必须使用编译器的词汇来描述你的意思,而且这些词汇是由C语言标准设立的正规规则。(即使很受限制,还是比直接用机器语言来表达强的多了!)

编译器期望着收到以独特的形式写出的指令,这一章已经详细地描述了。你作为编程人员的工作就是表达在编译器框架下你对于程序应该如何运作的想法-由C语言标准所引导就可以正确地运行。

回顾问题

  1. Indiana Sloth准备了如下的程序,让你看一看是不是正确,请帮帮他吧。

问题4

main函数用了{}来括起来,函数体用的是括号,是错的。
声明语句没加分号,赋值只需要一个=号即可。
是printf而不是print。没加双引号,没加引用列表。最后没结束。stdio.h打错了而且#和<>,注释另一边写反了,导致全文为注释。

5.假设下面的每一个例子都是完整程序的一部分,它们每一个会打印什么呢?

问题5

1
2
3
4
5
6
7
8
9
10
11
12
#a.
Baa Baa Black Sheep.Have you any wool?

#b.
Begone!
creature of lard!
#c.
What?
No/nBonzo?

#d.
2 + 2 = 4
  1. 下面哪些是C语言关键字? main, int, function, char, =
    int、char

  2. 你会如何把行数字数打印出来?
    在以下的形式中There were 3020 words and 350 lines.

1
2
3
word = 3020;
line = 350;
printf("There were %d words and %d lines.", word, line);
  1. 思考下面的程序
    问题8
    在第7行之后的程序状态是什么?
    第八与第九行呢?

第7行 a = 5,b = 2

第8行 a = 5, b = 5

第9行 a = 5, b = 5

编程训练

只是阅读是不够的,你得尝试写一两个简单的程序来看看是不是写程序和你看这一章的感觉一样流畅。下面有一些建议,但是你也确实需要独立思考一些问题。你会在网站上找到下面选出问题的答案。

  1. 调用一次printf,把你的姓与名在一行中输出
    再调用一次printf,把你的姓与名在两行中输出。
    使用两次printf把你的姓和名在一行中输出。
1
2
3
4
5
//简略一写
printf("Mike Smith\n");
printf("Mike\nSmith\n");
printf("Mike");
printf("Smith\n");
  1. 写一个打印你的姓名与地址的程序
1
2
3
4
5
6
7
8
#include <stdio.h>

int main(void)
{
printf("My name is Ywx\n");
printf("Address: Zhengzhou city in Henan province");
return 0;
}
  1. 写一个把你的年龄转换成天数的程序,展示两者的值。不必关心闰年和平年。
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main(void)
{
int age, days;
scanf("%d", &age);
days = age * 365;
printf("%d %d", age, days);
return 0;
}

4.写一个会打印出以下语句的程序

语句

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(void)
{
for(int i = 0;i < 3;i++)
printf("For he's a jolly good fellow!\n");
printf("Which nobody can deny!");
return 0;
}

5.写一个程序,创建一个叫做toes的变量,把toes设为10,
计算toes的二倍和平方分别是什么,并把它们打印下来

1
2
3
4
5
6
7
8
#include <stdio.h>

int main(void)
{
toes = 10;
printf("%d %d %d", toes, toes * 2, toes * toes);
return 0;
}

6.写一个会打印出如下的输出的程序

Smile!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main(void)
{
for(int i = 3;i >= 1;i--)
{
for(int j = 0;j < i;j++)
{
printf("Smile");
}
putchar('\n');
}
return 0;
}

7.写一个叫做one_three()的函数,这个函数应该在一行内展现文字one然后调用函数two(),接着在另一行内显示文字three
two()把文字two在一行输出。以Stating now开始,像这样。

数数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

void two()
{
printf("two\n");
}

void one_three()
{
printf("one\n");
two();
printf("three\n")l
}

int main(void)
{
printf("starting now:\n");
one_three();
return 0;
}

End…

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2023-2024 大学生暮暖
  • 访问人数: | 浏览次数:

请我喝杯奶茶吧~

支付宝
微信