第一章 C语言概述
1.1C语言的历史
1.1.1起源
C语言是由贝尔实验室的Ken Thompson和Dennis Ritchie等人开发的UNIX操作系统的“副产品”。最初,Thompson独自编写了UNIX操作系统,并在1969年将其运行在DEC PDP-7计算机上,该计算机仅有16KB内存。与其他操作系统一样,UNIX系统最初也是用汇编语言编写的,但这种语言难以调试和改进。因此,Thompson意识到需要一种更高级的编程语言来开发UNIX系统,于是他设计了一种小型的B语言。
Thompson的B语言是基于20世纪60年代中期产生的BCPL系统,而BCPL语言又可以追溯到影响深远的Algol语言。不久之后,Ritchie也加入了UNIX项目,并开始用B语言编写程序。1970年,贝尔实验室为UNIX项目指定了一台PDP-11计算机。当B语言经过改进能够在PDP-11上运行时,Thompson用B语言重新编写了部分UNIX代码。
然而,到了1971年,B语言已经不再适合PDP-11计算机,于是Ritchie开始开发B语言的升级版。最初,他将新语言命名为NB(“New B”),但随着新语言逐渐放弃B语言的特性,他最终将其命名为C语言。到1973年,C语言已经足够稳定,可以用来重新编写UNIX系统。
使用C语言编写程序的一个主要好处是可移植性。只要为其他贝尔实验室的计算机编写C语言编译器,他们的团队就能在那些机器上运行UNIX系统。
1.1.2标准化
- C语言在20世纪70年代(特别是1977年到1979年之间)持续发展。这一时期出现了第一本有关C语言的书。Brian Kernighan和Dennis Ritchie合作编写的The C Programming Language一书于1978年出版,并迅速成为C程序员必读的“圣经”。由于当时没有C语言的正式标准,所以这本书就成为了事实上的标准,编程爱好者把它称为“K&R”或者“白皮书”。
- 在20世纪70年代,C程序员相对较少,而且他们中的大多数人都是UNIX系统的用户。然而,到了20世纪80年代,C语言已不再局限于UNIX领域。运行在不同操作系统下的多种类型的计算机都开始使用C语言编译器。
- 随着C语言的迅速普及,一系列问题也接踵而至。编写新的C语言编译器的程序员都用“K&R”作为参考。但是遗憾的是,“K&R”对一些语言特性的描述非常模糊,以至于不同的编译器常常会对这些特性做出不同的处理。而且,“K&R”也没有对属于C语言的特性和属于UNIX系统的特性进行明确的区分。更糟糕的是,“K&R”出版以后C语言仍在不断变化,增加了新特性并且去除了一些旧的特性。很快,C语言需要一个全面、准确的最新描述开始成为共识。如果没有这样一种标准,就会出现各种“方言”,这势必威胁到C语言的主要优势——程序的可移植性。
- 1983年,在美国国家标准协会(ANSI)的推动下,美国开始制订本国的C语言标准。经过多次修订,C语言标准于1988年完成并在1989年12月正式通过,成为ANSI标准X3.159-1989。1990年,国际标准化组织(ISO)通过了此项标准,将其作为ISO/IEC 9899:1990国际标准[插图]。我们把这一C语言版本称为C89或C90,以区别于原始的C语言版本(经典C)。
- 1995年,C语言发生了一些改变(相关描述参见Amendment 1文档)。1999年通过的ISO/IEC 9899:1999新标准中包含了一些更重要的改变,这一标准所描述的语言通常称为C99。由于存在两种标准,以前用于描述C89的ANSI C、ANSI/ISO C和ISO C等术语现在就有了二义性。
- 由于C99还没有得到普遍使用,并且我们需要维护数百万(甚至数十亿)行的旧版本C代码,本书中我将用一个特殊的图标来标记对C99新增特性的讨论。不能识别这些新增特性的编译器就不是“C99兼容的”。根据以往的经验,至少还要再过几年才能让所有的C编译器都成为C99兼容的(也仅限于那些真正的C编译器)。
1.1.3基于C语言的编程语言
C语言对现代编程语言有着巨大的影响,许多现代编程语言都借鉴了大量C语言的特性。在众多基于C的语言中,以下几种非常具有代表性。
- ·C++:包括了所有C特性,但增加了类和其他特性以支持面向对象编程。
- ·Java:是基于C++的,所以也继承了C的许多特性。
- ·C#:是由C++和Java发展起来的一种较新的语言。
- ·Perl:最初是一种非常简单的脚本语言,在发展过程中采用了C的许多特性。
当然,C语言还是值得一学的,原因嘛,有几个。首先,学了C语言,你就能更容易地理解那些基于C的语言,比如C++、Java、C#和Perl。就像学了基础数学,再学高等数学就容易多了。其次,现在还有一大堆C语言写的程序,你得会读会修这些代码。最后,C语言在新软件开发里还是挺火的,特别是在那些对内存和处理能力要求高,或者需要简单直接的程序的地方。简单来说,C语言就像是编程世界里的老兵,依然能打能拼。
如果大家还没有学习上述任何一种基于C的语言,那么这个专栏的文章将是非常好的预备教材。本专栏文章强调了数据抽象、信息隐藏和其他在面向对象编程中非常重要的原理。C++语言包含了C语言的全部特性,因此大家今后在使用C++语言时可以用到从这里学到的所有知识。在其他基于C的语言中也能发现许多C语言的特性。
1.2 C语言的优缺点
1.2.1 C语言的优点
C语言,就像其他编程语言一样,有它自己的长处和短板。它之所以有这些特点,主要是因为它最初是为编写操作系统和其他底层软件设计的,而且它的设计哲学也影响了它的这些特性。
C语言是种底层语言,它让你能够直接接触到计算机的“内脏”,比如字节和内存地址,这些是很多其他语言都尽量隐藏起来的东西。C语言还和计算机的底层指令紧密相连,这样你的程序就能跑得飞快。因为操作系统需要处理输入输出、内存管理等等,所以它可不能慢吞吞的。
C语言也很小巧。和其他语言比起来,C语言提供的特性不多,就像K&R那本书里只用了49页就把C语言讲完了。因为它特性少,C语言就依赖一个标准库来扩展功能,这个库里面有很多“函数”,在其他语言里可能叫做“过程”或者“方法”。
C语言还很自由。它假设你知道自己在做什么,所以给你很多自由去发挥。它不像其他语言那样,会有很多严格的错误检查。
C语言的这些优点,也解释了为什么它这么受欢迎。
首先,它很高效。C语言设计出来就是为了写那些以前用汇编语言写的程序,所以它在有限的内存里跑得快是它的一大优势。
C语言的真正优势在于它的可移植性。这意味着C语言编写的程序可以在不同的环境中运行,而不需要太多的修改。这种特性使得C语言非常适合跨平台开发,无论是在个人电脑、服务器还是嵌入式系统中。C语言的可移植性得益于它没有分裂成多种不兼容的版本,这要归功于早期与UNIX系统的紧密结合以及后来的ANSI/ISO标准。此外,C语言编译器的广泛可用性也促进了这种可移植性。所以,C语言的这种特性让它在软件开发中非常有用,尤其是当你需要在多种不同的系统上运行程序时。
功能强大。它有很多数据类型和运算符,让你可以用很少的代码实现很多功能。
也很灵活。虽然它最初是为系统编程设计的,但它没有被限制在这个范围内。C语言可以用来写从嵌入式系统到商业数据处理的各种程序。C语言在使用上的限制很少,很多在其他语言里不允许的操作,在C语言里都是允许的。比如,C语言允许你把一个字符和一个整数或者浮点数相加。虽然这种灵活性可能会让一些错误逃过检查,但它确实让编程变得更简单。
C语言还有一个突出的优点,就是它的标准库。这个库里面有几百个函数,可以用来做输入输出、字符串处理、内存分配和其他实用操作。
最后,C语言和UNIX系统的结合特别紧密,包括大家都知道的Linux。实际上,一些UNIX工具甚至假设你懂C语言。
1.2.2 C语言的缺点
C语言的确有一些缺点,这些缺点往往和它的优点是同根同源的,主要是因为C语言和硬件的紧密结合。
-
错误隐藏性:C语言的灵活性是双刃剑。它让编程出错的可能性更高,而且很多错误在编译时是发现不了的。这和汇编语言很像,很多问题要等到程序运行起来才能暴露。更糟糕的是,C语言里还有很多小陷阱,比如多一个分号可能就导致无限循环,少一个引用符号&可能导致程序崩溃。
-
代码可读性差:尽管C语言本身不算复杂,但它有一些特性很容易让人混淆。这些特性可以以多种方式组合使用,有时候编程者自己明白,但其他人可能就看不懂了。另外,C语言的简洁性也是个问题。在人机交互还很原始的时候,设计者为了让输入和编辑程序更快捷,特意让C语言简洁。但这也可能导致过于聪明的程序员写出除了他们自己谁也看不懂的代码。
-
代码难以维护:如果一开始没有考虑到后期维护,用C语言写的大型程序会很难修改。现代编程语言通常会提供类和包这样的特性,帮助把大程序拆分成许多小模块,便于管理。但C语言没有这些特性。
-
代码阅读难度大:即使是C语言的忠实粉丝也不得不承认,C代码有时候很难读懂。比如每年举行的国际模糊C代码大赛,就是鼓励人们写出最难懂的C程序。比如1991年的最佳小程序,是一段解决八皇后问题的代码,由Doron Osovlanski和Baruch Nissenbaum编写。这段代码不仅外人看不懂,甚至可能连作者自己过段时间也看不懂了。
简单来说,C语言的这些缺点提醒我们,虽然它功能强大、灵活,但在使用时也需要格外小心,特别是在代码的编写和维护上。
即使是那些最热爱C语言的人也不得不承认C代码难以阅读。每年一次的国际模糊C代码大赛(International Obfuscated C Code Contest)竟然鼓励参赛者编写最难以理解的C程序。获奖作品着实让人感觉莫名其妙。例如,1991年的“最佳小程序”如下:
v,i,j,k,l,s,a[99];
main()
{
for(scanf("%d",&s);*a-s;v=a[j*=v]-a[i],k=i<s,j+= (v=j<s&&
(!k&&!!printf(2+"\n\n%c"-(!l<<!j)," #Q"[l^v?(l^j)&1:2])&&
++1||a[i]<s&&v&&v-i+j&&v+i-j))&&!(1%=s),v||(i==j?a[i+=k]=0:
++a[i])>=s*k&&++a[--i]);
}
这个程序是由Doron Osovlanski和Baruch Nissenbaum共同编写的,其功能是打印出八皇后问题(此问题要求在一个棋盘上放置8个皇后,使得皇后之间不会出现相互“攻击”的局面)的全部解决方案。事实上,此程序可用于求解皇后数量在4~99范围内的全部问题。
1.2.3如何高效的使用C语言
高效使用C语言,确实需要在享受其优点的同时,注意规避它的缺点。这里有一些实用的建议:
- 了解并规避C语言的缺陷:现代编译器虽然能发现一些常见问题并发出警告,但它们无法捕捉到所有潜在的问题。因此,程序员需要对C语言的缺陷有所了解,并学会如何规避它们。
- 使用软件工具增强程序的可靠性:C程序员经常使用各种软件工具来帮助编写更可靠的代码。例如,`lint`是UNIX系统中广泛使用的C语言工具,它能进行比编译器更全面的代码检查。如果可能的话,使用`lint`或类似的工具是个好主意。此外,调试工具也是必不可少的,因为许多C语言的错误在编译时无法发现,它们可能以运行时错误或不正确的输出形式出现。
- 利用现有的代码库:C语言的一个好处是它有着庞大的用户基础和丰富的代码库。重用他人的代码不仅可以减少错误,还可以节省大量的编程时间。C代码常被组织成库,即函数的集合。这些库广泛用于执行常见任务,如用户界面开发、图形学、通信、数据库管理和网络等。有些库是公共的,有些是开源的,还有些是商业销售的。
- 采用一套实际的编码规范:编码规范是一套设计风格准则,即使语言本身没有强制要求,程序员也选择遵守。一套精心挑选的规范可以使代码更加统一,更易于阅读和修改。对于C语言来说,这一点尤其重要,因为C语言的灵活性可能导致代码难以理解。本书的编程示例遵循一套编码规范,但还有其他一些同样有效的规范可以使用。重要的是要采纳某些规范并坚持使用它们。
- 避免编写“投机取巧”和极度复杂的代码:C语言鼓励使用编程技巧,程序员可能会尝试选择最简洁的方式来完成任务。但是,简洁并不总是最好的,因为最简洁的解决方案往往也是最难以理解的。本书将展示一种既简洁又易于理解的编码风格。
- 紧贴标准:大多数C编译器都提供了超出C89或C99标准的特性和库函数。为了保持程序的可移植性,除非绝对必要,最好避免使用这些非标准的特性和库函数。
总的来说,高效使用C语言需要程序员具备深入的语言理解、良好的工具使用习惯、对现有资源的有效利用、严格的编码规范,以及对标准的遵循。通过这些方法,可以最大限度地发挥C语言的优势,同时减少其潜在的缺点。
第二章 C语言基本概念
2.1编写一个简单的C程序
C语言的确以其简洁著称,与其他语言相比,它不需要太多的“仪式感”。一个C程序可以短小精悍,几行代码就能表达出完整的意图。比如,下面这个名为hello.c
的程序,每次运行时都会打印出一句经典的问候:
#include <stdio.h>
int main(void) {
printf("Hello, world!\n");
return 0;
}
让我们来简单解释一下这段代码的含义:
- 首行
#include <stdio.h>
是必须的,它告诉编译器包含C语言标准输入输出库的声明,这个库提供了输入输出功能。 main
函数是程序的入口点,也就是程序开始执行的地方。printf("Hello, world!\n");
是main
函数中的第一条可执行代码,printf
函数用于向标准输出(通常是屏幕)打印字符串。字符串中的\n
是一个转义字符,它告诉printf
在字符串打印完成后进行换行。return 0;
表示程序正常结束,并返回状态码0给操作系统,通常0表示程序成功执行。
C语言的这种简洁性,使得它在编写小型程序或者需要直接与硬件交互的场合非常有用。不过,简洁也意味着程序员需要对语言有更深入的理解,以避免写出难以维护的代码。
2.1.1 编译和链接
虽然pun.c程序看起来十分简短,但是为运行这个程序而包含的内容是要比想象的要多。首先,需要生成一个含有上述程序代码名为pun.c的文件(使用任何文本编辑器都可以创建该文件)。文件的名字无关紧要,但是编译器通常要求带上文件的扩展名.c。
接下来,就需要把程序转化为机器可以执行的形式。对于C程序来说,通常包含下列3个步骤。
- ·预处理。首先程序会被送交给预处理器(preprocessor)。预处理器执行以#开头的命令(通常称为指令)。预处理器有点类似于编辑器,它可以给程序添加内容,也可以对程序进行修改。
- ·编译。修改后的程序现在可以进入编译器(compiler)了。编译器会把程序翻译成机器指令(即目标代码)。然而,这样的程序还是不可以运行的。
- ·链接。在最后一个步骤中,链接器(linker)把由编译器产生的目标代码和所需的其他附加代码整合在一起,这样才最终产生了完全可执行的程序。这些附加代码包括程序中用到的库函数(如printf函数)。
虽然看起来很复杂,不过不要担心,上述过程往往是自动实现的,事实上,由于预处理器通常会和编译器集成在一起,所以我们甚至可能不会注意到它在工作。
根据编译器和操作系统的不同,编译和链接所需的命令也是多种多样的。在UNIX系统环境下,通常把C编译器命名为cc。为了编译和链接pun.c程序,在终端或命令行窗口输入如下命令:
% cc pun.c
在编译和链接好程序后,编译器cc会把可执行程序放到默认名为a.out的文件中。假设要把文件pun.c生成的可执行文件命名为pun,以下命令可以实现:
% cc -o pun pun.c
GCC编译器是最流行的C编译器之一,它随Linux发行,但也有面向其他很多平台的版本。这种编译器的使用与传统的UNIX cc编译器相似。程序pun.c也可以使用gcc编译器进行编译,命令如下:
% gcc -o pun pun.c
2.1.2 集成开发环境
随着技术的发展,编程的方式也在不断进步。早期的编程确实需要在终端中手动输入编译命令,但现代的集成开发环境(IDE)提供了一个更为便捷和强大的工具集,让开发者可以在单一的应用程序中完成从编写代码到程序运行的全过程。
集成开发环境通常具备以下功能:
- 代码编辑:提供语法高亮、代码补全、代码折叠等编辑功能。
- 编译:集成编译器,可以一键编译代码。
- 链接:在编译后,自动或手动链接生成可执行文件。
- 执行:可以直接在IDE中运行编译后的程序。
- 调试:提供断点、单步执行、变量查看等调试工具。
这些功能极大地提高了开发效率,降低了编程的入门门槛,并且使得代码管理更加方便。现在的IDE种类繁多,例如 9 提到的Clion、Visual Studio、DevC++等,都是流行的C语言IDE选择。它们各自具有不同的特点和优势,开发者可以根据自己的需求和喜好选择合适的IDE。
2.2 简单程序的一般形式
接下来我们来仔细研究一下pun.c程序,归纳出一些通用的程序格式。简单的C程序一般具有如下形式:
-
指令 int main(void) { 语句 }
C语言的确以其简洁和高效的语法著称,这在很大程度上得益于它对缩写词和特殊符号的利用。这种特性使得C语言的代码看起来非常紧凑,但同时也可能使得代码的阅读和理解对初学者来说更具挑战性。下面我们来详细解释一下C语言中的几个关键概念:
-
大括号
{}
:在C语言中,大括号用来定义代码块的范围,这与其他语言中使用begin
和end
来标记代码块的方式非常相似。在C语言中,大括号通常用于定义函数体、循环体、条件语句等的开始和结束。 -
指令:在C语言中,指令通常指的是预处理指令,如
#include
、#define
等。这些指令在编译前就被处理,用于包含头文件、定义宏等,它们对程序的编译过程有着重要的影响。 -
函数:C语言中的函数是一段具有特定功能的代码块,可以被命名并重复调用。每个C程序都至少包含一个
main
函数,它是程序的入口点。函数允许程序员将代码组织成模块,提高代码的重用性和可读性。 -
语句:语句是C语言中执行特定操作的指令。它们在程序运行时被执行,包括赋值、条件判断、循环控制等。C语言的语句以分号
;
结尾,表示语句的结束。
2.2.1 指令
-
在编译C程序之前,预处理器会首先对其进行编辑。我们把预处理器执行的命令称为指令。
-
程序pun.c由下列这行指令开始:
-
#include <stdio.h>
-
-
-
这条指令说明,在编译前把<stdio.h>中的信息“包含”到程序中。<stdio.h>包含了关于C标准输入/输出库的信息。C语言拥有大量类似于<stdio.h>的头(header),每个头都包含一些标准库的内容。这段程序中包含<stdio.h>的原因是:C语言不同于其他的编程语言,它没有内置的“读”和“写”命令。输入/输出功能由标准库中的函数实现。
-
所有指令都是以字符#开始的。这个字符可以把C程序中的指令和其他代码区分开来。指令默认只占一行,每条指令的结尾没有分号或其他特殊标记。
2.2.2 函数
在编程的世界里,函数就像是乐高积木,一块块拼凑起来,构建出整个程序的宏伟大厦。C语言的精髓,就在于这些灵活多变的函数。它们分为两大家族:一类是我们亲手编写的函数,另一类则是C语言自带的库函数,这些库函数就像是编译器赠予我们的一份厚礼。
说到“函数”,这个词其实源自数学,它描述的是输入某些值,按照特定规则得到结果的过程。而在C语言的王国里,函数的定义更加宽泛。它们是一组有序的语句,冠以一个名字,可以是计算数值的,也可以执行其他任务。当函数完成其使命,通过return
关键字,它将结果传递回召唤它的地方。比如,一个简单的函数,可能会对输入的数字进行加一操作:
return x + 1;
或者,更复杂一些,计算两个数的平方差:
return y * y - z * z;
一个C语言的程序可以由多个函数组成,但永远只有一个main
函数,它是程序的心脏。系统在启动程序时,会自动寻找并执行main
函数。在后续的章节中,我们将探索如何定义和使用其他函数,但在那之前,我们的每个程序都仅有一个main
函数。
main
函数的名字具有特殊的意义,它必须保持原样,不能被替换为begin
、start
或者任何其他变体,甚至连大小写变换如MAIN
也是不被允许的。那么,如果main
是一个函数,它是否也会返回一个值呢?答案是肯定的。当程序运行结束时,main
函数会向操作系统报告一个状态码,表明程序的执行状态。例如,在pun.c
程序中:
#include <stdio.h>
int main(void)
{
printf("To C, or not to C: that is the question.\n");
return 0;
}
这里的int
表明main
函数将返回一个整数。而void
则说明main
函数不接受任何参数。return 0;
语句在这里有两个作用:一方面它标志着main
函数的结束,另一方面它告诉操作系统,程序已经顺利完成,返回值为0。
如果main
函数的结尾没有return
语句,程序依然能够结束,但很多编译器会发出警告,因为按照函数的定义,它应该返回一个整数,却没有做到。
通过这样的方式,C语言以其独特的语法和结构,将函数的力量和灵活性展现得淋漓尽致。
2.2.3 语句
在C语言的舞台上,语句扮演着执行动作的主角。以pun.c
这个小程序为例,它仅使用了两种类型的语句:一种是return
语句,它标志着函数的使命完成并返回结果;另一种是函数调用语句,它唤醒另一个函数,赋予其执行特定任务的使命。
在这个例子中,printf
函数被召唤来完成显示信息的任务。它接收一个字符串作为参数,并将其呈现在屏幕上:
printf("To C, or not to C: that is the question.\n");
在C语言的规则里,每个语句都必须以分号;
作为结束标志。由于语句可能会跨越多行,使用分号帮助编译器明确地识别出语句的终结。这就像是给语句划定了边界,让编译器知道何处开始,何处结束。然而,预处理指令通常简洁地占据单行,因此它们不受此规则的约束,无需分号来标记结束。
分号在C语言中的作用至关重要,它是语句完整性的象征,确保了程序的清晰和准确。正如诗人用句号结束诗句,程序员用分号为每条语句画上完美的句点。
2.2.4 显示字符串
printf是一个功能强大的函数。当用printf函数显示字符串字面量时,最外层的双引号不会出现。
当显示结束时,printf函数不会自动跳转到下一输出行。为了让printf跳转到下一行,必须在要显示的字符串中包含\n(换行符)。写换行符就意味着终止当前行,然后把后续的输出转到下一行。为了说明这一点,请思考把语句
printf("To C, or not to C: that is the question.\n");
替换成下面两个对printf函数的调用后所产生的效果:
printf("To C, or not to C: "); printf("that is the question.\n");
第一条printf函数的调用显示出To C, or not to C:,而第二条调用语句则显示出that is the question.并且跳转到下一行。最终的效果和前一个版本的printf语句完全一样。
换行符可以在一个字符串字面量中多次出现。显示下列信息的代码可以这样写:
Brevity is the soul of wit. --Shakespeare
printf("Brevity is the soul of wit.\n --Shakespeare\n");
2.3 注释
在编写程序时,记录关键信息是一种良好的编程习惯,这有助于未来的代码维护和理解。这些信息包括程序名称、编写日期、作者、用途等。在C语言中,我们使用注释来存放这些元信息。注释是给阅读代码的人看的,编译器在编译过程中会忽略它们。
在C语言中,多行注释以/*
开始,以*/
结束。例如:
/* This is a comment */
注释几乎可以出现在程序的任何位置上。它既可以单独占行也可以和其他程序文本出现在同一行中。下面展示的程序pun.c就把注释加在了程序开始的地方:
/* Name: pun.c */
/* Purpose: Prints a bad pun. */
/* Author: K. N. King */
#include <stdio.h>
int main(void) {
printf("To C, or not to C: that is the question.\n");
return 0 ;
}
注释还可以占用多行。一旦遇到符号/*,那么编译器读入(并且忽略)随后的内容直到遇到符号*/为止。还可以把一串短注释合并成为一条长注释,单独把*/符号放在一行会比较直观:
/* Name: pun.c Purpose: Prints a bad pun. Author: K. N. King */
更好的方法是用一个“盒形”格式把注释单独标记出来。使用这种格式,注释被清晰地框定起来,形成一个视觉隔离区,使得阅读者可以迅速识别出注释内容。这是一种常见的实践,特别是在较长的代码文件中,有助于保持代码的整洁和可读性。
/********************************************************** *
Name: pun.c * * Purpose: Prints a bad pun. * * Author: K. N. King *
**********************************************************/
2.4 变量和赋值
在C语言中,变量扮演着存储和操作数据的角色,它们是程序执行过程中的临时存储单元。正如大多数编程语言一样,变量允许我们保存计算结果、用户输入或其他任何需要在程序中使用的信息。
变量相当于程序中的“容器”,每个容器都有一个名字,我们称之为变量名,它用于在程序中引用该变量。变量还必须有类型,这决定了变量可以存储的数据类型(如整数、浮点数、字符等)以及它在内存中占用的空间大小。
以下是一些C语言中使用变量的基本示例:
-
声明变量:声明一个变量以告知编译器需要为该变量分配内存空间。
int age; // 声明一个整数变量 age float height; // 声明一个浮点数变量 height char initial; // 声明一个字符变量initial
-
初始化变量:在声明变量时同时赋予它一个初始值。
int age = 25; // 声明并初始化age变量 float height = 1.75;// 声明并初始化height变量 char initial = 'J'; // 声明并初始化initial变量
-
使用变量:在程序中使用变量来存储和操作数据。
age = 30; // 给age变量赋新值 height += 0.05; // 给height变量增加一个值 initial = 'K'; // 改变initial变量的值
-
打印变量:使用
printf
函数输出变量的值。printf("Age: %d, Height: %f, Initial: %c\n", age, height, initial);
变量的使用是编程中的基础,它们是程序逻辑构建和数据处理不可或缺的部分。在编写程序时,合理地声明和使用变量,可以使程序更加清晰、有效和易于维护。
2.4.1 类型
在C语言的多彩世界里,变量是构建程序的基石,而变量的类型则是它们的灵魂。类型决定了变量能够存储的数据种类,以及我们能够对这些数据执行的操作。让我们聚焦于两种基础但极其重要的类型:int
和float
。int
类型,即整数型,它专门用来存储不带小数部分的数值。这些数值可以是正的,如1
或392
,也可以是负的,如-2553
。然而,int
类型的取值范围是有限的。在大多数现代计算机上,一个int
可以存储的最大值是2147483647
,但这个上限在某些系统上可能会低一些,比如32767
。
另一方面,float
类型,即浮点型,它能够存储的数值范围远远超过int
类型,并且能够表示小数。这意味着float
类型可以存储如379.125
这样带有小数位的数值。不过,float
类型也有它的局限性。
首先,float
类型的算术运算通常比int
类型慢,这是因为处理小数点后的数据需要更多的计算资源。其次,由于浮点数的表示方式,float
类型的数值往往只能近似表示实际数值。例如,如果你尝试存储0.1
,实际上在内存中它可能被近似存储为0.09999999999999987
,这种舍入误差是浮点数计算中不可避免的一部分。
选择合适的类型对于编写高效、准确的程序至关重要。理解每种类型的特性和限制,能够帮助我们更好地设计程序和处理数据。
2.4.2 声明
在使用变量之前必须对其进行声明(为编译器所做的描述)。为了声明变量,首先要指定变量的类型,然后说明变量的名字。我们先来声明变量height和profit:
int height;
float profit;
第一行声明height是一个int型变量,这也就意味着变量height可以存储一个整数值。第二行声明profit是一个float型变量。如果几个变量具有相同的类型,就可以把它们的声明合并:
int height, length, width, volume;
float profit, loss;
注意每一条完整的声明语句都要以分号结尾。当main函数包含声明时,必须把声明放置在语句之前,我们建议在声明和语句之间留出一个空行。
int main(void) {
声明
语句
}
2.4.3 赋值
变量通过赋值(assignment)的方式获得值。例如 把数值8、12和10分别赋给变量height、length和width,8、12和10称为常量(constant)
height = 8;
length = 12;
width = 10;
变量在赋值或以其他方式使用之前必须先声明。
int height;
height = 8;
但下面这样是不行的,变量的使用必须先声明
height = 8;
/*** WRONG ***/
int height;
当我们把一个包含小数点的常量赋值给float型变量时,最好在该常量后面加一个字母f(代表float),不加f会引发编译器的警告。
profit = 2150.48f;
如果要将int型的值赋给int型的变量,将float型的值赋给float型的变量。混合类型赋值(如把int型的值赋给float型变量或者把float型的值赋给int型变量)是可以的,但不一定安全,可能引发数据精度丢失或不符合预期的结果。
一旦变量被赋值,就可以用它来辅助计算其他变量的值:
height = 8;
length = 12;
width = 10;
volume = height * length * width;
/* volume is now 960 */
在C语言中,符号*表示乘法运算,因此上述语句把存储在height、length和width这3个变量中的数值相乘,然后把运算结果赋值给变量volume。通常情况下,赋值运算的右侧可以是一个含有常量、变量和运算符的公式(在C语言的术语中称为表达式)。
2.4.4 显示变量的值
用printf可以显示出变量的当前值。如下的printf调用来实现输出height这个变量的值:
printf("Height: %d\n", height);
占位符%d用来指明在显示过程中变量height的值的显示位置。由于这里在%d后面放置了\n,所以printf在显示完height的值后会自动换行。%d仅用于int型变量。如果要显示float型变量,需要用%f来代替%d。默认情况下,%f会显示出小数点后6位数字。
如果要强制%f显示小数点后p位数字,可以把p放置在%和f之间。
printf("Profit: $%.2f\n", profit);
会输出以下结果,显示小数点后两位数字,当然如果我们要显示小数点后五位数字,将%.2f中的2换成5就好啦。
Profit: $2150.48
C语言没有限制调用一次printf可以显示的变量的数量。为了同时显示变量height和变量length的值,可以使用下面的printf调用语句:
printf("Height: %d Length: %d\n", height, length);
下面,我们来试试写一个程序吧。
【计算矩形的面积和周长】
想象一下,你是一位建筑师,需要快速计算不同房间尺寸的面积和周长。面积和周长是两个基本的几何量度,它们的计算公式非常简单:面积是长乘以宽,周长是长和宽的和的两倍。
在C语言中,我们可以编写一个程序来实现这一功能。下面是这个程序的代码示例:
/* Calculates the area and perimeter of a rectangle */
#include <stdio.h>
int main(void) {
int length, width, area, perimeter;
// 假设房间的长是15英尺,宽是10英尺
length = 15;
width = 10;
// 计算面积和周长
area = length * width;
perimeter = 2 * (length + width);
// 使用printf输出结果
printf("Room dimensions: %dx%d feet\n", length, width);
printf("Area: %d square feet\n", area);
printf("Perimeter: %d feet\n", perimeter);
return 0;
}
这段程序首先定义了两个整型变量length
和width
来存储房间的长和宽。然后,使用这两个变量计算出面积(area
)和周长(perimeter
)。最后,使用printf
函数输出房间的尺寸、面积和周长。程序的输出结果将如下所示:
Room dimensions: 15x10 feet
Area: 150 square feet
Perimeter: 50 feet
这个程序展示了如何使用printf
函数来格式化输出,其中%d
用作整数的占位符。通过这个简单的例子,我们可以看到C语言在实际应用中的直接和有效性。
2.4.5 初始化
当你的程序跑起来的时候,有些变量会自动设置为零,但大多数变量可不会。如果你用到了一个既没有默认值又没有在程序里明确赋过值的变量,那可能会得到一些乱七八糟、没有意义的数字。有时候,这种情况甚至会导致程序崩溃。
为了避免这种麻烦,你可以在声明变量的时候就给它一个初始值。比如,如果你有一个变量height
,你可以这样写:
int height = 8;
数值8是一个初始化式(initializer)。在同一个声明中可以对任意数量的变量进行初始化:
int height = 8, length = 12, width = 10;
注意,上述每个变量都有属于自己的初始化式。在接下来的例子中,只有变量width拥有初始化式10,而变量height和变量length都没有(也就是说这两个变量仍然未初始化):
int height, length, width = 10;
2.4.6 显示表达式的值
printf的功能不局限于显示变量中存储的数,它可以显示任意数值表达式的值。利用这一特性既可以简化程序,又可以减少变量的数量。例如
volume = height * length * width;
printf("%d\n", volume);
可以用以下形式代替:
printf("%d\n", height * length * width);
printf显示表达式的能力说明了C语言的一个通用原则:在任何需要数值的地方,都可以使用具有相同类型的表达式。
2.5 读入输入
我们之前写的那个箱子空间重量计算程序有点简单,它只能算出我们给定尺寸的箱子。现在,我们来升级一下这个程序,让它能根据用户输入的尺寸来计算。
要让用户自己输入数据,我们得用到scanf
函数。这个函数就像printf
函数的反过来,printf
是用来输出的,而scanf
是用来接收用户输入的。这两个函数都要用到格式字符串来决定数据的输入输出样子。
比如说,如果你想用scanf
来读取一个整数,你可以这么写:
scanf("%d", &i); /* 读取一个整数,存到变量i里 */
这里的%d
就是告诉scanf
我们期待的是一个整数。变量i
就是用来存放用户输入的整数的。
如果你想读取一个小数(浮点数),scanf
的调用就得稍微变一下:
scanf("%f", &x); /* 读取一个小数,存到变量x里 */
这里的%f
就是告诉scanf
我们期待的是一个可以有小数点的数。
下面是一个改进版的程序,用户可以自己输入箱子的尺寸:
/* 根据用户输入的尺寸计算箱子的空间重量,并检查输入有效性 */
#include <stdio.h>
int main(void) {
int height, length, width, volume, weight;
int valid_input;
// 循环直到用户输入有效的高度
do {
printf("请输入箱子的高度(英寸):");
while(scanf("%d", &height) != 1) {
printf("无效输入,请输入一个整数。\n");
while(getchar() != '\n'); // 清除错误的输入
}
} while (height <= 0); // 确保高度是正数
// 清除输入缓冲区的剩余部分
while(getchar() != '\n');
// 循环直到用户输入有效的长度
do {
printf("请输入箱子的长度(英寸):");
while(scanf("%d", &length) != 1) {
printf("无效输入,请输入一个整数。\n");
while(getchar() != '\n');
}
} while (length <= 0); // 确保长度是正数
// 循环直到用户输入有效的宽度
do {
printf("请输入箱子的宽度(英寸):");
while(scanf("%d", &width) != 1) {
printf("无效输入,请输入一个整数。\n");
while(getchar() != '\n');
}
} while (width <= 0); // 确保宽度是正数
volume = height * length * width; // 计算体积
weight = (volume + 165) / 166; // 计算空间重量
printf("体积(立方英寸): %d\n", volume);
printf("空间重量(磅): %d\n", weight);
return 0;
}
在这个版本中,程序使用了do...while
循环来确保用户输入的是有效的正整数。如果输入无效(即非数字或小于等于0的数值),程序会输出错误消息,并要求用户重新输入,直到得到有效输入为止。
程序的输出将根据用户的输入而变化,但格式如下:
请输入箱子的高度(英寸):8
请输入箱子的长度(英寸):12
请输入箱子的宽度(英寸):10
体积(立方英寸): 960
空间重量(磅): 6
如果用户输入了无效数据,程序会显示:
无效输入,请输入一个整数。
并要求用户重新输入,直到输入有效为止。这样,程序就更加健壮,能够处理各种用户输入的情况。
2.6 定义常量的名字
当程序含有常量时,建议给这些常量命名。程序dweight.c和程序dweight2.c都用到了常量166。在后期阅读程序时也许有些人会不明白这个常量的含义。所以可以采用称为宏定义(macro definition)的特性给常量命名:
#define INCHES_PER_POUND 166
这里的#define是预处理指令,类似于前面所讲的#include,因而在此行的结尾也没有分号。当对程序进行编译时,预处理器会把每一个宏替换为其表示的值。例如,语句
weight = (volume + INCHES_PER_POUND - 1) / INCHES_PER_POUND;
将变为
weight = (volume + 166 - 1) / 166;
效果就如同在前一个地方写的是后一条语句。此外,还可以利用宏来定义表达式:
#define RECIPROCAL_OF_PI (1.0f / 3.14159f)
当宏包含运算符时,必须用括号把表达式括起来。注意,宏的名字只用了大写字母。
程序 【华氏温度转换为摄氏温度】
下面的程序提示用户输入一个华氏温度,然后输出一个对应的摄氏温度。此程序的输出格式如下:
用户输入:
Enter
Fahrenheit temperature: 212
Celsius equivalent: 100.0
这段程序允许温度值不是整数,这也是摄氏温度显示为100.0而不是100的原因。首先来阅读一下整个程序,随后再讨论程序是如何构成的。
/* Converts a Fahrenheit temperature to Celsius */
#include <stdio.h>
#define FREEZING_PT 32.0f
#define SCALE_FACTOR (5.0f / 9.0f)
int main(void) {
float fahrenheit, celsius;
printf("Enter Fahrenheit temperature: ");
scanf("%f", &fahrenheit);
celsius = (fahrenheit - FREEZING_PT) * SCALE_FACTOR;
printf("Celsius equivalent: %.1f\n", celsius);
return 0;
}
celsius = (fahrenheit - FREEZING_PT) * SCALE_FACTOR;
表示把华氏温度转换为相应的摄氏温度。因为FREEZING_PT表示的是常量32.0f,而SCALE_FACTOR表示的是表达式(5.0f / 9.0f),所以编译器会把这条语句看成
celsius = (fahrenheit - 32.0f) * (5.0f / 9.0f);
在定义SCALE_FACTOR时,表达式采用(5.0f / 9.0f)的形式而不是(5 / 9)的形式,这一点非常重要,因为如果两个整数相除,那么C语言会对结果向下取整。表达式(5 / 9)的值将为0,这并不是我们想要的。最后的printf函数调用输出相应的摄氏温度:
printf("Celsius equivalent: %.1f\n", celsius);
注意,使用%.1f显示celsius的值时,小数点后只显示一位数字。
2.7 标识符
在编写程序时,需要对变量、函数、宏和其他实体进行命名。这些名字称为标识符(identifier)。在C语言中,标识符可以含有字母、数字和下划线,但是必须以字母或者下划线开头。下面是合法标识符的一些示例:
times10 get_next_char _done
接下来这些则是不合法的标识符,反面教材:
10times get-next-char
C语言是区分大小写的;也就是说,在标识符中C语言区别大写字母和小写字母。例如,下列标识符全是不同的:
job joB jOb jOB Job JoB JOb JOB
上述8个标识符可以同时使用,每一个都可以有完全不同的意义。但是还是建议不要这样起名。
因为C语言是区分大小写的,许多程序员都会遵循在标识符中只使用小写字母的规范(宏命名除外)。为了使名字清晰,必要时还会插入下划线:
symbol_table current_page name_and_address
而另外一些程序员则避免使用下划线,他们的方法是把标识符中的每个单词用大写字母开头:
symbolTable currentPage nameAndAddress
C对标识符的最大长度没有限制,所以不用担心使用较长的描述性名字。诸如current_page这样的名字比cp之类的名字更容易理解。
关键字
下图中的所有关键字(keyword)对C编译器而言都有着特殊的意义,因此这些关键字不能作为标识符来使用。注意,其中有5个关键字是C99新增的。
①代表此关键字仅C99有。因为C语言是区分大小写的,所以程序中出现的关键字必须严格按照表2-1所示的格式全部采用小写字母。(C99关键字_Bool、_Complex和_Imaginary例外。)标准库中函数(如printf)的名字也只能包含小写字母。某些可怜的程序员用大写字母录入了整个程序,结果却发现编译器不能识别关键字和库函数的调用。应该避免这类情况发生。
2.8 C程序的书写规范
C语言程序就像是一串一串的符号串起来的,每个符号都是一些字符组成的小团体,它们不能被拆分,也不能改变,否则就失去了原本的意义。这些符号包括变量名、关键字、运算符、标点符号,还有直接写在代码中的文本(比如"Hello, World!"
)。
举个例子,看这行代码:
printf("Height: %d\n", height);
它其实是由7个符号组成的。其中,printf
和height
是变量名,"Height: %d\n"
是一个直接写在代码中的文本,而圆括号、逗号这些都是标点符号。在大多数情况下,这些符号之间加不加空格都行,只要它们之间不会混在一起变成新的东西。比如,intmain
就不行,因为int
和main
是两个不同的符号。下面是一个转换华氏温度到摄氏温度的程序例子:
/* Converts a Fahrenheit temperature to Celsius */
#include <stdio.h>
#define FREEZING_PT 32.0f
#define SCALE_FACTOR (5.0f/9.0f)
int main(void)
{
float fahrenheit, celsius;
printf("Enter Fahrenheit temperature: ");
scanf("%f", &fahrenheit);
celsius = (fahrenheit - FREEZING_PT) * SCALE_FACTOR;
printf("Celsius equivalent: %.1f\n", celsius);
return 0;
}
如果屏幕足够宽,你可以把整个main
函数写在一行里。但是,预处理指令(像#define
和#include
)必须得单独占一行。当然,把所有东西都挤在一起写并不是个好主意。实际上,适当地加一些空格和空行可以让代码更容易读。C语言允许你在符号之间加空格,这些空格可以是空格键、制表键或者回车键打出来的。
就像下面这样,我们可以给运算符两边加空格,让它们更显眼:
volume = height * length * width;
还可以在逗号后面加空格,有些程序员甚至在圆括号两边也加空格。这样可以让代码的层次结构更清楚,比如:
int main(void)
{
float fahrenheit, celsius; // ...
}
这样一看就知道float fahrenheit, celsius;
这一行是main
函数的一部分。
另外,大括号{}
也有它自己的规则。在main()
下面开始一个大括号 {
,结束的时候大括号 }
要单独一行,并且要和开始的大括号对齐。这样,如果你想在函数末尾加一行代码或者删掉一行代码,就很容易找到开始和结束的地方。
最后,记得不要在单个符号里面加空格,比如float
写成fl oat
或者分成两行写,这样编译器会报错。同样,字符串里面加空格是可以的,但是不能把字符串写到两行上,除非用特殊的方式连接。