C++ 秘籍:问题解决方法(三)
六、继承
C++ 允许你以多种方式构建复杂的软件应用程序。其中最常见的是面向对象编程(OOP)范式。C++ 中的类用于为包含数据的对象以及可以对该数据执行的操作提供蓝图。
继承通过让您构造复杂的类层次结构而更进一步。C++ 语言提供了各种不同的特性,您可以使用这些特性以逻辑方式组织代码。
食谱 6-1。从类继承
问题
您正在编写一个程序,它在对象之间有一种自然的 is-a 关系,并且希望减少代码重复。
解决办法
从父类继承类允许您将代码添加到父类中,并在多个派生类型之间共享它。
它是如何工作的
在 C++ 中,你可以从一个类继承另一个类。继承类获得基类的所有属性。清单 6-1 显示了一个从共享父类继承的两个类的例子。
清单 6-1 。类继承
#include <cinttypes>
#include <iostream>
using namespace std;
class Vehicle
{
private:
uint32_t m_NumberOfWheels{};
public:
Vehicle(uint32_t numberOfWheels)
: m_NumberOfWheels{ numberOfWheels }
{
}
uint32_t GetNumberOfWheels() const
{
return m_NumberOfWheels;
}
};
class Car : public Vehicle
{
public:
Car()
: Vehicle(4)
{
}
};
class Motorcycle : public Vehicle
{
public:
Motorcycle()
: Vehicle(2)
{
}
};
int main(int argc, char* argv[])
{
Car myCar{};
cout << "A car has " << myCar.GetNumberOfWheels() << " wheels." << endl;
Motorcycle myMotorcycle;
cout << "A motorcycle has " << myMotorcycle.GetNumberOfWheels() << " wheels." << endl;
return 0;
}
Vehicle类包含一个成员变量来存储车辆的车轮数量。默认情况下,该值初始化为 0,或者在构造函数中设置。Vehicle后面是另一个名为Car的类。Car类只包含一个用于调用Vehicle构造函数的构造函数。Car构造函数将数字 4 传递给Vehicle构造函数,因此将m_NumberOfWheels设置为 4。
Motorcycle类也只包含一个构造函数,但是它将 2 传递给了Vehicle构造函数。因为Car和Motorcycle都继承自Vehicle类,所以它们都继承了它的属性。它们都包含一个保存车轮数量的变量,并且都有一个检索车轮数量的方法。您可以在main函数中看到这一点,其中GetNumberOfWheels在myCar对象和myMotorcycle对象上都被调用。图 6-1 显示了这段代码生成的输出。
图 6-1 。由清单 6-1 中的代码生成的输出
Car类和Motorcycle类都继承了Vehicle的属性,并且都在它们的构造函数中设置了适当的轮数。
食谱 6-2。控制对派生类中成员变量和方法的访问
问题
您的派生类需要能够访问其父类中的字段。
解决办法
C++ 访问修饰符对在派生类中访问变量的方式有影响。使用正确的访问修饰符是正确构造类层次结构的关键。
它是如何工作的
公共访问说明符
public访问说明符 授予对类中变量或方法的公共访问权。这同样适用于成员变量和方法。你可以在清单 6-2 中清楚地看到这一点。
清单 6-2 。访问说明符
#include <cinttypes>
#include <iostream>
using namespace std;
class Vehicle
{
public:
uint32_t m_NumberOfWheels{};
Vehicle() = default;
};
class Car : public Vehicle
{
public:
Car()
{
m_NumberOfWheels = 4;
}
};
class Motorcycle : public Vehicle
{
public:
Motorcycle()
{
m_NumberOfWheels = 2;
}
};
int main(int argc, char* argv[])
{
Car myCar{};
cout << "A car has " << myCar.m_NumberOfWheels << " wheels." << endl;
myCar.m_NumberOfWheels = 3;
cout << "A car has " << myCar.m_NumberOfWheels << " wheels." << endl;
Motorcycle myMotorcycle;
cout << "A motorcycle has " << myMotorcycle.m_NumberOfWheels << " wheels." << endl;
myMotorcycle.m_NumberOfWheels = 3;
cout << "A motorcycle has " << myMotorcycle.m_NumberOfWheels << " wheels." << endl;
return 0;
}
任何具有public访问权限的变量都可以被派生类访问。Car构造器和Motorcycle构造器都利用了这一点,并适当地设置了它们拥有的轮数。缺点是其他代码也可以访问公共成员变量。你可以在main函数中看到这一点,其中m_NumberOfWheels被读取并分配给myCar对象和myMotorcycle对象。图 6-2 显示了该代码生成的输出。
图 6-2 。清单 6-2 生成的输出
私有访问说明符
您可以将变量设为私有并为其提供公共访问器,而不是将其设为公共。清单 6-3 显示了私有成员变量的使用。
清单 6-3 。private访问说明符
#include <cinttypes>
#include <iostream>
using namespace std;
class Vehicle
{
private:
uint32_t m_NumberOfWheels{};
public:
Vehicle(uint32_t numberOfWheels)
: m_NumberOfWheels{ numberOfWheels }
{
}
uint32_t GetNumberOfWheels() const
{
return m_NumberOfWheels;
}
};
class Car : public Vehicle
{
public:
Car()
: Vehicle(4)
{
}
};
class Motorcycle : public Vehicle
{
public:
Motorcycle()
: Vehicle(2)
{
}
};
int main(int argc, char* argv[])
{
Car myCar{};
cout << "A car has " << myCar.GetNumberOfWheels() << " wheels." << endl;
Motorcycle myMotorcycle;
cout << "A motorcycle has " << myMotorcycle.GetNumberOfWheels() << " wheels." << endl;
return 0;
}
清单 6-3 显示了private访问说明符与m_NumberOfWheels变量的使用。Car和Motorcycle类不再能直接访问m_NumberOfWheels变量;因此,Vehicle类提供了一种通过其构造函数初始化变量的方法。这使得类更难处理,但是增加了不允许任何外部代码直接访问成员变量的好处。您可以在main函数中看到这一点,其中的代码必须通过GetNumberOfWheels访问器方法获得车轮的数量。
受保护的访问说明符
protected访问说明符允许混合使用public和private访问说明符。对于从当前类派生的类,它就像一个public说明符,对于外部代码,它就像一个private说明符。清单 6-4 展示了这种行为。
清单 6-4 。protected访问说明符
#include <cinttypes>
#include <iostream>
using namespace std;
class Vehicle
{
protected:
uint32_t m_NumberOfWheels{};
public:
Vehicle() = default;
uint32_t GetNumberOfWheels() const
{
return m_NumberOfWheels;
}
};
class Car : public Vehicle
{
public:
Car()
{
m_NumberOfWheels = 4;
}
};
class Motorcycle : public Vehicle
{
public:
Motorcycle()
{
m_NumberOfWheels = 2;
}
};
int main(int argc, char* argv[])
{
Car myCar{};
cout << "A car has " << myCar.GetNumberOfWheels() << " wheels." << endl;
Motorcycle myMotorcycle;
cout << "A motorcycle has " << myMotorcycle.GetNumberOfWheels() << " wheels." << endl;
return 0;
}
清单 6-4 显示了Car和Motorcycle都可以直接从它们的父类Vehicle中访问m_NumberOfWheels变量。这两个类都在它们的构造函数中设置了m_NumberOfWheels变量。main函数中的调用代码不能访问这个变量,因此必须调用GetNumberOfWheels方法才能打印这个值。
食谱 6-3。隐藏派生类中的方法
问题
您有一个派生类,它需要一个不同于父类提供的行为的方法中的行为。
解决办法
C++ 允许您通过在派生类中定义一个具有相同签名的方法来隐藏父类中的方法。
它是如何工作的
通过在基类中定义具有完全相同签名的方法,可以隐藏父类中的方法。此示例显示派生类如何使用显式方法隐藏来提供不同于父类的功能。当你使用继承时,这是一个需要理解的关键概念,因为它是用来区分类类型层次的主要方法。
清单 6-5 包含一个Vehicle类、一个Car类和一个Motorcycle类。Vehicle类定义了一个名为GetNumberOfWheels的方法,该方法返回 0。在Car类和Motorcycle类中定义了相同的方法;这些版本的方法分别返回 4 和 2。
清单 6-5 。隐藏方法
#include <cinttypes>
#include <iostream>
using namespace std;
class Vehicle
{
public:
Vehicle() = default;
uint32_t GetNumberOfWheels() const
{
return 0;
}
};
class Car : public Vehicle
{
public:
Car() = default;
uint32_t GetNumberOfWheels() const
{
return 4;
}
};
class Motorcycle : public Vehicle
{
public:
Motorcycle() = default;
uint32_t GetNumberOfWheels() const
{
return 2;
}
};
int main(int argc, char* argv[])
{
Vehicle myVehicle{};
cout << "A vehicle has " << myVehicle.GetNumberOfWheels() << " wheels." << endl;
Car myCar{};
cout << "A car has " << myCar.GetNumberOfWheels() << " wheels." << endl;
Motorcycle myMotorcycle;
cout << "A motorcycle has " << myMotorcycle.GetNumberOfWheels() << " wheels." << endl;
return 0;
}
清单 6-5 中的main函数调用GetNumberOfWheels的三个不同版本,并为每个版本返回适当的值。您可以在图 6-3 中看到这段代码生成的输出。
图 6-3 。执行清单 6-5 中的代码生成的输出
通过对象或指向这些类类型的指针直接访问这些方法会产生正确的输出。
注意当你使用多态时,方法隐藏不能正常工作。通过指向基类的指针访问派生类会导致基类上的方法被调用。这很少是你想要的行为。使用多态性时的正确解决方案见配方 8-5。
食谱 6-4。使用多态基类
问题
您希望编写泛型代码,它使用指向基类的指针,并且仍然调用派生类中的正确方法。
解决办法
virtual关键字 允许你创建可以被派生类覆盖的方法。
它是如何工作的
关键字virtual告诉 C++ 编译器你希望一个类包含一个虚拟方法表(v-table)。v-table 包含对方法的查找,允许为给定类型调用正确的方法,即使对象是通过指向其父类之一的指针来访问的。清单 6-6 显示了一个类层次结构,它使用virtual关键字来指定一个方法应该包含在类的 v 表中。
清单 6-6 。创建虚拟方法
#include <cinttypes>
class Vehicle
{
public:
Vehicle() = default;
virtual uint32_t GetNumberOfWheels() const
{
return 2;
}
};
class Car : public Vehicle
{
public:
Car() = default;
uint32_t GetNumberOfWheels() const override
{
return 4;
}
};
class Motorcycle : public Vehicle
{
public:
Motorcycle() = default;
};
清单 6-6 中的Car和Motorcycle类是从Vehicle类派生而来的。Vehicle类中的GetNumberOfWheels方法被列为虚拟方法。这使得通过指针对该方法的任何调用都将通过 v 表来调用。清单 6-7 显示了一个完整的例子,其中的main函数通过一个Vehicle指针访问对象。
清单 6-7 。通过基指针访问虚方法
#include <cinttypes>
#include <iostream>
using namespace std;
class Vehicle
{
public:
Vehicle() = default;
virtual uint32_t GetNumberOfWheels() const
{
return 2;
}
};
class Car : public Vehicle
{
public:
Car() = default;
uint32_t GetNumberOfWheels() const override
{
return 4;
}
};
class Motorcycle : public Vehicle
{
public:
Motorcycle() = default;
};
int main(int argc, char* argv[])
{
Vehicle* pVehicle{};
Vehicle myVehicle{};
pVehicle = &myVehicle;
cout << "A vehicle has " << pVehicle->GetNumberOfWheels() << " wheels." << endl;
Car myCar{};
pVehicle = &myCar;
cout << "A car has " << pVehicle->GetNumberOfWheels() << " wheels." << endl;
Motorcycle myMotorcycle;
pVehicle = &myMotorcycle;
cout << "A motorcycle has " << pVehicle->GetNumberOfWheels() << " wheels." << endl;
return 0;
}
main函数在第一行定义了一个指向Vehicle对象的指针。然后这个指针被用在每个cout语句中来访问当前对象的GetNumberOfWheels方法。Vehicle和Motorcycle对象在它们的 v 表中有Vehicle::GetNumberOfWheels方法的地址;因此,两者都为它们的轮数返回 2。
Car类覆盖了GetNumberOfWheels方法。这使得Car用Car::GetNumberOfWheels的地址替换查找表中Vehicle::GetNumberOfWheels的地址。因此,当同一个Vehicle指针被分配了myCar的地址并随后调用GetNumberOfWheels时,它调用的是Car类中定义的方法,而不是Vehicle类中定义的方法。图 6-4 显示了清单 6-7 中的代码生成的输出,你可以看到情况就是这样。
图 6-4 。执行清单 6-7 中的代码生成的输出
override关键字用在Car类中GetNumberOfWheels方法签名的末尾。该关键字是对编译器的一个提示,即您希望此方法重写父类中的虚方法。如果您输入的签名不正确,或者您正在重写的方法的签名后来被更改,编译器将会引发错误。这个特性非常有用,我推荐你使用它(虽然override关键字本身是可选的)。
食谱 6-5。防止方法重写
问题
您有一个不想被派生类重写的方法。
解决办法
你可以使用关键字final来防止类覆盖方法 。
它是如何工作的
关键字通知编译器你不希望一个虚方法被派生类覆盖。清单 6-8 展示了一个使用final关键字的例子。
清单 6-8 。使用final关键字
#include <cinttypes>
#include <iostream>
using namespace std;
class Vehicle
{
public:
Vehicle() = default;
virtual uint32_t GetNumberOfWheels() const final
{
return 2;
}
};
class Car : public Vehicle
{
public:
Car() = default;
uint32_t GetNumberOfWheels() const override
{
return 4;
}
};
class Motorcycle : public Vehicle
{
public:
Motorcycle() = default;
};
int main(int argc, char* argv[])
{
Vehicle* pVehicle{};
Vehicle myVehicle{};
pVehicle = &myVehicle;
cout << "A vehicle has " << pVehicle->GetNumberOfWheels() << " wheels." << endl;
Car myCar{};
pVehicle = &myCar;
cout << "A car has " << pVehicle->GetNumberOfWheels() << " wheels." << endl;
Motorcycle myMotorcycle;
pVehicle = &myMotorcycle;
cout << "A motorcycle has " << pVehicle->GetNumberOfWheels() << " wheels." << endl;
return 0;
}
Vehicle类中的GetNumberOfWheels方法使用final关键字来防止派生类试图重写它。这导致清单 6-8 中的代码无法编译,因为Car类试图覆盖GetNumberOfWheels。您可以注释掉此方法来编译代码。
关键字final也可以在一个更长的链中停止方法的进一步重写。清单 6-9 展示了这是如何实现的。
清单 6-9 。防止继承层次结构中的重写
#include <cinttypes>
class Vehicle
{
public:
Vehicle() = default;
virtual uint32_t GetNumberOfWheels() const
{
return 2;
}
};
class Car : public Vehicle
{
public:
Car() = default;
uint32_t GetNumberOfWheels() const final
{
return 4;
}
};
class Ferrari : public Car
{
public:
Ferrari() = default;
uint32_t GetNumberOfWheels() const override
{
return 5;
}
};
Vehicle定义了一个名为GetNumberOfWheels的虚拟方法,该方法返回值 2。Car覆盖这个方法返回 4(这个例子忽略了不是所有的汽车都有四个轮子的事实)并声明这个方法是最终的。不允许从Car派生的其他类覆盖相同的方法。如果需求只需要支持四轮汽车,这对应用程序来说是有意义的。当编译器到达任何从Car派生的类或者从任何其他层次结构中有Car的类派生的类并且试图覆盖GetNumberOfWheels方法时,它将抛出一个错误。
食谱 6-6。创建界面
问题
您有一个基类方法,它不应该定义任何行为,而应该简单地被派生类重写。
解决办法
您可以在 C++ 中创建不定义方法体的纯虚拟方法。
它是如何工作的
你可以在 C++ 中通过在方法签名的末尾添加= 0来定义纯虚方法。清单 6-10 显示了一个例子。
清单 6-10 。创建纯虚拟方法
#include <cinttypes>
#include <iostream>
using namespace std;
class Vehicle
{
public:
Vehicle() = default;
virtual uint32_t GetNumberOfWheels() const = 0;
};
class Car : public Vehicle
{
public:
Car() = default;
uint32_t GetNumberOfWheels() const override
{
return 4;
}
};
class Motorcycle : public Vehicle
{
public:
Motorcycle() = default;
uint32_t GetNumberOfWheels() const override
{
return 2;
}
};
int main(int argc, char* argv[])
{
Vehicle* pVehicle{};
Car myCar{};
pVehicle = &myCar;
cout << "A car has " << pVehicle->GetNumberOfWheels() << " wheels." << endl;
Motorcycle myMotorcycle;
pVehicle = &myMotorcycle;
cout << "A motorcycle has " << pVehicle->GetNumberOfWheels() << " wheels." << endl;
return 0;
}
Vehicle类将GetNumberOfWheels定义为一个纯虚拟方法。这就确保了Vehicle类型的对象永远不会被创建。编译器不允许这样做,因为它没有一个方法来调用GetNumberOfWheels. Car和Motorcycle都覆盖了这个方法并且可以被实例化。您可以在main功能中看到这种情况。图 6-5 显示这些方法返回了Car和Motorcycle的正确值。
图 6-5 。执行清单 6-10 中的代码生成的输出
包含纯虚拟方法的类被称为接口。如果一个类从一个接口继承,并且您希望能够实例化该类,您必须重写父类中的任何纯虚方法。可以从一个接口派生而不覆盖这些方法,但是这个派生类只能作为进一步派生类的接口。
食谱 6-7。多重继承
问题
您有一个希望从多个父类派生的类。
解决办法
C++ 支持多重继承 。
它是如何工作的
在 C++ 中,可以使用逗号分隔的父类列表从多个父类中派生出一个类。清单 6-11 展示了这是如何实现的。
清单 6-11 。多重继承
#include <cinttypes>
#include <iostream>
using namespace std;
class Printable
{
public:
virtual void Print() = 0;
};
class Vehicle
{
public:
Vehicle() = default;
virtual uint32_t GetNumberOfWheels() const = 0;
};
class Car
: public Vehicle
, public Printable
{
public:
Car() = default;
uint32_t GetNumberOfWheels() const override
{
return 4;
}
void Print() override
{
cout << "A car has " << GetNumberOfWheels() << " wheels." << endl;
}
};
class Motorcycle
: public Vehicle
, public Printable
{
public:
Motorcycle() = default;
uint32_t GetNumberOfWheels() const override
{
return 2;
}
void Print() override
{
cout << "A motorcycle has " << GetNumberOfWheels() << " wheels." << endl;
}
};
int main(int argc, char* argv[])
{
Printable* pPrintable{};
Car myCar{};
pPrintable = &myCar;
pPrintable->Print();
Motorcycle myMotorcycle;
pPrintable = &myMotorcycle;
pPrintable->Print();
return 0;
}
Car和Motorcycle类都来自多个父类。这些类现在都是Vehicle和Printable的。你可以在被覆盖的Print方法中看到两个父类之间的相互作用。这些方法都调用了Car和Motorcycle中被覆盖的GetNumberOfWheels方法。main函数通过指向Printable对象的指针访问被覆盖的Print方法,使用多态调用正确的Print方法以及Print中正确的GetNumberOfWheels方法。图 6-6 显示程序输出正确。
图 6-6 。显示多重继承与多态性一起工作的输出
七、STL 容器
标准模板库(STL) 由一组要求实现者支持的标准功能组成。创建标准可以确保代码可以在不同的平台和操作系统上互换使用,只要所提供的实现符合该标准。该标准的很大一部分定义了一组可用于存储数据结构的容器。本章着眼于不同的场景,每个 STL 容器都被证明是有用的。
注第三章的中提到了字符串容器。
配方 7-1。存储固定数量的对象
问题
您需要在程序中存储固定数量的对象。
解决办法
C++ 提供了可用于此目的的内置数组,然而 STL 数组提供了比其他 STL 容器更灵活的接口。
它是如何工作的
C++ 支持自语言形成以来就存在的内置数组。如果你以前用过 C 或 C++ 编程,这些对你来说会很熟悉。清单 7-1 显示了一个标准的 C 风格数组。
清单 7-1 。一个 C 风格的数组
#include <cinttypes>
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
const uint32_t numberOfElements{ 5 };
int32_t normalArray[numberOfElements]{ 10, 65, 3000, 2, 49 };
for (uint32_t i{ 0 }; i < numberOfElements; ++i)
{
cout << normalArray[i] << endl;
}
return 0;
}
这段代码展示了 C++ 中 C 风格数组的用法。数组包含 5 个整数,main函数有一个for循环,用于迭代数组并打印出每个位置的值。也可以使用基于范围的for循环来迭代 C 风格的数组。清单 7-2 展示了这是如何做到的。
清单 7-2 。对 C 样式数组使用基于范围的 for 循环
#include <cinttypes>
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
const uint32_t numberOfElements{ 5 };
int32_t normalArray[numberOfElements]{ 10, 65, 3000, 2, 49 };
for (auto&& number : normalArray)
{
cout << number << endl;
}
return 0;
}
清单 7-2 中的函数利用基于范围的 for 循环来迭代数组。当您不需要数组的索引值时,这是一个有用的构造。
注意清单 7-2 中循环的基于范围使用了看起来像右值引用的语法。事实并非如此。如果你不确定这段代码是如何工作的,或者不知道左值和右值之间的区别,请阅读第二章。
C 风格的数组在很多情况下都很有用,但是现代 C++ 也提供了另一种版本的数组,可以用于 STL 迭代器和算法。清单 7-3 展示了如何定义 STL array。
清单 7-3 。使用 STL array
#include <array>
#include <cinttypes>
#include <iostream>
int main(int argc, char* argv[])
{
const uint32_t numberOfElements{ 5 };
std::array<int32_t, numberOfElements> stlArray{ 10, 65, 3000, 2, 49 };
for (uint32_t i = 0; i < numberOfElements; ++i)
{
std::cout << stlArray[i] << std::endl;
}
for (auto&& number : stlArray)
{
std::cout << number << std::endl;
}
return 0;
}
清单 7-3 显示了 STL array是通过将存储在array中的类型和它包含的元素数量传递到类型模板中来定义的。一旦定义了array,它就可以和普通的 C 风格数组互换使用。这是因为基于范围的 for 循环可以迭代两种类型的数组,并且 STL 数组定义了一个数组操作符重载,允许使用[]访问元素。
注意与 C 风格的数组相比,使用 STL 数组容器的主要优点是它允许访问 STL 迭代器和算法,这两者都在第八章中有所涉及。
数组将它们的对象存储在连续的内存块中。这意味着每个数组元素的地址在内存中是相邻的。这使得它们在现代处理器上的迭代非常有效。阵列通常会带来出色的高速缓存一致性,因此当处理器从 RAM 读取数据到本地高速缓存时,会导致较少的暂停。对于性能至关重要并且需要固定数量的对象的算法来说,数组是最佳选择。
配方 7-2。存储越来越多的对象
问题
有时候你在编译时不知道需要在数组中存储多少对象。
解决办法
STL 提供了允许动态增长数组的向量模板。
它是如何工作的
vector的工作方式与array非常相似。清单 7-4 显示了一个vector的定义和两种类型的for循环。
清单 7-4 。使用 STL vector
#include <cinttypes>
#include <iostream>
#include <vector>
using namespace std;
int main(int argc, char* argv[])
{
vector<int32_t> stlVector{ 10, 65, 3000, 2, 49 };
for (uint32_t i = 0; i < stlVector.size(); ++i)
{
std::cout << stlVector[i] << std::endl;
}
for (auto&& number : stlVector)
{
std::cout << number << endl;
}
return 0;
}
vector和array的定义之间的主要区别是缺少尺寸。由于vector是可调整大小的,因此限制它可以包含的元素数量没有什么意义。这在main函数中的传统 for 循环中得到了体现。您可以看到,循环结束条件通过比较索引和从size方法返回的值来检查完成情况。在这种情况下,size将返回 5,因为vector包含 5 个元素。
清单 7-5 让您看到vector可以在运行时调整大小,不像array。
清单 7-5 。调整矢量的大小
#include <cinttypes>
#include <iostream>
#include <vector>
using namespace std;
int main(int argc, char* argv[])
{
vector<int32_t> stlVector{ 10, 65, 3000, 2, 49 };
cout << "The size is: " << stlVector.size() << endl;
stlVector.emplace_back( 50 );
cout << "The size is: " << stlVector.size() << endl;
for (auto&& number : stlVector)
{
std::cout << number << endl;
}
return 0;
}
清单 7-5 的结果输出如图图 7-1 所示。
图 7-1 。清单 7-5 中的生成的输出显示了一个不断增长的vector
图 7-1 显示在调用emplace_back后vector从 5 号增长到 6 号。基于循环的范围打印出存储在vector中的所有值。你可以看到emplace_back已经把值加到了vector的末尾。
一个vector调整大小的方式是实现定义的,这意味着它取决于创建你所使用的库的供应商。所有的实现都使用相似的方法。它们通常倾向于在内部为新的array分配内存,包括vector的当前大小以及新值的可变数量的空槽。清单 7-6 包含了使用capacity方法来决定vector在调整大小之前能够存储多少元素的代码。
清单 7-6 。调整大小vector
#include <cinttypes>
#include <iostream>
#include <vector>
using namespace std;
int main(int argc, char* argv[])
{
vector<int32_t> stlVector
{
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16
};
cout << "The size is: " << stlVector.size() << endl;
cout << "The capacity is: " << stlVector.capacity() << endl;
stlVector.emplace_back(17);
cout << "The size is: " << stlVector.size() << endl;
cout << "The capacity is: " << stlVector.capacity() << endl;
for (auto&& number : stlVector)
{
std::cout << number << std::endl;
}
return 0;
}
清单 7-6 中的代码创建了一个包含 16 个元素的向量。图 7-2 显示了添加新元素对vector容量的影响。
图 7-2 。显示使用 Microsoft Visual Studio 2013 STL 时增加的容量的输出
图 7-2 显示给一个vector增加一个值并不会导致一个元素大小的增加。微软已经决定,他们的 STL 实现将把vector的容量增加 50%。向大小为 16 的vector添加新元素会在添加单个新元素时增加 8 个新元素的容量。
也可以在vector中除了结尾以外的地方添加元素。清单 7-7 展示了如何将emplace方法用于此目的。
清单 7-7 。向 a vector中的任意点添加元素
#include <cinttypes>
#include <iostream>
#include <vector>
using namespace std;
int main(int argc, char* argv[])
{
vector<int32_t> stlVector
{
1,
2,
3,
4,
5
};
auto iterator = stlVector.begin() + 2;
stlVector.emplace(iterator, 6);
for (auto&& number : stlVector)
{
std::cout << number << std::endl;
}
return 0;
}
清单 7-7 使用迭代器将值 6 放入向量的第 3 rd 位置。如有必要,此操作将增加 vector 的容量,并将该位置之后的所有元素向右移动一位。图 7-3 显示了该操作的输出。
图 7-3 。清单 7-7 中的输出显示了插入到vector中第 3 个 rd 位置的元素
也可以从向量中移除元素。清单 7-8 显示了使用迭代器删除vector的每个元素到最后一个元素的代码。
清单 7-8 。从向量中移除元素
#include <cinttypes>
#include <iostream>
#include <vector>
using namespace std;
int main(int argc, char* argv[])
{
vector<int32_t> stlVector
{
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16
};
cout << "The size is: " << stlVector.size() << endl;
cout << "The capacity is: " << stlVector.capacity() << endl << endl;
for (auto&& number : stlVector)
{
std::cout << number << ", ";
}
while (stlVector.size() > 0)
{
auto iterator = stlVector.end() - 1;
stlVector.erase(iterator);
}
cout << endl << endl << "The size is: " << stlVector.size() << endl;
cout << "The capacity is: " << stlVector.capacity() << endl << endl;
for (auto&& number : stlVector)
{
std::cout << number << ", ";
}
std::cout << std::endl;
return 0;
}
清单 7-8 中的main函数中的 while 循环逐个擦除vector中的每个元素。这将改变矢量的大小,但不会改变容量。清单 7-9 增加了代码来减少vector的容量。
清单 7-9 。减少一个vector的容量
#include <cinttypes>
#include <iostream>
#include <vector>
using namespace std;
int main(int argc, char* argv[])
{
vector<int32_t> stlVector
{
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16
};
while (stlVector.size() > 0)
{
auto iterator = stlVector.end() - 1;
stlVector.erase(iterator);
if ((stlVector.size() * 2) == stlVector.capacity())
{
stlVector.shrink_to_fit();
}
cout << "The size is: " << stlVector.size() << endl;
cout << "The capacity is: " << stlVector.capacity() << endl << endl;
}
return 0;
}
当while循环删除元素时,它也会检查vector的大小何时达到容量的一半。当这个条件满足时,调用shrink_to_fit方法。图 7-4 显示了shrink_to_fit对vector容量的影响。
图 7-4 。shrink_to_fit 对vector's容量的影响
调整vector的大小,无论是向上还是向下,都要付出性能代价。必须分配新的内存,并且必须将内部数组中的元素从一个转移到另一个。在这种情况下,建议做两件事:
- 计算出运行时可以添加到
vector中的元素的最大数量,并使用reserve方法分配一次所需的必要内存量。 - 确定是否可以完全避免使用
vector而使用array来创建对象池。这可以通过使用诸如最近最少使用算法之类的方案重用数组中的元素来实现。
配方 7-3。存储一组不断变化的元素
问题
您有一组数据,您将不断地从任意位置输入和移除元素。
解决办法
STL 提供了两个容器,它们提供了从容器中间进行有效插入和删除的功能。这些是list和forward_list容器。
它是如何工作的
array和vector容器在连续内存中存储元素。这提供了对集合的快速迭代,因为它们发挥了现代 CPU 架构的优势。在运行时,数组容器不能被添加或删除,元素只能被改变。vector容器可以添加和删除元素,但这需要一个新的内存分配,并将所有元素从旧内存块转移到新内存块。
另一方面,list容器不在连续的内存块中存储元素。相反,列表中的每个元素都存储在一个独立的节点中,该节点包含一个指向列表中下一个和最后一个元素的指针。这允许在list容器中双向遍历。一个forward_list只存储指向下一个元素的指针,而不是最后一个,因此只能从前到后遍历。在更新引用列表结构中下一个和最后一个节点的指针时,在列表中添加和删除元素变成了一项琐碎的工作。
这种不连续的存储导致遍历列表时性能下降。CPU 缓存不能总是预加载列表中的下一个元素,因此对于定期遍历的数据集,应该避免使用这些结构。它们的优势来自于节点的快速插入和删除。清单 7-10 显示了一个正在使用的list容器。
清单 7-10 。使用list
#include <cinttypes>
#include <iostream>
#include <list>
using namespace std;
int main(int argv, char* argc[])
{
list<int32_t> myList{ 1, 2, 3, 4, 5 };
myList.emplace_front(6);
myList.emplace_back(7);
auto forwardIter = myList.begin();
++forwardIter;
++forwardIter;
myList.emplace(forwardIter, 9);
auto reverseIter = myList.end();
--reverseIter;
--reverseIter;
--reverseIter;
myList.emplace(reverseIter, 8);
for (auto&& number : myList)
{
cout << number << endl;
}
return 0;
}
清单 7-10 的 main 函数中使用的列表容器允许从 begin 或 end 返回的迭代器向前和向后遍历。图 7-5 包含了遍历list生成的输出,在这里你可以看到添加元素的任意顺序。
图 7-5 。遍历清单 7-10 中容器时的输出
清单 7-11 显示了与forward_ list 类似的代码
清单 7-11 。使用forward_list
#include <cinttypes>
#include <forward_list>
#include <iostream>
using namespace std;
int main(int argv, char* argc[])
{
forward_list<int32_t> myList{ 1, 2, 3, 4, 5 };
myList.emplace_front(6);
auto forwardIter = myList.begin();
++forwardIter;
++forwardIter;
myList.emplace_after(forwardIter, 9);
for (auto&& number : myList)
{
cout << number << endl;
}
return 0;
}
与清单 7-10 中的相比,清单 7-11 中的有一些不同。一个forward_list不包含方法emplace或者emplace_back。它确实包含了emplace_front和emplace_after,允许你在forward_list的开头或forward_list的特定位置之后添加元素。
配方 7-4。将排序后的对象存储在支持快速查找的容器中
问题
您有一个很大的对象集合,您希望对其进行排序,并且经常需要查找特定的信息。
解决办法
STL 提供了set和map容器,它们可以自动对它们的对象进行排序,并提供非常快速的搜索特性。
它是如何工作的
set和map容器是关联容器。这意味着它们将其数据元素与一个键相关联。在set的情况下,键是对象或值本身,而对于map来说,键是与对象或值一起提供的值。
这些容器是使用二分搜索法树实现的,这也是它们提供自动排序和快速搜索特性的原因。二分搜索法树通过比较对象的键来运行。如果一个对象的键小于当前节点的键,那么它被添加到左边,如果它大于当前节点的键,那么它被添加到右边,反之亦然。
注意事实上你可以为两个容器提供一个函数,允许你为自己指定排序顺序。
清单 7-12 展示了一个set的创建,它将元素从最小到最大排序。
清单 7-12 。使用set
#include <cinttypes>
#include <iostream>
#include <set>
#include <string>
using namespace std;
class SetObject
{
private:
string m_Name;
int32_t m_Key{};
public:
SetObject(int32_t key, const string& name)
: m_Name{ name }
, m_Key{ key }
{
}
SetObject(int32_t key)
: SetObject(key, "")
{
}
const string& GetName() const
{
return m_Name;
}
int32_t GetKey() const
{
return m_Key;
}
bool operator<(const SetObject& other) const
{
return m_Key < other.m_Key;
}
bool operator>(const SetObject& other) const
{
return m_Key > other.m_Key;
}
};
int main(int argv, char* argc[])
{
set<SetObject> mySet
{
{ 6, "Six" },
{ 3, "Three" },
{ 4, "Four" },
{ 1, "One" },
{ 2, "Two" }
};
for (auto&& number : mySet)
{
cout << number.GetName() << endl;
}
auto iter = mySet.find(3);
if (iter != mySet.end())
{
cout << "Found: " << iter->GetName() << endl;
}
return 0;
}
清单 7-12 的main函数中定义的set用五个SetObject实例初始化。每个实例都存储一个整数键和该键的一个string表示。默认情况下,一个set被初始化为从低到高排列它包含的元素。你可以在图 7-6 中看到这一点。
图 7-6 。由清单 7-12 中的代码生成的输出
类对象的排序是使用运算符重载实现的。SetObject类重载了<和>操作符,这使得该类可以和这些操作符一起使用。当添加一个新元素时,set将调用一个比较函数来决定元素在set中出现的顺序。default case要求在元素上使用<操作符。正如你所看到的,SetObject 类比较了操作符中的m_Key变量来决定它们应该被存储的顺序。
清单 7-13 展示了如何改变默认的set来从最高到最低排列元素。
清单 7-13 。从最高到最低对 a set中的元素进行排序
#include <cinttypes>
#include <functional>
#include <iostream>
#include <set>
#include <string>
using namespace std;
class SetObject
{
private:
string m_Name;
int32_t m_Key{};
public:
SetObject(int32_t key, const string& name)
: m_Name{ name }
, m_Key{ key }
{
}
SetObject(int32_t key)
: SetObject(key, "")
{
}
const string& GetName() const
{
return m_Name;
}
int32_t GetKey() const
{
return m_Key;
}
bool operator<(const SetObject& other) const
{
return m_Key < other.m_Key;
}
bool operator>(const SetObject& other) const
{
return m_Key > other.m_Key;
}
};
using namespace std;
int main(int argv, char* argc[])
{
set<SetObject, greater<SetObject>> mySet
{
{ 6, "Six" },
{ 3, "Three" },
{ 4, "Four" },
{ 1, "One" },
{ 2, "Two" }
};
for (auto&& number : mySet)
{
cout << number.GetName() << endl;
}
auto iter = mySet.find(3);
if (iter != mySet.end())
{
cout << "Found: " << iter->GetName() << endl;
}
return 0;
}
清单 7-12 中的和清单 7-13 中的的唯一区别是在set中增加了第二个模板参数。清单 7-13 从功能标题提供greater模板。该模板将从一个函数中创建一个方法,该函数可以在两个SetObject实例上调用>操作符。您可以想象默认的set有一个隐含的less参数:
set<SetObject, less<SetObject>>
图 7-7 显示了一个set的结果输出,元素从最高到最低排序。
图 7-7 。使用greater将set从最高到最低排序
清单 7-14 展示了如何在初始化后给一个set添加元素。
清单 7-14 。向set添加元素
#include <cinttypes>
#include <functional>
#include <iostream>
#include <set>
#include <string>
using namespace std;
class SetObject
{
private:
string m_Name;
int32_t m_Key{};
public:
SetObject(int32_t key, const string& name)
: m_Name{ name }
, m_Key{ key }
{
}
SetObject(int32_t key)
: SetObject(key, "")
{
}
const string& GetName() const
{
return m_Name;
}
int32_t GetKey() const
{
return m_Key;
}
bool operator<(const SetObject& other) const
{
return m_Key < other.m_Key;
}
bool operator>(const SetObject& other) const
{
return m_Key > other.m_Key;
}
};
int main(int argv, char* argc[])
{
set<SetObject, greater<SetObject>> mySet
{
{ 6, "Six" },
{ 3, "Three" },
{ 4, "Four" },
{ 1, "One" },
{ 2, "Two" }
};
for (auto&& number : mySet)
{
cout << number.GetName() << endl;
}
cout << endl;
mySet.emplace(SetObject( 5, "Five" ));
for (auto&& number : mySet)
{
cout << number.GetName() << endl;
}
cout << endl;
auto iter = mySet.find(3);
if (iter != mySet.end())
{
cout << "Found: " << iter->GetName() << endl;
}
return 0;
}
emplace方法可以用来给set添加新元素,如清单 7-14 所示。图 7-8 显示新元素被插入set中给定greater顺序的正确位置。
图 7-8 。显示一个新元素已经被添加到set的正确位置
除了键的存储独立于对象值之外,map容器与set容器非常相似。清单 7-15 显示了创建一个map容器的代码。
清单 7-15 。创建一个map
#include <cinttypes>
#include <functional>
#include <iostream>
#include <map>
#include <string>
using namespace std;
class MapObject
{
private:
string m_Name;
public:
MapObject(const string& name)
: m_Name{ name }
{
}
const string& GetName() const
{
return m_Name;
}
};
int main(int argv, char* argc[])
{
map<int32_t, MapObject, greater<int32_t>> myMap
{
pair<int32_t, MapObject>(6, MapObject("Six")),
pair<int32_t, MapObject>(3, MapObject("Three")),
pair<int32_t, MapObject>(4, MapObject("Four")),
pair<int32_t, MapObject>(1, MapObject("One")),
pair<int32_t, MapObject>(2, MapObject("Two"))
};
for (auto&& number : myMap)
{
cout << number.second.GetName() << endl;
}
cout << endl;
myMap.emplace(pair<int32_t, MapObject>(5, MapObject("Five")));
for (auto&& number : myMap)
{
cout << number.second.GetName() << endl;
}
cout << endl;
auto iter = myMap.find(3);
if (iter != myMap.end())
{
cout << "Found: " << iter->second.GetName() << endl;
}
return 0;
}
清单 7-15 使用map代替set获得了与清单 7-14 中的代码完全相同的结果。MapObject类不包含键,也不包含任何重载操作符来比较使用该类实例化的对象。这是因为map的键是独立于数据存储的。使用pair模板将元素添加到map中,每个pair将一个键值关联到一个对象。
一个map的代码比一个set的代码更冗长,但是包含的对象可以不那么复杂。当键与类中的其他数据不相关时,map比set更适合。具有自然顺序并且已经具有可比性的对象是存储在set中的良好候选对象。
一个map的迭代器也是一个pair。它包含的MapObject可以使用iterator上的second字段检索,同时首先存储键值。在map或set上迭代是一个缓慢的操作,因为元素不包含在连续的存储器中。关联容器的好处主要是它们的快速查找,而排序是次要的好处,出于性能原因应该尽量少用。
配方 7-5。将未排序的元素存储在容器中,以便快速查找
问题
您有一组不需要排序但将用于频繁查找和数据检索的数据。
解决办法
STL 为此提供了unordered_set和unordered_map容器。
它是如何工作的
unordered_set和unordered_map容器被实现为哈希映射。哈希映射提供了对象的固定时间插入、移除和搜索。恒定时间意味着无论容器中有多少元素,操作都将花费相同的时间。
由于unordered_set和unordered_map容器是散列映射,它们依赖于提供的散列函数,该函数可以将数据转换成数值。清单 7-16 展示了如何创建一个集合来存储可以被散列和比较的用户定义的类。
清单 7-16 。使用unordered_ set
#include <cinttypes>
#include <functional>
#include <iostream>
#include <string>
#include <unordered_set>
using namespace std;
class SetObject;
namespace std
{
template <>
class hash<SetObject>
{
public:
template <typename... Args>
size_t operator()(Args&&... setObject) const
{
return hash<string>()((forward<Args...>(setObject...)).GetName());
}
};
}
class SetObject
{
private:
string m_Name;
size_t m_Hash{};
public:
SetObject(const string& name)
: m_Name{ name }
, m_Hash{ hash<SetObject>()(*this) }
{
}
const string& GetName() const
{
return m_Name;
}
const size_t& GetHash() const
{
return m_Hash;
}
bool operator==(const SetObject& other) const
{
return m_Hash == other.m_Hash;
}
};
int main(int argv, char* argc[])
{
unordered_set<SetObject> mySet;
mySet.emplace("Five");
mySet.emplace("Three");
mySet.emplace("Four");
mySet.emplace("One");
mySet.emplace("Two");
cout << showbase << hex;
for (auto&& number : mySet)
{
cout << number.GetName() << " - " << number.GetHash() << endl;
}
auto iter = mySet.find({ "Three" });
if (iter != mySet.end())
{
cout << "Found: " << iter->GetName() << " with hash: " << iter->GetHash() << endl;
}
return 0;
}
使用一个unordered_set来存储类对象需要一些难以理解的代码。首先,我们对hash模板进行了部分专门化。这允许我们创建一个能够为SetObject类创建哈希值的函数。这是通过传递一个SetObject实例并为string调用 STL hash函数来实现的。使用通用引用和转发功能将SetObject实例传递给()操作符,以实现完美转发。
注意模板包含在第九章的中,通用引用与左值、右值和完全转发包含在第二章的中。
SetObject类需要重载的==操作符才能在unordered_set中正常运行。如果缺少这一项,代码将无法编译。成员变量m_Hash是不需要的,我只是把它包含进来,向您展示hash创建的值,以及您如何为自己调用散列函数。如果m_Hash变量不存在,您可以比较m_Name字符串是否相等。图 7-9 显示了该代码生成的结果输出。
图 7-9 。清单 7-16 生成的输出
只要你使用 STL 已经可以散列的键的类型,创建你自己的散列函数并不困难。清单 7-17 显示了一个使用整数作为键的unordered_map。
清单 7-17 。使用unordered_map
#include <cinttypes>
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
class MapObject
{
private:
string m_Name;
public:
MapObject(const string& name)
: m_Name{ name }
{
}
const string& GetName() const
{
return m_Name;
}
};
int main(int argv, char* argc[])
{
unordered_map<int32_t, MapObject> myMap;
myMap.emplace(pair<int32_t, MapObject>(5, MapObject("Five")));
myMap.emplace(pair<int32_t, MapObject>(3, MapObject("Three")));
myMap.emplace(pair<int32_t, MapObject>(4, MapObject("Four")));
myMap.emplace(pair<int32_t, MapObject>(1, MapObject("One")));
myMap.emplace(pair<int32_t, MapObject>(2, MapObject("Two")));
cout << showbase << hex;
for (auto&& number : myMap)
{
cout << number.second.GetName() << endl;
}
auto iter = myMap.find(3);
if (iter != myMap.end())
{
cout << "Found: " << iter->second.GetName() << endl;
}
return 0;
}
清单 7-17 显示了unordered_map容器存储键值对作为它的元素。pair的第一个字段存储键,而pair的第二个字段存储值,在本例中是MapObject的一个实例。
八、STL 算法
STL 提供了一套算法,可以和它提供的容器一起使用。这些算法都使用迭代器。迭代器是一种抽象机制,允许遍历许多不同的 STL 集合。本章包括迭代器和一些不同的算法以及它们的用途。
配方 8-1。使用迭代器定义容器中的序列
问题
你有一个 STL 容器,你想在这个容器中标记一个序列,这个序列在特定的点开始和结束。
解决办法
STL 提供了适用于所有容器的迭代器,可以用来表示容器中序列的开始和结束。该序列可以包括容器中的每个节点,也可以包括容器中节点的子集。
它是如何工作的
迭代器的工作方式与指针相似。它们的语法非常相似。你可以在清单 8-1 中看到迭代器的使用。
清单 8-1 。使用带有vector和的iterator
#include <cinttypes>
#include <iostream>
#include <vector>
using namespace std;
int main(int arcg, char* argv[])
{
using IntVector = vector<int32_t>;
using IntVectorIterator = IntVector::iterator;
IntVector myVector{ 0, 1, 2, 3, 4 };
for (IntVectorIterator iter = myVector.begin(); iter != myVector.end(); ++iter)
{
cout << "The value is: " << *iter << endl;
}
return 0;
}
在清单 8-1 中的main函数中创建了一个int类型的vector。一个类型别名被用来制作一个新类型的IntVector来表示这种类型的集合。第二个别名用于表示这个集合使用的iterator的类型。可以看到iterator类型是通过初始的vector类型访问的。这是必要的,因为iterator也必须操作与矢量本身操作相同类型的对象。在 vector 类型中包含迭代器类型允许您指定要操作的类型,在本例中是int32 _t,同时用于两者。
iterator类型用于在for循环中获取对myVector集合的开始和结束的引用。向量返回迭代器的begin和end方法。如果表示集合开始的iterator等于表示集合结束的迭代器,则称该集合为空。这是iterators与指针共有的第一个属性,它们是可比较的。
for 循环中的iter变量被初始化为由vector::begin方法返回的值。执行for循环,直到 iter 变量等于由vector::end方法 返回的iterator。这说明集合中的值序列可以用两个iterators来表示,一个在序列的开头,一个在序列的结尾。一个iterator提供了一个增量操作符,允许iterator移动到序列中的下一个元素。这就是如何将 for 循环中的iter变量初始化为由begin返回的iterator,并针对end进行测试,直到序列遍历完成。这也恰好是iterators与指针共享的另一个属性,递增或递减会将迭代器移动到序列中的下一个或最后一个元素。
注意不是所有的迭代器都支持递增和递减操作。在下面的段落中,您将会看到这种情况。
用iterator覆盖的最后一个重要操作是解引用操作符。你可能在标准指针操作中熟悉这些,这是迭代器与指针共享的最后一个属性。从清单 8-1 中可以看到,解引用操作符用于检索由iterator表示的值。在本例中,解引用用于从集合中检索每个迭代器,并将其发送到控制台。图 8-1 表明情况就是如此。
图 8-1 。当myVector集合被遍历时清单 8-1 的输出
试图在不使用解引用操作符的情况下打印出iterator会导致编译错误,因为cout::<<操作符不支持iterator类型。
清单 8-1 中的代码使用了标准的正向迭代器。这种迭代器为容器中的每个元素提供非常量访问。清单 8-2 显示了这个属性的含义。
清单 8-2 。使用非常数迭代器
#include <cinttypes>
#include <iostream>
#include <vector>
using namespace std;
int main(int arcg, char* argv[])
{
using IntVector = vector<int32_t>;
using IntVectorIterator = IntVector::iterator;
IntVector myVector(5, 0);
int32_t value{ 0 };
for (IntVectorIterator iter = myVector.begin(); iter != myVector.end(); ++iter)
{
*iter = value++;
}
for (IntVectorIterator iter = myVector.begin(); iter != myVector.end(); ++iter)
{
cout << "The value is: " << *iter << endl;
}
return 0;
}
如果你将清单 8-2 与清单 8-1 进行比较,你会发现myVector集合的初始化是以不同的方式处理的。清单 8-2 初始化vector以包含值 0 的 5 个副本。然后一个for循环遍历vector,并使用iterator解引用操作符将递增后的值变量分配给myVector中的每个位置。由于iterator类型的非常数性质,这是可能的。如果你想使用一个iterator,你知道它不应该有写权限,那么你可以使用一个const_iterator,如清单 8-3 所示。
清单 8-3 。使用const_iterator
#include <cinttypes>
#include <iostream>
#include <vector>
using namespace std;
int main(int arcg, char* argv[])
{
using IntVector = vector<int32_t>;
using IntVectorIterator = IntVector::iterator;
using ConstIntVectorIterator = IntVector::const_iterator;
IntVector myVector(5, 0);
int32_t value{ 0 };
for (IntVectorIterator iter = myVector.begin(); iter != myVector.end(); ++iter)
{
*iter = value++;
}
for (ConstIntVectorIterator iter = myVector.cbegin(); iter != myVector.cend(); ++iter)
{
cout << "The value is: " << *iter << endl;
}
return 0;
}
清单 8-3 在第二个for循环中使用vector::cbegin和vector::cend方法来获得对myVector元素的访问,但不提供写访问。任何试图给const_iterator赋值的行为都会导致编译错误。C++ 集合提供的iterator和const_iterator类型都是正向迭代器的例子。这意味着它们都按照您可能会想到的顺序从头到尾遍历集合。STL 集合也支持reverse_iterator和const_reverse_iterator类型。这些允许你向后遍历你的序列。清单 8-4 显示了使用reverse_itertor从最高到最低初始化myVector集合。
清单 8-4 。使用reverse_iterator 初始化myVector
#include <cinttypes>
#include <iostream>
#include <vector>
using namespace std;
int main(int arcg, char* argv[])
{
using IntVector = vector<int32_t>;
using IntVectorIterator = IntVector::iterator;
using ConstIntVectorIterator = IntVector::const_iterator;
using ReverseIntVectorIterator = IntVector::reverse_iterator;
using ConstReverseIntVectorIterator = IntVector::const_reverse_iterator;
IntVector myVector(5, 0);
int32_t value { 0 };
for (ReverseIntVectorIterator iter = myVector.rbegin(); iter != myVector.rend(); ++iter)
{
*iter = value++;
}
for (ConstIntVectorIterator iter = myVector.cbegin(); iter != myVector.cend(); ++iter)
{
cout << "The value is: " << *iter << endl;
}
return 0;
}
清单 8-4 显示reverse_iterator应该与vector提供的rbegin 和rend方法一起使用。递增一个reverse_iterator会导致它在集合中向后移动。图 8-2 显示myVector集合已经以相反的顺序存储了值。
图 8-2 。从myVector开始按相反顺序取值
图 8-2 中的输出也可以使用清单 8-5 中的代码来实现,该代码使用一个const_reverse_iterator来打印数值。
清单 8-5 。使用const_reverse_iterator反向打印myVector
#include <cinttypes>
#include <iostream>
#include <vector>
using namespace std;
int main(int arcg, char* argv[])
{
using IntVector = vector<int32_t>;
using IntVectorIterator = IntVector::iterator;
using ConstIntVectorIterator = IntVector::const_iterator;
using ReverseIntVectorIterator = IntVector::reverse_iterator;
using ConstReverseIntVectorIterator = IntVector::const_reverse_iterator;
IntVector myVector(5, 0);
int32_t value{ 0 };
for (IntVectorIterator iter = myVector.begin(); iter != myVector.end(); ++iter)
{
*iter = value++;
}
for (ConstReverseIntVectorIterator iter = myVector.crbegin();
iter != myVector.crend();
++iter)
{
cout << "The value is: " << *iter << endl;
}
return 0;
}
清单 8-5 使用const_reverse_iterator以及crbegin和crend方法从最后到第一步遍历集合,并以相反的顺序打印值。
迭代器将在本章的剩余部分扮演重要的角色,因为它们被用作 STL 提供的算法的输入。
食谱 8-2。对容器中的每个元素调用函数
问题
你有一个容器,想要一个简单的方法来调用每个元素的函数。
解决办法
STL 提供了for_each函数 ,它采用一个开始迭代器、一个结束迭代器和一个函数来调用两者之间的每个元素。
它是如何工作的
for_each函数可以传递两个迭代器。这些迭代器定义了容器中应该被遍历的起点和终点。3 rd 参数是一个应该为每个元素调用的函数。元素本身被传递到函数中。清单 8-6 显示了for_each函数的用法。
清单 8-6 。for_each算法
#include <algorithm>
#include <cinttypes>
#include <iostream>
#include <vector>
using namespace std;
int main(int argc, char* argv[])
{
vector<int32_t> myVector
{
1,
2,
3,
4,
5
};
for_each(myVector.begin(), myVector.end(),
[](int32_t value)
{
cout << value << endl;
});
return 0;
}
清单 8-6 中的代码创建了一个包含 5 个元素的vector,数字 1 到 5。向for_each函数传递由begin和end方法返回的迭代器,以定义应该传递给参数 3 中提供的函数的值的范围。参数 3 是未命名的函数或 lambda。
lambda 的方括号表示捕获列表。这个列表用于允许 lambda 访问存在于创建它的函数中的变量。在这种情况下,我们没有从函数中捕获任何变量。括号表示参数列表。清单 8-1 中的 lambda 将一个int32_t作为参数,因为它是存储在vector中的类型。花括号表示函数体,就像它们表示标准函数体一样。执行这段代码会产生如图图 8-3 所示的输出。
图 8-3 。清单 8-6 中的for_each和生成的输出
生成此输出是因为for_each算法将来自myVector中每个位置的整数传递给所提供的函数,在本例中是一个 lambda。
食谱 8-3。查找容器中的最大值和最小值
问题
偶尔你会想找出容器中的最大值或最小值。
解决办法
STL 提供了允许你在 STL 容器中找到最大和最小值的算法。这些是min_element和max_element功能。
它是如何工作的
寻找容器中的最小值
min_element功能通过在给定序列的开头和结尾放置一个iterator来运行。它遍历该序列,并找到该序列中包含的最小值。清单 8-7 展示了这个算法的使用。
清单 8-7 。使用最小元素算法
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
int main(int argc, char* argv[])
{
vector<int> myVector{ 4, 10, 6, 9, 1 };
auto minimum = min_element(myVector.begin(), myVector.end());
cout << "Minimum value: " << *minimum << std::endl;
return 0;
}
在这种情况下,您可以看到一个vector被用来存储integer元素。向min_element函数传递的iterator表示vector包含的序列的开始和结束。该算法向包含最小值的元素返回一个iterator。我在这里使用auto是为了避免写出整个迭代器的类型(应该是vector<int>::iterator)。很明显,当查看输出值的行时,返回的是迭代器。从迭代器中检索integer值需要指针解引用操作符。您可以在图 8-4 中看到代码生成的输出。
图 8-4 。来自清单 8-7 的输出显示了检索到的最小值
清单 8-7 中的容器显示了一个容器存储整数值的简单例子。这种情况是微不足道的,因为两个int变量已经可以使用<操作符进行比较。通过在你的类中提供一个重载的<操作符,你可以在你自己的类中使用min_element。你可以在清单 8-8 中看到这样的例子。
清单 8-8 。将min_element与包含<操作符的class结合使用
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
class MyClass
{
private:
int m_Value;
public:
MyClass(const int value)
: m_Value{ value }
{
}
int GetValue() const
{
return m_Value;
}
bool operator <(const MyClass& other) const
{
return m_Value < other.m_Value;
}
};
int main(int argc, char* argv[])
{
vector<MyClass> myVector{ 4, 10, 6, 9, 1 };
auto minimum = min_element(myVector.begin(), myVector.end());
if (minimum != myVector.end())
{
cout << "Minimum value: " << (*minimum).GetValue() << std::endl;
}
return 0;
}
清单 8-7 和 10-8 的不同之处在于使用了MyClass对象的vector而不是integer值的vector。然而,对min_element的呼叫仍然完全一样。在这种情况下,min_element调用将遍历序列,并使用添加到MyClass class的<操作符来查找最小值。在这种情况下,防止碰到序列的结尾也是必要的,因为 end 元素不会指向有效的对象,因此对GetValue的解引用和调用可能会崩溃。
比较非基本类型的另一个选择是直接向min_element函数提供一个比较函数。该选项如清单 8-9 中的所示。
清单 8-9 。使用带有 min_element 的独立函数
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
class MyClass
{
private:
int m_Value;
public:
MyClass(const int value)
: m_Value{ value }
{
}
int GetValue() const
{
return m_Value;
}
};
bool CompareMyClasses(const MyClass& left, const MyClass& right)
{
return left.GetValue() < right.GetValue();
}
int main(int argc, char* argv[])
{
vector<MyClass> myVector{ 4, 10, 6, 9, 1 };
auto minimum = min_element(myVector.begin(), myVector.end(), CompareMyClasses);
if (minimum != myVector.end())
{
cout << "Minimum value: " << (*minimum).GetValue() << std::endl;
}
return 0;
}
在清单 8-9 中,我们为min_element函数提供了一个指向比较函数的指针。该函数用于比较从MyClass GetValue方法返回的值。比较函数 是以一种非常特殊的方式构造的,它有两个参数,都是对MyClass对象的常量引用。如果第一个参数被评估为小于第二个参数,该函数应该返回true。选择名称left和right是为了帮助形象化<操作员的通常外观。对min_element的调用被修改为包含第三个参数,即指向CompareMyClasses函数的指针。清单 10-8 和清单 10-9 中显示的代码产生的输出与图 8-4 中显示的输出相同。
寻找容器中的最大值
min_element函数可用于查找序列中的最小值,而max_element函数可用于查找最大值。该函数的使用方式与min_element函数完全相同,如清单 8-10 中的所示。
清单 8-10 。使用max_element
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
class MyClass
{
private:
int m_Value;
public:
MyClass(const int value)
: m_Value{ value }
{
}
int GetValue() const
{
return m_Value;
}
bool operator <(const MyClass& other) const
{
return m_Value < other.m_Value;
}
};
bool CompareMyClasses(const MyClass& left, const MyClass& right)
{
return left.GetValue() < right.GetValue();
}
int main(int argc, char* argv[])
{
vector<int> myIntVector{ 4, 10, 6, 9, 1 };
auto intMinimum = max_element(myIntVector.begin(), myIntVector.end());
if (intMinimum != myIntVector.end())
{
cout << "Maxmimum value: " << *intMinimum << std::endl << std::endl;
}
vector<MyClass> myMyClassVector{ 4, 10, 6, 9, 1 };
auto overrideOperatorMinimum = max_element(myMyClassVector.begin(),
myMyClassVector.end());
if (overrideOperatorMinimum != myMyClassVector.end())
{
cout << "Maximum value: " << (*overrideOperatorMinimum).GetValue() <<
std::endl << std::endl;
}
auto functionComparisonMinimum = max_element(myMyClassVector.begin(),
myMyClassVector.end(),
CompareMyClasses);
if (functionComparisonMinimum != myMyClassVector.end())
{
cout << "Maximum value: " << (*functionComparisonMinimum).GetValue() <<
std::endl << std::endl;
}
return 0;
}
清单 8-10 显示了max_element函数可以用来代替min_element函数。认识到max_element函数仍然使用<操作符是很重要的。看起来,max_element可能会使用>操作符,但是使用<操作符并响应false而不是true的结果来表明一个值大于另一个值也是有效的。
食谱 8-4。对序列中某个值的实例计数
问题
有时您可能希望知道一个序列中有多少个特定值的实例。
解决办法
STL 提供了一种叫做count的算法。该算法可以搜索一系列值,并返回找到所提供值的次数。
它是如何工作的
count函数有 3 个参数,一个开始参数iterator,一个结束参数iterator和一个要查找的值。给定这三条信息,算法将返回该值出现的次数。清单 8-11 展示了这个算法的使用。
清单 8-11 。使用计数算法
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
int main(int argc, char* argv[])
{
vector<int> myVector{ 3, 2, 3, 7, 3, 8, 9, 3 };
auto number = count(myVector.begin(), myVector.end(), 3);
cout << "The number of 3s in myVector is: " << number << endl;
return 0;
}
清单 8-11 中的代码将让count函数遍历序列并返回遇到值 3 的次数。在图 8-5 中可以看到这个操作的结果是 4。
图 8-5 。由生成的结果输出见清单 8-11
C++ 还提供了一些特殊的谓词函数,可以与字符数据和count_if函数结合使用。这些函数可以用来计算大写或小写字母的数量,以及字符是字母数字、空格还是标点符号。你可以在清单 8-12 中看到所有这些。
清单 8-12 。使用带有count的字符谓词
#include <algorithm>
#include <cctype>
#include <iostream>
#include <string>
using namespace std;
int main(int argc, char* argv[])
{
string myString{ "Bruce Sutherland!" };
auto numberOfCapitals = count_if(
myString.begin(),
myString.end(),
[](auto&& character)
{
return static_cast<bool>(isupper(character));
});
cout << "The number of capitals: " << numberOfCapitals << endl;
auto numberOfLowerCase = count_if(
myString.begin(),
myString.end(),
[](auto&& character)
{
return static_cast<bool>(islower(character));
});
cout << "The number of lower case letters: " << numberOfLowerCase << endl;
auto numberOfAlphaNumerics = count_if(
myString.begin(),
myString.end(),
[](auto&& character)
{
return static_cast<bool>(isalpha(character));
});
cout << "The number of alpha numeric characters: " << numberOfAlphaNumerics << endl;
auto numberOfPunctuationMarks = count_if(
myString.begin(),
myString.end(),
[](auto&& character)
{
return static_cast<bool>(ispunct(character));
});
cout << "The number of punctuation marks: " << numberOfPunctuationMarks << endl;
auto numberOfWhiteSpaceCharacters = count_if(
myString.begin(),
myString.end(),
[](auto&& character)
{
return static_cast<bool>(isspace(character));
});
cout << "The number of white space characters: " << numberOfWhiteSpaceCharacters << endl;
return 0;
}
在清单 8-12 中,可以看到谓词使用 lambda 传递给了count_if函数。lambda 对于count_if模板来说是必要的,它可以满足被提供的函数是一个返回bool的谓词。count_if函数将返回所提供的函数返回true的次数。您可以在图 8-6 的中看到不同调用count_if的结果。
图 8-6 。调用清单 8-6 中代码的结果
清单 8-6 中提供的字符串相当简单,因此很容易确认字符谓词是否按预期工作。您可以对照图 8-6 的结果来确认这一点。
配方 8-5。在序列中查找值
问题
您可能希望找到序列中匹配特定值的第一个元素的迭代器。
解决办法
STL 提供了 find 函数来检索序列中匹配给定值的第一个元素的迭代器。
它是如何工作的
find 函数可用于检索与您提供的值匹配的第一个值的迭代器。你可以用它从头到尾地浏览一个序列。清单 8-13 展示了如何使用 while 循环来移动整个序列。
清单 8-13 。使用find
#include <algorithm>
#include <iostream>
#include <string>
using namespace std;
int main(int argc, char* argv[])
{
string myString{ "Bruce Sutherland" };
auto found = find(myString.begin(), myString.end(), 'e');
while (found != myString.end())
{
cout << "Found: " << *found << endl;
found = find(found+1, myString.end(), 'e');
}
return 0;
}
清单 8-13 中的代码将打印出字母 e 两次,因为变量myString中的string中有两个字母。对find的第一次调用返回一个迭代器,指向字符串中字符 e 的第一个实例。然后,while 循环中的调用从紧接该迭代器之后的位置开始。这使得 find 函数逐步搜索所提供的数据集,并最终到达末尾。一旦发生这种情况,while 循环将终止。清单 8-13 中的代码生成如图 8-7 所示的输出。
图 8-7 。执行清单 8-13 中的代码生成的输出
配方 8-6。排序序列中的元素
问题
有时,容器中的数据变得无序,您希望对这些数据进行重新排序。
解决办法
STL 提供了排序算法来对序列中的数据进行重新排序。
它是如何工作的
sort 函数将一个迭代器放在序列的开头,将一个迭代器放在序列的结尾。它会自动将迭代器之间的值按数字升序排序。你可以在清单 8-14 中看到实现这一点的代码。
清单 8-14 。使用sort算法
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
int main(int argc, char* argv[])
{
vector<int> myVector{ 10, 6, 4, 7, 8, 3, 9 };
sort(myVector.begin(), myVector.end());
for (auto&& element : myVector)
{
cout << element << ", ";
}
cout << endl;
return 0;
}
清单 8-14 中的代码将把myVector中的值按升序重新排序。图 8-8 显示了这段代码产生的输出。
图 8-8 。按升序排序的 myVector 元素
如果您希望按照自定义的顺序对数据进行排序,比如降序,那么您必须为sort算法提供一个谓词函数。清单 8-15 展示了一个谓词对一个数字vector进行降序排序的用法。
清单 8-15 。使用带sort的谓词
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
bool IsGreater(int left, int right)
{
return left > right;
}
int main(int argc, char* argv[])
{
vector<int> myVector{ 10, 6, 4, 7, 8, 3, 9 };
sort(myVector.begin(), myVector.end(), IsGreater);
for (auto&& element : myVector)
{
cout << element << ", ";
}
return 0;
}
清单 8-15 中的 myVector 中的数据与清单 8-14 中存储的数据相同。这两个清单的区别是在清单 8-15 中使用了IsGreater函数。这被传递给sort函数,用于比较myVector中的值。标准排序函数将数值从最低到最高排序,如图 8-9 中的所示。图 8-10 显示清单 8-15 中的代码将把数字从最高到最低排序。
图 8-9 。清单 8-15 生成的输出,数字从最高到最低排序