跳转到内容

优化 C++/编写高效代码/构造和析构

来自 Wikibooks,开放世界中的开放书籍

对象的构造和析构需要时间,尤其是当对象拥有其他对象时。

本节将提供有关减少对象构造次数及其对应析构次数的指南。

变量范围

[编辑 | 编辑源代码]

尽可能晚地声明变量。

为此,程序员必须在最局部的范围内声明所有变量。通过这样做,如果该范围从未到达,则不会构造或析构变量。在范围内尽可能延迟声明意味着如果在声明之前存在提前退出(使用returnbreakcontinue语句),则与变量关联的对象不会被构造或析构。

通常情况下,在例程开始时,没有可用于初始化变量的适当值。因此,变量使用默认值进行初始化,并在以后的赋值中设置正确的值,当它变得可用时。相反,如果变量仅在适当的值可用时定义,则对象使用该值进行初始化,并且不需要随后的赋值。本节中的“初始化”指南建议这样做。

初始化

[编辑 | 编辑源代码]

使用初始化而不是赋值。特别是,在构造函数中,使用初始化列表。

例如,不要写

string s;
...
s = "abc"

string s("abc");

即使类实例(在上面的第一个示例中为 s)没有明确初始化,它仍然会通过默认构造函数自动初始化。

调用默认构造函数后跟赋值操作可能不如仅调用具有相同值的构造函数效率高。

增量/减量运算符

[编辑 | 编辑源代码]

如果表达式值未使用,则使用前缀增量(++)或减量(--)运算符,而不是相应的后缀运算符。

如果递增的对象是基本类型,则前缀运算符和后缀运算符之间没有区别。但是,如果它是复合对象,则后缀运算符会导致创建临时对象,而前缀运算符不会。

因为每个基本类型的对象都可能在将来成为复合对象,所以最好尽可能使用前缀运算符,尤其是在编写对迭代器进行操作的泛型(模板化)代码时。

仅当变量位于更大的表达式中并且必须在表达式计算完成后才递增时才使用后缀运算符。

赋值复合运算符

[编辑 | 编辑源代码]

使用赋值复合运算符(如a += b)而不是简单运算符与赋值运算符的组合(如a = a + b)。

例如,不要写以下代码

string s1("abc");
string s2 = s1 + " " + s1;

写以下代码

string s1("abc");
string s2 = s1;
s2 += " ";
s2 += s1;

通常,简单运算符会创建一个临时对象。在示例中,运算符+创建临时字符串,它们的创建和析构需要很多时间。

相反,使用+=运算符的等效代码不会创建临时对象。

函数参数传递

[编辑 | 编辑源代码]

当您将类型为T的对象x作为参数传递给函数时,请使用以下准则

  • 如果x是仅输入参数,
    • 如果x可能是空值,
      • 通过指向常量的指针传递它 (const T* x),
    • 否则,如果T是基本类型或迭代器或函数对象,
      • 通过值传递它 (T x) 或通过常量值传递它 (const T x),
    • 否则,
      • 通过指向常量的引用传递它 (const T& x),
  • 否则,即如果x是仅输出参数或输入/输出参数,
    • 如果x可能是空值,
      • 通过指向非常量的指针传递它 (T* x),
    • 否则,
      • 通过指向非常量的引用传递它 (T& x)。

通过引用传递比通过指针传递更有效率,因为它有助于编译器消除变量,并且因为被调用者不需要检查引用是否有效或为空。但是,如果参数可能缺失,那么传递空指针比传递可能为虚拟对象的引用和一个布尔值来指示引用是否有效更有效率。

对于可能包含在一个或两个寄存器中的对象,通过值传递比通过引用传递更有效率(或效率相同)。这适用于微小的对象,如基本类型、迭代器和函数对象。对于更大的对象,通过引用传递比通过值传递更有效率,因为使用后者,对象必须复制到堆栈中。

当前复制速度快的复合对象可以通过值传递,从而提高效率。但是,除非对象是迭代器或函数对象(假设始终能够有效复制),否则这种技术存在风险。对象将来的更改可能会增加其大小并使复制变得更昂贵。例如,如果Point类的对象只包含两个float,那么它可以通过值传递来高效地传递;但如果将来添加了第三个float,或者如果两个float变成了两个double,那么通过引用传递可能会更高效。

explicit 声明

[编辑 | 编辑源代码]

将接收单个参数的所有构造函数声明为explicit,除了具体类的复制构造函数。

当编译器执行自动(隐式)类型转换时,非explicit构造函数可能会被自动调用。执行此类构造函数可能需要很长时间。

如果此类转换强制显式,并且代码中未指定新的类名,则编译器可以选择另一个重载函数,避免调用代价高昂的构造函数,或者它可能会生成错误,从而迫使程序员选择另一种方法来避免构造函数调用。

对于具体类的复制构造函数,必须做出区分以允许它们通过值传递。对于抽象类,即使复制构造函数也可以声明为explicit,因为根据定义,抽象类不能被实例化,因此不应该通过值传递此类类型的对象。

转换运算符

[编辑 | 编辑源代码]

仅为了与旧库保持兼容而声明转换运算符(在 C++11 中,将其声明为explicit)。

转换运算符允许隐式转换,因此会遇到本节中“explicit 声明”指南中描述的与隐式构造函数相同的问题。

如果需要此类转换,请提供等效的成员函数,因为它只能被显式调用。

转换运算符唯一可接受的剩余用法是在新库必须与旧的类似库共存时。在这种情况下,使用运算符自动将旧库中的对象转换为新库中相应的类型,反之亦然可能会很方便。

Pimpl 习语

[编辑 | 编辑源代码]

仅当您希望使程序的其余部分独立于类的实现时,才使用 Pimpl 惯用法。

Pimpl 惯用法(表示指向 implementation 的指针)包含仅在对象中存储指向数据结构的指针,该数据结构包含有关对象的所有有用信息。

该惯用法的主要优点是通过降低少量源代码更改导致需要重新编译大量代码行的可能性,从而加快了代码的增量编译。

此惯用法还会使某些操作更有效,例如两个对象的 swap。但是,通常情况下,由于增加了间接级别,它会减慢对对象数据的每次访问,并且在每次创建或复制此类对象时都会导致额外的内存分配。因此,不应将其用于公共成员函数被频繁调用的类。

迭代器和函数对象

[edit | edit source]

确保自定义迭代器和函数对象很小,并且不分配动态内存。

STL 算法按值传递此类对象。因此,如果它们的副本效率不高,则 STL 算法会变慢。

如果迭代器或函数对象出于某种原因需要一个详细的内部状态,则动态分配它并使用共享指针。例如,假设您想在 Linux/OpenBSD /dev/urandom 设备之上实现一个符合 STL 的 32 位 随机数生成器

#include <boost/shared_ptr.hpp>
#include <fstream>

class urandom32 {
        boost::shared_ptr<std::ifstream> device;

  public:
        urandom32() : device(new std::ifstream("/dev/urandom")) { }

        uint32_t operator()()
        {
                uint32_t r;
                device->read(reinterpret_cast<char *>(&r), sizeof(uint32_t));
                return r;
        }
};

在这种情况下,使用指针实际上是必要的,因为 ifstream 类不可复制:它的复制构造函数被声明为 private。此示例使用 boost::shared_ptr 智能指针;为了获得更高的速度,可以使用侵入式引用计数。

华夏公益教科书