« IDE 不是程序员的唯一选择(一) | 返回首页 | IDE 不是程序员的唯一选择(三) »

IDE 不是程序员的唯一选择(二)

话接上回,话说我们已经大致了解了 C 编译器的工作流程,知道了 IDE 在背后如何在驱动编译器生成代码。对于传统 IDE ,就是集成了编辑器、项目管理、编译器,和调试器等几个大件的一个庞然大物。其中 IDE 企图节省人力的最大的部分就是将源代码组织起来,自动生成其间的关系,调用编译器构建项目。

(此处删去几百字关于 IDE 优劣的讨论。因为我觉得这个话题会陷于无谓的争论,还是直入主题比较好。承接上篇的宗旨,本文只写给有兴趣学习相关知识却不知该如何入门的朋友。说服程序员放弃 IDE 不是本文的初衷。)

对于不太大的项目,比如学校里日常做作业。写一个 make.bat 文件管理你的 C 代码已经足够了。如果你用一些 windows style 的编辑器,比如流行的 editplus 之流,都可以配置相应的所谓 user tool,一键调用 .bat 构建出最终的程序。也可以设置捕获编译器的输出,方便的双击错误信息定位到源代码编译错误的地方。

如果想摆脱鼠标(纯键盘操作对于基于命令行的工作方式来说,非常的有效率。毕竟写程序最终也得靠键盘的。),可以考虑使用 vim 。不要惧怕学习新事物。任何被公认优秀的工具,都有学习的价值。vim 属于有一定学习门槛,但一旦掌握(一般程序员可能需要一周左右的时间熟悉),威力无穷的那种。我的另一个同事强烈推荐 emacs ,我没怎么用过。如果你打算写 10 年以上的程序,花上几天时间学习一个无数程序员公认好用的工具,这项投资我个人认为是非常值得的。


使用 bat 的方式来构建项目有一个问题,那就是当项目慢慢增大时,效率受到影响。因为每次 build 一次,都要重新编译链接所有的源代码。而 C/C++ 从设计之初就考虑到节省编译时间,可以每个源文件独立编译的,然后最后在把分别编译出来的目标文件链接在一起。(关于编译和链接的概念,不明白的同学请自己 google )下一步,我们可以每次只编译刚刚修改过的源文件,和那些没有修改过的源文件以前编译出来的目标链接在一起就够了。

早期的 C 编译器,但是将编译和链接过程分开由两个程序完成的(现在其实也是)。我们今天看到的类似微软的 cl 这种程序只是给独立的连接器加了个壳而已(gcc 也是这样)。下面我们来看怎样分开编译多个源文件,并链接它们。

如果只想编译一个 .c 文件,只需要在给 cl 加上参数 /c ( gcc 是加 -c )。那么编译上一篇中提到的例子 foo.c ,即用命令行指令:

cl /c foo.c

我们可以看到,当前目录下生成了目标文件 foo.obj

我们可以用同样的方法生成 bar.obj 。即 cl /c bar.c

那么如何把 foo.obj 和 bar.obj 链接起来?还是用 cl 即可。

cl /Fefoobar foo.obj bar.obj

cl 这个壳会正确的识别输入文件的类型,做出对应的链接这个行为。我们也可以直接调用微软的链接器:Link 。

link /out:foobar.exe foo.obj bar.obj

注意,这里输出文件必须写全 .exe 后缀。

罗罗嗦嗦写了一大堆,主要是想加深以前不太明白这些的同学们的印象。如果是玩 IDE 的老鸟,其实在 IDE 的各种设置中,差不多也都看过这些了。关于编译器的命令行参数不记得不要紧。一则可以用 /? 看帮助;二还可以去 VS 的 IDE 中对应的设置菜单里看看,通常一个 IDE 菜单选项的修改,都对应了最终命令行参数的差别。生成 exe 还是生成 dll ;使用控制台模式,还是标准 Win32 程序;预定义些什么宏,打开调试信息或是开启编译优化,无所不在。

记住这些参数的写法并不容易,也没有必要。因为相对于编写代码,敲打这些编译开关只占很少的工作。但要记住,如何调用编译器去构建工程,这个步骤本身其实也是项目构建的一部分。亲手写过,也就多了一分了解。


编程之道在于,让机器做机器的事,人专注于人的思考。如果真的靠手键入每条编译指令,无疑让程序员去做了机器之事。把编译指令写入批处理(脚本)文件,仅仅只是节省了每次编译的重复劳动,并没有从根本上解脱。

