免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
楼主: pmerofc
打印 上一主题 下一主题

[C] 《C解毒》征询意见帖 [复制链接]

论坛徽章:
2
程序设计版块每日发帖之星
日期:2015-06-17 22:20:00每日论坛发贴之星
日期:2015-06-17 22:20:00
141 [报告]
发表于 2012-02-01 00:09 |只看该作者
提示: 作者被禁止或删除 内容自动屏蔽

论坛徽章:
2
程序设计版块每日发帖之星
日期:2015-06-17 22:20:00每日论坛发贴之星
日期:2015-06-17 22:20:00
142 [报告]
发表于 2012-02-04 12:08 |只看该作者
提示: 作者被禁止或删除 内容自动屏蔽

论坛徽章:
0
143 [报告]
发表于 2012-02-04 13:43 |只看该作者
支持楼主,方便大家学习即可

论坛徽章:
2
程序设计版块每日发帖之星
日期:2015-06-17 22:20:00每日论坛发贴之星
日期:2015-06-17 22:20:00
144 [报告]
发表于 2012-02-04 14:38 |只看该作者
提示: 作者被禁止或删除 内容自动屏蔽

论坛徽章:
5
狮子座
日期:2013-08-20 10:12:24午马
日期:2013-11-23 18:04:102015年辞旧岁徽章
日期:2015-03-03 16:54:152015亚冠之德黑兰石油
日期:2015-06-29 18:11:1115-16赛季CBA联赛之新疆
日期:2024-02-21 10:00:53
145 [报告]
发表于 2012-02-07 13:11 |只看该作者
本帖最后由 starwing83 于 2012-02-09 16:56 编辑

包含源文件 —— 是奇技淫巧还是饮鸩止渴?

    C语言如此流行,有一个很重要的原因是C语言志向于“抽象、泛化”概念而不是为了
一个应用就加一个特殊的概念。这样的思想使得C语言的概念通用而有生命力。然而,正
是这个原因,也导致了C语言极易被误用。因此教材就应该在这方面尽一本教材的引导的
责任——告诉你什么是应该做的,什么是容易出错的。而至少在预处理这个方面,谭老并
没有尽到自己该进的责任。

    C语言的预处理是很通用的,因为它在文本上操作,进行简单的文本替换。所以它和C
的语法并不在一个逻辑层次上。其中有一种预处理操作就是今天我们讨论的包含:
#include 预处理符,#include 预处理符并不一定非要出现在程序的开头,事实上它可以
出现在程序文本的任何地方,只要它出现在行首即可。包含本质上就是把一个文件原封不
动地“粘贴”到 #include 预处理符所在的地方。包含提供了强大的功能,比如可以在文
件间共享一系列的信息。因此谭老就“灵活运用”了这个概念:

   
图11.2(a)为文件file1.c,该文件中有一个#include <file2.c>指令

——谭浩强 ,《C程序设计(第四版)学习辅导》,清华大学出版社,2010年7月,p186

    我们知道,C语言有个约定俗成的惯例——被包含的文件应该以 .h 后缀名结尾,这
些文件被称为头文件。而谭老在这里却在教材里面堂而皇之地包含了一个通常我们认为是
被直接编译(而不是用作包含目的)的源代码文件。在一本面向初学者的教材中这样不顾
约定肆意书写代码正确合适吗?

    首先,我们要知道,为什么C语言会有“.h文件用于包含,而.c文件用于编译”这么
一个约定俗成的规定。这里我们需要介绍一个基本的概念:翻译单元,并了解C程序到底
是怎么被“编译”出来的。

    C语言的程序是以翻译单元为单位编译的。通常一个翻译单元就是一个源程序文件。
