《C++编程规范》二、设计风格


第5条 一个实体应该只有一个紧凑的职责

一次只解决一个问题:只给一个实体(变量、类、函数、名字空间、模块和库)赋予一个定义良好的职责。随着实体变大,其职责范围自然也会扩大,但是职责不应该发散。



第6条 正确、简单和清晰第一

软件简单为美(Keep It Simple Software,KISS):质量优于速度,简单优于复杂,清晰优于机巧,安全优于不安全(见第83条和第99条)。
简单设计和清晰代码的价值怎么强调都不过分。代码的维护者将因为你编写的代码容易理解而感谢你——而且这个维护者往往就是未来的你,要努力回忆起6个月前的所思所想。

于是有了下面这些经典的格言警句。

  • 程序必须为阅读它的人而编写,只是顺便用于机器执行。——Harold Abelson 和 Gerald Jay Sussman
  • 编写程序应该以人为本,计算机第二。——Steve McConnell
  • 计算机系统中最便宜、最快速、最可靠的组件还不曾出现过。——Gordon Bell [2]
  • 所缺乏的恰恰是最精确(永不出错),最安全(坚不可摧),以及设计、文档编写、测试和维护起来最容易的部分。简单设计的重要性怎么强调也不过分。——Jon Bentley


第7条 编程中应知道何时和如何考虑可伸缩性

小心数据的爆炸性增长:不要进行不成熟的优化,但是要密切关注渐近复杂性。处理用户数据的算法应该能够预测所处理的数据量耗费的时间,最好不差于线性关系。如果能够证明优化必要而且非常重要,尤其在数据量逐渐增长的情况下,那么应该集中精力改善算法的O(N)复杂性,而不是进行小型的优化,比如节省一个多余的加法运算。



第8条 不要进行不成熟的优化

拉丁谚语云,快马无需鞭策:不成熟优化的诱惑非常大,而它的无效性也同样严重。优化的第一原则就是:不要优化。优化的第二原则(仅适用于专家)是:还是不要优化。再三测试,而后优化。

我们将不成熟的优化定义为这样的行为:以性能为名,使设计或代码更加复杂,从而导致可读性更差,但是并没有经过验证的性能需求(比如实际的度量数据和与目标的比较结果)作为正当理由,因此本质上对程序没有真正的好处。毫无必要而且无法度量的优化行为其实根本不能使程序运行得更快,这种情况简直是太常见了。

请永远记住:

让一个正确的程序更快速,比让一个快速的程序正确,要容易得太多、太多。

因此,默认时,不要把注意力集中在如何使代码更快上;首先关注的应该是使代码尽可能地清晰和易读(见第6条)。清晰的代码更容易正确编写,更容易理解,更容易重构——当然也更容易优化。使事情复杂的行为,包括优化,总是以后再进行的——而且只在必要的时候进行。



第9条 不要进行不成熟的劣化

放松自己,轻松编程:在所有其他事情特别是代码复杂性和可读性都相同的情况下,一些高效的设计模式和编程惯用法会从你的指尖自然流出,而且不会比悲观的替代方案更难写。这并不是不成熟的优化,而是避免不必要的劣化(pessimization)。
避免不成熟的优化并不意味着必然损害性能。所谓不成熟的劣化,指的就是编写如下这些没有必要的、可能比较低效的程序。

在可以通过引用传递的时候,却定义了通过值传递的参数(见第25条)。

在使用前缀 ++ 操作符很合适的场合,却使用后缀版本(见第28条)。

在构造函数中使用赋值操作而不是初始化列表(见第48条)。

如果减少对象的伪临时副本(尤其是在内循环中)并不影响代码的复杂性,那么这个优化就算不上是不成熟的优化。在第18条中,我们提倡尽可能将变量声明为局部的,但是又提到了一个例外情况,即有时候将变量从循环中提出来是有好处的。大多数时候,这一点也不会混淆代码的意图,相反,实际上这有助于澄清循环内部执行了哪些功能,哪些计算是不随循环变化的。当然,应该优先使用算法,而不是显式的循环(见第84条)。

构造既清晰又有效的程序有两种重要的方式:使用抽象(见第11条和第36条)和库(见第84条)。例如,使用标准库的 vector、list、map、find、sort 和其他设施,这些都是由世界级的专家标准化并实现的,不仅能使你的代码更加清晰,更容易理解,而且启动也经常更快。



第10条 尽量减少全局和共享数据

共享会导致冲突:避免共享数据,尤其是全局数据。共享数据会增加耦合度,从而降低可维护性,通常还会降低性能。
应该尽量降低类之间的耦合,尽量减少交互(参阅[Cargill92])。



