`
sunguanxing
  • 浏览: 1083231 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

系统程序员成长计划-写得又快又好的秘诀

阅读更多
http://www.limodev.cn/blog
“快” 是指开发效率高,“好”是指软件质量高。呵呵,写得又快又好的人就是高手了。记得这是林锐博士下的定义,读他那篇著名的《C/C++高质量编程》时,我还 是个初学者,印象特别深。我现在仍然赞同他的观点,不过这里标题改为成为高手的秘诀,感觉就有点像标题党了,所以还是用比较通俗的说法吧。废话少说,请读 者回顾一下这段时间的编程经验,回答下面两个问题:

1.快与好是什么关系?写得快就不能写得好?写得好就不能写得快?还是写得好才能写得快?是不是绕晕了?不过这确实是值得思考的问题。

2.我们的时间花在哪里了?记得刚来深圳时到华为面试,面试的人是我的学长。他问我,你一天能写多少行代码?我想了想说,100行吧。他用看外行的 眼光看着我说,能写100行吗?我知道说错话了,赶快补充说,嗯,从整个项目来看可能没有吧。他才点了点头。一天只写100行代码?初学者可能觉得不可思 议,以同时应付10个网友聊天的速度,写100行代码不用三分钟。不过,经过这段时间的练习后,我们想大家已经明白,敲代码不是花时间最多的地方,那时间 又花到哪里去了呢?

1.好与快的关系

几年前和一个朋友聊天时,他抱怨他的上司说,要我写得好又要写快,那怎么可能呢?我当时一愣,反问到,写不好怎么可能写得快?他也一愣。

传统观点认为在功能、成本(人*时间)和质量这个铁三角中,提高质量就意味投入更多成本或者减少一些功能。在功能不变的情况下,不可能在提高质量的同时降低开成成本(对个人来讲就是缩短开发时间)。我的朋友持的正是这种传统观点。

而根据我的经验来看,结论恰恰相反。每次我想以牺牲质量来加快速度的时候,结果反而花了更多时间,甚至可能到最后搞不定而放弃了。有了多次这样的经 验之后,我决定把每一步都做好,从开发的前期来看,我花的时间比别人多一点,但从整个任务来看,我反而能以别人几倍的速度完成任务。时间长了,我形成了这 样的观念:只有写得好才可能写得快。

两种观点截然相反,所以我们都愣了。虽然我相信我的经验没有错,但传统的铁三角定律是大师们总结出来的,也不可能出错。那是怎么回事呢?我开始到处 查资料,但是没有一个人支持我的观点。我又不想这样放弃,后来我用了一个简单的办法进行推理,结果证明两个观点都有各自的适用范围。

这个推理过程很简单,把两种观点推向极端:

先看看以牺牲质量来追求进度的例子。我以前参加过两个大项目,其一个项目的BUG总数达到17000多个,耗时近三年后项目被取消。另一个项目的 BUG总数也超过10000个,三年之后带着很多BUG发布了,结果可想而知,产品很快从市场上消失了。这两个项目在开始阶段都制定了极其可笑的项目计 划,为了赶在这个根本不可能的最后期限前,都采用了牺牲质量的方式来提高开发速度,前期进展都很“顺利”,基本功能点很快就完成了,但是项目马上陷入了无 止境的debug之中,开发人员的士气一下跌到谷底,管理层开始暴跳如雷。

如果这两个项目有超过170000个BUG,即使项目不取消,再做时间十年也做不完。由此可见:质量低到一定限度时,降低质量会延长项目时间,如果质量降到最低,那项目永远也不可能完成。这和我的观点一致:写不好就写不快。

再看看追求完美质量的例子。以前参与一个手机模拟器的开发,我们很快达到88%的真实度,半年之后达到95%的真实度,客户要98%的真实度。但是怎么努力也达不到这个标准,花了极大的代价才达到96%多一点,到后来项目被取消了。

如果要达到99%的真实度,即使项目不取消,再做十年也做不完。由此可见:质量高到一定程度,提高质量会延长项目时间,如果质量要高到最高,那任务远也不可能完成。这和传统观点一致,提高质量就要延长开发时间。

从两个极端往中间走,我们可以找到一个中间质量点。低于这个质量点,想以牺牲质量来赶进度,那只会适得其反,质量越低耗时越长。高于这个质量点,想提高质量就得增加成本,质量越高开发时间越长。这样两种观点就统一起来了。

如果在大多数项目中,这个中间质量点是可以作为高质量接受的,那我们就找到了又快又好的最佳方法。这个质量点到底是多少?呵,我可以告诉你,那是87.5。但是谁知道怎么去度量呢?没有人知道,只能凭感觉和经验了。

2.我们的时间花在哪里

经过这段时间的练习,大多数人都体会到敲代码不是耗费时间最多的地方,一个高效率的程序员,并不是打字比别人快,而他节省了别人浪费了的时间。我常 说达到别人五倍的效率并不难,因为在软件开发中,大部分人的大部分时间都浪费掉了,你只要把别人浪费的时间省下来,你的效率就提高上去了。像在优化软件性 能时采用的方法一样,要优化程序员的性能,我们要找出性能的瓶颈。也就是弄清楚我们的时间花在哪些地方,然后想办法省掉某些浪费了的时间。根据我的经验, 耗费时间最多的地方有:

o 分析

需求分析通常是SPEC工程师(或者所谓的系统分析员)的任务,程序员也会参与到这个过程中,但程序员的任务主要是理解需求,然后分析如何实现它 们,这个分析工作也就是软件设计。无论你是在计算机上用设计工具画出正规的软件架构图,还在纸上用自然语言描述出算法的逻辑,甚至在脑海中一闪而过的想法 都是设计。设计其实就是打草稿,把你的想法进行推敲,最后得到可行的方案。设计文档只是设计的结果,是设计的表现形式,没有写设计文档,并不代表没有做设 计(但是写设计文档可以加深你的思考)。

设计本身是一个思考过程,需要耗费大量时间,对于新手来说更是如此。前面几节中的需求并不难,理解它们只需要很少的时间,但要花不少时间去思考其实 现的方法。这个时间因人而异,有的读者到最后也没有想出办法,这没有关系,没有人天生就会的,不会的原因只是因为你暂时还不知道常用的设计方法,甚至连基 本数据结构和算法都不熟悉。

在后面的章节中,我们会一步步的深入学习各种常用设计方法,反复练习基本数据和算法,熟能生巧,软件设计也一样,在你什么都不懂的时候,不可能做出好的设计。你要学习各种经典的设计方法,开始可能生搬硬套,多写多练多思考,到后来就随心所欲了,设计的时间就会大大缩短。

o测试

要写得好自然离不开测试,初学者都有这个概念。他们忠实的使用了教科书上讲的方法,用scanf输入数据,做些操作之后,用printf打印来,这 是一个完美的输入-处理-输出的过程。测试也就是要保证正确的输入能产生正确的输出,这种方法的原理是没有错的,但它们确实耗费了我们大量时间。

如果测试只需要做一次,这种方法还是可取的,问题是每次修改之后都要重复这个过程,耗费的时间就更多了。这种工作单调乏味,而且很难坚持做下去,单 元测试做得不全面,就有更多BUG等着就调试了。时间久了,或者换人维护了,谁也搞不清楚什么样输入产生什么样的输出,结果可能是连测试也省了,那就等着 把大量的时间浪费在调试上吧。总而言之,这种测试方法不好,我们需要更有效的测试方法才行。

o调试

测试时发现程序有BUG,自然要用调试器了,对一些人来说,调试是一件充满挑战和乐趣的事。而对大部分人来说,特别是对我这种做过两年专职调试的人 来说,调试是件无趣无聊无用的工作。熟练使用调试器是必要的,在分析现有软件时,调试器是非常有用的工具。但在开发新软件时,调试器在浪费我们的时间。

调试器是最后一招,只有迫不得已时才使用。一位敏捷方法的高手说他已经记不得上次使用调试器是什么时候了,我想这就是为什么敏捷方法能够提高开发速度的原因吧。因为没有什么比一次性写好,不用调试器更快的方法了。

知道了浪费时间的地方,接下来几节中,我们将介绍避免浪费时间的方法。学完这些方法之后,我希望读者也能达到普通工程师五倍的效率,呵,读完本系列文章后之,希望你会达到更高。

代码阅读法

软件工程实践已经证明Code Review是提高代码质量最有效的手段之一,极限编程(XP)更是把Code Review推向极致,形成著名的结对编程工作方式,两个程序员在一台电脑前面工作,一个人编写程序,另一个Review输入每一行代码,写程序人的专注 于目前细节上的工作,Review的人同时要从高层次考虑如何改进代码质量,两个人的角色会经常互换。

可惜我即没有结对编程的经验,也没有在CMM3(及以上)团队中工作过。不过现在我要介绍比结对编程更敏捷更轻量级,但是同样有效的Review方 法。这种方法不需要其他程序员配合,有你自己就够了。为了把这种方法与传统的Code Review区分开来,我把它称为代码阅读法吧。

很多初学者包括一些有经验的程序员,在敲完代码的最后一个字符后,马上开始编译和运行,迫不急待的想看到自己的工作成果。快速反馈有助于满足自己的成就感,但是同时也会带来一些问题:

让编译器帮你检查语法错误可以省些时间,但程序员往往太专注这些错误了,以为改完这些错误就万事大吉了。其实不然,很多错误编译器是发现不了的,像 内存错误和线程死锁等等,这些错误可能逃过简单的测试而遗留在代码中,直到集成测试或者软件发布之后才暴露出来,那时就要花更大代价去修改它们了。

修改完编译错误之后就是运行程序了,运行起来有错误,就轮到调试器上场了。花了不少时间去调试,发现无非是些低级错误,或许你会自责自己粗心大意, 但是下次可能还是犯同样的错误。更严重的是这种debug & fix的方法,往往是头痛医头脚痛医脚,导致低质量的软件。

让编译器帮你检查语法错误,让调试器帮你查BUG,这是天经地义的事,但这确实是又慢又烂的方法。就像你要到离家东边1000米的地方开会,结果你 往西边走,又是坐车又是搭飞机,花了一周时间,也绕着地球转了一周,终于到了会议室,你还大发感慨说,现代的交通工具真是发达啊。其实你往东走,走路也只 要十多分钟就到了。不管你的调试技巧有多高,都不如一次性写好更高效。

我以前也一样,想赶时间结果花了更多时间,在经过很多痛苦的经历之后,我开始学会放松自己,让自己慢下来。写完程序之后,我会花些时间去阅读它,一 遍两遍甚至多遍之后,才开始编译它,只要有时间,在通过测试之后,我还会阅读它们,每读一遍都有不同的收获,有时候会发现一些错误,有时候会做些改进,有 时候也有新的想法。

下面是我在阅读自己代码时的一些方法:

o检查常见错误。

第一遍阅读时主要关注语法错误、代码排版和命名规则等等问题,只要看不顺眼就修改它们。读完之后,你的代码很少有低级错误,看起来也比较干净清爽。 第二遍重点关注常见编程错误,比如内存泄露和可能的越界访问,变量没有初始化,函数忘记返回值等等,在后面的章节中,我会介绍这些常见错误,避免这些错误 可以为你省大量的时间。如果有时间,在测试完成之后,还可以考虑是否有更好的实现方法,甚至尝试重新去实现它们。说了读者可能不相信,在学习编程的前几 年,我经常重写整个模块,只我觉得能做得更好,能验证我的一些想法,或提高我的编程能力,即使连续几天加班到晚上十一点,我也要重写它们。

o模拟计算机执行。

常见错误是比较死的东西,按照检查列表一条一条的做就行了。有些逻辑通常不是这么直观的,这时可以自己模拟计算机去执行,假想你自己是计算机,读入 这些代码时你会怎么处理。这种方法能有效的完善我们的思路,考虑不同的输入数据,各种边界值,这能帮助我们想到一些没有处理的情况,让程序的逻辑更严谨。

o假想讲给朋友听。

据说在Code Review时发现错误的,往往不是Review的人而是程序员自己。我也有很多这样的经历,在讲给别人听的时候,别人还没有听明白,自己已经发现里面存 在的错误了。上大学时,我常常把写的或者学到的东西讲给隔壁寝室的一个同学听,他说他从我这里学到很多知识,其实我从讲的过程中,经常发现一些问题,对提 高自己的能力大有帮助。可惜并不是随时都能找到好的听众,幸好我们有另外一个替代办法,记得刚开始写程序时看过一本书(忘记名字了),作者说他在写程序 时,常常把思路讲给他的布娃娃听。我没有布娃娃当听众,讲给鼠标听总是有点怪怪的,所以就假想旁边有个朋友,我把自己的思路讲给他听,同时也假想他来质 疑我。这种方法很效,能够让自己的思路更清晰,据说一些大师也经常使用这种方法。

这种代码阅读法会花你一些时间,但是可以省下更多调试时间,而且能够提高代码质量,可以说是名符其实的“又快又好的” 秘诀之一。至于读几遍合适,要根据情况而定,个人觉得读两到三遍是最佳的投资。

避免常见错误

在C语言中,内存错误是最为人诟病的。这些错误让项目延期或者被取消,引发无数的安全问题,甚至出现人命关天的灾难。抛开这些大道理不谈,它们确实 浪费了我们大量时间,这些错误引发的是随机现象,即使有一些先进工具的帮助,为了找到重现的路径,花上几天时间也不足为怪。如果能够在编写代码的时候避免 这些错误,开发效率至少提高一倍以上,质量可以提高几倍了。这里列举一些常见的内存错误,供新手参考。

o 内存泄露

大家都知道,在堆上分配的内存,如果不再使用了,应该把它释放掉,以便后面其它地方可以重用。在C/C++中,内存管理器不会帮你自动回收不再使用的内存。如果你忘了释放不再使用的内存,这些内存就不能被重用了,这就造成了所谓的内存泄露。

把内存泄露列为首位,倒并不是因为它有多么严重的后果,而因为它是最为常见的一类错误。一两处内存泄露通常不至于让程序崩溃,也不会出现逻辑上的错 误,加上进程退出时,系统会自动释放该进程所有相关的内存(共享内存除外),所以内存泄露的后果相对来说还是比较温和的。但是,量变会导致质变,一旦内存 泄露过多以致于耗尽内存,后续内存分配将会失败,程序可能因此而崩溃。

现在PC机的内存够大了,加上进程有独立的内存空间,对于一些小程序来说,内存泄露已经不是太大的威胁。但对于大型软件,特别是长时间运行的软件,或者嵌入式系统来说,内存泄露仍然是致命的因素之一。

不管在什么情况下,采取谨慎的态度,杜绝内存泄露的出现,都是可取的。相反,认为内存有的是,对内存泄露放任自流都不是负责的。尽管一些工具可以帮助我们检查内存泄露问题,我认为还是应该在编程时就仔细一点,及早排除这类错误,工具只是用作验证的手段。

o 内存越界访问

内存越界访问有两种:一种是读越界,即读了不属于自己的数据,如果所读的内存地址是无效的,程度立刻就崩溃了。如果所读内存地址是有效的,在读的时 候不会出问题,但由于读到的数据是随机的,它会产生不可预料的后果。另外一种是写越界,又叫缓冲区溢出,所写入的数据对别人来说是随机的,它也会产生不可 预料的后果。

内存越界访问造成的后果非常严重,是程序稳定性的致命威胁之一。更麻烦的是,它造成的后果是随机的,表现出来的症状和时机也是随机的,让BUG的现象和本质看似没有什么联系,这给BUG的定位带来极大的困难。

一些工具可以够帮助检查内存越界访问的问题,但也不能太依赖于工具。内存越界访问通常是动态出现的,即依赖于测试数据,在极端的情况下才会出现,除 非精心设计测试数据,工具也无能为力。工具本身也有一些限制,甚至在一些大型项目中,工具变得完全不可用。比较保险的方法还是在编程是就小心,特别是对于 外部传入的参数要仔细检查。

我们来看一个例子:

#include <stdlib.h>
#include <string.h>

int main(int argc, char* argv[])
{
    char str[10];
    int array[10] = {0,1,2,3,4,5,6,7,8,9};

    int data = array[10];
    array[10] = data;

    if(argc == 2)
    {
        strcpy(str, argv[1]);
    }

    return 0;
}
这个例子中有两个错误是新手常犯的:

其一:int array[10] 定义了10个元素大小的数组,由于C语言中数组的索引是从0开始的,所以只能访问array[0]到array[9],访问array[10]就造成了越界错误。

其二:strcpy(str, argv[1]);这里是否存在越界错误依赖于外部输入的数据,这样的写法在正常下可能没有问题,但受到一点恶意攻击就完蛋了。除非你确定输入数据是在你 控制内的,否则不要用strcpy、strcat和sprintf之类的函数,而要用strncpy、strncat和snprintf代替。

o 野指针。

野指针是指那些你已经释放掉的内存指针。当你调用free(p)时,你真正清楚这个动作背后的内容吗?你会说p指向的内存被释放了。没错,p本身有变化吗?答案是p本身没有变化。它指向的内存仍然是有效的,你继续读写p指向的内存,没有人能拦得住你。

释放掉的内存会被内存管理器重新分配,此时,野指针指向的内存已经被赋予新的意义。对野指针指向内存的访问,无论是有意还是无意的,都为此会付出巨大代价,因为它造成的后果,如同越界访问一样是不可预料的。

释放内存后立即把对应指针置为空值,这是避免野指针常用的方法。这个方法简单有效,只是要注意,当然指针是从函数外层传入的时,在函数内把指针置为 空值,对外层的指针没有影响。比如,你在析构函数里把this指针置为空值,没有任何效果,这时应该在函数外层把指针置为空值。

o 访问空指针。

空指针在C/C++中占有特殊的地址,通常用来判断一个指针的有效性。空指针一般定义为0。现代操作系统都会保留从0开始的一块内存,至于这块内存有多大,视不同的操作系统而定。一旦程序试图访问这块内存,系统就会触发一个异常/信号。

操作系统为什么要保留一块内存,而不是仅仅保留一个字节的内存呢?原因是:一般内存管理都是按页进行管理的,无法单纯保留一个字节,至少要保留一个页面。保留一块内存也有额外的好处,可以检查诸如p=NULL; p[1]之类的内存错误。

在一些嵌入式系统(如arm7)中,从0开始的一块内存是用来安装中断向量的,没有MMU的保护,直接访问这块内存好像不会引发异常。不过这块内存是代码段的,不是程序中有效的变量地址,所以用空指针来判断指针的有效性仍然可行。

o 引用未初始化的变量。

未初始化变量的内容是随机的(有的编译器会在调试版本中把它们初始化为固定值,如0xcc),使用这些数据会造成不可预料的后果,调试这样的BUG也是非常困难的。

对于态度严谨的程度员来说,防止这类BUG非常容易。在声明变量时就对它进行初始化,是一个好的编程习惯。另外也要重视编译器的警告信息,发现有引用未初始化的变量,立即修改过来。

在下面这个例子中,全局变量g_count是确定的,因为它在bss段中,自动初始化为0了。临时变量a是没有初始化的,堆内存str是没有初始化 的。但这个例子有点特殊,因为程序刚运行起来,很多东西是确定的,如果你想把它们当作随机数的种子是不行的,因为它们还不够随机。

#include <stdlib.h>
#include <string.h>

int g_count;

int main(int argc, char* argv[])
{
    int a;
    char* str = (char*)malloc(100);

    return 0;
}
o 不清楚指针运算。

对于一些新手来说,指针常常让他们犯糊涂。

比如int *p = …; p+1等于(size_t)p + 1吗

老手自然清楚,新手可能就搞不清了。事实上, p+n 等于 (size_t)p + n * sizeof(*p)

指针是C/C++中最有力的武器,功能非常强大,无论是变量指针还是函数指针,都应该非常熟练的掌握。只要有不确定的地方,马上写个小程序验证一下。对每一个细节了然于胸,在编程时会省下不少时间。

o 结构的成员顺序变化引发的错误。

在初始化一个结构时,老手可能很少像新手那样老老实实的,一个成员一个成员的为结构初始化,而是采用快捷方式,如:

Struct s
{
    int   l;
    char* p;
};

int main(int argc, char* argv[])
{
    struct s s1 = {4, "abcd"};

    return 0;
}
以上这种方式是非常危险的,原因在于你对结构的内存布局作了假设。如果这个结构是第三方提供的,他很可能调整结构中成员的相对位置。而这样的调整往 往不会在文档中说明,你自然很少去关注。如果调整的两个成员具有相同数据类型,编译时不会有任何警告,而程序的逻辑可能相距十万八千里了。

正确的初始化方法应该是(当然,一个成员一个成员的初始化也行):

struct s
{
    int   l;
    char* p;
};

int main(int argc, char* argv[])
{
    struct s s1 = {.l=4, .p = "abcd"};

    return 0;
}
(有的编译器可能不支持新标准)

o 结构的大小变化引发的错误。

我们看看下面这个例子:

struct base
{
    int n;

};

struct s
{
    struct base b;
    int m;
};
在OOP中,我们可以认为第二个结构继承了第一结构,这有什么问题吗?当然没有,这是C语言中实现继承的基本手法。

现在假设第一个结构是第三方提供的,第二个结构是你自己的。第三方提供的库是以DLL方式分发的,DLL最大好处在于可以独立替换。但随着软件的进化,问题可能就来了。

当第三方在第一个结构中增加了一个新的成员int k;,编译好后把DLL给你,你直接把它给了客户了,让他们替换掉老版本。程序加载时不会有任何问题,在运行逻辑可能完全改变!原因是两个结构的内存布局重叠了。

解决这类错误的唯一办法就是重新编译全部代码。由此看来,动态库并不见得可以动态替换,如果你想了解更多相关内容,建议你阅读《COM本质论》。

o 分配/释放不配对。

大家都知道malloc要和free配对使用,new要和delete/delete[]配对使用,重载了类new操作,应该同时重载类的delete/delete[]操作。这些都是书上反复强调过的,除非当时晕了头,一般不会犯这样的低级错误。

而有时候我们却被蒙在鼓里,两个代码看起来都是调用的free函数,实际上却调用了不同的实现。比如在Win32下,调试版与发布版,单线程与多线 程是不同的运行时库,不同的运行时库使用的是不同的内存管理器。一不小心链接错了库,那你就麻烦了。程序可能动则崩溃,原因在于在一个内存管理器中分配的 内存,在另外一个内存管理器中释放时就会出现问题。

o 返回指向临时变量的指针

大家都知道,栈里面的变量都是临时的。当前函数执行完成时,相关的临时变量和参数都被清除了。不能把指向这些临时变量的指针返回给调用者,这样的指针指向的数据是随机的,会给程序造成不可预料的后果。

下面是个错误的例子:

char* get_str(void)
{
    char str[] = {"abcd"};

    return str;
}

int main(int argc, char* argv[])
{

    char* p = get_str();

    printf("%s\n", p);

    return 0;
}
下面这个例子没有问题,大家知道为什么吗?

char* get_str(void)
{
    char* str = {"abcd"};

    return str;
}

int main(int argc, char* argv[])
{

    char* p = get_str();

    printf("%s\n", p);

    return 0;
}
o 试图修改常量

在函数参数前加上const修饰符,只是给编译器做类型检查用的,编译器禁止修改这样的变量。但这并不是强制的,你完全可以用强制类型转换绕过去,一般也不会出什么错。

而全局常量和字符串,用强制类型转换绕过去,运行时仍然会出错。原因在于它们是放在.rodata里面的,而.rodata内存页面是不能修改的。试图对它们修改,会引发内存错误。

下面这个程序在运行时会出错:

int main(int argc, char* argv[])
{
    char* p = "abcd";
    *p = '1';

    return 0;
}
o 误解传值与传引用

在C/C++中,参数默认传递方式是传值的,即在参数入栈时被拷贝一份。在函数里修改这些参数,不会影响外面的调用者。如:

#include <stdlib.h>
#include <stdio.h>

void get_str(char* p)
{

    p = malloc(sizeof("abcd"));

    strcpy(p, "abcd");

    return;
}

int main(int argc, char* argv[])
{
    char* p = NULL;

    get_str(p);

    printf("p=%p\n", p);

    return 0;
}
在main函数里,p的值仍然是空值。当然在函数里修改指针指向的内容是可以的。

o 重名符号。

无论是函数名还是变量名,如果在不同的作用范围内重名,自然没有问题。但如果两个符号的作用域有交集,如全局变量和局部变量,全局变量与全局变量之 间,重名的现象一定要坚决避免。gcc有一些隐式规则来决定处理同名变量的方式,编译时可能没有任何警告和错误,但结果通常并非你所期望的。

下面例子编译时就没有警告:

t.c

#include <stdlib.h>
#include <stdio.h>

int count = 0;

int get_count(void)

{
    return count;
}

main.c

#include <stdio.h>

extern int get_count(void);

int count;

int main(int argc, char* argv[])
{
    count = 10;

    printf("get_count=%d\n", get_count());

    return 0;

}
如果把main.c中的int count;修改为int count = 0;,gcc就会编辑出错,说multiple definition of `count’。它的隐式规则比较奇妙吧,所以还是不要依赖它为好。

