18. 好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。
"促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
“阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
tr1::shared_ptr支持定制型删除器。这可防范DLL问题,可被用来自动解除互斥锁等待。
19. Class的设计就是type的设计。在定义一个新type之前,请确定你已经考虑过本条款覆盖的所有讨论主题。
20.宁
以pass-by-reference-to-const替代pass-by-value
尽量以pass-by-reference-to-const替代pass-by-value.前者通常比较高效,并可避免切割问题。
以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较合适。
21.必须返回对象时,别妄想返回其reference
绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static 对象而有可能同时需要多个这样的对象。
22.切记将成员变量声明为private.这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。
protected并不比public更具封装性。
23.如果你需要为某个函数的所有参数(包括被this指针所指的哪个参数隐喻参数)进行类型转换,那么这个函数必须是个non-member.
注解:
如果成员变量不是public,客户唯一能够访问对象的办法就是通过成员函数。如果public接口内的每样东西都是函数,客户就不需要在打算访问class成员时迷惑地试着记住是否使用小括号。如果你通过函数访问成员变量,日后可改以某个计算替换这个成员变量,而class客户一点也不会直到class地内部实现已经发生变化,这事封装地特性。如果你不隐藏成员变量,即使拥有class原始码,改变任何public事物地能力还是极端受到束缚,因为那会破坏太多客户码。Public意味着不封装,而不封装意味着不可改变,特别是堆被广泛使用地class而言。被广泛使用地class是最需要封装地一个族群,因为他们最能够从“改采用一个较佳实现版本”中获益
某些东西的封装性与“当其内容改变的时候可造成的代码破坏量”成反比。假设我们有一个protected成员变量,当我们要最终取消它的时候,所有从它继承的派生类都会受到影响。
protected成员变量就行public成员变量一样缺乏封装性,因为在这两种情况下,如果成员变量被改变,都会有不可预知的大量代码受到破坏。太多的代码需要重写、重新测试、重新编写文档、重新编译。
从封装的角度来说其实只有两种访问权限:private(提供封装)和其他(不提供封装).
24.若所有参数皆需类型转换,轻为此采用non-member函数
如果你需要为某个函数的所有参数(包括this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-memeber.
25.考虑写出一个不抛异常的swap函数
当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
如果你提供一个memeber swap,也应该提供一个non-member swap用来调用前者。对于class ,可以特化std::swap。
为“用户类型”进行std templates 全特化是好的,但千万不要尝试在std内加入某些对std而言全是新的东西。
调用swap时针对std::swap使用using声明式,然后调用swap并不带任何"命名空间资格修饰".
26.尽可能延后变量定义式地出现。这样做可增加程序的清晰度并改善程序的效率。
27.如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代设计。
如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用改函数,而不需将转型放进他们自己的代码内。
宁可使用C++ style(新式)转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的职掌。
28.避免返回handlers(包括references、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生”虚吊号码牌”的可能性降至最低。
29.异常安全函数(Exception-safe functions)即使发生异常不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
“强烈保证”往往能够以copy-and-swap实现出来,但”强烈保证”并非对所有函数都可实现或具备现实意义。
函数提供的”异常安全保证"通常最高只等于其所调用之各个函数的”异常安全保证”中的最弱者。
注解:
较少的代码就是好的代码,因为出错机会比较少,而且一旦有所改变,被误解的机会也比较少。
基本型:如果异常被抛出,程序内的任何事物仍然保持在有效的状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态。
强烈型:如果常量被抛出,程序状态不改变。调用这样的函数需要这样的认知:如果函数成功,就是完全成功,如果函数失败,程序会回复到”调用函数之前”的状态。
不抛异常型:承诺不抛出异常,因为它们总是能够完成它们原先的承诺。作用于内置类型身上的所有操作都提供nothrow保证。但是者很难满足,而合使用动态内存的东西如果无法找到足够内存以满足需求,通常便会抛出一个bad_alloc异常。对于大部分函数而言,抉择往往落在基本保证和强烈保证之间。
以对象管理资源是良好的设计的根部。
有一个一般化的设计很典型地会导致强烈保证,很值得熟悉它。这个策略被称为copy and swap.原则很简单:为你打算修改地对象(原件)做出一份副本,然后在那副本身上做一切必要地修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过地那个副本和原对象地一个不抛出异常地操作中置换 (swap)。
如果系统内有一个(唯一一个)函数不具备异常安全性,整个系统就不具备异常安全性,因为调用那个函数可能导致资源泄漏或数据结构败坏。
如何让自己撰写地代码更具安全性:1.以对象管理资源,那可阻止资源泄漏。2.挑选三个异常保证中地某一个实施于你所写地每一个函数身上。
个人特别喜欢书中最后的一段话:
“四十年前,满载goto的代码被视为一种美好实践,而今天我们却致力于写出结构化控制流。二十年前,全局数据被视为一种美好的实践,而今我们却致力于数据的封装。十年前,撰写“未将异常考虑在内”的函数被视为一种美好的实践,而今我们致力于写出”异常安全的代码””.
30.将大多数inline限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
不要只因为function templates出现在头文件,就将它们声明为inline.
31. 支持"编译依存最小化”的一般构想是:相依于声明式,不要相依于定义式。给予此构想的两个手段是Handle classes和Interface classes.
程序库头文件应该以"完全且仅有声明式”的形式存在。这种做法不论是否涉及templates都适用。
注解:
#include
#include "date.h"
#include "address.h"
class Person
{
public:
Person(const std::string& name,const Date& birthday,const Address &addr);
std::string name() const;
std::string birthday() const;
std::string address() const;
private:
std::string theName;
Date theBirthday;
Address theAddress;
}
在编译 class person 需要包含string Date 和Address 类的相应的头文件,这样在Person定义文件和其含入文件之间形成了一种编译依存关系。如果这些头文件中有任何一个被修改,或这些文件所依赖的其他头文件有任何改变,那么每一个含入Person class的文件就得重新编译,任何使用Person class的文件也必须重新编译。这样的连串编译依存关系会对许多项目造成难以形容的灾难。
可以使用两种方式来解除接口和实现之间的耦合关系:组合(Handle classes)和继承(Interface classes)
第一种方法
//Person.h
#include
#include
class PersonImpl;
class Date;
class Address;
class Person
{
public:
Person(const std::string& name,const Date& birthday,const Address &addr);
std::string name() const;
std::string birthday() const;
std::string address() const;
private:
std::tr1::shared_ptr pImpl;
}
//Person.cpp
#include "Person.h"
#include "PersonImpl.h"
Person::Person(const std::string& name,const Date& birthday,const Address &addr)
:pImpl(new PersonImpl(name,birthday,addr))
{
}
std::string Person::name() const
{
return pImpl->name();
}
在上面的代码中,Person类内含一个指针成员,指向其实现类,这种设计叫“Pointer to implementation”。在这样的设计下,Person的客户就完全与Dates,Address以及Person的实现细目分离了,那些classes的任何实现修改都不需要Person客户端重新编译。此外由于客户无法看到Person的实现细目,也就不可能写出什么”取决于那些细节“的代码。这真正是”接口和实现分离”!
这种分离的关键在于以”声明的依存性“替换”定义的依存性“,这正是编译依存性最小化的本质:现实中让头文件尽可能自我满足,万一做不到,则让它与其他文件的声明式(而非定义式)相依。可以使用如下的一些策略达到这样的目的:
1.如果使用object reference或object pointerrs可以完成的任务,就不要使用objects
2.如果能够,尽量以class声明式替换class定义式。
注意:当你声明一个函数而它用到某个class时,你并不需要该class的定义;纵使函数以by value的方式传递该类型的参数(或返回值)。但是一旦任何人调用那些函数,调用之前Date定义式一定得先曝光才行。
第二种方法
#include
#include
class PersonImpl;
class Date;
class Address;
class Person
{
public:
static std::str1::shared_ptr create(const std::string& name,
const Date& birthday,
const Address& addr);
virtual std::string name() const = 0;
virtual std::string birthday() const = 0;
virtual std::string address() const = 0;
virtual ~Person();
};
class RealPerson:public Person
{
public:
RealPerson(const std::string& name,const Date& birthday,const Address &addr)
:theName(name),theBirthday(birthday),theAddress(addr)
{
}
std::string name() const;
std::string birthday() const;
std::string address() const;
virtual ~Person();
private:
std::string theName;
Date theBirthday;
Address theAddress;
};
std::tr1::shared_ptr Person::create(const std::string& name,
const Date& birthday,
const Address& addr)
{
return std::tr1::shared_ptr(new RealPerson(name,birthday,addr));
}
//
客户端使用
std::string name;
Date dateOfBirth;
Address address;
std::tr1::shared_ptr pp(Person::create(name,dateOfBirth,address));
std::cout << pp->name()
<< pp->birthday()
<< pp->address();