实现
尽可能延后变量定义式的出现时间
这样做可以增加程序的清晰度并改善程序效率。
你不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初始值为止。如果这样,不仅能够避免构造和析构非必要对象,还可以避免无意义的default构造行为。
void Foo()
{
//...
string xxx;
if(xxx) throw error("...");
return xxx;
}
void FooNew()
{
//...
string xxx(yyy);
return xxx;
}
尽量少做类型转换动作
如果可以,尽量避免转型,特别是在注重效率的代码中避免
dynamic_cast
。
如果转型时必要的,试着将它隐藏在某个函数背后,客户随后可以调用该函数,而不需要将转型放进它们自己的代码内。
宁可使用C++风格的转型,不要使用旧式转型。
- 旧式转型
C风格转型:(T)expression
函数风格转型:T(expression)
- C++风格转型
const_cast<>()
通常用来将对象的常量性转除(cast away the constness)。它也是唯一有此能力的C++风格转型操作符。dynamic_cast<>()
主要用来执行“安全向下转型”,也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。reinterpret_cast<>()
意图执行低级转型。实际动作可能取决于编译器,这也就表示它不可移植。(这一类转型在低级代码之外很少见)static_cast<>()
用来强迫隐式转换,例如将non-static对象转换为const对象,或将int转为double等。(他无法将const转换为non-const,这只有const_cast
能办到)
- 注意事项
C++中对于一个多继承的类对象进行指针类型转换,可能得到不同的指针值,单继承有时也会有这种情况,就是因为内存排布问题。(C语言、Java/C#都不可能发生这种情况)
避免返回handles指向对象内部成分
避免返回handles(包括引用、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将dangling handles的可能性降至最低。
CRE:主要是防止外部对对象进行更改,破坏了封装性。
const Rectangle rect;
rect.getUpperLeft().SetX(50);
//getUpperLeft的调用者能够使用返回指向rect内部的reference更改成员。但是rect其实应该是不可变的!
异常安全
当异常抛出时,带有异常安全性的函数会:
- 不泄露任何资源。(解决资源泄露的问题很容易,前面的关于RAII的部分讨论过如何以对象管理资源)
- 不允许数据败坏。
异常安全保证级别:
- 基本保证:即使发生异常,不会泄露资源。数据可能变成部分修改的状态,但是不会变成不一致或无效状态。
- 强保证:要么完全成功,要么不修改任何数据并回滚到函数调用前的状态。(通常需要复制数据或者利用临时数据结构)
- 不抛异常保证:函数承诺不抛出任何异常。(通常用于析构函数和内存分配等关键操作)
强保证可以使用copy-and-swap方式实现。
函数提供的”异常安全保证”通常最高只等于其所调用的各个函数的”异常安全保证”中的最弱的那个。(CRE:木桶短板)
inline
将大多数inline限制在小型、被频繁调用的函数身上。这可以使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
inline函数背后的整体观念是,将“对此函数的每一个调用”都以函数本体替换之。
过度热衷inlining会造成程序体积太大,造成的代码膨胀甚至可能影响CPU缓存命中率,伴随着效率损失。
inline只是对编译器的一个申请,不是强制命令。这项申请可以明确提出,也可以隐喻提出。(virtual函数通常也会让inline落空)
CRE:成员函数直接实现在类定义中就是隐喻内联。
不要只因为函数模板声明在头文件中,就将它们声明为inline。模板的实现与inlining无关。如果你正在写一个template而你认为所有根据此template具现出来的函数都应该inlined,请将此template声明为inline。但是如果你写的template没有理由要求它所具现的每一个函数都是inlined,就应该避免将这个template声明为inline。
inline的另一个问题。如果修改一个内联函数f,所有用到f的客户端程序都必须重新编译。然而如果是非inline的函数,一旦它有任何修改,客户端只需要重新链接就好,远比重新编译好。
将文件之间的编译依赖关系降至最低
支持“编译依赖性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle类和Interface类。
程序库头文件应该以“完全且仅有声明式”的形式存在。这种做法不论是否涉及模板都适用。
如果修改头文件中一个类定义的private成员变量,那么可能导致所有包含或使用这个类的文件都要重新编译。
编译器看到一个类对象的定义式,他需要直到需要分配多少内存空间,所以就需要类的定义式。
考虑将成员变量和其他实现细目移到单独的实现类中,主类维护一个指针指向实现类。这种设计称为pimpl(pointer-to-implementation)。
使用pimpl模式的类,往往被称为Handle类。这样的类要真正做点事情,办法之一是将它们的所有函数转交给相应的实现类并由后者实际完成实际工作。
class Foo {
public:
Foo();
~Foo(); // 需要在析构函数中定义,以确保正确删除impl
private:
class Impl;
std::unique_ptr<Impl> pImpl;
};
// Foo.cpp
class Foo::Impl {
public:// 实现细节
};
另一个制作Handle类的方法,是让一个类成为一种特殊的抽象基类,成为Interface类。这种类的目的是详细地一一描述派生类的接口,因此它不带成员变量也没有构造函数,只有一个virtual析构函数以及一组纯虚(pure virutal)函数,用来描述整个接口。然后使用接口的一个特殊方法(实际是作为实现类的构造函数角色)来创建对象。
class Person
{
public:
virtual ~Person();
virtual string name() const = 0;
virtual void dosth() const = 0;
//...
static shared_ptr<Person> CreateInstance(const string& name);
}
int main()
{
shared_ptr<Person> person(Person::CreateInstance("xxx"));
cout << person->name();
}
(END)