在从 C 的源代码构建最终的执行文件这个流程中,哪些是人的创造,哪些是机器应行之事?显而易见,程序员应该做的是:

  1. 教会机器,如何把一个 .c 文件编译成 .obj 文件。

  2. 教会机器,如何把若干个 .obj 文件链接成 .exe 文件。

  3. 告诉机器,你的项目由那些 .c 文件构成,最终你想生成一个叫什么名字的 .exe 文件。

其中前两步,对于大多数类似的项目来说是共同的,所以我们只用也只应该教机器一次。而第三步,提供一个文件列表和一个目标文件名给机器即可。

实现以上目标,显然我们需要额外的工具。IDE 的项目管理及构建模块是一个选择,但不是唯一的选择。IDE 对最为常见的构建需求做的相当不错。也就是说,IDE 完美的教会了机器前两个步骤,我们无须干预。但这件事情意义有多大呢?既然我们现在已经知道整件事情的流程手工怎么进行,完成它就有了无数的选择。教会机器做这前两件事情,只是一项一次性劳动。我们可以通过教授机器做这件事的过程,学会更多东西。而后举一反三去干更为复杂的工作。下面,云风将介绍一种叫做 MAKE 的工具。它是我们完成这个目的的第一个台阶。


为什么是 Make ?

我想说, make 只是诸多选择中的一个。它绝对不是最好的,但它是最容易理解的。make 以一种极其简单的规则运作,让程序员可以轻易看透它的实质。而简单的工具组合起来往往可以发挥出极大的能量。因为简单,所以 make 也很容易学习。你只需要掌握它很少的一部分功能,它就帮助你完成各种各样的工作。绝不仅仅是编译程序这么简单。你可以用它收发邮件、下载文件、制作安装包、运行单元测试,等等你能想的到的在你的机器上用命令行能够完成的所有事情。当然这些事情不用 make 也有别的方式去做,只是 make 用起来更方便。Make 会按你告诉它的事情的依赖关系,决定了做事情的前后次序,然后批量完成交给它的任务。如此而已。如果你用过 FreeBSD ,一定会爱上它的 ports 。需要什么软件,进入对应目录,make install 。下载、配置、编译、安装,一气呵成。这就是 make 的威力。

学会了 Make 后,你再接触别的项目构建工具,就不会有太多障碍。Make 做起来比较困难的事,也可以再高一个层次的工具来完成,比如 Automake 。我们要做的是,认识问题是什么,选择合适的工具,用程序员的方式解决它。

Make 有许多分支版本,细节使用起来各有差异。VS 里带了一个叫做 NMAKE 的小工具,是 Make 的一个旁支。我本来想从这个讲起,无奈用的不多,还是换成日常用的比较多的 GNU Make 吧。gmake 的内建指令也更为丰富,稍微熟悉一下,得心应手。更难得的是,在 Windows 下获得 gmake 非常方便,使用 Mingw 版的 gmake 即可,在 google 搜索 “mingw gnu make” 即可。其实不需要安装,它是一个绿色软件,一个独立的一百多K 的 gmake.exe 小程序就可以直接运行。

下面,云风假设你已经正确安装了 Mingw 版的 GNU Make (我的 Windows 系统上安装的版本是 3.81 )。在命令行下,无论在什么目录下都可以直接输入 gmake 运行。(默认安装的 Gnu make 的执行文件名可能不叫这个名字,但你可以自己改个顺手的名字)

让我们开始吧。


还是在一个你可以随意做实验的目录,顶好是你已经创建了 foo.c bar.c 的那个目录下,新建一个叫做 Makefile 的文本文件。编译它,写上:

all :
    echo Hello World

这是你的第一个 Makefile ,输入请小心。第一行的 all: 应该顶格写,而第二行 echo 之前,必须有一个 Tab ,而不能用空格替代。即:第二行必须是 Tab 打头。

Tab 不是可以忽略的空白字符,并且是 Make 工作的关键。这个设定早就为许多人诟病,没有正确的输入 Tab ,也是许多 Make 初学者常见的错误。骂归骂,只能说这是一个历史原因造成的。好在一旦你习惯它,同样会觉得编写 Makefile 其实是非常顺手的。而且现在很多编辑器都可以让 Tab 明显的显示出来,而不会和空格混淆。

现在,运行 gmake ,你会看到它调用了命令行指令 echo Hello World ,回显了这行字符串。庆祝一下,你成功运行了自己编写的第一个 Makefile 。