o 栈溢出。

我们在前面关于堆栈的一节讲过,在PC上,普通线程的栈空间也有十几M,通常够用了,定义大一点的临时变量不会有什么问题。

而在一些嵌入式中,线程的栈空间可能只5K大小,甚至小到只有256个字节。在这样的平台中,栈溢出是最常用的错误之一。在编程时应该清楚自己平台的限制,避免栈溢出的可能。

o 误用sizeof。

尽管C/C++通常是按值传递参数,而数组则是例外,在传递数组参数时,数组退化为指针(即按引用传递),用sizeof是无法取得数组的大小的。

从下面这个例子可以看出:

void test(char str[20])
{
    printf("%s:size=%d\n", __func__, sizeof(str));


int main(int argc, char* argv[])
{
    char str[20]  = {0};

    test(str);

    printf("%s:size=%d\n", __func__, sizeof(str));

    return 0;
}

[root@localhost mm]# ./t.exe
test:size=4
main:size=20
o 字节对齐。

字节对齐主要目的是提高内存访问的效率。但在有的平台(如arm7)上,就不光是效率问题了,如果不对齐,得到的数据是错误的。

所幸的是,大多数情况下,编译会保证全局变量和临时变量按正确的方式对齐。内存管理器会保证动态内存按正确的方式对齐。要注意的是,在不同类型的变量之间转换时要小心,如把char*强制转换为int*时,要格外小心。

另外,字节对齐也会造成结构大小的变化,在程序内部用sizeof来取得结构的大小,这就足够了。若数据要在不同的机器间传递时,在通信协议中要规定对齐的方式,避免对齐方式不一致引发的问题。

o 字节顺序。

字节顺序历来是设计跨平台软件时头疼的问题。字节顺序是关于数据在物理内存中的布局的问题,最常见的字节顺序有两种:大端模式与小端模式。

大端模式是高位字节数据存放在低地址处,低位字节数据存放在高地址处。

小端模式指低位字节数据存放在内存低地址处,高位字节数据存放在内存高地址处;

在普通软件中,字节顺序问题并不引人注目。而在开发与网络通信和数据交换有关的软件时,字节顺序问题就要特殊注意了。

o 多线程共享变量没有用valotile修饰。

关键字valotile的作用是告诉编译器,不要把变量优化到寄存器里。在开发多线程并发的软件时,如果这些线程共享一些全局变量,这些全局变量最好用valotile修饰。这样可以避免因为编译器优化而引起的错误,这样的错误非常难查。

o 忘记函数的返回值

函数需要返回值,如果你忘记return语句,它仍然会返回一个值,因为在i386上,EAX用来保存返回值,如果没有明确返回,EAX最后的内容被返回,所以EAX的内容是随机的。



分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics