咱们鼓励在编程时应有清晰的哲学思惟,而不是给予硬性规则。我并不但愿大家能承认全部的东西,由于它们只是观点,观点会随着时间的变化而变化。但是,若是不是直到如今把它们写在纸上,长久以来这些基于许多经验的观点一直积累在个人头脑中。所以但愿这些观点能帮助大家,了解如何规划一个程序的细节。(我尚未看到过一篇讲关于如何规划整个事情的好文章,不过这部分能够是课程的一部分)要是能发现它们的特质,那很好;要是不认同的话,那也很好。但若是能启发大家思考为何不认同,那样就更好了。在任何状况下,都不该该照搬我所说的方式进行编程;要用你认为最好的编程方式来尝试完成程序。请一以贯之并且绝不留情的这么作。node
欢迎在评论区留言讨论~程序员
程序是一种出版物。意味着程序员们会先阅读(也许是几天、几周或几年后的你本身阅读),最后才轮到机器。机器的快乐就是程序能编译,机器才不在意程序写的有多么漂亮,但是人们应该保持程序的美观。有时人们会过分关心:用漂亮的打印机呆板地打印出漂亮的输出,而这些输出只是将全部介词用英文文本以粗体字体凸显出来,都是些与程序无关的细节。虽然有不少人认为程序就应该像 Algol.68 所描述的同样(有些系统甚至要求照搬该风格编写程序),可清晰的程序不会由于这样的呈现而变得更清晰,只会使糟糕的程序变得更好笑。算法
对于清晰的程序来讲,排版规范一贯都是相当重要的。固然,众所周知最有用的是缩进,可是当墨水遮盖了意图时,就会控制住排版。所以即使坚持使用简单的旧打字机输出,也该意识到愚蠢的排版。避免过分修饰,好比保持注释的简洁和灵活。经过程序整齐一致地说出想表达的。接着往下看。编程
对于变量名称,长度并非名称的价值所在,清晰的表达才是。不经常使用的全局变量可能会有一个很长的名称,像 maxphysaddr。在循环中每一行所使用的数组索引,并不须要取一个比 i 更详尽的名字。取 index 或者 elementnumber 会输入更多的字母(或调用文本编辑器),而且会遮盖住计算的细节。当变量名称很长时,很难明白发生了什么。在必定程度上,这是排版问题,看看下面数组
for(i=0 to 100) array[i]=0
vs.网络
for(elementnumber=0 to 100) array[elementnumber]=0;
现实例子中的问题会变得更糟。因此仅需把索引当成符号来对待。数据结构
指针也须要合理的符号。np 仅仅只是做为指针 nodepointer 的助记符。若是一向都听从命名规范,那么很容易就能推断出 np 表示“节点指针”。在下一篇文章中会提到更多。编程语言
同时在编程可读性的其它方面,一致性也是极其重要的。假使变量名为 maxphysaddr,则不要给同级关系的变量取名 lowestaddress。编辑器
最后,我倾向于「最小长度」但「最大信息量」的命名,并让上下文补齐其他部分。例如:全局变量在使用时不多有上下文帮助理解,那么它们的命名相对而言更须要使人易懂。所以我称 maxphyaddr 做为一个全局变量名,对于在本地定义和使用的指针来讲 np 并不必定是 NodePoint。这是品味的问题,但品味又与清晰度相关。函数
我避免在命名时嵌入大写字母;它们的阅读温馨性太别扭了,像糟糕的排版同样使人心烦。
C 语言不一样寻常,由于它容许指针指向任何事物。指针是锋利的工具,像任何这样的工具同样,使用得当能够产生使人愉悦的生产力,但使用不当也能够形成极大的破坏。指针在学术界的名声不太好,由于它太危险了,莫名其妙地就变得糟糕的不行。但我认为它是强大的符号,它能够帮助咱们清楚地自我表达。
思考:当有指针指向对象时,对于那个对象,确切地说它只是名称,其它什么也不是。听起来很琐碎,但看看下面的两个表达式:
np node[i]
第一个指向一个 node(节点),第二个计算为(能够说)同一个 node。但第二种形式是不太容易理解的表达式。这里解释一下,由于咱们必需要知道 node 是什么,i 是什么,还要知道 i 和 node 与周围程序之间相关的规则是什么。孤立的表达式并不能说明 i 是 node 的有效索引,更不用提是咱们想要元素的索引。若是 i、j 和 k 都是 node 数组中的索引将很容易出差错,并且连编译器都不能帮助找出错误。当给子程序传参数时,尤为容易出错:指针只是一个单独的参数;但在接收的子程序中必须认为数组和索引是一体的。
计算为对象表达式自己,比该对象的地址更不易察觉,并且容易出错。正确使用指针能够简化代码:
parent->link[i].type
vs.
lp->type.
若是想取下一个元素的 type 能够是
parent->link[++i].type
或
(++lp)->type.
i 前移,但其他的表达式必须保持不变;用指针的话,只须要作一件事,就是指针前移。
把排版因素也考虑进来。对于处理连续的结构体来讲,使用指针比用表达式可读性更好:只须要较少的笔墨,并且编译器和计算机的性能消耗也很小。与此相关的问题是,指针类型会影响指针正确使用,这也就容许在编译阶段使用一些有用的错误检测,来检查数组序列不能分开。并且若是是结构体,那么它们的标签字段就是其类型的提示。所以
np->left
是足以让人明白的。若是是索引数组,数组将取一些精心挑选的名字,并且表达式也会变得更长:
node[i].left.
此外,因为例子变得愈来愈大,额外的字符更加让人恼火。
通常来讲,若是发现代码中包含许多类似并复杂的表达式,并且表达式计算为数据结构中的元素,那么明智地使用指针能够消除这些问题。考虑一下
if(goleft) p->left=p->right->left; else p->right=p->left->right;
看起来像利用复合表达式表示 p。有时这值得用一个临时变量(这里的 p)或者把运算提取成一个宏。
过程名称应该代表它们是作什么的,函数名称应该代表它们返回什么。函数一般在像 if 这样的表达式使用,所以可读性要好。
if(checksize(x))
是没有太大帮助的,由于不能推断出 checksize 错误时返回 true,仍是非错误时返回。相反
if(validsize(x))
使这点能清晰表达,而且在常规使用中未来也不大可能出错。
这一个微妙的问题,须要本身体会和判断。因为一些缘由,我倾向于宁肯清除注释。第一,假如代码清晰,而且使用了规范的类型名称和变量名称,应该从代码自己就能够理解。第二,编译器不能检查注释,所以不能保证准确,特别是代码修改过之后。误导性的注释会很是使人困惑。第三,排版问题:注释会使代码变得杂乱。
但有时我会写注释,像下文同样仅仅只是把它们用于介绍。例如:解释全局变量的使用和类型(我老是在庞大的程序中写注释);做为一个不寻常或者关键过程的介绍;或标记出大规模计算的一节。
有一个糟糕注释风格的例子:
i=i+1; /* Add one to i */
还有更糟糕的作法:
/********************************** * * * Add one to i * * * **********************************/ i=i+1;
先不要嘲笑,等到在现实中看到再去吧。
或许除了诸如重要数据结构的声明(对数据的注释一般比对算法的更有帮助),这样相当重要部分以外,须要避免对注释的“可爱”排版和大段的注释;基本上最好就不要写注释。若是代码须要靠注释来讲明,那最好的方法是重写代码,以便能更容易地理解。这就把咱们带到了复杂度。
许多程序过于复杂,比须要有效解决的问题更加复杂。这是为何呢?大部分是因为设计很差,但我会跳过这个问题,由于这个问题太大了。然而程序每每在微观层面就很复杂,有关这些能够在这里解决。
规则 1:不要判定程序会在什么地方耗费运行时间。 瓶颈老是出如今使人意想不到的地方,直到证明瓶颈在哪,不要试图再次猜想并加快运行速度。
规则 2:估量(measure) 在没有对代码作出估量以前不要优化速度,除非发现最耗时的那部分代码,要不也不要去作。
规则 3:当 n 很小时(一般也很小),花哨的算法运行很慢。 花哨算法有很大的常数级别复杂度。在你肯定 n 老是很大以前, 不要使用花哨算法。(即便假如 n 变大,也优先使用规则 2).例如,对于常见问题,二叉树总比伸展树高效。
规则 4:花哨的算法比简单的算法更容易有 bug,并且实现起来也更困难 尽可能使用简单的算法与简单的数据结构。
如下几乎是全部实际程序中用到的数据结构:
固然也必需要有把这些数据结构灵活结合的准备,好比用哈希表实现的符号表,其中哈希表是由字符型数组组成的链表。
规则 5:以数据为核心 若是选择了适当的数据结构并把一切都组织得颇有条理性,算法老是不言而喻的。编程的核心是数据结构,而不是算法。(参考 Brooks p. 102)
规则 6:就是没有规则 6。
不像许多 if 语句,算法或算法的细节一般以紧凑、高效和明确的数据进行编码。眼前的工做能够编码,归根究竟是因为其复杂性都是由不相干的细节组合而成。分析表是典型例子,它经过一种解析固定、简单代码段的形式,对编程语言的语法进行编码。有限状态机特别适合这种处理形式,可是几乎任何涉及到对构建数据驱动算法有益的程序,都是将某些抽象数据类型的输入“解析”成序列,序列会由一些独立“动做”构成。
也许这种设计最有趣的地方是表结构有时能够由另外一个程序生成(经典案例是解析生成器)。有个更接地气的例子,假如操做系统是由一组表驱动,这组表包含链接 I/O 请求到相应设备驱动的操做,那么能够经过程序“配置“系统,该程序能够读取到某些特殊设备与可疑机器链接的描述,并打印相应的表。
数据驱动程序在初学者中不常见的缘由之一是因为 Pascal 的专制。 Pascal 像它的创始人同样,坚信代码要和数据分开。于是(至少在原始形式上)没法建立初始化的数据。与图灵和冯诺依曼的理论背道而驰,这些理论可都是定义存储计算机的基本原理。代码和数据是同样的,或至少能够算是。还能怎样解释编译器的工做原理呢?(函数式语言对 I/O 也有相似的问题)
Pascal 专制的另外一个结果是初学者不使用函数指针。(在 Pascal 中没有把函数做为变量) 用函数指针来处理编码复杂度会有一些使人感兴趣的地方。
指针指向的程序有必定的复杂度。这些程序必须遵照一些标准协议,像要求一组都是相同调用的程序就是其中之一。除此以外,所要实现的只是完成业务,复杂度是分散的。
有个协议的主张是既然全部使用的功能类似,那么它们的行为也必须类似。这对简单的文档、测试、程序扩展和甚至使程序经过网络分布都有帮助——远程过程调用能够经过该协议进行编码。
我认为面相对象编程的核心是清晰使用函数指针。规定好要对数据执行的一系列操做,以及对这些操做响应的整套数据类型。将程序合拢到一块儿最简单的方法是为每种类型使用一组函数指针。简而言之,就是定义类和方法。固然,面向对象语言提供了更多更漂亮的语法、派生类型等等,但在概念上几乎没有提出额外的东西。
数据驱动程序与函数指针的结合,变成了一种表现使人惊讶的工做方法。根据个人经验,这种方法常常会产生惊喜的结果。即便没有面向对象语言,无需额外的工做也能够得到 90% 的好处,而且能更好地管理结果。我没法再推荐出更高标准的实现方式。我全部的程序都是由这种方式组织管理,并且通过屡次开发后都相安无事——远远优于缺乏约束的方法。也许正如所说:从长远来看,约束会带来丰厚的回报。
简单规则:包含(include)文件时应该永远不要嵌套包含。 若是声明(在注释或隐式声明里)须要的文件没有优先包含进来,那么使用者(程序员)要决定包含哪些文件,但要以简单的方式处理,并采用避免多重包含的结构。多重包含是系统编程的祸根。将文件包含五次或更屡次来编译一个单独的 C 源文件的事情家常便饭。Unix 系统中 /usr/include/sys 就用了这么可怕的方式。
说到 #ifdef,有一个小插曲,虽然它能防止读取两次文件,但实际上常常用错。#ifdef 是定义在文件自己中,而不是文件包含它。结果是经常致使让成千上万没必要要的代码经过词汇分析器,这是(优秀编译器中)耗费最大的阶段。
只需听从以上简单规则,就能让你的代码变得优雅而美观,至少也是赏心悦目,从技术变成艺术~~
最后仍是要推荐下小编的C/C++学习群:710520381,邀请码(柳猫),无论你是小白仍是大牛,小编我都欢迎,不按期分享干货,包括小编本身整理的一份2018最新的C/C++和0基础入门教程,欢迎初学和进阶中的小伙伴。