第11条 隐藏信息

不要泄密:不要公开提供抽象的实体的内部信息
绝对不要将类的数据成员设为public(见第41条),或者公开指向它们的指针或句柄(见第42条)而使其公开,这是一个很常见的信息隐藏的例子,但是它同样适用于更大的实体比如程序库——程序库同样不能暴露内部信息。模块和程序库同样应该提供定义抽象和其中信息流的接口,从而使与调用代码的通信比采用数据共享方式更安全,耦合度更低。



第12条 懂得何时和如何进行并发性编程

如果应用程序使用了多个线程或者进程,应该知道如何尽量减少共享对象(见第10条),以及如何安全地共享必须共享的对象。
其中最重要的问题是避免死锁、活锁(livelock)和恶性的竞争条件(包括加锁不足导致的崩溃)。

  • 第一,必须事先知道该类型的对象几乎总是要被跨线程共享的,否则到头来只不过进行了无效加锁。
  • 第二,必须事先知道成员函数级加锁的粒度是合适的,而且能满足大多数调用者的需要。


第13条 确保资源为对象所拥有。使用显式的RAII和智能指针

利器在手,不要再徒手为之:C++的“资源获取即初始化”(Resource Acquisition Is Initialization,RAII)惯用法是正确处理资源的利器。RAII 使编译器能够提供强大且自动的保证,这在其他语言中可是需要脆弱的手工编写的惯用法才能实现的。分配原始资源的时候,应该立即将其传递给属主对象。永远不要在一条语句中分配一个以上的资源。

C++语言所强制施行的构造函数/析构函数对称反映了资源获取/释放函数对比如fopen/fclose、lock/unlock和new/delete的本质的对称性。这使具有资源获取的构造函数和具有资源释放的析构函数的基于栈(或引用计数)的对象成为了自动化资源管理和清除的极佳工具。

每当处理需要配对的获取/释放函数调用的资源时,都应该将资源封装在一个对象中,让对象为我们强制配对,并在其析构函数中执行资源释放。例如,我们无需直接调用一对非成员函数OpenPort/ClosePort,而是可以考虑如下方法:

class Port {
public:
  Port(const string&destination);   // 调用OpenPort
  ~Port();             // 调用ClosePort
  // ……通常无法复制端口,因此需要禁用复制和赋值……
};

void DoSomething() {
  Port port1( "server1:80" );
  // ……
}// 不会忘记关闭port1;它会在作用域结束时自动关闭

shared_ptr<Port>port2=/*…*/;  // port2在最后一个引用它的
                                // shared_ptr离开作用域后关闭

在实现RAII时,要小心复制构造和赋值(见第49条),编译器生成的版本可能并不正确。如果复制没有意义,请通过将复制构造和赋值设为私有并且不做定义来明确禁用二者(见第53条)。

确保所有资源都为对象所有。最好用智能指针而不是原始指针来保存动态分配的资源。同样,应该在自己的语句中执行显式的资源分配(比如 new),而且每次都应该马上将分配的资源赋予管理对象(比如shared_ptr),否则,就可能泄漏资源,因为函数参数的计算顺序是未定义的(见第31条)。 例如:

void Fun( shared_ptr<Widget> sp1, shared_ptr<Widget> sp2 );
// ……
Fun( shared_ptr<Widget>(new Widget), shared_ptr<Widget>(new Widget) );

这种代码是不安全的。C++标准给了编译器巨大的回旋余地,可以将构成函数两个参数的两个表达式重新排序。说得更具体一些,就是编译器可以交叉执行两个表达式:可能先执行两个对象的内存分配(通过调用operator new),然后再试图调用两个Widget构造函数。这恰恰为资源泄漏准备了温床,因为如果其中一个构造函数调用抛出异常的话,另一个对象的内存就永远也没有机会释放了!(详细情况请参阅 [Sutter02]。)

这种微妙的问题有一个简单的解决办法:遵循建议,绝对不要在一条语句中分配一个以上的资源,应该在自己的代码语句中执行显式的资源分配(比如 new),而且每次都应该马上将分配的资源赋予管理对象(比如shared_ptr)。例如:

shared_ptr<Widget> sp1(new Widget), sp2(new Widget);
Fun( sp1, sp2 );

智能指针有可能会被过度使用。如果被指向的对象只对有限的代码(比如纯粹在类的内部,诸如一个Tree类的内部节点导航指针)可见,那么原始指针就够用了。


posted @ 2024-08-29 17:06  guanyubo  阅读(10)  评论(0编辑  收藏  举报