2012年(16)
分类: C/C++
2012-08-14 20:12:32
|
俗话说知己知彼百战不殆,我们要想学好C语言,做好程序设计,自然首先应该从了解程序设计和C语言开始。
谭老师的书在这一个章节中对程序设计和C语言作了非常详细的介绍,可惜的是,这些介绍都非常官方,当我们看过这些介绍之后,恐怕对到底什么是程序设计?什么是C语言还是没有一个清晰的概念。实际上,我们不需要了解那么多官方的资料,我们只需要掌握这两个概念的本质就可以了。话不多说,且听我这个过来人来给你说说到底什么是程序设计?什么是C语言?
|
关于“什么是计算机程序?”这个问题,谭老师的书解释的是:
“所谓程序,就是一组计算机能识别和执行的指令。”
维基百科上的解释是:
“计算机程序或者软件程序(通常简称程序)是指一组指示计算机或其他具有消息处理能力的装置每一步动作的指令,通常用某种程序设计语言编写,运行于某种目标体系结构上。打个比方,一个程序就像一个用汉语(程序设计语言)写下的红烧肉菜谱(程序),用于指导懂汉语和烹饪手法的人(体系结构)来做这个菜。通常,计算机程序要经过编译和连结而成为一种人们不易看清而计算机可解读的格式,然后运行。未经编译就可运行的程序,通常称之为脚本程序(script)。”
这两种解释都比较官方,其实,通俗地讲,程序就像一个“传令官”,将我们的旨意传达给计算机,让计算机去执行。比如,我们要想写一篇文章,需要让Word.exe这个程序将我们输入的文字记录下来,然后保存或者打印出来;我们要想通过计算机跟他人聊天,就需要QQ.exe这个程序将我们想说的话通过计算机传递给对方。一个程序,可以接受我们下达的指令,然后让计算机去执行。这就是程序最本质的特征。
可是,我们都知道,计算机是不懂得我们人类使用的自然语言的,那么程序是如何将我们的旨意传达给计算机又让它去执行的呢?这里,就需要用到我们接下来要讨论的计算机语言。
|
计算机不懂得我们人类的语言,没有耳朵,它又是如何听懂我们下达的指令的呢?
谭老师对这个问题给出的答案是:“人和计算机交流,也需要解决语言问题。需要创造一种计算机和人都能识别的语言,这就是计算机语言。”这段话给计算机语言下了一个定义,但是我们学习的目的,不仅仅是为了知道什么是计算机语言,而是想通过计算机语言与计算机进行沟通,给计算机下达指令让计算机为我们做事。那么,我们到底又是如何利用计算机语言在计算机和人之间进行沟通的呢?这就是程序这个在人和计算机之间的“传令官”的功劳了。我们通过跟我们人类使用的自然语言比较接近的高级程序设计语言(例如我们常见的C语言、C++语言等等)在程序中表达我们的旨意。而程序则负责调度各种计算机资源(申请内存,执行计算等等)来完成我们下达的旨意。
1.2.A 如何用计算机语言来表达一个程序计算机的天职是接受并执行我们的指令,为我们工作。在计算机世界中,我们要通过程序这个传令官向计算机传达一个命令,必须经过下面的三个步骤:
l 第一步:在源文件中用程序设计语言表达指令
我们(程序员)通过计算机程序设计语言表达我们的指令。通常,我们的指令是记录在某个文件,以源代码的形式出现的。在进行具体操作的时候,根据我们所使用的开发平台,具体的操作步骤可能有所不同。
如果我们是在Windows平台下的Visual C++这个集成开发环境中进行C语言程序的开发,在这一步中,我们首先要创建一个项目。我们可以通过它的“File→New…”菜单命令来打开“新建项目对话框”,然后在其中选择我们的项目类型为“Win32 Console Application”,然后输入项目名称(例如,helloworld)并选择项目文件存放的位置(例如,“C:\sourcecode\”)。填写完成这些项目的基本信息后,我们点击“OK”按钮进入下一步的设置。
在Visual C++中创建一个C语言程序的新项目
在接下来的向导页面中,Visual C++为我们提供了几个项目模板以供选择,为了简便,我们选择“An Empty Project”选项,并点击“Finish”按钮完成项目参数设置,Visual C++将为我们创建一个没有任何文件的空项目。
项目创建完成后,接下来的工作才是创建新的源文件并将其添加到项目中。再次点击“File→New…”菜单项,在弹出的新建文件对话框中,选择文件类型为“C++ Source File”,然后输入文件名(例如,helloworld.c。注意,C语言程序的源文件是以.c为后缀的),最后点击“OK”按钮,Visual C++就会为我们创建一个新的空白的源文件helloworld.c并将其添加到当前项目中。最后,我们在这个空白的新源文件中输入如下代码,让计算机在屏幕输出“Hello World”字符串:
// HelloWorld.c
#include "stdio.h"
int main()
{
// 在控制台窗口输出“Hello World”这个字符串
printf("Hello World");
return 0;
}
这样,我们就完成了一个C语言程序项目的创建和源代码的输入工作。
如果我们的开发环境是Linux\UNIX,这一过程将更加简单。我们只需要使用系统自带的文本编辑工具(例如,vi或者是vim等)创建一个名为helloworld.c的文本文件,并编辑其内容如上即可。
l 第二步:将源代码编译成可执行文件
有了源文件,我们只是在文件中用程序设计语言表达了我们的指令(让计算机输出一段字符串,也就是“printf("Hello World");”),然而,我们在源文件中记录的指令是以高级程序设计语言表达的,这种语言接近于人类使用的自然语言,我们人类可以大致看懂,但是对于计算机来说,恐怕就是天书了。我们这里的编译器,更像一个翻译官,由它负责将我们在源代码文件中以高级程序设计语言表达的指令翻译成计算机可以理解的机器语言,并记录在可执行文件中。
在Windows开发平台下,可执行程序通常是一个扩展名为.exe的文件,而在Linux\UNIX开发平台下,则可以是其他任何的可执行文件。要完成将源文件翻译成可执行文件的这一编译过程,在Windows开发平台下,我们可以在Visual C++中通过执行“Build→Build helloworld.exe”或者是“Build→ Rebuild All”菜单命令来完成。而在Linux开发平台下,则需要调用GCC编译器的编译命令来完成,例如:
gcc helloworld.c –o helloworld
这个命令的意义是,调用GCC编译器,将helloworld.c源文件编译成可执行文件helloworld。
经过这个编译过程后,我们就得到了可执行的helloworld.exe或者是helloworld文件。也就是刚才我们编写的源代码文件变为了:
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
63 79 67 67 63 6A 2D 31 32 2E 64 6C 6C 00 5F 4A
76 5F 52 65 67 69 73 74 65 72 43 6C 61 73 73 65
73 00 00 00 48 65 6C 6C 6F 20 57 6F 72 6C 64 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
哦,对我们来说,这已经是天书了。
l 第三步:计算机执行可执行程序
当计算机执行这个可执行程序时(在Visual C++中,可以方便地通过“Build→Execute helloworld.exe”菜单命令来执行这个程序),这些指令会被调入计算机,虽然我们人类看不懂这些指令,但是计算机却能够看懂,最终将其翻译成“0101”的机器语言,并遵照这些指令执行,完成我们下达的旨意。
010101010001010100001001000100001100101010101001010101001001000100010100101110
用0和1 表示的机器语言就像上面这样。
整个过程,可以用下面这幅图来表示:
一个C语言程序的一生
1.2.B 从汇编语言到高级语言——风格各异的计算机语言在理解了计算机语言的本质以及使用过程之后,我们有必要来了解一下计算机世界中的各种语言。就像现实世界中有很多种语言一样,有广为人知的英语,也有只有几百人知道的土著语言;有写起来方方正正的汉语,也有写起来拐弯抹角的阿拉伯语。不同的语言各具特色,也都有各自不同的应用场景。这一现象在计算机世界中也是同样的。虽然计算机语言可以泛指所有人与计算机交流的语言,包括谭老师介绍的机器语言、符号语言以及高级语言,但是实际上,机器语言和符号语言我们都很少用到,更多的,我们与计算机沟通是通过高级语言,也就是我们在这里要介绍的高级程序设计语言。
在过去的几十年间,大量的计算机程序设计语言被发明、被取代、被修改或组合在一起成为新的计算机语言。经过实践的检验,现在还在业界普遍流行的计算机程序设计语言主要有以下几种:
l 汇编语言
汇编语言(Assembly Language)是面向机器的程序设计语言。它是一种功能很强的程序设计语言,也是可以充分利用计算机所有硬件特性并能直接控制硬件的语言。相对于机器语言,在汇编语言中,用助记符(Memoni)代替操作码,用地址符号(Symbol)或标号(Label)代替地址码。这样用符号代替机器语言的二进制码,就把机器语言变成了汇编语言。于是汇编语言亦称为符号语言。使用汇编语言编写的程序,机器不能直接识别,要由一种程序将汇编语言翻译成机器语言,这种起翻译作用的程序叫汇编程序(也被称作汇编器),汇编程序是系统软件中的语言处理系统软件。汇编程序把汇编语言翻译成机器语言的过程称为汇编,也就对应于高级程序设计语言的编译,而汇编程序也就对应于高级语言的编译程序。
现在主流的汇编器如MASM、TASM等等为我们写汇编程序提供了很多类似于高级语言的特征,比如结构化、抽象等。在这样的环境中编写的汇编程序,有很大一部分是面向汇编器的伪指令,已经类同于高级语言。现在的汇编环境已经如此高级,即使全部用汇编语言来编写Windows的应用程序也不是一件难事,但这并不是汇编语言的长处。汇编语言的长处在于编写高效且需要对机器硬件精确控制的程序。大多数情况下,C语言程序员不需要使用汇编语言,因为即便是硬件驱动这样的底层程序在操作系统中也可以完全用C语言来实现,再加上GCC这样一些优秀的编译器目前已经能够对最终生成的代码进行很好的优化,的确有足够的理由让我们可以暂时将汇编语言抛在一边,放进历史博物馆了。但实际情况是,C语言程序员有时还是需要使用汇编,或者不得不使用汇编,理由很简单:汇编语言精简、高效,同时与库无关。特别是当今越来越流行的嵌入式硬件环境下的开发,首先必然面临如何减少系统大小、提高执行效率等问题,而这正是汇编语言的用武之地。在这一领域,目前正是汇编语言的春天。
l C语言
C语言是我们这本书的主角,在程序设计当中,自然少不了它的戏份。作为一种计算机程序设计语言,它既具有高级语言的面向过程的特点,又具有汇编语言的面向底层的特点。它可以作为操作系统设计语言,编写系统应用程序,也可以作为应用程序设计语言,编写不依赖计算机硬件的应用程序。因此,它的应用范围非常广泛,不仅仅是在软件开发上,而且各类科研都需要用到C语言,具体应用包括单片机以及嵌入式系统开发等。关于C语言,我们稍后还有详细介绍。不要走开,马上就来。
l C++语言
说到C语言,就不得不提到C++语言。单单从它们的名字上,就可以看出它们之间的亲戚关系。C++语言是一种优秀的面向对象程序设计语言,它是在C语言的基础上增加了一些现代程序设计语言的机制(例如面向对象思想、异常处理等)发展而来的,但它比C语言更容易为人们所学习和掌握。C++以其独特的语言机制在计算机科学的各个领域中得到了广泛的应用。相对于C语言的面向过程的设计方法,C++的面向对象的设计思想让它有了一个质的飞跃,使得C++更加适合于对性能要求较高的、大型的复杂系统的开发。
l Java语言
Java语言诞生于1991年,起初被称为OAK语言,Java平台是SUN公司为一些消费性电子产品而设计的一个通用环境。他们最初的目的只是为了开发一种独立于平台的软件技术,而且在网络出现之前,OAK可以说是默默无闻的,甚至差点夭折。但是,网络的出现改变了OAK的命运。从此,Java与网络结下了不解之缘。
要全面地介绍Java,我们需要用到很多定语,它是一种简单的、跨平台的、面向对象的、分布式的、解释执行的、健壮安全的、结构中立的、可移植的、性能优异的、多线程的、动态的高级程序设计语言。
在众多的计算机程序设计语言中,Java确实是一颗耀眼的明星。当1995年SUN推出Java语言之后,全世界的目光都被这个神奇的语言所吸引。如果你希望进行网络相关的开发,Java语言确实是一个不错的选择。
程序设计语言中的刀枪剑戟
除了我们在上面介绍的几种程序设计语言之外,还有很多优秀的程序设计语言我们没有介绍,比如Python、C#、PHP等等。尽管人们多次试图创造一种通用的语言,却没有一次尝试是成功的。各种语言都有自己的特点,也都有自己的应用领域,并没有高低优劣之分。我并不会因为这本书是讲解C语言的,就将C语言吹到天上去了。我们能做的,就是根据各种语言的特点,根据自己的需要,根据自己的应用场景,来选择合适的语言。
|
在这一小节中,谭老师简要地介绍了C语言的发展历程,让我们认识到C语言是一门历史悠久的程序设计语言,从最初的只是为了描述和实现UNIX的需要而设计的一种工作语言,到后来得到业界的广泛认同而逐渐成为一种流行的程序设计语言。它的流行,也证明了其鲜明的特点让它具有了旺盛的生命力,C语言已经流行,并且还将继续流行,它始终是一门应用广泛的主流程序设计语言。(这句话的潜台词是,学习C语言大有“前/钱”途。)
C语言能够受到大家的欢迎和认可,能够在软件业界广泛流行,这不是偶然的,是由它的鲜明的程序设计特点决定的。谭老师总结了C语言的8个特点,从“语言简洁、紧凑,使用方便、灵活”到“运算符丰富”,从“具有结构化的控制语句”到“语法限制不太严格,程序设计自由度大”等等。
没错,这些确实是C语言的特点,但是,真正让C语言能够在众多的程序设计语言中屹立不倒的特点只有两个:
l 接近底层,可以直接对硬件进行操作
严格地说,C语言是一种介于低级程序设计语言(例如,汇编语言)和高级程序设计语言(例如,C++、Java)之间的中级程序设计语言。它把高级语言的基本结构和语句与低级语言的实用性结合起来,这使得程序员可以非常容易地利用C语言直接对计算机的硬件单元位、字节和地址进行操作。这样的特点,决定了C语言在某些需要对硬件进行操作的应用场景下,例如嵌入式系统中,成为程序员们的不二之选 。
l 执行效率高,具有接近汇编的性能
除了汇编语言之外,C语言应该是当今主流程序设计语言中,执行效率最高的程序设计语言了。一般而言,经过编译器优化后的C语言程序,其执行效率只比汇编程序生成的目标代码效率低10%~20%。对于某些对性能要求极高的系统软件,诸如Linux内核、搜索引擎算法,以及大型的科学计算程序等等,恐怕只有C语言才能够胜任。这也决定了C语言在这些领域具有长久的生命力,始终处于一种无可替代的地位。
事物都有两面性,C语言的优点非常突出,但相对于那些高级程序设计语言,其缺点也非常明显。
l 数据封装性不强
C语言的一个重要的缺点就是它对数据的封装性不强,基本上,在程序中我们可以对任何数据进行任意的访问,而不管这个数据是关键的重要数据还是某个无关紧要的临时数据。这一点使得C语言在数据的安全性上有很大缺陷,这也是C语言和C++语言的一大区别。
l 结构化的程序设计语言,无法支持复杂的大型业务型系统的开发
C语言是一种优秀的结构化程序设计语言,可以很好地实现程序代码及数据的相互分离,利于程序的开发和维护。出于性能的需求,它也被用于构建一些底层系统(例如Linux kernel)。但是,在开发一些更加复杂的业务型系统的时候,因为其抽象层次比较低,这样的设计方法却可能让整个项目陷入“需求变化”的深渊,一旦需求发生变化,则可能需要对整个系统的设计进行变更,这使得C语言无法很好地支持复杂的大型系统的开发,极大地限制了C语言的应用。这也是为什么后来出现了面向对象的设计思想以及C++语言。
l C语言语法灵活,难以掌握
C语言的理念与UNIX系统以及Hacker精神是一脉传承的:程序员应该对他自己的行为负责。因此没有在语言层级上做过多的约束。保持语言的强大、简洁、灵活是优先考虑的,而不是对初学者的亲和度。正是因为贯彻着这样的精神,C语言的语法限制不太严格,对变量的类型约束不严格,影响程序的安全性,对数组下标越界不作检查等。因为其灵活性,使得从应用的角度,C语言比其他高级语言较难掌握,这也限制了C语言的应用普及。
优点对缺点,半斤对八两
这样看来,C语言是一种优点和缺点都非常突出的程序设计语言,就像一个有个性的人一样,一面是天使,一面是魔鬼。我们只有对C语言的优点和缺点都有一个全面而清晰的认识,才能真正理解C语言,把握C语言的个性,充分利用好C语言的优点,尽量避免C语言的缺点,让C语言在合适的应用场景下发挥最大的作用。
1.3A C语言与C++不得不说的那点事说到C语言,就不得不说它的继承者——C++语言。众所周知,C++语言是在C语言的基础上,添加了面向对象、模板等现代程序设计语言的特性而发展起来的。两者无论是从语法规则上,还是从运算符的数量和使用上,都非常相似,所以我们常常将这两门语言统称为“C/C++”。虽然因为天然的血缘关系,导致两者非常相似,就像父亲与儿子相貌相似一样,但是,父亲和儿子毕竟是两个不同的各自独立的个体,而C语言和C++语言也同样是两种各自独立、各有其特点的程序设计语言。在对硬件资源的操作上,C语言更加直接,而C++语言则相对温和一些。
除了这些语法规则上的差异之外,两者最本质的差别在于所采用的程序设计方法的不同。C语言是一门结构化程序设计语言,正如谭老师所说,它的重点在于算法和数据结构。C程序的设计首要考虑的是如何通过一个过程(通常表现为函数的形式),对输入(或环境条件)进行运算处理得到输出(或实现对过程或者事务的控制);而对于C++,首要考虑的是如何构造一个对象模型,让这个模型能够契合与之对应的实际问题,这样就可以通过获取对象的状态信息得到输出或实现对过程或事务的控制。
所以C语言与C++语言的最大区别在于它们的用于解决问题的思想方法不一样。之所以说C++比C更先进,是因为“设计”这个概念已经被融入到C++之中,而单就语言本身而言,在C中更多的是算法的概念。C语言可以说是C++语言的一个自治子集。C语言实现了C++语言中的过程化控制及其他相关功能,而在C++语言中的C(我们可以称之为“C+”),相对于原来的C还有所加强,引入了重载、内联函数、异常处理等等新鲜玩意儿,C++更是拓展了面向对象设计的内容,如类、继承、虚函数、模板和容器类等等,使之更加符合现代程序设计的需要。
程序世界中的父与子:虽然相似,却也各不相同
从这些C语言和C++语言的对比来看,C++语言作为C语言的继任者,似乎已经全面超越了C语言,正所谓青出于蓝而胜于蓝。那么我们自然会产生一个疑问,既然C++语言比C语言优秀,为什么我们不直接学习更优秀的C++语言而要学习它的过去时——C语言呢?
毫无疑问,C++是一门优秀的程序设计语言,它吸收了C语言的诸多优点,同时又添加了很多现代程序设计语言的新特性,这也是它为什么能够成为主流的程序设计语言而长期屹立不倒的原因。但是,这个世界上没有万灵丹,任何语言都不是万能的,C++也并不是适合于所有应用场景。在某些方面,C语言有着比C++语言更大的优势。很多C++不能完成的任务,C语言可以轻松完成。例如在NVIDIA CUDA计算系统上,由于操作系统ABI特性的限制以及其特殊架构的执行单元,无法支持C++初始化的任务,最终只实现了C语言,过于复杂的C++最终没有实现。很多嵌入式开发系统,都只是提供了C语言的开发环境而并没有提供C++的开发环境;很多C++语言不愿意干的脏活累活,C语言干起来快活得很。例如某些对性能要求极高的大型系统,诸如搜索引擎算法、银行金融系统等等,只有勤快的C语言能够出色地完成这些任务,而C++因为过于复杂,在这方面就稍逊一筹了。
正是因为C语言兼顾了接近底层与更高性能的特性,同时语言又比C++语言更加简洁,使得它自从上世纪70年代被创建以来,一直都是一门主流的程序设计语言,受到广大程序员的钟爱。而随着当今世界嵌入式开发的发展、大型算法应用,特别是搜索引擎、云计算的兴起,C语言的用武之地不但没有萎缩,反而有逐渐扩展的趋势。所以,如果你有意向这些领域发展,C语言是你的不二之选。
老当益壮的C语言
综合起来看,对C语言和C++语言的学习可以相互促进。学好C语言,可以为我们将来进一步地学习C++语言打好基础,而C++语言的学习,也会促进我们对于C语言的理解,从而更好地运用C语言。
1.3B 我们为什么现在还要学习C语言在上面小节中,通过C语言和C++语言的对比,我们更加清楚地知道了C语言的面向底层和更高性能的特点,这使得它在众多的程序设计语言中独树一帜,是一门优秀的程序设计语言。但是,并不是C语言优秀,就意味着我们一定要学习它。世界上优秀的程序设计语言多了去了,为什么我们要选择C语言来学习呢?只有认清了这个问题,我们的学习才会更有动力,才会事半功倍。
回归到我们为什么要学习程序设计这个最基本的问题,如果我说我是因为爱好程序设计,想要为程序设计奋斗终生而学习程序设计,恐怕难以得到所有人的认可。事实上,我们学习程序设计,想要从事软件行业,很多时候是想找一份理想的工作,好养活老婆孩子。既然我们选择了程序设计行业,那么相应的程序设计语言的选择就至关重要了。正所谓男怕入错行,女怕嫁错郎。选择一门正确的程序设计语言,可以让我们如鱼得水,左右逢源,而错误地选择了程序设计语言,恐怕就会让我们处处碰壁,职业生涯前途堪忧。而正是C语言广阔的市场前景、优厚的薪酬待遇,让我们在众多的程序设计语言中,慧眼独具地选择它来作为自己的开发语言。当你拿起这本书开始学习C语言的时候,我们应该为自己感到庆幸,因为我们作出了正确的选择,不仅仅是选择了C语言,更是选择了这本书。
l 应用广
在程序设计界有一个著名的TOBIE编程语言排行榜,它是某种编程语言流行趋势的一个晴雨表。而在这个由全世界的程序员、第三方厂商选出的编程语言排行榜上,作为编程语言之林的常青树,C语言的市场份额10年来长期保持在15%至20%之间。在某些时候,还能够位于这份榜单的榜首位置,成为最热门最受欢迎的编程语言。
这份榜单也从一个侧面反映了C语言在当今软件业界的应用范围之广,其受欢迎程度丝毫不逊色于后起的Java以及C#等。C语言不是最时髦的编程语言,但却是不折不扣的最受欢迎的、应用最广泛的编程语言。
C语言长期名列前茅
l 机会多
C语言的应用范围广泛,意味着它提供给我们的工作机会也相对较多。虽然从总量上来看,Java和C#等新兴语言的工作机会更多,但是,学习这些语言的程序员也太多。据说,如果从楼上往下扔一块砖头,砸到十个程序员,有五个是学Java的,有四个是学C#的,只有一个是学C语言的。所以平均下来,C语言程序员的工作机会,要比其他语言的程序员的工作机会多很多。即使你在一些招聘网站,使用C语言作为关键词搜索得到的结果也会比其他语言多。学习C语言,也就意味你将不再为你将来的工作机会发愁。只要你学好了C语言,工作机会多得是。
l 薪酬高
尽管对于顶尖高手来说,达到了编程思想、方法论层面的炉火纯青,可鸟瞰一切平台和编程语言,但对大多数初涉软件行业的程序员来说,选择哪种编程语言和开发工具往往直接决定了当下的收入水平和生活水准。选择学习和使用C语言,除了可以获得更多的工作机会之外,更加诱人的一个原因是,C语言程序员的薪酬普遍较高。从一份来自CSDN的关于程序员薪酬的调查报告来看,使用C语言的程序员收入较低(不到2000元)的比率相对较少,而更多的是收入较高(5000元及以上)的群体,这个比率达到了62.5%,比所谓的编程语言新贵C#高出了整整16个百分点。即使对比同样风头正劲的C++,也高出了接近2个百分点。这一统计结果表明,市场对于C语言以及使用C语言的程序员都是认可的。学习和使用C语言,大有“钱”途。
学习和使用C语言大有“钱”途
综合起来看,不仅C语言的应用范围非常广泛,就业机会更多,同时相对而言薪酬水平也较其他编程语言更高。而这些,都是我们为什么现在还要学习和使用C语言的原因。我们有理由拒绝吗?
|
无论是学习任何编程语言,我们基本上都是从一个最简单的HelloWorld程序开始的,可以说,HelloWorld程序恐怕是这个世界上最普通最简单的一个程序了,同样她也是程序员们的初恋。
(HelloWorld程序最早出现在Brian W. Kernighan和Dennis M. Ritchie所著的《The C Programming Language》一书中。后因C语言的广泛使用而逐渐成为各种程序设计语言中最基本、最简单的程序,因而亦通常是初学者所编写的第一个程序。同时,它还可以用来验证该语言的编译器、程序开发环境,以及运行环境是否已经安装配置妥当,是否可以开始我们的学习开发之旅。)
为了学习C语言,让我们先来看看谭老师是如何向我们介绍这位初恋情人的。
1.4.A 最简单的C语言程序背后的故事——它的汇编代码是如何被执行的在这个小节中,谭老师列出了C语言中比较简单的三个小程序,分别实现了简单的输出、简单的加法运算,以及数据的输入输出和对数据的处理(比较获得两个数据中较大的一个)。虽然谭老师对这些程序作了详细的解释,但是我们心中可能还是有很多疑问:一个C语言程序为什么从main()函数开始?它到底是如何执行的?要想获得自由,我们必须知道事情的真相。我们现在就来学一次庖丁解牛,将一个C语言程序分解开,看看它背后隐藏的秘密。
在书中,我们首先接触到的最简单的C语言程序是:
#include
int main()
{
printf("This is a C program. \n");
return 0;
}
这个C语言程序只有短短的7行代码,实现的也只是简单地向屏幕输出一个字符串,但是别小看这个简单的C语言程序,在它的背后,也有着同样精彩的故事。
在Visual C++ 6.0调试模式下的汇编视图(Disassembly)中,我们可以清楚地看到每个C语言程序背后的故事。在汇编视图中,我们可以看到C语言程序中的各条语句所对应的汇编代码。这下,各条语句做了什么事情、各个功能是如何实现的,都一目了然了。C语言程序语句所对应的汇编语句,反映了C语言程序语句操作硬件的实质,也就是C语言程序背后的故事。这个号称最简单的程序虽然只是简单地输出一个字符串,但是当我们把这个程序拆解开,却可以发现它背后做了很多事情。在汇编视图下这个最简单的程序是这样的(汇编代码太长了,我只保留其中的关键操作):
// 每一个程序的入口地址mainCRTStartup
mainCRTStartup:
00401120 push ebp
00401121 mov ebp,esp
// …
// 调用GetVersion()函数,获得操作系统版本
00401146 call dword ptr [__imp__GetVersion@0 (0042513c)]
// …
// 进行堆的初始化
0040119E call _heap_init (00403c40)
// …
// 向程序传递参数
004011D5 call _setargv (004031a0)
004011DA call _setenvp (00403050)
004011DF call _cinit (00402c70)
// …
// 开始进入main函数的入口地址执行main函数
00401204 call @ILT+0(_main) (00401005)
// …
// 退出整个程序的执行
00401213 call exit (00402cb0)
// …
这段汇编代码,几乎就是一个C语言程序的一生。当我们启动一个程序后,操作系统会创建一个新的进程来执行这个程序。所谓进程,就是应用程序的一个实例。操作系统创建进程的时候,会为其分配一定的内存空间(默认堆),作为其私有的虚拟地址空间。通常,一个应用程序的执行对应于一个进程,进程负责管理这个程序运行时的一切事物,例如资源的分配与调度等等。但是,作为程序执行的调度者,它并不负责程序的执行,具体的执行工作是由它创建的线程来完成的。每个进程都有一个主线程,如果是多线程应用程序,还可以有多个辅助线程。线程并不拥有资源(它使用的是它所属进程的资源),但是它拥有自己的执行入口、执行的顺序系列和一个终点。一个进程的内存分布如下图所示:
程序的进程与线程
当进程的主线程被创建之后,它会首先寻找程序当中的入口地址。我们知道,程序实质上就是一系列计算机指令,程序的入口地址代表了从哪一条指令开始执行。通常,每个程序中都有mainCRTStartup这样一个地址,这个默认的入口函数地址是编译器插入到程序中的(其中完成了一些必要的初始化和清理工作)。主线程就是找到这个地址并从这里开始向下逐条执行程序当中的指令。在这个函数执行的时候,首先会执行一些初始化工作,例如获得操作系统的信息、对堆进行初始化以及完成程序参数的传递等等。然后,就是最关键的对主函数的调用,一句“call @ILT+0(_main)”就是跳转到main()函数的入口地址,开始进入main()函数的执行了。mainCRTStartup所做的事情我们无法控制,而main()函数就是我们的一亩三分地,可以自由发挥了。接下来我们来看看main()函数到底是如何执行的。
// main()函数入口地址
@ILT+0(_main):
00401005 jmp main (00401010)
// …
--- e:\sourcecode\clan\clan.c ---------------------------------------------------
1: #include
2: int main()
3: {
00401010 push ebp
00401011 mov ebp,esp
00401013 sub esp,40h
00401016 push ebx
00401017 push esi
00401018 push edi
00401019 lea edi,[ebp-40h]
0040101C mov ecx,10h
00401021 mov eax,0CCCCCCCCh
00401026 rep stos dword ptr [edi]
4: printf("This is a C program. \n");
00401028 push offset string "This is a C program. \n" (00420f7c)
0040102D call printf (00401060)
00401032 add esp,4
5:
6: return 0;
00401035 xor eax,eax
7: }
00401037 pop edi
00401038 pop esi
00401039 pop ebx
0040103A add esp,40h
0040103D cmp ebp,esp
0040103F call __chkesp (004010e0)
00401044 mov esp,ebp
00401046 pop ebp
00401047 ret
--- No source file ----
从汇编代码中我们可以看到,主函数的执行,也不过是对于一些寄存器的操作和对库函数的调用而已。例如,在main()函数的第一句就是用“push ebp”保存当前地址。在汇编代码中,ebp代表了当前地址。为什么在进入main()函数后的第一件事不是我们在C语言程序代码中看到的输出一个字符串,而是保存当前地址呢?实际上,我们从C语言程序代码中看到的只是我们对于要实现的功能的描述,而真正地要实现这些功能,C语言程序背后所对应的汇编代码还要为我们完成很多事情。这里的“push ebp”保存当前地址,就是为了让这个main()函数执行完毕后可以顺利返回(也就相当于在出发的地方插上一个标签,好让我们可以找到回来时的路)。除了对于寄存器的操作(push、move以及pop等汇编指令)之外,汇编代码中更重要的是对其他函数的调用,这都是通过call指令来实现的。例如,“call printf (00401060)”这个call指令就是调用printf函数,进入printf函数的执行以输出字符串。因为printf函数是由C语言函数库提供的一个函数,我们这里看不到它的具体代码,但是其内部与上面的main()函数都是相似的。
虽然我们的源代码中只有一行代码,但是编译后的程序在背后却做了很多事情。而正是因为汇编代码太过繁琐,我们才更加钟爱简洁易懂的C语言程序代码。而至于如何将C语言源代码转变成可以执行的汇编代码或目标机器代码,这些复杂的事情就让任劳任怨的编译器去完成吧。
|
谭老师在列出这些程序代码的同时,详细地介绍了其中每一句的作用。我们可以在自己的开发环境中实现这些程序,然后尝试着修改其中的某些代码以实现不同的功能,以便对C语言程序有一个感性的认识。比如,其中的例1.3只是实现了两个数的比较,我们可以对其进行简单修改,从而实现三个数的比较。当然,这已经不是“最简单的C语言程序”了。进行这样的修改,可以激发我们对于C语言的兴趣。看着程序在我们的修改之下,实现了新的功能,一定会有一种“C语言在手,江山我有”的成就感。话不多说了,让我们来看看如何修改例1.3以实现三个数的比较。
// 引入标准输入输出头文件
#include
// 使用“?:”操作符对两个数进行比较
int max(int a, int b)
{
return a > b ? a : b;
}
// 主函数
int main()
{
// 定义三个变量并进行初始化,分别保存三个整数和最大值
int a=b=c=m=0;
// 提示输入格式
printf("Please input integers.(e.g. 19 83 73)\n");
// 获得用户输入的整数并保存到三个变量中
scanf("%d %d %d", &a,&b,&c);
// 获得a和b中较大的一个整数
m = max(a, b);
// 获得c与a、b中较大值比较的结果,也就是三个整数中最大的一个
m = max(c, m);
// 输出结果
printf("the max integer of %d,%d,%d is %d.\n",
a,b,c,m);
return 0;
}
经过简单的改写,我们就将原来只能进行两个数的比较的程序扩展为可以进行三个数的比较的程序,当然,如果愿意,我们也可以将其扩展为可以进行更多数据的比较。相对而言,改写一个程序以对其进行扩展从而实现自己的功能,要比全新地去设计实现一个程序简单得多,这也告诉我们这样一条实践的经验,如果我们要实现的程序已经有原型代码,也即是实现了基本功能的代码(例如这里的例1.3,已经实现了两个数的比较),我们最好的策略应该是修改原型代码,对其进行扩展以实现新的功能,而不是全部重新再来。这样既可以提高效率(只需要对原型代码进行扩展),也可以保证质量(原型代码已经得到验证,质量可以得到保证)。我们要学会站在别的程序员的肩膀上。
回到代码本身,我们对例1.3的修改主要有这样几个地方:
l 提前声明max()函数
在C语言中,当我们要调用某个函数时,必须知道这个函数的声明,也就是要知道这个函数的函数名、参数以及返回值等等,这样我们才知道如何对其进行调用。否则,编译器在编译这个函数调用时,并不知道它是一个函数调用,就会产生编译错误。这也就是我们为什么在用到标准库中的函数时,首先需要在源文件的开始部分,使用“#include”预编译指令引入相应的头文件的原因。
除了将函数提前定义之外,我们还可以将函数提前声明,就像例1.3在main()函数中提前声明max()函数一样。只要在函数被调用的时候是已经声明过的就可以了,而至于函数的具体定义可以放在源文件的其他位置,比如main()函数之后。
l 修改max()函数的实现
在max()函数中,我们修改了max()函数的实现,将原来比较复杂的条件判断语句简化为使用“?:”操作符对两个整数进行判断并返回较大的一个整数。经过这样的修改,代码更加简洁,逻辑也更加清晰明了。在C语言中,条条道路通罗马,任何问题都不止一种解决办法。我们编写程序,很多时候都是在对程序不断优化,寻求问题的最佳解决方案。
l 在定义变量的同时进行初始化
在C语言中,局部变量定义后是一个随机的初始值。如果直接使用这个未经过初始化的变量,可能会让我们的程序产生奇奇怪怪的结果。如果你遇到这样的问题,那将是你的噩梦。虽然在定义变量的同时对其进行初始化并不是必需的,但是这实在是一个好习惯。它可以让你逃离那调试程序的无数个不眠之夜。
l 对用户友好的输入输出提示
一个程序,或者说一个软件能否成功的关键,往往不是看它的功能有多么强大,而是看它对用户是否足够友好,用户是否乐于使用这款软件。如果我们开发的软件功能很强大,但是对用户极不友好,用户不会使用或者是使用起来很麻烦,那就没人会用这个软件。如果那样的话,功能再强大又有什么用呢?所以,软件的易用性和它的功能同样重要。改写后的程序改善了程序的易用性,对输入输出都做了很好的提示,用户可以轻松地学会使用这个程序。对比没有任何提示的例1.3,用户一定会更喜欢这个改写后的程序。如果这是两个商业软件,谁会获得商业上的成功,自然不言而喻。虽然C语言不能像Java、C#等高级编程语言一样,为用户提供华美的程序界面,但是即使是简单的纯文本界面,也同样可以做到对用户友好。让程序对用户友好,始终应该是我们在开发C语言程序时的一个目标。
东边太阳西边雨:不同的用户体验,不同的程序命运
l 算法逻辑的改进
max()函数只能比较两个整数,但是在改写的程序中,我们通过两次简单的max()函数调用的组合,首先比较获得a和b之间的较大的整数,然后将其与c进行比较,最终获得三个数当中最大的一个整数。经过简单的一个比较规则的改变,就获得了三个数中最大的一个数。这就是算法,也是C语言程序的灵魂。算法和程序设计就像一对好兄弟,程序设计能力的提高,离不开我们算法设计与实现能力的提高;而算法能力的提高,同样会促进我们的程序设计能力。所以在学好C语言的同时,也要学好算法设计。
算法与程序设计是好兄弟
改写后的程序,并不是“最简单的C语言程序”,但是却很好地反映了C语言程序设计的全貌。小小一个例子的改写,折射了C语言程序设计的方方面面。我们可以从中获得很多程序之外的知识和经验。语法规则人人都可以学会,但这些程序之外的知识经验却更为宝贵。
|
在上一小节中见识过几个C语言中最简单的程序之后,在接下来的这个小节中,谭老师向我们介绍了C语言程序的结构,从而让我们对C语言程序有一个整体的认识和把握。
谭老师将一个C语言源程序主要分成三个部分:预处理指令、全局声明和函数定义。这三个部分有机地组合起来,就形成了一个完整的程序。谭老师这样的划分虽然非常清晰,但是没法在我们头脑中形成一个比较形象的概念,我们虽然知道一个C语言程序包含这三个部分,但可能还是不会组织编写一个完整的C语言程序。
事实上,正如谭老师所讲,“一个C语言程序是由一个或者多个函数组成的,其中必须包含一个main函数(且只能有一个main函数)”。我们只要牢牢把握了这一点,就可以自己来组织编写一个完整的C语言程序了。更形象地说,一个C语言程序中的预处理指令、全局声明和函数定义就像是一个个的积木方块,而我们编写一个C语言程序,就是将这些积木方块,按照一定的顺序搭建在一个C语言的源文件中。比如在上一个小节的例子程序中,它就包含了预处理指令、max函数和main函数这三个积木方块。而整个写程序的过程,就是将这三个积木方块按照一定的顺序规则搭建成一个完整的C语言程序的过程。明白这个流程之后,实际上我们写一个程序的过程,就变成了寻找合适的积木方块(使用合适的头文件、编写需要的函数、编写解决问题的main()函数等等),然后将这些积木方块按照一定的顺序规则组织起来的过程。写程序,就是搭积木。
写程序就是搭积木
一个完整的C语言程序是按照搭积木的方式来组织的,而在最重要的main()函数内部,也同样是以这种方式来组织代码实现具体功能的。
因为main()函数的特殊性——一个C语言程序总是从main()开始执行,整个main()函数执行完毕之后,程序就结束执行而退出。可以说,main()函数就是一个C语言程序短暂而光辉的一生。正是因为一个C语言程序是以一个main()函数来贯穿始终的,所以我们就在这个函数中获取数据的输入、对数据进行处理然后将数据输出,这几乎是每一个C语言程序的三部曲。如果其中的某些步骤比较复杂或者相对比较独立,我们还需要将其提取出来,成为单独的函数,比如我们将实现比较功能的代码提取出来形成单独的max()函数,也就是一个单独的模块(积木)。再加上一些库函数,比如实现输入输出功能的scanf()函数和printf()函数,这些都是我们可以在main()函数中使用的积木方块。而main()函数,就是合理地组织这些函数(积木方块),以完成对数据的处理或者是对某个过程的控制。这就是我们编写C语言程序的整个过程。
C语言程序的结构
无论是整个C语言程序的编写,还是main()函数的编写,都可以看成是一个利用积木方块(预处理指令、全局声明以及函数等)来搭建一个解决某个问题的完整程序的过程。我们之所以能够像玩积木一样地来写C语言程序,归根结底,是因为C语言采用的是面向过程的编程思想,它总是将一个比较复杂的问题,采取“自顶向下,逐步求精”的策略将其分解成多个小问题。而每一个小问题,就是我们所说的模块,也就是我们这里的积木方块。这些模块常常表现为某个函数,而main()函数就是这些模块函数组织起来的一个解决某个问题的过程。例如,我们要解决“比较三个整数的大小”这个问题,在main()函数中,我们就首先需要使用库函数scanf()来获得用户输入,然后使用我们自己定义的max()函数来比较获得最大的一个整数,最后使用库函数printf()将结果输出,从而完美地解决了这个问题。概括起来,编写C语言程序,实际上就是利用各个不同功能的函数(如果没有现成的函数可用,我们就需要自己定义实现某个功能的函数),将它们在main()函数中组织成一个解决某个问题的流程,而我们最终得到的是一个解决了某个问题的main()函数,它就是一个C语言程序的实质。如果将一个个库函数(例如scanf()函数)或者是自定义函数(例如max()函数)看成是一个个积木方块,那么整个main()函数就是由这些积木搭建而成的美丽城堡。
1.4.B 编码规范——如何写出简洁优美的代码下面是来自两位C语言程序员的实现相同功能的两段代码:
l 月薪1000元的程序员的代码:
#include
int main()
{
FILE *Wenjian; char Str[100];
Wenjian = fopen("test.txt","w");
do {
gets(Str);
if(Str[0] == '!')
break; fputc(Str,Wenjian); }while(1);
fclose(Wenjian);
return 0;
}
l 月薪10000元的程序员的代码:
#include
int main()
{
// 文件指针和保存输入的字符串
FILE *fp = NULL;
char str[100] = "";
// 以可写方式打开文件
if(NULL==(fp = fopen("test.txt","w")))
{
printf("cannot open the file.\n");
exit(0);
}
// 提示用户输入
printf("please input a string:\n");
do
{
// 判断输入是否结束
gets(str);
if('!'==str[0])
break;
// 写入文件
fputc(str,fp);
}while(1);
// 关闭文件
fclose(fp);
}
其实,不用我多说,两位程序员的工资,已经很明显地说明了这两段代码孰优孰劣。那位月薪1000元的程序员的代码,有着大多数初学者编写代码时的坏习惯:变量名大小写混用、中英文混用(这大约是中国程序员的一大特点);同一行代码定义多个不相关变量;定义变量而不进行必要的初始化;对文件打开等可能出错的操作不进行防错处理;缺少必要的程序提示;“==”比较中变量在前常量在后;程序中只有代码没有注释;各种语句没有经过较规范的排版、代码版式错乱无章、无规则的代码缩进等等。
第一段代码集中地反映了初学者在开始学习编写程序的时候,因为没有接触良好的编码规范而形成的混乱的编码习惯,使得自己写出来的代码“惨不忍睹”,没有任何的可读性和可维护性。虽然编译器并不在意你编写的代码是否美观,但是,我们应该记住,除了编译器之外,我们的代码也是写给别人看的。如果不想让别人在维护你的代码时骂娘的话,如果不想让你的工资始终停留在1000元上下的话,就要好好学习一下编码规范,向月薪10000元的程序员看齐。
对比于第一段月薪1000元的程序员的代码,第二段程序阅读起来就流畅自然多了,几乎不用动任何脑筋就可以理解整个程序,自然其可维护性也就大大提高了。实现相同功能的两段代码,只是因为编码风格的不同,写代码的人所受到的待遇就有着天壤之别。也许,第二位程序员每个月拿到的10000元中,有1000元是付给他所实现的功能的,而有9000元是付给了他所遵循的良好的编码规范。
所谓编码规范,是在项目进行过程中所制定的关于编码格式、注释风格的书写规范,它可以极大地提高代码的可读性,增加代码的可维护性。除了这些看得见的好处之外,良好的编码规范还可以预防一些常见的编程错误(例如名字冲突、嵌套错误等)。
世界上正在应用的编码规范有很多,有各大公司根据自己的情况制定的编码规范,也有针对各种编程语言而制定的编码规范。但是,这个世界上并没有一种所谓“最好的”编码规范,即使是现在所流行的各种编码规范也都各有其优缺点。所以,没有普遍适用的标准。如果我们所在的项目团队已经有了一份编码规范了,那么就可以按照上面说的做。如果硬要推翻重来,那么可能会带来更多的争吵而不是把问题解决。从商业角度来看,只有两件事是重要的:一是代码可读性好,二是团队中的每个成员都使用相同的风格。
因此,我们不要妄图去制定一种“最好的”编码规范,只能结合自己的项目实际,同时参照现在流行的编码规范,采纳其优点,摒弃其缺点,制定出一种“最适合”的编码规范,并且在项目实践中认真严格地执行,这就是“最好的”编码规范。
|
不断地上机练习是学好C语言的不二法门。在这个小节中,谭老师总结了上机练习的四个步骤:
1. 上机输入和编辑源程序
2. 对源程序进行编译
3. 进行链接处理
4. 运行可执行程序,得到执行结果
这四个步骤是对C语言程序编写过程的一个高度总结。实际上,C语言程序的编写过程是一个循环往复的过程,我们往往需要不断地编写代码、对代码进行调试、再编写代码、再进行调试,直到最终程序执行得到正确的结果为止。整个过程如下图所示:
编写C语言程序的两个循环
从这里我们可以看到,整个C语言程序的开发过程可以分成两个循环,其中第一个比较大的循环就是谭老师总结出来的四个步骤,它代表着开发一个C语言程序所需要的某一次循环过程。每次循环负责实现C语言程序的某个功能,而多次循环又可以累加起来,从而可以完成一个比较复杂的C语言程序。在这四个步骤当中,第一步和第四步比较简单,只需要简单操作就可完成。而相对比较复杂的是第二步和第三步,它们通常是在一起完成的。因为C语言程序的编译和链接是一个非常复杂的过程,我们需要在编译和链接的过程中,通过不同的参数对编译程序和链接程度的很多行为进行控制,以得到我们想要的最终的可执行程序。如果我们使用的是IDE,例如Visual C++ 6.0或者Eclipse,来进行C语言程序的开发的,通常可以直接使用它已经配置好的参数对源程序进行编译和链接。如果是在命令行方式下直接调用编译器,例如GCC,对源程序进行编译和链接,我们就需要熟悉编译器的常用编译选项。
除了编写C语言程序的整个比较大的循环之外,在每次循环中,当程序遇到错误需要调试时,就开始进入了一个小循环。这个循环负责查找程序中存在的错误,使程序的执行结果正确,实现程序功能。在这个世界上,完全没有Bug的程序是不存在的,一个程序员功力的高深,也许不是看它能否正确地实现程序功能,更多地是看他在程序执行遇到问题的时候,能否及时准确地找到Bug并将其解决掉。所以,调试能力也同样是我们应该重点学习和培养的能力。
在调试程序寻找Bug的时候,我们往往需要跟踪程序的执行过程,这时候就需要一些调试工具的辅助,我们可以使用IDE提供的调试工具,比如Visual C++ 6.0的调试工具。也可以使用单独的调试工具,例如Linux下的GDB调试工具。学习并熟练地掌握这些调试工具,可以极大地提高我们的开发实践能力。
1.5A 工欲善其事,必先利其器——使用Eclipse编写C程序俗话说,工欲善其事,必先利其器。我们要想高效地编写C语言程序,离不开一款优秀的开发工具的支持。在这里,谭老师向我们介绍了Visual C++ 6.0作为开发工具。从它在软件开发界的流行程度来看,它确实是一款优秀的开发工具。但是,夸张一点地说,它已经是上个世纪的开发工具了。在软件界,1年就意味着过时,3年就意味着淘汰,而上个世纪,恐怕就意味着古董了。因为它是上个世纪的产品,很多先进的技术无法在它身上得到体现,更重要的是,它对于标准的支持不够完善,但同时又对标准进行了不少扩展,这可能导致你使用Visual C++开发的C语言程序缺乏可移植性。
我们今天来学习C语言,除了Visual C++之外,当然还有更好的选择,那就是大名鼎鼎的Eclipse。Eclipse是著名的跨平台的集成开发环境(IDE)。最初是由IBM公司开发的替代Visual Age for Java的下一代集成开发环境,2001年11月贡献给开源社区,现在它由非营利软件供应商联盟Eclipse基金会(Eclipse Foundation)管理。
从Eclipse的发展历史来看,它最初只是用来进行Java语言的开发。Eclipse的本身只是一个框架平台,但是众多插件的支持,使得Eclipse拥有较佳的灵活性,同样可以支持C、C++、Python、PHP等其他语言的开发,成为软件业界最受欢迎,同时也是应用最广泛的集成开发环境。
Eclipse CDT就是Eclipse为了支持C/C++的开发而提供的插件,它将把Eclipse转换为功能强大的 C/C++ IDE。它被设计为将Java开发人员喜爱的许多Eclipse优秀功能提供给C/C++开发人员,例如项目管理、集成调试、类向导、自动构建、语法着色和代码完成等等。当Eclipse被用作Java IDE时,它将利用JDK并与之集成。同样地,CDT将利用标准的C/C++工具集并与之集成,例如C语言编译工具GCC、构建工具make和调试工具GDB等。这些工具都已经由Linux操作系统提供,并可在Linux中单独使用,用于大多数C/C++开发。而Eclipse的作用,就是将这些分散的工具集成在一起,使得它们更加易于使用,生产效率更高。而正是因为Eclipse将复杂的事情简单化了,原来需要使用多个工具才能完成的事情现在只需要使用Eclipse一个工具就能完成,这使得Eclipse CDT在Linux世界中变得非常流行,成为最受欢迎的C/C++开发工具。
在Eclipse中编辑C语言源程序,又快又好
而对于大家更加熟悉的Windows平台,因为Eclipse CDT所集成的开发工具并不是由操作系统天然提供的,所以我们首先需要安装相应的工具,例如Windows平台上的GCC编译器Cygwin或者MinGW等,然后同样可以设置CDT以使用这些工具,从而自己动手将Eclipse CDT打造成一款Windows平台上的C/C++开发工具。
1.5B 使用GCC编译C语言程序使用Eclipse CDT开发C语言程序,虽然使用很方便,效率也很高,但是它向我们隐藏了很多细节。虽然大多数时候这并不影响我们的开发,但是当我们需要对程序的编译链接过程进行更加特殊的控制,以获得可以满足特殊要求(例如,对程序进行优化、附带调试信息等)的程序的时候,就需要直接调用编译器程序进行编译链接,从而可以使用编译器所提供的丰富的编译选项,以达到对编译器的行为进行灵活控制,得到我们想要的个性化的可执行程序的目的。
我们通常所说的GCC编译器,是GUN Compiler Collection的简称。GCC原本只能编译C语言程序。但是,当C++流行起来后,GCC很快地被扩展,变得可编译C++程序。之后,GCC更是被进一步扩展,变得可支持Fortran、Objective-C以及Java等主流开发语言的编译。从GCC的应用来看,它是Linux/UNIX平台下最常用的编译器程序。同时,在Linux/UNIX平台下的嵌入式开发领域,GCC也是用得最普遍的一种编译器。除了Linux/UNIX操作系统之外,GCC还能运行在其他不同的操作系统上,如Solaris、Windows(需要自己安装Cygwin或者是MinGW)、Mac OS X操作系统等。正是GCC编译器应用的广泛性,使其成为C以及C++编译器的事实上的标准。
GCC的使用方法跟谭老师所介绍的Visual C++的编译器的使用方法非常相似,它的调用方式如下:
gcc [编译选项] [文件名]
其中,gcc是GCC的C语言编译命令,其后的编译选项,是用于控制编译器的编译行为的。GCC常用的编译选项有:
l -c
“-c”选项表示只编译,不链接成为可执行文件,编译器只是将输入的.cpp等源代码文件生成.o为后缀的目标文件,通常用于编译不包含主函数的子程序文件。
l -o
“-o 输出文件名”,这个选项用于指定输出的可执行文件的名称,如果不给出这个选项,GCC 就给出预设的可执行文件。在Linux系统上,这个默认的输出文件名为a.out,相应的,在Windows系统上这个默认的文件名就是a.exe。
l -g
使用“-g”编译选项将会产生调试工具(例如,GNU的GDB)所必需的符号信息,要想对源代码进行调试,我们就必须加入这个编译选项。
l -O
“-O”选项表示编译器将对程序进行优化编译、链接,采用这个选项,整个源代码会在编译、链接过程中进行优化处理,这样产生的可执行文件的体积可能减小,执行效率也可能提高,但是,编译、链接的速度就相应地要慢一些。
l -I
“-I 目录名”可以指定GCC的附加头文件目录,我们在使用第三方提供的程序库的时候常常会用到这个选项,用于将程序库的include目录添加为程序的附加头文件目录。
实际上,GCC所提供的编译选项可能有数百个之多,我们只需要了解以上这些常用的编译选项就可以了。到真正需要的时候,可以查阅相应的参考手册。
在了解了GCC的基本使用方法之后,利用GCC,我们同样可以以命令行的方式来编译链接C语言程序。例如,我们可以以下面的命令将上面小节中的例子源程序编译成一个可执行程序。
gcc -g –O max.c -o max
通过这样一个简单的命令,我们就将max.c这个C语言源文件编译并优化成了一个可执行文件max,在编译的过程中,还生成了相应的调试信息。通过这样的方式,我们对C语言程序的编译链接过程进行了自定义。
虽然以命令行的方式使用GCC编译链接C语言程序,可以对编译链接过程进行很好的控制,但是每次编译都需要输入这些常用的编译命令,无疑是一件烦人的事情。程序员就是为了解决麻烦而存在的,怎么能够被麻烦所困扰呢。为了解决这个问题,我们通常将一些文本编辑器(例如vim或者EditPlus)配置成可以直接调用GCC命令,这样,在文本编辑器中编辑好源文件之后,就可以直接调用它的自定义命令来编译链接源文件,得到最终的可执行程序。如果是一些比较大型的项目,有多个源文件,我们则常常使用更加强大的make构建工具,在一个Make File中定义好各个源文件的编译链接规则,然后使用make命令来编译链接整个项目。
开发工具拼图
利用GCC,GDB、make工具以及文本编辑器,我们完全可以打造一个高度自定义的,完全适合自己开发习惯的C语言开发环境,真的是做到了“我的地盘我作主”。
|
在这一小节中,谭老师将程序设计概括为“程序设计是指从确定任务到得到结果、写出文档的全过程”。他总结归纳了程序设计的6大任务:
1. 问题分析
2. 设计算法
3. 编写程序
4. 对源程序进行编辑、编译和链接
5. 运行程序,分析结果
6. 编写程序文档
整个软件设计的流程如下图所示:
软件设计流程
从这里看,整个程序设计的流程是不是很像一个瀑布呢:各个任务逐级依次向下。设计上,这就是程序设计模型中的最著名的瀑布模型(Waterfall Model)。瀑布模型中的程序设计过程是通过设计一系列阶段顺序展开的。实际上,根据程序开发的具体实践,我们可以把程序设计更加科学地划分为制订计划、需求分析、软件设计、程序编写、软件测试和运行维护等六个基本活动。从问题分析开始直到产品发布和维护,每个阶段都会产生循环反馈,因此,如果有信息未被覆盖或者发现了问题,那么最好 “返回”上一个阶段并进行适当的修改,程序开发进程从一个阶段“流动”到下一个阶段,这也是瀑布模型名称的由来。瀑布模型的核心思想是,按工序将问题化简,将功能的实现与设计分开,便于分工协作,即采用结构化的分析与设计方法,将逻辑实现与物理实现分开。它将一个完整的程序设计过程划分为如上的六个任务,并且规定了它们自上而下、相互衔接的固定次序,如同瀑布流水,逐级下落。因为瀑布模型所采用的各个细分逐渐化简的设计思想,跟C语言所支持的结构化程序设计非常贴合,虽然世界上还有其他的很优秀的程序设计模型,例如敏捷编程等等,但是因为两者之间天然的相似关系,使得瀑布模型成为C语言程序设计中应用最多的程序设计模型。
总体而言,程序设计是一个完整的过程,而不是我们通常认为的程序设计就是“编写程序”,它只是整个程序设计过程中的一个环节而已。从实践来看,“编写程序”这个任务在整个程序设计当中占有的比例,往往不到三分之一。在一个项目中,我们往往是将更多的时间用在前面的“问题分析”与“设计算法”以及后期的编写文档等等。这正是“汝果欲学诗,功夫在诗外”,如果我们想提高自己的程序设计能力,除了“编写程序”之外,我们更应该培养自己分析问题、解决问题的能力,而不是去钻研各种高深的语法细节,将时间浪费在“茴香豆的茴倒底有几种写法”上。
程序设计的这六个任务共同组织起来,才能完成一个程序的设计。如果中间缺失了某一个环节,我们就无法“得到结果”,也即最终的能够解决问题的程序。而我们也可以针对自己的薄弱环节,有意识地加以强化锻炼,这样才能真正成为一个专业的程序员。