这个源程序文件可以利用包含的方法去包含其他的文件。那些被包含的文件就被直接“复
制”到包含的地方了。你可以认为是一堆的文件,比如 stdio.h 啦,stdlib.h 啦,这里
的 file2.c 啦“拼接”成了一个非常非常大的文件,这个过程是编译系统中的“预处理
器”完成的。然后系统中的“编译器”就在这个预处理器生成的非常大的文件上工作了,
并将其翻译成一个“目标文件”。

    在这个目标文件中,会有一些C语言的对象(如函数,全局变量等等,他们在C语言中
统称“对象”,注意和C++对象不同)是“有名字的”,你也会用到一些有名字却不知道
在哪儿的对象,比如 printf ,那么为了整个程序最后能执行,C编译器就需要去查找哪
儿有这个 printf ,它按照这个名字去一些预定的地方,以及你提供给它的地方去找各种
做好的“目标文件”,然后在这些目标文件中查找有没有一个东西叫做 printf  ,一旦
找到了,系统中的“链接器”就会用这个目标文件中导出的 printf 的接口在你自己的目
标文件里面替换,最后把所有的目标文件合起来做成一个大文件,这个文件就是可执行程
序了。

    C程序都是通过预处理(前面说的拼接)、编译(将拼接后的翻译单元变成目标文件
)、链接(在各个目标文件中根据名字查找需要的对象)三个过程,利用预处理器,编译
器和链接器三个工具才会变成最终可以被执行的程序的。[1]

[1]: 这里要提到的是,大多数现代的编译系统中的“编译器”的部分,实际上是由两个
部分组成的:一个叫做编译器,功能是将预处理器处理后的结果变成汇编语言代码,一个
叫做汇编器,功能是将汇编代码变成最后的目标文件。这部分内容和主题无关,这里就省
略掉了。

    这里我们可以把一个又一个的目标文件看做是一个个的黑盒子,把有名字的对象当作
这个黑盒子上的“接口”,我们将不同的黑盒子接在一起,然后整个黑盒子组合就能工作
了,这就是链接过程。而编译就是从被处理好的程序文本中生成黑盒子的过程,当然我们
今天提到的包含就是处理程序文本的其中一个过程。

    在C语言里面,什么样的对象有资格可以在目标文件间有一个名字呢?答案是“外部
链接性对象”,C语言的对象都是有链接性的。这又是一个大话题。我们现在只需要知道
有办法让C的函数或者全局变量等等这些对象的名字被其他目标文件“看到”即可。这种
让其他目标文件看到自己的名字的对象所具有的链接性,就叫做“外部链接性”。C语言
的函数默认具有外部链接性。而特殊的静态函数不具有外部链接性。

    从上面的介绍中,大家可以看到,如果你在 .c 文件中写了一些函数定义,而将其
include 到其他的 .c 文件中去了,那么如果同时编译这两个 .c 文件。那么就在两个目
标文件中有相同的名字被所有人知道。编译器不知道该选择那个对象有资格拥有这个名字
,于是链接器拒绝进行链接操作,这就是链接错误。

    也就是说,谭老的这种写法是很容易出问题的。如果你只编译链接 file2.c,那么一
切都没有问题,但是如果你同时编译这两个C文件,那么就会导致一个链接错误。这也就
是为什么前面提到的,被包含的文件必须是 .h 文件的原因。因为我们约定俗成不会在
.h 文件里面书写定义,我们只是在 .h 文件里面告诉编译器:在某个目标文件中有一个
对象,它的名字叫做 printf 或者 rand ,所以即使多个 .c 文件包含了同一个 .h 文件
,也不会造成链接错误。最重要的是,约定俗成地,我们默认一个翻译单元就是一个 .c
文件,而 .h 文件除非被主动包含否则是不参与编译过程的。这样就很安全了。

    我们来总结一下,在C语言的世界里,我们约定俗成:
        - .c 文件约定俗成地是作为一个独立翻译单元存在的。它的功能就是真正的生
          成一系列对象,让链接器“有米可炊”。
        - .h 文件是用于向其他模块导出,告诉别人“我这里有这么一个对象,快来用
          吧”的。它存在的目的是告诉编译器“这个就是米”
        - 而至于“米在哪儿”的问题,则是链接器的工作,由链接器自行寻找,我们只
          是偶尔提供一些启示,告诉它在哪儿寻找而已。

    谭老完全无视掉了这种约定俗成的规则,这会对初学者造成很大的误导。这实际上是
