C++ Annotations Version 12.5.0 学习(3)

虚基类

如图 14.2 所示,AirCar 代表两个 Vehicle。这不仅导致了选择哪个函数来访问质量数据的歧义,还在 AirCar 中定义了两个质量字段。这有些多余,因为我们可以假设 AirCar 只有一个质量。

然而,可以通过定义那些在派生类的继承树中被多次提及的基类为虚拟基类来实现 AirCar 只包含一个 Vehicle,同时使用多重继承。对 AirCar 类来说,这意味着在从 LandAir 类派生时需要做一些小的修改:

class Land: virtual public Vehicle
{
    // 其他成员
};

class Car: public Land
{
    // 其他成员
};

class Air: virtual public Vehicle
{
    // 其他成员
};

class AirCar: public Car, public Air
{
};

虚拟继承确保 Vehicle 只被添加一次到派生类中。这意味着 Vehicle 被添加到 AirCar 的路径不再依赖于其直接的基类;我们只需声明 AirCar 是一个 Vehicle。图 14.3 显示了虚拟继承后的 AirCar 的内部组织。
在这里插入图片描述

当一个类 Third 从一个基类 Second 继承,而 Second 又从基类 First 继承时,在构造 Third 对象时,Second 类构造函数调用的 First 类构造函数也会被使用。示例:

class First
{
public:
    First(int x);
};

class Second: public First
{
public:
    Second(int x)
    :
    First(x)
    {}
};

class Third: public Second
{
public:
    Third(int x)
    :
    Second(x)
    {}
};

// 调用 First(x)

Second 使用虚拟继承时,上述行为将不再成立。当 Second 使用虚拟继承时,从 Third 调用 Second 的构造函数时,Second 的基类构造函数会被忽略。相反,Second 默认调用 First 的默认构造函数。如下例所示:

class First
{
public:
    First()
    {
        cout << "First()\n";
    }
    First(int x);
};

class Second: public virtual First // 注意:虚拟继承
{
public:
    Second(int x)
    :
    First(x)
    {}
};

class Third: public Second
{
public:
    Third(int x)
    :
    Second(x)
    {}
};

int main()
{
    Third third{ 3 };
    // 显示 `First()`
}

当构造 Third 时,默认使用 First 的默认构造函数。然而,Third 的构造函数可以通过显式指定要使用的构造函数来覆盖这一默认行为。由于 First 对象必须在 Second 可以构造之前就存在,因此必须首先指定它。要在构造 Third(int) 时调用 First(int),可以将 Third 的构造函数定义如下:

class Third: public Second
{
public:
    Third(int x)
    :
    First(x),
    Second(x)
    {}
    // 现在调用 First(int)
};

当使用虚拟继承时,这种行为可能会让人感到困惑,但它在使用虚拟继承的多重继承中是有意义的。考虑 AirCar:当 AirCar 都虚拟继承自 Vehicle 时,AirCar 会初始化共同的 Vehicle 对象吗?如果是这样,哪个会先被调用?如果 AirCar 使用不同的 Vehicle 构造函数呢?所有这些问题可以通过将共同基类对象的初始化责任传递给最终使用共同基类对象的类来避免。在上面的示例中就是 Third。因此,Third 可以在初始化 First 时指定要使用的构造函数。

多重继承也可以用于从不全都使用虚拟继承的类中继承。假设我们有两个类 Derived1Derived2,它们都(可能是虚拟)继承自 Base。现在我们讨论当调用 Final 类的构造函数时,会调用哪些构造函数:

public Derived1, public Derived2;

区分涉及的构造函数时,Base1 指示作为 Derived1 的基类初始化器调用的 Base 类构造函数(类比地,Base2Derived2 调用)。一个普通的 Base 指示 Base 的默认构造函数。

Derived1Derived2 指示在构造 Final 对象时使用的基类初始化器。现在我们准备区分构造 Final 类对象时的各种情况:

  • 类:

    Derived1: public Base
    Derived2: public Base
    

    这是正常的非虚拟多重继承。调用构造函数的顺序如下:

    Base1,
    Derived1,
    Base2,
    Derived2
    
  • 类:

    Derived1: public Base
    Derived2: virtual public Base
    

    只有 Derived2 使用虚拟继承。Derived2 的基类构造函数被忽略。相反,Base 被调用,并且在任何其他构造函数之前调用:

    Base,
    Base1,
    Derived1,
    Derived2
    

    由于只有一个类使用虚拟继承,因此最终 Final 类对象中仍然存在两个 Base 类对象。

  • 类:

    Derived1: virtual public Base
    Derived2: public Base
    

    只有 Derived1 使用虚拟继承。Derived1 的基类构造函数被忽略。相反,Base 被调用,并且在任何其他构造函数之前调用。与第一个(非虚拟)情况不同,Base 现在被调用,而不是 Base1

    Base,
    Derived1,
    Base2,
    Derived2,
    
  • 类:

    Derived1: virtual public Base
    Derived2: virtual public Base
    

    两个基类都使用虚拟继承,因此 Final 类对象中只会存在一个 Base 类对象。调用构造函数的顺序如下:

    Base,
    Derived1,
    Derived2
    

虚拟继承与虚拟函数不同,它是一个纯编译时问题。虚拟继承仅定义编译器如何定义类的数据组织和构造过程。

何时不适用虚拟继承

虚拟继承可以用于合并多个出现的基类。然而,可能会遇到一些情况下多个基类的出现是合适的。考虑下面的 Truck 类的定义(参考第 13.5 节):

class Truck: public Car
{
    int d_trailer_mass;
public:
    Truck();
    Truck(int engine_mass, int sp, char const *nm, int trailer_mass);
    void setMass(int engine_mass, int trailer_mass);
    int mass() const;
};

Truck::Truck(int engine_mass, int sp, char const *nm, int trailer_mass)
    : Car(engine_mass, sp, nm)
{
    d_trailer_mass = trailer_mass;
}

int Truck::mass() const
{
    return
        // 总和:
        Car::mass() +   // 引擎部分
        d_trailer_mass; // 拖车部分
}

这个定义展示了如何构造一个 Truck 对象,该对象包含两个质量字段:一个通过从 Car 继承获得,另一个通过其自身的 int d_trailer_mass 数据成员获得。这种定义当然是有效的,但它也可以被重写。我们可以将 TruckCarVehicle 中派生,从而明确请求 Vehicle 的双重存在:一个用于引擎和驾驶室的质量,另一个用于拖车的质量。

一个小的复杂之处是,如下的类组织:

class Truck: public Car, public Vehicle

是不被 C++ 编译器接受的。由于 Vehicle 已经是 Car 的一部分,因此不需要再次存在。然而,这种组织可以通过一个小技巧来解决。通过创建一个额外的类继承自 Vehicle,并将 Truck 从这个额外的类中派生,而不是直接从 Vehicle 中派生,可以解决问题。只需将 Vehicle 继承到一个名为 TrailerVeh 的类中,然后将 TruckCarTrailerVeh 中派生:

class TrailerVeh: public Vehicle
{
public:
    TrailerVeh(int mass)
        : Vehicle(mass)
    {}
};

class Truck: public Car, public TrailerVeh
{
public:
    Truck();
    Truck(int engine_mass, int sp, char const *nm, int trailer_mass);
    void setMass(int engine_mass, int trailer_mass);
    int mass() const;
};

inline Truck::Truck(int engine_mass, int sp, char const *nm, int trailer_mass)
    : Car(engine_mass, sp, nm),
      TrailerVeh(trailer_mass)
{}

inline int Truck::mass() const
{
    return
        // 总和:
        Car::mass() +   // 引擎部分
        TrailerVeh::mass(); // 拖车部分
}

这种方式允许我们有效地使用多个基类,同时避免了直接从 Vehicle 派生所引发的问题。

运行时类型识别

C++ 提供了两种在运行时检索对象和表达式类型的方法。与 Java 等语言相比,C++ 的运行时类型识别能力较为有限。通常情况下,C++ 使用静态类型检查和静态类型识别。静态类型检查比运行时类型识别可能更安全,也肯定更高效,因此在大多数情况下应优先考虑静态类型检查。但在某些情况下,运行时类型识别是合适的。

C++ 通过 dynamic_casttypeid 操作符提供运行时类型识别功能:

  • dynamic_cast 用于将基类指针或引用转换为派生类指针或引用,这也称为下转型(down-casting)。
  • typeid 操作符返回表达式的实际类型。

这些操作符可以与具有至少一个虚拟成员函数的类的对象一起使用。

dynamic_cast 操作符

dynamic_cast<> 操作符用于将基类指针或引用转换为派生类指针或引用,这也被称为下转型(down-casting),因为转型的方向是沿着继承树向下。dynamic_cast 的操作是在运行时决定的;它只能用于基类声明了至少一个虚拟成员函数的情况。为了使 dynamic_cast 成功,目标类的虚表(Vtable)必须与 dynamic_cast 参数所指向的虚表相同,否则转换会失败,并且在请求指针时返回 0(如果进行了指针转换),或者在请求引用时抛出 std::bad_cast 异常(如果进行了引用转换)。

在以下示例中,从 Base 类指针 bp 获取一个指向 Derived 类的指针:

#include <iostream>
using namespace std;

class Base {
public:
    virtual ~Base();
};

class Derived : public Base {
public:
    char const *toString();
};

inline char const *Derived::toString() { return "Derived object"; }

int main() {
    Base *bp;
    Derived *dp, d;
    bp = &d;

    dp = dynamic_cast<Derived *>(bp);
    if (dp)
        cout << dp->toString() << '\n';
    else
        cout << "dynamic cast conversion failed\n";
}

在上述 if 语句的条件中,验证了 dynamic_cast 的成功与否。此验证是在运行时进行的,因为只有到运行时才知道指针所指向的对象的实际类。

如果提供了基类指针,dynamic_cast 操作符在失败时返回 0,而在成功时返回指向请求的派生类的指针。假设使用了 vector<Base *>。这种向量的指针可能指向各种从 Base 派生的对象。如果基类指针确实指向指定类的对象,则 dynamic_cast 返回该类的指针,否则返回 0。

我们可以通过执行一系列检查来确定指针所指向对象的实际类。例如:

#include <iostream>
#include <vector>
using namespace std;

class Base {
public:
    virtual ~Base();
};

class Derived1 : public Base {};
class Derived2 : public Base {};

int main() {
    vector<Base *> vb(initializeBase());
    Base *bp = vb.front();
    if (dynamic_cast<Derived1 *>(bp))
        cout << "bp points to a Derived1 class object\n";
    else if (dynamic_cast<Derived2 *>(bp))
        cout << "bp points to a Derived2 class object\n";
}

另外,如果有一个指向基类对象的引用,则 dynamic_cast 操作符在下转型失败时会抛出异常。例如:

#include <iostream>
#include <typeinfo>
using namespace std;

class Base {
public:
    virtual ~Base();
    virtual char const *toString();
};

inline char const *Base::toString() {
    return "Base::toString() called";
}

class Derived1 : public Base {};
class Derived2 : public Base {};

Base::~Base() {}

void process(Base &b) {
    try {
        cout << dynamic_cast<Derived1 &>(b).toString() << '\n';
    } catch (std::bad_cast) {}
    try {
        cout << dynamic_cast<Derived2 &>(b).toString() << '\n';
    } catch (std::bad_cast) {
        cout << "Bad cast to Derived2\n";
    }
}

int main() {
    Derived1 d;
    process(d);
}

在此示例中,如果将引用的 dynamic_cast 失败,则会抛出 std::bad_cast 异常。

请注意 catch 子句的形式:bad_cast 是一个类型名。第 17.4.1 节描述了如何定义这样的类型。

dynamic_cast 操作符在现有基类不能或不应该被修改(例如,源代码不可用)而派生类可以被修改的情况下非常有用。接收基类指针或引用的代码可以执行 dynamic_cast 转换以访问派生类的功能。

你可能会想知道 dynamic_cast 的行为与 static_cast 有何不同。static_cast 在使用时,我们告诉编译器必须将指针或引用转换为目标类型的指针或引用。这适用于基类是否声明了虚拟成员函数。因此,所有 static_cast 的操作都可以由编译器确定,以下代码可以正常编译:

class Base {
    // 可能有虚拟成员,也可能没有
};

class Derived1 : public Base {};
class Derived2 : public Base {};

int main() {
    Derived1 derived1;
    Base *bp = &derived1;
    Derived1 &d1ref = static_cast<Derived1 &>(*bp);
    Derived2 &d2ref = static_cast<Derived2 &>(*bp);
}

请注意第二个 static_cast:这里将基类对象转换为 Derived2 类引用。编译器对此没有问题,因为 BaseDerived2 通过继承关系相关。然而,从语义上讲,这并没有意义,因为 bp 实际上指向的是一个 Derived1 类对象。这一点 dynamic_cast 可以检测到。dynamic_caststatic_cast 相似,都用于转换相关的指针或引用类型,但 dynamic_cast 提供了运行时的保护。如果请求的类型与我们实际指向的对象的类型不匹配,dynamic_cast 将失败。此外,dynamic_cast 的使用比 static_cast 更加受限,因为 dynamic_cast 只能用于具有虚拟成员的派生类进行下转型。

最终,dynamic_cast 是一种类型转换,而类型转换应尽可能避免。当需要进行 dynamic_cast 时,应考虑基类是否正确设计。在需要基类引用或指针的代码中,基类接口应该是足够的,使用 dynamic_cast 不应该是必要的。也许基类的虚拟接口可以被修改,以避免使用 dynamic_cast。遇到使用 dynamic_cast 的代码时应保持警惕。在自己代码中使用 dynamic_cast 时,应当清楚地记录为什么选择了 dynamic_cast,而不是避免使用它。

typeid 操作符

dynamic_cast 操作符类似,typeid 通常应用于基类对象的引用,这些引用指向派生类对象。typeid 仅应与提供虚拟成员的基类一起使用。

在使用 typeid 之前,必须包含 <typeinfo> 头文件。

typeid 操作符返回一个 type_info 类型的对象。不同的编译器可能会提供不同的 type_info 类实现,但至少 typeid 必须提供以下接口:

class type_info
{
public:
    virtual ~type_info();
    int operator==(type_info const &other) const;
    int operator!=(type_info const &other) const;
    bool before(type_info const &rhs) const;
    char const *name() const;
private:
    type_info(type_info const &other);
    type_info &operator=(type_info const &other);
};

请注意,这个类有一个私有的拷贝构造函数和一个私有的重载赋值操作符。这防止了代码构造 type_info 对象和将 type_info 对象赋值给彼此。相反,type_info 对象是通过 typeid 操作符构造和返回的。

如果 typeid 操作符接收一个基类引用,它可以返回引用所指向的实际类型的名称。例如:

#include <iostream>
#include <typeinfo>
using namespace std;

class Base {};
class Derived : public Base {};

int main() {
    Derived d;
    Base &br = d;
    cout << typeid(br).name() << '\n';
}

在这个例子中,typeid 操作符接收一个基类引用。它输出“Derived”,即 br 实际指向的类的名称。如果 Base 不包含虚函数,则输出“Base”。

typeid 操作符可以用于确定表达式的实际类型名称,而不仅仅是类类型对象。例如:

cout << typeid(12).name() << '\n';  // 输出:int
cout << typeid(12.23).name() << '\n';  // 输出:double

然而,上述例子只是建议性的。它可能输出 intdouble,但这并不是必然的。如果需要移植性,确保不依赖这些静态的内建字符串。遇到疑问时,请检查编译器生成的内容。

在应用 typeid 操作符来确定派生类的类型时,应使用基类引用作为 typeid 操作符的参数。考虑以下示例:

#include <iostream>
#include <typeinfo>
using namespace std;

class Base {
public:
    virtual ~Base();
};
class Derived : public Base {};

int main() {
    Base *bp = new Derived;

    if (typeid(bp) == typeid(Derived *))   // 1: false
    ...
    if (typeid(bp) == typeid(Base *))      // 2: true
    ...
    if (typeid(bp) == typeid(Derived))     // 3: false
    ...
    if (typeid(bp) == typeid(Base))        // 4: false
    ...
    if (typeid(*bp) == typeid(Derived))    // 5: true
    ...
    if (typeid(*bp) == typeid(Base))       // 6: false
    ...
    Base &br = *bp;
    if (typeid(br) == typeid(Derived))     // 7: true
    ...
    if (typeid(br) == typeid(Base))        // 8: false
    ...
}

在这里,(1)返回 false,因为 Base * 不是 Derived *。 (2)返回 true,因为这两个指针类型相同;(3)和(4)返回 false,因为指向对象的指针不是对象本身。另一方面,如果在上述表达式中使用 *bp,则(1)和(2)返回 false,因为对象(或对对象的引用)不是指向对象的指针,而(5)现在返回 true:*bp 实际上指向一个 Derived 类对象,typeid(*bp) 返回 typeid(Derived)。如果使用基类引用,结果类似:7 返回 true,8 返回 false。

type_info::before(type_info const &rhs) 成员用于确定类的排序顺序。这在比较两个类型是否相等时很有用。该函数返回非零值如果 *this 在所用类型的层次结构或排序顺序中位于 rhs 之前。当比较派生类与其基类时,比较返回 0,否则返回非零值。例如:

cout << typeid(ifstream).before(typeid(istream)) << '\n';  // 0
cout << typeid(istream).before(typeid(ifstream)) << '\n';  // 非 0

对于内建类型,实现者可能会实现:当一个“更宽”的类型与一个“更小”的类型比较时返回非零值,否则返回 0:

cout << typeid(double).before(typeid(int)) << '\n';  // 非 0
cout << typeid(int).before(typeid(double)) << '\n';  // 0

当比较两个相等的类型时,返回 0:

cout << typeid(ifstream).before(typeid(ifstream)) << '\n';  // 0

当将 0 指针传递给 typeid 操作符时,会抛出 bad_typeid 异常。

继承:何时使用以及实现什么

继承不应自动且无意识地应用。通常,组合可以作为替代方案,改进类设计,减少耦合。当使用继承时,应该根据程序员的意图选择适当的继承类型,而不是默认使用公共继承。

我们已经看到,具有多态性的类一方面提供了定义基本类可以请求的功能的接口成员,另一方面提供了可以被重写的虚成员。良好的类设计的标志之一是成员函数按照“一函数,一任务”的原则设计。在当前上下文中:类的成员应该是类的公共或保护接口的一部分,或者应该作为虚成员提供给派生类进行重实现。通常,这归结为在基类的私有部分定义虚成员。这些函数不应由使用基类的代码调用,而是存在于基类以便由派生类重写,利用多态性重新定义基类的行为。

之前在本章的介绍段落中提到的基本原则是:根据里氏替换原则(LSP),类之间的“是一个”(is-a)关系(表示派生类对象是基类对象)意味着派生类对象可以在期望基类对象的代码中使用。在这种情况下,继承用于不是为了让派生类使用基类已经实现的功能,而是为了通过在派生类中重新实现基类的虚成员,以多态的方式重用基类。

在本节中,我们将讨论使用继承的原因。为什么应该(或不应该)使用继承?如果使用,试图实现什么目标?

继承通常与组合竞争。考虑以下两种替代类设计:

class Derived : public Base { ... };
class Composed {
    Base d_base;
    ...
};

为什么以及何时更倾向于使用 Derived 而不是 Composed,反之亦然?在设计 Derived 类时应该使用什么类型的继承?

  • 由于 ComposedDerived 是作为替代方案提供的,我们正在考虑将一个类(DerivedComposed)实现为另一个类的基础。
  • 由于 Composed 本身并不提供 Base 的接口,因此 Derived 也不应提供。基本原则是,当从 Base 实现 Derived 时,应该使用私有继承(private inheritance)。

使用继承还是组合?以下是一些论据:

  • 一般而言,组合结果的耦合度较低,因此应该优先于继承。
  • 组合允许我们定义具有多个相同类型成员的类(例如,一个类具有多个 std::string 成员),而继承无法实现这种需求。
  • 组合允许我们将类的接口与实现分开。这使我们能够修改类的数据组织,而无需重新编译使用我们类的代码。这也被称为桥接设计模式(bridge design pattern)或编译器防火墙(compiler firewall)或 Pimpl(指向实现的指针)惯用法(idiom)。
  • 如果 Base 提供了在实现 Derived 时必须使用的保护接口成员,则必须使用继承。再次强调:由于我们是基于 Base 实现的,继承类型应该是私有的。
  • 受保护继承(protected inheritance)可以考虑用于当派生类(D)本身作为基类时,应该仅将其自身基类(B)的成员提供给从中派生的类(即 D)使用。
  • 当派生类是某种类型的基类,但为了初始化基类,必须使用另一种类类型的对象时,也应使用私有继承。例如,假设有一个新的 istream 类类型(比如:一个可以提取随机数的流 IRandStream),它继承自 std::istream。尽管 istream 可以空构造(通过其 rdbuf 成员稍后接收 streambuf),但显然更倾向于直接初始化 istream 基类。假设已经创建了用于生成随机数的 Randbuffer 类(public std::streambuf),那么 IRandStream 可以从 Randbufferstd::istream 继承。这样可以使用 Randbuffer 基类来初始化 istream 基类。

由于 IRandStream 绝对不是 Randbuffer 的公共继承,因此公共继承不适用。在这种情况下,IRandStream 是基于 Randbuffer 实现的,因此应使用私有继承。IRandStream 的类接口应如下所示:

class IRandStream : private Randbuffer, public std::istream
{
public:
    IRandStream(int lowest, int highest)
        // 定义范围
        : Randbuffer(lowest, highest),
          std::istream(this) // 传递 &Randbuffer
    {}
    ...
};

公共继承 应保留用于符合 LSP 的类。在这些情况下,派生类可以始终替代基类使用,只需代码使用基类的引用、指针或成员(即,概念上派生类是基类)。这最常适用于从提供虚成员的基类派生的类。为了将用户接口与可重定义接口分开,基类的公共接口不应包含虚成员(除了虚析构函数),虚成员应全部在基类的私有部分。这样可以确保基类完全控制重新定义成员的上下文。通常,公共接口仅调用虚成员,但这些成员可以始终被重定义以执行附加的任务。

原型形式的基类通常如下所示:

class Base {
public:
    virtual ~Base();
    void process(); // 调用虚成员(例如,v_process)
private:
    virtual void v_process(); // 被派生类重写
};

或者,基类可以提供一个非虚析构函数,这应该是受保护的。它不应是公共的,以防止通过基类指针删除对象(在这种情况下,应使用虚析构函数)。它应受保护,以允许派生类析构函数调用其基类析构函数。出于相同的原因,这些基类应具有非公共构造函数和重载的赋值操作符。

streambuf

std::streambuf 类接收流处理的字符序列,并定义了流对象与设备(如磁盘上的文件)之间的接口。通常不会直接构造 streambuf 对象,而是将其用作某些派生类的基类,这些派生类实现与具体设备的通信。

streambuf 类存在的主要原因是将流类与其操作的设备解耦。这样做的理由是为类与设备之间的通信增加一个额外的层次。这实现了一个在软件设计中经常见到的命令链(chain of command)模式。

命令链模式在设计可重用软件时被视为一种通用模式,例如在 TCP/IP 堆栈中也会遇到。

streambuf 可以被视为命令链模式的另一个示例。在这里,程序与流对象进行交互,这些流对象又将请求转发给 streambuf 对象,streambuf 对象则与设备进行通信。因此,如我们将很快看到的,我们能够在用户软件中实现以前需要通过(昂贵的)系统调用完成的操作。

streambuf 类没有公共构造函数,但提供了几个公共成员函数。除了这些公共成员函数外,还有一些成员函数仅对 streambuf 派生类可用。在 14.8.2 节中介绍了 streambuf 类的预定义特化。这里讨论的 streambuf 的所有公共成员也可以在 filebuf 中找到。

公共成员函数:输入操作

  • std::streamsize in_avail()

    • 返回可以立即读取的字符数量的下限。
  • int sbumpc()

    • 返回下一个可用字符或 EOF。返回的字符会从 streambuf 对象中移除。如果没有可用的输入,sbumpc 会调用(受保护的)成员函数 uflow 来提供新的字符。如果没有更多字符可用,则返回 EOF。
  • int sgetc()

    • 返回下一个可用字符或 EOF。字符不会从 streambuf 对象中移除。要移除字符,可以使用 sbumpc(或 sgetn)。
  • int sgetn(char *buffer, std::streamsize n)

    • 从输入缓冲区中检索最多 n 个字符,并存储在 buffer 中。返回实际读取的字符数量。该成员函数调用(受保护的)成员函数 xsgetn 来获取请求的字符数量。
  • int snextc()

    • 从输入缓冲区中获取当前字符,并将其返回为下一个可用字符或 EOF。字符不会从 streambuf 对象中移除。
  • int sputbackc(char c)

    • 将字符 c 插入到 streambuf 的缓冲区中,以便下次读取时返回。使用此函数时应谨慎:通常只能放回一个字符。
  • int sungetc()

    • 将最后一个读取的字符返回到输入缓冲区,以便在下次输入操作中再次读取。使用此函数时应谨慎:通常只能放回一个字符。

公共成员函数:输出操作

  • int pubsync()

    • 同步(即刷新)缓冲区,将 streambuf 缓冲区中当前可用的任何信息写入设备。通常仅由派生自 streambuf 的类使用。
  • int sputc(char c)

    • 将字符 c 插入到 streambuf 对象中。如果写入字符后缓冲区满了,该函数会调用(受保护的)成员函数 overflow 将缓冲区刷新到设备。
  • int sputn(char const *buffer, std::streamsize n)

    • buffer 中插入最多 n 个字符到 streambuf 对象中。返回实际插入的字符数量。该成员函数调用(受保护的)成员函数 xsputn 来插入请求的字符数量。

公共成员函数:其他操作

以下三个成员函数通常仅由派生自 streambuf 的类使用。

  • ios::pos_type pubseekoff(ios::off_type offset, ios::seekdir way, ios::openmode mode = ios::in | ios::out)

    • 将下一个要读取或写入的字符的偏移量设置为 offset,相对于标准的 ios::seekdir 值指示的查找方向。
  • ios::pos_type pubseekpos(ios::pos_type pos, ios::openmode mode = ios::in | ios::out)

    • 将下一个要读取或写入的字符的绝对位置设置为 pos
  • streambuf *pubsetbuf(char *buffer, std::streamsize n)

    • streambuf 对象将使用 buffer,该 buffer 可能包含至少 n 个字符。

受保护的 streambuf 成员

streambuf 类的受保护成员对理解和使用 streambuf 对象非常重要。尽管 streambuf 类中定义了受保护的数据成员和成员函数,但这里不讨论受保护的数据成员,因为使用它们会违反数据隐藏原则。由于 streambuf 的成员函数集非常广泛,通常不需要直接使用其数据成员。以下小节仅覆盖了对构建特化类有用的受保护成员函数,而不是列出所有受保护的成员函数。

streambuf 对象控制一个用于输入和/或输出的缓冲区,定义了开始指针、实际指针和结束指针,如图 14.4 所示。

streambuf 提供了两个受保护的构造函数:

  • streambuf::streambuf()

    • streambuf 类的默认(受保护)构造函数。
  • streambuf::streambuf(streambuf const &rhs)

    • streambuf 类的(受保护)拷贝构造函数。请注意,此拷贝构造函数仅复制 rhs 的数据成员值:使用拷贝构造函数后,两个 streambuf 对象都引用相同的数据缓冲区,最初它们的指针指向相同的位置。还要注意,这些不是共享指针,只是“原始拷贝”。

受保护的输入操作成员函数

几个受保护的成员函数用于输入操作。标记为 virtual 的成员函数当然可以在派生类中重新定义:

  • char *eback()

    • streambuf 维护三个指针来控制其输入缓冲区:eback 指向“放回区的末端”:字符可以安全地放回到此位置。详见图 14.4。eback 指向输入缓冲区的开始位置。
  • char *egptr()

    • egptr 指向可以从输入缓冲区中检索的最后一个字符之后的位置。详见图 14.4。如果 gptr 等于 egptr,则缓冲区必须被重新填充。这应通过调用 underflow 来实现,见下文。
  • void gbump(int n)

    • 将对象的 gptr(见下文)向前移动 n 个位置。
  • char *gptr()

    • gptr 指向从对象的输入缓冲区中检索的下一个字符。详见图 14.4。
  • virtual int pbackfail(int c)

    • 派生类可以重写此成员函数,以在放回字符 c 失败时执行一些智能操作。可以考虑在输入缓冲区的 begin 达到时恢复旧的读取指针。当以下情况发生时,将调用此成员函数:
      • gptr() == 0:未使用缓冲区,
      • gptr() == eback():没有更多空间进行放回,
      • *gptr() != c:必须放回的字符不同于下一个要读取的字符。
        如果 c == endOfFile(),则必须将输入设备重置为一个字符位置。否则,c 必须被预置在要读取的字符之前。函数在失败时应返回 EOF。否则可以返回 0。
        在这里插入图片描述

在这里插入图片描述

受保护的 streambuf 成员

输入操作的受保护成员函数

  • void setg(char *beg, char *next, char *beyond)

    • 初始化输入缓冲区。beg 指向输入区域的开始,next 指向下一个要检索的字符,beyond 指向输入缓冲区最后一个字符之后的位置。通常,next 至少是 beg + 1,以允许执行放回操作。当调用 setg(0, 0, 0) 时,表示不使用输入缓冲区。另见下面的成员函数 uflow
  • virtual streamsize showmanyc()

    • (发音为:s-how-many-c)此成员函数可以被派生类重写。它必须返回一个保证的下界,表示在 uflowunderflow 返回 EOF 之前,可以从设备中读取的字符数量。默认情况下返回 0(意味着在这两个函数返回 EOF 之前没有或只有一些字符被返回)。当返回正值时,下一次调用 u(nder)flow 不会返回 EOF。
  • virtual int uflow()

    • 此成员函数可以被派生类重写,用于重新加载输入缓冲区中的新字符。其默认实现是调用 underflow。如果 underflow() 失败,则返回 EOF。否则,返回下一个可用字符作为 static_cast<unsigned char>(*gptr()),并执行 gbump(-1)uflow 还会将返回的待处理字符移动到备份序列中。这与 underflow() 的行为不同,后者仅返回下一个可用字符,而不改变输入指针位置。如果不需要输入缓冲,这个函数可以被重写以从设备中生成下一个可用字符进行读取。
  • virtual int underflow()

    • 此成员函数可以被派生类重写,用于从设备读取另一个字符。默认实现是返回 EOF。它在以下情况下被调用:
      • 没有输入缓冲区(eback() == 0
      • gptr() >= egptr():输入缓冲区已耗尽。
        通常,当使用缓冲时,整个缓冲区不会被刷新,因为这会使在重新加载后无法立即放回字符。相反,缓冲区通常分半刷新,这种系统称为分割缓冲区。派生自 streambuf 的类通常至少会重写 underflow。重写的 underflow 函数的原型示例如下:
    int underflow() {
        if (not refillTheBuffer())
            // 假设有一个成员 d_buffer
            return EOF;
        // 重置输入缓冲区指针
        setg(d_buffer, d_buffer, d_buffer + d_nCharsRead);
        // 返回下一个可用字符
        // (使用 cast 防止将 0xff 字符错误解读为 EOF)
        return static_cast<unsigned char>(*gptr());
    }
    
  • virtual streamsize xsgetn(char *buffer, streamsize n)

    • 此成员函数可以被派生类重写,以一次性从输入设备中检索 n 个字符。默认实现是调用 sbumpc 来处理每个字符,这意味着默认情况下此成员函数(最终)为每个字符调用 underflow。函数返回实际读取的字符数量或 EOF。一旦返回 EOF,streambuf 停止从设备中读取。

输出操作的受保护成员函数

以下受保护的成员函数可用于输出操作。某些成员函数可以被派生类重写:

  • virtual int overflow(int c)

    • 此成员函数可以被派生类重写,以将当前存储在输出缓冲区中的字符刷新到输出设备,然后重置输出缓冲区指针以表示一个空缓冲区。参数 c 被初始化为下一个要处理的字符。如果不使用输出缓冲,则每写一个字符到 streambuf 对象时都会调用 overflow。没有输出缓冲是通过将缓冲区指针(使用 setp,见下文)设置为 0 实现的。默认实现返回 EOF,表示无法向设备写入字符。派生自 streambuf 的类通常至少会重写 overflow。重写 overflow 函数的原型示例如下:
    int OFdStreambuf::overflow(int c)
    {
        sync(); // 刷新缓冲区
        if (c != EOF) // 是否写入字符?
        {
            *pptr() = static_cast<char>(c); // 将其放入缓冲区
            pbump(1); // 移动缓冲区的指针
        }
        return c;
    }
    
  • char *pbase()

    • streambuf 维护三个指针来控制其输出缓冲区:pbase 指向输出缓冲区区域的开始。见图 14.4。
  • char *epptr()

    • streambuf 维护三个指针来控制其输出缓冲区:epptr 指向输出缓冲区最后一个可用位置之后的位置。见图 14.4。如果 pptr(见下文)等于 epptr,则缓冲区必须被刷新。这通过调用 overflow 实现,如前所述。
  • void pbump(int n)

    • pptr(见下文)返回的位置向前移动 n 个位置。下一个写入到流的字符将被输入到该位置。
  • char *pptr()

    • streambuf 维护三个指针来控制其输出缓冲区:pptr 指向输出缓冲区中下一个要写入的位置。见图 14.4。
  • void setp(char *beg, char *beyond)

    • 初始化 streambuf 的输出缓冲区到传递给 setp 的位置。beg 指向输出缓冲区的开始,beyond 指向输出缓冲区最后一个可用位置之后的位置。使用 setp(0, 0) 表示不使用缓冲。在这种情况下,overflow 会为每个要写入设备的字符调用。

受保护的 streambuf 成员

输出操作的受保护成员函数

  • virtual streamsize xsputn(char const *buffer, streamsize n)

    • 此成员函数可以被派生类重写,以将最多 n 个字符写入输出缓冲区。实际插入的字符数量将被返回。如果返回 EOF,则停止向设备写入。默认实现是为每个单独的字符调用 sputc。如果例如 streambuf 应该支持 ios::openmode ios::app,则应重写此成员函数。假设 MyBuf 类派生自 streambuf,并具有一个数据成员 ios::openmode d_mode(表示请求的 ios::openmode),以及一个成员函数 write(char const *buf, streamsize len)(在 pptr() 位置写入 len 字节),则以下代码确认了 ios::app 模式:
    std::streamsize MyStreambuf::xsputn(char const *buf, std::streamsize len)
    {
        if (d_openMode & ios::app)
            seekoff(0, ios::end);
        return write(buf, len);
    }
    

缓冲区管理的受保护成员函数

  • virtual streambuf *setbuf(char *buffer, streamsize n)

    • 此成员函数可以被派生类重写,以安装缓冲区。默认实现不执行任何操作。它由 pubsetbuf 调用。
  • virtual ios::pos_type seekoff(ios::off_type offset, ios::seekdir way, ios::openmode mode = ios::in | ios::out)

    • 此成员函数可以被派生类重写,以将输入或输出的下一个指针重置到一个新的相对位置(使用 ios::begios::curios::end)。默认实现通过返回 -1 表示失败。当调用 tellgtellp 时,会调用此函数。当派生类支持定位时,应该定义此函数以处理重新定位请求。它由 pubseekoff 调用。返回新的位置或(默认情况下)无效的位置(即 -1)。
  • virtual ios::pos_type seekpos(ios::pos_type offset, ios::openmode mode = ios::in | ios::out)

    • 此成员函数可以被派生类重写,以将输入或输出的下一个指针重置到一个新的绝对位置(即相对于 ios::beg)。当调用 seekgseekp 时,会调用此函数。返回新的位置或(默认情况下)无效的位置(即 -1)。
  • virtual int sync()

    • 此成员函数可以被派生类重写,以将输出缓冲区刷新到输出设备,或将输入设备重置到最后返回字符之后的位置。成功时返回 0,失败时返回 -1。默认实现(不使用缓冲)是返回 0,表示成功同步。此成员用于确保所有仍在缓冲区中的字符被写入设备,或者在 streambuf 对象消失时将未消耗的字符放回设备。

streambuf 派生的类

当从 streambuf 派生类时,至少应该重写 underflow 以便于从设备读取信息,重写 overflow 以便于向设备写入信息。第 25 章提供了几个从 streambuf 派生的类示例。

fstream 类类型对象使用一个结合了输入/输出的缓冲区,结果是 istreamostreamios 虚拟派生,该类定义了一个 streambuf。要构造一个支持输入和输出的类,使用不同的缓冲区,streambuf 本身可以定义两个缓冲区。当调用 seekoff 进行读取时,可以将模式参数设置为 ios::in,否则设置为 ios::out。因此,派生类知道是否应该访问读取缓冲区或写入缓冲区。当然,underflowoverflow 不必检查模式标志,因为它们暗示了它们应该操作的缓冲区。

当然,以下是一个简单的示例,演示了如何从 std::streambuf 派生一个自定义的流缓冲区类,重写 underflowoverflow 方法,以处理输入和输出操作。

在这个例子中,我们定义了一个自定义的 MyStreambuf 类,该类使用固定大小的缓冲区进行输入和输出操作。

#include <cstring>
#include <iostream>
#include <streambuf>

// 自定义的 streambuf 类
class MyStreambuf : public std::streambuf {
public:
    MyStreambuf() {
        // 设置缓冲区大小
        buffer_size = 1024;
        buffer = new char[buffer_size];
        setp(buffer, buffer + buffer_size);  // 设置输出缓冲区
        setg(buffer, buffer, buffer);        // 设置输入缓冲区
        data_loaded = false;                 // 标志数据是否已加载
    }

    ~MyStreambuf() { delete[] buffer; }

protected:
    // 处理输出操作
    virtual int overflow(int c) override {
        if (c != EOF) {
            if (pptr() == epptr()) {
                // 缓冲区已满,刷新缓冲区
                if (sync() == -1) {
                    return EOF;
                }
            }
            *pptr() = static_cast<char>(c);
            pbump(1);
        }
        return c;
    }

    // 处理输入操作
    virtual int underflow() override {
        if (gptr() == egptr() && !data_loaded) {
            // 缓冲区已空且未加载数据,加载数据
            std::memcpy(buffer, "Hello, world!\n", 14);  // 添加换行符
            setg(buffer, buffer, buffer + 14);
            data_loaded = true;  // 标志数据已加载
        }
        if (gptr() == egptr()) {
            return EOF;  // 当没有更多数据时返回 EOF
        }
        return static_cast<unsigned char>(*gptr());
    }

    virtual int sync() override {
        std::cout.write(buffer,
                        pptr() - pbase());  // 将缓冲区内容输出到标准输出
        pbump(-(pptr() - pbase()));         // 重置缓冲区指针
        return 0;
    }

private:
    char* buffer;                 // 缓冲区
    std::streamsize buffer_size;  // 缓冲区大小
    bool data_loaded;             // 标志是否已加载输入数据
};

// 测试自定义 streambuf
int main() {
    MyStreambuf mybuf;
    std::ostream os(&mybuf);
    std::istream is(&mybuf);

    // 测试输出
    os << "Hello, streambuf!" << std::endl;
    os.flush();

    // 测试输入
    std::string input;
    std::getline(is, input);
    std::cout << "Read from streambuf: " << input << std::endl;

    return 0;
}

说明

  1. overflow(int c): 处理输出操作。如果缓冲区已满,调用 sync() 刷新缓冲区,然后将新字符写入缓冲区。

  2. underflow(): 处理输入操作。如果缓冲区已空,模拟填充数据,并设置缓冲区的开始和结束位置。

  3. sync(): 将缓冲区内容写入到标准输出,并重置缓冲区指针。

此代码示例创建了一个简单的自定义缓冲区类 MyStreambuf,它演示了如何处理输入和输出操作。实际应用中,可能需要更复杂的逻辑来处理实际设备或文件。

filebuf

filebuf 类是 streambuf 的一种特化,专门用于文件流类。在使用 filebuf 之前,必须包含头文件 <fstream>

除了通过 streambuf 类提供的(公共)成员之外,filebuf 还提供了以下(公共)成员:

  • filebuf()
    filebuf 提供一个公共构造函数。它初始化一个尚未连接到流的 filebuf 对象。

  • bool is_open()
    如果 filebuf 实际上已连接到一个打开的文件,则返回 true,否则返回 false。可以使用此方法检查 filebuf 是否已成功打开文件。

  • filebuf* open(char const* name, ios::openmode mode)
    filebuf 对象与指定名称的文件关联起来。文件将根据提供的 openmode 打开。

  • filebuf* close()
    关闭 filebuf 对象与文件之间的关联。filebuf 对象在销毁时会自动关闭关联。

安全地将流接口切换到另一个 std::streambuf

考虑从 std::istreamstd::ostream 派生的类。这样的类可以设计如下:

class XIstream : public std::istream
{
public:
    // 构造函数和其他成员函数
    ...
};

假设在构造时 XIstream 需要连接的 streambuf 尚不可用,XIstream 可能仅提供默认构造函数。然而,该类可以提供一个成员函数 void switchStream(std::streambuf* sb) 以为 XIstream 对象提供一个 streambuf 接口。

如何实现 switchStream?我们可以简单地调用 rdbuf,传递新的 streambuf 指针,但问题是可能存在一个已有的 streambuf,它可能已经缓存了一些信息,我们不想丢失。为了避免使用 rdbuf,应该使用受保护的成员函数 void init(std::streambuf* sb) 来切换到另一个 streambuf

init 成员函数期望一个 streambuf 指针,这个指针应该与 istreamostream 对象相关联。init 成员函数会在切换到提供的 streambuf 之前,正确地结束任何现有的关联。

假设 switchStreamsb 指向的 streambuf 是持久的,那么 switchStream 可以简单地实现如下:

void switchStream(std::streambuf* sb)
{
    init(sb);
}

不需要进一步的操作。init 成员函数会结束当前的关联,然后切换到使用 streambuf* sb

使用 std::iostream 进行读写

std::iostream 提供了从 std::streambuf 对象中读取和写入的功能。在实践中,iostream 通过一个从 std::streambuf 派生的类进行操作,该派生类重写了 streambuf 的读取和写入虚拟成员。下面将介绍 StreamBuf 的常见特性。

  • iostream 的读取和写入的寻位操作是同步的:使用 seekgseekp 会同时更新 tellgtellp
  • 寻位操作不会重新加载 StreamBuf 的数据缓冲区。例如:假设 StreamBuf 的数据缓冲区包含来自设备的连续 1000 个字符块,且最后的读写操作使 tellg 返回 1100,因此第二块 1000 个字符位于 StreamBuf 的缓冲区中。此时发出 seekp(250) 不会导致加载第一块 1000 个字符。类似地,假设设备的大小是 10,000 字节,seekg(12000) 只会让 tellptellg 返回 12,000。然后,在发出 tellg(1200) 之后,所有读写操作将使用当前加载的缓冲区,从缓冲区的偏移量 200 开始操作。
  • tellgtellp 函数调用 StreamBufseekoff(0, ios::cur) 成员函数,返回 StreamBuf 管理的设备的当前位置信息。

因此,StreamBuf 应该跟踪设备中的当前位置信息,以及当前加载的缓冲区的区域。一旦块被加载,iostream 的函数 setgsetp 被用来定义加载缓冲区的开始和结束位置,而 std::streambuf 的成员 pbumpgbump 可用于重新定位缓冲区中的 gptr()pptr() 指向的位置。

一旦 iostream 被用于读取或写入,StreamBuf 的成员 underflowoverflow 分别在 gptr() == eback()pptr() == epptr() 时被调用。

复杂性在于,读取和写入会更新 gptr()pptr(),但由于这些位置由 iostream 对象修改,StreamBuf 对象维护的当前设备位置并不会更新。StreamBuf 必须以某种方式管理这种情况。然而,当前的读/写位置主要与相对于当前使用的设备位置进行寻位操作时相关。当请求的寻位位置相对于设备的开始或结束位置时,结果位置只是这些位置的总和加上请求的偏移量。但是,当请求的寻位位置相对于当前的位置时,则必须使用当前的位置。然而,seekposseekoff 也可以在尚未从设备加载缓冲区时调用。

以下是解决这一复杂性的一个方法(实现细节参见 Bobcat 库中的 MmapBuf 类):

  • StreamBuf 使用以下成员来管理当前情况:
    • d_pos 记录设备中的当前位置信息;
    • d_activeBuffer 如果缓冲区从设备加载并且正在被使用(通过读/写操作),则为 true;否则为 false
    • d_buffer 如果没有缓冲区加载,则为 0;否则指向分配的缓冲区的位置;
    • d_bufSize 包含分配的缓冲区的大小;
    • d_offset 对应于设备中缓冲区第一个字符位置的物理偏移量;
    • d_sync 在当前加载的缓冲区被写操作修改时设置为 true

在寻位请求时:

  • 在寻位请求结束时,调用 setpsetg,传递 0 个参数(强制在下一个读/写操作时调用 underflowoverflow),并将 d_activeBuffer 设置为 falsed_buffer 可能已定义,但在寻位操作后不再积极使用)。
  • 相对于设备的开始和结束位置的寻位请求简单地更新 d_pos。对于 ios::cur 规范:如果当前没有活动的缓冲区,则没有自上次寻位操作以来请求的读/写位置,因此 d_pos 保持最新的位置,并用请求的 pos 参数进行更新。如果有活动的缓冲区,则当前位置是 pos + d_offset 加上当前读取(gptr())或写入(pptr())位置在加载缓冲区中的最大值。
  • 最终返回 d_pos

underflowoverflow 成员函数在分别满足 gptr() == eback()pptr() == epptr() 时被调用。

StreamBuf::underflow

  • 最后一个操作可能是写操作(d_sync == true)。如果是这样,则将当前加载的缓冲区写入设备。
  • 如果有当前活动的缓冲区,则缓冲区已耗尽,d_pos 设置为设备中超出缓冲区的位置(即 d_offset + d_bufSize);
  • 如果缓冲区可用且 d_pos 对应于当前加载缓冲区中的位置,则将缓冲区的 gptr() 设置为该位置。否则:
    • 如果 d_pos 超过设备的大小,则返回 EOF。否则,更新 d_offsetd_offset = d_pos / d_bufSize * d_bufSize)并从设备加载新缓冲区。

StreamBuf::overflow

  • 如果有当前活动的缓冲区,则缓冲区已耗尽,d_pos 设置为设备中超出缓冲区的位置。否则:
  • 如果缓冲区可用且 d_pos 对应于当前加载缓冲区中的位置,则将缓冲区的 pptr() 设置为该位置,并将溢出的字符写入缓冲区并由 overflow 返回。否则:
    • 如果缓冲区可用,则将其刷新到设备。然后更新 d_offsetd_offset = d_pos / d_bufSize * d_bufSize)并从设备加载新缓冲区。

多态异常类

在之前的 C++ 注释中(第 10.3.1 节),我们提到过设计一个 Exception 类,其 process 成员根据抛出的异常类型表现不同的可能性。现在,我们已经引入了多态性,可以进一步发展这个示例。

我们设计的 Exception 类应当是一个多态的基类,其他特定的异常处理类可以从它派生。第 10.3.1 节中使用了一个 severity 成员,提供了可能被 Exception 基类的成员替代的功能。基类 Exception 可以这样设计:

#ifndef INCLUDED_EXCEPTION_H_
#define INCLUDED_EXCEPTION_H_

#include <iostream>
#include <string>

class Exception
{
    std::string d_reason;

public:
    Exception(std::string const &reason);
    virtual ~Exception();
    std::ostream &insertInto(std::ostream &out) const;
    void handle() const;

private:
    virtual void action() const;
};

inline void Exception::action() const
{
    throw;
}

inline Exception::Exception(std::string const &reason)
    : d_reason(reason)
{}

inline void Exception::handle() const
{
    action();
}

inline std::ostream &Exception::insertInto(std::ostream &out) const
{
    return out << d_reason;
}

inline std::ostream &operator<<(std::ostream &out, Exception const &e)
{
    return e.insertInto(out);
}

#endif

这个类的对象可以被插入到 ostream 中,但该类的核心元素是虚成员函数 action,默认情况下重新抛出异常。

一个派生类 Warning 会在抛出的警告文本前加上 “Warning:” 的前缀,而另一个派生类 Fatal 则通过调用 std::terminate 覆盖 Exception::action,强制终止程序。

以下是 WarningFatal 类的定义:

#ifndef WARNINGEXCEPTION_H_
#define WARNINGEXCEPTION_H_

#include "exception.h"

class Warning : public Exception
{
public:
    Warning(std::string const &reason)
        : Exception("Warning: " + reason)
{}
};

#endif
#ifndef FATAL_H_
#define FATAL_H_

#include "exception.h"

class Fatal : public Exception
{
public:
    Fatal(std::string const &reason);

private:
    void action() const override;
};

inline Fatal::Fatal(std::string const &reason)
    : Exception(reason)
{}

inline void Fatal::action() const
{
    std::cout << "Fatal::action() terminates" << '\n';
    std::terminate();
}

#endif

当示例程序在没有参数时启动时,它会抛出一个 Fatal 异常,否则它会抛出一个 Warning 异常。当然,也可以轻松定义其他异常类型。

为了使示例程序可编译,Exception 的析构函数定义在 main 之前。默认的析构函数不可用,因为它是虚拟析构函数。实际中,析构函数应在自己的源文件中定义:

#include "exception.h"

Exception::~Exception()
{}

示例程序如下:

#include <iostream>
#include "warning.h"
#include "fatal.h"

using namespace std;

int main(int argc, char **argv)
try
{
    try
    {
        if (argc == 1)
            throw Fatal("Missing Argument");
        else
            throw Warning("the argument is ignored");
    }
    catch (Exception const &e)
    {
        cout << e << '\n';
        e.handle();
    }
}
catch(...)
{
    cout << "caught rethrown exception\n";
}

多态性是如何实现的

本节简要描述了 C++ 中多态性是如何实现的。虽然了解多态性的实现并不是使用多态性的必要条件,但了解其实现方式有助于理解为什么使用多态性会有(小的)内存和效率开销。

多态性的基本思想是编译器在编译时不知道调用哪个函数。适当的函数在运行时被选择。这意味着函数的地址必须在实际调用之前能够被查找。这个“某个地方”必须对对象可访问。因此,当 Vehicle *vp 指向一个 Truck 对象时,vp->mass() 会调用 Truck 的成员函数。这个函数的地址是通过实际对象来获取的。

多态性通常是这样实现的:一个包含虚成员函数的对象通常包含一个隐藏的数据成员,指向一个数组,这个数组包含了类的虚成员函数的地址。这个隐藏的数据成员通常称为 vpointer,而虚成员函数地址的数组称为 vtable(虚函数表)。

类的 vtable 是所有该类对象共享的。因此,多态性在内存消耗上的开销是:

  • 每个对象一个 vpointer 数据成员,指向:
  • 每个类一个 vtable

因此,像 vp->mass 这样的语句首先检查 vp 指向的对象的隐藏数据成员。在车辆分类系统的例子中,这个数据成员指向一个表,该表包含两个地址:一个指向 mass 函数的指针,另一个指向 setMass 函数的指针(如果类定义了虚析构函数,表中会有三个指针)。实际调用的函数是从这个表中确定的。

具有虚函数的对象的内部组织如下图所示(图由 Guillaume Caumon 提供):
在这里插入图片描述

如图所示,潜在使用虚成员函数的对象必须有一个(隐藏的)数据成员来地址一个函数指针表。类 VehicleCar 的对象都指向相同的表。然而,类 Truck 重写了 mass 函数。因此,Truck 需要自己的 vtable
在这里插入图片描述

VehicleCar 都指向相同的虚函数表(vtable)。然而,Truck 类重写了 mass 函数,因此 Truck 需要自己的虚函数表(vtable)。

当一个类从多个基类派生,并且每个基类定义了虚函数时,会出现一些小的复杂性。考虑以下示例:

class Base1
{
public:
    virtual ~Base1();
    void fun1(); // 调用 vOne 和 vTwo
private:
    virtual void vOne();
    virtual void vTwo();
};

class Base2
{
public:
    virtual ~Base2();
    void fun2(); // 调用 vThree
private:
    virtual void vThree();
};

class Derived : public Base1, public Base2
{
public:
    ~Derived() override;
private:
    void vOne() override;
    void vThree() override;
};

在这个示例中,Derived 类从 Base1Base2 多重派生,每个基类都支持虚函数。因此,Derived 也具有虚函数,因此 Derived 需要一个虚函数表(vtable),允许基类指针或引用访问适当的虚成员函数。

当调用 Derived::fun1(或一个指向 Derived 对象的 Base1 指针调用 fun1)时,fun1 会调用 Derived::vOneBase1::vTwo。类似地,当调用 Derived::fun2 时,会调用 Derived::vThree

问题出在 Derived 的虚函数表(vtable)上。当 fun1 被调用时,它的类类型决定了使用哪个虚函数表,因此决定了调用哪个虚成员函数。因此,当从 fun1 调用 vOne 时,它应该是 Derived 的虚函数表中的第二项,因为它必须匹配 Base1 的虚函数表中的第二项。然而,当 fun2 调用 vThree 时,它也显然是 Derived 的虚函数表中的第二项,因为它必须匹配 Base2 的虚函数表中的第二项。

显然,无法通过单一的虚函数表来实现这一点。因此,当使用多重继承(每个基类都定义了虚成员函数)时,会采取另一种方法来确定调用哪个虚函数。在这种情况下(参见图 14.7),Derived 类会有两个虚函数表,每个基类一个,每个 Derived 类对象都有两个隐藏的虚函数指针(vpointers),每个指针指向对应的虚函数表。
在这里插入图片描述

由于基类指针、基类引用或基类接口成员明确地指向一个基类,编译器可以确定使用哪个虚函数指针。因此,对于从提供虚成员函数的基类多重派生的类,以下几点成立:

  • 派生类为每个提供虚成员函数的基类定义一个虚函数表(vtable)。
  • 每个派生类对象包含与虚函数表数量相同数量的隐藏虚函数指针(vpointers)。
  • 每个派生类对象的虚函数指针指向唯一的虚函数表,并且要使用哪个虚函数指针由基类指针、基类引用或基类接口函数的类类型决定。

未定义的引用到 vtable

有时,链接器会生成如下错误信息:

In function `Derived::Derived()':
: undefined reference to `vtable for Derived'

这个错误是在派生类中缺少虚函数的实现,但在派生类的接口中提到该函数时发生的。

这种情况很容易遇到:

  1. 构造一个(完整的)基类,定义了一个虚成员函数。
  2. 构造一个派生类,在其接口中提到虚函数。
  3. 派生类的虚函数没有实现。当然,编译器不知道派生类的函数未实现,因此会在请求时生成创建派生类对象的代码。
  4. 最终,链接器无法找到派生类的虚成员函数。因此,它无法构造派生类的虚函数表(vtable)。
  5. 链接器因此抱怨:
    undefined reference to `vtable for Derived'
    

以下是一个产生该错误的示例:

class Base
{
    virtual void member();
};

inline void Base::member()
{}

class Derived: public Base
{
    void member() override;
    // 仅声明
};

int main()
{
    Derived d; // 会编译,因为所有成员都已声明。
    // 链接将失败,因为没有提供 Derived::member() 的实现。
}

要解决这个错误,当然很简单:为派生类实现缺失的虚成员函数。

虚函数不应该以内联方式实现。由于 vtable 包含了类虚函数的地址,这些函数必须有地址,因此它们必须被编译成实际的(外部的)函数。通过将虚函数定义为内联函数,你可能会遇到编译器忽视这些函数的风险,因为这些函数可能不会被显式调用(而只通过基类指针或引用进行多态调用)。结果,它们的地址可能永远不会进入它们类的 vtable 中(甚至 vtable 本身可能仍然未定义),这会导致链接问题或程序出现意外行为。为避免这些问题,应该避免将虚成员函数定义为内联函数(参见第 7.8.2.1 节)。

虚构造函数

在第 14.2 节中,我们了解到 C++ 支持虚拟析构函数。然而,与许多其他面向对象语言(例如 Java)不同,C++ 不支持虚构造函数。当只有基类引用或指针可用时,并且需要创建派生类对象的副本时,不支持虚构造函数会带来困难。Gamma 等人(1995 年)讨论了原型设计模式来处理这种情况。

根据原型设计模式,每个派生类负责实现一个成员函数,该函数返回一个指向当前对象副本的指针。这个函数通常被称为 clone。为了将用户接口与重新实现接口分离,clone 成为接口的一部分,newCopy 被定义在重新实现接口中。一个支持“克隆”的基类定义了虚拟析构函数、clone(返回 newCopy 的返回值),以及虚拟复制构造函数,newCopy 是一个纯虚函数,其原型为 virtual Base* newCopy() const = 0。由于 newCopy 是纯虚函数,所有派生类现在必须实现自己的“虚构造函数”。

这种设置在大多数情况下是足够的,当我们有一个基类指针或引用时。但是,当与抽象容器一起使用时,这种方法失败了。我们不能创建 vector<Base>,因为 Base 在其接口中具有纯虚拟复制成员,vector 需要用基类来初始化新元素。这是不可能的,因为 newCopy 是纯虚函数,因此无法构造 Base 对象。

直观的解决方案是为 newCopy 提供一个默认实现,将其定义为普通的虚函数,但这也失败了,因为容器调用 Base(Base const &other),这将调用 newCopy 来复制 other。此时,不清楚如何处理这个副本,因为新的 Base 对象已经存在,并且没有 Base 指针或引用成员来分配 newCopy 的返回值。

另外(也是推荐的方式),可以保持原始的 Base 类(定义为抽象基类)不变,并使用一个包装类 Clonable 来管理由 newCopy 返回的 Base 类指针。在第 17 章中讨论了将 BaseClonable 合并为一个类的方法,但现在我们将 BaseClonable 定义为两个独立的类。

Clonable 类是一个非常标准的类。它包含一个指针成员,因此需要一个复制构造函数、析构函数和重载的赋值操作符。它还具有至少一个非标准成员:Base &base() const,返回对 ClonableBase* 数据成员所引用的派生对象的引用。它还提供了一个额外的构造函数来初始化其 Base* 数据成员。

任何从 Base 派生的非抽象类必须实现 Base* newCopy(),返回一个新创建(分配)的对象副本的指针。

一旦我们定义了一个派生类(例如 Derived1),我们可以充分利用我们的 ClonableBase 设施。在下面的示例中,main 定义了一个 vector<Clonable>。然后,匿名的 Derived1 对象被插入到向量中,步骤如下:

  • 创建一个新的匿名 Derived1 对象;
  • 使用 Clonable(Base *bp) 初始化 Clonable
  • 使用 Clonable 的移动构造函数将刚创建的 Clonable 对象插入到向量中。此时只有临时的 DerivedClonable 对象,因此不需要复制构造。

在这个过程中,只需要创建(或销毁)包含 Derived1Clonable 对象。没有额外的复制。

接下来,基成员与 typeid 一起使用,以显示 Base & 对象的实际类型:Derived1 对象。

main 还包含有趣的定义 vector<Clonable> v2(bv)。这里创建了 bv 的一个副本。这个复制构造函数观察 Base 引用的实际类型,确保向量的副本中出现正确的类型。

程序结束时,我们创建了两个 Derived1 对象,这些对象由向量的析构函数正确删除。以下是完整的程序,演示了“虚构造函数”概念:

#include <iostream>
#include <vector>
#include <algorithm>
#include <typeinfo>

// Base 和它的内联成员:
class Base {
public:
    virtual ~Base();
    Base* clone() const;

private:
    virtual Base* newCopy() const = 0;
};

inline Base* Base::clone() const { return newCopy(); }

// Clonable 和它的内联成员:
class Clonable {
    Base* d_bp;

public:
    Clonable();
    explicit Clonable(Base* base);
    ~Clonable();
    Clonable(Clonable const& other);
    Clonable(Clonable&& tmp);
    Clonable& operator=(Clonable const& other);
    Clonable& operator=(Clonable&& tmp);
    Base& base() const;
};

inline Clonable::Clonable() : d_bp(0) {}
inline Clonable::Clonable(Base* bp) : d_bp(bp) {}
inline Clonable::Clonable(Clonable const& other) : d_bp(other.d_bp->clone()) {}
inline Clonable::Clonable(Clonable&& tmp) : d_bp(tmp.d_bp) { tmp.d_bp = 0; }
inline Clonable::~Clonable() { delete d_bp; }
inline Base& Clonable::base() const { return *d_bp; }

// Derived 和它的内联成员:
class Derived1 : public Base {
public:
    ~Derived1() override;

private:
    Base* newCopy() const override;
};

inline Base* Derived1::newCopy() const { return new Derived1(*this); }

// 非内联实现:
Base::~Base() {}
Clonable& Clonable::operator=(Clonable const& other) {
    Clonable tmp(other);
    std::swap(d_bp, tmp.d_bp);
    return *this;
}

Clonable& Clonable::operator=(Clonable&& tmp) {
    std::swap(d_bp, tmp.d_bp);
    return *this;
}

Derived1::~Derived1() { std::cout << "~Derived1() called\n"; }

// main 函数:
using namespace std;

int main() {
    vector<Clonable> bv;
    bv.push_back(Clonable(new Derived1()));
    cout << "bv[0].name: " << typeid(bv[0].base()).name() << '\n';
    vector<Clonable> v2(bv);
    cout << "v2[0].name: " << typeid(v2[0].base()).name() << '\n';
}

输出:

bv[0].name: 8Derived1
v2[0].name: 8Derived1
~Derived1() called
~Derived1() called
下面是对上面理解

这个解释涵盖了 C++ 中的虚构造函数问题以及如何通过原型设计模式解决这一问题。这里是如何理解这个概念:

虚构造函数的背景

在 C++ 中,虚构造函数的概念实际上并不存在。虚构造函数的想法是允许在运行时根据对象的实际类型创建对象副本,类似于虚析构函数允许正确销毁对象。然而,由于 C++ 的设计不支持虚构造函数,当我们只有基类的指针或引用时,无法直接创建派生类对象的副本。

原型设计模式

为了解决这个问题,原型设计模式被引入。这个设计模式通过让每个类实现一个返回对象副本的函数来解决虚构造函数的问题。这个函数通常称为 clone,其作用是返回一个指向当前对象副本的指针。

如何实现
  1. 基类定义:

    • 基类定义一个纯虚函数 newCopy(),其作用是返回当前对象的副本。由于 newCopy() 是纯虚函数,所以基类是一个抽象类,不能直接实例化。
    • 基类还定义了一个 clone() 函数,这个函数调用 newCopy() 并返回其结果。
    class Base {
    public:
        virtual ~Base();
        Base* clone() const;
    
    private:
        virtual Base* newCopy() const = 0;
    };
    
    inline Base* Base::clone() const { return newCopy(); }
    
  2. 派生类实现:

    • 每个派生类需要实现 newCopy(),以提供其具体的对象副本。
    class Derived1 : public Base {
    public:
        ~Derived1() override;
    
    private:
        Base* newCopy() const override;
    };
    
    inline Base* Derived1::newCopy() const { return new Derived1(*this); }
    
  3. 包装类 Clonable:

    • Clonable 类用于管理 Base 类指针,并提供复制和移动构造函数以及赋值操作符。
    • Clonable 还包括一个成员函数 base(),返回指向 Base 对象的引用。
    class Clonable {
        Base* d_bp;
    
    public:
        Clonable();
        explicit Clonable(Base* base);
        ~Clonable();
        Clonable(Clonable const& other);
        Clonable(Clonable&& tmp);
        Clonable& operator=(Clonable const& other);
        Clonable& operator=(Clonable&& tmp);
        Base& base() const;
    };
    
    inline Clonable::Clonable() : d_bp(0) {}
    inline Clonable::Clonable(Base* bp) : d_bp(bp) {}
    inline Clonable::Clonable(Clonable const& other) : d_bp(other.d_bp->clone()) {}
    inline Clonable::Clonable(Clonable&& tmp) : d_bp(tmp.d_bp) { tmp.d_bp = 0; }
    inline Clonable::~Clonable() { delete d_bp; }
    inline Base& Clonable::base() const { return *d_bp; }
    

示例程序

main 函数中,创建了一个 vector<Clonable> 并将 Derived1 对象插入到这个向量中。以下是步骤:

  1. 创建一个新的 Derived1 对象。
  2. 使用 Clonable 类初始化一个 Clonable 对象。
  3. 使用 Clonable 的移动构造函数将对象插入到向量中。

复制构造函数确保向量的副本中包含正确的类型。程序的输出显示了正确的对象类型,并且对象在程序结束时被正确删除。

int main() {
    vector<Clonable> bv;
    bv.push_back(Clonable(new Derived1()));
    cout << "bv[0].name: " << typeid(bv[0].base()).name() << '\n';
    vector<Clonable> v2(bv);
    cout << "v2[0].name: " << typeid(v2[0].base()).name() << '\n';
}

输出:

bv[0].name: 8Derived1
v2[0].name: 8Derived1
~Derived1() called
~Derived1() called

friend

在前面的所有示例中,我们看到私有成员仅对其所属类的成员可访问。这种做法很好,因为它强制执行了封装和数据隐藏。通过将功能封装在一个类中,我们防止了一个类暴露多个职责;通过隐藏数据,我们促进了类的数据完整性,并防止其他软件部分依赖于类的数据实现。

在本章(非常简短的章节)中,我们介绍了 friend 关键字及其使用原理。关键点是,通过使用 friend 关键字,可以授予函数访问类的私有成员的权限。然而,这并不意味着在使用 friend 关键字时就放弃了数据隐藏的原则。

本章不会讨论类之间的友元关系。类之间的友元关系在第 17 章和第 21 章中有讨论,这些情况是对函数友元处理方式的自然扩展。

声明友元(即使用 friend 关键字)应该有明确的概念性理由。传统上对类概念的定义通常是这样的:

类是一组数据和操作这些数据的函数。

正如我们在第 11 章中看到的,一些函数必须在类接口之外定义。它们在类接口之外定义是为了允许对其操作数进行提升,或者扩展不直接受我们控制的现有类的功能。根据上述传统的类概念定义,那些不能在类接口中定义的函数,仍然应该被视为属于类的函数。换句话说:如果语言的语法允许,它们肯定会被定义在类接口内部。实现这些函数有两种方法。一种方法是使用可用的公共成员函数来实现这些函数。另一种方法是将类概念应用于这些函数。通过声明这些函数实际上属于类,它们应该直接访问对象的成员数据。这可以通过 friend 关键字来实现。

一般原则是,所有在与类接口相同文件中声明的操作对象数据的函数都属于该类,并且可以直接访问类的数据成员。

友元函数

在第 11.2 节中,Person 类的插入操作符被实现为:

ostream &operator<<(ostream &out, Person const &person)
{
    return out <<
        "Name: " << person.name() << ", "
        "Address: " << person.address() << ", "
        "Phone: " << person.phone();
}

现在,Person 对象可以被插入到流中。

然而,这种实现需要调用三个成员函数,这可能被认为是一种效率问题。可以通过定义一个成员函数 Person::insertInto,并让 operator<< 调用该函数来改进。这两个函数可以定义如下:

std::ostream &operator<<(std::ostream &out, Person const &person)
{
    return person.insertInto(out);
}

std::ostream &Person::insertInto(std::ostream &out)
{
    return out << "Name: " << d_name << ", "
               "Address: " << d_address << ", "
               "Phone: " << d_phone;
}

由于 insertInto 是一个成员函数,它可以直接访问对象的数据成员,因此在将 person 插入到 out 时不需要调用额外的成员函数。

下一步是认识到 insertInto 仅仅是为了 operator<< 的利益而定义的,并且 operator<< 是在包含 Person 类接口的头文件中声明的,因此应当将其视为属于 Person 类的函数。因此,insertInto 成员函数可以省略,当 operator<< 被声明为友元时。

友元函数必须在类接口中声明为友元。这些友元声明不是成员函数,因此它们与类的私有、保护和公共部分无关。友元声明可以放在类接口中的任何位置。惯例是将友元声明直接列在类接口的顶部。使用友元声明的 Person 类如下:

class Person
{
    friend std::ostream &operator<<(std::ostream &out, Person &pd);
    friend std::istream &operator>>(std::istream &in, Person &pd);
    // 先前显示的接口(数据和函数)
};

插入操作符现在可以直接访问 Person 对象的数据成员:

std::ostream &operator<<(std::ostream &out, Person const &person)
{
    return out << "Name: " << person.d_name << ", "
               "Address: " << person.d_address << ", "
               "Phone: " << person.d_phone;
}

友元声明是真正的声明。一旦类中包含友元声明,这些友元函数不需要在类接口下方再次声明。这也清楚地表明了类设计者的意图:友元函数由类声明,因此可以视为属于该类的函数。

扩展的友元声明

C++ 引入了扩展的友元声明。当一个类被声明为友元时,不再需要提供 class 关键字。例如:

class Friend; // 声明一个类
using FriendClass = Friend; // 也可以使用 using 声明
class Class1
{
    friend FriendClass; // Friend: 也可以使用
};

在 C++11 之前的标准中,友元声明需要显式地指定 class,例如:friend class Friend

如果编译器还未看到友元的名称,显式地使用 class 仍然是必要的。例如:

class Class1
{
    // friend Unseen; // 编译失败:Unseen 未知。
    friend class Unseen; // 正确
};

具有成员指针的类

具有指针数据成员的类在第 9 章中已详细讨论。定义指针数据成员的类值得特别关注,因为它们通常需要定义拷贝构造函数、重载赋值操作符和析构函数。

存在一些情况,我们不需要指向对象的指针,而是需要指向类成员的指针。成员指针可以有效地用来配置类对象的行为。根据成员指针指向的成员不同,对象将表现出不同的行为。

虽然成员指针有其用处,但多态通常可以实现类似的行为。考虑一个类,它的成员 process 执行一系列备选行为之一。类可以使用某个(抽象)基类的接口,将某个派生类的对象传递给其构造函数,从而配置其行为。这种方法提供了简单、可扩展和灵活的配置方式,但访问类的数据成员的灵活性较差,并且可能需要使用“友元”声明。在这种情况下,成员指针可能更为合适,因为它们允许(虽然灵活性稍差)配置以及直接访问类的数据成员。

因此,选择显然是:一方面是配置的方便性,另一方面是访问类数据成员的方便性。本章将重点讨论成员指针,探讨这些指针所提供的功能。

成员指针示例

了解如何使用变量和对象的指针并不会自然地引导到成员指针的概念。即使考虑了成员函数的返回类型和参数类型,也容易遇到意外的情况。例如,考虑以下类:

class String
{
    char const *(*d_sp)() const;
public:
    char const *get() const;
};

对于这个类,char const *(*d_sp)() const 不能指向 String::get 成员函数,因为 d_sp 不能被赋予 get 成员函数的地址。

原因之一是,变量 d_sp 的作用域是全局的(它是一个函数指针,而不是指向 String 内的函数的指针),而成员函数 get 是在 String 类内部定义的,因此具有类作用域。d_spString 类的成员这一事实在这里并不相关。根据 d_sp 的定义,它指向一个在类之外的函数。

因此,要定义一个类的成员指针(无论是数据成员还是函数成员,但通常是函数成员),指针的作用域必须指示类作用域。为此,String::get 的成员指针定义如下:

char const *(String::*d_sp)() const;

通过在 *d_sp 指针数据成员前加上 String::,它被定义为在 String 类上下文中的指针。根据其定义,它是指向 String 类中的一个函数的指针,该函数不接受参数,不修改对象的数据,并返回一个指向常量字符的指针。

定义成员指针

成员指针的定义通过在常规指针表示法前加上适当的类名和作用域解析运算符来完成。因此,在前面的部分中,我们使用 char const * (String::*d_sp)() const 来表示 d_sp

  • 是一个指针(*d_sp);
  • 指向类 String 内的某个成员(String::*d_sp);
  • 是一个指向 const 函数的指针,返回 char const *char const * (String::*d_sp)() const)。

与之匹配的函数原型是:

char const *String::somefun() const;

这是一个 const 的无参数函数,返回 char const *

在定义成员指针时,可以继续应用构造函数指针的标准程序:

  1. 将完整的函数名(即包括函数类名在内的函数头)用括号括起来:
    char const * (String::somefun)() const
    
  2. 在函数名之前立即放置一个指针(*)字符:
    char const * (String::*somefun)() const
    
  3. 将函数名替换为指针变量的名称:
    char const * (String::*d_sp)() const
    

这是一个定义指向数据成员的指针的例子。假设 String 类包含一个字符串成员 d_text,如何构造指向该成员的指针?同样地,我们按照标准程序进行:

  1. 将完全限定的变量名用括号括起来:
    std::string (String::d_text)
    
  2. 在变量名之前立即放置一个指针(*)字符:
    std::string (String::*d_text)
    
  3. 将变量名替换为指针变量的名称:
    std::string (String::*tp)
    

在这种情况下,括号是多余的,可以省略:

std::string String::*tp

或者,一个非常简单的经验法则是:

  • 定义一个普通(即全局)指针变量,
  • 在指针字符前加上类名,一旦你指向类内部的某个成员。

例如,以下指向全局函数的指针:

char const * (*sp)() const;

在添加类作用域前缀后变成了指向成员函数的指针:

char const * (String::*sp)() const;

没有强制要求我们在目标(String)类中定义成员指针。成员指针可以在其目标类中定义(这样它们成为数据成员),也可以在其他类中定义,或作为局部变量或全局变量定义。在所有这些情况下,成员指针可以赋值为它所指向的成员的地址。重要的是,成员指针的初始化或赋值只指示了指针所指向的成员。这可以被视为某种相对地址;相对于函数调用的对象。在初始化或赋值成员指针时不需要对象。而且,虽然可以初始化或赋值成员指针,但(当然)不能在不指定正确类型的对象的情况下调用这些成员。

在以下示例中,初始化和赋值成员指针进行了说明(为了说明,PointerDemo 类的所有成员都定义为 public)。在示例中使用了 & 运算符来确定成员的地址。这些运算符以及类作用域是必需的,即使在成员实现内部使用时:

#include <cstddef>

class PointerDemo
{
public:
    size_t d_value;
    size_t get() const;
};

inline size_t PointerDemo::get() const
{
    return d_value;
}

int main()
{
    // 初始化
    size_t (PointerDemo::*getPtr)() const = &PointerDemo::get;
    size_t PointerDemo::*valuePtr = &PointerDemo::d_value;

    // 赋值
    getPtr = &PointerDemo::get;
    valuePtr = &PointerDemo::d_value;
}

赋值涉及的并没有特殊之处。与全局作用域的指针相比,我们现在限制在 PointerDemo 类的作用域内。因此,所有指针定义和所有变量的地址都必须给出 PointerDemo 类作用域。

成员指针也可以与虚拟成员函数一起使用。在指向虚拟成员时不需要特殊的语法。指针的构造、初始化和赋值方式与非虚拟成员相同。

使用成员指针

使用成员指针来调用成员函数需要存在一个类对象。对于全局作用域的指针,使用解引用运算符 *。对于对象的指针,使用成员选择运算符(->)或操作符(.)来选择适当的成员。

要在对象上使用成员指针,必须使用成员选择运算符(.*)。要通过指向对象的指针使用成员指针,必须使用“通过指针选择成员运算符”(->*)。这两个运算符结合了字段选择(.-> 部分)和解引用操作:解引用操作用于访问成员指针指向的函数或变量。

以下是一个使用成员函数指针和数据成员指针的示例:

#include <iostream>

class PointerDemo
{
public:
    size_t d_value;
    size_t get() const;
};

inline size_t PointerDemo::get() const
{
    return d_value;
}

using namespace std;

int main() {
    // 初始化
    size_t (PointerDemo::*getPtr)() const = &PointerDemo::get;
    size_t PointerDemo::*valuePtr = &PointerDemo::d_value;

    PointerDemo object;  // (1)(见文本)
    PointerDemo *ptr = &object;

    object.*valuePtr = 12345;  // (2)
    cout << object.*valuePtr << '\n' << object.d_value << '\n';

    ptr->*valuePtr = 54321; // (3)
    cout << object.d_value << '\n'
         << (object.*getPtr)() << '\n' // (4)
         << (ptr->*getPtr)() << '\n';
}

我们注意到:

  • 在 (1) 中,定义了一个 PointerDemo 对象和一个指向该对象的指针。
  • 在 (2) 中,我们指定了一个对象(因此使用 .* 运算符)来访问 valuePtr 指向的成员。这个成员被赋予了一个值。
  • 在 (3) 中,使用指向 PointerDemo 对象的指针对同一成员赋予了另一个值。因此我们使用了 ->* 运算符。
  • 在 (4) 中,再次使用了 .*->* 运算符,这次是通过成员指针调用函数。由于函数参数列表的优先级高于成员指针字段选择运算符,因此后者必须用括号保护。

成员指针可以在类具有根据配置设置表现不同的成员的情况下有益。例如,考虑在第 9.3 节中提到的 Person 类。Person 定义了存储一个人的姓名、地址和电话号码的数据成员。假设我们要构建一个员工数据库。可以查询该员工数据库,但根据查询数据库的人员类型,可能会显示姓名、姓名和电话号码,或所有存储的信息。这意味着像 address 这样的成员函数必须在不允许查看人员地址的情况下返回类似“<不可用>”的信息,而在其他情况下返回实际地址。

员工数据库通过指定一个反映查询人员状态的参数来打开。状态可能反映他或她在组织中的职位,如 BOARD、SUPERVISOR、SALESPERSON 或 CLERK。前两种类别允许查看所有员工信息,SALESPERSON 允许查看员工的电话号码,而 CLERK 仅允许验证某人是否确实是组织的成员。

我们现在在数据库类中构造一个成员 string personInfo(char const *name)。该类的标准实现可以是:

string PersonData::personInfo(char const *name) {
    Person *p = lookup(name);
    // 检查 `name` 是否存在
    if (!p) return "not found";

    switch (d_category) {
        case BOARD:
        case SUPERVISOR:
            return allInfo(p);
        case SALESPERSON:
            return noPhone(p);
        case CLERK:
            return nameOnly(p);
    }
}

虽然这不会花费太多时间,但每次调用 personInfo 时仍需评估一次 switch 语句。我们可以通过定义一个成员 d_infoPtr 作为一个指向 PersonData 类中返回字符串且接受指向 Person 的指针的成员函数的指针来代替 switch 语句。

通过使用成员指针,personInfo 成员函数现在可以简单地实现为:

string PersonData::personInfo(char const *name) {
    Person *p = lookup(name);
    // 检查 `name` 是否存在
    return p ? (this->*d_infoPtr)(p) : "not found";
}

成员 d_infoPtr 定义如下(在 PersonData 类中,省略了其他成员):

class PersonData
{
    std::string (PersonData::*d_infoPtr)(Person *p);
};

最后,构造函数初始化 d_infoPtr。这可以通过一个简单的 switch 实现:

PersonData::PersonData(PersonData::EmployeeCategory cat) {
    switch (cat) {
        case BOARD:
        case SUPERVISOR:
            d_infoPtr = &PersonData::allInfo;
            break;
        case SALESPERSON:
            d_infoPtr = &PersonData::noPhone;
            break;
        case CLERK:
            d_infoPtr = &PersonData::nameOnly;
            break;
    }
}

请注意如何确定成员函数的地址。即使我们已经在 PersonData 类的成员函数中,也必须指定类 PersonData 的作用域。

由于 EmployeeCategory 的值是已知的,上述构造函数中的 switch 语句也可以通过定义一个静态成员函数指针数组来轻松避免。PersonData 类定义了静态数组:

class PersonData
{
    std::string (PersonData::*d_infoPtr)(Person *p);
    static std::string (PersonData::*s_infoPtr[])(Person *p);
};

s_infoPtr[] 可以在编译时初始化:

std::string (PersonData::*PersonData::s_infoPtr[])(Person *p) = {
    &PersonData::allInfo, // BOARD
    &PersonData::allInfo, // SUPERVISOR
    &PersonData::noPhone, // SALESPERSON
    &PersonData::nameOnly  // CLERK
};

构造函数现在可以直接从适当的数组元素调用所需的成员,而不使用 switch

PersonData::PersonData(PersonData::EmployeeCategory cat) :
    d_infoPtr(s_infoPtr[cat])
{}

参考

第 19.1.54 节中提供了使用数据成员指针的示例,涉及稳定排序的通用算法。

静态成员指针

类的静态成员可以在没有类对象的情况下使用。公共静态成员可以像自由函数一样被调用,但调用时必须指定其类名。

假设有一个类 String,它有一个公共静态成员函数 count,返回迄今为止创建的字符串对象的数量。那么,在不使用任何 String 对象的情况下,可以调用 String::count 函数:

void fun()
{
    cout << String::count() << '\n';
}

公共静态成员可以像自由函数一样被调用(但请参见第 8.2.1 节)。私有静态成员只能在其类的上下文中,通过类的成员函数或友元函数进行调用。

由于静态成员没有关联的对象,因此它们的地址可以存储在普通的函数指针变量中,这些指针变量在全局作用域内操作。成员指针不能用于存储静态成员的地址。示例:

void fun()
{
    size_t (*pf)() = String::count;
    // 用静态成员函数的地址初始化 pf

    cout << (*pf)() << '\n';
    // 显示 String::count 返回的值
}

指针大小

成员指针的一个有趣特点是,它们的大小与“普通”指针不同。考虑以下小程序:

#include <iostream>
#include <string>
class X
{
public:
    void fun();
    std::string d_str;
};
inline void X::fun()
{
    std::cout << "hello\n";
}
using namespace std;
int main()
{
    cout <<
        "size of pointer to data-member: " << sizeof(&X::d_str) << "\n"
        "size of pointer to member function: " << sizeof(&X::fun) << "\n"
        "size of pointer to non-member data: " << sizeof(char *) << "\n"
        "size of pointer to free function: " << sizeof(&printf) << '\n';
}

生成的输出(在32位架构上):

size of pointer to data-member: 4
size of pointer to member function: 8
size of pointer to non-member data: 4
size of pointer to free function: 4

在32位架构上,成员函数指针需要八个字节,而其他类型的指针需要四个字节(使用 GNU 的 g++ 编译器)。

指针大小很少被明确使用,但它们的大小可能会导致在像 printf("%p", &X::fun); 这样的语句中出现混淆。当然,printf 可能不是显示这些 C++ 特有指针值的合适工具。

可以使用一个联合体将这些8字节指针解释为一系列 size_t 类型的 char 值,并将其插入到流中:

#include <iostream>
#include <string>
#include <iomanip>
class X
{
public:
    void fun();
    std::string d_str;
};
inline void X::fun()
{
    std::cout << "hello\n";
}
using namespace std;
int main()
{
    union
    {
        void (X::*f)();
        unsigned char *cp;
    }
    u = { &X::fun };
    cout.fill('0');
    cout << hex;
    for (unsigned idx = sizeof(void (X::*)()); idx-- > 0; )
        cout << setw(2) << static_cast<unsigned>(u.cp[idx]);
    cout << '\n';
}

为什么成员指针的大小与普通指针不同

为了回答这个问题,我们首先来看一下熟悉的 std::fstreamstd::fstream 是从 std::ifstreamstd::ofstream 派生出来的。因此,一个 fstream 包含了 ifstreamofstreamfstream 的组织结构如图 16.1 所示。
在这里插入图片描述

fstream (a) 中,第一个基类是 std::istream,第二个基类是 std::ofstream。但它也可以完全颠倒过来,如 fstream (b) 所示:首先是 std::ofstream,然后是 std::ifstream。这正是关键所在。

如果我们有一个 fstream fstr{"myfile"} 对象,并调用 fstr.seekg(0),那么我们调用的是 ifstreamseekg 函数。但是,如果我们调用 fstr.seekp(0),则调用的是 ofstreamseekp 函数。这些函数各自有自己的地址,例如 &seekg&seekp。但是当我们调用一个成员函数(如 fstr.seekp(0))时,实际上是在调用 seekp(&fstr, 0)

问题在于,&fstr 并不代表正确的对象地址:seekp 操作的是 ofstream,而 ofstream 对象并不是从 &fstr 开始的(在 fstream (a) 中,ofstream 对象的地址是在 &(fstr + sizeof(ifstream)) 处)。因此,编译器在调用一个类的成员函数时,必须对成员函数的对象进行偏移调整。

然而,当我们定义类似 ostream &(fstream::*ptr)(ios::off_type step, ios::seekdir org) = &seekp; 的指针,并调用 (fstr->*ptr)(0) 时,编译器不再知道实际调用的是哪个函数:它只是接收到函数的地址。为了解决编译器的问题,成员指针中存储了相对于对象位置的偏移量。这就是为什么使用函数指针时需要额外的数据字段的原因之一。

以下是一个具体的示例。首先定义两个结构体,每个结构体都有一个成员函数(所有成员函数都是内联的,使用单行实现以节省空间):

struct A
{
    int a;
};

struct B
{
    int b;
    void bfun() {}
};

然后定义结构体 C,它同时从 AB 派生(类似于 fstream,它包含 ifstreamofstream):

struct C : public A, public B
{};

接下来,在 main 函数中定义两个不同的联合体,并将 B::bfun 的地址赋值给它们的 ptr 字段。BPTR.ptr 将其视为结构体 B 的成员,而 CPTR.ptr 将其视为结构体 C 的成员。

一旦将联合体的指针字段赋值,它们的 value[] 数组被用来显示 ptr 字段的内容,如下所示:

int main() {
    union BPTR {
        void (B::*ptr)();
        unsigned long value[2];
    };
    BPTR bp;
    bp.ptr = &B::bfun;
    cout << hex << bp.value[0] << ' ' << bp.value[1] << dec << '\n';

    union CPTR {
        void (C::*ptr)();
        unsigned long value[2];
    };
    CPTR cp;
    cp.ptr = &C::bfun;
    cout << hex << cp.value[0] << ' ' << cp.value[1] << dec << '\n';
}

运行该程序时,我们看到输出:

400b0c 0
400b0c 4

(您看到的地址值(两行中的第一个值)可能不同)。注意到函数的地址是相同的,但由于在 CB 对象的位置在 A 对象之后,而 A 对象大小为 4 字节,因此在从 C 对象调用函数时,必须将 this 指针的值加 4。这正是指针的第二个字段中存储的偏移量告诉编译器的内容。

嵌套类

类可以在其他类内部定义。在其他类内部定义的类被称为嵌套类。嵌套类用于嵌套类与其外围类有密切的概念关联的情况。例如,类 string 有一个类型 string::iterator,它提供了字符串中存储的所有字符。这个 string::iterator 类型可以定义为一个对象迭代器,即作为类 string 中的嵌套类来定义。

由于嵌套类是在其他类内部定义的,当提供了对其外围类对象的引用或指针时,它们可以访问这些对象的所有成员,甚至是它们的私有成员。

一个类可以嵌套在其外围类的任何部分:公共部分(public),受保护部分(protected)或私有部分(private)。如果一个类嵌套在类的公共部分,那么它在外围类之外也是可见的。如果它嵌套在受保护部分,那么它在从外围类派生的子类中可见;如果它嵌套在私有部分,那么它仅对外围类的成员可见。

外围类对嵌套类没有特殊的权限。例如,考虑以下类定义:

class Surround {
public:
    class FirstWithin {
        int d_variable;
    public:
        FirstWithin();
        int var() const;
    };

private:
    class SecondWithin {
        int d_variable;
    public:
        SecondWithin();
        int var() const;
    };
};

inline int Surround::FirstWithin::var() const {
    return d_variable;
}

inline int Surround::SecondWithin::var() const {
    return d_variable;
}

在注释中,为了节省空间,嵌套类的接口通常在其外围类内部声明,如上所示。通常可以避免这样做,因为这更清楚地将外部类的接口和嵌套类的接口分开。同样,也应避免在类中实现成员函数。以下是如何分离外部类和嵌套类接口的示例:

class Surround {
    class SecondWithin;

public:
    class FirstWithin;
};

class Surround::FirstWithin {
    int d_variable;

public:
    FirstWithin();
    int var() const;
};

class Surround::SecondWithin {
    int d_variable;

public:
    SecondWithin();
    int var() const;
};

对于这三个类,成员的访问权限定义如下:

  • Surround::FirstWithin 类在 Surround 内部和外部都可见。因此,Surround::FirstWithin 具有全局可见性。
  • FirstWithin 的构造函数及其成员函数 var 也是全局可见的。
  • 数据成员 d_variable 仅对 Surround::FirstWithin 类的成员可见。Surround 类的成员和 Surround::SecondWithin 类的成员都不能直接访问 Surround::FirstWithin::d_variable
  • Surround::SecondWithin 类仅在 Surround 内部可见。Surround::SecondWithin 类的公共成员也可以被 Surround::FirstWithin 类的成员使用,因为嵌套类可以被视为其外围类的成员。
  • Surround::SecondWithin 的构造函数及其成员函数 var 也只能被 Surround 的成员(以及它的嵌套类的成员)访问。
  • Surround::SecondWithin::d_variable 仅对 Surround::SecondWithin 类的成员可见。Surround 的成员和 Surround::FirstWithin 的成员都不能直接访问 Surround::SecondWithind_variable
  • 像往常一样,在调用类成员之前,必须先有该类类型的对象。这对嵌套类也同样适用。

要授予外围类对嵌套类私有成员的访问权限,嵌套类可以将其外围类声明为友元类。相反,由于嵌套类可以被视为其外围类的成员,如果提供了外围类对象,则嵌套类的成员函数可以完全访问外部类的成员。

虽然嵌套类可以被视为外围类的成员,但嵌套类的成员并不是外围类的成员:Surround 类的成员不能直接调用 FirstWithin::var。这是可以理解的,因为 Surround 对象并不是 FirstWithinSecondWithin 对象。实际上,嵌套类只是类型名。这并不意味着这种类的对象会自动存在于外围类中。如果外围类的成员要使用嵌套类的(非静态)成员,那么外围类必须定义一个嵌套类对象,然后外围类的成员才能使用该嵌套类的成员。

例如,在以下类定义中,有一个外围类 Outer 和一个嵌套类 Inner。类 Outer 包含一个成员函数 caller。成员函数 caller 使用包含在 Outer 中的 d_inner 对象来调用 Inner::infunction

class Outer {
public:
    void caller();

private:
    class Inner {
    public:
        void infunction();
    };

    Inner d_inner; // 必须先知道类 Inner
};

void Outer::caller() {
    d_inner.infunction();
}

定义嵌套类的成员

嵌套类的成员函数可以定义为内联函数。内联成员函数可以像在类定义外部定义一样进行定义。为了在类 Outer 外部定义成员函数 Outer::caller,必须向编译器提供该函数的完全限定名称(从最外层的类作用域 Outer 开始)。内联和类内函数可以按照此方式定义,并且它们可以使用任何嵌套类,即使嵌套类的定义出现在外部类接口的后面。

当(嵌套的)成员函数被定义为内联时,它们的定义应放在其类接口的下面。静态嵌套数据成员通常也在其类外部定义。如果类 FirstWithin 有一个静态 size_t 数据成员 epoch,则可以如下初始化:

size_t Surround::FirstWithin::epoch = 1970;

此外,在外围类外部的代码中引用公共静态成员时,需要使用多个作用域解析运算符:

void showEpoch() {
    cout << Surround::FirstWithin::epoch;
}

在类 Surround 中,只需使用 FirstWithin:: 作用域;在类 FirstWithin 中,不需要显式引用作用域。

那么类 SecondWithin 的成员呢?类 FirstWithinSecondWithin 都嵌套在 Surround 内部,并且可以被视为外围类的成员。由于类的成员可以直接相互引用,SecondWithin 类的成员可以引用 FirstWithin 类的(公共)成员。因此,SecondWithin 类的成员可以通过 FirstWithin::epoch 引用 FirstWithinepoch 成员。

声明嵌套类

嵌套类可以在其实际定义之前声明在外围类中。如果一个类包含多个嵌套类,并且这些嵌套类包含指向其他嵌套类对象的指针、引用、参数或返回值,则需要进行这样的前向声明。

例如,下面的 Outer 类包含两个嵌套类 Inner1Inner2Inner1 类包含一个指向 Inner2 对象的指针,而 Inner2 类包含一个指向 Inner1 对象的指针。交叉引用需要前向声明。前向声明必须具有与其定义相同的访问权限说明。在下面的示例中,由于 Inner2 的定义是 Outer 类的私有接口的一部分,因此 Inner2 的前向声明必须在私有部分中给出:

class Outer {
private:
    class Inner2;  // 前向声明
    class Inner1 {
        Inner2 *pi2;  // 指向 Inner2 对象的指针
    };
    class Inner2 {
        Inner1 *pi1;  // 指向 Inner1 对象的指针
    };
};

访问嵌套类中的私有成员

要授予嵌套类访问其他嵌套类的私有成员的权限,或者授予外围类访问其嵌套类私有成员的权限,必须使用 friend 关键字。

不需要 friend 声明即可授予嵌套类访问其外围类私有成员的权限。外围类的静态成员可以直接访问,其他成员则可以通过在嵌套类中定义或传递外围类对象来访问。毕竟,嵌套类是由其外围类定义的类型,因此嵌套类的对象是外围类的成员,因而可以访问外围类的所有成员。以下是一个展示这一原则的示例。这个示例无法编译,因为 Extern 类的成员被拒绝访问 Outer 的私有成员,但 Outer::Inner 的成员可以访问 Outer 的私有成员:

class Outer {
    int d_value;
    static int s_value;
public:
    Outer() : d_value(12) {}

    class Inner {
    public:
        Inner() {
            cout << "Outer's static value: " << s_value << '\n';
        }
        Inner(Outer &outer) {
            cout << "Outer's value: " << outer.d_value << '\n';
        }
    };
};

class Extern { // 无法编译!
public:
    Extern(Outer &outer) {
        cout << "Outer's value: " << outer.d_value << '\n';
    }
    Extern() {
        cout << "Outer's static value: " << Outer::s_value << '\n';
    }
};

int Outer::s_value = 123;

int main() {
    Outer outer;
    Outer::Inner in1;
    Outer::Inner in2{outer};
}

现在考虑类 Surround 有两个嵌套类 FirstWithinSecondWithin 的情况。三个类都有一个静态数据成员 int s_variable

class Surround {
    static int s_variable;
public:
    class FirstWithin {
        static int s_variable;
    public:
        int value();
    };
    int value();
private:
    class SecondWithin {
        static int s_variable;
    public:
        int value();
    };
};

如果 Surround 类应该能够访问 FirstWithinSecondWithin 的私有成员,那么这两个类必须将 Surround 声明为它们的朋友。然后,函数 Surround::value 就可以访问其嵌套类的私有成员。例如(注意两个嵌套类中的 friend 声明):

class Surround {
    static int s_variable;
public:
    class FirstWithin {
        friend class Surround;
        static int s_variable;
    public:
        int value();
    };
    int value();
private:
    class SecondWithin {
        friend class Surround;
        static int s_variable;
    public:
        int value();
    };
};

inline int Surround::FirstWithin::value() {
    FirstWithin::s_variable = SecondWithin::s_variable;
    return s_variable;
}

friend 声明可以在被视为朋友的实体定义之后提供。因此,一个类可以在其定义之外被声明为朋友。在这种情况下,类内代码可能已经使用了即将被声明为朋友的事实。例如,考虑函数 Surround::FirstWithin::value 的类内实现。所需的 friend 声明也可以在 value 函数的实现之后插入:

class Surround {
public:
    class FirstWithin {
        static int s_variable;
    public:
        int value() {
            FirstWithin::s_variable = SecondWithin::s_variable;
            return s_variable;
        }
        friend class Surround;
    };
private:
    class SecondWithin {
        friend class Surround;
        static int s_variable;
    };
};

请注意,在外部类和内部类中同名的成员(例如 s_variable)可以使用适当的作用域解析表达式来访问,如下所示:

class Surround {
    static int s_variable;
public:
    class FirstWithin {
        friend class Surround;
        static int s_variable; // 同名成员
    public:
        int value();
    };
    int value();
private:
    class SecondWithin {
        friend class Surround;
        static int s_variable; // 同名成员
    public:
        int value();
    };
    static void classMember();
};

inline int Surround::value() {
    // 作用域解析表达式
    FirstWithin::s_variable = SecondWithin::s_variable;
    return s_variable;
}

inline int Surround::FirstWithin::value() {
    Surround::s_variable = 4;
    // 作用域解析表达式
    Surround::classMember();
    return s_variable;
}

inline int Surround::SecondWithin::value() {
    Surround::s_variable = 40;
    // 作用域解析表达式
    return s_variable;
}

嵌套类并不自动成为彼此的朋友。为了授予一个嵌套类访问另一个嵌套类私有成员的权限,必须提供 friend 声明。

要授予 FirstWithin 访问 SecondWithin 私有成员的权限,SecondWithin 必须包含 friend 声明。同样,FirstWithin 类也可以使用 friend class SecondWithin 声明,以授予 SecondWithin 访问 FirstWithin 私有成员的权限。即使编译器尚未看到 SecondWithinfriend 声明也被视为前向声明。

注意,SecondWithin 的前向声明不能通过 class Surround::SecondWithin;FirstWithin 中指定,因为这会生成类似于“‘Surround’ does not have a nested type named ‘SecondWithin’”的错误消息。

现在假设除了嵌套类 SecondWithin 之外,还有一个外层级别的 SecondWithin 类。要将该类声明为 FirstWithin 的朋友,请在 FirstWithin 类中声明 friend ::SecondWithin。在这种情况下,必须在编译器遇到 friend ::SecondWithin 声明之前提供 FirstWithin 的外层级别类声明。

下面是一个示例,其中所有类都可以完全访问所有涉及类的所有私有成员,并且还声明了一个外层级别的 FirstWithin

class SecondWithin;

class Surround {
    // 类 SecondWithin; 不需要(但没有错误):
    // friend 声明(见下文)也被视为前向声明
    static int s_variable;
public:
    class FirstWithin {
        friend class Surround;
        friend class SecondWithin;
        friend class ::SecondWithin;
        static int s_variable;
    public:
        int value();
    };
    int value(); // 实现见上文
private:
    class SecondWithin {
        friend class Surround;
        friend class FirstWithin;
        static int s_variable;
    public:
        int value();
    };
};

inline int Surround::FirstWithin::value() {
    Surround::s_variable = SecondWithin::s_variable;
    return s_variable;
}

inline int Surround::SecondWithin::value() {
    Surround::s_variable = FirstWithin::s_variable;
    return s_variable;
}

嵌套枚举

枚举类型也可以嵌套在类中。嵌套枚举是一种展示枚举与其所属类紧密关系的好方法。嵌套枚举具有与其他类成员相同的受控可见性。它们可以定义在类的私有(private)、受保护(protected)或公共(public)部分,并且可以被派生类继承。我们在 ios 类中看到过类似 ios::begios::cur 的值。在当前的 GNU C++ 实现中,这些值被定义为 seek_dir 枚举类型的值:

class ios: public _ios_fields {
public:
    enum seek_dir {
        beg,
        cur,
        end
    };
};

举个例子,假设有一个 DataStructure 类,表示可以向前或向后遍历的数据结构。这样的类可以定义一个枚举 Traversal,其值为 FORWARDBACKWARD。此外,还可以定义一个 setTraversal 成员函数,该函数需要一个 Traversal 类型的参数。该类可以定义如下:

class DataStructure {
public:
    enum Traversal {
        FORWARD,
        BACKWARD
    };

    void setTraversal(Traversal mode);

private:
    Traversal d_mode;
};

DataStructure 类内部,可以直接使用 Traversal 枚举的值。例如:

void DataStructure::setTraversal(Traversal mode) {
    d_mode = mode;
    switch (d_mode) {
    case FORWARD:
        // ... 执行某些操作
        break;
    case BACKWARD:
        // ... 执行其他操作
        break;
    }
}

DataStructure 类外部,引用枚举值时不需要使用枚举类型的名称,类名已经足够。只有当需要枚举类型的变量时,才需要枚举类型的名称,如以下代码所示:

void fun() {
    DataStructure::Traversal localMode = DataStructure::FORWARD;  // 需要枚举类型名称

    DataStructure ds;
    ds.setTraversal(DataStructure::BACKWARD);  // 不需要枚举类型名称
}

在上述例子中,使用 DataStructure::FORWARD 常量来指定 DataStructure 类中定义的枚举的值。实际上,使用 ds.FORWARD 这种语法结构也是可以接受的。但在我看来,这种语法自由度显得不太好:FORWARD 是在类级别定义的符号值,并不是 ds 的成员,而使用成员选择操作符则暗示了这一点。

只有在 DataStructure 定义了一个嵌套类 Nested,且该嵌套类又定义了枚举 Traversal 时,才需要使用两个类作用域。在这种情况下,下面的例子应该这样编写:

void fun() {
    DataStructure::Nested::Traversal localMode = DataStructure::Nested::FORWARD;
    DataStructure ds;

    ds.setTraversal(DataStructure::Nested::BACKWARD);
}

在这种情况下,类似 DataStructure::Nested::Traversal localMode = ds.Nested::FORWARD 的构造也是可以使用的,尽管我个人会避免使用它,因为 FORWARD 不是 ds 的成员,而是 DataStructure 中定义的一个符号。

空枚举

枚举类型通常定义符号值。然而,这并不是必需的。在 14.6.1 节中介绍了 std::bad_cast 类型。当不能将基类对象的引用转换为派生类引用时,dynamic_cast<> 操作符会抛出 bad_cast 异常。bad_cast 可以作为一种类型被捕获,而不考虑它可能代表的任何值。

类型可以在没有任何关联值的情况下定义。可以定义一个不包含任何值的空枚举。然后,这个空枚举的类型名称可以作为合法类型使用,例如在 catch 子句中。

下面的例子展示了如何定义一个空枚举(通常,但不一定在类中定义),以及如何将其作为异常抛出(和捕获):

#include <iostream>

enum EmptyEnum {};

int main() 
try 
{
    throw EmptyEnum();
}
catch (EmptyEnum) 
{
    std::cout << "Caught empty enum\n";
}

重新审视虚构造函数

在第 14.13 节中引入了虚构造函数的概念。在该章节中,类 Base 被定义为一个抽象基类。类 Clonable 被定义为管理 Base 类指针的容器(如 vector)。

由于类 Base 是一个非常简单的类,几乎不需要任何实现,因此可以将其作为 Clonable 的嵌套类来定义。这突出了 ClonableBase 之间的紧密关系。将 Base 嵌套在 Clonable 之下,会将原来的定义:

class Derived: public Base

改为:

class Derived: public Clonable::Base

除了将 Base 定义为嵌套类,并从 Clonable::Base 而非 Base 派生(以及为 Base 的成员提供适当的 Clonable:: 前缀以完成其完全限定名称)之外,不需要进行进一步的修改。以下是之前显示的程序的修改部分(参见第 14.13 节),现在使用嵌套在 Clonable 下的 Base

// Clonable 和嵌套的 Base,包括它们的内联成员:
class Clonable
{
public:
    class Base;

private:
    Base *d_bp;

public:
    class Base {
    public:
        virtual ~Base();
        Base *clone() const;

    private:
        virtual Base *newCopy() const = 0;
    };

    Clonable();
    explicit Clonable(Base *base);
    ~Clonable();
    Clonable(Clonable const &other);
    Clonable(Clonable &&tmp);
    Clonable &operator=(Clonable const &other);
    Clonable &operator=(Clonable &&tmp);
    Base &base() const;
};

inline Clonable::Base *Clonable::Base::clone() const
{
    return newCopy();
}

inline Clonable::Base &Clonable::base() const
{
    return *d_bp;
}

// Derived1 及其内联成员:
class Derived1: public Clonable::Base
{
public:
    ~Derived1();

private:
    virtual Clonable::Base *newCopy() const;
};

inline Clonable::Base *Derived1::newCopy() const
{
    return new Derived1(*this);
}

// 非内联实现的成员:
Clonable::Base::~Base()
{}

这个例子展示了如何将虚构造函数的概念与嵌套类结合使用,以实现更紧密的类关系定义和管理。

虚构造函数实际上并不存在于 C++ 语言中,但可以通过设计模式(如原型模式)来模拟其行为。完整的代码示例通常会展示如何通过嵌套类和虚函数实现基类的克隆行为。在这个示例中,我们使用 Clonable 类来管理指向抽象基类 Base 的指针,并通过派生类来实现克隆操作。

以下是使用嵌套类来模拟虚构造函数的完整代码示例:

#include <iostream>
#include <memory>
#include <vector>
// 基类 Clonable
class Clonable {
public:
    // 抽象基类 Base,定义在 Clonable 内
    class Base {
    public:
        virtual ~Base() {}
        // 克隆函数,调用派生类的 newCopy 函数
        Base* clone() const { return newCopy(); }
        // 打印对象类型的虚拟函数
        virtual void printType() const = 0;

    private:
        // 纯虚函数,派生类需要实现此函数来实现深拷贝
        virtual Base* newCopy() const = 0;
    };
    // 构造函数和析构函数
    Clonable() : d_bp(nullptr) {}
    explicit Clonable(Base* base) : d_bp(base) {}
    ~Clonable() { delete d_bp; }
    // 拷贝构造函数
    Clonable(const Clonable& other)
        : d_bp(other.d_bp ? other.d_bp->clone() : nullptr) {}
    // 移动构造函数
    Clonable(Clonable&& tmp) noexcept : d_bp(tmp.d_bp) { tmp.d_bp = nullptr; }
    // 拷贝赋值运算符
    Clonable& operator=(const Clonable& other) {
        if (this != &other) {
            delete d_bp;
            d_bp = other.d_bp ? other.d_bp->clone() : nullptr;
        }
        return *this;
    }
    // 移动赋值运算符
    Clonable& operator=(Clonable&& tmp) noexcept {
        if (this != &tmp) {
            delete d_bp;
            d_bp = tmp.d_bp;
            tmp.d_bp = nullptr;
        }
        return *this;
    }
    // 访问 Base 类对象
    Base* base() const { return d_bp; }

private:
    Base* d_bp;
};
// 派生类 Derived1,继承自 Clonable::Base
class Derived1 : public Clonable::Base {
public:
    Derived1() : data(1) {}  // 构造函数
    ~Derived1() {}           // 析构函数
private:
    int data;
    // 实现克隆方法,创建当前对象的深拷贝
    virtual Clonable::Base* newCopy() const override {
        return new Derived1(*this);
    }
    // 实现打印对象类型的方法
    virtual void printType() const override {
        std::cout << "I am Derived1 data = " << data << std::endl;
    }
};
// 派生类 Derived2,继承自 Clonable::Base
class Derived2 : public Clonable::Base {
public:
    Derived2() : data(2) {}  // 构造函数
    ~Derived2() {}           // 析构函数
private:
    int data;
    // 实现克隆方法,创建当前对象的深拷贝
    virtual Clonable::Base* newCopy() const override {
        return new Derived2(*this);
    }
    // 实现打印对象类型的方法
    virtual void printType() const override {
        std::cout << "I am Derived2 data = " << data << std::endl;
    }
};
int main() {
    // 创建 Clonable 对象,管理 Derived1 对象
    Clonable c1(new Derived1());
    // 通过拷贝构造函数复制 c1
    Clonable c2 = c1;
    // 创建 Clonable 对象,管理 Derived2 对象
    Clonable c3(new Derived2());
    // 通过移动构造函数移动 c3 到 c4
    Clonable c4 = std::move(c3);
    // 测试打印对象类型
    std::cout << "Testing original object:" << std::endl;
    c1.base()->printType();  // 应该输出 "I am Derived1"
    std::cout << "Testing cloned object:" << std::endl;
    c2.base()->printType();  // 应该输出 "I am Derived1"
    std::cout << "Testing moved object:" << std::endl;
    if (c4.base()) {
        c4.base()->printType();  // 应该输出 "I am Derived2"
    } else {
        std::cout << "Moved object is empty." << std::endl;
    }
    std::cout << "Testing original object after move:" << std::endl;
    if (c3.base()) {
        c3.base()->printType();  // 应该输出 "I am Derived2" 或者 "Moved object
                                 // is empty."
    } else {
        std::cout << "Original object after move is empty." << std::endl;
    }
    return 0;
}

代码解释:

  1. Clonable 类:该类包含一个嵌套的抽象基类 Base,并且管理指向 Base 对象的指针。clone 函数通过调用纯虚函数 newCopy 来实现深拷贝。
  2. Derived1 和 Derived2 类:它们是从 Clonable::Base 派生的类,实现了 newCopy 函数,以返回自身的深拷贝。
  3. main 函数:展示了如何创建和操作 Clonable 对象,并通过拷贝和移动语义来管理这些对象。

这个设计模式让我们能够以一种模拟“虚构造函数”的方式创建对象的副本,并管理这些对象的生命周期。

标准模板库(STL)

标准模板库(STL)是一个通用库,包括容器、通用算法、迭代器、函数对象、分配器、适配器和数据结构。STL 使用的这些数据结构是抽象的,这意味着算法可以用于(几乎)任何数据类型。

算法能够处理这些抽象数据类型是因为它们是基于模板的。本章不涵盖模板的构造(参见第21章)。本章重点介绍算法的使用。

标准模板库中使用的几个元素已经在C++注释中讨论过。在第12章中讨论了抽象容器,第11.11节介绍了函数对象。此外,迭代器也在本文件的多个地方提到过。

STL 的主要组件在本章和下一章中进行介绍。迭代器、适配器、智能指针、多线程以及 STL 的其他特性将在后续部分讨论。通用算法将在下一章(第19章)中介绍。

分配器负责 STL 中的内存分配。默认的分配器类足以满足大多数应用需求,本书中不会进一步讨论。

STL 的所有元素都定义在标准命名空间中。因此,除非明确指定使用所需的命名空间,否则需要使用 using namespace std 或类似的指令。在头文件中应明确使用 std 命名空间(参见第7.11.1节)。

在本章中,空尖括号符号表示法被频繁使用。在代码中,必须在尖括号中提供一个类型名。例如,plus<> 在 C++ 注释中使用,但在代码中可能会遇到 plus<string>

预定义函数对象

在使用本节中介绍的预定义函数对象之前,必须包含 <functional> 头文件。

函数对象在通用算法中发挥着重要作用。例如,存在一个通用算法 sort,它期望两个迭代器来定义应该排序的对象范围,以及一个函数对象,用于调用适当的比较操作符。让我们快速了解一下这种情况。假设字符串存储在一个向量中,我们想要按降序排序这个向量。在这种情况下,对向量 stringVec 进行排序是非常简单的:

sort(stringVec.begin(), stringVec.end(), greater<string>());

最后一个参数被识别为构造函数:它是 greater<> 类模板的一个实例,应用于字符串。这个对象被通用算法 sort 作为函数对象调用。

通用算法调用函数对象的 operator() 成员函数来比较两个字符串对象。函数对象的 operator() 将会调用字符串数据类型的 operator>。最终,当 sort 返回时,向量的第一个元素将包含所有字符串中值最大的字符串。

函数对象的 operator() 本身在这个阶段是不可见的。不要将 'greater<string>()' 参数中的圆括号与调用 operator() 混淆。当 operator() 实际上在 sort 内部被使用时,它接收两个参数:两个字符串用于比较“大小”。由于 greater<string>::operator() 是内联定义的,因此上述 sort 调用中实际上并没有 operator() 的调用。相反,sort 通过 greater<string>::operator() 调用 string::operator>

现在我们知道构造函数作为参数传递给(许多)通用算法,我们可以设计自己的函数对象。假设我们想要对向量进行不区分大小写的排序。我们该如何操作?首先我们注意到默认的 string::operator<(用于递增排序)不适合,因为它进行的是区分大小写的比较。因此,我们提供了自己的 CaseInsensitive 类,它不区分大小写地比较两个字符串。使用 POSIX 函数 strcasecmp,以下程序可以实现这一功能。它对命令行参数进行不区分大小写的升序排序:

#include <iostream>
#include <string>
#include <cstring>
#include <algorithm>

using namespace std;

class CaseInsensitive
{
public:
    bool operator()(string const &left, string const &right) const
    {
        return strcasecmp(left.c_str(), right.c_str()) < 0;
    }
};

int main(int argc, char **argv)
{
    sort(argv, argv + argc, CaseInsensitive{});
    for (int idx = 0; idx < argc; ++idx)
        cout << argv[idx] << " ";
    cout << '\n';
}

CaseInsensitive 类的默认构造函数用于为 sort 提供最后一个参数。因此,唯一需要定义的成员函数是 CaseInsensitive::operator()。由于我们知道它会接受 string 类型的参数,因此我们将其定义为接受两个 string 参数,这些参数在调用 strcasecmp 时使用。此外,函数调用运算符 operator() 被定义为内联,以避免 sort 函数调用时的额外开销。sort 函数会用各种字符串组合调用函数对象。如果编译器满足我们的内联请求,它实际上会调用 strcasecmp,跳过两个额外的函数调用。比较函数对象通常是预定义的函数对象。预定义函数对象类可用于许多常用操作。在接下来的章节中,将介绍可用的预定义函数对象,并提供一些使用示例。在关于函数对象的部分末尾,还介绍了函数适配器。

预定义函数对象主要与通用算法一起使用。预定义函数对象存在于算术、关系和逻辑操作中。

算术函数对象

算术函数对象支持标准的算术操作:加法、减法、乘法、除法、取模和取反。这些函数对象调用其实例化数据类型的相应运算符。例如,对于加法,函数对象 plus<Type> 可用。如果我们将 Type 替换为 size_t,则使用 size_t 值的加法运算符;如果我们将 Type 替换为 string,则使用字符串的加法运算符。示例如下:

#include <iostream>
#include <string>
#include <functional>
using namespace std;

int main(int argc, char **argv) {
    plus<size_t> uAdd;
    // 用于加法的 size_t 函数对象
    cout << "3 + 5 = " << uAdd(3, 5) << '\n';

    plus<string> sAdd;
    // 用于字符串加法的函数对象
    cout << "argv[0] + argv[1] = " << sAdd(argv[0], argv[1]) << '\n';
}

输出(当以 a.out going 调用时):

3 + 5 = 8
argv[0] + argv[1] = a.outgoing
为什么有用?

函数对象可以与所有支持被调用运算符的数据类型一起使用(不仅限于预定义的数据类型)。这在进行如下操作时特别有用:

  • 对一个左操作数(总是相同的变量)进行操作,并将右操作数的所有元素用于处理。例如,我们想计算数组中所有元素的总和;或者我们想连接文本数组中的所有字符串。在这些情况下,函数对象非常方便。

如前所述,函数对象在通用算法的上下文中被广泛使用。接下来,我们来看看另一个常见的算法。

accumulate 算法

accumulate 算法访问由迭代器范围指定的所有元素,并对一个公共元素和范围内的每个元素执行请求的二元操作,最终返回在访问所有元素后累积的结果。以下程序将所有命令行参数累加并打印最终字符串:

#include <iostream>
#include <string>
#include <functional>
#include <numeric>
using namespace std;

int main(int argc, char **argv) {
    string result = accumulate(argv, argv + argc, string{}, plus<string>());
    cout << "All concatenated arguments: " << result << '\n';
}

前两个参数定义了要访问的(迭代器)范围,第三个参数是 string。这个匿名字符串对象提供了初始值。我们也可以使用 "All concatenated arguments: "s,这样 cout 语句可以简单地写为 cout << result << '\n'。字符串加法操作由 plus<string> 调用,最终返回连接后的字符串。

自定义类型的函数对象

现在我们定义一个 Time 类,重载了 operator+。我们可以将预定义的函数对象 plus 应用于我们新定义的数据类型 Time,以实现时间的加法:

#include <iostream>
#include <string>
#include <vector>
#include <functional>
#include <numeric>
using namespace std;

class Time {
    friend ostream &operator<<(ostream &str, Time const &time);
    size_t d_days;
    size_t d_hours;
    size_t d_minutes;
    size_t d_seconds;

public:
    Time(size_t hours, size_t minutes, size_t seconds);
    Time &operator+=(Time const &rhs);
};

Time operator+(Time const &lhs, Time const &rhs) {
    Time ret(lhs);
    return std::move(ret += rhs);
}

Time::Time(size_t hours, size_t minutes, size_t seconds)
    : d_days(0), d_hours(hours), d_minutes(minutes), d_seconds(seconds) {}

Time &Time::operator+=(Time const &rhs) {
    d_seconds += rhs.d_seconds;
    d_minutes += rhs.d_minutes + d_seconds / 60;
    d_hours += rhs.d_hours + d_minutes / 60;
    d_days += rhs.d_days + d_hours / 24;
    d_seconds %= 60;
    d_minutes %= 60;
    d_hours %= 24;
    return *this;
}

ostream &operator<<(ostream &str, Time const &time) {
    return cout << time.d_days << " days, " << time.d_hours
                << " hours, " << time.d_minutes << " minutes and "
                << time.d_seconds << " seconds.";
}

int main() {
    vector<Time> tvector;
    tvector.push_back(Time(1, 10, 20));
    tvector.push_back(Time(10, 30, 40));
    tvector.push_back(Time(20, 50, 0));
    tvector.push_back(Time(30, 20, 30));

    cout << accumulate(tvector.begin(), tvector.end(), Time(0, 0, 0), plus<Time>()) << '\n';
}

显示:

2 days, 14 hours, 51 minutes and 30 seconds.
  • 算术函数对象:支持标准算术操作,并可以用于各种数据类型。
  • accumulate 算法:使用函数对象对元素进行累积操作。
  • 自定义类型的函数对象:可以通过重载运算符和应用预定义的函数对象来处理自定义类型。

上述程序的设计相当直接。Time 类定义了一个构造函数、一个插入运算符(operator<<)以及一个用于添加两个时间对象的 operator+。在 main 函数中,四个 Time 对象被存储在一个 vector<Time> 对象中。接着,使用 accumulate 算法计算累积时间,并将结果插入到 cout 中。

在这一节的第一个示例中,演示了如何使用命名函数对象。而最后两个示例展示了如何将匿名对象传递给 accumulate 函数。

STL 支持的算术函数对象

STL 支持以下一组算术函数对象。这些函数对象的函数调用运算符(operator())调用传递给函数调用运算符的对象的匹配算术运算符,并返回该运算符的返回值。实际调用的算术运算符如下:

  • plus<>: 调用二元运算符 +
  • minus<>: 调用二元运算符 -
  • multiplies<>: 调用二元运算符 *
  • divides<>: 调用二元运算符 /
  • modulus<>: 调用二元运算符 %
  • negate<>: 调用一元运算符 -。这个算术函数对象是一个一元函数对象,因为它只接受一个参数。
使用 transform 算法示例

接下来,使用 transform 通用算法来切换数组中所有元素的符号。transform 需要两个迭代器来定义要转换的对象范围;一个迭代器来定义目标范围的开始(这个迭代器可以与第一个参数相同);以及一个函数对象来定义对指示数据类型的单一操作。

#include <iostream>
#include <string>
#include <functional>
#include <algorithm>
using namespace std;

int main(int argc, char **argv) {
    int iArr[] = { 1, -2, 3, -4, 5, -6 };
    transform(iArr, iArr + 6, iArr, negate<int>());
    for (int idx = 0; idx < 6; ++idx)
        cout << iArr[idx] << ", ";
    cout << '\n';
}

输出:

-1, 2, -3, 4, -5, 6,

关系函数对象

关系函数对象调用关系运算符。所有标准关系运算符都被支持:==!=>>=<<=

STL 支持以下一组关系函数对象。这些函数对象的函数调用运算符(operator())调用传递给函数调用运算符的对象的匹配关系运算符,并返回该运算符的返回值。实际调用的关系运算符如下:

  • equal_to<>: 调用运算符 ==
  • not_equal_to<>: 调用运算符 !=
  • greater<>: 调用运算符 >
  • greater_equal<>: 调用运算符 >=
  • less<>: 这个对象的成员 operator() 调用运算符 <
  • less_equal<>: 调用运算符 <=
示例

下面的示例展示了如何将关系函数对象与 sort 算法结合使用:

#include <iostream>
#include <algorithm>
#include <functional>
#include <string>
using namespace std;

int main(int argc, char **argv) {
    sort(argv, argv + argc, greater_equal<string>());
    for (int idx = 0; idx < argc; ++idx) 
        cout << argv[idx] << " ";
    cout << '\n';

    sort(argv, argv + argc, less<string>());
    for (int idx = 0; idx < argc; ++idx) 
        cout << argv[idx] << " ";
    cout << '\n';
}

输出:

(以降序排列的字符串)
(以升序排列的字符串)
说明

此示例展示了如何按字母顺序对字符串进行排序,并按反向字母顺序进行排序。通过传递 greater_equal<string>,字符串按降序排列(第一个单词将是“最大”的);通过传递 less<string>,字符串按升序排列(第一个单词将是“最小”的)。

需要注意的是,argv 包含 char* 类型的值,而关系函数对象期望的是 string 类型。char const*string 的转换会自动完成。

逻辑函数对象

逻辑函数对象调用逻辑运算符。支持的标准逻辑运算符包括:andornot

STL 支持以下一组逻辑函数对象。这些函数对象的函数调用运算符(operator())调用传递给函数调用运算符的对象的匹配逻辑运算符,并返回该运算符的返回值。实际调用的逻辑运算符如下:

  • logical_and<>: 调用运算符 &&
  • logical_or<>: 调用运算符 ||
  • logical_not<>: 调用运算符 !
示例

下面的示例展示了如何使用 logical_not 逻辑函数对象,将数组中的逻辑值进行转换:

#include <iostream>
#include <string>
#include <functional>
#include <algorithm>
using namespace std;

int main(int argc, char **argv)
{
    bool bArr[] = {true, true, true, false, false, false};
    size_t const bArrSize = sizeof(bArr) / sizeof(bool);
    for (size_t idx = 0; idx < bArrSize; ++idx)
        cout << bArr[idx] << " ";
    cout << '\n';
    
    transform(bArr, bArr + bArrSize, bArr, logical_not<bool>());
    
    for (size_t idx = 0; idx < bArrSize; ++idx)
        cout << bArr[idx] << " ";
    cout << '\n';
}

输出:

1 1 1 0 0 0
0 0 0 1 1 1
说明

在这个程序中,数组 bArr 初始包含了布尔值 truefalse。首先打印出原始数组的内容。接着,使用 transform 算法和 logical_not<bool> 函数对象将数组中的每个布尔值取反。最后,打印出取反后的数组内容。

std::not_fn 否定器

否定器是一个函数对象,用于切换被调用函数的布尔值:如果函数返回 true,则否定器返回 false,反之亦然。

标准否定器是 std::not_fn,在 <functional> 头文件中声明。std::not_fn 函数接受一个(可移动的)对象作为参数,返回其函数调用运算符的返回值的否定值。

示例

假设在 main 函数中定义了一个 int 值数组:

int main()
{
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
}

要计算数组中偶数值的数量,可以使用 lambda 函数和 count_if 算法:

cout << count_if(arr, arr + size(arr),
[&](int value)
{
    return (value & 1) == 0;
}
) << '\n';

要计算数组中奇数值的数量,可以使用 std::not_fn,如下所示:

cout << count_if(arr, arr + size(arr),
std::not_fn(
    [&](int value)
    {
        return (value & 1) == 0;
    }
)
) << '\n';

说明

在这个简单的例子中,lambda 函数也可以很容易地进行修改。然而,如果使用的是一个现有的实现了函数对象的类,而不是 lambda 函数,改变该类的行为可能会很困难或不可能。如果该类提供了移动操作,则可以使用 std::not_fn 来否定该类的函数调用运算符返回的值。

迭代器

除了本节介绍的概念性迭代器类型,STL 还定义了几种适配器,允许将对象作为迭代器传递。这些适配器将在后续章节中介绍。在使用这些适配器之前,必须包含 <iterator> 头文件。

标准迭代器 (std::iterator) 现在已被弃用,编译器会发出相应的警告。因此,在设计自己的迭代器时,不应再使用 std::iterator(第 22.14 节介绍了如何设计自己的迭代器)。

迭代器是行为类似于指针的对象。迭代器具有以下一般特征:

  • 两个迭代器可以使用 ==!= 操作符进行比较。通常不能使用排序操作符(例如 ><)。
  • 给定一个迭代器 iter*iter 表示迭代器指向的对象(或者,可以使用 iter-> 访问迭代器指向的对象的成员)。
  • 给定一个迭代器 iteriter.base() 返回 *iter 的地址。它返回与 &*iter 相同的类型。例如:
    vector<int> vi{ 1, 2, 3 };
    int *ip = vi.begin().base();
    cout << *ip << '\n';  // 输出:1
    
  • ++iteriter++ 将迭代器推进到下一个元素。因此,推进迭代器到下一个元素的概念被应用:多个容器支持 reverse_iterator 类型,在这些迭代器中,++iter 操作实际上会到达序列中的前一个元素。
  • 对于存储其元素在内存中连续的容器(如 vectordeque),可以使用指针算术。对于这些容器,iter + 2 指向比 iter 指向的元素后的第二个元素。另见第 18.2.1 节,涵盖了 std::distance
  • 仅定义一个迭代器类似于具有一个 0 指针。例如:
    #include <vector>
    #include <iostream>
    using namespace std;
    
    int main() {
        vector<int>::iterator vi;
        cout << &*vi;  // 输出 0
    }
    

STL 容器通常定义了提供迭代器的成员(即定义了自己的 iterator 类型)。这些成员通常称为 beginend,以及(对于反向迭代器 reverse_iteratorrbeginrend

虽然可以使用 reverse_iterator 构造函数从普通(前向)迭代器构造反向迭代器,如:

string str;
auto revit = string::reverse_iterator{ str.begin() };

但反向迭代器的前向迭代器不能这样获取。要检索与反向迭代器对应的前向迭代器,可以使用 reverse_iterator.base() 成员。例如,要获得与 revit 对应的前向迭代器,可以使用:

auto forward { revit.base() };

标准做法要求迭代器范围是左闭右开区间。[left, right) 表示 left 是指向第一个元素的迭代器,而 right 是指向最后一个元素之后的位置的迭代器。当 left == right 时,迭代器范围为空。

以下示例展示了如何使用迭代器范围 [begin(), end())[rbegin(), rend())vector<string> 的所有元素插入到 cout 中。注意,两个范围的 for 循环是相同的。此外,它还很好地展示了如何使用 auto 关键字来定义循环控制变量的类型,而不是使用更详细的变量定义,如 vector<string>::iterator(另见第 3.3.7 节):

#include <iostream>
#include <string>
#include <vector>
using namespace std;

int main(int argc, char **argv) {
    vector<string> args(argv, argv + argc);
    for (auto iter = args.begin(); iter != args.end(); ++iter)
        cout << *iter << " ";
    cout << '\n';

    for (auto iter = args.rbegin(); iter != args.rend(); ++iter)
        cout << *iter << " ";
    cout << '\n';
}

常量迭代器

STL 还定义了 const_iterator 类型,用于访问常量容器中的元素。虽然在前面的示例中 vector 的元素可以被修改,但在下一个示例中,vector 的元素是不可变的,因此需要使用 const_iterator

#include <iostream>
#include <string>
#include <vector>
using namespace std;

int main(int argc, char **argv) {
    vector<string> const args(argv, argv + argc);

    for (vector<string>::const_iterator iter = args.begin(); iter != args.end(); ++iter)
        cout << *iter << " ";
    cout << '\n';

    for (vector<string>::const_reverse_iterator iter = args.rbegin(); iter != args.rend(); ++iter)
        cout << *iter << " ";
    cout << '\n';
}

迭代器类型

STL 定义了六种迭代器类型。这些迭代器类型是泛型算法所期望的,因此创建特定类型的迭代器时,需要了解它们的特性。一般来说,迭代器(另见第 22.14 节)必须定义:

  • operator==:测试两个迭代器是否相等。
  • operator!=:测试两个迭代器是否不相等。
  • operator++:递增迭代器,作为前缀操作符。
  • operator*:访问迭代器所引用的元素。

以下是不同类型的迭代器及其用途:

  • 输入迭代器(InputIterators):用于从容器中读取数据。解引用操作符在表达式中保证能作为右值使用。也可以使用前向迭代器(ForwardIterators)、双向迭代器(BidirectionalIterators)或随机访问迭代器(RandomAccessIterators)。例如,泛型算法 inner_product 的原型为:

    Type inner_product(InputIterator1 first1, InputIterator1 last1,
                       InputIterator2 first2, Type init);
    

    InputIterator1 first1InputIterator1 last1 定义了一对输入迭代器,而 InputIterator2 first2 定义了另一个范围的开始。类似的符号也可以用于其他迭代器类型。

  • 输出迭代器(OutputIterators):用于向容器中写入数据。解引用操作符在表达式中保证能作为左值使用,但不一定能作为右值。也可以使用前向迭代器、双向迭代器或随机访问迭代器。

  • 前向迭代器(ForwardIterators):结合了输入迭代器和输出迭代器。可以用于在一个方向上遍历容器,以进行读取和/或写入。也可以使用双向迭代器或随机访问迭代器。

  • 双向迭代器(BidirectionalIterators):可以在两个方向上遍历容器,以进行读取和写入。也可以使用随机访问迭代器。

  • 随机访问迭代器(RandomAccessIterators):提供对容器元素的随机访问。算法如 sort 需要随机访问迭代器,因此不能用于排序仅提供双向迭代器(BidirectionalIterators)的列表或映射(maps)。

  • 连续迭代器(ContiguousIterators):类似于随机访问迭代器,但还保证这些迭代器指向的元素在内存中是连续存储的。像 std::vector 这样的容器提供连续迭代器。

通过使用随机访问迭代器的示例,可以看到如何将迭代器与泛型算法相关联:查找算法所需的迭代器类型,然后查看数据结构是否支持所需类型的迭代器。如果不支持,则该算法不能与特定数据结构一起使用。

std::distancestd::size

在这里插入图片描述

在之前的第 18.2 节中提到,迭代器支持用于存储其元素连续在内存中的容器的指针算术运算。但这并不完全准确:要确定两个迭代器所指元素之间的元素数量,迭代器必须支持减法操作。

对于像 std::liststd::unordered_map 这样的容器,由于它们的元素不是连续存储的,因此不能使用指针算术来计算两个迭代器之间的元素数量。

函数 std::distance 填补了这一空白:std::distance 接受两个 InputIterator 并返回它们之间的元素数量。在使用 distance 之前,必须包含 <iterator> 头文件。

如果作为第一个参数的迭代器超出了作为第二个参数的迭代器,则元素数量为非正值;否则,它是非负值。如果元素数量无法确定(例如,迭代器不引用同一个容器中的元素),则 distance 的返回值是未定义的。

示例:

#include <iostream>
#include <unordered_map>
using namespace std;

int main() {
    unordered_map<int, int> myMap = {{1, 2}, {3, 5}, {-8, 12}};
    cout << distance(++myMap.begin(), myMap.end()) << '\n'; // 输出:2
}

std::size

迭代器头文件还定义了 std::size 函数,用于返回容器中的元素数量(由容器的 size 成员返回)或编译器在调用 std::size 时已知的数组的维度。例如,如果编译器知道数组 data 的大小,则可以使用以下语句调用一个处理程序函数(期望数组的第一个元素的地址和数组边界之外的位置的地址):

handler(data, data + std::size(data));

如前所述,std::size 函数在迭代器头文件中定义。然而,当包含支持迭代器的容器的头文件时(包括 string 头文件),它也保证可用。

插入迭代器

通用算法通常需要一个目标容器,用于存储算法的结果。例如,copy 通用算法有三个参数。前两个参数定义了要访问的元素范围,第三个参数定义了结果应存储的位置。

使用 copy 算法时,通常可以预先知道要复制的元素数量,因为该数量通常可以通过指针算术得到。然而,在一些情况下,指针算术无法使用。类似地,结果元素的数量有时与初始范围中的元素数量不同。unique_copy 通用算法就是一个例子。在这种情况下,通常无法提前知道复制到目标容器中的元素数量。

在这些情况下,插入适配器函数可以用来在目标容器中创建元素。插入适配器有三种类型:

  • back_inserter: 调用容器的 push_back 成员函数,将新元素添加到容器的末尾。例如,要将 source 中的所有元素反向复制到 destination 的末尾,可以使用 copy 通用算法:

    copy(source.rbegin(), source.rend(), back_inserter(destination));
    
  • front_inserter: 调用容器的 push_front 成员函数,将新元素添加到容器的开头。例如,要将 source 中的所有元素复制到 destination 的开头(从而也会反转元素的顺序):

    copy(source.begin(), source.end(), front_inserter(destination));
    
  • inserter: 调用容器的 insert 成员函数,从指定的起始位置开始添加新元素。例如,要将 source 中的所有元素复制到 destination 容器中,从 destination 的开头开始,将现有元素向后移动到新插入的元素之外:

    copy(source.begin(), source.end(), inserter(destination, destination.begin()));
    

插入适配器需要容器具备两种类型:

  • 使用 value_type = Data,其中 Data 是存储在提供 push_backpush_frontinsert 成员函数的类中的数据类型(例如:using value_type = std::string)。
  • 使用 const_reference = const &value_type

back_inserter 为例,该迭代器期望一个支持 push_back 成员的容器。插入器的 operator() 成员调用容器的 push_back 成员函数。任何支持 push_back 成员的类都可以作为 back_inserter 的参数,只要该类在其接口中添加了:

using const_reference = DataType const &;

(其中 DataType const & 是类的 push_back 成员函数的参数类型)。

示例:

#include <iostream>
#include <algorithm>
#include <iterator>
using namespace std;

class Insertable {
public:
    using value_type = int;
    using const_reference = int const &;
    void push_back(int const &) {}
};

int main() {
    int arr[] = {1};
    Insertable insertable;
    copy(arr, arr + 1, back_inserter(insertable));
}

istream 对象的迭代器

istream_iterator<Type> 可用于为 istream 对象定义一组迭代器。istream_iterator 迭代器的一般形式是:

istream_iterator<Type> identifier(istream &in)

这里,Type 是从 istream 流中读取的数据元素的类型。它作为迭代器范围中的“开始”迭代器使用。Type 可以是任何与 istream 对象配合使用定义了 operator>> 的类型。

默认构造函数用于定义结束迭代器,对应于流的结束。例如:

istream_iterator<string> endOfStream;

默认构造函数定义的结束迭代器不提及在定义开始迭代器时指定的流对象。

通过使用 back_inserteristream_iterator 适配器,可以很容易地将流中的所有字符串存储到一个容器中。示例(使用匿名 istream_iterator 适配器):

#include <iostream>
#include <iterator>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    vector<string> vs;
    copy(istream_iterator<string>(cin), istream_iterator<string>(),
         back_inserter(vs));
    for (vector<string>::const_iterator begin = vs.begin(), end = vs.end();
         begin != end; ++begin)
        cout << *begin << ' ';
    cout << '\n';
}

在这个示例中,istream_iterator<string>(cin) 用于从标准输入流 cin 读取字符串,并将它们存储到 vs 向量中。istream_iterator<string>() 作为结束迭代器,表示流的结束。使用 back_inserter(vs) 将读取的字符串插入到 vs 向量的末尾。

istreambuf 对象的迭代器

输入迭代器也可用于 streambuf 对象。

要从支持输入操作的 streambuf 对象中读取数据,可以使用 istreambuf_iterator。这些迭代器支持与 istream_iterator 相同的操作。与后者不同的是,istreambuf_iterator 支持以下三种构造函数:

  • istreambuf_iterator<Type>
    使用默认构造函数创建的结束迭代器,表示从 streambuf 中提取类型 Type 的值时的流结束条件。

  • istreambuf_iterator<Type>(streambuf *)
    在定义 istreambuf_iterator 时,可以使用 streambuf 的指针。它表示迭代器范围的开始迭代器。

  • istreambuf_iterator<Type>(istream)
    在定义 istreambuf_iterator 时,也可以使用 istream。它访问 istreamstreambuf,同时表示迭代器范围的开始迭代器。

在第18.2.4.1节中提供了一个示例,展示了如何使用 istreambuf_iteratorostreambuf_iterator

ostream 对象的迭代器

ostream_iterator<Type> 适配器可以用来将 ostream 传递给期望 OutputIterator 的算法。定义 ostream_iterator 时可以使用两个构造函数:

  • ostream_iterator<Type> identifier(ostream &outStream);
    该构造函数创建的 ostream_iterator 不使用分隔符。

  • ostream_iterator<Type> identifier(ostream &outStream, char const *delim);
    该构造函数创建的 ostream_iterator 使用指定的分隔符字符串来分隔各个 Type 数据元素。

其中,Type 是要插入到 ostream 中的数据元素的类型。Type 可以是任何与 ostream 对象一起定义了 operator<< 的类型。

下面的示例展示了如何使用 istream_iteratorostream_iterator 将一个文件的信息复制到另一个文件。需要注意的是,你可能希望使用 in.unsetf(ios::skipws),它用于清除 ios::skipws 标志。这样一来,空白字符将被正常返回,文件将按字符逐个复制。

示例代码如下:

#include <iostream>
#include <algorithm>
#include <iterator>
using namespace std;

int main() {
    cin.unsetf(ios::skipws);
    copy(istream_iterator<char>(cin), istream_iterator<char>(),
         ostream_iterator<char>(cout));
}

‘ostreambuf’ 迭代器对象

输出迭代器也适用于 streambuf 对象。

要向支持输出操作的 streambuf 对象写入数据,可以使用 ostreambuf_iterator。这种迭代器支持与 ostream_iterator 相同的操作。ostreambuf_iterator 支持两种构造函数:

  • ostreambuf_iterator<Type>(streambuf *):可以使用 streambuf 的指针来定义一个 ostreambuf_iterator。它可以用作一个输出迭代器。
  • ostreambuf_iterator<Type>(ostream):也可以使用 ostream 来定义一个 ostreambuf_iterator。它访问 ostreamstreambuf 并且也可以用作输出迭代器。

下面的示例演示了如何同时使用 istreambuf_iteratorostreambuf_iterator 来以另一种方式复制流。由于直接访问了流的 streambuf,流和流标志被绕过。因此,像之前的部分那样清除 ios::skipws 是不必要的,而这个程序的效率可能也超过了前面示例中的程序。

#include <iostream>
#include <algorithm>
#include <iterator>

using namespace std;

int main() {
    // 使用 istreambuf_iterator 从标准输入流读取字符
    istreambuf_iterator<char> in(cin.rdbuf());
    
    // 使用 ostreambuf_iterator 向标准输出流写入字符
    ostreambuf_iterator<char> out(cout.rdbuf());
    
    // 复制字符从输入流到输出流
    copy(in, istreambuf_iterator<char>(), out);

    return 0;
}

代码说明

  1. #include <iostream>: 引入输入输出流库,用于 cincout

  2. #include <algorithm>: 引入标准算法库,以便使用 copy 算法。

  3. #include <iterator>: 引入迭代器库,用于 istreambuf_iteratorostreambuf_iterator

  4. istreambuf_iterator<char> in(cin.rdbuf());: 使用 istreambuf_iterator 从标准输入流的 streambuf 读取字符。

  5. ostreambuf_iterator<char> out(cout.rdbuf());: 使用 ostreambuf_iterator 向标准输出流的 streambuf 写入字符。

  6. copy(in, istreambuf_iterator<char>(), out);: 使用 copy 算法将字符从输入流复制到输出流。

这段代码高效地将标准输入的所有字符直接复制到标准输出,而不受流的标志影响。

移动元素到另一个容器

有时我们需要将元素从一个容器移动到另一个容器。除了按顺序提取元素并将它们移动到目标容器中(例如,从流中提取单词),或者显式地调用 std::move 来移动源容器的元素外,还可以使用 make_move_iterator。该函数的参数是一个指向可移动元素的迭代器,通过调用两个 make_move_iterators 来指定要从一个容器移动到另一个容器的元素范围。其中一个接收源容器的开始迭代器,另一个接收源容器的结束迭代器。下面的示例演示了如何将单词移动到 std::vector<std::string> 中:

#include <algorithm>
#include <iostream>
#include <iterator>
#include <string>
#include <vector>

using namespace std;

// 查找第一个非空字符串的索引
size_t fstNonEmpty(vector<string> const &vs) {
    return find_if(vs.begin(), vs.end(),
                   [&](string const &str) { return str != ""; }) -
           vs.begin();
}

int main() {
    vector<string> vs;
    // 从标准输入流中读取单词并移动到 vs 中
    copy(istream_iterator<string>(cin), istream_iterator<string>(),
         back_inserter(vs));
    
    cout << "vs contains " << vs.size()
         << " words\n"
            "first non-empty word at index "
         << fstNonEmpty(vs)
         << "\n"
            "moving the first half into vector v2\n";
    
    // 将 vs 的前半部分移动到 v2
    vector<string> v2{make_move_iterator(vs.begin()),
                      make_move_iterator(vs.begin() + vs.size() / 2)};
    
    cout << "vs contains " << vs.size()
         << " words\n"
            "first non-empty word at index "
         << fstNonEmpty(vs)
         << "\n"
            "v2 contains "
         << v2.size() << " words\n";
}

注意,源容器中的元素只是被移动到了目标容器中:移动后源容器中的元素变为空。在移动其他类型的对象时,结果可能会有所不同,但在所有情况下,源对象应该保持有效状态。

unique_ptr

在使用 unique_ptr 类之前,必须包含 <memory> 头文件。

当使用指针访问动态分配的内存时,需要严格的记录来防止内存泄漏。当一个指针变量指向的动态分配内存超出作用域时,该动态分配的内存变得不可访问,程序会遭遇内存泄漏。因此,程序员必须确保在指针变量超出作用域之前,将动态分配的内存释放回公共池。

当一个指针变量指向一个动态分配的单一值或对象时,如果将指针变量定义为 std::unique_ptr 对象,则记录要求会大大简化。unique_ptr 是伪装成指针的对象。由于它们是对象,因此它们的析构函数在它们超出作用域时会被调用。析构函数会自动删除它们所指向的动态分配内存。unique_ptr(以及它的同类 shared_ptr)也被称为智能指针。

unique_ptr 具有几个特殊特性:

  • 当将 unique_ptr 赋值给另一个时,会使用移动语义。如果移动语义不可用,则编译会失败。另一方面,如果编译成功,则所使用的容器或通用算法支持 unique_ptr。例如:

    std::unique_ptr<int> up1(new int);
    std::unique_ptr<int> up2(up1);  // 编译错误
    

    第二个定义无法编译,因为 unique_ptr 的拷贝构造函数是私有的(赋值运算符也是如此)。但是,unique_ptr 类确实提供了从右值引用初始化和赋值的功能:

    class unique_ptr
    // 部分接口示例
    {
    public:
        unique_ptr(unique_ptr &&tmp);  // 右值绑定
    private:
        unique_ptr(const unique_ptr &other);  // 拷贝构造函数私有
    };
    

    在下一个示例中,使用了移动语义,因此它会正确编译:

    unique_ptr<int> cp(unique_ptr<int>(new int));
    
  • unique_ptr 对象只能指向动态分配的内存,因为只有动态分配的内存可以被删除。

  • 不允许多个 unique_ptr 对象指向同一块动态分配的内存。unique_ptr 的接口设计用来防止这种情况的发生。一旦 unique_ptr 对象超出作用域,它会删除它所指向的内存,立即将任何其他也指向该分配内存的对象变成悬挂指针。

  • 当一个 Derived 类从 Base 类派生时,分配的新 Derived 类对象可以分配给 unique_ptr<Base>,而无需为 Base 定义虚析构函数。unique_ptr 对象返回的 Base* 指针可以简单地静态转换为 Derived,并且 Derived 的析构函数会自动调用,只要 unique_ptr 的定义提供了删除器函数地址。示例如下:

    class Base
    { ... };
    
    class Derived: public Base {
    public:
        // 假设 Derived 有一个成员函数 void process()
        static void deleter(Base *bp);
    };
    
    void Derived::deleter(Base *bp)
    {
        delete static_cast<Derived *>(bp);
    }
    
    int main()
    {
        unique_ptr<Base, void (*)(Base *)> bp(new Derived, &Derived::deleter);
        static_cast<Derived *>(bp.get())->process();  // OK!
        // 此处会调用 ~Derived:不需要多态
    }
    

unique_ptr 类提供了几个成员函数来访问指针本身或让 unique_ptr 指向另一块内存。接下来的几节将介绍这些成员函数(和 unique_ptr 构造函数)。

unique_ptr 还可以与容器和(通用)算法一起使用。它们可以正确析构任何类型的对象,因为它们的构造函数接受自定义的删除器。此外,数组也可以由 unique_ptr 处理。

定义 unique_ptr 对象

定义 unique_ptr 对象有三种方式。每种定义都包含在尖括号中的常规 <type> 说明符:

  • 默认构造函数 只是创建一个不指向特定内存块的 unique_ptr 对象。其指针被初始化为 0(零):

    unique_ptr<type> identifier;
    

    这种形式将在 18.3.2 节中讨论。

  • 移动构造函数 用于初始化一个 unique_ptr 对象。在使用移动构造函数后,其 unique_ptr 参数将不再指向动态分配的内存,其指针数据成员将变为零指针:

    unique_ptr<type> identifier(another unique_ptr for type);
    

    这种形式将在 18.3.3 节中讨论。

  • 最常用的形式是将一个动态分配的内存块传递给 unique_ptr 对象的构造函数,从而初始化 unique_ptr 对象。可选地,还可以提供一个删除器(deleter)。一个接收 unique_ptr 指针作为其参数的(自由)函数或函数对象可以作为删除器传递。该删除器应将动态分配的内存返回到公共池(如果指针为零则不执行任何操作)。

    unique_ptr<type> identifier (new-expression [, deleter]);
    

    这种形式将在 18.3.4 节中讨论。

创建一个普通的 unique_ptr

unique_ptr 的默认构造函数定义了一个不指向特定内存块的 unique_ptr 对象:

unique_ptr<type> identifier;

unique_ptr 对象控制的指针被初始化为 0(零)。虽然 unique_ptr 对象本身不是指针,但它的值可以与 0 进行比较。例如:

unique_ptr<int> ip;
if (!ip)
    cout << "unique_ptr 对象当前为零指针\n";

或者,可以使用成员函数 get(参见 18.3.5 节)。

移动另一个 unique_ptr

一个 unique_ptr 对象可以使用对同类型 unique_ptr 对象的右值引用来初始化:

unique_ptr<type> identifier(other unique_ptr object);

在下面的例子中使用了移动构造函数:

void mover(unique_ptr<string> &&param) {
    unique_ptr<string> tmp(move(param));
}

类似地,赋值运算符也可以使用。一个 unique_ptr 对象可以赋值给一个相同类型的临时 unique_ptr 对象(同样使用了移动语义)。例如:

#include <iostream>
#include <memory>
#include <string>
using namespace std;

int main() {
    unique_ptr<string> hello1{new string{"Hello world"}};
    unique_ptr<string> hello2{move(hello1)};
    unique_ptr<string> hello3;
    hello3 = move(hello2);
    cout << *hello3 << '\n';
}
// 输出: Hello world

这个例子说明了以下几点:

  • hello1 被初始化为指向动态分配的字符串的指针(见下一节)。
  • unique_ptr 对象 hello2 使用移动构造函数获取了由 hello1 控制的指针。这使得 hello1 变成了一个空指针(零指针)。
  • 然后 hello3 被定义为一个默认的 unique_ptr<string>,随后通过移动赋值从 hello2 中获取其值(结果是 hello2 也变成了空指针)。

如果将 hello1hello2 插入到 cout 中,程序会导致段错误。原因是对空指针解引用。最终,只有 hello3 实际上指向最初分配的字符串。

指向新分配的对象

unique_ptr 通常通过指向动态分配内存的指针进行初始化。其通用形式如下:

unique_ptr<type [, deleter_type]> identifier(new-expression [, deleter = deleter_type{}]);

第二个模板参数(deleter_type)是可选的,它可以引用一个自由函数、一个处理分配内存销毁的函数对象,或者一个 lambda 函数。例如,在需要遍历并销毁分配的双重指针内存时,可以使用 deleter(下面会有一个示例)。

以下是一个初始化 unique_ptr 指向 string 对象的示例:

unique_ptr<string> strPtr{ new string{ "Hello world" } };

传递给构造函数的参数是 operator new 返回的指针。注意,type 中不包含指针符号。unique_ptr 构造中使用的类型与 new 表达式中使用的类型相同。

下面的示例展示了如何使用显式定义的删除器来删除动态分配的字符串指针数组:

#include <string>
#include <memory>
using namespace std;

struct Deleter {
    size_t d_size;
    Deleter(size_t size = 0)
    : d_size(size) {}
    
    void operator()(string **ptr) const {
        for (size_t idx = 0; idx < d_size; ++idx)
            delete ptr[idx];
        delete[] ptr;
    }
};

int main() {
    unique_ptr<string *, Deleter> sp2{new string *[10](), Deleter{10}};
    // 示例:检索 Deleter
    [[maybe_unused]] Deleter &obj = sp2.get_deleter();
}

unique_ptr 可以用于访问通过 new 表达式分配的对象的成员函数。这些成员函数可以像 unique_ptr 是指向动态分配对象的普通指针一样被访问。例如,在下面的程序中,“Hello” 之后插入了文本 “C++”:

#include <iostream>
#include <memory>
#include <cstring>
using namespace std;

int main() {
    unique_ptr<string> sp{ new string{ "Hello world" } };
    cout << *sp << '\n';
    sp->insert(strlen("Hello "), "C++ ");
    cout << *sp << '\n';
}

/*
输出:
Hello world
Hello C++ world
*/

运算符和成员函数

unique_ptr 类提供以下运算符:

  • unique_ptr<Type> &operator=(unique_ptr<Type> &&tmp)
    此运算符通过移动语义将右值 unique_ptr 对象指向的内存转移给左值 unique_ptr 对象。右值对象失去对内存的控制,并变为空指针(0-pointer)。可以通过先使用 std::move 将现有的 unique_ptr 转换为右值引用,然后将其赋值给另一个 unique_ptr。例如:

    unique_ptr<int> ip1(new int);
    unique_ptr<int> ip2;
    ip2 = std::move(ip1);
    
  • operator bool() const
    如果 unique_ptr 不指向内存(即其 get 成员函数返回 0),则此运算符返回 false。否则,返回 true

  • Type &operator*()
    此运算符返回对通过 unique_ptr 对象可访问的信息的引用。它的行为类似于普通的指针解引用运算符。

  • Type *operator->()
    此运算符返回一个指针,用于访问 unique_ptr 对象可以访问的信息。它允许通过 unique_ptr 对象选择对象的成员。例如:

    unique_ptr<string> sp{ new string{ "hello" } };
    cout << sp->c_str();
    

unique_ptr 类支持以下成员函数:

  • Type *get()
    返回由 unique_ptr 对象控制的信息的指针。其行为类似于 operator->。返回的指针可以被检查,如果该指针为 0,则表示 unique_ptr 对象不指向任何内存。

  • Deleter &unique_ptr<Type>::get_deleter()
    返回 unique_ptr 使用的删除器对象的引用。

  • Type *release()
    返回由 unique_ptr 对象控制的信息的指针,同时将该对象自身变为空指针(即其指针数据成员变为 0)。此成员函数可用于将 unique_ptr 对象控制的信息转移给普通的 Type 指针。调用此成员后,动态分配内存的正确销毁责任由程序员承担。

  • void reset(Type *)
    将由 unique_ptr 对象控制的动态分配内存返回给公共内存池;此后,该对象将控制传递给函数的参数指向的内存。此函数也可以不带参数调用,从而将对象变为空指针。此成员函数可用于将新的动态分配内存块赋给 unique_ptr 对象。

  • void swap(unique_ptr<Type> &)
    交换两个类型相同的 unique_ptr 对象。

使用 unique_ptr 对象管理数组

unique_ptr 用于存储数组时,解引用运算符(*)意义不大,但对于数组,unique_ptr 对象可以使用索引运算符来访问元素。通过模板特化,可以区分用于单个对象的 unique_ptr 和用于管理动态分配数组的 unique_ptr

对于动态分配的数组,可以使用以下语法:

  • 使用索引([])表示智能指针控制一个动态分配的数组。例如:

    unique_ptr<int[]> intArr(new int[3]);
    
  • 可以使用索引运算符访问数组的元素。例如:

    intArr[2] = intArr[0];
    

在这种情况下,智能指针的析构函数将调用 delete[] 而不是 delete 来释放内存。

shared_ptr

除了 unique_ptr 类,标准库中还提供了 std::shared_ptr<Type> 类,这是一个引用计数智能指针。

在使用 shared_ptr 之前,必须包含 <memory> 头文件。

shared_ptr 会在其引用计数降为零时自动销毁其内容。与 unique_ptr 类似,当定义一个 shared_ptr<Base> 用于存储一个新分配的 Derived 类对象时,返回的 Base* 可以使用 static_cast 转换为 Derived*:这不需要多态支持。当 shared_ptr 被重置或超出作用域时,不会发生对象切片,Derived 的析构函数(或者如果配置了:删除器)会被调用。

shared_ptr 支持复制构造函数、移动构造函数,以及标准和移动重载的赋值运算符。

unique_ptr 一样,shared_ptr 也可以指向动态分配的数组。

定义 shared_ptr 对象

定义 shared_ptr 对象有四种方式。每种定义都在尖括号中包含常规的 <type> 类型说明符:

  • 默认构造函数 仅创建一个 shared_ptr 对象,该对象不指向特定的内存块。其指针初始化为 0(零):

    shared_ptr<type> identifier;
    

    这种形式在 18.4.2 节中讨论。

  • 复制构造函数 初始化一个 shared_ptr,使两个对象共享原有对象所指向的内存。复制构造函数还会增加 shared_ptr 的引用计数。例如:

    shared_ptr<string> org{ new string{ "hi there" } };
    shared_ptr<string> copy(org); // 引用计数现在为 2
    
  • 移动构造函数 用临时 shared_ptr 对象的指针和引用计数初始化一个 shared_ptr。临时 shared_ptr 被转变为 0 指针。现有的 shared_ptr 可以将其数据移动到新定义的 shared_ptr 中(同时将现有的 shared_ptr 转变为 0 指针)。在下一个例子中,构造了一个临时的匿名 shared_ptr 对象,然后用于构造 grabber。由于 grabber 的构造函数接收到一个匿名的临时对象,编译器使用了 shared_ptr 的移动构造函数:

    shared_ptr<string> grabber{ shared_ptr<string>{ new string{ "hi there" } } };
    
  • 最常用的形式 是将一个 shared_ptr 对象初始化为传递给对象构造函数的动态分配内存块。可以选择性地提供删除器。一个接受 shared_ptr 指针作为其参数的(自由)函数(或函数对象)可以作为删除器传递。该函数应将动态分配的内存返回到公共池(如果指针为零则不执行任何操作)。

    shared_ptr<type> identifier(new-expression [, deleter]);
    

    这种形式在 18.4.3 节中讨论。

创建一个普通的 shared_ptr

shared_ptr 的默认构造函数定义了一个不指向特定内存块的 shared_ptr 对象:

shared_ptr<type> identifier;

shared_ptr 对象控制的指针初始化为 0(零)。虽然 shared_ptr 对象本身不是指针,但它的值可以与 0 进行比较。例如:

shared_ptr<int> ip;
if (!ip)
    cout << "0-pointer with a shared_ptr object\n";

或者,可以使用 get 成员函数(参见 18.4.4 节)。

指向新分配的对象

shared_ptr 最常通过动态分配的内存块进行初始化。通用形式为:

shared_ptr<type> identifier(new-expression [, deleter]);

第二个参数(deleter)是可选的,指向处理分配内存销毁的函数对象或自由函数。deleter 在某些情况下很有用,例如,当分配了双重指针,并且需要访问每个嵌套的指针以销毁分配的内存时(参见下面的示例)。deleter 的使用方式与 unique_ptr 类似(参见 18.3.4 节)。

以下是一个初始化 shared_ptr 指向字符串对象的示例:

shared_ptr<string> strPtr{ new string{ "Hello world" } };

传递给构造函数的参数是由 operator new 返回的指针。请注意,type 中不包含指针。用于 shared_ptr 构造的类型与 new 表达式中使用的类型相同。

下一个示例说明了两个 shared_ptr 确实共享它们的信息。在修改一个对象控制的信息后,另一个对象控制的信息也会被修改:

#include <iostream>
#include <memory>
#include <cstring>
using namespace std;

int main()
{
    shared_ptr<string> sp(new string{ "Hello world" });
    shared_ptr<string> sp2(sp);
    sp->insert(strlen("Hello "), "C++ ");
    cout << *sp << '\n' <<
            *sp2 << '\n';
}

输出结果为:

Hello C++ world
Hello C++ world

运算符和成员函数

shared_ptr 类提供了以下运算符:

  • shared_ptr &operator=(shared_ptr<Type> const &other);
    复制赋值:操作符左侧操作数的引用计数减少。如果引用计数降至零,则删除操作符左侧操作数控制的动态分配内存。然后,它与操作符右侧操作数共享信息,增加信息的引用计数。

  • shared_ptr &operator=(shared_ptr<Type> &&tmp);
    移动赋值:操作符左侧操作数的引用计数减少。如果引用计数降至零,则删除操作符左侧操作数控制的动态分配内存。然后,它获取操作符右侧操作数控制的信息,并将其变为 0 指针。

  • operator bool() const;
    如果 shared_ptr 实际上指向内存,则返回 true,否则返回 false

  • Type &operator*();
    返回对存储在 shared_ptr 对象中的信息的引用。它像普通指针一样工作。

  • Type *operator->();
    返回指向 shared_ptr 对象控制的信息的指针。例如:

    shared_ptr<string> sp{ new string{ "hello" } };
    cout << sp->c_str() << '\n';
    

以下成员函数被支持:

  • Type *get();
    返回指向 shared_ptr 对象控制的信息的指针。它像 operator-> 一样工作。返回的指针可以被检查。如果它为零,则 shared_ptr 对象不指向任何内存。

  • Deleter &get_deleter();
    返回 shared_ptrdeleter(函数或函数对象)的引用。

  • void reset(Type *);
    减少 shared_ptr 对象控制的信息的引用计数,如果引用计数降至零,则删除它指向的内存。随后,对象的信息将指向传递给函数的参数,将共享计数设置为 1。它也可以在没有参数的情况下调用,将对象变为 0 指针。此成员函数可用于将新分配的内存块分配给 shared_ptr 对象。

  • void reset(Type *, DeleterType &&);
    该成员的变体接受特定的 Deleter 类型:如果 Type 是基类并且使用了派生类对象,则这些派生类对象在销毁时可能需要特定的操作。使用前一个成员时,最终新分配对象的析构函数将被调用而不使用显式的 deleter 函数。当前成员确保在共享计数降至零时使用提供的 deleter。

  • void shared_ptr<Type>::swap(shared_ptr<Type> &&);
    交换两个相同类型的 shared_ptr 对象。

  • bool unique() const;
    如果当前对象是唯一指向对象控制的内存的对象,则返回 true,否则(包括对象是 0 指针的情况)返回 false

  • size_t use_count() const;
    返回共享内存的对象数量。

共享指针的类型转换

在使用标准 C++ 风格的类型转换与 shared_ptr 对象时需要小心。考虑以下两个类:

struct Base {};
struct Derived : public Base {};

unique_ptr 类似,当定义一个 shared_ptr<Base> 来存储新分配的 Derived 类对象时,可以使用 static_cast 将返回的 Base* 转换为 Derived*:不需要多态性,并且在重置 shared_ptrshared_ptr 超出作用域时,不会发生切片,Derived 的析构函数会被调用(参见第 18.3 节)。

当然,也可以轻松定义一个 shared_ptr<Derived>。由于 Derived 对象也是 Base 对象,因此 Derived 的指针可以被视为 Base 的指针,而无需使用转换,但可以使用 static_cast 强制将 Derived* 解释为 Base*

Derived d;
static_cast<Base*>(&d);

然而,当使用 shared_ptr<Derived> 对象的 get 成员函数初始化一个 shared_ptr<Base> 时,普通的 static_cast 无法使用。以下代码片段最终会导致尝试两次删除动态分配的 Base 对象:

shared_ptr<Derived> sd{ new Derived };
shared_ptr<Base> sb{ static_cast<Base *>(sd.get()) };

由于 sdsb 指向同一个对象,当 sbsd 超出作用域时,都将调用 ~Base,这将导致程序因双重释放错误而提前终止。

这些错误可以通过使用专门为 shared_ptr 设计的类型转换来避免。这些转换使用专门的构造函数,创建一个指向内存的 shared_ptr,但与现有的 shared_ptr 共享所有权(即引用计数)。这些特殊的类型转换包括:

  • std::static_pointer_cast<Base>(std::shared_ptr<Derived> ptr);
    返回指向 Base 类对象的 shared_ptr。返回的 shared_ptr 引用 shared_ptr<Derived> ptr 所指向的 Derived 类的基类部分。例如:

    shared_ptr<Derived> dp{ new Derived };
    shared_ptr<Base> bp = std::static_pointer_cast<Base>(dp);
    
  • std::const_pointer_cast<Class>(std::shared_ptr<Class const> ptr);
    返回指向 Class 类对象的 shared_ptr。返回的 shared_ptr 引用一个非 constClass 对象,而 ptr 参数引用一个 constClass 对象。例如:

    shared_ptr<Derived const> cp{ new Derived };
    shared_ptr<Derived> ncp = std::const_pointer_cast<Derived>(cp);
    
  • std::dynamic_pointer_cast<Derived>(std::shared_ptr<Base> ptr);
    返回指向 Derived 类对象的 shared_ptrBase 类必须至少有一个虚成员函数,并且 Derived 类可能重写了 Base 的虚成员。返回的 shared_ptr 引用 Derived 类对象,如果从 Base*Derived* 的动态转换成功。如果动态转换不成功,shared_ptrget 成员返回 0。例如(假设 DerivedDerived2Base 继承):

    shared_ptr<Base> bp(new Derived());
    cout << std::dynamic_pointer_cast<Derived>(bp).get() << ' ' <<
            std::dynamic_pointer_cast<Derived2>(bp).get() << '\n';
    

    第一个 get 返回一个非 0 的指针值,第二个 get 返回 0。

使用 shared_ptr 对象处理数组

shared_ptr 类也可以用来处理动态分配的对象数组。要在数组上使用它,只需在指定 shared_ptr 的类型时使用方括号。以下是一个示例:

shared_ptr 本身初始化为指向指针数组时,必须使用删除器来删除指针所指向的内存。在这种情况下,删除器负责将内存归还给通用内存池,并在最终删除指针数组时使用 delete[]

#include <memory>
#include <iostream>

using namespace std;

// 删除器类,用于释放指针数组及其指向的内存
struct Deleter {
    size_t size;
    Deleter(size_t s = 0) : size(s) {}
    void operator()(int **ptr) const {
        for (size_t i = 0; i < size; ++i) {
            delete ptr[i];
        }
        delete[] ptr;
    }
};

int main() {
    // 创建一个包含10个指针的动态数组
    shared_ptr<int*[]> sp(new int*[10](), Deleter{10});
    for (int i = 0; i < 10; ++i) {
        sp[i] = new int(i);
    }

    // 使用 shared_ptr 访问数组元素
    for (int i = 0; i < 10; ++i) {
        cout << *sp[i] << ' ';
    }
    cout << endl;

    // shared_ptr 的析构函数将调用 Deleter 来释放内存
}

在这个示例中,shared_ptr<int*[]> 用于处理动态分配的指针数组。Deleter 类用于删除指针数组及其指向的内存。Deleteroperator() 函数负责遍历指针数组并删除每个指针指向的内存,然后删除整个指针数组。这样,当 shared_ptr 对象超出作用域时,内存将被正确释放。

智能指针的智能构造:make_sharedmake_unique

通常情况下,shared_ptr 在定义时会被初始化为指向一个新分配的对象。如下示例所示:

std::shared_ptr<string> sptr{ new std::string{ "hello world" } }

在这样的语句中,会进行两次内存分配调用:一次用于分配 std::string,另一次用于 std::shared_ptr 构造函数自身内部使用的分配。

通过使用 make_shared 模板,可以将这两次分配合并为一次单独的分配(这比显式调用 shared_ptr 的构造函数稍微更高效)。函数模板 std::make_shared 的原型如下:

template<typename Type, typename ...Args>
std::shared_ptr<Type> std::make_shared(Args ...args);

在使用 make_shared 之前,必须包含 <memory> 头文件。

此函数模板会分配一个 Type 类型的对象,将 args 传递给其构造函数(使用完美转发,参见第22.5.2节),并返回一个用新分配的 Type 对象的地址初始化的 shared_ptr

以下示例展示了如何使用 std::make_shared 初始化上述 sptr 对象。请注意使用 auto,这使我们无需显式指定 sptr 的类型:

auto sptr(std::make_shared<std::string>("hello world"));

在这种初始化之后,std::shared_ptr<std::string> sptr 已经定义并初始化。可以如下使用:

std::cout << *sptr << '\n';

除了 make_shared,还可以使用 std::make_unique 函数。它的用法类似于 make_shared,但返回的是 std::unique_ptr 而不是 shared_ptr

含有指针数据成员的类

含有指针数据成员的类需要特别注意,尤其是在构造时,必须小心防止野指针和/或内存泄漏。考虑以下定义了两个指针数据成员的类:

class Filter
{
    istream *d_in;
    ostream *d_out;
public:
    Filter(char const *in, char const *out);
};

假设 Filter 对象从 *d_in 读取信息并将过滤后的信息写入 *d_out。使用指向流的指针允许我们将它们指向任何类型的流,例如 istreamsifstreamsfstreamsistringstreams。构造函数可以像这样实现:

Filter::Filter(char const *in, char const *out)
    : d_in(new ifstream{ in }),
      d_out(new ofstream{ out })
{
    if (!*d_in || !*d_out)
        throw "Input and/or output stream not available"s;
}

当然,构造可能会失败。new 可能会抛出异常;流构造函数可能会抛出异常;或者流无法打开,在这种情况下,构造函数会抛出异常。使用 try 块有助于处理这种情况。注意,如果 d_in 的初始化抛出异常,则无需担心。Filter 对象尚未构造,其析构函数不会被调用,处理会在捕获到的异常点继续。但是,当 d_out 的初始化或构造函数的 if 语句抛出异常时,Filter 的析构函数也不会被调用:没有对象,自然也就没有析构函数被调用。这可能导致内存泄漏,因为 d_in 和/或 d_outdelete 没有被调用。为防止这种情况发生,必须首先将 d_ind_out 初始化为 0,然后才能进行初始化:

Filter::Filter(char const *in, char const *out)
try
    : d_in(0),
      d_out(0)
{
    d_in = new ifstream{ in };
    d_out = new ofstream{ out };
    if (!*d_in || !*d_out)
        throw "Input and/or output stream not available"s;
}
catch (...)
{
    delete d_out;
    delete d_in;
}

然而,这很快就会变得复杂。如果 Filter 还包含另一个类的数据成员,而该类的构造函数需要两个流,则无法构造该数据成员,或必须将其自身转换为指针:

Filter::Filter(char const *in, char const *out)
try
    : d_in(0),
      d_out(0),
      d_filterImp(*d_in, *d_out) // 不可行
{ ... }

替代方法:

Filter::Filter(char const *in, char const *out)
try
    : d_in(0),
      d_out(0),
      d_filterImp(0)
{
    d_in = new ifstream(in);
    d_out = new ofstream(out);
    d_filterImp = new FilterImp(*d_in, *d_out);
    ...
}
catch (...)
{
    delete d_filterImp;
    delete d_out;
    delete d_in;
}

尽管后一种替代方法有效,但它很快变得复杂。在这种情况下,应该使用智能指针来避免复杂性。通过将流指针定义为(智能指针)对象,一旦构造,它们将被正确销毁,即使构造函数的其他代码抛出异常。使用 FilterImp 和两个 unique_ptr 数据成员,Filter 的设置和构造函数变为:

class Filter
{
    std::unique_ptr<std::ifstream> d_in;
    std::unique_ptr<std::ofstream> d_out;
    FilterImp d_filterImp;
    ...
};

Filter::Filter(char const *in, char const *out)
try
    : d_in(new ifstream(in)),
      d_out(new ofstream(out)),
      d_filterImp(*d_in, *d_out)
{
    if (!*d_in || !*d_out)
        throw "Input and/or output stream not available"s;
}

我们回到了最初的实现,但这次不必担心野指针和内存泄漏。如果某个成员初始化器抛出异常,之前构造的数据成员(现在是对象)的析构函数将始终被调用。

经验法则:当类需要定义指针数据成员时,如果它们的构造函数有抛出异常的可能性,这些指针数据成员应定义为智能指针。

比较类

随着飞船操作符(<=>,参见第 11.7.2 节)的引入,标准命名空间中增加了多个比较类别类。

在实现飞船操作符时需要使用比较类,并且为了使用它们(或在声明和实现飞船操作符时),必须包含 <compare> 头文件。

弱类类型不支持替换性。替换性意味着如果两个对象 onetwo 相等(即 one == two 为真),那么 fun(one) == fun(two) 也应为真。这里的 fun 是任何仅使用其参数的公共 const 成员且返回值类型(而非指针类型)的函数,这些值类型也支持比较(也称为比较显著状态)。

比较类的操作符至少需要一个它们自己类类型的参数。另一个参数可以是它们自己类类型的参数,也可以是值为 0(或任何可以视为 0 的值,如 nullptr_t)。

有五种主要用于实现飞船操作符的比较类:

  • weak_equality:用于只支持 ==!= 操作符的类,但不支持替换性;
  • strong_equality:用于支持 ==!= 操作符以及替换性的类;
  • partial_ordering:用于支持所有比较操作符、不支持替换性、且在比较不可比参数时所有比较操作符返回 false 的类;
  • weak_ordering:用于支持所有比较操作符且不支持替换性的类;
  • strong_ordering:用于支持所有比较操作符并且支持替换性的类。

weak_equality

std::weak_equality 用于实现仅支持(不)等式比较但不支持替换性的类的飞船操作符。该类提供了自由函数 operator==operator!=,这些函数期望 weak_equality 类型的参数(其中一个参数可以是 0),并定义了两个静态对象:

  • weak_equality::equivalent,表示相等;
  • weak_equality::nonequivalent,表示不相等。

注意:在当前版本的 Gnu C++ 编译器(10.0.0)中,这个类还没有包含在 <compare> 头文件中。

strong_equality

std::strong_equality 用于实现支持(不)等式比较以及替换性的类的飞船操作符。该类提供了自由函数 operator==operator!=,这些函数期望 strong_equality 类型的参数(其中一个参数可以是 0),并定义了四个静态对象:

  • strong_equality::equal,表示相等;
  • strong_equality::equivalent,表示相等;
  • strong_equality::nonequal,表示不相等;
  • strong_equality::nonequivalent,表示不相等。

注意:在当前版本的 Gnu C++ 编译器(10.0.0)中,这个类还没有包含在 <compare> 头文件中。

partial_ordering

std::partial_ordering 用于实现支持所有比较运算符的类的飞船操作符(其中一个操作数可以为零),这些类不支持替换性,并且使用飞船操作符的对象也可以与任何其他类型的对象进行比较。

partial_ordering 提供了用于所有比较操作的自由函数(==!=<<=>>=),这些函数期望 partial_ordering 类型的参数(其中一个参数可以为 0)。它还定义了四个可以由飞船操作符返回的静态对象:

  • partial_ordering::less:当飞船操作符的左操作数应排在右操作数之前时返回;
  • partial_ordering::equivalent:表示相等:飞船操作符的两个操作数之间没有排序优先级;
  • partial_ordering::greater:当飞船操作符的左操作数应排在右操作数之后时返回;
  • partial_ordering::unordered:当实现飞船操作符的类的所有比较运算符都应该返回 false 时返回(即 ==!=<<=>>= 全部返回 false)。

例如,考虑道路税的情况。卡车、汽车和摩托车需要缴纳道路税,但自行车无需缴纳道路税。为了对道路税进行排序,可以使用 RoadTax 类,并定义以下飞船操作符(假设所有类型的车辆都继承自 Vehicle 类,且 Vehicle 类具有一个(虚拟)成员函数 double roadTax(),该函数返回各种车辆应缴纳的道路税金额;如果不需要缴纳道路税,则金额为负数):

partial_ordering RoadTax::operator<=>(Vehicle const &lhs, Vehicle const &rhs)
{
    return lhs.roadTax() < 0 or rhs.roadTax() < 0 ?
           partial_ordering::unordered :
           lhs.roadTax() < rhs.roadTax() ? partial_ordering::less :
           lhs.roadTax() > rhs.roadTax() ? partial_ordering::greater :
           partial_ordering::equivalent;
}

weak_ordering

std::weak_ordering 用于实现支持所有比较运算符(其中一个操作数可以为零)且不支持替换性的类的飞船操作符。

weak_orderingpartial_ordering 类的不同之处在于,它不能使用 unordered 作为比较结果。与 partial_ordering 类一样,它为所有比较操作(==!=<<=>>=)提供了自由函数,这些函数期望 partial_ordering 类型的参数(其中一个参数可以为 0)。它还定义了三个可以由飞船操作符返回的静态对象:

  • weak_ordering::less:当飞船操作符的左操作数应排在右操作数之前时返回;
  • weak_ordering::equal:表示相等:飞船操作符的两个操作数之间没有排序优先级;
  • weak_ordering::greater:当飞船操作符的左操作数应排在右操作数之后时返回;

上一节中的示例可以很容易地适配为 weak_ordering 比较类:如果车辆的 roadTax 成员对于无需缴纳道路税的车辆返回 0,则可以这样实现 RoadTax 的飞船操作符:

weak_ordering RoadTax::operator<=>(Vehicle const &lhs, Vehicle const &rhs)
{
    return lhs.roadTax() < rhs.roadTax() ? weak_ordering::less :
           lhs.roadTax() > rhs.roadTax() ? weak_ordering::greater :
           weak_ordering::equal;
}

strong_ordering

std::strong_ordering 用于实现支持所有比较运算符(其中一个操作数可以为零)且支持替换性的类的飞船操作符。

strong_ordering 为所有比较操作(==!=<<=>>=)提供了自由函数,这些函数期望 partial_ordering 类型的参数(其中一个参数可以为 0)。它还定义了三个可以由飞船操作符返回的静态对象:

  • strong_ordering::less:当飞船操作符的左操作数应排在右操作数之前时返回;
  • strong_ordering::equal:表示相等:飞船操作符的两个操作数之间没有排序优先级;
  • strong_ordering::greater:当飞船操作符的左操作数应排在右操作数之后时返回;

在 11.7.2 节中,已经提供了一个使用 strong_ordering 类的示例,该节介绍了飞船操作符本身。

正则表达式

C++ 本身提供了处理正则表达式的功能。虽然通过 C 的遗产,C++ 已经可以使用正则表达式(因为 C 一直提供像 regcompregexec 这样的函数),但专门的 C++ 正则表达式功能接口比传统的 C 功能更加丰富,并且可以在使用模板的代码中使用。

在使用 C++ 实现的正则表达式之前,必须包含头文件 <regex>

正则表达式在其他地方有详细的文档记录(例如,regex(7),J.E.F Friedl 的《精通正则表达式》(O’Reilly))。读者可以参考这些资料来复习正则表达式的主题。本质上,正则表达式定义了一种小型的元语言,用来识别文本单元(如“数字”、“标识符”等)。它们在词法扫描器(参见 25.6.1 节)中用于定义与标记相关的输入字符序列时被广泛使用,但在其他情况下也被广泛应用。像 sed(1)grep(1) 这样的程序使用正则表达式来查找具有特定特征的文件中的文本片段,而像 perl(1) 这样的程序则为正则表达式语言添加了一些“糖”,简化了正则表达式的构造。然而,虽然正则表达式非常有用,但众所周知,正则表达式往往很难阅读。有些人甚至称正则表达式语言为“只写不读”语言:在编写正则表达式时,为什么要以某种方式编写它通常是清楚的。但是相反的,如果缺乏适当的上下文,理解一个正则表达式的意图可能极其困难。因此,从一开始,就强调应为每个正则表达式提供适当的注释,说明它应该匹配什么。

在接下来的章节中,首先提供正则表达式语言的简短概述,随后介绍 C++ 目前提供的用于使用正则表达式的功能。这些功能主要包括帮助你指定正则表达式、将它们与文本匹配,以及确定文本的哪些部分(如果有的话)与正在分析的文本的某些部分匹配的类。

正则表达式微语言

正则表达式是由类似于数字表达式的元素组成的表达式。正则表达式由基本元素和运算符组成,具有不同的优先级和关联性。与数字表达式类似,可以使用括号将元素组合在一起以形成一个单元,运算符在这个单元上进行操作。要了解详细内容,读者可以参考例如 ecma-international.org 的第 15.10 节,该部分描述了 C++ 的 regex 类默认使用的正则表达式的特性。

C++ 的正则表达式默认定义以下原子(atoms):

  • x:字符 ‘x’;
  • .:除换行符以外的任何字符;
  • [xyz]:字符类;在此情况下,正则表达式匹配 ‘x’、‘y’ 或 ‘z’ 中的任意一个字符。有关字符类的更多信息,请参见下文;
  • [abj-oZ]:包含字符范围的字符类;此正则表达式匹配字符 ‘a’、‘b’,字母 ‘j’ 到 ‘o’ 之间的任意一个字符,或字符 ‘Z’。有关字符类的更多信息,请参见下文;
  • [^A-Z]:取反的字符类:此正则表达式匹配除类内字符以外的任意字符。在此情况下,匹配除大写字母之外的任意字符。有关字符类的更多信息,请参见下文;
  • [:predef:]:预定义字符集。见下文概述。使用时,它被解释为字符类中的一个元素,因此总是嵌入在定义字符类的方括号中(例如 [[:alnum:]]);
  • \X:如果 X 是 ‘a’、‘b’、‘f’、‘n’、‘r’、‘t’ 或 ‘v’,则使用 ANSI-C 对 ‘\x’ 的解释。否则,为字面量 ‘X’(用于转义运算符,如 *);
  • (r):正则表达式 r。用于覆盖优先级(见下文),也用于将 r 定义为标记的子表达式,其匹配字符可以直接从例如 std::smatch 对象中检索(参见 18.8.3 节);
  • (?:r):正则表达式 r。用于覆盖优先级(见下文),但不视为标记的子表达式;

除了这些基本原子,还可以使用以下特殊原子(也可以在字符类中使用):

  • \s:空白字符;
  • \S:除空白字符以外的任意字符;
  • \d:十进制数字字符;
  • \D:除十进制数字字符以外的任意字符;
  • \w:字母数字字符或下划线 (_) 字符;
  • \W:除字母数字字符或下划线 (_) 字符以外的任意字符;

原子可以连接。如果 rs 是原子,则正则表达式 rs 匹配目标文本,如果目标文本匹配 rs,且顺序一致(在目标文本中没有中间字符)。例如,正则表达式 [ab][cd] 匹配目标文本 ac,但不匹配目标文本 a:c

原子可以通过运算符组合。运算符绑定到前一个原子。如果一个运算符应作用于多个原子,则必须将这些原子用括号括起来(如前述的 (r):如果 r 是一个单元,则在将运算符应用于单词 one 时,使用 (one),而不是仅仅是最后一个 e)。要将运算符字符用作原子,可以将其转义。例如,* 表示一个运算符,\* 表示原子字符星号。注意,字符类不识别转义序列:[\*] 表示一个由两个字符组成的字符类:一个反斜杠和一个星号。

支持以下运算符(rs 表示正则表达式原子):

  • r*:零个或多个 r
  • r+:一个或多个 r
  • r?:零个或一个 r(即一个可选的 r);
  • r{m, n}:其中 1 <= m <= n:匹配至少 m 次,但最多 n 次的 r
  • r{m,}:其中 1 <= m:匹配至少 m 次的 r
  • r{m}:其中 1 <= m:精确匹配 m 次的 r
  • r|s:匹配 rs。此运算符的优先级低于任何乘法运算符;
  • ^r^ 是伪运算符。如果 r 出现在目标文本的开头,此表达式匹配 r。如果 ^ 字符不是正则表达式的第一个字符,则解释为字面量 ^ 字符;
  • r$$ 是伪运算符。如果 r 出现在目标文本的末尾,此表达式匹配 r。如果 $ 字符不是正则表达式的最后一个字符,则解释为字面量 $ 字符;

当一个正则表达式包含标记的子表达式和乘数,并且标记的子表达式被多次匹配时,报告为匹配标记子表达式的目标子字符串是最终匹配的子字符串。例如,使用 regex_search(参见 18.8.4.3 节),标记的子表达式 (((a|b)+\s?)) 和目标文本 a a b,则 a a b 是完全匹配的文本,而 b 被报告为匹配第一个和第二个标记子表达式的子字符串。

字符类

在字符类内部,除了特殊原子 \s\S\d\D\w\W,字符范围运算符 -,字符类结束符 ],以及字符类开始时的 ^ 外,所有正则表达式运算符都会失去其特殊意义。除了与特殊原子结合使用外,转义字符被解释为字面量反斜杠字符(例如,要定义一个包含反斜杠和 d 的字符类,只需使用 [d\])。

要在字符类中添加一个闭合方括号,可以在初始开括号后立即使用 [],或者对于不包含闭合方括号的取反字符类,可以从 [^] 开始。减号字符用于定义字符范围(例如,[a-d],定义 [abcd])(请注意,实际范围可能依赖于所使用的区域设置)。要在字符类中添加一个字面量减号字符,可以将其放在字符类的最开始(例如 [-[^-),或放在字符类的最末尾(例如 -])。

一旦字符类开始,所有随后的字符都会被添加到字符类的字符集,直到达到最终的闭合方括号(])。

除了字符和字符范围之外,字符类还可以包含预定义的字符集。这些预定义的字符集包括:

  • [:alnum:]:所有字母数字字符;
  • [:alpha:]:所有字母字符;
  • [:blank:]:所有空白字符(包括空格和制表符);
  • [:cntrl:]:所有控制字符;
  • [:digit:]:所有数字字符;
  • [:graph:]:所有可打印字符(包括空格);
  • [:lower:]:所有小写字母字符;
  • [:print:]:所有可打印字符(包括空格);
  • [:punct:]:所有标点符号字符;
  • [:space:]:所有空白字符(包括制表符和换行符);
  • [:upper:]:所有大写字母字符;
  • [:xdigit:]:所有十六进制数字字符(包括 a-fA-F)。

这些预定义集指定的字符集等同于标准 C 的 isXXX 函数。例如,[:alnum:] 定义了所有 isalnum(3) 返回 true 的字符。

定义正则表达式:std::regex

在使用本节介绍的 (w)regex 类之前,必须包含 <regex> 头文件。

std::regexstd::wregex 类型定义了正则表达式模式。它们分别定义了 basic_regex<char>basic_regex<wchar_t> 类型。下面的例子中使用了 regex,但也可以使用 wregex

正则表达式的功能在很大程度上是通过模板实现的,例如,使用了 basic_string<char> 类型(等同于 std::string)。类似地,像 OutputIter(输出迭代器)和 BidirConstIter(双向常量迭代器)这样的通用类型也用于多个函数。这些函数是函数模板,函数模板根据调用时提供的参数确定实际类型。

使用正则表达式时通常采取以下步骤:

  1. 定义正则表达式:这涉及到定义或修改 regex 对象。
  2. 提供目标文本:将正则表达式应用于目标文本,这可能会导致目标文本的某些部分与正则表达式匹配。
  3. 检索匹配的部分:从目标文本中检索匹配(或不匹配)的部分以便进一步处理,或者:
  4. 直接修改目标文本:利用现有的正则表达式功能直接修改目标文本,然后将修改后的目标文本用于其他处理。

regex 对象如何处理正则表达式可以通过一组 std::regex_constants 值的按位或组合进行配置,从而定义一个 regex::flag_type 值。这些 regex_constants 包括:

  • std::regex_constants::awk:使用 awk(1)(POSIX)的正则表达式语法来指定正则表达式(例如,正则表达式用 / 字符分隔,如 /\w+/;有关详细信息,请参阅各程序的手册页);
  • std::regex_constants::basic:使用基本 POSIX 正则表达式语法来指定正则表达式;
  • std::regex_constants::collate:字符类中使用的字符范围运算符 - 定义一个与区域设置相关的范围(例如,[a-k]);
  • std::regex_constants::ECMAScriptregex 构造函数默认使用的标志类型。正则表达式使用修改版 ECMAScript 正则表达式语法;
  • std::regex_constants::egrep:使用 egrep(1)(POSIX)的正则表达式语法来指定正则表达式。这与 regex_constants::extended 使用的语法相同,但增加了换行符(\n)作为 | 运算符的替代;
  • std::regex_constants::extended:使用扩展 POSIX 正则表达式语法来指定正则表达式;
  • std::regex_constants::grep:使用 grep(1)(POSIX)的正则表达式语法来指定正则表达式。这与 regex_constants::basic 使用的语法相同,但增加了换行符(\n)作为 | 运算符的替代;
  • std::regex_constants::icase:忽略目标字符串中的字母大小写。例如,正则表达式 A 匹配 aA
  • std::regex_constants::nosubs:在进行匹配时,所有子表达式 ((expr)) 被视为非标记子表达式 (?:expr)
  • std::regex_constants::optimize:优化正则表达式的匹配速度,但会稍微降低正则表达式的构造速度。如果同一个正则表达式对象被频繁使用,则此标志可能显著提高匹配目标文本的速度。

构造函数

std::regex 提供了默认构造函数、移动构造函数和拷贝构造函数。实际上,默认构造函数定义了一个 regex::flag_type 类型的参数,默认值为 regex_constants::ECMAScript

  • regex()
    默认构造函数定义了一个不包含正则表达式的 regex 对象。

  • explicit regex(char const *pattern)
    使用 pattern 中找到的正则表达式定义一个 regex 对象。

  • regex(char const *pattern, std::size_t count)
    使用 pattern 的前 count 个字符中的正则表达式定义一个 regex 对象。

  • explicit regex(std::string const &pattern)
    使用 pattern 中找到的正则表达式定义一个 regex 对象。这个构造函数是一个成员模板,接受 basic_string 类型的参数,该参数也可以使用非标准字符特性和分配器。

  • regex(ForwardIterator first, ForwardIterator last)
    使用 [first, last) 范围中的正则表达式定义一个 regex 对象。这个构造函数是一个成员模板,接受任何前向迭代器类型(例如,普通字符指针),这些迭代器可以用于定义正则表达式的模式。

  • regex(std::initializer_list<Char> init)
    使用初始化列表 init 中的字符定义一个 regex 对象。

以下是一些示例:

std::regex re("\\w+");
// 匹配一个由字母、数字和/或下划线组成的序列

std::regex re{'\\', 'w', '+'};
// 同上

std::regex re(R"(\w+xxx")", 3);
// 同上

成员函数

  • regex &operator=(RHS)
    拷贝和移动赋值运算符可用。RHS 可以是:

    • 一个非终止字符字符串(类型为 char const *);
    • 一个 std::string const &(或任何兼容的 std::basic_string);
    • 一个 std::initializer_list<char>
  • regex &assign(RHS)
    这个成员函数接受与 regex 构造函数相同的参数,包括(可选的)regex_constants 值。

  • regex::flag_type flag() const
    返回当前 regex 对象激活的 regex_constants 标志。例如:

    int main() {
        regex re;
    
        regex::flag_type flags = re.flags();
        cout <<
            // 显示:16 0 0
            (re.flags() & regex_constants::ECMAScript) << ' '
             << (re.flags() & regex_constants::icase) << ' '
             << (re.flags() & regex_constants::awk) << ' ' << '\n';
    }
    

    注意,当在构造时指定了标志类型值的组合时,仅设置那些指定的标志。例如,如果 re(regex_constants::icase) 被指定,cout 语句将显示 0 1 0。也可以指定相互冲突的标志值组合,如 regex_constants::awk | regex_constants::grep。虽然这样的 regex 对象的构造会成功,但应避免这种做法。

  • locale_type get_loc() const
    返回与当前 regex 对象关联的区域设置。

  • locale_type imbue(locale_type locale)
    locale 替换 regex 对象当前的区域设置,并返回被替换的区域设置。

  • unsigned mark_count() const
    返回 regex 对象中标记的子表达式的数量。例如:

    int main() {
        regex re("(\\w+)([[:alpha:]]+)");
        cout << re.mark_count() << '\n'; // 显示:2
    }
    
  • void swap(regex &other) noexcept
    other 交换当前 regex 对象。也可以作为一个自由函数使用:void swap(regex &lhs, regex &rhs),交换 lhsrhs

获取匹配结果:std::match_results

一旦有了 regex 对象,就可以用它来匹配目标文本与正则表达式。为了将目标文本与正则表达式进行匹配,可以使用以下函数(将在下一节18.8.4中详细描述):

  • regex_match:仅匹配目标文本与正则表达式,通知调用者是否找到匹配项。
  • regex_search:同样匹配目标文本与正则表达式,但允许检索标记的子表达式(即,带括号的正则表达式)的匹配结果。
  • regex_replace:将目标文本与正则表达式进行匹配,并用另一段文本替换匹配到的部分目标文本。

这些函数必须提供目标文本和一个 regex 对象(这些函数不会修改 regex 对象)。通常,还会传递一个 std::match_results 对象,以包含正则表达式匹配过程的结果。

在使用 match_results 类之前,必须包含 <regex> 头文件。

match_results 对象的使用示例将在第18.8.4节中提供。本节及下一节主要供参考。

match_results 类有不同的特化版本。选择特化版本时,应该与所使用的 regex 类的特化版本相匹配。例如,如果正则表达式是以 char const * 形式指定的,则 match_results 的特化版本也应操作 char const * 类型的值。

match_results 类的各种特化版本及其名称如下:

  • cmatch
    定义了 match_results<char const *>,使用 char const * 类型的迭代器。应与 regex(char const *) 正则表达式规格一起使用。

  • wcmatch
    定义了 match_results<wchar_t const *>,使用 wchar_t const * 类型的迭代器。应与 regex(wchar_t const *) 正则表达式规格一起使用。

  • smatch
    定义了 match_results<std::string::const_iterator>,使用 std::string::const_iterator 类型的迭代器。应与 regex(std::string const &) 正则表达式规格一起使用。

  • wsmatch
    定义了 match_results<std::wstring::const_iterator>,使用 std::wstring::const_iterator 类型的迭代器。应与 regex(std::wstring const &) 正则表达式规格一起使用。

构造函数

match_results 类提供了默认构造函数、拷贝构造函数和移动构造函数。默认构造函数定义了一个 Allocator const & 参数,该参数默认初始化为默认分配器。通常,match_results 类的对象通过将其传递给上述函数(如 regex_match)来接收与匹配相关的信息。当从这些函数返回时,可以使用 match_results 类的成员来检索匹配过程的具体结果。

成员函数

  • match_results &operator=: 提供了拷贝和移动赋值运算符。

  • std::string const &operator[](size_t idx) const: 返回索引为 idx 的子匹配的(常量)引用。对于索引值 0,返回对完整匹配的引用。如果 idx >= size(),则返回目标字符串的一个空子范围。 如果成员函数 ready()(见下文)返回 false,则该成员函数的行为是未定义的。

  • Iterator begin() const: 返回指向第一个子匹配的迭代器。Iterator 是对 const match_results 对象的常量迭代器。

  • Iterator cbegin() const: 返回指向第一个子匹配的常量迭代器。

  • Iterator cend() const: 返回指向超出最后一个子匹配的常量迭代器。

  • Iterator end() const: 返回指向超出最后一个子匹配的迭代器。Iterator 是对 const match_results 对象的常量迭代器。

  • ReturnType format(Parameters) const: 由于此成员函数需要较为详细的描述,因此会在当前概述中打断流程。此成员函数与 regex_replace 函数一起使用,因此将在 regex_replace 函数的部分(18.8.4.5)中详细介绍。

  • allocator_type get_allocator() const: 返回对象的分配器。

  • bool empty() const: 如果 match_results 对象不包含匹配(这也是使用默认构造函数后返回的结果),则返回 true。否则,返回 false

  • int length(size_t idx = 0) const: 返回索引为 idx 的子匹配的长度。默认返回完整匹配的长度。如果 idx >= size(),则返回 0

  • size_type max_size() const: 返回 match_results 对象中可以包含的最大子匹配数。这是一个依赖于实现的常量值。

  • int position(size_t idx = 0) const: 返回子匹配 idx 的第一个字符在目标文本中的偏移量。默认返回完整匹配第一个字符的偏移量。如果 idx >= size(),则返回 -1

  • std::string const &prefix() const: 返回目标文本的一个(常量)引用,该引用指向以完整匹配的第一个字符结尾的子字符串。

  • bool ready() const: 从默认构造的 match_results 对象中没有匹配结果。它从上述匹配函数中接收匹配结果。一旦有匹配结果,返回 true,否则返回 false

  • size_type size() const: 返回子匹配的数量。例如,对于正则表达式 (abc)|(def) 和目标文本 defcon,报告三个子匹配:完整匹配(def)、(abc) 的空文本,以及 def 对应的 (def) 标记子表达式。注意:当使用量词时,仅计算并报告最后一个匹配。例如,对于模式 (a|b)+ 和目标 aaab,报告两个子匹配:完整匹配 aaab 和最后一个匹配 b

  • std::string str(size_t idx = 0) const: 返回定义索引为 idx 的子匹配的字符。默认返回完整匹配。如果 idx >= size(),则返回一个空字符串。

  • std::string const &suffix() const: 返回目标文本的一个(常量)引用,该引用指向以完整匹配的最后一个字符之后开始的子字符串。

  • void swap(match_results &other) noexcept: 交换当前的 match_results 对象与 other 对象。也可以作为自由函数使用:void swap(match_results &lhs, match_results &rhs),交换 lhsrhs

正则表达式匹配函数

在使用本节中介绍的函数之前,必须包含 <regex> 头文件。

有三大类函数可用于将目标文本与正则表达式进行匹配。每个这些函数,以及 match_results::format 成员,都具有一个最终的 std::regex_constants::match_flag_type 参数(见下一节),该参数的默认值为 regex_constants::match_default,用于微调正则表达式和匹配过程的使用。这个最终参数在正则表达式匹配函数或格式成员中没有明确提及。这三大类函数分别是:

  • bool std::regex_match(Parameters): 这个函数系列用于将正则表达式与目标文本进行匹配。只有当正则表达式完全匹配目标文本时,才返回 true;否则返回 false。有关可用的重载 regex_match 函数的概述,请参见第18.8.4.2节。

  • bool std::regex_search(Parameters): 这个函数系列也用于将正则表达式与目标文本进行匹配。这个函数在正则表达式匹配到目标文本的某个子字符串时返回 true;否则返回 false。有关可用的重载 regex_search 函数的概述,请见下文。

  • ReturnType std::regex_replace(Parameters): 这个函数系列用于生成修改后的文本,使用目标字符串的字符、正则表达式对象和格式字符串。这个成员函数的功能与 match_results::format 成员非常相似,后者在第18.8.4.4节中讨论。

在讨论 regex_replace 之后,可以使用 match_results::format 成员,具体内容将在第18.8.4.4节中讨论。

std::regex_constants::match_flag_type 标志

所有重载的格式成员和所有正则表达式匹配函数都接受一个最终的 regex_constants::match_flag_type 参数,这是一个位掩码类型,可以使用 bit_or 操作符进行组合。所有格式成员默认指定的参数是 match_default

match_flag_type 枚举定义了以下值(下面的“[first, last)”指的是被匹配的字符序列):

  • format_default: 不是一个位掩码值,而是一个默认值,等于 0。仅使用这个规范,std::regex_replace 使用 ECMAScript 规则来构造字符串。
  • format_first_only: std::regex_replace 仅替换第一个匹配项。
  • format_no_copy: 不匹配的字符串不会被传递到输出中,std::regex_replace 不会输出这些字符串。
  • format_sed: 使用 POSIX sed(1) 规则来构造字符串,在 std::regex_replace 中应用。
  • match_any: 如果可能有多个匹配项,则任何一个匹配项都是可接受的结果。
  • match_continuous: 子序列仅在从开始位置开始时才匹配。
  • match_not_bol: [first, last) 中的第一个字符被视为普通字符:^ 不匹配 [first, first)。
  • match_not_bow: \b 不匹配 [first, first)。
  • match_default: 不是一个位掩码值,而是等于 0 的默认值:传递给正则表达式匹配函数和 match_results::format 成员的最终参数。std::regex_replace 使用 ECMAScript 规则来构造字符串。
  • match_not_eol: [first, last) 中的最后一个字符被视为普通字符:$ 不匹配 [last, last)。
  • match_not_eow: \b 不匹配 [last, last)。
  • match_not_null: 空序列不被视为匹配项。
  • match_prev_avail: +NOTRANS(-{}-{}) 指的是一个有效的字符位置。当指定时,match_not_bolmatch_not_bow 被忽略。

匹配整个文本:std::regex_match

正则表达式匹配函数 std::regex_match 如果正则表达式在其提供的 regex 参数中完全匹配提供的目标文本,则返回 true。这意味着 match_results::prefixmatch_results::suffix 必须返回空字符串。但是,定义子表达式是可以的。

以下是该函数的几个重载版本:

  • bool regex_match(BidirConstIter first, BidirConstIter last, std::match_results &results, std::regex const &re):

    • BidirConstIter 是一个双向常量迭代器。范围 [first, last) 定义了目标文本。匹配结果将返回在 results 中。迭代器的类型必须与使用的 match_results 的类型匹配。例如,如果迭代器是 char const * 类型,则应使用 cmatch,如果迭代器是 string::const_iterator 类型,则应使用 smatch。其他重载版本的函数也有类似的对应要求。
  • bool regex_match(BidirConstIter first, BidirConstIter last, std::regex const &re):

    • 该函数的行为类似于前一个函数,但不返回匹配过程中的结果。
  • bool regex_match(char const *target, std::match_results &results, std::regex const &re):

    • 该函数的行为类似于第一个重载版本,使用 target 中的字符作为目标文本。
  • bool regex_match(char const *str, std::regex const &re):

    • 该函数的行为类似于前一个函数,但不返回匹配结果。
  • bool regex_match(std::string const &target, std::match_results &results, std::regex const &re):

    • 该函数的行为类似于第一个重载版本,使用 target 中的字符作为目标文本。
  • bool regex_match(std::string const &str, std::regex const &re):

    • 该函数的行为类似于前一个函数,但不返回匹配结果。
  • bool regex_match(std::string const &&, std::match_results &, std::regex &) = delete:

    • regex_match 函数不接受临时字符串对象作为目标字符串,因为这将导致 match_results 参数中的字符串迭代器无效。

以下是一个小例子:如果正则表达式匹配文本(由 argv[1] 提供),该文本以 5 位数字开头,然后仅包含字母([[:alpha:]])。可以将数字作为子表达式 1 提取:

#include <iostream>
#include <regex>
using namespace std;

int main(int argc, char const **argv) {
    regex re("(\\d{5})[[:alpha:]]+");
    cmatch results;
    if (not regex_match(argv[1], results, re))
        cout << "No match\n";
    else
        cout << "size: " << results.size() << ": " << results.str(1) << " -- "
             << results.str() << '\n';
}

部分匹配文本:std::regex_search

regex_match 不同,正则表达式匹配函数 std::regex_search 返回 true 如果正则表达式在其提供的 regex 参数中部分匹配目标文本。

以下是该函数的几个重载版本:

  • bool regex_search(BidirConstIter first, BidirConstIter last, std::match_results &results, std::regex const &re):

    • BidirConstIter 是一个双向常量迭代器。范围 [first, last) 定义了目标文本。匹配结果将返回在 results 中。迭代器的类型必须与使用的 match_results 的类型匹配。例如,如果迭代器是 char const * 类型,则应使用 cmatch,如果迭代器是 string::const_iterator 类型,则应使用 smatch。其他重载版本的函数也有类似的对应要求。
  • bool regex_search(BidirConstIter first, BidirConstIter last, std::regex const &re):

    • 该函数的行为类似于前一个函数,但不返回匹配过程中的结果。
  • bool regex_search(char const *target, std::match_results &results, std::regex const &re):

    • 该函数的行为类似于第一个重载版本,使用 target 中的字符作为目标文本。
  • bool regex_search(char const *str, std::regex const &re):

    • 该函数的行为类似于前一个函数,但不返回匹配结果。
  • bool regex_search(std::string const &target, std::match_results &results, std::regex const &re):

    • 该函数的行为类似于第一个重载版本,使用 target 中的字符作为目标文本。
  • bool regex_search(std::string const &str, std::regex const &re):

    • 该函数的行为类似于前一个函数,但不返回匹配结果。
  • bool regex_search(std::string const &&, std::match_results &, std::regex &) = delete:

    • regex_search 函数不接受临时字符串对象作为目标字符串,因为这将导致 match_results 参数中的字符串迭代器无效。

以下是一个示例,说明如何使用 regex_search

#include <iostream>
#include <regex>
#include <string>
using namespace std;

int main() {
    while (true) {
        cout << "Enter a pattern or plain Enter to stop: ";
        string pattern;
        if (not getline(cin, pattern) or pattern.empty()) break;
        regex re(pattern);
        while (true) {
            cout << "Enter a target text for `" << pattern
                 << "'\n"
                    "(plain Enter for the next pattern): ";
            string text;

            if (not getline(cin, text) or text.empty()) break;
            smatch results;
            if (not regex_search(text, results, re))
                cout << "No match\n";
            else {
                cout << "Prefix: " << results.prefix()
                     << "\n"
                        "Match: "
                     << results.str()
                     << "\n"
                        "Suffix: "
                     << results.suffix() << "\n";
                for (size_t idx = 1; idx != results.size(); ++idx)
                    cout << "Match " << idx << " at offset "
                         << results.position(idx) << ": " << results.str(idx)
                         << '\n';
            }
        }
    }
}

这个示例程序循环接收用户输入的正则表达式模式和目标文本,并使用 regex_search 函数查找匹配项。它将输出匹配的前缀、匹配的字符串以及后缀,还会列出所有匹配项及其位置。

成员函数 std::match_results::format

match_results::format 成员函数是 match_results 类中的一个相对复杂的成员函数,可以用来修改之前通过正则表达式匹配的文本(例如,通过 regex_search 函数)。由于其复杂性,并且因为另一个正则表达式处理函数(regex_replace)提供了类似的功能,format 成员函数在此进行讨论。

format 成员函数对 match_results 对象中包含的(子)匹配项进行操作,使用格式化字符串生成文本,其中格式说明符(如 $&)被替换为原始目标文本中的匹配部分。此外,format 成员函数识别所有标准 C 转义序列(如 \n)。format 成员函数用于创建与原始目标文本相比经过修改的文本。

以下是 format 成员函数的所有支持的格式说明符概述:

  • $': 对应于 prefix 成员返回的文本:原始目标文本中到完全匹配文本的第一个字符之前的所有字符。
  • $&: 对应于完全匹配的文本(即 match_results::str 成员返回的文本)。
  • $n(其中 n 是一个整数自然数): 对应于 operator[](n) 返回的文本。
  • $': 对应于 suffix 成员返回的文本:原始目标文本中在完全匹配文本最后一个字符之后的所有字符。
  • $$: 对应于单个 $ 字符。

format 成员函数有四个重载版本。所有重载版本都定义了一个最终的 regex_constants::match_flag_type 参数,默认初始化为 match_default。这个最终参数在以下对 format 成员函数的覆盖中没有明确提及。

为了进一步说明 format 成员函数的使用,假设以下代码已经被执行:

regex re("([[:alpha:]]+)\\s+(\\d+)"); // 字母 空格 数字
smatch results;
string target("this value 1024 is interesting");
if (not regex_search(target, results, re))
    return 1;

在调用 regex_search(第 6 行)后,正则表达式匹配过程的结果将保存在 match_results 对象 results 中,该对象在第 3 行定义。

前两个重载的 format 函数期望一个输出迭代器,用于写入格式化文本。这些重载成员返回最终的输出迭代器,指向刚刚写入的字符之后的位置。

  • OutputIter format(OutputIter out, char const *first, char const *last) const:

    • 范围 [first, last) 中的字符应用于 match_results 对象中存储的子表达式,生成的字符串插入到 out 中。下一个重载版本提供了一个示例。
  • OutputIter format(OutputIter out, std::string const &fmt) const:

    • fmt 的内容应用于 match_results 对象中存储的子表达式,生成的字符串插入到 out 中。以下代码将值 1024 插入到 cout 中(注意 fmt 必须是 std::string 类型,因此使用了 string 构造函数):
      results.format(ostream_iterator<char>(cout, ""), "$2"s);
      

其余两个重载的 format 成员期望一个 std::string 或 NTBS(非空终止的字符串)来定义格式字符串。这两个成员返回一个包含格式化文本的 std::string

  • std::string format(std::string const &fmt) const
  • std::string format(char const *fmt) const

以下示例展示了如何获得一个字符串,其中之前获得的 match_results 对象中的第一个和第二个标记子表达式的顺序被交换:

string reverse(results.format("$2 and $1"));

修改目标字符串:std::regex_replace

std::regex_replace 函数族使用正则表达式对字符序列进行替换。它们的功能与前面讨论的 match_results::format 成员函数类似。以下是可用的重载版本:

  • OutputIt regex_replace(OutputIter out, BidirConstIter first, BidirConstIter last, std::regex const &re, std::string const &fmt):

    • OutputIter 是一个输出迭代器;BidirConstIter 是一个双向常量迭代器。
    • 该函数在迭代器范围 [out, retvalue) 中返回可能被修改的文本,其中 out 是传递给 regex_replace 的输出迭代器,retvalueregex_replace 返回的输出迭代器。
    • 该函数将范围 [first, last) 内的文本与 re 中存储的正则表达式进行匹配。如果正则表达式未能匹配范围 [first, last) 内的目标文本,则目标文本将字面上复制到 out
    • 如果正则表达式匹配了目标文本,则:
      • 首先,将匹配结果的前缀复制到 out。前缀是目标文本中到完全匹配文本第一个字符之前的所有字符。
      • 接下来,匹配的文本将被格式字符串 fmt 中的内容替换,格式说明符可以使用前一节(18.8.4.4)中描述的格式说明符,替换后的文本将复制到 out
      • 最后,将匹配结果的后缀复制到 out。后缀是目标文本中在匹配文本最后一个字符之后的所有字符。
    • 下面的例子展示了 regex_replace 的工作原理:
      regex re("([[:alpha:]]+)\\s+(\\d+)"); // 字母 空格 数字
      string target("this value 1024 is interesting");
      regex_replace(ostream_iterator<char>(cout, ""), target.begin(), target.end(), re, "$2"s);
      
      在第 5 行调用 regex_replace。其格式字符串仅包含 $2,匹配目标文本中的 1024。前缀在单词 “value” 处结束,后缀在 1024 之后开始,因此第 5 行的语句将文本 this 1024 is interesting 插入到标准输出流中。
  • OutputIt regex_replace(OutputIter out, BidirConstIter first, BidirConstIter last, std::regex const &re, char const *fmt):

    • 该变体的行为与第一个变体类似。如果在上述示例中使用 $2 而不是 $2"s,则会使用此变体。
  • std::string regex_replace(std::string const &str, std::regex const &re, std::string const &fmt):

    • 该变体返回一个包含修改文本的 std::string,并期望一个包含目标文本的 std::string。除此之外,其行为与第一个变体相同。在上述示例中,可以用以下语句替换第 5 行,初始化 string result
      string result(regex_replace(target, re, "$2"s));
      
  • std::string regex_replace(std::string const &str, std::regex const &re, char const *fmt):

    • 在上述语句中,将 $2s 更改为 $2,使用此变体,其行为与前一个变体完全相同。
  • std::string regex_replace(char const *str, std::regex const &re, std::string const &fmt):

    • 该变体使用 char const * 指向目标文本,行为与前一个变体完全相同。
  • std::string regex_replace(char const *str, std::regex const &re, char const *fmt):

    • 此变体也使用 char const * 指向目标文本,行为与前一个变体完全相同。

随机化和统计分布

在使用统计分布和相关的随机数生成器之前,需要包含 <random> 头文件。

STL 提供了几种标准的数学(统计)分布。这些分布允许程序员从选择的分布中获取随机选择的值。这些统计分布需要配合一个随机数生成对象。提供了多种这种随机数生成对象,扩展了传统的 rand 函数,该函数是 C 标准库的一部分。

这些随机数生成对象生成伪随机数,然后由统计分布处理,以获得从指定分布中随机选择的值。

尽管 STL 提供了各种统计分布,但其功能相对有限。这些分布允许我们从这些分布中获取随机数,但 STL 目前不提供概率密度函数或累积分布函数。然而,这些函数(分布以及密度和累积分布函数)可以在其他库中找到,如 Boost 数学库(具体网址: Boost Math Library)。

C++ 注释不涉及各种统计分布的数学特性。有兴趣的读者可以参考相关的数学教科书(如 Stuart 和 Ord 的《Kendall’s Advanced Theory of Statistics》,Wiley)或网站,如 维基百科上的伯努利分布

随机数生成器

以下是可用的生成器:

类模板类型质量速度状态大小
linear_congruential_engine整数中等中等1
subtract_with_carry_engine整数/浮点数中等快速25
mersenne_twister_engine整数良好快速624

随机数生成器

线性同余生成器 (linear_congruential_engine)
该生成器计算公式为:
value i + 1 = ( a ⋅ value i + c ) % m \text{value}_{i+1} = (a \cdot \text{value}_i + c) \% m valuei+1=(avaluei+c)%m

它需要模板参数,分别是生成随机值的数据类型;乘数 ( a );加法常数 ( c );以及模数 ( m )。例如:

linear_congruential_engine<int, 10, 3, 13> lincon;

可以通过构造函数传递一个种子值来初始化生成器。例如:

lincon(time(0));

减法进位生成器 (subtract_with_carry_engine)
该生成器计算公式为:
value i = ( value i − s − value i − r − carry i − 1 ) % m \text{value}_i = (\text{value}_{i-s} - \text{value}_{i-r} - \text{carry}_{i-1}) \% m valuei=(valueisvalueircarryi1)%m

它需要模板参数,分别是生成随机值的数据类型;模数 ( m );以及减法常数 ( s ) 和 ( r )。例如:

subtract_with_carry_engine<int, 13, 3, 13> subcar;

可以通过构造函数传递一个种子值来初始化生成器。例如:

subcar(time(0));

梅森旋转生成器 (mersenne_twister_engine)
mersenne_twister_enginemt19937 预定义类型在 <random> 头文件中。可以使用如下方式构造:

mt19937 mt;

也可以通过构造函数传递种子值来初始化生成器,例如:

mt19937 mt(time(0));

其函数调用操作符返回一个随机的无符号整数值。

其他初始化 mersenne_twister_engine 的方法超出了 C++ 注释的范围(但可以参考 Lewis 等人(1969 年)的相关文献)。

这些随机数生成器也可以通过调用其 seed 成员来进行初始化,该成员接受无符号长整型值或生成器函数(例如,lc.seed(time(0))lc.seed(mt))。

随机数生成器提供了 minmax 成员,分别返回生成器的最小值和最大值(包括)。如果需要较小的范围,可以将生成器嵌套在函数或类中以适配范围。

以下是一个小示例,展示如何使用 mersenne_twister_engine mt19937 生成随机数:

以下是 C++ 代码的翻译及其解释:

#include <iostream>
#include <ctime>
#include <random>

using namespace std;

// 程序参数:
// 1. 生成的随机数数量
// 2. 最小的正随机数
// 3. 最大的正随机数

int main(int argc, char **argv) {
    mt19937 mt(time(0));  // 使用当前时间的秒数作为种子初始化梅森旋转生成器
    for (size_t nGenerate = stoul(argv[1]),          // 从命令行参数获取要生成的随机数数量
                lowest = stoul(argv[2]),             // 从命令行参数获取最小的正随机数
                mod = stoul(argv[3]) + 1 - lowest;   // 计算最大值与最小值之间的范围
         nGenerate--;)
        cout << (lowest + mt() % mod) << ' ';        // 生成一个随机数,并打印到标准输出
    cout << '\n';  // 打印换行符
}

代码解释:

  1. 包含头文件:

    • <iostream> 用于输入输出操作。
    • <ctime> 用于时间相关的功能。
    • <random> 用于随机数生成。
  2. 初始化生成器:

    • mt19937 mt(time(0));:使用当前时间的秒数作为种子初始化梅森旋转生成器 mt19937
  3. 生成随机数:

    • 从命令行参数获取生成的随机数数量、最小值和最大值。
    • mod = stoul(argv[3]) + 1 - lowest; 计算生成随机数的范围。
    • lowest + mt() % mod 生成的随机数落在 [lowest, highest] 范围内。
    • 使用 for 循环生成并输出指定数量的随机数。
  4. 程序输出:

    • 每生成一个随机数,就打印出来,最后打印一个换行符。

使用示例:

如果你运行程序时提供如下参数:

./program 10 1 100

它会生成 10 个在 1 到 100 之间的随机数,并将它们打印出来。

以下是关于统计分布的翻译及其示例代码:

统计分布

在接下来的部分中,将介绍 C++ 支持的各种统计分布。使用 RNG 表示随机数生成器,使用 URNG 表示均匀随机数生成器。每种分布都有一个 struct param_type,包含该分布的参数。param_type 结构体的组织形式取决于具体的分布。

所有分布都提供以下成员函数(result_type 指的是分布返回值的类型):

  • result_type max() const:返回分布的上界。
  • result_type min() const:返回分布的下界。
  • param_type param() const:返回分布的参数对象。
  • void param(const param_type &param):重新定义分布的参数。
  • void reset():清除所有缓存值。

所有分布支持以下操作符(distribution-name 应替换为所用分布的名称,例如 normal_distribution):

  • template<typename URNG> result_type operator()(URNG &urng):返回从统计分布中生成的下一个随机值,其中函数对象 urng 返回从均匀随机分布中选择的下一个随机数。
  • template<typename URNG> result_type operator()(URNG &urng, param_type &param):返回用提供的参数初始化的统计分布中的下一个随机值。函数对象 urng 返回从均匀随机分布中选择的下一个随机数。
  • std::istream &operator>>(std::istream &in, distribution-name &object):从 std::istream 中提取分布的参数。
  • std::ostream &operator<<(std::ostream &out, distribution-name const &bd):将分布的参数插入到 std::ostream 中。

以下示例演示了如何使用这些分布。只需将分布名称(normal_distribution)替换为其他分布的名称即可切换分布。所有分布都有参数,例如正态分布的均值和标准差,所有参数都有默认值。参数的名称在不同的分布中有所不同,并在每个分布的详细说明中提到。分布提供成员函数用于返回或设置其参数。

大多数分布定义为类模板,需要指定用于函数返回类型的数据类型。如果需要,您可以使用空模板参数类型规范(<>)来获取默认类型。默认类型通常为 double(用于实数值返回类型)或 int(用于整数值返回类型)。对于那些未定义为模板类的分布,必须省略模板参数类型规范。

以下是一个示例,展示了如何使用统计分布,应用于正态分布:

#include <iostream>
#include <ctime>
#include <random>

using namespace std;

int main() {
    std::mt19937 engine(time(0));  // 使用当前时间的秒数作为种子初始化梅森旋转生成器
    std::normal_distribution<> dist;  // 使用默认参数初始化正态分布

    for (size_t idx = 0; idx < 10; ++idx)
        std::cout << "a random value: " << dist(engine) << "\n";  // 生成并打印 10 个随机值

    cout << '\n' << dist.min() << " " << dist.max() << '\n';  // 打印分布的下界和上界
}

代码解释:

  1. 包含头文件:

    • <iostream>:用于输入输出操作。
    • <ctime>:用于时间相关的功能。
    • <random>:用于随机数生成。
  2. 初始化生成器和分布:

    • std::mt19937 engine(time(0));:使用当前时间的秒数作为种子初始化梅森旋转生成器 mt19937
    • std::normal_distribution<> dist;:初始化正态分布,使用默认参数(均值为 0,标准差为 1)。
  3. 生成随机数:

    • 使用 for 循环生成并打印 10 个随机值。
  4. 打印分布的下界和上界:

    • dist.min()dist.max() 返回并打印分布的最小值和最大值。

伯努利分布

bernoulli_distribution 用于生成具有特定概率 p 的布尔值(逻辑真值)。它相当于进行一次实验的二项分布(参见 18.9.2.2)。

bernoulli_distribution 并未定义为类模板。

定义的类型:

  • using result_type = bool;:结果类型为布尔值。
  • struct param_type
    struct param_type
    {
        explicit param_type(double prob = 0.5);
        double p() const; // 返回 prob
    };
    

构造函数和成员函数

  • bernoulli_distribution(double prob = 0.5):构造一个伯努利分布,其返回 true 的概率为 prob
  • double p() const:返回分布的概率 prob
  • result_type min() const:返回布尔值 false
  • result_type max() const:返回布尔值 true

示例代码

以下是一个使用 bernoulli_distribution 的简单示例:

#include <iostream>
#include <random>

using namespace std;

int main() {
    std::mt19937 engine(time(0)); // 初始化梅森旋转生成器
    std::bernoulli_distribution dist(0.7); // 生成一个概率为 0.7 的伯努利分布

    for (size_t idx = 0; idx < 10; ++idx)
        std::cout << "Random boolean value: " << dist(engine) << "\n"; // 生成并打印 10 个随机布尔值

    cout << '\n' << "Min value: " << dist.min() << "\n"; // 打印布尔值的最小值 (false)
    cout << "Max value: " << dist.max() << "\n"; // 打印布尔值的最大值 (true)

    return 0;
}

代码解释:

  1. 初始化生成器和分布:

    • std::mt19937 engine(time(0));:使用当前时间的秒数作为种子初始化梅森旋转生成器。
    • std::bernoulli_distribution dist(0.7);:创建一个伯努利分布对象,其返回 true 的概率为 0.7。
  2. 生成布尔值:

    • 使用 for 循环生成并打印 10 个随机布尔值。
  3. 打印布尔值的最小值和最大值:

    • dist.min() 返回布尔值的最小值 false
    • dist.max() 返回布尔值的最大值 true

二项分布

binomial_distribution<IntType = int> 用于确定在一系列 n 次独立的成功/失败实验中成功的次数的概率,每次实验成功的概率为 p

模板类型参数 IntType 定义了生成的随机值的类型,该类型必须是一个整数类型。

定义的类型:

  • using result_type = IntType;:结果类型为 IntType

  • struct param_type

    struct param_type
    {
        explicit param_type(IntType trials, double prob = 0.5);
        IntType t() const; // 返回 trials
        double p() const; // 返回 prob
    };
    

构造函数和成员函数:

  • binomial_distribution<>(IntType trials = 1, double prob = 0.5):构造一个二项分布对象,进行 trials 次实验,每次实验成功的概率为 prob
  • binomial_distribution<>(param_type const &param):根据 param 结构中的值构造一个二项分布对象。
  • IntType t() const:返回实验次数 trials
  • double p() const:返回成功概率 prob
  • result_type min() const:返回成功次数的最小值 0
  • result_type max() const:返回成功次数的最大值 trials

示例代码

以下是一个使用 binomial_distribution 的简单示例:

#include <iostream>
#include <random>

using namespace std;

int main() {
    std::mt19937 engine(time(0)); // 初始化梅森旋转生成器
    std::binomial_distribution<int> dist(10, 0.5); // 进行 10 次实验,每次实验成功的概率为 0.5

    for (size_t idx = 0; idx < 10; ++idx)
        std::cout << "Random number of successes: " << dist(engine) << "\n"; // 生成并打印 10 个随机成功次数

    cout << '\n' << "Min value: " << dist.min() << "\n"; // 打印成功次数的最小值 (0)
    cout << "Max value: " << dist.max() << "\n"; // 打印成功次数的最大值 (10)

    return 0;
}

代码解释:

  1. 初始化生成器和分布:

    • std::mt19937 engine(time(0));:使用当前时间的秒数作为种子初始化梅森旋转生成器。
    • std::binomial_distribution<int> dist(10, 0.5);:创建一个二项分布对象,进行 10 次实验,每次实验成功的概率为 0.5。
  2. 生成成功次数:

    • 使用 for 循环生成并打印 10 个随机成功次数。
  3. 打印成功次数的最小值和最大值:

    • dist.min() 返回成功次数的最小值 0
    • dist.max() 返回成功次数的最大值 10

柯西分布

cauchy_distribution<RealType = double> 看起来类似于正态分布,但柯西分布具有更重的尾部。在研究假设检验时,如果假设正态性,通过查看检验在柯西分布数据上的表现,可以很好地评估这些检验对偏离正态性的重尾数据的敏感性。

柯西分布的均值和标准差是未定义的。

定义的类型:

  • using result_type = RealType;:结果类型为 RealType

  • struct param_type

    struct param_type
    {
        explicit param_type(RealType a = RealType(0),
                            RealType b = RealType(1));
        double a() const; // 返回分布的 a 参数
        double b() const; // 返回分布的 b 参数
    };
    

构造函数和成员函数:

  • cauchy_distribution<>(RealType a = RealType(0), RealType b = RealType(1)):构造一个柯西分布对象,使用指定的 ab 参数。
  • cauchy_distribution<>(param_type const &param):根据 param 结构中的值构造一个柯西分布对象。
  • RealType a() const:返回分布的 a 参数。
  • RealType b() const:返回分布的 b 参数。
  • result_type min() const:返回 result_type 的最小正值。
  • result_type max() const:返回 result_type 的最大值。

示例代码

以下是一个使用 cauchy_distribution 的简单示例:

#include <iostream>
#include <random>

using namespace std;

int main() {
    std::mt19937 engine(time(0)); // 初始化梅森旋转生成器
    std::cauchy_distribution<double> dist(0, 1); // 创建柯西分布,参数 a=0,b=1

    for (size_t idx = 0; idx < 10; ++idx)
        std::cout << "Random value: " << dist(engine) << "\n"; // 生成并打印 10 个随机值

    cout << '\n' << "Parameter a: " << dist.a() << "\n"; // 打印参数 a
    cout << "Parameter b: " << dist.b() << "\n"; // 打印参数 b
    cout << "Min value: " << dist.min() << "\n"; // 打印结果类型的最小值
    cout << "Max value: " << dist.max() << "\n"; // 打印结果类型的最大值

    return 0;
}

代码解释:

  1. 初始化生成器和分布:

    • std::mt19937 engine(time(0));:使用当前时间的秒数作为种子初始化梅森旋转生成器。
    • std::cauchy_distribution<double> dist(0, 1);:创建一个柯西分布对象,参数 a 为 0,参数 b 为 1。
  2. 生成随机值:

    • 使用 for 循环生成并打印 10 个随机值。
  3. 打印分布参数及值:

    • dist.a() 返回分布的参数 a
    • dist.b() 返回分布的参数 b
    • dist.min() 返回 result_type 的最小正值。
    • dist.max() 返回 result_type 的最大值。

卡方分布

chi_squared_distribution<RealType = double> 具有 ( n ) 个自由度的卡方分布表示 ( n ) 个独立标准正态随机变量平方和的分布。虽然分布的参数 ( n ) 通常是一个整数值,但它不必是整数,因为卡方分布是通过取实数参数的函数(如指数函数和伽玛函数)定义的(例如,GNU g++ 编译器分发的 <bits/random.h> 头文件中提供了相关公式)。

卡方分布通常用于检验观察分布与理论分布的拟合优度。

定义的类型:

  • using result_type = RealType;:结果类型为 RealType

  • struct param_type

    struct param_type
    {
        explicit param_type(RealType n = RealType(1));
        RealType n() const; // 返回自由度 n
    };
    

构造函数和成员函数:

  • chi_squared_distribution<>(RealType n = 1):构造一个卡方分布对象,指定自由度 ( n )。
  • chi_squared_distribution<>(param_type const &param):根据 param 结构中的值构造一个卡方分布对象。
  • RealType n() const:返回分布的自由度 ( n )。
  • result_type min() const:返回 0。
  • result_type max() const:返回 result_type 的最大值。

示例代码

以下是一个使用 chi_squared_distribution 的简单示例:

#include <iostream>
#include <random>

using namespace std;

int main() {
    std::mt19937 engine(time(0)); // 初始化梅森旋转生成器
    std::chi_squared_distribution<double> dist(10); // 创建一个卡方分布,参数为 10 个自由度

    for (size_t idx = 0; idx < 10; ++idx)
        std::cout << "Random value: " << dist(engine) << "\n"; // 生成并打印 10 个随机值

    cout << '\n' << "Degrees of freedom: " << dist.n() << "\n"; // 打印自由度 n
    cout << "Min value: " << dist.min() << "\n"; // 打印结果类型的最小值
    cout << "Max value: " << dist.max() << "\n"; // 打印结果类型的最大值

    return 0;
}

代码解释:

  1. 初始化生成器和分布:

    • std::mt19937 engine(time(0));:使用当前时间的秒数作为种子初始化梅森旋转生成器。
    • std::chi_squared_distribution<double> dist(10);:创建一个卡方分布对象,设置自由度为 10。
  2. 生成随机值:

    • 使用 for 循环生成并打印 10 个随机值。
  3. 打印分布参数及值:

    • dist.n() 返回分布的自由度 ( n )。
    • dist.min() 返回 result_type 的最小值(即 0)。
    • dist.max() 返回 result_type 的最大值。

极值分布

extreme_value_distribution<RealType = double> 与 Weibull 分布有关,用于统计模型中,其中变量是许多随机因素的最小值,这些因素可以取正值或负值。有关详细信息,请参见 NIST 网站

定义的类型:

  • using result_type = RealType;:结果类型为 RealType

  • struct param_type

    struct param_type
    {
        explicit param_type(RealType a = RealType(0), RealType b = RealType(1));
        RealType a() const; // 返回位置参数 a
        RealType b() const; // 返回尺度参数 b
    };
    

构造函数和成员函数:

  • extreme_value_distribution<>(RealType a = 0, RealType b = 1):构造一个极值分布对象,指定位置参数 ( a ) 和尺度参数 ( b )。
  • extreme_value_distribution<>(param_type const &param):根据 param 结构中的值构造一个极值分布对象。
  • RealType a() const:返回分布的地点参数 ( a )。
  • RealType b() const:返回分布的尺度参数 ( b )。
  • result_type min() const:返回 result_type 的最小正值。
  • result_type max() const:返回 result_type 的最大值。

示例代码

以下是一个使用 extreme_value_distribution 的简单示例:

#include <iostream>
#include <random>

using namespace std;

int main() {
    std::mt19937 engine(time(0)); // 初始化梅森旋转生成器
    std::extreme_value_distribution<double> dist(1.0, 2.0); // 创建一个极值分布,位置参数 a = 1.0,尺度参数 b = 2.0

    for (size_t idx = 0; idx < 10; ++idx)
        std::cout << "Random value: " << dist(engine) << "\n"; // 生成并打印 10 个随机值

    cout << '\n' << "Location parameter: " << dist.a() << "\n"; // 打印位置参数 a
    cout << "Scale parameter: " << dist.b() << "\n"; // 打印尺度参数 b
    cout << "Min value: " << dist.min() << "\n"; // 打印结果类型的最小正值
    cout << "Max value: " << dist.max() << "\n"; // 打印结果类型的最大值

    return 0;
}

代码解释:

  1. 初始化生成器和分布:

    • std::mt19937 engine(time(0));:使用当前时间的秒数作为种子初始化梅森旋转生成器。
    • std::extreme_value_distribution<double> dist(1.0, 2.0);:创建一个极值分布对象,设置位置参数 ( a = 1.0 ) 和尺度参数 ( b = 2.0 )。
  2. 生成随机值:

    • 使用 for 循环生成并打印 10 个随机值。
  3. 打印分布参数及值:

    • dist.a() 返回分布的地点参数 ( a )。
    • dist.b() 返回分布的尺度参数 ( b )。
    • dist.min() 返回 result_type 的最小正值(通常为极小的正值)。
    • dist.max() 返回 result_type 的最大值。

指数分布

exponential_distribution<RealType = double> 用于描述可以通过均匀泊松过程建模的事件之间的时间间隔。它可以被解释为几何分布的连续形式。

其参数 lambda 定义了分布的速率参数(lambda 参数)。其期望值和标准差均为 ( \frac{1}{\lambda} )。

定义的类型:

  • using result_type = RealType;:结果类型为 RealType

  • struct param_type

    struct param_type
    {
        explicit param_type(RealType lambda = RealType(1));
        RealType lambda() const;
    };
    

构造函数和成员函数:

  • exponential_distribution<>(RealType lambda = 1):构造一个指数分布对象,指定速率参数 ( \lambda )。
  • exponential_distribution<>(param_type const &param):根据 param 结构中的值构造一个指数分布对象。
  • RealType lambda() const:返回分布的速率参数 ( \lambda )。
  • result_type min() const:返回 result_type 的最小值(通常为 0)。
  • result_type max() const:返回 result_type 的最大值。

Fisher F 分布

fisher_f_distribution<RealType = double> 在统计方法中广泛使用,如方差分析(ANOVA)。它是将两个卡方分布的结果进行除法得到的分布。它由两个参数定义,分别是两个卡方分布的自由度。

请注意,尽管分布的参数通常是整数值,但它们不必是整数,因为 Fisher F 分布是由接受非整数参数值的卡方分布构造的(参见第 18.9.2.4 节)。

定义的类型:

  • using result_type = RealType;:结果类型为 RealType

  • struct param_type

    struct param_type
    {
        explicit param_type(RealType m = RealType(1), RealType n = RealType(1));
        RealType m() const; // 分子自由度
        RealType n() const; // 分母自由度
    };
    

构造函数和成员函数:

  • fisher_f_distribution<>(RealType m = RealType(1), RealType n = RealType(1)):构造一个 Fisher F 分布对象,指定自由度 ( m )(分子自由度)和 ( n )(分母自由度)。
  • fisher_f_distribution<>(param_type const &param):根据 param 结构中的值构造一个 Fisher F 分布对象。
  • RealType m() const:返回分子的自由度。
  • RealType n() const:返回分母的自由度。
  • result_type min() const:返回 result_type 的最小值(通常为 0)。
  • result_type max() const:返回 result_type 的最大值。

Gamma 分布

gamma_distribution<RealType = double> 用于处理不服从正态分布的数据,常用于建模等待时间等情况。它有两个参数,α 和 β。其期望值为 α ∗ β,标准差为 √(α ∗ β²)。

定义的类型:

  • using result_type = RealType;:结果类型为 RealType

  • struct param_type

    struct param_type
    {
        explicit param_type(RealType alpha = RealType(1), RealType beta = RealType(1));
        RealType alpha() const;
        RealType beta() const;
    };
    

构造函数和成员函数:

  • gamma_distribution<>(RealType alpha = 1, RealType beta = 1):构造一个 Gamma 分布对象,指定 α(形状参数)和 β(尺度参数)。
  • gamma_distribution<>(param_type const &param):根据 param 结构中的值构造一个 Gamma 分布对象。
  • RealType alpha() const:返回分布的 α 参数。
  • RealType beta() const:返回分布的 β 参数。
  • result_type min() const:返回 result_type 的最小值(通常为 0)。
  • result_type max() const:返回 result_type 的最大值。

几何分布

geometric_distribution<IntType = int> 用于建模直到第一次成功所需的伯努利试验次数。它有一个参数 prob,表示每次伯努利试验的成功概率。

定义的类型:

  • using result_type = IntType;:结果类型为 IntType

  • struct param_type

    struct param_type
    {
        explicit param_type(double prob = 0.5);
        double p() const;
    };
    

构造函数、成员函数及示例:

  • geometric_distribution<>(double prob = 0.5):构造一个几何分布对象,表示每次伯努利试验的成功概率为 prob
  • geometric_distribution<>(param_type const &param):根据 param 结构中的值构造一个几何分布对象。
  • double p() const:返回分布的成功概率 prob
  • param_type param() const:返回分布的 param_type 结构。
  • void param(const param_type &param):重新定义分布的参数。
  • result_type min() const:返回分布的下界(= 0)。
  • result_type max() const:返回分布的上界。
  • template<typename URNG> result_type operator()(URNG &urng):返回从几何分布中生成的下一个随机值。
  • template<typename URNG> result_type operator()(URNG &urng, param_type &param):返回由提供的 param 结构初始化的几何分布中的下一个随机值。

示例代码:

#include <iostream>
#include <ctime>
#include <random>

int main()
{
    std::linear_congruential_engine<unsigned, 7, 3, 61> engine(0);
    std::geometric_distribution<> dist;
    for (size_t idx = 0; idx < 10; ++idx)
        std::cout << "a random value: " << dist(engine) << "\n";
    std::cout << '\n' << dist.min() << " " << dist.max() << '\n';
}

在这个示例中,使用了 linear_congruential_engine 作为随机数生成器,并用 geometric_distribution 生成了10个随机值。

对数正态分布

lognormal_distribution<RealType = double> 是一个概率分布,其中随机变量的对数服从正态分布。如果一个随机变量 ( X ) 服从正态分布,那么 ( Y = e^X ) 服从对数正态分布。

它有两个参数,ms,分别表示 ( \ln(X) ) 的均值和标准差。

定义的类型:

  • using result_type = RealType;:结果类型为 RealType

  • struct param_type

    struct param_type
    {
        explicit param_type(RealType m = RealType(0), RealType s = RealType(1));
        RealType m() const;
        RealType s() const;
    };
    

构造函数和成员函数:

  • lognormal_distribution<>(RealType m = 0, RealType s = 1):构造一个对数正态分布对象,其中随机变量的均值为 m,标准差为 s
  • lognormal_distribution<>(param_type const &param):根据 param 结构中的值构造一个对数正态分布对象。
  • RealType m() const:返回分布的 m 参数。
  • RealType stddev() const:返回分布的 s 参数。
  • result_type min() const:返回分布的下界(= 0)。
  • result_type max() const:返回分布的上界。

正态分布

normal_distribution<RealType = double> 通常用于科学中描述复杂现象。在预测或测量变量时,错误通常被假定为正态分布。

它有两个参数:均值(mean)和标准差(standard deviation)。

定义的类型:

  • using result_type = RealType;:结果类型为 RealType

  • struct param_type

    struct param_type
    {
        explicit param_type(RealType mean = RealType(0), RealType stddev = RealType(1));
        RealType mean() const;
        RealType stddev() const;
    };
    

构造函数和成员函数:

  • normal_distribution<>(RealType mean = 0, RealType stddev = 1):构造一个正态分布对象,均值为 mean,标准差为 stddev。默认参数值定义了标准正态分布。
  • normal_distribution<>(param_type const &param):根据 param 结构中的值构造一个正态分布对象。
  • RealType mean() const:返回分布的均值参数。
  • RealType stddev() const:返回分布的标准差参数。
  • result_type min() const:返回 result_type 的最小正值。
  • result_type max() const:返回 result_type 的最大值。

负二项分布

negative_binomial_distribution<IntType = int> 概率分布描述了在指定次数失败之前,伯努利试验中的成功次数。例如,如果你重复掷骰子直到第三次出现1,那么出现其他面的次数的概率分布就是负二项分布。

它有两个参数:IntType 类型的 kk > 0),表示实验停止前的失败次数,以及 double 类型的 p,表示每次实验中的成功概率。

定义的类型:

  • using result_type = IntType;:结果类型为 IntType

  • struct param_type

    struct param_type
    {
        explicit param_type(IntType k = IntType(1), double p = 0.5);
        IntType k() const;
        double p() const;
    };
    

构造函数和成员函数:

  • negative_binomial_distribution<>(IntType k = IntType(1), double p = 0.5):构造一个负二项分布对象,指定 kp 参数。
  • negative_binomial_distribution<>(param_type const &param):根据 param 结构中的值构造一个负二项分布对象。
  • IntType k() const:返回分布的 k 参数。
  • double p() const:返回分布的 p 参数。
  • result_type min() const:返回 result_type 的最小值(通常是 0)。
  • result_type max() const:返回 result_type 的最大值。

泊松分布

poisson_distribution<IntType = int> 用于建模在固定时间段内发生某些事件的概率,这些事件以已知的概率发生,并且与上一个事件发生的时间无关。

它有一个参数 mean,表示在考虑的时间间隔内期望发生的事件数量。例如,如果在一分钟内平均观察到 2 个事件,而研究的时间间隔为 10 分钟,则 mean = 20

定义的类型:

  • using result_type = IntType;:结果类型为 IntType

  • struct param_type

    struct param_type
    {
        explicit param_type(double mean = 1.0);
        double mean() const;
    };
    

构造函数和成员函数:

  • poisson_distribution<>(double mean = 1):构造一个泊松分布对象,指定 mean 参数。
  • poisson_distribution<>(param_type const &param):根据 param 结构中的值构造一个泊松分布对象。
  • double mean() const:返回分布的 mean 参数。
  • result_type min() const:返回 result_type 的最小值(通常是 0)。
  • result_type max() const:返回 result_type 的最大值。

学生 t 分布

student_t_distribution<RealType = double> 是一种概率分布,用于在样本量较小时估计正态分布总体的均值。它由一个参数特征,即自由度(degrees of freedom),其值等于样本量减去 1。

定义的类型:

  • using result_type = RealType;:结果类型为 RealType

  • struct param_type

    struct param_type
    {
        explicit param_type(RealType n = RealType(1));
        RealType n() const; // 自由度
    };
    

构造函数和成员函数:

  • student_t_distribution<>(RealType n = RealType(1)):构造一个 student_t 分布对象,指定自由度 n
  • student_t_distribution<>(param_type const &param):根据 param 结构中的值构造一个 student_t 分布对象。
  • RealType n() const:返回分布的自由度。
  • result_type min() const:返回 result_type 的最小值(通常是 0)。
  • result_type max() const:返回 result_type 的最大值。

均匀实数分布

uniform_real_distribution<RealType = double> 用于在一个均匀分布的实数范围内随机选择 RealType 值。它具有两个参数 ab,分别指定了分布可以返回的半开区间 [a, b)

定义的类型:

  • using result_type = RealType;:结果类型为 RealType

  • struct param_type

    struct param_type
    {
        explicit param_type(RealType a = 0, RealType b = max(RealType));
        RealType a() const;
        RealType b() const;
    };
    

构造函数和成员函数:

  • uniform_real_distribution<>(RealType a = 0, RealType b = max(RealType)):构造一个 uniform_real_distribution 对象,指定值的范围 [a, b)
  • uniform_real_distribution<>(param_type const &param):根据 param 结构中的值构造一个 uniform_real_distribution 对象。
  • RealType a() const:返回分布的 a 参数。
  • RealType b() const:返回分布的 b 参数。
  • result_type min() const:返回分布的 a 参数(即范围的下界)。
  • result_type max() const:返回分布的 b 参数(即范围的上界)。

威布尔分布

weibull_distribution<RealType = double> 常用于可靠性工程和生存(寿命数据)分析。它有两种或三种参数形式,其中 STL 提供了两参数形式。三参数形式包含一个形状(或斜率)参数、一个尺度参数和一个位置参数。两参数形式隐含位置参数值为 0。在两参数形式中,形状参数(a)和尺度参数(b)是显式指定的。有关威布尔分布参数的有趣解释,请参见 这里

定义的类型:

  • using result_type = RealType;:结果类型为 RealType

  • struct param_type

    struct param_type
    {
        explicit param_type(RealType a = RealType{ 1 }, RealType b = RealType{ 1 });
        RealType a() const; // 形状(斜率)参数
        RealType b() const; // 尺度参数
    };
    

构造函数和成员函数:

  • weibull_distribution<>(RealType a = 1, RealType b = 1):构造一个威布尔分布对象,指定形状参数 a 和尺度参数 b
  • weibull_distribution<>(param_type const &param):根据 param 结构中的值构造一个威布尔分布对象。
  • RealType a() const:返回分布的形状(或斜率)参数。
  • RealType stddev() const:返回分布的尺度参数。
  • result_type min() const:返回 0。
  • result_type max() const:返回 result_type 的最大值。

tie

我们已经在 3.3.7.1 节中遇到过结构化绑定。结构化绑定允许我们在函数内部将结构类型(如 struct、std::pair 或(见 22.6 节)元组)的字段作为局部变量来访问。以下是一个使用结构化绑定的基本示例:

std::pair<int, int> factory()
{
    return { 1, 2 };
}

void fun()
{
    auto [one, two] = factory();
    std::cout << one << ' ' << two << '\n';
}

能够使用结构化绑定在这种情况下非常有用。但是,如果我们希望将结构体的字段分配给已经定义的变量或作为参数传递给函数的变量,结构化绑定则没有帮助。例如,在下面的代码片段中,我们定义了一个 retrieve 函数,具有一个 int & 参数和一个 int 局部变量,并且我们希望将 factory 返回的值分配给这些变量:

void retrieve(int &one)
{
    int two;
    // ... = factory() ??
}

在这里,结构化绑定不能使用:结构化绑定的元素不能是引用。虽然可以定义一个 std::pair<int &, int &> 对象,但该对象不能用 factory 返回的字段的引用进行初始化。这些语句将无法编译:

std::pair<int &, int &> p{one, two} = factory();
std::pair<int &, int &>{one, two} = factory();

虽然可以首先定义一个 std::pair<int &, int &> 对象,然后将 factory 的返回值赋给它,但这种方法显然不如结构化绑定所提供的优雅。

幸运的是,还有一个更好的替代方案。在包含 <tuple> 头文件之后(参见 22.6 节),std::tie 可以用来“绑定”对结构化数据类型字段的引用。使用 std::tie,可以很容易地将 retrieve 函数中的 onetwo 变量与 factory 返回的 pair 的字段关联起来:

void retrieve(int &one)
{
    int two;
    std::tie(one, two) = factory();
    std::cout << one << ' ' << two << '\n';
}

执行以下语句:

int one = 0;
int two = 0;
std::cout << one << ' ' << two << '\n';
retrieve(one);
std::cout << one << ' ' << two << '\n';

将得到以下输出:

0 0
1 2
1 0

此外,std::tie 函数还支持排序和(不)相等比较。下一个示例中的 Data 结构体定义了三个字段:一个 int、一个 std::string 和一个 double。这些字段都支持排序和(不)相等比较。在这些情况下,所有比较运算符都可以通过太空船运算符(参见 11.7.2 节)使用 std::tie 容易实现:

struct Data
{
    int d_int;
    std::string d_string;
    double d_double;
};

bool operator==(Data const &lhs, Data const &rhs)
{
    return std::tie(lhs.d_int, lhs.d_string, lhs.d_double) ==
           std::tie(rhs.d_int, rhs.d_string, rhs.d_double);
}

std::partial_ordering operator<=>(Data const &lhs, Data const &rhs)
{
    return std::tie(lhs.d_int, lhs.d_string, lhs.d_double) <=>
           std::tie(rhs.d_int, rhs.d_string, rhs.d_double);
}

注意,Data 结构体的太空船运算符返回的是 std::partial_ordering 值(参见 18.7.3 节)。尽管 intstd::string 的太空船运算符返回的是 std::strong_ordering 值,但 double 的太空船运算符返回的是 std::partial_ordering 值。因此,Data 结构体的太空船运算符也返回 std::partial_ordering 值。

可选返回值

要使用 std::optional 对象,必须包含 <optional> 头文件。考虑一个返回流中后续行的函数。该函数可能是一个从其对象打开的流中读取数据的成员函数。一个基本实现可能是:

std::string Class::nextLine()
{
    std::string line;
    std::getline(d_stream, line);
    return line;
}

当然,这种实现方式并不理想,因为 getline 可能会失败。处理这类失败的常见方法有:

  • 函数返回指向字符串的指针,当 getline 失败时指针为 nullptr,当 getline 成功时指向包含行的字符串。
  • 函数返回布尔值,并定义字符串指针或引用参数:函数的返回值表示 getline 是否成功。
  • 函数返回 std::pairstd::tuple 对象,其中一个字段是布尔值,另一个字段是 std::string

标准模板库提供了另一种处理这种情况的方法:模板类 std::optional。模板类的声明如下:

template <typename DataType>
class optional;

其中,DataType 指代 optional 类处理的数据类型。

替代返回 std::stringnextLine 函数可以指定 std::optional<std::string> 作为返回类型:

std::optional<std::string> Class::nextLine();

std::optional 对象的解释很简单:它要么包含一个 DataType 对象,要么不包含。如果它包含 DataType 对象,则该对象作为对象可用,而不是指向动态分配的 DataType 对象的指针。同时,optional 对象可以被解释为布尔值。如果 optional 对象包含 DataType 对象,则 optional 的布尔值为 true;如果不包含 DataType 对象,则其布尔值为 false

std::optional 类提供了以下功能:

  • 构造函数

    • 默认构造函数(例如 std::optional<std::string> opt;)不包含值;
    • 提供复制构造函数和移动构造函数;
    • 对象可以从可以转换为 optionalDataType 的值构造(例如,optional<string> 可以从 NTBS(非空终止的字符序列)初始化)。如果初始化值是右值引用,则 DataType 对象从初始化值进行移动构造。
  • 赋值运算符

    • 赋值运算符可以用来重新分配 optional 对象的 DataType 值,或从另一个相同 DataTypeoptional 对象重新分配;
    • 提供复制赋值运算符和移动赋值运算符。
  • 访问器

    • explicit operator bool()has_value() 成员函数返回 true 如果 optional 对象包含 DataType 值;
    • value()operator*()operator->() 返回对 optionalDataType 值的引用。如果从 const optional<DataType> 对象调用引用为 const 引用;如果从 rvalue 引用调用,则为右值引用。
      • operator*operator-> 成员函数类似于 value,但不表示 optionalDataType 成员本身是作为指针存储的;
      • value() 检查 optional 对象是否实际包含 DataType 对象,如果不包含则抛出 std::bad_optional_access 异常;
    • value_or(Type &&defaultValue) 返回 optional 对象的 DataType 的副本,如果对象包含值,则返回;如果不包含,则返回 DataType{ defaultValue }。注意,DataType 必须能够从 Type 构造。
  • 修改器

    • swap(optional<DataType> &other):交换当前 optional 对象和另一个 optional 对象的内容;
    • reset():清除 optionalDataType 成员。调用 reset() 后,has_value() 返回 false
    • emplace(Args &&...args)emplace(initializer_list, Args &&...args):第一个 emplaceargs 转发到 DataType 的构造函数;第二个 emplaceargs 转发到初始化列表,并将该列表转发到 DataType 的构造函数。
  • 比较运算符

    • 所有比较运算符(包括 operator<=>)都可用(如果 optionalDataType 定义了这些运算符),用于比较两个 optional 对象的 DataType 值。
  • std::optional<DataType> make_optional(...)

    • 返回一个构造自 DataType 左值或右值引用的 optional 对象,或者从接受 emplace 的相同参数构造的 optional 对象。

    以下是一个使用 std::optional<std::string>nextLine 函数实现及其简单的主函数示例:

#include <iostream>
#include <sstream>
#include <string>
#include <optional>

using namespace std;

optional<string> nextLine(istream &in)
{
    std::optional<std::string> opt;
    string line;
    if (getline(in, line))
        opt = move(line);

    // 输出调试信息
    cout << "internal: has value: " << opt.has_value() <<
        ", value = " << (opt ? *opt : "no value") << '\n';
    
    return opt;
}

int main()
{
    istringstream in{ "hello world\n" };
    auto opt = nextLine(in);
    cout << "main: has value: " << opt.has_value() <<
        ", value = " << (opt ? *opt : "no value") << '\n';

    opt = nextLine(in);
    cout << "main: has value: " << opt.has_value() <<
        ", value = " << (opt ? *opt : "no value") << '\n';
}

这个程序的输出是:

internal: has value: 1, value = hello world
main: has value: 1, value = hello world
internal: has value: 0, value = no value
main: has value: 0, value = no value

注意,在第二次调用后,当没有返回值时,opt 保留了第一次调用时获得的值:optional 的赋值运算符在发现 has_value 返回 false 后不会处理已经存在的值。因此,在调用 value 之前,务必检查 has_valueoperator bool

以下是对文本的翻译:

STL泛型算法

在使用本章介绍的泛型算法之前,除了属于“Operators”(操作符)类别的算法(如下所述),需要包含<algorithm>头文件。使用“Operators”类别中的泛型算法之前,需要包含<numeric>头文件。

在上一章中,介绍了标准模板库(STL)。然而,STL中一个重要的组成部分——泛型算法——并未在上一章中提及,因为它们在STL中占有相当大的比重。随着时间的推移,由于模板的重要性和普及性日益增加,STL得到了显著扩展。如果在STL一章中涵盖泛型算法,那一章将变得过于庞大,因此泛型算法被移到了独立的一章中。

泛型算法具有非常强大的功能。借助模板的强大功能,可以开发出适用于广泛数据类型的算法,同时保持类型安全性。这方面的典型例子就是sort泛型算法。与C语言相比,C语言要求程序员编写回调函数,其中必须使用类型不安全的void const *参数,程序员不得不在内部进行类型转换,而STL的sort算法通常只需程序员指定类似如下的调用:

sort(first-element, last-element);

在可能的情况下,应该尽量使用泛型算法。避免自己编写常见算法的代码。养成先彻底查找泛型算法中的可用候选项的习惯。泛型算法应该成为你编写代码时的首选工具:熟练掌握它们并将其使用成为“第二天性”。

另一方面,尽管泛型算法非常重要,但很明显,随着时间的推移,泛型算法的数量几乎无限增长。许多新算法被添加了进来,即使其中很多算法可以非常容易地直接实现,或者几乎很少使用。例如,is_sorted泛型算法,它只是简单地返回一个元素范围是否已排序,返回truefalse。定义这样的函数在你可能需要的情况下又有多难呢?这也适用于其他不少泛型算法。当然,拥有一个庞大的工具箱是很好的,但你真的需要完全匹配螺丝头的螺丝刀吗?可能不需要…为了避免本章内容过于庞杂,一些部分合并了相似的算法,并在某些部分提供了指向cppreference网站的链接,以便参考相似的算法。

尽管如此,本章的各节涵盖了STL的大量泛型算法(按字母顺序排列)。对于每个算法,提供了以下信息:

  • 所需的头文件;
  • 函数原型;
  • 简短描述;
  • 简短示例。

在算法的原型中,Type用于指定泛型数据类型。此外,提到了所需的特定迭代器类型(参见第18.2节)以及可能需要的其他泛型类型(例如,执行二元操作,如plus<Type>)。虽然迭代器通常由抽象容器和类似的预定义数据结构提供,但在某些情况下,你可能希望设计自己的迭代器。第22.14节提供了构造自己迭代器类的指南,并概述了不同类型的迭代器需要实现的操作符。

几乎所有的泛型算法都期望一个迭代器范围[first, last),定义了算法操作的元素序列。迭代器指向对象或值。当迭代器指向Type值或对象时,算法使用的函数对象通常接收Type const &对象或值。通常情况下,函数对象不能修改它们接收到的对象,但修改型泛型算法当然可以修改它们操作的对象。

泛型算法可以分类。C++注解将泛型算法分为以下几类:

  • 比较器:比较(元素范围):

    • all_ofany_ofequalincludeslexicographical_comparemismatchnone_of
  • 复制/移动操作:执行复制/移动操作:

    • copycopy_backwardcopy_ifmovemove_backwardpartition_copypartial_sort_copyremove_copyremove_copy_ifreplace_copyreplace_copy_ifreverse_copyrotate_copysampleshift_leftshift_rightunique_copy
  • 计数器:执行计数操作:

    • countcount_if
  • 堆操作符:操作最大堆:

    • make_heappop_heappush_heapsort_heap
  • 初始化器:初始化数据:

    • fillfill_ngenerategenerate_niota;未初始化的(原始)内存;
  • 边界确定器:确定数据的边界:

    • beginend
  • 操作符:执行某种(算术?)操作:

    • accumulateadjacent_differenceexclusive_scaninclusive_scaninner_productpartial_sumreducetransform_reduce
  • 搜索器:执行搜索(和查找)操作:

    • adjacent_findbinary_searchequal_rangefindfind_endfind_first_offind_iffind_if_notlower_boundmaxmax_elementmin_elementminminmaxminmax_elementpartition_pointsearchsearch_nset_differenceset_intersectionset_symmetric_differenceset_unionupper_bound
  • 打乱器:执行重新排序操作(排序、合并、排列、交换):

    • inplace_mergeiter_swapmergenext_permutationnth_elementpartial_sortpartitionprev_permutationremoveremove_copyremove_copy_ifremove_ifreversereverse_copyrotaterotate_copyshufflesortstable_partitionstable_sortswapswap_rangesunique
  • 验证器:检查特定条件:

    • is_partitionedis_permutationis_sortedis_sorted_until
  • 访问器:访问范围内的元素:

    • for_eachreplacereplace_copyreplace_copy_ifreplace_iftransformunique_copy

执行策略

许多以下的泛型算法都可以进行并行化。例如,生成(generate)算法(第19.1.19节),用于通过生成函数填充元素。如果这些值是随机生成的,那么生成操作非常适合并行化,每个并行线程处理要填充的范围的一个独立部分。另一个适合并行执行的例子是对一系列值进行排序。为此,可以使用排序(sort)泛型算法(第19.1.54节,该节还包含了并行化排序的示例)。

这些(以及其他许多)泛型算法可以根据指定的执行策略进行并行执行。如果没有指定执行策略,那么算法将以标准的、顺序的方式运行。支持执行策略的泛型算法具有重载版本,其中第一个参数指定要使用的执行策略,后面是其他参数。在以下章节中列出的函数原型显示第一个参数为 [ExecPol,],它可以指定为第一个参数之一的执行策略。例如,排序泛型算法的一个原型是:

void sort([ExecPol,] RandomAccessIterator first, RandomAccessIterator last);

要对一个字符串向量(vs)进行排序,可以调用:

sort(vs.begin(), vs.end());

或使用(见下文):

sort(execution::par, vs.begin(), vs.end());
#define _PSTL_PAR_BACKEND_SERIAL
#include <execution>
#include <iostream>
#include <vector>
int main() {
    std::vector<int> vs{1, 2, 3, 3, 54, 3, 2, 1};
    std::sort(std::execution::par, vs.begin(), vs.end());
}

要使用执行策略,必须包含 <execution> 头文件,并在链接编译对象时指定 -ltbb 链接选项。

有四种类型的执行策略(都定义在 std 命名空间中):

  • execution::sequenced_policy:预定义对象 execution::seq 用于在调用泛型算法时指定此执行策略。
    当使用此策略调用泛型算法时,将不会使用并行执行。

  • execution::parallel_policy:预定义对象 execution::par 用于在调用泛型算法时指定此执行策略。
    当使用此策略调用泛型算法时,可能会使用并行执行:如果并行执行的开销实际上降低了非并行执行的效率,泛型算法可能会决定不使用并行执行。例如,当排序100个元素时,顺序执行比并行执行更快,因此类似排序的算法不会使用并行执行。

  • execution::parallel_unsequenced_policy:预定义对象 execution::par_unseq 用于在调用泛型算法时指定此执行策略。
    当使用此策略调用泛型算法时,可能会使用并行执行,执行可能会跨线程迁移(使用所谓的父进程窃取调度程序),或者执行可能会向量化(即,单个线程访问完全不同位置的数据项(如交换向量的第一个和中间元素))。使用此策略时,处理的元素访问顺序以及访问这些元素的线程是未定义的。

  • execution::unsequenced_policy:预定义对象 execution::unseq 用于在调用泛型算法时指定此执行策略。
    当使用此策略调用泛型算法时,算法将使用向量化执行。

在使用上述策略规范调用算法时,如果在算法执行期间调用的函数生成未捕获的异常,则会调用 std::terminate

使用并行执行时,传递给泛型算法的对象或函数可能会访问在其他地方定义的数据。如果这些数据被修改,那么可能会出现来自不同执行线程的修改请求,这可能导致数据竞争或死锁。程序员应确保在使用并行执行时不会发生数据竞争和/或死锁。

accumulate 函数

在这里插入图片描述

  • 头文件:<numeric>

  • 函数原型:

    • Type accumulate(InputIterator first, InputIterator last, Type init);
    • Type accumulate(InputIterator first, InputIterator last, Type init, BinaryOperation op);
  • 描述:

    • 第一个原型:operator+ 被应用于初始值 init(左操作数)和从迭代器范围内依次获取的所有元素(右操作数)。最终的值将被返回。
    • 第二个原型:二元操作符 op 被应用于初始值 init(左操作数)和从迭代器范围内依次获取的所有元素。最终的值将被返回。

    在每一步中,初始值接收该步计算的结果。例如,如果使用 minus<int> 并且初始值为 1,而迭代器指向 2 和 3,那么在第1步:1 - 2 = -1, 在第2步:-1 - 3 = -4。所以 accumulate 返回 -4。

    (参见 reduce 算法)。

  • 示例:

#include <numeric>
#include <vector>
#include <iostream>
using namespace std;

int main()
{
    int ia[] = {1, 2, 3, 4};
    vector<int> iv(ia, ia + 4);
    
    cout << "Sum: " << accumulate(iv.begin(), iv.end(), int()) << "\n"
         << "Product: " << accumulate(iv.begin(), iv.end(), int(1), multiplies<int>{}) << '\n';
}

// 输出:
// Sum: 10
// Product: 24

adjacent_difference 函数

在这里插入图片描述

  • 头文件:<numeric>

  • 函数原型:

    • OutputIterator adjacent_difference([ExecPol,] InputIterator first, InputIterator last, OutputIterator result);
    • OutputIterator adjacent_difference([ExecPol,] InputIterator first, InputIterator last, OutputIterator result, BinaryOperation op);
  • 描述:所有操作都是在原始值上执行的,所有计算值都是返回值。

    • 第一个原型:返回的第一个元素等于输入范围的第一个元素。其余返回的元素等于输入范围中相应元素与其前一个元素的差。
    • 第二个原型:返回的第一个元素等于输入范围的第一个元素。其余返回的元素等于二元操作符 op 应用于输入范围中相应元素(左操作数)和前一个元素(右操作数)的结果。
  • 示例:

#include <numeric>
#include <vector>
#include <iterator>
#include <iostream>
using namespace std;

int main() {
    int ia[] = {1, 2, 5, 10};
    vector<int> iv(ia, ia + 4);
    vector<int> ov(iv.size());

    adjacent_difference(iv.begin(), iv.end(), ov.begin());
    copy(ov.begin(), ov.end(), ostream_iterator<int>(cout, " "));
    cout << '\n';
    
    adjacent_difference(iv.begin(), iv.end(), ov.begin(), minus<int>());
    copy(ov.begin(), ov.end(), ostream_iterator<int>(cout, " "));
    cout << '\n';
}

// 输出:
//
// 1 1 3 5
// 1 (2-1=1) (5-2=3) (10-5=5)
// 1 1 3 5

adjacent_find 函数

在这里插入图片描述

  • 头文件:<algorithm>

  • 函数原型:

    • ForwardIterator adjacent_find([ExecPol,] ForwardIterator first, ForwardIterator last);
    • OutputIterator adjacent_find([ExecPol,] ForwardIterator first, ForwardIterator last, Predicate pred);
  • 描述:

    • 第一个原型:返回一个指向第一个相邻的两个相等元素中的第一个元素的迭代器。如果不存在这样的元素,则返回 last
    • 第二个原型:返回一个指向第一个满足二元谓词 predtrue 的相邻元素中的第一个元素的迭代器。如果不存在这样的元素,则返回 last
  • 示例:

#include <algorithm>
#include <string>
#include <iostream>
using namespace std;

bool squaresDiff10(size_t first, size_t second) {
    return second * second - first * first >= 10;
}

int main() {
    string sarr[] = {
        "Alpha", "bravo", "charley", "delta", "echo", "echo",
        "foxtrot", "golf"
    };
    
    auto last = end(sarr);
    // see the 'begin / end' section
    string *result = adjacent_find(sarr, last);
    //返回相邻相等的第一个元素的迭代器
    cout << *result << '\n';
    
    result = adjacent_find(++result, last);
    cout << "Second time, starting from the next position:\n" <<
    (result == last ? "** No more adjacent equal elements **" : "*result") << '\n';

    size_t iv[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    size_t *ires = adjacent_find(iv, end(iv), squaresDiff10);
    cout << "The first numbers for which the squares differ at least 10: "
         << *ires << " and " << *(ires + 1) << '\n';
        //返回相邻满足条件的第一个元素的迭代器
}

// 输出:
//
// echo
//
// Second time, starting from the next position:
//
// ** No more adjacent equal elements **
//
// The first numbers for which the squares differ at least 10: 5 and 6

all_of / any_of / none_of

在这里插入图片描述

  • 头文件:<algorithm>
  • 函数原型:
    • bool all_of([ExecPol,] InputIterator first, InputIterator last, Predicate pred);
    • bool any_of([ExecPol,] InputIterator first, InputIterator last, Predicate pred);
    • bool none_of([ExecPol,] InputIterator first, InputIterator last, Predicate pred);

描述:

  • 第一个函数原型:如果一元谓词 pred 对迭代器范围 [first, last) 中的所有元素返回 true,则该函数返回 true;否则返回 false
  • 第二个函数原型:如果一元谓词 pred 对迭代器范围 [first, last) 中的至少一个元素返回 true,则该函数返回 true;否则返回 false
  • 第三个函数原型:如果一元谓词 pred 对迭代器范围 [first, last) 中的所有元素都返回 false,则该函数返回 true;否则返回 false

示例:

#include <algorithm>
#include <string>
#include <iostream>
using namespace std;

bool contains_a(string const &str)
{
    return str.find('a') != string::npos;
}

int main()
{
    string sarr[] = { "Alpha", "Bravo", "Charley", "Delta", "Echo" };
    auto past = end(sarr);
    // 参见下一节
    cout << "All elements contain 'a': " <<
    all_of(sarr, past, contains_a) << "\n"
    "At least one element contains 'a': " <<
    any_of(sarr, past, contains_a) << "\n"
    "None of the elements contains 'a': " <<
    none_of(sarr, past, contains_a) << '\n';
}

输出:

All elements contain 'a': 0
At least one element contains 'a': 1
None of the elements contains 'a': 0

这个示例展示了如何使用 all_ofany_ofnone_of 函数来检查字符串数组中的元素是否包含字母 'a'

begin / end

  • 头文件:<iterator>(也可通过包含 <algorithm> 使用)
  • 函数原型:
    • auto begin([const] &obj);
    • auto end([const] &obj);
    • Type [const] *begin(Type [const] (&array)[N]);
    • Type [const] *end(Type [const] (&array)[N]);
    • auto cbegin(const &obj);
    • auto cend(const &obj);

描述:

beginend 函数分别返回对象的起始迭代器和结束迭代器,或者数组的首地址和末尾地址。具体说明如下:

  • 前两个原型:分别返回对象 objbegin()end() 成员函数的返回值(可能是 const 引用)。
  • 第三和第四个原型:分别返回指向数组(编译时已知大小)的首元素和末尾之后元素的指针(可能是 const)。
  • 最后两个原型:分别返回对象 objcbegin()cend() 成员函数的返回值(const 引用)。

示例:

#include <iterator>
#include <iostream>
#include <string>
using namespace std;

int main()
{
    string sarr[] = { "Alpha", "Bravo", "Charley", "Delta", "Echo" };
    // 由于 sarr == begin(sarr),这里没有使用 begin(...)
    auto next = end(sarr);
    cout << "sarr has " << size(sarr) <<
    " (== " << (next - sarr) << ") elements\n"
    "the last char. of the first element is " <<
    *(end(sarr[0]) - 1) << '\n';
}

输出:

sarr has 5 (== 5) elements
the last char. of the first element is a

该示例展示了如何使用 beginend 函数来获取数组的起始和结束迭代器,以及如何从中获取元素的信息。

binary_search

在这里插入图片描述

  • 头文件:<algorithm>
  • 函数原型:
    • bool binary_search(ForwardIterator first, ForwardIterator last, Type const &value);
    • bool binary_search(ForwardIterator first, ForwardIterator last, Type const &value, Comparator comp);

描述:

  • 第一个原型:在迭代器范围 [first, last) 中使用二分搜索查找 value。范围内的元素必须按 Type::operator< 函数排序。如果找到该元素,则返回 true;否则返回 false
  • 第二个原型:在迭代器范围 [first, last) 中使用二分搜索查找 value。范围内的元素必须按 Comparator 函数对象排序。如果找到该元素,则返回 true;否则返回 false。函数对象的第一个参数表示范围内的元素,第二个参数表示 value

示例:

#include <algorithm>
#include <string>
#include <iostream>
#include <functional>
using namespace std;

int main()
{
    string sarr[] = {
        "alpha", "bravo", "charley", "delta", "echo",
        "foxtrot", "golf", "hotel"
    };
    auto past = end(sarr);
    bool result = binary_search(sarr, past, "foxtrot");
    cout << (result ? "found " : "didn't find ") << "foxtrot" << '\n';
    
    reverse(sarr, past); // 反转元素的顺序
    // 现在二分搜索失败:
    result = binary_search(sarr, past, "foxtrot");
    cout << (result ? "found " : "didn't find ") << "foxtrot" << '\n';
    
    // 使用适当的比较器:
    result = binary_search(sarr, past, "foxtrot", greater<string>());
    cout << (result ? "found " : "didn't find ") << "foxtrot" << '\n';
    
    // 使用 lambda 表达式显示比较的索引和第二个参数的值:
    result = binary_search(sarr, past, "foxtrot",
        [&](string const &sarrEl, string const &value) {
            cout << "comparing element " << (&sarrEl - sarr) <<
            " (" << sarrEl << ") to " << value << '\n';
            return sarrEl > value;
        }
    );
    cout << "found it: " << result << '\n';
}

输出:

found foxtrot
didn't find foxtrot
found foxtrot
comparing element 4 (delta) to foxtrot
comparing element 2 (foxtrot) to foxtrot
comparing element 1 (golf) to foxtrot
comparing element -3 (foxtrot) to foxtrot
found it: 1

如果值确实在值的范围内,那么这个泛型算法并不能回答值的位置。如果需要知道值的位置,可以使用泛型算法 lower_boundupper_bound。有关这两个算法的示例,请参见第 19.1.30 和 19.1.61 节。

clamp 是 C++17 引入的一个标准库函数,用于将一个值限制在指定的范围内。它确保一个值不小于指定的最小值,不大于指定的最大值。如果值在范围之外,则返回范围的边界值。

clamp

在这里插入图片描述

  • 头文件: <algorithm>
  • 函数原型:
    • constexpr const T& clamp(const T& v, const T& lo, const T& hi);
    • constexpr const T& clamp(const T& v, const T& lo, const T& hi, Compare comp);

描述:

  • 第一个原型: 将值 v 限制在 [lo, hi] 的范围内。函数返回以下三者之一:

    • 如果 v < lo,则返回 lo
    • 如果 v > hi,则返回 hi
    • 否则,返回 v
  • 第二个原型: 与第一个原型相似,不同之处在于使用自定义的比较器 comp 来进行比较操作,而不是默认的 operator<

示例:

#include <algorithm>
#include <iostream>

int main() {
    int value = 15;
    int lower = 10;
    int upper = 20;

    int result = std::clamp(value, lower, upper);
    std::cout << "Clamped value: " << result << '\n'; // 输出: Clamped value: 15

    value = 5;
    result = std::clamp(value, lower, upper);
    std::cout << "Clamped value: " << result << '\n'; // 输出: Clamped value: 10

    value = 25;
    result = std::clamp(value, lower, upper);
    std::cout << "Clamped value: " << result << '\n'; // 输出: Clamped value: 20

    return 0;
}

输出:

Clamped value: 15
Clamped value: 10
Clamped value: 20

说明:

  1. value = 15 时,它已经在 [10, 20] 的范围内,因此直接返回 15
  2. value = 5 时,5 小于范围的下限 10,因此返回 10
  3. value = 25 时,25 大于范围的上限 20,因此返回 20

clamp 函数在需要限制值的范围,防止越界或不合理的值时非常有用。它可以用来确保数值保持在合理的上下限之间。

copy / copy_if

在这里插入图片描述

在这里插入图片描述

  • 头文件:<algorithm>
  • 函数原型:
    • OutputIterator copy([ExecPol,] InputIterator first, InputIterator last, OutputIterator destination);
    • OutputIterator copy_if([ExecPol,] InputIterator first, InputIterator last, OutputIterator destination, Predicate pred);

描述:

  • 第一个原型:将迭代器范围 [first, last) 中的元素复制(赋值)到目标输出范围 destination。返回值是指向目标范围中最后一个被复制元素之后位置的 OutputIterator(即目标范围中的 last 位置)。
  • 第二个原型:与第一个原型相同,但在复制满足 Predicate pred 的元素后结束,返回一个指向目标范围中最后一个被复制元素之后位置的迭代器。

示例:

注意第二次调用 copy。它使用了一个 ostream_iterator 来处理字符串对象。这个迭代器将字符串值写入指定的 ostream(即 cout),并用指定的分隔字符串(即 " ")分隔值。

#include <algorithm>
#include <string>
#include <iostream>
#include <iterator>
using namespace std;

bool pred(std::string const &str)
{
    return "aceg"s.find(str.front()) == string::npos;
}

int main()
{
    string sarr[] = {
        "alpha", "bravo", "charley", "delta", "echo",
        "foxtrot", "golf", "hotel"
    };
    auto last = end(sarr);
    
    // 将所有元素向左移动两位
    copy(sarr + 2, last, sarr);
    
    // 使用 ostream_iterator 将字符串复制到 cout
    copy(sarr, last, ostream_iterator<string>(cout, " "));
    cout << '\n';
    
    // 使用 copy_if:
    copy_if(sarr, sarr + size(sarr), sarr, pred);
    copy(sarr, sarr + size(sarr), ostream_iterator<string>(cout, " "));
    cout << '\n';
}

输出:

charley delta echo foxtrot golf hotel golf hotel
delta foxtrot hotel hotel golf hotel golf hotel

参考:

  • 查看 unique_copy 以获取更多有关唯一元素复制的示例。

copy_n

在这里插入图片描述

copy_n 是 C++ 标准库中的一个算法,用于从输入范围复制指定数量的元素到目标范围。与 copy 不同,copy_n 允许指定要复制的元素数量。

头文件:

#include <algorithm>

函数原型:

template <class InputIterator, class Size, class OutputIterator>
OutputIterator copy_n(InputIterator first, Size n, OutputIterator result);

描述:

  • 功能: 从起始迭代器 first 指向的范围开始,复制 n 个元素到 result 指向的目标范围。目标范围的大小必须足够容纳 n 个元素。返回一个指向目标范围中最后复制元素之后的位置的迭代器。
  • 输入参数:
    • first: 指向源范围起始位置的输入迭代器。
    • n: 需要复制的元素个数。
    • result: 指向目标范围起始位置的输出迭代器。
  • 返回值: 返回一个指向目标范围最后一个复制的元素之后的位置的输出迭代器。

示例:

#include <algorithm>
#include <iostream>
#include <vector>
#include <iterator>

int main() {
    std::vector<int> src = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    std::vector<int> dest(5);

    // 将 src 中前 5 个元素复制到 dest 中
    std::copy_n(src.begin(), 5, dest.begin());

    std::cout << "Destination vector: ";
    for (int val : dest) {
        std::cout << val << " ";
    }
    std::cout << std::endl;

    return 0;
}

输出:

Destination vector: 1 2 3 4 5 

说明:

  1. src 向量包含从 1 到 10 的整数。
  2. dest 向量预先分配了 5 个元素的空间。
  3. 使用 copy_nsrc 向量中的前 5 个元素复制到 dest 向量中。

copy_n 非常适合在知道确切复制数量的情况下使用,而无需担心整个输入范围的结束位置。这在处理部分数据或需要限定范围的场景中尤为有用。

copy_backward

在这里插入图片描述

  • 头文件:<algorithm>
  • 函数原型:
    • BidirectionalIterator copy_backward(InputIterator first, InputIterator last, BidirectionalIterator last2);

描述:

  • 这个函数将迭代器范围 [first, last) 中的元素从位置 last - 1 开始向前复制到目标范围 [last2 - (last - first), last2)。目标范围的结束位置为 last2 - 1
  • 注意,这个算法在复制元素时不会逆转元素的顺序。
  • 返回值是指向目标范围中最后一个被复制元素的 BidirectionalIterator,即目标范围中的 first 位置(由 last2 - (last - first) 指向)的迭代器。

示例:

#include <algorithm>
#include <string>
#include <iostream>
#include <iterator>
using namespace std;

int main()
{
    string sarr[] = {
        "alpha", "bravo", "charley", "delta", "echo",
        "foxtrot", "golf", "hotel"
    };
    auto past = end(sarr);
    
    // 从 sarr + 3 到 past 的元素逆向复制到目标范围
    copy(
        copy_backward(sarr + 3, past, past - 3),
        past,
        ostream_iterator<string>(cout, " ")
    );
    cout << '\n';
}

输出:

golf hotel foxtrot golf hotel foxtrot golf hotel

count / count_if

在这里插入图片描述

头文件: <algorithm>

函数原型:

  • size_t count([ExecPol,] InputIterator first, InputIterator last, Type const &value);
  • size_t count_if([ExecPol,] InputIterator first, InputIterator last, Predicate predicate);

描述:

  • 第一个原型: 返回在迭代器范围 [first, last) 中,值为 value 的元素出现的次数。使用 Type::operator== 来确定 value 是否等于迭代器范围中的元素。
  • 第二个原型: 返回在迭代器范围 [first, last) 中,单目谓词 predicate 对每个元素应用后返回 true 的次数。

示例:

#include <algorithm>
#include <iostream>
using namespace std;

bool pred(int value)
{
    return value & 1;  // 判断是否为奇数
}

int main()
{
    int ia[] = {1, 2, 3, 4, 3, 4, 2, 1, 3};
    cout << "Number of times the value 3 is available: " <<
        count(ia, ia + size(ia), 3) << "\n"  // 统计值为 3 的元素个数
        "Number of odd values in the array is: " <<
        count_if(ia, ia + size(ia), pred) << '\n';  // 统计数组中的奇数个数
}
// 输出:
// Number of times the value 3 is available: 3
// Number of odd values in the array is : 5

解释

  • count:

    • 用于计算指定值 value 在指定范围内出现的次数。
    • 例如,count(ia, ia + size(ia), 3) 计算数组 ia 中值为 3 的元素的个数。
  • count_if:

    • 用于计算满足特定条件(由 predicate 定义)的元素个数。
    • 例如,count_if(ia, ia + size(ia), pred) 统计数组 ia 中满足 pred 条件(在此示例中是奇数)的元素个数。

ends_with

在这里插入图片描述

ends_with 是 C++20 中引入的一个新的字符串操作函数,用于检查一个字符串是否以指定的子字符串结尾。

头文件:

#include <string>

函数原型:

bool ends_with(const std::string& str, const std::string& suffix);
bool ends_with(const std::string& str, const char* suffix);
bool ends_with(const std::string& str, char suffix);

描述:

  • 功能: ends_with 用于检查字符串 str 是否以子字符串 suffix 结尾。
  • 输入参数:
    • str: 待检查的源字符串。
    • suffix: 用于匹配的子字符串,可以是 std::string 类型、C 风格字符串 (const char*) 或单个字符 (char)。
  • 返回值: 如果 strsuffix 结尾,则返回 true;否则返回 false

示例:

#include <iostream>
#include <string>

int main() {
    std::string str1 = "Hello, World!";
    std::string str2 = "World!";
    char suffix_char = '!';

    // 检查 str1 是否以 str2 结尾
    if (str1.ends_with(str2)) {
        std::cout << "\"" << str1 << "\" ends with \"" << str2 << "\"\n";
    } else {
        std::cout << "\"" << str1 << "\" does not end with \"" << str2 << "\"\n";
    }

    // 检查 str1 是否以 '!' 结尾
    if (str1.ends_with(suffix_char)) {
        std::cout << "\"" << str1 << "\" ends with '" << suffix_char << "'\n";
    } else {
        std::cout << "\"" << str1 << "\" does not end with '" << suffix_char << "'\n";
    }

    return 0;
}

输出:

"Hello, World!" ends with "World!"
"Hello, World!" ends with '!'

说明:

  1. 在上面的例子中,str1"Hello, World!",而 str2"World!"。由于 str1 确实以 str2 结尾,因此 ends_with 返回 true
  2. 另外,通过检查 str1 是否以字符 ! 结尾,ends_with 也返回 true

ends_with 提供了一种简洁的方法来检查字符串末尾是否符合特定的子字符串或字符,这在许多实际应用场景中非常有用,比如验证文件扩展名、URL 后缀等。

equal

在这里插入图片描述

头文件: <algorithm>

函数原型:

  • bool equal([ExecPol,] InputIterator first, InputIterator last, InputIterator otherFirst);
  • bool equal([ExecPol,] InputIterator first, InputIterator last, InputIterator otherFirst, BinaryPredicate pred);

描述:

  • 第一个原型: 比较范围 [first, last) 中的元素与从 otherFirst 开始的等长范围中的元素。如果两个范围中的元素逐一相等,则返回 true。两个范围不需要完全相同长度,只需考虑指定范围内的元素(这些元素必须是可用的)。
  • 第二个原型: 比较范围 [first, last) 中的元素与从 otherFirst 开始的等长范围中的元素。如果二元谓词 pred 对两个范围中的所有对应元素应用后返回 true,则返回 true。两个范围不需要完全相同长度,只需考虑指定范围内的元素(这些元素必须是可用的)。

示例:

#include <algorithm>
#include <string>
#include <cstring>
#include <iostream>
using namespace std;

bool caseString(string const &first, string const &second)
{
    return strcasecmp(first.c_str(), second.c_str()) == 0;
}

int main()
{
    string first[] = {
        "Alpha", "bravo", "Charley", "delta", "Echo",
        "foxtrot", "Golf", "hotel"
    };
    string second[] = {
        "alpha", "bravo", "charley", "delta", "echo",
        "foxtrot", "golf", "hotel"
    };
    auto past = end(first);
    cout << "The elements of `first' and `second' are pairwise " <<
        (equal(first, past, second) ? "equal" : "not equal") <<
        '\n' <<
        "compared case-insensitively, they are " <<
        (
            equal(first, past, second, caseString) ?
            "equal" : "not equal"
        ) << '\n';
}
// 输出:
// The elements of `first' and `second' are pairwise not equal
// compared case-insensitively, they are equal

解释

  • equal:
    • 第一个原型:
      • 比较两个范围 [first, last)[otherFirst, otherFirst + (last - first)) 中的元素是否逐一相等。返回值是 true 当且仅当每对对应元素都相等。
    • 第二个原型:
      • 使用二元谓词 pred 比较两个范围中的元素。返回值是 true 当且仅当每对对应元素在谓词 pred 的判断下都返回 true

equal_range

在这里插入图片描述

头文件: <algorithm>

函数原型:

  • pair<ForwardIterator, ForwardIterator> equal_range(ForwardIterator first, ForwardIterator last, Type const &value);
  • pair<ForwardIterator, ForwardIterator> equal_range(ForwardIterator first, ForwardIterator last, Type const &value, Compare comp);

描述:

  • 第一个原型: 从一个已排序的序列开始(排序是通过数据类型的 operator< 实现的),返回一个 pair 结构,该结构的第一个迭代器是 lower_bound 的返回值(返回第一个不小于给定参考值的元素),第二个迭代器是 upper_bound 的返回值(返回第一个大于给定参考值的元素)。
  • 第二个原型: 从一个已排序的序列开始(排序是通过 comp 函数对象实现的),返回一个 pair 结构,该结构的第一个迭代器是 lower_bound 的返回值,第二个迭代器是 upper_bound 的返回值。

示例:

#include <algorithm>
#include <functional>
#include <iterator>
#include <iostream>
using namespace std;

int main()
{
    int range[] = {1, 3, 5, 7, 7, 9, 9, 9};
    auto past = end(range);
    pair<int *, int *> pi;

    // 查找值为6的范围
    pi = equal_range(range, past, 6);
    cout << "Lower bound for 6: " << *pi.first << "\n"
         << "Upper bound for 6: " << *pi.second << '\n';

    // 查找值为7的范围
    pi = equal_range(range, past, 7);
    cout << "Lower bound for 7: ";
    copy(pi.first, past, ostream_iterator<int>(cout, " "));
    cout << '\n';
    cout << "Upper bound for 7: ";
    copy(pi.second, past, ostream_iterator<int>(cout, " "));
    cout << '\n';

    // 按降序排序
    sort(range, past, greater<int>());
    cout << "Sorted in descending order\n";
    copy(range, past, ostream_iterator<int>(cout, " "));
    cout << '\n';

    // 查找值为7的范围(降序排序)
    pi = equal_range(range, past, 7, greater<int>());
    cout << "Lower bound for 7: ";
    copy(pi.first, past, ostream_iterator<int>(cout, " "));
    cout << '\n';
    cout << "Upper bound for 7: ";
    copy(pi.second, past, ostream_iterator<int>(cout, " "));
    cout << '\n';
}
// 输出:
// Lower bound for 6: 7
// Upper bound for 6: 7
// Lower bound for 7: 7 7 9 9 9
// Upper bound for 7: 9 9 9
// Sorted in descending order
// 9 9 9 7 7 5 3 1
// Lower bound for 7: 7 7 5 3 1
// Upper bound for 7: 5 3 1

解释

  • equal_range:
    • 第一个原型:
      • 返回一个 pair,其第一个元素是 lower_bound 的返回值(第一个不小于给定值的元素),第二个元素是 upper_bound 的返回值(第一个大于给定值的元素)。适用于元素按 operator< 排序的序列。
    • 第二个原型:
      • 使用自定义的 comp 函数对象来排序,返回一个 pair,其第一个元素是 lower_bound 的返回值,第二个元素是 upper_bound 的返回值。适用于元素按自定义排序准则排序的序列。

这段代码演示了如何使用 equal_range 算法来查找数组中的值范围,并展示了如何按降序排序数组以及查找排序后的范围。以下是对代码的详细解释:

代码解释

#include <algorithm>
#include <functional>
#include <iostream>
#include <iterator>
using namespace std;

int main() {
    int range[] = {1, 3, 5, 7, 7, 9, 9, 9};
    auto past = end(range);
    pair<int *, int *> pi;
  • 头文件 <algorithm> 提供了 equal_rangesort 等算法。
  • <functional> 提供了 greater 比较器。
  • <iostream> 用于输入输出。
  • <iterator> 提供了 ostream_iterator 用于将数据写入输出流。
  • range 是一个整数数组,包含重复的值。
  • past 是一个指向数组末尾的迭代器。
    // 查找值为6的范围
    pi = equal_range(range, past, 6);
    cout << "Lower bound for 6: " << *pi.first << "\n"
         << "Upper bound for 6: " << *pi.second << '\n';
  • equal_range(range, past, 6) 查找值 6 在数组中的范围。由于数组没有 6lower_boundupper_bound 都会返回指向 7 的迭代器,即 6 的插入位置。
  • 输出 6 的下界和上界:6 的下界是第一个大于等于 6 的元素(7),上界是第一个大于 6 的元素(7)。
    // 查找值为7的范围
    pi = equal_range(range, past, 7);
    cout << "Lower bound for 7: ";
    copy(pi.first, past, ostream_iterator<int>(cout, " "));
    cout << '\n';
    cout << "Upper bound for 7: ";
    copy(pi.second, past, ostream_iterator<int>(cout, " "));
    cout << '\n';
  • equal_range(range, past, 7) 查找值 7 在数组中的范围。返回值是一个 pair,包含两个迭代器:
    • pi.first 是第一个等于 7 的位置。
    • pi.second 是第一个大于 7 的位置。
  • copy 函数用于将范围内的元素输出到 cout,以便显示从 7 的下界到数组末尾的所有元素,以及从 7 的上界到数组末尾的所有元素。
    // 按降序排序
    sort(range, past, greater<int>());
    cout << "Sorted in descending order\n";
    copy(range, past, ostream_iterator<int>(cout, " "));
    cout << '\n';
  • sort(range, past, greater<int>()) 将数组 range 按降序排序。
  • copy 函数输出排序后的数组元素。
    // 查找值为7的范围(降序排序)
    pi = equal_range(range, past, 7, greater<int>());
    cout << "Lower bound for 7: ";
    copy(pi.first, past, ostream_iterator<int>(cout, " "));
    cout << '\n';
    cout << "Upper bound for 7: ";
    copy(pi.second, past, ostream_iterator<int>(cout, " "));
    cout << '\n';
}
  • equal_range(range, past, 7, greater<int>()) 查找值 7 在降序排序数组中的范围。由于排序方式不同,查找的下界和上界也会有所不同。
  • 输出 7 在降序排序数组中的下界和上界。

输出解释

  1. 查找值为 6 的范围:

    • Lower bound for 6: 7
    • Upper bound for 6: 7
    • 6 不存在于数组中,lower_boundupper_bound 都指向第一个 7
  2. 查找值为 7 的范围:

    • Lower bound for 7: 7 7 9 9 9
    • Upper bound for 7: 9 9 9
    • 7 存在于数组中,lower_bound 指向第一个 7upper_bound 指向第一个 9
  3. 降序排序后的数组:

    • Sorted in descending order: 9 9 9 7 7 5 3 1
  4. 降序排序后的值为 7 的范围:

    • Lower bound for 7: 7 7 5 3 1
    • Upper bound for 7: 5 3 1
    • 在降序排序的数组中,7 的下界是 77 的上界是比 7 小的第一个元素。

exchange

exchange 是 C++ 标准库中的一个函数,定义在 <utility> 头文件中。它的作用是将一个对象的值替换为新的值,并返回原来的值。以下是该函数的详细说明及示例代码的翻译:

函数原型

Type exchange(Type &object1, ValueType &&newValue);

功能描述

  • newValue:新的值,它将被赋值给 object1
  • object1:被替换值的对象。
  • 函数会将 object1 的旧值返回,并将 newValue 赋给 object1

示例代码

#include <utility>
#include <iostream>
using namespace std;

int main(int argc, char **argv)
{
    bool more = argc > 5;
    cout << "more than 5: " << exchange(more, argc > 2) <<
            ", more than 2: " << more << '\n';
}

代码解释

  1. bool more = argc > 5;:根据命令行参数的数量,初始化 more 变量。如果参数数量大于 5,则 moretrue,否则为 false

  2. exchange(more, argc > 2)

    • exchange 函数将 more 的值替换为 argc > 2 的结果。
    • argc > 2 是一个布尔表达式,根据命令行参数数量判断其值为 truefalse
    • exchange 函数会返回 more 原来的值,同时将 more 的值更新为 argc > 2 的结果。
  3. cout << "more than 5: " << exchange(more, argc > 2) << ", more than 2: " << more << '\n';

    • 打印 exchange 函数的返回值(more 原来的值)以及 more 的新值。

输出示例

假设程序运行命令为 a.out one two three,这时 argc 的值为 4。

  • argc > 5 结果是 false(即 0),所以 more 初始化为 false
  • argc > 2 结果是 true(即 1)。

调用 exchange(more, true) 时,more 的原值(false)被返回并输出,同时 more 被更新为 true

输出结果:

more than 5: 0, more than 2: 1
  • more than 5: 0:表示 more 变量在被更新之前的值。
  • more than 2: 1:表示 more 变量在被更新之后的值。

fillfill_n

在这里插入图片描述

fillfill_n 是 C++ 标准库中的算法函数,定义在 <algorithm> 头文件中。它们用于将指定的值填充到容器或范围中。

函数原型

void fill([ExecPol,] ForwardIterator first, ForwardIterator last, Type const &value);
void fill_n([ExecPol,] ForwardIterator first, Size n, Type const &value);

功能描述

  • fill

    • [first, last) 范围内的所有元素初始化为指定的 value,并覆盖之前存储的值。
  • fill_n

    • first 指向的元素开始,将 n 个元素初始化为指定的 value,并覆盖之前存储的值。

示例代码

#include <algorithm>
#include <vector>
#include <iterator>
#include <iostream>
using namespace std;

int main()
{
    vector<int> iv(8);
    fill(iv.begin(), iv.end(), 8);
    copy(iv.begin(), iv.end(), ostream_iterator<int>(cout, " "));
    cout << '\n';

    fill_n(iv.begin() + 2, 4, 4);
    copy(iv.begin(), iv.end(), ostream_iterator<int>(cout, " "));
    cout << '\n';
}

代码解释

  1. vector<int> iv(8);

    • 创建一个包含 8 个 int 类型元素的 vector,初始值为默认值(通常是 0)。
  2. fill(iv.begin(), iv.end(), 8);

    • 使用 fill 函数将 iv 中的所有元素都设置为 8。
  3. copy(iv.begin(), iv.end(), ostream_iterator<int>(cout, " "));

    • 使用 copy 函数将 iv 中的元素输出到标准输出流 cout,并用空格分隔。
  4. fill_n(iv.begin() + 2, 4, 4);

    • 使用 fill_n 函数从 iv 的第三个元素(iv.begin() + 2)开始,将接下来的 4 个元素设置为 4。
  5. copy(iv.begin(), iv.end(), ostream_iterator<int>(cout, " "));

    • 再次使用 copy 函数输出 iv 中的元素。

输出结果

8 8 8 8 8 8 8 8
8 8 4 4 4 4 8 8
  • 第一行:fill 函数将所有元素设置为 8。
  • 第二行:fill_n 函数将从第 3 个元素开始的 4 个元素设置为 4。

findfind_iffind_if_not

在这里插入图片描述

findfind_iffind_if_not 是 C++ 标准库中的算法函数,定义在 <algorithm> 头文件中。这些函数用于在给定的范围内查找元素。

函数原型

在这里插入图片描述

InputIterator find([ExecPol,] InputIterator first, InputIterator last, Type const &value);
InputIterator find_if([ExecPol,] InputIterator first, InputIterator last, Predicate pred);
InputIterator find_if_not([ExecPol,] InputIterator first, InputIterator last, Predicate pred);

功能描述

  • find

    • [first, last) 范围内查找与 value 相等的元素。
    • 如果找到了匹配的元素,返回指向该元素的迭代器;否则返回 last
    • 比较使用 Type::operator==
  • find_if

    • [first, last) 范围内查找第一个使得 pred 返回 true 的元素。
    • 如果找到了符合条件的元素,返回指向该元素的迭代器;否则返回 last
    • pred 是一个一元谓词(即接受一个参数的函数)。
  • find_if_not

    • [first, last) 范围内查找第一个使得 pred 返回 false 的元素。
    • 如果找到了符合条件的元素,返回指向该元素的迭代器;否则返回 last
    • pred 是一个一元谓词。

示例代码

#include <algorithm>
#include <string>
#include <cstring>
#include <iterator>
#include <iostream>
using namespace std;

class CaseName
{
    std::string d_string;
public:
    CaseName(char const *str) : d_string(str) {}
    bool operator()(std::string const &element) const
    {
        return strcasecmp(element.c_str(), d_string.c_str()) == 0;
    }
};

void show(string const *begin, string const *end)
{
    if (begin == end)
        cout << "No elements were found";
    else
        copy(begin, end, ostream_iterator<string>{ cout, " " });
    cout << '\n';
}

int main()
{
    string sarr[] = {
        "Alpha", "Bravo", "Charley", "Delta", "Echo"
    };
    auto past = end(sarr);

    // 查找值为 "Delta" 的元素
    show(find(sarr, past, "Delta"), past);

    // 查找值为 "India" 的元素
    show(find(sarr, past, "India"), past);

    // 使用 CaseName 类查找值为 "Charley" 的元素(忽略大小写)
    show(find_if(sarr, sarr + size(sarr), CaseName{ "charley" }), past);

    // 查找值为 "India" 的元素(忽略大小写),如果未找到则输出提示信息
    if (find_if(sarr, sarr + size(sarr), CaseName{ "india" }) == past)
        cout << "`india' was not found in the range\n";

    // 查找第一个不为 "Alpha" 的元素
    show(find_if_not(sarr, sarr + size(sarr), CaseName{ "alpha" }), past);
}

代码解释

  1. class CaseName

    • 定义了一个类,用于忽略大小写地比较字符串。
  2. find 函数:

    • 查找 sarr 中的元素 “Delta” 和 “India”。如果找不到元素,则显示 “No elements were found”。
  3. find_if 函数:

    • 使用 CaseName 类来忽略大小写地查找字符串 “Charley” 和 “India”。如果找不到 “India”,则输出 “`india’ was not found in the range”。
  4. find_if_not 函数:

    • 查找第一个不等于 “Alpha” 的元素。

输出结果

Delta Echo
No elements were found
Charley Delta Echo
`india' was not found in the range
Bravo Charley Delta Echo

find_end

在这里插入图片描述

find_end 是 C++ 标准库中的一个算法函数,用于在一个序列中查找另一个子序列的最后出现位置。它定义在 <algorithm> 头文件中。

函数原型

ForwardIterator1 find_end([ExecPol,] ForwardIterator1 first1, ForwardIterator1 last1, ForwardIterator2 first2, ForwardIterator2 last2);
ForwardIterator1 find_end([ExecPol,] ForwardIterator1 first1, ForwardIterator1 last1, ForwardIterator2 first2, ForwardIterator2 last2, BinaryPredicate pred);

功能描述

  • 第一个原型

    • [first1, last1) 范围内查找子序列 [first2, last2) 的最后一次出现。
    • 如果找到,则返回指向匹配子序列第一个元素的迭代器;如果未找到,则返回 last1
    • 比较使用 Type::operator==
  • 第二个原型

    • [first1, last1) 范围内查找子序列 [first2, last2) 的最后一次出现。
    • 如果找到,则返回指向匹配子序列第一个元素的迭代器;如果未找到,则返回 last1
    • 使用提供的二元谓词 pred 比较子序列中的元素。

示例代码

#include <algorithm>
#include <string>
#include <iterator>
#include <iostream>
using namespace std;

bool twice(size_t first, size_t second)
{
    return first == (second << 1);
}

int main()
{
    string sarr[] = {
        "alpha", "bravo", "charley", "delta", "echo",
        "foxtrot", "golf", "hotel",
        "foxtrot", "golf", "hotel",
        "india", "juliet", "kilo"
    };
    string search[] = {
        "foxtrot", "golf", "hotel"
    };
    auto past = end(sarr);

    // 查找子序列 ["foxtrot", "golf", "hotel"] 在 sarr 中最后出现的位置
    copy(
        find_end(sarr, past, search, search + 3),
        past, ostream_iterator<string>{ cout, " " }
    );
    cout << '\n';

    size_t range[] = { 2, 4, 6, 8, 10, 4, 6, 8, 10 };
    size_t nrs[] = { 2, 3, 4 };

    // 查找范围 [2, 4, 6, 8, 10] 中最后出现的值是 nrs 中值的两倍的子序列
    copy(
        find_end(range, range + 9, nrs, nrs + 3, twice),
        range + 9, ostream_iterator<size_t>{ cout, " " }
    );
    cout << '\n';
}

代码解释

  1. find_end 的使用

    • 查找字符串数组 sarr 中最后一次出现的子序列 search
    • 查找的结果是从第二个 “foxtrot” 开始的子序列。
  2. twice 函数

    • 用于检查 range 数组中是否存在 nrs 中的值的两倍。
  3. 输出结果

    • 第一个输出是 "foxtrot golf hotel india juliet kilo",表示子序列 ["foxtrot", "golf", "hotel"]sarr 中的最后一次出现位置。
    • 第二个输出是 "4 6 8 10",表示在 range 数组中最后一次出现的符合条件的子序列,其中 nrs 中的每个值都被 range 中的元素的两倍所匹配。

输出结果

foxtrot golf hotel india juliet kilo
4 6 8 10

find_first_of

在这里插入图片描述

find_first_of 是 C++ 标准库中的一个算法函数,用于在一个序列中查找另一个子序列中的第一个匹配元素。它定义在 <algorithm> 头文件中。

函数原型

ForwardIterator1 find_first_of([ExecPol,] ForwardIterator1 first1, ForwardIterator1 last1, ForwardIterator2 first2, ForwardIterator2 last2);
ForwardIterator1 find_first_of([ExecPol,] ForwardIterator1 first1, ForwardIterator1 last1, ForwardIterator2 first2, ForwardIterator2 last2, BinaryPredicate pred);

功能描述

  • 第一个原型

    • [first1, last1) 范围内查找第一个与 [first2, last2) 范围中的元素匹配的元素。
    • 如果找到匹配的元素,则返回指向该元素的迭代器;如果没有找到,则返回 last1
    • 比较使用 Type::operator==
  • 第二个原型

    • [first1, last1) 范围内查找第一个与 [first2, last2) 范围中的元素匹配的元素。
    • 使用提供的二元谓词 pred 比较每个元素。
    • 返回指向第一个匹配元素的迭代器;如果没有找到匹配,则返回 last1

示例代码

#include <algorithm>
#include <string>
#include <iterator>
#include <iostream>
using namespace std;

bool twice(size_t first, size_t second)
{
    return first == (second << 1);
}

int main()
{
    string sarr[] = {
        "alpha", "bravo", "charley", "delta", "echo",
        "foxtrot", "golf", "hotel",
        "foxtrot", "golf", "hotel",
        "india", "juliet", "kilo"
    };
    string search[] = {
        "foxtrot", "golf", "hotel"
    };
    auto past = end(sarr);

    // 查找子序列 ["foxtrot", "golf", "hotel"] 中的第一个元素在 sarr 中出现的位置
    copy(
        find_first_of(sarr, past, search, search + 3),
        past, ostream_iterator<string>{ cout, " " }
    );
    cout << '\n';

    size_t range[] = { 2, 4, 6, 8, 10, 4, 6, 8, 10 };
    size_t nrs[] = { 2, 3, 4 };

    // 查找 range 中第一个等于 nrs 中某个值的两倍的元素
    copy(
        find_first_of(range, range + 9, nrs, nrs + 3, twice),
        range + 9, ostream_iterator<size_t>{ cout, " " }
    );
    cout << '\n';
}

代码解释

  1. find_first_of 的使用

    • 查找字符串数组 sarr 中第一个与子序列 search 中的元素匹配的元素。
    • 结果是第一个 “foxtrot”,后续的元素也被打印出来,因为 find_first_of 返回的是匹配元素及其后的所有元素。
  2. twice 函数

    • 用于检查 range 数组中的元素是否等于 nrs 中值的两倍。
  3. 输出结果

    • 第一个输出 "foxtrot golf hotel foxtrot golf hotel india juliet kilo",表示子序列 search 中的第一个元素 “foxtrot” 在 sarr 中的位置及其后的所有元素。
    • 第二个输出 "4 6 8 10 4 6 8 10",表示 range 数组中第一个等于 nrs 中某个值的两倍的元素及其后的所有元素。

输出结果

foxtrot golf hotel foxtrot golf hotel india juliet kilo
4 6 8 10 4 6 8 10

for_each

在这里插入图片描述

for_each 是 C++ 标准库中的一个算法函数,用于对一个序列中的每个元素执行指定的操作。它定义在 <algorithm> 头文件中。

函数原型

Function for_each([ExecPol,] ForwardIterator first, ForwardIterator last, Function func);

功能描述

  • for_each[first, last) 范围内的每个元素依次调用指定的函数对象 func(或函数)。函数对象 func 可以修改传入的元素(因为使用的是前向迭代器)。
  • 如果需要对元素进行转换,可以使用 transform 算法。
  • for_each 函数或其副本会被返回。与基于范围的 for 循环类似,但 for_each 算法也可以用于子范围和逆向迭代器。

示例代码

示例 1

#include <algorithm>
#include <string>
#include <iostream>
#include <cctype>
using namespace std;

// 修改字符为小写
void lowerCase(char &ch) {
    ch = tolower(static_cast<unsigned char>(ch));
}

// 打印字符串的首字母大写,其余字母小写
void capitalizedOutput(string const &str) {
    char *tmp = new char[str.size() + 1]();
    str.copy(tmp, str.size());
    for_each(tmp + 1, tmp + str.size(), lowerCase);
    tmp[0] = toupper(*tmp);
    cout << tmp << ' ';
    delete[] tmp;
}

int main() {
    string sarr[] = {
        "alpha", "BRAVO", "charley", "DELTA",
        "echo", "FOXTROT", "golf", "HOTEL"
    };
    for_each(sarr, end(sarr), capitalizedOutput);
    cout << "that's all, folks" << '\n';
}

输出结果

Alpha Bravo Charley Delta Echo Foxtrot Golf Hotel That's all, folks

示例 2

#include <algorithm>
#include <string>
#include <iostream>
#include <cctype>
using namespace std;

// 修改字符为小写
void lowerCase(char &c) {
    c = tolower(static_cast<unsigned char>(c));
}

// 函数对象,用于处理字符串
class Show {
    int d_count;
public:
    Show() : d_count(0) {}

    void operator()(std::string &str) {
        std::for_each(str.begin(), str.end(), lowerCase);
        str[0] = toupper(str[0]); // 假设 str 不为空
        std::cout << ++d_count << " " << str << "; ";
    }

    int count() const {
        return d_count;
    }
};

int main() {
    string sarr[] = {
        "alpha", "BRAVO", "charley", "DELTA", "echo",
        "FOXTROT", "golf", "HOTEL"
    };
    string *last = sarr + sizeof(sarr) / sizeof(string);
    cout << for_each(sarr, last, Show{}).count() << '\n';
}

输出结果

1 Alpha; 2 Bravo; 3 Charley; 4 Delta; 5 Echo; 6 Foxtrot; 7 Golf; 8 Hotel; 8

代码解释

  1. for_each 的使用

    • 第一个示例
      • 对字符串数组 sarr 中的每个字符串进行处理,转换为首字母大写,其余字母小写,然后打印出来。
    • 第二个示例
      • 使用函数对象 Show 对字符串数组中的每个字符串进行处理,将每个字符串的首字母大写,并统计处理的字符串数量,最后打印处理的字符串数量。
  2. for_eachtransform 的区别

    • for_each 用于对每个元素执行操作,而 transform 用于将每个元素转换成新值并存储在另一个范围中。
  3. 注意事项

    • for_each 不能直接在成员函数中使用(例如,修改自身对象)。可以使用 lambda 表达式或封装类来解决这个问题。

generategenerate_n

在这里插入图片描述

generategenerate_n 是 C++ 标准库中的算法,用于生成元素填充一个范围。它们定义在 <algorithm> 头文件中。

函数原型

void generate([ExecPol,] ForwardIterator first, ForwardIterator last, Generator generator);
void generate_n([ExecPol,] ForwardIterator first, Size n, Generator generator);

功能描述

  • generate:

    • generator 生成的值初始化 [first, last) 范围内的所有元素。generator 可以是一个函数或函数对象,其 operator() 不接受任何参数。
  • generate_n:

    • generator 生成的值初始化从迭代器 first 开始的 n 个元素。

示例代码

#include <algorithm>
#include <vector>
#include <iterator>
#include <iostream>
using namespace std;

// 生成自然数的平方
class NaturalSquares {
    size_t d_newsqr;  // 最新的平方
    size_t d_last;    // 最新的自然数

public:
    NaturalSquares() : d_newsqr(0), d_last(0) {}

    size_t operator()() {
        // 使用: (a + 1)^2 == a^2 + 2*a + 1
        return d_newsqr += (d_last++ << 1) + 1;
    }
};

int main() {
    vector<size_t> uv(10);

    // 生成自然数的平方
    generate(uv.begin(), uv.end(), NaturalSquares{});
    copy(uv.begin(), uv.end(), ostream_iterator<int>{ cout, " " });
    cout << '\n';

    uv = vector<size_t>(10);

    // 生成前5个自然数的平方
    generate_n(uv.begin(), 5, NaturalSquares{});
    copy(uv.begin(), uv.end(), ostream_iterator<int>{ cout, " " });
    cout << '\n';
}

输出结果

1 4 9 16 25 36 49 64 81 100
1 4 9 16 25 0 0 0 0 0

代码解释

  1. NaturalSquares

    • 用于生成自然数的平方。
    • 通过累积方式计算平方值:(a + 1)^2 = a^2 + 2*a + 1,从而得到下一个平方值。
  2. generate 使用示例

    • generate(uv.begin(), uv.end(), NaturalSquares{})NaturalSquares 生成前 10 个自然数的平方,并将其填充到 uv 向量中。
  3. generate_n 使用示例

    • generate_n(uv.begin(), 5, NaturalSquares{})NaturalSquares 生成前 5 个自然数的平方,并将其填充到 uv 向量的前 5 个元素中,剩余元素保持为 0。

总结

  • generate 用于初始化一个范围内的所有元素。
  • generate_n 用于初始化从指定迭代器开始的 n 个元素。

generate: 根据生成器动态生成值。每次调用生成器的 operator() 时,返回不同的值。
fill: 用固定值填充所有元素。

includes

在这里插入图片描述

includes 是 C++ 标准库中的一个算法,用于检查一个已排序的范围是否包含另一个已排序的范围。下面是对 includes 函数的详细翻译和解释:

  • 头文件: <algorithm>

  • 函数原型:

    • bool includes([ExecPol,] InputIterator1 first1, InputIterator1 last1, InputIterator2 first2, InputIterator2 last2);
    • bool includes([ExecPol,] InputIterator1 first1, InputIterator1 last1, InputIterator2 first2, InputIterator2 last2, Compare comp);
  • 描述:

    • 第一个原型:两个范围 [first1, last1)[first2, last2) 的元素都应该已经按 operator< 排序。该函数返回 true 如果第二个范围 [first2, last2) 中的每个元素都在第一个范围 [first1, last1) 中存在(即第二个范围是第一个范围的子集)。
    • 第二个原型:两个范围 [first1, last1)[first2, last2) 的元素都应该已经按照 comp 函数对象排序。该函数返回 true 如果第二个范围 [first2, last2) 中的每个元素都在第一个范围 [first1, last1) 中存在(即第二个范围是第一个范围的子集),其中 comp 用于比较元素。

示例代码

#include <algorithm>
#include <string>
#include <cstring>
#include <iostream>
using namespace std;

bool caseString(string const &first, string const &second)
{
    return strcasecmp(first.c_str(), second.c_str()) == 0;
}

int main()
{
    string first1[] = {
        "alpha", "bravo", "charley", "delta",
        "echo", "foxtrot", "golf", "hotel"
    };
    auto past1 = end(first1);

    string first2[] = {
        "Alpha", "bravo", "Charley", "delta",
        "Echo", "foxtrot", "Golf", "hotel"
    };
    auto past2 = end(first2);

    string second[] = { "charley", "foxtrot", "hotel" };

    cout << "The elements of `second' are " <<
    (includes(first1, past1, second, second + 3) ? "" : "not") <<
    " contained in the first sequence:\n"
    "second is a subset of first1\n";

    cout << "The elements of `first1' are " <<
    (includes(second, second + 3, first1, past1) ? "" : "not") <<
    " contained in the second sequence\n";

    cout << "The elements of `second' are " <<
    (includes(first2, past2, second, second + 3) ? "" : "not") <<
    " contained in the first2 sequence\n";

    cout << "Using case-insensitive comparison,\n"
    "the elements of `second' are " <<
    (includes(first2, past2, second, second + 3, caseString) ?
    "" : "not") <<
    " contained in the first2 sequence\n";

    return 0;
}

显示结果

The elements of `second' are contained in the first sequence:
second is a subset of first1
The elements of `first1' are not contained in the second sequence
The elements of `second' are not contained in the first2 sequence
Using case-insensitive comparison,
the elements of `second' are contained in the first2 sequence

说明

  1. includes 函数用于判断一个已排序范围是否包含另一个已排序范围的所有元素。它要求两个范围都已经排序,并且可以使用默认的 < 比较或自定义的比较函数。
  2. 第一个原型 使用默认的比较方式 (operator<)。
  3. 第二个原型 允许使用自定义的比较函数,如 caseString 进行大小写不敏感的比较。

在上述代码中:

  • secondfirst1 的子集,因此 includes(first1, past1, second, second + 3) 返回 true
  • first1 不是 second 的子集,因此 includes(second, second + 3, first1, past1) 返回 false
  • second 不是 first2 的子集,因此 includes(first2, past2, second, second + 3) 返回 false
  • 使用 caseString 进行不区分大小写的比较时,second 被正确地识别为 first2 的子集,因此 includes(first2, past2, second, second + 3, caseString) 返回 true

inclusive_scan

在这里插入图片描述

inclusive_scan 是 C++17 标准库中新增的算法,用于对范围内的元素进行前缀和计算。与 exclusive_scan 不同,inclusive_scan 的结果包含当前元素本身的计算结果。

头文件:

#include <numeric>

函数原型:

// 使用默认的加法运算符进行扫描
template<class InputIt, class OutputIt>
OutputIt inclusive_scan(InputIt first, InputIt last, OutputIt d_first);

// 使用自定义的二元操作符进行扫描
template<class InputIt, class OutputIt, class BinaryOp>
OutputIt inclusive_scan(InputIt first, InputIt last, OutputIt d_first, BinaryOp binary_op);

// 使用自定义的二元操作符和初始值进行扫描
template<class InputIt, class OutputIt, class BinaryOp, class T>
OutputIt inclusive_scan(InputIt first, InputIt last, OutputIt d_first, BinaryOp binary_op, T init);

描述:

  • 第一个版本: 对输入范围 [first, last) 进行前缀和计算,并将结果存储在 d_first 开始的输出范围中,使用默认的加法运算符 +
  • 第二个版本: 使用自定义的二元操作符 binary_op 进行前缀和计算。
  • 第三个版本: 使用自定义的二元操作符 binary_op 和初始值 init 进行前缀和计算,init 作为前缀和计算的初始值。

示例:

#include <iostream>
#include <vector>
#include <numeric>  // 包含 inclusive_scan

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    std::vector<int> result(vec.size());

    // 使用默认的加法运算符进行前缀和计算
    std::inclusive_scan(vec.begin(), vec.end(), result.begin());

    std::cout << "使用默认加法的结果: ";
    for (int v : result) {
        std::cout << v << " ";
    }
    std::cout << std::endl;  // 输出: 1 3 6 10 15

    // 使用乘法进行前缀和计算
    std::inclusive_scan(vec.begin(), vec.end(), result.begin(), std::multiplies<>());

    std::cout << "使用乘法的结果: ";
    for (int v : result) {
        std::cout << v << " ";
    }
    std::cout << std::endl;  // 输出: 1 2 6 24 120

    // 使用乘法和初始值 10 进行前缀和计算
    std::inclusive_scan(vec.begin(), vec.end(), result.begin(), std::multiplies<>(), 10);

    std::cout << "使用乘法和初始值10的结果: ";
    for (int v : result) {
        std::cout << v << " ";
    }
    std::cout << std::endl;  // 输出: 10 20 60 240 1200

    return 0;
}

输出结果:

使用默认加法的结果: 1 3 6 10 15
使用乘法的结果: 1 2 6 24 120
使用乘法和初始值10的结果: 10 20 60 240 1200

总结:

  • inclusive_scan 计算范围内的前缀和,并包括当前元素本身。
  • 该函数允许使用自定义的二元操作符进行计算,也可以指定初始值。
  • 它适用于需要逐步累积结果的情形,例如在处理序列求和、求积或其他累加操作时。

inclusive_scan 是一个强大的工具,尤其在并行计算和大数据处理领域非常有用。

inner_product

在这里插入图片描述

inner_product 是 C++ 标准库中的一个算法,用于计算两个序列元素的乘积和,然后加上一个初始值。这个函数可以通过自定义的操作符来进行加法和乘法操作。以下是对 inner_product 函数的详细翻译和解释:

  • 头文件: <numeric>

  • 函数原型:

    • Type inner_product(InputIterator1 first1, InputIterator1 last1, InputIterator2 first2, Type init);
    • Type inner_product(InputIterator1 first1, InputIterator1 last1, InputIterator2 first2, Type init, BinaryOperator1 op1, BinaryOperator2 op2);
  • 描述:

    • 第一个原型:计算范围 [first1, last1) 中所有元素与从 first2 开始的相同数量的元素的乘积之和,并将结果加到 init 上。该函数使用 operator+operator* 进行加法和乘法操作。
    • 第二个原型:使用二元操作符 op1 代替默认的加法操作符,使用 op2 代替默认的乘法操作符,对范围 [first1, last1) 中的元素和从 first2 开始的相同数量的元素进行操作。将结果加到 init 上,并返回 init 的最终值。

示例代码

#include <numeric>
#include <algorithm>
#include <iterator>
#include <iostream>
#include <string>
using namespace std;

class Cat
{
    std::string d_sep;
public:
    Cat(string const &sep)
    : d_sep(sep)
    {}

    string operator()(string const &s1, string const &s2) const
    {
        return s1 + d_sep + s2;
    }
};

int main()
{
    size_t ia1[] = { 1, 2, 3, 4, 5, 6, 7 };
    cout << "The sum of all squares in ";
    copy(ia1, ia1 + 7, ostream_iterator<size_t>{ cout, " " });
    cout << "is " << inner_product(ia1, ia1 + 7, ia1, 0) << '\n';

    size_t ia2[] = { 7, 6, 5, 4, 3, 2, 1 };
    cout << "The sum of all cross-products in ";
    copy(ia1, ia1 + 7, ostream_iterator<size_t>{ cout, " " });
    cout << "and ";
    copy(ia2, ia2 + 7, ostream_iterator<size_t>{ cout, " " });
    cout << "is " << inner_product(ia1, ia1 + 7, ia2, 0) << '\n';

    string names1[] = { "Frank", "Karel", "Piet" };
    string names2[] = { "Brokken", "Kubat", "Plomp" };
    cout << "All combined names of ";
    copy(names1, names1 + 3, ostream_iterator<string>{ cout, " " });
    cout << "and\n";
    copy(names2, names2 + 3, ostream_iterator<string>{ cout, " " });
    cout << "are:" <<
    inner_product(names1, names1 + 3, names2, string{ "\t" },
    Cat{ "\n\t"}, Cat{ " " }) << '\n';

    return 0;
}

显示结果

The sum of all squares in 1 2 3 4 5 6 7 is 140
The sum of all cross-products in 1 2 3 4 5 6 7 and 7 6 5 4 3 2 1 is 84
All combined names of Frank Karel Piet and Brokken Kubat Plomp are:
 Frank Brokken
 Karel Kubat
 Piet Plomp

说明

  1. inner_product 函数用于计算两个范围内所有对应元素乘积的和,并加上一个初始值。它的行为类似于内积运算。
  2. 第一个原型 使用默认的加法和乘法操作进行计算。
  3. 第二个原型 允许使用自定义的二元操作符来替代默认的加法和乘法操作。

在上述代码中:

  • inner_product(ia1, ia1 + 7, ia1, 0) 计算 ia1 中每个元素的平方和,结果是 140。
  • inner_product(ia1, ia1 + 7, ia2, 0) 计算 ia1ia2 对应元素的乘积之和,结果是 84。
  • inner_product(names1, names1 + 3, names2, string{ "\t" }, Cat{ "\n\t"}, Cat{ " " }) 使用 Cat 函数对象将两个字符串数组元素合并,结果是按指定分隔符连接的字符串。

inplace_merge 是 C++ 标准库中的一个算法,用于合并两个已排序的区间,并将结果直接存储在原来的内存空间中。以下是对 inplace_merge 函数的详细翻译和解释:

inplace_merge

在这里插入图片描述

  • 头文件: <algorithm>

  • 函数原型:

    • void inplace_merge([ExecPol,] BidirectionalIterator first, BidirectionalIterator middle, BidirectionalIterator last);
    • void inplace_merge([ExecPol,] BidirectionalIterator first, BidirectionalIterator middle, BidirectionalIterator last, Compare comp);
  • 描述:

    • 第一个原型:将两个已排序的区间 [first, middle)[middle, last) 合并为一个有序区间,使用数据类型的 operator< 进行比较。合并后的结果存储在 [first, last) 区间内。
    • 第二个原型:将两个已排序的区间 [first, middle)[middle, last) 合并为一个有序区间,使用二元比较操作符 comp 的布尔结果进行比较。合并后的结果存储在 [first, last) 区间内。

示例代码

#include <algorithm>
#include <string>
#include <cstring>
#include <iterator>
#include <iostream>
using namespace std;

bool caseString(string const &first, string const &second)
{
    return strcasecmp(first.c_str(), second.c_str()) == 0;
}

int main()
{
    string range[] =
    {
        "alpha", "charley", "echo", "golf",
        "bravo", "delta", "foxtrot",
    };
    inplace_merge(range, range + 4, range + 7);
    copy(range, range + 7, ostream_iterator<string>{ cout, " " });
    cout << '\n';

    string range2[] =
    {
        "ALPHA", "CHARLEY", "DELTA", "foxtrot", "hotel",
        "bravo", "ECHO", "GOLF"
    };
    inplace_merge(range2, range2 + 5, range2 + 8, caseString);
    copy(range2, range2 + 8, ostream_iterator<string>{ cout, " " });
    cout << '\n';

    return 0;
}

显示结果

alpha bravo charley delta echo foxtrot golf
ALPHA bravo CHARLEY DELTA ECHO foxtrot GOLF hotel

说明

  1. inplace_merge(range, range + 4, range + 7) 将数组 range 的前四个元素(“alpha”, “charley”, “echo”, “golf”)和后三个元素(“bravo”, “delta”, “foxtrot”)合并成一个有序的区间,合并结果直接存储在 range 原来的位置中。最终输出的顺序为 alpha bravo charley delta echo foxtrot golf

  2. inplace_merge(range2, range2 + 5, range2 + 8, caseString) 使用自定义的字符串比较函数 caseString 合并两个已排序的区间,结果同样存储在 range2 原来的位置中。输出结果为 ALPHA bravo CHARLEY DELTA ECHO foxtrot GOLF hotel

这个函数特别适用于在不需要额外内存的情况下将两个相邻的排序区间合并为一个有序区间。

iota

在这里插入图片描述

iota 是 C++ 标准库中的一个函数,用于填充指定范围内的元素,使它们形成一个递增的序列。这个函数的名字来源于希腊字母 iota,表示“最小的”。

头文件:

#include <numeric>

函数原型:

void iota(ForwardIterator first, ForwardIterator last, Type value);

描述:

  • iota 函数会从 value 开始,依次对 [first, last) 范围内的元素赋值,使这些元素形成一个递增的序列。*first 会被赋予 value*(first + 1) 会被赋予 value + 1,依此类推。

示例:

#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
#include <numeric>

using namespace std;

int main()
{
    vector<size_t> uv(10);  // 创建一个大小为10的向量
    iota(uv.begin(), uv.end(), 0);  // 用从0开始的递增序列填充向量
    copy(uv.begin(), uv.end(), ostream_iterator<int>{ cout, " " });  // 输出向量中的元素
    cout << '\n';
    return 0;
}

输出结果:

0 1 2 3 4 5 6 7 8 9

总结:

  • iota 函数用于快速生成递增序列,并将其填充到指定的范围内。
  • 它的使用非常简单,但在初始化或重置序列数据时非常有效。

is_heap

在这里插入图片描述

is_heap 是 C++ 标准库中的一个算法,用于检查给定范围内的元素是否满足堆的属性。堆是一种特殊的二叉树结构,其中每个节点的值都大于或等于其子节点的值(最大堆),或者每个节点的值都小于或等于其子节点的值(最小堆)。

头文件:

#include <algorithm>

函数原型:

bool is_heap(RandomAccessIterator first, RandomAccessIterator last);
bool is_heap(RandomAccessIterator first, RandomAccessIterator last, Compare comp);

描述:

  • 第一个原型:检查 [first, last) 范围内的元素是否满足最大堆的属性。使用默认的比较操作 operator<
  • 第二个原型:检查 [first, last) 范围内的元素是否满足给定比较函数 comp 所定义的堆属性。该比较函数定义了堆的顺序,比如是否为最小堆或最大堆。

示例:

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

int main()
{
    vector<int> vec = {9, 7, 5, 3, 1};  // 一个可能的最大堆
    if (is_heap(vec.begin(), vec.end())) {
        cout << "vec 是一个最大堆" << endl;
    } else {
        cout << "vec 不是一个最大堆" << endl;
    }

    vector<int> vec2 = {1, 3, 5, 7, 9};  // 一个可能的最小堆
    if (is_heap(vec2.begin(), vec2.end(), greater<int>())) {
        cout << "vec2 是一个最小堆" << endl;
    } else {
        cout << "vec2 不是一个最小堆" << endl;
    }

    return 0;
}

输出结果:

vec 是一个最大堆
vec2 是一个最小堆

总结:

  • is_heap 是一个简单有效的工具,用于检查给定范围内的元素是否满足堆的属性。
  • 可以通过自定义比较函数来检查是否满足特定的堆属性(如最小堆或最大堆)。

is_heap_until

在这里插入图片描述

is_heap_until 是 C++ 标准库中的一个算法,用于找到给定范围内不再满足堆属性的第一个元素位置。换句话说,它返回一个迭代器,指向从范围的开始到该迭代器位置的元素都满足堆属性的最后一个元素的下一个位置。

头文件:

#include <algorithm>

函数原型:

RandomAccessIterator is_heap_until(RandomAccessIterator first, RandomAccessIterator last);
RandomAccessIterator is_heap_until(RandomAccessIterator first, RandomAccessIterator last, Compare comp);

描述:

  • 第一个原型:检查 [first, last) 范围内的元素,使用默认的比较操作 operator<,并返回第一个不再满足最大堆属性的位置的迭代器。如果整个范围都是一个堆,则返回 last
  • 第二个原型:使用自定义的比较函数 comp 检查 [first, last) 范围内的元素,并返回第一个不再满足该比较函数定义的堆属性的位置的迭代器。

示例:

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

int main()
{
    vector<int> vec = {9, 7, 5, 3, 4, 1};  // 部分元素满足最大堆
    auto it = is_heap_until(vec.begin(), vec.end());

    cout << "vec 中第一个不满足最大堆属性的元素是: " << *it << endl;
    cout << "迭代器 it 之前的元素是最大堆: ";
    for(auto i = vec.begin(); i != it; ++i)
        cout << *i << " ";
    cout << endl;

    return 0;
}

输出结果:

vec 中第一个不满足最大堆属性的元素是: 4
迭代器 it 之前的元素是最大堆: 9 7 5 3

总结:

  • is_heap_until 提供了一种便捷的方法来查找给定范围内不再满足堆属性的位置。如果需要在非完整堆的数据结构中找到最大的堆子结构,可以使用该算法。
  • is_heap 类似,is_heap_until 也可以通过传递自定义的比较函数来检查特定类型的堆属性(如最小堆)。

is_partitioned

在这里插入图片描述

is_partitioned 是 C++ 标准库中的一个算法,用于检查给定范围内的元素是否根据特定的谓词函数进行了分区。分区的定义是:范围内的所有元素在谓词返回 true 之前都满足该谓词,之后的元素都不再满足谓词。

头文件:

#include <algorithm>

函数原型:

bool is_partitioned([ExecPol,] InputIterator first, InputIterator last, UnaryPred pred);

描述:

  • 返回值:如果范围 [first, last) 中的所有元素按照谓词 pred 进行分区,则返回 true。即谓词 pred 对范围中的元素依次应用,首先返回 true,直到某个元素后开始返回 false,后续所有元素也应返回 false。如果范围为空,则也返回 true
  • 空范围:如果范围为空,也返回 true

示例:

#include <algorithm>
#include <iterator>
#include <vector>
#include <iostream>

using namespace std;

bool pCheck(int value)
{
    return value < 3;
}

int main()
{
    vector<int> uv = { 1, -2, 3, -4, 5, -6, 7, -8, 9};

    cout << "Partitioned before: " <<
        is_partitioned(uv.begin(), uv.end(), pCheck) << "\n";

    cout << "First value returning 'false' when partitioning: " <<
        *partition(uv.begin(), uv.end(), pCheck) << '\n';

    copy(uv.begin(), uv.end(), ostream_iterator<int>{ cout, " " });
    cout << '\n';

    cout << "Partitioned after: " <<
        is_partitioned(uv.begin(), uv.end(), pCheck) << '\n';
}

输出结果:

Partitioned before: 0
First value returning 'false' when partitioning: 3
1 -2 -4 -6 -8 3 5 7 9 
Partitioned after: 1

总结:

  • is_partitioned 用于检查序列是否已根据某个谓词函数分区。分区意味着前部分的所有元素都满足谓词,而后部分的所有元素都不满足。
  • 在示例中,原始序列没有被分区,但在 partition 函数调用后,序列被分区,此时 is_partitioned 返回 true

is_permutation

在这里插入图片描述

is_permutation 是 C++ 标准库中的一个算法,用于检查两个范围的元素是否是彼此的排列组合。即判断一个范围的元素是否能通过重新排列得到另一个范围的元素。

头文件:

#include <algorithm>

函数原型:

bool is_permutation(ForwardIterator first1, ForwardIterator last1, ForwardIterator first2);
bool is_permutation(ForwardIterator first1, ForwardIterator last1, ForwardIterator first2, ForwardIterator last2);
bool is_permutation(ForwardIterator first1, ForwardIterator last1, ForwardIterator first2, BinaryPred pred);
bool is_permutation(ForwardIterator first1, ForwardIterator last1, ForwardIterator first2, ForwardIterator last2, BinaryPred pred);

描述:

  • 第一个原型:检查范围 [first1, last1) 的元素是否是范围 [first2, last2) 元素的一个排列组合。即两个范围的大小相同,且一个范围的元素可以通过重新排列得到另一个范围的元素。
  • 第二个原型:与第一个原型类似,但提供了第二个范围的结束迭代器 last2,用于指定第二个范围的大小。
  • 第三个原型:与第一个原型类似,但使用 pred 判断两个元素是否相等,pred 是一个二元谓词函数。
  • 第四个原型:与第二个原型类似,但使用 pred 判断两个元素是否相等。

示例:

#include <algorithm>
#include <iostream>

using namespace std;

int main()
{
    int one[] = { 1, -2, 3, -4, 5, -6, 7, -8, 9 };
    int two[] = { -8, -2, -4, -6, 3, 1, 5, 9, 7 };
    int three[] = { -8, -8, -4, -6, 3, 1, 5, 9, 7 };

    cout << "one is a permutation of two: " <<
        is_permutation(one, end(one), two) << "\n"
        "one is a permutation of three: " <<
        is_permutation(one, end(one), three, end(three)) << '\n';
}

输出结果:

one is a permutation of two: 1
one is a permutation of three: 0

总结:

  • is_permutation 用于判断两个范围的元素是否互为排列组合,即一个范围的元素是否可以通过重新排列得到另一个范围的元素。
  • 在示例中,onetwo 的元素是排列组合关系,因此返回 true (1)。而 onethree 的元素不是排列组合关系,因此返回 false (0)。

is_sorted

在这里插入图片描述

is_sorted 是 C++ 标准库中的一个算法,用于检查指定范围内的元素是否已经按升序排序。

头文件:

#include <algorithm>

函数原型:

bool is_sorted(ForwardIterator first, ForwardIterator last);
bool is_sorted(ForwardIterator first, ForwardIterator last, BinaryPredicate pred);

描述:

  • 第一个原型:检查范围 [first, last) 内的元素是否按照 operator< 进行排序。如果是升序排列,则返回 true,否则返回 false
  • 第二个原型:检查范围 [first, last) 内的元素是否按照二元谓词 pred 进行排序。pred 是一个二元谓词函数,用于比较两个元素是否满足特定的排序条件。如果元素按照 pred 排序,则返回 true,否则返回 false

示例:

#include <algorithm>
#include <vector>
#include <iostream>

using namespace std;

int main()
{
    vector<int> uv = { 1, -2, 3, -4, 5, -6, 7, -8, 9 };

    cout << "sorted before: " << is_sorted(uv.begin(), uv.end()) << '\n';

    sort(uv.begin(), uv.end());

    cout << "sorted after " << is_sorted(uv.begin(), uv.end()) << '\n';
}

输出结果:

sorted before: 0
sorted after 1

总结:

  • is_sorted 用于检查一个范围的元素是否已经按升序排列。
  • 在示例中,uv 的初始状态不是有序的,因此 is_sorted 返回 false (0)。经过 sort 排序后,uv 成为了升序排列,is_sorted 返回 true (1)。

is_sorted_until

在这里插入图片描述

is_sorted_until 是 C++ 标准库中的一个算法,用于查找范围内从起始位置开始的最长已排序子范围的结束迭代器。

头文件:

#include <algorithm>

函数原型:

ForwardIterator is_sorted_until(ForwardIterator first, ForwardIterator last);
ForwardIterator is_sorted_until(ForwardIterator first, ForwardIterator last, BinaryPredicate pred);

描述:

  • 第一个原型:返回一个迭代器,指向范围 [first, last) 中从 first 开始的最长升序子范围的结束位置。元素的比较是基于 operator<
  • 第二个原型:返回一个迭代器,指向范围 [first, last) 中从 first 开始的最长子范围的结束位置,这个子范围是基于二元谓词 pred 进行排序的。

示例:

#include <algorithm>
#include <vector>
#include <iostream>

using namespace std;

int main()
{
    vector<int> uv = { 1, -2, 3, -4, 5, -6, 7, -8, 9 };
    
    auto it = is_sorted_until(uv.begin(), uv.end());
    cout << "First unsorted element is at index: " << distance(uv.begin(), it) << '\n';
    
    sort(uv.begin(), uv.end());

    it = is_sorted_until(uv.begin(), uv.end());
    cout << "First unsorted element is at index: " << distance(uv.begin(), it) << '\n';
}

输出结果:

First unsorted element is at index: 2
First unsorted element is at index: 9

总结:

  • is_sorted_until 用于查找范围内从起始位置开始的最长已排序子范围的结束位置。
  • 在示例中,uv 的初始状态中从索引 0 开始的已排序子范围到达索引 2 之前不是有序的,因此返回的迭代器指向索引 2。在排序后,整个范围都成为升序排列,因此 is_sorted_until 返回的迭代器指向范围的末尾。

iter_swap

在这里插入图片描述

iter_swap 是 C++ 标准库中的一个算法,用于交换两个迭代器所指向的元素。

头文件:

#include <algorithm>

函数原型:

void iter_swap(ForwardIterator1 iter1, ForwardIterator2 iter2);

描述:

  • 将由 iter1iter2 指向的元素进行交换。

示例:

#include <algorithm>
#include <iterator>
#include <iostream>
#include <string>

using namespace std;

int main()
{
    string first[] = { "alpha", "bravo", "charley" };
    string second[] = { "echo", "foxtrot", "golf" };

    cout << "Before:\n";
    copy(first, first + 3, ostream_iterator<string>(cout, " "));
    cout << '\n';
    copy(second, second + 3, ostream_iterator<string>(cout, " "));
    cout << '\n';

    for (size_t idx = 0; idx != 3; ++idx)
        iter_swap(first + idx, second + idx);

    cout << "After:\n";
    copy(first, first + 3, ostream_iterator<string>(cout, " "));
    cout << '\n';
    copy(second, second + 3, ostream_iterator<string>(cout, " "));
    cout << '\n';
}

输出结果:

Before:
alpha bravo charley 
echo foxtrot golf 
After:
echo foxtrot golf 
alpha bravo charley 

总结:

  • iter_swap 用于交换两个迭代器所指向的元素。
  • 在示例中,firstsecond 数组中的元素在 iter_swap 调用之后被互换。

lexicographical_compare

在这里插入图片描述

lexicographical_compare 是 C++ 标准库中的一个算法,用于按字典顺序比较两个序列。

头文件:

#include <algorithm>

函数原型:

bool lexicographical_compare(InputIterator1 first1, InputIterator1 last1, 
                             InputIterator2 first2, InputIterator2 last2);

bool lexicographical_compare(InputIterator1 first1, InputIterator1 last1, 
                             InputIterator2 first2, InputIterator2 last2, 
                             Compare comp);

描述:

  • 第一个原型:比较两个迭代器范围 [first1, last1)[first2, last2) 所指向的元素,返回 true 如果第一个范围按字典顺序在第二个范围之前。具体比较方法如下:

    • 当第一个范围的某个元素小于第二个范围的对应元素(使用 < 运算符),返回 true
    • 如果第一个范围的元素全部比第二个范围的对应元素小,但第一个范围已经结束,而第二个范围还未结束,返回 true
    • 否则,返回 false,表示第一个序列不是字典序小于第二个序列。
  • 第二个原型:使用自定义的二元比较操作 comp 代替默认的 < 运算符来进行比较。

示例:

#include <algorithm>
#include <iterator>
#include <iostream>
#include <string>
#include <cstring>

using namespace std;

bool caseString(string const &first, string const &second)
{
    return strcasecmp(first.c_str(), second.c_str()) < 0;
}

void compare(string const &word1, string const &word2)
{
    cout << '`' << word1 << "' is " <<
        (lexicographical_compare(word1.begin(), word1.end(),
                                 word2.begin(), word2.end()) ?
         "before `" : "beyond or at `") <<
        word2 << "' in the alphabet\n";
}

int main()
{
    string word1 = "hello";
    string word2 = "help";
    compare(word1, word2);
    compare(word1, word1);
    compare(word2, word1);

    string one[] = {"alpha", "bravo", "charley"};
    string two[] = {"ALPHA", "BRAVO", "DELTA"};
    
    copy(one, one + 3, ostream_iterator<string>{cout, " "});
    cout << " is ordered " <<
        (lexicographical_compare(one, one + 3, two, two + 3, caseString) ?
         "before " : "beyond or at ") <<
        "using case-insensitive comparisons.\n";
    
    copy(two, two + 3, ostream_iterator<string>{cout, " "});
    cout << "\n";
}

输出结果:

`hello' is before `help' in the alphabet
`hello' is beyond or at `hello' in the alphabet
`help' is beyond or at `hello' in the alphabet
alpha bravo charley is ordered before ALPHA BRAVO DELTA
using case-insensitive comparisons.

总结:

  • lexicographical_compare 用于按字典顺序比较两个序列,确定第一个序列是否在第二个序列之前。
  • 在示例中,lexicographical_compare 被用来比较两个字符串和两个字符串数组,以演示字典序的比较。

lower_bound

在这里插入图片描述

lower_bound 是 C++ 标准库中的一个算法,用于在排序序列中查找第一个不小于(即大于或等于)指定值的位置。

头文件:

#include <algorithm>

函数原型:

ForwardIterator lower_bound(ForwardIterator first, ForwardIterator last, const Type &value);

ForwardIterator lower_bound(ForwardIterator first, ForwardIterator last, const Type &value, BinaryPredicate pred);

描述:

  • 第一个原型:在迭代器范围 [first, last) 中,找到第一个不小于 value 的元素的位置。这里的排序是基于默认的升序排列(使用 < 运算符)。返回的迭代器表示可以在这个位置插入 value 而不会破坏序列的排序。如果没有找到这样的元素,返回 last

  • 第二个原型:使用自定义的二元谓词 pred 代替默认的 < 运算符来进行比较。谓词 pred 用于比较范围中的元素和 value,返回第一个使得 pred 返回 false 的元素的位置。如果没有找到这样的元素,返回 last。谓词的第一个参数是范围中的元素,第二个参数是 value

示例:

#include <algorithm>
#include <iostream>
#include <iterator>
#include <vector>
#include <functional>

using namespace std;

int main()
{
    int ia[] = {10, 20, 30};
    cout << "Sequence: ";
    copy(ia, ia + 3, ostream_iterator<int>(cout, " "));
    cout << "\n15 can be inserted before " <<
        *lower_bound(ia, ia + 3, 15) << "\n" <<
        "35 can be inserted after " <<
        (lower_bound(ia, ia + 3, 35) == ia + 3 ? "the last element" : "???") << '\n';

    cout << "Sequence: ";
    copy(ia, ia + 3, ostream_iterator<int>(cout, " "));
    cout << "\n15 can be inserted before " <<
        *lower_bound(ia, ia + 3, 15, less<int>()) << "\n" <<
        "35 can be inserted before " <<
        (lower_bound(ia, ia + 3, 35, less<int>()) == ia ? "the first element " : "???") << '\n';

    vector<int> array{5, 10, 20, 20, 20, 30};
    auto iter = lower_bound(array.begin(), array.end(), 20,
        [&](int &arrayEl, int value)
        {
            cout << "Comparing " << arrayEl <<
                " (index: " << (&arrayEl - &array[0]) << ")" <<
                " to " << value << '\n';
            return arrayEl < value;
        }
    );
    cout << "New 20 to insert at idx " << (iter - array.begin()) << '\n';
}

输出结果:

Sequence: 10 20 30
15 can be inserted before 20
35 can be inserted after the last element
Sequence: 10 20 30
15 can be inserted before 20
35 can be inserted before ???
Comparing 20 (index: 3) to 20
Comparing 10 (index: 1) to 20
Comparing 20 (index: 2) to 20
New 20 to insert at idx 2

总结:

  • lower_bound 用于在已排序的序列中查找第一个不小于指定值的位置。
  • 在示例中,lower_bound 用于查找可以插入特定值的位置,演示了如何使用默认比较和自定义比较。

make_heap

在这里插入图片描述

make_heap 是 C++ 标准库中的一个算法,用于将一个元素范围重新排列成堆(heap)结构。堆是一种特殊的二叉树结构,可以是最大堆或最小堆。

头文件:

#include <algorithm>

函数原型:

template <class RandomAccessIterator>
void make_heap(RandomAccessIterator first, RandomAccessIterator last);

template <class RandomAccessIterator, class Compare>
void make_heap(RandomAccessIterator first, RandomAccessIterator last, Compare comp);

描述:

  • 第一个原型:将迭代器范围 [first, last) 内的元素重新排列成一个最大堆(即堆顶元素最大)。使用默认的比较操作 operator< 来决定堆的顺序。

  • 第二个原型:将迭代器范围 [first, last) 内的元素重新排列成一个堆(最小堆或最大堆),由自定义比较函数 comp 决定堆的顺序。比较函数 comp 应该满足堆的性质。

示例:

#include <algorithm>
#include <iostream>
#include <vector>

using namespace std;

int main()
{
    vector<int> data = {10, 20, 30, 5, 15};

    // 将数据重新排列成最大堆
    make_heap(data.begin(), data.end());

    cout << "最大堆:";
    for (int x : data)
        cout << x << " ";
    cout << '\n';

    // 使用自定义比较函数将数据重新排列成最小堆
    make_heap(data.begin(), data.end(), greater<int>());

    cout << "最小堆:";
    for (int x : data)
        cout << x << " ";
    cout << '\n';
}

输出结果:

最大堆:30 20 10 5 15 
最小堆:5 10 15 20 30 

总结:

  • make_heap 将给定范围的元素重新排列成堆的结构(最大堆或最小堆)。
  • 默认情况下,make_heap 创建的是最大堆。可以通过提供自定义比较函数来创建最小堆。
  • 堆的根元素是堆中最大或最小的元素。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值