Make 的一切设计都是为了简单、快捷。你把要做的事情写在一个文件中,运行 gmake 去跑这些任务。按通常的设计,gmake 应该跟一个任务文件的文件名,指定跑哪个文件。但是为了简洁,gmake 默认去找当前目录下的名为 Makefile 的文件了。至于你想把这些任务放在别的文件中,可以通过 gmake 的参数控制。有兴趣的同学可以按 Gnu 软件的习惯,通过 gmake --help 查询。

光能显示一行 Hello World 显然离题万里。接下来我们看看让 gmake 帮我们编译程序。不要删除前面的 Makefile 文件,再后面追加几行:

foobar.exe :
    cl /Fefoobar foo.c bar.c

记住第二行开头的 Tab 不要敲漏了。

现在你的 Makefile 文件看起来应该是这样:

all :
    echo Hello World

foobar.exe :
    cl /Fefoobar foo.c bar.c

我们再运行一下 gmake foobar.exe 看看:gmake 调用了你写好的 cl 指令,编译出了 foobar.exe 这个文件。

OK,大家应该看出点什么。Makefile 比传统的 bat 批处理文件多了点功能。它可以把多个任务放在一个文件里,而不需要我们写多个文件。为了区分任务,我们在命令行指定要做什么。

那些顶行写的文本,如果由一个单词加一个冒号开始。这个单词就被称为一个目标。gmake 目标,就可以做对应的任务了。而 gmake 会去做什么呢?自然由目标定义的下面几行决定。所有以 Tab 开头的文本行,定义了完成这个目标应该执行的命令行指令。gmake 会运行目标定义之下一直到下一个目标定义之间的所有指令。(注:Make 并不会保证这些指令的执行次序,虽然原则上是按你书写次序执行。这可以让 Make 使用多个 CPU 加快运行成为可能)

当我们在命令行输出 gmake all ,它就 echo Hello World ;而输入 gmake foobar.exe 它就调用 cl 去生成 foobar.exe 。如果没有直接输入 gmake 而不跟任何目标参数。它会自动寻找文件里的第一个目标。(而 Makefile 编写的惯例,我们通常把第一个目标起名字为 all ,这仅仅是一个惯例而已)

多试几次看看。

第 2 次运行 gmake foobar.exe 你会发现,gmake 报告:

gmake: `foobar.exe' is up to date.

而拒绝再次编译。

没错,这就是 Make 为数不多的原则之一:一旦目标已经存在,就直接跳过任务。

所有的目标,都被 Make 认为是一个文件,它的规则就是,如果目标文件存在,就认为事情已经做完。除非……

让我们修改一下 Makefile 把 foobar.exe : 这行改成

foobar.exe : foo.c bar.c

然后修改一下 foo.c 存盘,再运行 gmake foobar.exe 试试?又重新编译了对吧。

这就是 Make 的第二条原则:目标 : 后面以空格写上它所依赖的其它目标。如果所依赖目标存在,且比目标本身的时间新,就重新构键一次目标。

由于我们修改了 foo.c 导致了 foo.c 这个文件(对 Make 来说是一个目标,只不过没有构建这个目标的方法而已,只能靠用户自己编辑生成)比 foobar.exe 更新。foobar.exe 依赖 foo.c ,所以触发了 foobar.exe 的构建方法。

同样,我们可以让 all 依赖与 foobar.exe 。这样 gmake all (或省略 all 不写,因为 all 是第一个目标)时,由于不存在 all 这个文件,而触发 foobar.exe 的构建流程。


接下来,我们回头来看看自己编写的这个 Makefile 文件。现在大约是这个样子:

all : foobar.exe
    echo Hello World

foobar.exe : foo.c bar.c
    cl /Fefoobar foo.c bar.c

第 2 个目标 foobar.exe 的编写非常的累赘。程序员的直觉告诉我们,信息的重复不是一个好味道。那么让我们来修改一下。

foobar.exe : foo.c bar.c
    cl /Fe$@ $^

这样是不是味道好点了?$@ 和 $^ 都是 Make 的内设变量,也可以看成是一种类似 C 语言中宏的东西。$@ 在指令被运行时,被宏替换为目标,而 $^ 被宏替换为所有的依赖目标,即冒号后面的那一长串东西。同样常用的还是 $< ,可以替换所依赖的第一个目标。


今天已经写的足够多了。没接触过 Make 工具的同学应该能初窥门径了。没错,从现在已经介绍的知识来看,Make 并没有为我们节省太多体力,甚至还多敲了许多字符,今天快结束时,居然还要多记几个诸如 $@ 这样古怪的符号。而做到的事情离我们的目标还很远。

没关系,云风会带着你渐入佳境的,那么,且听下回分解。

Comments

在微软的平台用命令行,很难想象。仅仅从使用体验来说,没有类unix的shell那么好使,更不用说shell功能的强大了
test...
@ sjinny //----------------- 那个……把@解释为目标不会是因为它有点像射箭的靶子吧……怎么没见到哪里是这么介绍的捏…… //----------------- 嗯嗯,我严重怀疑这个才是正解
D:\mingw\bin>gmake make_file gmake: Nothing to be done for `make_file'.
@cloud 确实如此,我现在就是希望找到一种重新描述游戏逻辑的方式。以传统C++过程的方式来表达的不合适。 我现在的程序已经把游戏逻辑用C++实现了,或许我想的太天真,认为只要引入一个coroutine机制就可以改变现有的结构。 请云风以后能否写个关于介绍coroutine的专栏。
@frank28_nfls, 我在 windows 下使用 mingw ,不过项目也同时在 freebsd 和 ubuntu 下用 gcc 编译。gcc 的版本对我意义不大,因为我一直在用 C 开发。 @houmingyuan, coroutine 的问题说来话长,因为你想说的问题跟 coroutine 差的很远。就好象说,我想用面向对象来实现游戏,或是我想用函数式编程实现游戏等等。展开了话题就大了。coroutine 只是一个基本工具吧。
24楼是故意的吧
终于忍不住了,大笑ing
33楼可真有幽默感。
24楼的同学生动的为我们解释了什么叫:班门弄斧。
24楼的朋友,c-view是云风写的,呵呵
呵呵,看来看云风BLOG的还是windows 下程序员比较多
顶一下29楼的,不如也开过专题吧:) 接触LUA时我也是这个问题最迷惑……至今
云风,你好。 我最近用了一个CMake工具,这个工具可以自动生成GNU Make, VS Sln, Eclipse CDT等等多种编译配置文件,配置文件语法简单,直接,所有平台只需维护一份。 想想你请请教一个问题,我本人也在做游戏开发,现在游戏逻辑部分使用超多callback来,来处理异步情况。我想要把这部分也用coroutine的方式来实现,但是找不到好的方法,你有说到你经常使用coroutine不知道你是怎样使用的? 是否coroutine的部分是用lua或是stackless python来实现?还是其他方法?
发现,make的语言标记和bash有点像,比如echo,难道*iux上的东西都很像?
= =! 那个……把@解释为目标不会是因为它有点像射箭的靶子吧……怎么没见到哪里是这么介绍的捏……
似乎我一直都把&读作and……是怎么关联起来的也忘了……可能就像把+读做加一样吧,呵呵。 @在我的理解中一直是用来表示两个概念之间的包含关系,也就是at的含义。这时用来表示目标对我就比较无厘头。 python里用**表示幂运算,这与*结合起来就很容易记忆。 $已经开始有点习惯了,但是和其他的符号混在一起又容易糊涂。 如果makefile里的符号更容易看明白一点,特别是不需要抱着手册就能容易得看明白、不需要刻意花费很多时间精力来死记硬背就能熟悉,我想我会经常使用它。
make 这个跟 C 并没有本质区别。 & 为什么是 and , | 为什么是 or ,^ 为什么是 xor 。这些都是没有道理可言的。更奇怪的是 & 即可以表示 and ,又有可能是取地址,这就更加晦涩了。 所以 @ 表示目标,^ 表示依赖,也没有太多道理。如果只是为了想办法记下来,倒是可以找到的。(我个人觉得很形象) 当初你是怎么记住 & 是 and 的? 至于 $ 的前缀表示是一个变量,这个倒是许多语言的标准,比如 php 。
这两天我正把玩一个叫C-VIEW的东东,DOS下的读书器,云风肯定没听说过,呵呵。 突然发现C-VIEW在XP下加上液晶的显示效果非常不错,使我可以几乎平躺的姿势看文本,手里拿着鼠标。 说了半天废话,其实make的感觉和这很类似,都是看似老而简单的东西,却蕴藏着不可知的力量。
= =! 刚才的留言错别字太多了……重发…… 恩……符号化或者说形式化与清晰直接之间要做个平衡。 我是先学的basic,再接触C的。当时是在没接触过C的情况下看一本用C写的数据结构的书,当看到花括号替代了begin/end时就喜欢上了这个语言。而且当时很容易就能理解花括号的含义,特别是借助于良好的缩进。而当我看到shell和makefile里的那些诡异符号时,我怎么看都看不明白,对着手册看着也很费力,没过多久就忘了。所以makefile的这些符号过于晦涩了,而且符号的形式(也就是看上去的样子)和内容之间没啥关联,不像花括号,一个花括号对就括起了一个区域。
恩……符号化或者说形式化与清晰直接直接要做个平衡。 我是先学的basic,再接触C的。当时是在没基础C的情况下看一本用C写的数据结构的书,当看到花括号替代了begin/end时就喜欢上了这个语言。而且当时很容易就能理解花括号的含义,特别是借助于良好的缩进。而当我看到shell和makefile里的那些诡异符号时,我怎么看都看不明白,对着手册看着也很费力,没过多久又忘了。所以makefile的这些符号过于晦涩了,而且符号的形式(也就是看上去的样子)和内容之间没啥关联,不像花括号,一个花括号对就括起了一个区域。
梳理的很细致,读起来很有感觉。
最近也在看这方面的东西,多谢赐教了!持续关注中!
@sjinny 晦涩的表达是相对的,有其历史原因。make 要一步步学,慢慢的变的不晦涩 :D 最早学 C 语言的时候,^ 表示异或,& 表示取地址,++ 表示累加,/* 表示注释 ;C++ 里更有 ->* 这种更奇怪的符号,何尝不是晦涩呢? pascal 是不主张使用晦涩符号的,都用单词写出来。到底哪个更好?我想是没有定论的。
信息的重复不是好味道,但是“cl /Fe$@ $^”这样晦涩的表达也不是好味道。
既然提到了vim和emacs,偶来转个定义:"vim是编辑器之神",而"emacs是神之编辑器"。。。恩,我的发言完了,谢谢大家……
原来make里可以用cl啊,太好了,以后就用make了.
拜读大作,受益匪浅。非常期待后续讲座。
写的很不错,从你的blog上能学到很多东西!
很好玩 :)
云风大大既然讲到vim和mingw,就正好请教几个问题(2个vim的,2个mingw的,自知有点跑题,嘿嘿): vim: 1,win下的gvim的中文配置。其实主要是win下很多编辑器代码默认保存不是utf-8,所以vim这边配置就有点无所适从了,特别是要做跨平台/和linux上交换代码的时候。。。网上这方面的文章不是不多,而是太多!只是大多数文章都对应较老的版本,要么几篇文章还有自相矛盾之处,拿来试过几个都不甚满意,想请教。多说两句,就是开源的东东,相关的文章网上一搜一大片,但多数都不随版本更新而更新了,结果大多数都成了无用的电子垃圾,甚至还常常给人误导。虽然人家确实没啥义务随版本去更新自己的文章,但客观来说,对于开源普及来说,实在是有害无益啊(对比msdn。。。) 2,vim看代码,C++,搭配ctags好还是cscope?我两个都试过,跳转结果都会出错,还蛮郁闷的,也不知道是不是我配置不对?不知你这方面一般如何配置? mingw: 1,云风大大这里介绍只用到了单独的gmake。我自己使用mingw,不知你使用mingw会配合msys嘛?mingw带的make功能上有缺失(好象是没用到fork?),msys推荐使用msys自带的。 2,感觉mingw开发有点停滞不前,一直稳定版都使用gcc3,gcc4一直都只是technology preview/testing状态。也是这个原因,我一般是只拿mingw做些玩具测试代码,而不敢在太大项目上使用,怕万一遇上编译器问题被郁闷。不知云风大大的项目使用情况如何?
我试着装了一下mingw32-make.exe,不知道怎么使用(双击弹个黑框就没了)。想问下我是只装mingw32-make.exe这个呢,还是要安装 MinGW?
真是一篇相當好的 make 入門文章
给初学者写这么多入门的知识,真是不容易哦。
教程写的很好,可以使我这样的 Make 初哥既能在理论上有个了解还能不费功夫的得到大侠从众多工具中提取的比较好的工具。真的很好!我想我应该学下去~~~
听说vim配置好了,也和IDE差不多,函数名自动补全应该也是有的。
.c对.h的依赖也要显式的写出来吗?
学习,等下一篇,非常感谢!
其实我觉得对于像我一样喜欢用C#或者java的人来说,ide吸引我的原因是可以帮我补全超级长的函数名,以及处理引用问题。貌似自动编译我倒不是很在乎呢。不过用ruby的时候vim好用的没得说
发自内心的谢谢您。
很羡慕有心情写blog的人啊。有时候也会产生一些想法,想变成文字,但是一想到要花1,2个小时在上面,就觉得也许有更重要的事情做。
专栏怎么不更新了?嘿嘿 还等着呢..

Post a comment

非这个主题相关的留言请到:留言本