饮鸩止渴,谭老自己是自圆其说了,但是会给初学者造成很大的困扰和危害,甚至会导致
初学者走很多的弯路而不自知。

    那么,这种约定俗成是不是就是金科玉律呢?我们可不可以包含其他的非 .h 文件呢
?特别的,我们可不可以包含 .c 文件呢?

    虽然我们不提倡初学者这么做,但事实是,在很多情况下,这种现象是会出现的。很
多实际的项目都用到了这个技术。这个技术的出现是为了解决一些C语言在实际开发中的
固有问题。

    一种很通常的情况出现在一些小项目里,为了方便编译,这样的小项目通常会提供一
个所谓的 amalgamation 文件,在这个文件中,将所有的 .c 源代码包含进来,然后要编
译这个项目,只需要编译这个单个的 amalgamation 文件即可。这种技术的出现或者是为
了方便小项目被嵌入其他工程中,或者简化编译过程,或者方便编译器优化,或者加快编
译速度等等原因。但是我们要知道,这种做法和一个一个地编译 .c 文件在语法上是完全
等价的。它更多地是一种为了方便考虑而出现的解决方案。

    还有一种情况是某个实现需要跨平台,或者支持多种不同的实现。这个时候,我们会
将各种不同的实现或者针对不同操作系统的部分写成一个 .c 文件,并在主文件里面进行
选择性的包含。这种情况其实是可以通过编译指令来完成的,不过如果结合下面的这种情
况后,包含 .c 文件的方案能获得额外的好处。另外的好处是,如果程序需要同时支持多
个后端,则可以通过声明预处理符来改变一个 .c 文件的行为,并通过多次包含而获得多
个后端。Lua 的 lmd5 模块就是这么做的。

    另外一种情况是项目很复杂,而由很多很多的函数组成,我们知道C的函数默认是具
有外部链接性的。但是项目维护者恰巧不愿意这些小函数被外界知道了——也就是说,项
目维护者不希望链接器“看得到”有这些函数存在,于是维护者将其声明为静态函数,即
不允许它们被其他翻译单元(目标文件)“看见”。然而我们知道C语言的编译是以翻译
单元为单位的,既然不允许被其他文件所看到,那么这些静态函数就不得不被写在同一个
文件里面了。当项目很大时,这可能会造成一个文件就有几十万行代码。为了避免这个问
题,项目维护者将这些内部的小函数分别写在独立的文件中,将其声明为静态的,然后利
用包含让他们实际上属于同一个翻译单元。这样就能同时解决上述的两个问题了。

    但是,即便是这种情况,起一个不容易被误解的后缀名,比如 .inc 也是很有益的。
如果想要文件能够自动被编辑器认出是C文件,那么在后缀名非末尾的部分带一个 .c 就
行了(比如 .c.inc 这种),大多数的编辑器都会读取所有的后缀并选择一个最合适的。
虽然只是改一个后缀名,这样还是会给项目维护带来好处。而直接包含 .c 文件仍然是被
认为很不自然,也很违反直觉的。

    这里要提到一点,这里修改后缀名只是为了习惯考虑。如果不改后缀名,这些 .c 文
件即使被编译也不会造成恶劣的影响,因为这些文件中的函数都是隐藏的。实际上最后编
译出来的目标文件根本不会具有任何的接口,也不可能被链接器选中链接的。

    对于后面提出的情况,这里仍然有一个很不错的解决方案,它比直接包含 .c 文件更
好,比包含 .inc 文件要清晰要约定俗成得多。这个方案就是将这些小函数写成静态函数
,并放到私有头文件中,然后由需要的文件去包含这些私有的头文件。因为私有的头文件
比私有的源代码文件要安全得多,也好管理得多,也不容易造成上面提到的编辑器不认的
问题。最后,这些私有头文件可以起很明显的名字,有助于项目维护,甚至这些私有头文
件可以和公开头文件放在不同的地方,更不容易造成误解。

    我们知道,C语言是很自由的一门语言,那么为了更好地合作和沟通,我们在C语言之
外就会有很多约定俗成的规则。所谓“规则的出现就是为了被打破的”,我们并不是要读
者去墨守成规。但是,读者在做开发的时候,千万千万记住自己到底要做什么,不要为了
图个新鲜,或者为了打破规则而去打破规则。如果做得每件事情都有自己的道理,都经过
了自己独立的思考,那么打破规则也是可以的。

    然而,教科书要有教科书自己的考虑。教科书要告诉读者这些规则是什么,为什么会
产生这些规则,不遵守这些规则会怎么样,什么情况下可以打破这些规则。谭老的书里却
完全没有提到这些。我想是因为谭老自己都不知道有这些规则吧。谭书的这种做法,实在
不是一本稍微入流的教科书的做法。读者如果没有自我清晰的认识,是很容易被误导的。

    最后再强调一次,奇技淫巧是可以的,但是读者一定要非常明确“自己要做什么,除
此以外有没有更好的办法做这件事,有没有标准和自然的方法做这件事”。C语言如此自
由,所以需要开发者更清醒地去驾驭它。

论坛徽章:
2
程序设计版块每日发帖之星
日期:2015-06-17 22:20:00每日论坛发贴之星
日期:2015-06-17 22:20:00
146 [报告]
发表于 2012-02-07 21:00 |只看该作者
提示: 作者被禁止或删除 内容自动屏蔽

论坛徽章:
5
狮子座
日期:2013-08-20 10:12:24午马
日期:2013-11-23 18:04:102015年辞旧岁徽章
日期:2015-03-03 16:54:152015亚冠之德黑兰石油
日期:2015-06-29 18:11:1115-16赛季CBA联赛之新疆
日期:2024-02-21 10:00:53
147 [报告]
发表于 2012-02-08 03:07 |只看该作者
本帖最后由 starwing83 于 2012-02-08 03:10 编辑

回复 114# pmerofc


    不知道应该在哪个地方提及一下,谭氏风格有一项就是所有的变量声明都写在了函数开头,然而这不是必须的,这也不是好习惯。事实上,虽然C90不允许随意书写声明,但是C90的声明是必须写在块的开头而不是写在函数的开头,因此实际上绝大多数情况下C90是可以跟C++一样,把声明写在函数中间的,比如这里:

{
    int temp = i;
    i = m;
    m = temp;
}

这样会更好维护一些。

很少有需要在函数中间无缘无故地去写声明的,大多数情况还是在一个判断/循环体里面去写,这样就不会违反C90的限制了,然而,如果实在要在函数体中间写也不是没有办法:

{
    int i;
    ...
}

养成随时使用随地声明绝对是个好习惯。另外,这个习惯也会带来实际的好处:在块中写声明,一旦离开块则变量空间会被释放,在稍微复杂的程序里面这样会节约栈空间。

论坛徽章:
2
程序设计版块每日发帖之星
日期:2015-06-17 22:20:00每日论坛发贴之星
日期:2015-06-17 22:20:00
148 [报告]
发表于 2012-02-08 14:19 |只看该作者
提示: 作者被禁止或删除 内容自动屏蔽

论坛徽章:
1
2015年辞旧岁徽章
日期:2015-03-03 16:54:15
149 [报告]
发表于 2012-02-08 14:25 |只看该作者
  1.    while( printf("please input the value of n: ") ,
  2.                         scanf("%d",&n) ,
  3.                         n<=0 )
  4.       printf("error!");
复制代码
这个写法太二逼了。

论坛徽章:
2
程序设计版块每日发帖之星
日期:2015-06-17 22:20:00每日论坛发贴之星
日期:2015-06-17 22:20:00
150 [报告]
发表于 2012-02-08 15:02 |只看该作者
提示: 作者被禁止或删除 内容自动屏蔽
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP